lib

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

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