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