lib

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

export.rs (9051B)


      1 use radroots_sql_core::{SqlExecutor, error::SqlError, utils};
      2 use serde::{Deserialize, Serialize};
      3 use sha2::{Digest, Sha256};
      4 
      5 use crate::backup::{
      6     DATABASE_BACKUP_VERSION, MigrationBackup, REPLICA_DB_VERSION, SchemaEntry, escape_identifier,
      7     export_migrations, load_schema,
      8 };
      9 
     10 pub const REPLICA_DB_EXPORT_VERSION: &str = "1";
     11 
     12 #[derive(Debug, Clone, Serialize, Deserialize)]
     13 pub struct TableCount {
     14     pub name: String,
     15     pub row_count: u64,
     16 }
     17 
     18 #[derive(Debug, Clone, Serialize, Deserialize)]
     19 pub struct ReplicaDbExportManifestRs {
     20     pub export_version: String,
     21     pub replica_db_version: String,
     22     pub backup_format_version: String,
     23     pub schema_hash: String,
     24     pub schema: Vec<SchemaEntry>,
     25     pub migrations: Vec<MigrationBackup>,
     26     pub table_counts: Vec<TableCount>,
     27 }
     28 
     29 pub fn export_manifest(executor: &dyn SqlExecutor) -> Result<ReplicaDbExportManifestRs, SqlError> {
     30     let schema = load_schema(executor)?;
     31     let migrations = export_migrations();
     32     let table_counts = load_table_counts(executor, &schema)?;
     33     let schema_hash = schema_hash(&schema);
     34     Ok(ReplicaDbExportManifestRs {
     35         export_version: REPLICA_DB_EXPORT_VERSION.to_string(),
     36         replica_db_version: REPLICA_DB_VERSION.to_string(),
     37         backup_format_version: DATABASE_BACKUP_VERSION.to_string(),
     38         schema_hash,
     39         schema,
     40         migrations,
     41         table_counts,
     42     })
     43 }
     44 
     45 fn load_table_counts(
     46     executor: &dyn SqlExecutor,
     47     schema: &[SchemaEntry],
     48 ) -> Result<Vec<TableCount>, SqlError> {
     49     #[derive(Deserialize)]
     50     struct CountRow {
     51         count: u64,
     52     }
     53     let mut counts = Vec::new();
     54     for entry in schema.iter().filter(|s| s.object_type == "table") {
     55         let sql = format!(
     56             "select count(1) as count from {}",
     57             escape_identifier(&entry.name)
     58         );
     59         let json = executor.query_raw(&sql, "[]")?;
     60         let rows: Vec<CountRow> = utils::parse_json(&json)?;
     61         let row_count = rows.first().map(|row| row.count).unwrap_or(0);
     62         counts.push(TableCount {
     63             name: entry.name.clone(),
     64             row_count,
     65         });
     66     }
     67     Ok(counts)
     68 }
     69 
     70 fn schema_hash(schema: &[SchemaEntry]) -> String {
     71     let mut hasher = Sha256::new();
     72     for entry in schema {
     73         hasher.update(entry.object_type.as_bytes());
     74         hasher.update([0]);
     75         hasher.update(entry.name.as_bytes());
     76         hasher.update([0]);
     77         if let Some(table_name) = &entry.table_name {
     78             hasher.update(table_name.as_bytes());
     79         }
     80         hasher.update([0]);
     81         if let Some(sql) = &entry.sql {
     82             hasher.update(sql.as_bytes());
     83         }
     84         hasher.update([255]);
     85     }
     86     hex::encode(hasher.finalize())
     87 }
     88 
     89 #[cfg(test)]
     90 mod tests {
     91     use super::*;
     92     use radroots_sql_core::ExecOutcome;
     93 
     94     fn assert_sql_error_code<T: core::fmt::Debug>(result: Result<T, SqlError>, code: &str) {
     95         let err = result.unwrap_err();
     96         assert_eq!(err.code(), code);
     97     }
     98 
     99     struct MockExecutor {
    100         query_rules: Vec<(String, String)>,
    101         fail_query_contains: Option<String>,
    102     }
    103 
    104     impl MockExecutor {
    105         fn new(query_rules: Vec<(String, String)>, fail_query_contains: Option<String>) -> Self {
    106             Self {
    107                 query_rules,
    108                 fail_query_contains,
    109             }
    110         }
    111     }
    112 
    113     impl SqlExecutor for MockExecutor {
    114         fn exec(&self, _sql: &str, _params_json: &str) -> Result<ExecOutcome, SqlError> {
    115             Ok(ExecOutcome {
    116                 changes: 1,
    117                 last_insert_id: 1,
    118             })
    119         }
    120 
    121         fn query_raw(&self, sql: &str, _params_json: &str) -> Result<String, SqlError> {
    122             if let Some(needle) = &self.fail_query_contains {
    123                 if sql.contains(needle) {
    124                     return Err(SqlError::InvalidQuery(String::from("forced query failure")));
    125                 }
    126             }
    127             for (needle, response) in &self.query_rules {
    128                 if sql.contains(needle) {
    129                     return Ok(response.clone());
    130                 }
    131             }
    132             Ok(String::from("[]"))
    133         }
    134 
    135         fn begin(&self) -> Result<(), SqlError> {
    136             Ok(())
    137         }
    138 
    139         fn commit(&self) -> Result<(), SqlError> {
    140             Ok(())
    141         }
    142 
    143         fn rollback(&self) -> Result<(), SqlError> {
    144             Ok(())
    145         }
    146     }
    147 
    148     #[test]
    149     fn export_manifest_propagates_schema_query_errors() {
    150         let executor = MockExecutor::new(
    151             Vec::new(),
    152             Some(String::from(
    153                 "select type, name, tbl_name as table_name, sql from sqlite_master",
    154             )),
    155         );
    156         assert_sql_error_code(export_manifest(&executor), "ERR_INVALID_QUERY");
    157     }
    158 
    159     #[test]
    160     fn export_manifest_propagates_table_count_query_errors() {
    161         let schema_rows = serde_json::json!([
    162             {
    163                 "type": "table",
    164                 "name": "tb_a",
    165                 "table_name": "tb_a",
    166                 "sql": "CREATE TABLE tb_a (id TEXT);"
    167             }
    168         ])
    169         .to_string();
    170         let executor = MockExecutor::new(
    171             vec![(
    172                 String::from("select type, name, tbl_name as table_name, sql from sqlite_master"),
    173                 schema_rows,
    174             )],
    175             Some(String::from("select count(1) as count from \"tb_a\"")),
    176         );
    177         assert_sql_error_code(export_manifest(&executor), "ERR_INVALID_QUERY");
    178     }
    179 
    180     #[test]
    181     fn export_manifest_propagates_table_count_parse_errors() {
    182         let schema_rows = serde_json::json!([
    183             {
    184                 "type": "table",
    185                 "name": "tb_a",
    186                 "table_name": "tb_a",
    187                 "sql": "CREATE TABLE tb_a (id TEXT);"
    188             }
    189         ])
    190         .to_string();
    191         let executor = MockExecutor::new(
    192             vec![
    193                 (
    194                     String::from(
    195                         "select type, name, tbl_name as table_name, sql from sqlite_master",
    196                     ),
    197                     schema_rows,
    198                 ),
    199                 (
    200                     String::from("select count(1) as count from \"tb_a\""),
    201                     String::from("{"),
    202                 ),
    203             ],
    204             None,
    205         );
    206         assert_sql_error_code(export_manifest(&executor), "ERR_SERIALIZATION");
    207     }
    208 
    209     #[test]
    210     fn export_manifest_defaults_missing_count_row_to_zero() {
    211         let schema_rows = serde_json::json!([
    212             {
    213                 "type": "table",
    214                 "name": "tb_a",
    215                 "table_name": "tb_a",
    216                 "sql": "CREATE TABLE tb_a (id TEXT);"
    217             }
    218         ])
    219         .to_string();
    220         let executor = MockExecutor::new(
    221             vec![
    222                 (
    223                     String::from(
    224                         "select type, name, tbl_name as table_name, sql from sqlite_master",
    225                     ),
    226                     schema_rows,
    227                 ),
    228                 (
    229                     String::from("select count(1) as count from \"tb_a\""),
    230                     String::from("[]"),
    231                 ),
    232             ],
    233             None,
    234         );
    235         let manifest = export_manifest(&executor).expect("export should succeed");
    236         assert_eq!(manifest.table_counts.len(), 1);
    237         assert_eq!(manifest.table_counts[0].name, "tb_a");
    238         assert_eq!(manifest.table_counts[0].row_count, 0);
    239     }
    240 
    241     #[test]
    242     fn schema_hash_handles_optional_fields() {
    243         let with_all = SchemaEntry {
    244             object_type: String::from("table"),
    245             name: String::from("tb_a"),
    246             table_name: Some(String::from("tb_a")),
    247             sql: Some(String::from("CREATE TABLE tb_a (id TEXT);")),
    248         };
    249         let without_optional = SchemaEntry {
    250             object_type: String::from("index"),
    251             name: String::from("ix_a"),
    252             table_name: None,
    253             sql: None,
    254         };
    255         let hash = schema_hash(&[with_all, without_optional]);
    256         assert_eq!(hash.len(), 64);
    257     }
    258 
    259     #[test]
    260     fn mock_executor_trait_and_query_paths_are_covered() {
    261         let executor = MockExecutor::new(
    262             vec![(String::from("select 1"), String::from("[{\"count\":1}]"))],
    263             None,
    264         );
    265         let outcome = executor.exec("select 1", "[]").expect("exec");
    266         assert_eq!(outcome.changes, 1);
    267         assert_eq!(outcome.last_insert_id, 1);
    268 
    269         executor.begin().expect("begin");
    270         executor.commit().expect("commit");
    271         executor.rollback().expect("rollback");
    272 
    273         let matched = executor.query_raw("select 1", "[]").expect("matched query");
    274         assert_eq!(matched, "[{\"count\":1}]");
    275         let fallback = executor
    276             .query_raw("select 2", "[]")
    277             .expect("fallback query");
    278         assert_eq!(fallback, "[]");
    279 
    280         let failing = MockExecutor::new(Vec::new(), Some(String::from("select fail")));
    281         assert_sql_error_code(failing.query_raw("select fail", "[]"), "ERR_INVALID_QUERY");
    282     }
    283 }