lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

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, &params_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 }