backup.rs (34505B)
1 use radroots_sql_core::{SqlExecutor, error::SqlError, utils}; 2 use serde::{Deserialize, Serialize}; 3 use serde_json::{Map, Value}; 4 use std::collections::{BTreeMap, HashMap}; 5 6 pub const DATABASE_BACKUP_VERSION: &str = "1.0.0"; 7 /// Replica schema compatibility version recorded in backups and export manifests. 8 pub const REPLICA_STORE_SCHEMA_VERSION: &str = "1.0.0"; 9 const LEGACY_COMPATIBLE_REPLICA_STORE_SCHEMA_VERSIONS: [&str; 1] = ["0.1.0-alpha.2"]; 10 11 #[derive(Debug, Clone, Serialize, Deserialize)] 12 pub struct SchemaEntry { 13 pub object_type: String, 14 pub name: String, 15 #[serde(skip_serializing_if = "Option::is_none")] 16 pub table_name: Option<String>, 17 #[serde(skip_serializing_if = "Option::is_none")] 18 pub sql: Option<String>, 19 } 20 21 #[derive(Debug, Clone, Serialize, Deserialize)] 22 pub struct TableData { 23 pub name: String, 24 pub rows: Vec<Map<String, Value>>, 25 } 26 27 #[derive(Debug, Clone, Serialize, Deserialize)] 28 pub struct MigrationBackup { 29 pub name: String, 30 pub up_sql: String, 31 pub down_sql: String, 32 } 33 34 #[derive(Debug, Clone, Serialize, Deserialize)] 35 pub struct DatabaseBackup { 36 pub format_version: String, 37 /// Backup schema compatibility version; the field name is retained on wire. 38 pub replica_store_version: String, 39 pub schema: Vec<SchemaEntry>, 40 pub migrations: Vec<MigrationBackup>, 41 pub data: Vec<TableData>, 42 } 43 44 pub fn export_database_backup(executor: &dyn SqlExecutor) -> Result<DatabaseBackup, SqlError> { 45 let schema = load_schema(executor)?; 46 let data = read_tables_for_backup(executor, &schema)?; 47 let migrations = export_migrations(); 48 Ok(DatabaseBackup { 49 format_version: DATABASE_BACKUP_VERSION.to_string(), 50 replica_store_version: REPLICA_STORE_SCHEMA_VERSION.to_string(), 51 schema, 52 migrations, 53 data, 54 }) 55 } 56 57 pub fn export_database_backup_json(executor: &dyn SqlExecutor) -> Result<String, SqlError> { 58 let backup = export_database_backup(executor)?; 59 serde_json::to_string(&backup).map_err(SqlError::from) 60 } 61 62 pub fn restore_database_backup( 63 executor: &dyn SqlExecutor, 64 backup: &DatabaseBackup, 65 ) -> Result<(), SqlError> { 66 validate_backup_version(backup)?; 67 executor.exec("PRAGMA foreign_keys = OFF;", "[]")?; 68 executor.begin()?; 69 let result = (|| { 70 drop_existing_objects(executor)?; 71 create_schema_from_backup(executor, &backup.schema)?; 72 insert_rows_from_backup(executor, backup)?; 73 Ok(()) 74 })(); 75 76 match result { 77 Ok(()) => { 78 executor.commit()?; 79 let _ = executor.exec("PRAGMA foreign_keys = ON;", "[]")?; 80 Ok(()) 81 } 82 Err(err) => { 83 let _ = executor.rollback(); 84 let _ = executor.exec("PRAGMA foreign_keys = ON;", "[]"); 85 Err(err) 86 } 87 } 88 } 89 90 pub fn restore_database_backup_json( 91 executor: &dyn SqlExecutor, 92 backup_json: &str, 93 ) -> Result<(), SqlError> { 94 let backup: DatabaseBackup = serde_json::from_str(backup_json).map_err(SqlError::from)?; 95 restore_database_backup(executor, &backup) 96 } 97 98 fn drop_existing_objects(executor: &dyn SqlExecutor) -> Result<(), SqlError> { 99 #[derive(Deserialize)] 100 struct MasterRow { 101 #[serde(rename = "type")] 102 object_type: Option<String>, 103 name: Option<String>, 104 } 105 let query = "select type, name from sqlite_master where name not like 'sqlite_%'"; 106 let json = executor.query_raw(query, "[]")?; 107 let rows: Vec<MasterRow> = utils::parse_json(&json)?; 108 109 let mut groups: HashMap<String, Vec<String>> = HashMap::new(); 110 for row in rows.into_iter() { 111 let obj_type = row.object_type.unwrap_or_default(); 112 let name = match row.name { 113 Some(n) => n, 114 None => continue, 115 }; 116 groups.entry(obj_type).or_default().push(name); 117 } 118 119 for object_type in ["trigger", "view", "index", "table"] { 120 if let Some(names) = groups.get(object_type) { 121 for name in names { 122 let stmt = match object_type { 123 "trigger" => format!("DROP TRIGGER IF EXISTS {};", escape_identifier(name)), 124 "view" => format!("DROP VIEW IF EXISTS {};", escape_identifier(name)), 125 "index" => format!("DROP INDEX IF EXISTS {};", escape_identifier(name)), 126 _ => format!("DROP TABLE IF EXISTS {};", escape_identifier(name)), 127 }; 128 let _ = executor.exec(&stmt, "[]")?; 129 } 130 } 131 } 132 Ok(()) 133 } 134 135 fn create_schema_from_backup( 136 executor: &dyn SqlExecutor, 137 schema: &[SchemaEntry], 138 ) -> Result<(), SqlError> { 139 for entry in schema.iter().filter(|s| s.object_type == "table") { 140 if let Some(sql) = &entry.sql { 141 executor.exec(sql, "[]")?; 142 } 143 } 144 for entry in schema.iter().filter(|s| s.object_type != "table") { 145 if let Some(sql) = &entry.sql { 146 executor.exec(sql, "[]")?; 147 } 148 } 149 Ok(()) 150 } 151 152 fn insert_rows_from_backup( 153 executor: &dyn SqlExecutor, 154 backup: &DatabaseBackup, 155 ) -> Result<(), SqlError> { 156 let mut row_sources: HashMap<&str, &Vec<Map<String, Value>>> = HashMap::new(); 157 for table in &backup.data { 158 row_sources.insert(table.name.as_str(), &table.rows); 159 } 160 for entry in backup.schema.iter().filter(|s| s.object_type == "table") { 161 let rows = match row_sources.get(entry.name.as_str()) { 162 Some(r) => *r, 163 None => continue, 164 }; 165 for row in rows { 166 insert_row(executor, &entry.name, row)?; 167 } 168 } 169 Ok(()) 170 } 171 172 fn insert_row( 173 executor: &dyn SqlExecutor, 174 table: &str, 175 row: &Map<String, Value>, 176 ) -> Result<(), SqlError> { 177 if row.is_empty() { 178 return Ok(()); 179 } 180 181 let mut cols: BTreeMap<String, &Value> = BTreeMap::new(); 182 for (k, v) in row { 183 cols.insert(k.clone(), v); 184 } 185 186 let column_names: Vec<String> = cols.keys().cloned().collect(); 187 let placeholders = (0..column_names.len()) 188 .map(|_| "?") 189 .collect::<Vec<_>>() 190 .join(","); 191 let sql = format!( 192 "INSERT INTO {} ({}) VALUES ({});", 193 escape_identifier(table), 194 column_names 195 .iter() 196 .map(|c| escape_identifier(c)) 197 .collect::<Vec<_>>() 198 .join(","), 199 placeholders 200 ); 201 202 let binds: Vec<Value> = cols.values().map(|v| utils::to_db_bind_value(v)).collect(); 203 let params_json = Value::Array(binds).to_string(); 204 executor.exec(&sql, ¶ms_json)?; 205 Ok(()) 206 } 207 208 pub(crate) fn load_schema(executor: &dyn SqlExecutor) -> Result<Vec<SchemaEntry>, SqlError> { 209 let query = "select type, name, tbl_name as table_name, sql from sqlite_master where name not like 'sqlite_%' order by type, name"; 210 let json = executor.query_raw(query, "[]")?; 211 #[derive(Deserialize)] 212 struct RawSchema { 213 #[serde(rename = "type")] 214 object_type: Option<String>, 215 name: Option<String>, 216 table_name: Option<String>, 217 sql: Option<String>, 218 } 219 let rows: Vec<RawSchema> = utils::parse_json(&json)?; 220 Ok(rows 221 .into_iter() 222 .filter_map(|row| { 223 let name = row.name?; 224 let object_type = row.object_type.unwrap_or_default(); 225 Some(SchemaEntry { 226 object_type, 227 name, 228 table_name: row.table_name, 229 sql: row.sql, 230 }) 231 }) 232 .collect()) 233 } 234 235 pub(crate) fn export_migrations() -> Vec<MigrationBackup> { 236 crate::migrations::MIGRATIONS 237 .iter() 238 .map(|m| MigrationBackup { 239 name: m.name.to_string(), 240 up_sql: m.up_sql.to_string(), 241 down_sql: m.down_sql.to_string(), 242 }) 243 .collect() 244 } 245 246 fn read_tables_for_backup( 247 executor: &dyn SqlExecutor, 248 schema: &[SchemaEntry], 249 ) -> Result<Vec<TableData>, SqlError> { 250 let mut data = Vec::new(); 251 for entry in schema.iter().filter(|s| s.object_type == "table") { 252 let select_sql = format!("SELECT * FROM {};", escape_identifier(&entry.name)); 253 let json = executor.query_raw(&select_sql, "[]")?; 254 let rows: Vec<Map<String, Value>> = utils::parse_json(&json)?; 255 data.push(TableData { 256 name: entry.name.clone(), 257 rows, 258 }); 259 } 260 Ok(data) 261 } 262 263 pub(crate) fn escape_identifier(name: &str) -> String { 264 let mut escaped = String::with_capacity(name.len() + 2); 265 escaped.push('"'); 266 for c in name.chars() { 267 if c == '"' { 268 escaped.push('"'); 269 } 270 escaped.push(c); 271 } 272 escaped.push('"'); 273 escaped 274 } 275 276 fn validate_backup_version(backup: &DatabaseBackup) -> Result<(), SqlError> { 277 if backup.format_version != DATABASE_BACKUP_VERSION { 278 return Err(SqlError::InvalidArgument(format!( 279 "unsupported backup format {}, expected {}", 280 backup.format_version, DATABASE_BACKUP_VERSION 281 ))); 282 } 283 if backup.replica_store_version != REPLICA_STORE_SCHEMA_VERSION 284 && !LEGACY_COMPATIBLE_REPLICA_STORE_SCHEMA_VERSIONS 285 .contains(&backup.replica_store_version.as_str()) 286 { 287 return Err(SqlError::InvalidArgument(format!( 288 "unsupported replica-store backup schema version {}, expected {}", 289 backup.replica_store_version, REPLICA_STORE_SCHEMA_VERSION 290 ))); 291 } 292 Ok(()) 293 } 294 295 #[cfg(test)] 296 mod tests { 297 use super::*; 298 use radroots_sql_core::ExecOutcome; 299 use std::sync::Mutex; 300 use std::sync::atomic::{AtomicUsize, Ordering}; 301 302 fn assert_sql_error_code<T: core::fmt::Debug>(result: Result<T, SqlError>, code: &str) { 303 let err = result.unwrap_err(); 304 assert_eq!(err.code(), code); 305 } 306 307 struct MockExecutor { 308 query_rules: Vec<(String, String)>, 309 fail_exec_contains: Option<String>, 310 fail_query_contains: Option<String>, 311 fail_begin: bool, 312 fail_commit: bool, 313 exec_calls: Mutex<Vec<String>>, 314 begin_calls: AtomicUsize, 315 commit_calls: AtomicUsize, 316 rollback_calls: AtomicUsize, 317 } 318 319 impl MockExecutor { 320 fn new(query_rules: Vec<(String, String)>, fail_exec_contains: Option<String>) -> Self { 321 Self { 322 query_rules, 323 fail_exec_contains, 324 fail_query_contains: None, 325 fail_begin: false, 326 fail_commit: false, 327 exec_calls: Mutex::new(Vec::new()), 328 begin_calls: AtomicUsize::new(0), 329 commit_calls: AtomicUsize::new(0), 330 rollback_calls: AtomicUsize::new(0), 331 } 332 } 333 334 fn with_query_failure(mut self, needle: &str) -> Self { 335 self.fail_query_contains = Some(needle.to_string()); 336 self 337 } 338 339 fn with_begin_failure(mut self) -> Self { 340 self.fail_begin = true; 341 self 342 } 343 344 fn with_commit_failure(mut self) -> Self { 345 self.fail_commit = true; 346 self 347 } 348 349 fn exec_calls(&self) -> Vec<String> { 350 self.exec_calls.lock().expect("exec calls lock").clone() 351 } 352 353 fn begin_count(&self) -> usize { 354 self.begin_calls.load(Ordering::SeqCst) 355 } 356 357 fn commit_count(&self) -> usize { 358 self.commit_calls.load(Ordering::SeqCst) 359 } 360 361 fn rollback_count(&self) -> usize { 362 self.rollback_calls.load(Ordering::SeqCst) 363 } 364 } 365 366 impl SqlExecutor for MockExecutor { 367 fn exec(&self, sql: &str, _params_json: &str) -> Result<ExecOutcome, SqlError> { 368 self.exec_calls 369 .lock() 370 .expect("exec calls lock") 371 .push(sql.to_string()); 372 if let Some(needle) = &self.fail_exec_contains 373 && sql.contains(needle) 374 { 375 return Err(SqlError::InvalidQuery(String::from("forced exec failure"))); 376 } 377 Ok(ExecOutcome { 378 changes: 1, 379 last_insert_id: 1, 380 }) 381 } 382 383 fn query_raw(&self, sql: &str, _params_json: &str) -> Result<String, SqlError> { 384 if let Some(needle) = &self.fail_query_contains 385 && sql.contains(needle) 386 { 387 return Err(SqlError::InvalidQuery(String::from("forced query failure"))); 388 } 389 for (needle, response) in &self.query_rules { 390 if sql.contains(needle) { 391 return Ok(response.clone()); 392 } 393 } 394 Ok(String::from("[]")) 395 } 396 397 fn begin(&self) -> Result<(), SqlError> { 398 self.begin_calls.fetch_add(1, Ordering::SeqCst); 399 if self.fail_begin { 400 return Err(SqlError::InvalidQuery(String::from("forced begin failure"))); 401 } 402 Ok(()) 403 } 404 405 fn commit(&self) -> Result<(), SqlError> { 406 self.commit_calls.fetch_add(1, Ordering::SeqCst); 407 if self.fail_commit { 408 return Err(SqlError::InvalidQuery(String::from( 409 "forced commit failure", 410 ))); 411 } 412 Ok(()) 413 } 414 415 fn rollback(&self) -> Result<(), SqlError> { 416 self.rollback_calls.fetch_add(1, Ordering::SeqCst); 417 Ok(()) 418 } 419 } 420 421 fn backup_with_versions(format_version: &str, replica_store_version: &str) -> DatabaseBackup { 422 DatabaseBackup { 423 format_version: format_version.to_string(), 424 replica_store_version: replica_store_version.to_string(), 425 schema: Vec::new(), 426 migrations: Vec::new(), 427 data: Vec::new(), 428 } 429 } 430 431 #[test] 432 fn restore_database_backup_rolls_back_when_exec_fails() { 433 let executor = MockExecutor::new( 434 vec![( 435 String::from("select type, name from sqlite_master"), 436 String::from("[]"), 437 )], 438 Some(String::from("CREATE TABLE fail_table")), 439 ); 440 let backup = DatabaseBackup { 441 format_version: DATABASE_BACKUP_VERSION.to_string(), 442 replica_store_version: REPLICA_STORE_SCHEMA_VERSION.to_string(), 443 schema: vec![SchemaEntry { 444 object_type: String::from("table"), 445 name: String::from("fail_table"), 446 table_name: Some(String::from("fail_table")), 447 sql: Some(String::from("CREATE TABLE fail_table (id TEXT);")), 448 }], 449 migrations: Vec::new(), 450 data: Vec::new(), 451 }; 452 453 assert_sql_error_code( 454 restore_database_backup(&executor, &backup), 455 "ERR_INVALID_QUERY", 456 ); 457 assert_eq!(executor.begin_count(), 1); 458 assert_eq!(executor.commit_count(), 0); 459 assert_eq!(executor.rollback_count(), 1); 460 let calls = executor.exec_calls(); 461 assert!( 462 calls 463 .iter() 464 .any(|sql| sql.contains("PRAGMA foreign_keys = OFF")) 465 ); 466 assert!( 467 calls 468 .iter() 469 .any(|sql| sql.contains("PRAGMA foreign_keys = ON")) 470 ); 471 } 472 473 #[test] 474 fn drop_existing_objects_skips_rows_without_name() { 475 let master_rows = serde_json::json!([ 476 { "type": "trigger", "name": "tg_a" }, 477 { "type": "view", "name": "vw_a" }, 478 { "type": "index", "name": "ix_a" }, 479 { "type": "table", "name": "tb_a" }, 480 { "type": "table", "name": null } 481 ]) 482 .to_string(); 483 let executor = MockExecutor::new( 484 vec![( 485 String::from("select type, name from sqlite_master"), 486 master_rows, 487 )], 488 None, 489 ); 490 491 drop_existing_objects(&executor).expect("drop existing objects"); 492 let calls = executor.exec_calls(); 493 assert!( 494 calls 495 .iter() 496 .any(|sql| sql.contains("DROP TRIGGER IF EXISTS \"tg_a\";")) 497 ); 498 assert!( 499 calls 500 .iter() 501 .any(|sql| sql.contains("DROP VIEW IF EXISTS \"vw_a\";")) 502 ); 503 assert!( 504 calls 505 .iter() 506 .any(|sql| sql.contains("DROP INDEX IF EXISTS \"ix_a\";")) 507 ); 508 assert!( 509 calls 510 .iter() 511 .any(|sql| sql.contains("DROP TABLE IF EXISTS \"tb_a\";")) 512 ); 513 } 514 515 #[test] 516 fn create_schema_from_backup_executes_table_and_non_table_sql() { 517 let executor = MockExecutor::new(Vec::new(), None); 518 let schema = vec![ 519 SchemaEntry { 520 object_type: String::from("table"), 521 name: String::from("tb_a"), 522 table_name: Some(String::from("tb_a")), 523 sql: Some(String::from("CREATE TABLE tb_a (id TEXT);")), 524 }, 525 SchemaEntry { 526 object_type: String::from("table"), 527 name: String::from("tb_b"), 528 table_name: Some(String::from("tb_b")), 529 sql: None, 530 }, 531 SchemaEntry { 532 object_type: String::from("view"), 533 name: String::from("vw_a"), 534 table_name: Some(String::from("vw_a")), 535 sql: Some(String::from("CREATE VIEW vw_a AS SELECT 1;")), 536 }, 537 SchemaEntry { 538 object_type: String::from("index"), 539 name: String::from("ix_a"), 540 table_name: Some(String::from("ix_a")), 541 sql: None, 542 }, 543 ]; 544 545 create_schema_from_backup(&executor, &schema).expect("create schema from backup"); 546 let calls = executor.exec_calls(); 547 assert!( 548 calls 549 .iter() 550 .any(|sql| sql == "CREATE TABLE tb_a (id TEXT);") 551 ); 552 assert!( 553 calls 554 .iter() 555 .any(|sql| sql == "CREATE VIEW vw_a AS SELECT 1;") 556 ); 557 assert_eq!(calls.len(), 2); 558 } 559 560 #[test] 561 fn insert_rows_from_backup_skips_missing_data_and_empty_rows() { 562 let executor = MockExecutor::new(Vec::new(), None); 563 let mut row = Map::new(); 564 row.insert(String::from("co\"l"), Value::from(7)); 565 let backup = DatabaseBackup { 566 format_version: DATABASE_BACKUP_VERSION.to_string(), 567 replica_store_version: REPLICA_STORE_SCHEMA_VERSION.to_string(), 568 schema: vec![ 569 SchemaEntry { 570 object_type: String::from("table"), 571 name: String::from("tb_a"), 572 table_name: Some(String::from("tb_a")), 573 sql: Some(String::from("CREATE TABLE tb_a (id TEXT);")), 574 }, 575 SchemaEntry { 576 object_type: String::from("table"), 577 name: String::from("tb_b"), 578 table_name: Some(String::from("tb_b")), 579 sql: Some(String::from("CREATE TABLE tb_b (id TEXT);")), 580 }, 581 ], 582 migrations: Vec::new(), 583 data: vec![TableData { 584 name: String::from("tb_a"), 585 rows: vec![row], 586 }], 587 }; 588 589 insert_rows_from_backup(&executor, &backup).expect("insert rows from backup"); 590 let calls_after_insert = executor.exec_calls(); 591 assert!( 592 calls_after_insert 593 .iter() 594 .any(|sql| sql.contains("INSERT INTO \"tb_a\" (\"co\"\"l\") VALUES (?);")) 595 ); 596 assert!( 597 !calls_after_insert 598 .iter() 599 .any(|sql| sql.contains("\"tb_b\"")) 600 ); 601 602 let empty_row = Map::new(); 603 insert_row(&executor, "tb_a", &empty_row).expect("insert empty row"); 604 assert_eq!(executor.exec_calls().len(), calls_after_insert.len()); 605 assert_eq!(escape_identifier("a\"b"), "\"a\"\"b\""); 606 } 607 608 #[test] 609 fn load_schema_filters_rows_without_name() { 610 let schema_rows = serde_json::json!([ 611 { "type": "table", "name": null, "table_name": "tb_a", "sql": "CREATE TABLE tb_a (id TEXT);" }, 612 { "type": "view", "name": "vw_a", "table_name": "vw_a", "sql": "CREATE VIEW vw_a AS SELECT 1;" } 613 ]) 614 .to_string(); 615 let executor = MockExecutor::new( 616 vec![( 617 String::from("select type, name, tbl_name as table_name, sql from sqlite_master"), 618 schema_rows, 619 )], 620 None, 621 ); 622 623 let rows = load_schema(&executor).expect("load schema"); 624 assert_eq!(rows.len(), 1); 625 assert_eq!(rows[0].name, "vw_a"); 626 assert_eq!(rows[0].object_type, "view"); 627 } 628 629 #[test] 630 fn load_schema_rejects_invalid_json() { 631 let executor = MockExecutor::new( 632 vec![( 633 String::from("select type, name, tbl_name as table_name, sql from sqlite_master"), 634 String::from("{"), 635 )], 636 None, 637 ); 638 assert_sql_error_code(load_schema(&executor), "ERR_SERIALIZATION"); 639 } 640 641 #[test] 642 fn validate_backup_version_rejects_invalid_versions() { 643 let wrong_format = backup_with_versions("0.0.1", REPLICA_STORE_SCHEMA_VERSION); 644 assert_sql_error_code( 645 validate_backup_version(&wrong_format), 646 "ERR_INVALID_ARGUMENT", 647 ); 648 649 let wrong_db_version = backup_with_versions(DATABASE_BACKUP_VERSION, "0.0.0"); 650 assert_sql_error_code( 651 validate_backup_version(&wrong_db_version), 652 "ERR_INVALID_ARGUMENT", 653 ); 654 655 let previous_alpha = backup_with_versions(DATABASE_BACKUP_VERSION, "0.1.0-alpha.2"); 656 validate_backup_version(&previous_alpha) 657 .expect("schema-compatible alpha.2 backup must remain restorable"); 658 } 659 660 #[test] 661 fn restore_database_backup_commits_on_success_and_query_fallback_works() { 662 let executor = MockExecutor::new( 663 vec![( 664 String::from("select type, name from sqlite_master"), 665 String::from("[]"), 666 )], 667 None, 668 ); 669 let backup = backup_with_versions(DATABASE_BACKUP_VERSION, REPLICA_STORE_SCHEMA_VERSION); 670 671 let matched = executor 672 .query_raw("select type, name from sqlite_master", "[]") 673 .expect("query match"); 674 assert_eq!(matched, "[]"); 675 676 let fallback = executor 677 .query_raw("select 1", "[]") 678 .expect("query fallback"); 679 assert_eq!(fallback, "[]"); 680 681 restore_database_backup(&executor, &backup).expect("restore should succeed"); 682 assert_eq!(executor.begin_count(), 1); 683 assert_eq!(executor.commit_count(), 1); 684 assert_eq!(executor.rollback_count(), 0); 685 } 686 687 #[test] 688 fn restore_database_backup_json_rejects_invalid_json() { 689 let executor = MockExecutor::new(Vec::new(), None); 690 assert_sql_error_code( 691 restore_database_backup_json(&executor, "{"), 692 "ERR_SERIALIZATION", 693 ); 694 } 695 696 #[test] 697 fn restore_database_backup_json_accepts_valid_json() { 698 let executor = MockExecutor::new( 699 vec![( 700 String::from("select type, name from sqlite_master"), 701 String::from("[]"), 702 )], 703 None, 704 ); 705 let backup = backup_with_versions(DATABASE_BACKUP_VERSION, REPLICA_STORE_SCHEMA_VERSION); 706 let backup_json = serde_json::to_string(&backup).expect("serialize backup"); 707 708 restore_database_backup_json(&executor, &backup_json).expect("restore should succeed"); 709 assert_eq!(executor.begin_count(), 1); 710 assert_eq!(executor.commit_count(), 1); 711 assert_eq!(executor.rollback_count(), 0); 712 } 713 714 #[test] 715 fn export_database_backup_propagates_schema_query_errors() { 716 let executor = MockExecutor::new(Vec::new(), None).with_query_failure( 717 "select type, name, tbl_name as table_name, sql from sqlite_master", 718 ); 719 assert_sql_error_code(export_database_backup(&executor), "ERR_INVALID_QUERY"); 720 } 721 722 #[test] 723 fn export_database_backup_propagates_table_query_errors() { 724 let schema_rows = serde_json::json!([ 725 { 726 "type": "table", 727 "name": "tb_a", 728 "table_name": "tb_a", 729 "sql": "CREATE TABLE tb_a (id TEXT);" 730 } 731 ]) 732 .to_string(); 733 let executor = MockExecutor::new( 734 vec![( 735 String::from("select type, name, tbl_name as table_name, sql from sqlite_master"), 736 schema_rows, 737 )], 738 None, 739 ) 740 .with_query_failure("SELECT * FROM \"tb_a\";"); 741 assert_sql_error_code(export_database_backup(&executor), "ERR_INVALID_QUERY"); 742 } 743 744 #[test] 745 fn export_database_backup_json_propagates_export_errors() { 746 let executor = MockExecutor::new(Vec::new(), None).with_query_failure( 747 "select type, name, tbl_name as table_name, sql from sqlite_master", 748 ); 749 assert_sql_error_code(export_database_backup_json(&executor), "ERR_INVALID_QUERY"); 750 } 751 752 #[test] 753 fn export_database_backup_succeeds_with_empty_schema() { 754 let executor = MockExecutor::new( 755 vec![( 756 String::from("select type, name, tbl_name as table_name, sql from sqlite_master"), 757 String::from("[]"), 758 )], 759 None, 760 ); 761 let backup = export_database_backup(&executor).expect("backup success"); 762 assert_eq!(backup.replica_store_version, "1.0.0"); 763 assert!(backup.schema.is_empty()); 764 assert!(backup.data.is_empty()); 765 } 766 767 #[test] 768 fn export_database_backup_json_succeeds_with_empty_schema() { 769 let executor = MockExecutor::new( 770 vec![( 771 String::from("select type, name, tbl_name as table_name, sql from sqlite_master"), 772 String::from("[]"), 773 )], 774 None, 775 ); 776 let backup_json = export_database_backup_json(&executor).expect("backup json success"); 777 assert!(backup_json.contains("\"schema\":[]")); 778 } 779 780 #[test] 781 fn drop_existing_objects_rejects_invalid_master_json() { 782 let executor = MockExecutor::new( 783 vec![( 784 String::from("select type, name from sqlite_master"), 785 String::from("{"), 786 )], 787 None, 788 ); 789 assert_sql_error_code(drop_existing_objects(&executor), "ERR_SERIALIZATION"); 790 } 791 792 #[test] 793 fn drop_existing_objects_propagates_drop_exec_errors() { 794 let master_rows = serde_json::json!([{ "type": "table", "name": "tb_a" }]).to_string(); 795 let executor = MockExecutor::new( 796 vec![( 797 String::from("select type, name from sqlite_master"), 798 master_rows, 799 )], 800 Some(String::from("DROP TABLE IF EXISTS")), 801 ); 802 assert_sql_error_code(drop_existing_objects(&executor), "ERR_INVALID_QUERY"); 803 } 804 805 #[test] 806 fn create_schema_from_backup_propagates_non_table_exec_errors() { 807 let executor = MockExecutor::new(Vec::new(), Some(String::from("CREATE VIEW"))); 808 let schema = vec![SchemaEntry { 809 object_type: String::from("view"), 810 name: String::from("vw_a"), 811 table_name: Some(String::from("vw_a")), 812 sql: Some(String::from("CREATE VIEW vw_a AS SELECT 1;")), 813 }]; 814 assert_sql_error_code( 815 create_schema_from_backup(&executor, &schema), 816 "ERR_INVALID_QUERY", 817 ); 818 } 819 820 #[test] 821 fn read_tables_for_backup_propagates_query_errors() { 822 let executor = 823 MockExecutor::new(Vec::new(), None).with_query_failure("SELECT * FROM \"tb_a\";"); 824 let schema = vec![SchemaEntry { 825 object_type: String::from("table"), 826 name: String::from("tb_a"), 827 table_name: Some(String::from("tb_a")), 828 sql: Some(String::from("CREATE TABLE tb_a (id TEXT);")), 829 }]; 830 assert_sql_error_code( 831 read_tables_for_backup(&executor, &schema), 832 "ERR_INVALID_QUERY", 833 ); 834 } 835 836 #[test] 837 fn read_tables_for_backup_propagates_parse_errors() { 838 let executor = MockExecutor::new( 839 vec![(String::from("SELECT * FROM \"tb_a\";"), String::from("{"))], 840 None, 841 ); 842 let schema = vec![SchemaEntry { 843 object_type: String::from("table"), 844 name: String::from("tb_a"), 845 table_name: Some(String::from("tb_a")), 846 sql: Some(String::from("CREATE TABLE tb_a (id TEXT);")), 847 }]; 848 assert_sql_error_code( 849 read_tables_for_backup(&executor, &schema), 850 "ERR_SERIALIZATION", 851 ); 852 } 853 854 #[test] 855 fn restore_database_backup_rejects_invalid_versions_before_transaction() { 856 let executor = MockExecutor::new(Vec::new(), None); 857 let backup = backup_with_versions("0.0.1", REPLICA_STORE_SCHEMA_VERSION); 858 assert_sql_error_code( 859 restore_database_backup(&executor, &backup), 860 "ERR_INVALID_ARGUMENT", 861 ); 862 assert_eq!(executor.begin_count(), 0); 863 } 864 865 #[test] 866 fn restore_database_backup_fails_when_foreign_keys_disable_fails() { 867 let executor = MockExecutor::new( 868 vec![( 869 String::from("select type, name from sqlite_master"), 870 String::from("[]"), 871 )], 872 Some(String::from("PRAGMA foreign_keys = OFF;")), 873 ); 874 let backup = backup_with_versions(DATABASE_BACKUP_VERSION, REPLICA_STORE_SCHEMA_VERSION); 875 assert_sql_error_code( 876 restore_database_backup(&executor, &backup), 877 "ERR_INVALID_QUERY", 878 ); 879 } 880 881 #[test] 882 fn restore_database_backup_fails_when_begin_fails() { 883 let executor = MockExecutor::new( 884 vec![( 885 String::from("select type, name from sqlite_master"), 886 String::from("[]"), 887 )], 888 None, 889 ) 890 .with_begin_failure(); 891 let backup = backup_with_versions(DATABASE_BACKUP_VERSION, REPLICA_STORE_SCHEMA_VERSION); 892 assert_sql_error_code( 893 restore_database_backup(&executor, &backup), 894 "ERR_INVALID_QUERY", 895 ); 896 } 897 898 #[test] 899 fn restore_database_backup_fails_when_drop_query_fails() { 900 let executor = MockExecutor::new(Vec::new(), None) 901 .with_query_failure("select type, name from sqlite_master"); 902 let backup = backup_with_versions(DATABASE_BACKUP_VERSION, REPLICA_STORE_SCHEMA_VERSION); 903 assert_sql_error_code( 904 restore_database_backup(&executor, &backup), 905 "ERR_INVALID_QUERY", 906 ); 907 } 908 909 #[test] 910 fn restore_database_backup_fails_when_create_schema_fails() { 911 let executor = MockExecutor::new( 912 vec![( 913 String::from("select type, name from sqlite_master"), 914 String::from("[]"), 915 )], 916 Some(String::from("CREATE TABLE tb_a")), 917 ); 918 let backup = DatabaseBackup { 919 format_version: DATABASE_BACKUP_VERSION.to_string(), 920 replica_store_version: REPLICA_STORE_SCHEMA_VERSION.to_string(), 921 schema: vec![SchemaEntry { 922 object_type: String::from("table"), 923 name: String::from("tb_a"), 924 table_name: Some(String::from("tb_a")), 925 sql: Some(String::from("CREATE TABLE tb_a (id TEXT);")), 926 }], 927 migrations: Vec::new(), 928 data: Vec::new(), 929 }; 930 931 assert_sql_error_code( 932 restore_database_backup(&executor, &backup), 933 "ERR_INVALID_QUERY", 934 ); 935 assert_eq!(executor.begin_count(), 1); 936 assert_eq!(executor.commit_count(), 0); 937 assert_eq!(executor.rollback_count(), 1); 938 } 939 940 #[test] 941 fn restore_database_backup_fails_when_insert_rows_fail() { 942 let executor = MockExecutor::new( 943 vec![( 944 String::from("select type, name from sqlite_master"), 945 String::from("[]"), 946 )], 947 Some(String::from("INSERT INTO \"tb_a\"")), 948 ); 949 let mut row = Map::new(); 950 row.insert(String::from("id"), Value::from("1")); 951 let backup = DatabaseBackup { 952 format_version: DATABASE_BACKUP_VERSION.to_string(), 953 replica_store_version: REPLICA_STORE_SCHEMA_VERSION.to_string(), 954 schema: vec![SchemaEntry { 955 object_type: String::from("table"), 956 name: String::from("tb_a"), 957 table_name: Some(String::from("tb_a")), 958 sql: Some(String::from("CREATE TABLE tb_a (id TEXT);")), 959 }], 960 migrations: Vec::new(), 961 data: vec![TableData { 962 name: String::from("tb_a"), 963 rows: vec![row], 964 }], 965 }; 966 assert_sql_error_code( 967 restore_database_backup(&executor, &backup), 968 "ERR_INVALID_QUERY", 969 ); 970 } 971 972 #[test] 973 fn restore_database_backup_fails_when_commit_fails() { 974 let executor = MockExecutor::new( 975 vec![( 976 String::from("select type, name from sqlite_master"), 977 String::from("[]"), 978 )], 979 None, 980 ) 981 .with_commit_failure(); 982 let backup = backup_with_versions(DATABASE_BACKUP_VERSION, REPLICA_STORE_SCHEMA_VERSION); 983 assert_sql_error_code( 984 restore_database_backup(&executor, &backup), 985 "ERR_INVALID_QUERY", 986 ); 987 } 988 989 #[test] 990 fn restore_database_backup_fails_when_foreign_keys_enable_fails_after_commit() { 991 let executor = MockExecutor::new( 992 vec![( 993 String::from("select type, name from sqlite_master"), 994 String::from("[]"), 995 )], 996 Some(String::from("PRAGMA foreign_keys = ON;")), 997 ); 998 let backup = backup_with_versions(DATABASE_BACKUP_VERSION, REPLICA_STORE_SCHEMA_VERSION); 999 assert_sql_error_code( 1000 restore_database_backup(&executor, &backup), 1001 "ERR_INVALID_QUERY", 1002 ); 1003 } 1004 }