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 }