lib

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

coverage.rs (23207B)


      1 #[cfg(feature = "native")]
      2 use radroots_sql_core::SqliteExecutor;
      3 use radroots_sql_core::error::SqlError;
      4 use radroots_sql_core::migrations::{Migration, migrations_run_all_down, migrations_run_all_up};
      5 use radroots_sql_core::utils::{
      6     build_insert_query_with_meta, build_select_query_with_meta, build_where_clause_eq, parse_json,
      7     parse_query_value, time_created_on, to_db_bind_value, to_object_map, to_params_json,
      8     to_partial_object_map, uuidv4, with_transaction,
      9 };
     10 use radroots_sql_core::{ExecOutcome, SqlExecutor};
     11 use serde::ser::{SerializeMap, SerializeSeq};
     12 use serde::{Deserialize, Serialize, Serializer};
     13 use serde_json::{Map, Value, json};
     14 use std::collections::BTreeSet;
     15 use std::sync::Mutex;
     16 
     17 #[derive(Debug, Clone, PartialEq)]
     18 struct ExecutorSnapshot {
     19     exec_sql: Vec<String>,
     20     begin_count: usize,
     21     commit_count: usize,
     22     rollback_count: usize,
     23     applied: BTreeSet<String>,
     24 }
     25 
     26 #[derive(Debug, Clone, Default)]
     27 struct ExecutorState {
     28     exec_sql: Vec<String>,
     29     begin_count: usize,
     30     commit_count: usize,
     31     rollback_count: usize,
     32     applied: BTreeSet<String>,
     33     fail_begin: bool,
     34     fail_commit: bool,
     35     fail_rollback: bool,
     36     fail_sql_contains: Option<String>,
     37     query_override: Option<Result<String, SqlError>>,
     38 }
     39 
     40 #[derive(Debug, Default)]
     41 struct MockExecutor {
     42     state: Mutex<ExecutorState>,
     43 }
     44 
     45 impl MockExecutor {
     46     fn new() -> Self {
     47         Self::default()
     48     }
     49 
     50     fn with_fail_sql(mut self, needle: &str) -> Self {
     51         let state = self.state.get_mut().expect("state");
     52         state.fail_sql_contains = Some(needle.to_string());
     53         self
     54     }
     55 
     56     fn set_fail_begin(&self, value: bool) {
     57         let mut state = self.state.lock().expect("state");
     58         state.fail_begin = value;
     59     }
     60 
     61     fn set_fail_commit(&self, value: bool) {
     62         let mut state = self.state.lock().expect("state");
     63         state.fail_commit = value;
     64     }
     65 
     66     fn set_fail_rollback(&self, value: bool) {
     67         let mut state = self.state.lock().expect("state");
     68         state.fail_rollback = value;
     69     }
     70 
     71     fn set_query_override(&self, value: Option<Result<String, SqlError>>) {
     72         let mut state = self.state.lock().expect("state");
     73         state.query_override = value;
     74     }
     75 
     76     fn mark_applied(&self, name: &str) {
     77         let mut state = self.state.lock().expect("state");
     78         state.applied.insert(name.to_string());
     79     }
     80 
     81     fn snapshot(&self) -> ExecutorSnapshot {
     82         let state = self.state.lock().expect("state");
     83         ExecutorSnapshot {
     84             exec_sql: state.exec_sql.clone(),
     85             begin_count: state.begin_count,
     86             commit_count: state.commit_count,
     87             rollback_count: state.rollback_count,
     88             applied: state.applied.clone(),
     89         }
     90     }
     91 }
     92 
     93 impl SqlExecutor for MockExecutor {
     94     fn exec(&self, sql: &str, params_json: &str) -> Result<ExecOutcome, SqlError> {
     95         let mut state = self.state.lock().expect("state");
     96         state.exec_sql.push(sql.to_string());
     97         if let Some(needle) = &state.fail_sql_contains {
     98             if sql.contains(needle) {
     99                 return Err(SqlError::InvalidQuery(sql.to_string()));
    100             }
    101         }
    102 
    103         if sql.contains("insert or ignore into __migrations(name)") {
    104             let params: Vec<String> =
    105                 serde_json::from_str(params_json).map_err(|err| SqlError::from(err))?;
    106             if let Some(name) = params.first() {
    107                 state.applied.insert(name.clone());
    108             }
    109         }
    110 
    111         if sql.contains("delete from __migrations where name = ?") {
    112             let params: Vec<String> =
    113                 serde_json::from_str(params_json).map_err(|err| SqlError::from(err))?;
    114             if let Some(name) = params.first() {
    115                 state.applied.remove(name);
    116             }
    117         }
    118 
    119         Ok(ExecOutcome {
    120             changes: 1,
    121             last_insert_id: 11,
    122         })
    123     }
    124 
    125     fn query_raw(&self, _sql: &str, params_json: &str) -> Result<String, SqlError> {
    126         let state = self.state.lock().expect("state");
    127         if let Some(override_value) = &state.query_override {
    128             return override_value.clone();
    129         }
    130         let params: Vec<String> =
    131             serde_json::from_str(params_json).map_err(|err| SqlError::from(err))?;
    132         let Some(name) = params.first() else {
    133             return Ok(String::new());
    134         };
    135         if state.applied.contains(name) {
    136             Ok(json!([{ "applied": 1 }]).to_string())
    137         } else {
    138             Ok("[]".to_string())
    139         }
    140     }
    141 
    142     fn begin(&self) -> Result<(), SqlError> {
    143         let mut state = self.state.lock().expect("state");
    144         state.begin_count += 1;
    145         if state.fail_begin {
    146             return Err(SqlError::Internal);
    147         }
    148         Ok(())
    149     }
    150 
    151     fn commit(&self) -> Result<(), SqlError> {
    152         let mut state = self.state.lock().expect("state");
    153         state.commit_count += 1;
    154         if state.fail_commit {
    155             return Err(SqlError::Internal);
    156         }
    157         Ok(())
    158     }
    159 
    160     fn rollback(&self) -> Result<(), SqlError> {
    161         let mut state = self.state.lock().expect("state");
    162         state.rollback_count += 1;
    163         if state.fail_rollback {
    164             return Err(SqlError::Internal);
    165         }
    166         Ok(())
    167     }
    168 }
    169 
    170 #[test]
    171 fn sql_executor_reference_impl_forwards_all_methods() {
    172     let exec = MockExecutor::new();
    173     let exec_ref = &exec;
    174 
    175     let exec_result = <&MockExecutor as SqlExecutor>::exec(&exec_ref, "select 1", "[]")
    176         .expect("reference exec should forward");
    177     assert_eq!(exec_result.changes, 1);
    178 
    179     let query_result = <&MockExecutor as SqlExecutor>::query_raw(&exec_ref, "select 1", "[]")
    180         .expect("reference query should forward");
    181     assert_eq!(query_result, String::new());
    182 
    183     <&MockExecutor as SqlExecutor>::begin(&exec_ref).expect("reference begin should forward");
    184     <&MockExecutor as SqlExecutor>::commit(&exec_ref).expect("reference commit should forward");
    185     <&MockExecutor as SqlExecutor>::rollback(&exec_ref).expect("reference rollback should forward");
    186 
    187     let snapshot = exec.snapshot();
    188     assert_eq!(snapshot.begin_count, 1);
    189     assert_eq!(snapshot.commit_count, 1);
    190     assert_eq!(snapshot.rollback_count, 1);
    191     assert!(snapshot.exec_sql.iter().any(|sql| sql == "select 1"));
    192 }
    193 
    194 #[cfg(feature = "native")]
    195 #[test]
    196 fn sqlite_executor_exec_runs_multi_statement_batches_without_params() {
    197     let exec = SqliteExecutor::open_memory().expect("open sqlite memory");
    198 
    199     let outcome = exec
    200         .exec(
    201             "create table demo (id integer primary key, name text not null);\ncreate unique index demo_name_idx on demo(name);",
    202             "[]",
    203         )
    204         .expect("multi-statement batch should succeed");
    205     assert_eq!(outcome.changes, 0);
    206 
    207     let insert = exec
    208         .exec("insert into demo(name) values ('alpha')", "[]")
    209         .expect("insert should succeed");
    210     assert_eq!(insert.changes, 1);
    211 
    212     let index_rows = exec
    213         .query_raw(
    214             "select name from sqlite_master where type = 'index' and name = 'demo_name_idx'",
    215             "[]",
    216         )
    217         .expect("index metadata query should succeed");
    218     assert_eq!(index_rows, json!([{ "name": "demo_name_idx" }]).to_string());
    219 }
    220 
    221 #[derive(Debug, Serialize, Deserialize, PartialEq)]
    222 struct Payload {
    223     id: String,
    224     amount: Option<i64>,
    225 }
    226 
    227 #[derive(Debug, Clone, Copy)]
    228 enum FilterMode {
    229     Object,
    230     Array,
    231     Error,
    232 }
    233 
    234 #[derive(Debug, Clone)]
    235 struct FilterInput {
    236     mode: FilterMode,
    237     id: Option<String>,
    238     amount: Option<i64>,
    239 }
    240 
    241 impl FilterInput {
    242     fn object(id: Option<&str>, amount: Option<i64>) -> Self {
    243         Self {
    244             mode: FilterMode::Object,
    245             id: id.map(str::to_string),
    246             amount,
    247         }
    248     }
    249 
    250     fn array() -> Self {
    251         Self {
    252             mode: FilterMode::Array,
    253             id: None,
    254             amount: None,
    255         }
    256     }
    257 
    258     fn error() -> Self {
    259         Self {
    260             mode: FilterMode::Error,
    261             id: None,
    262             amount: None,
    263         }
    264     }
    265 }
    266 
    267 impl Serialize for FilterInput {
    268     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    269     where
    270         S: Serializer,
    271     {
    272         match self.mode {
    273             FilterMode::Error => Err(serde::ser::Error::custom("serialize fail")),
    274             FilterMode::Array => {
    275                 let mut seq = serializer.serialize_seq(Some(2))?;
    276                 seq.serialize_element(&1)?;
    277                 seq.serialize_element(&2)?;
    278                 seq.end()
    279             }
    280             FilterMode::Object => {
    281                 let mut map = serializer.serialize_map(Some(2))?;
    282                 map.serialize_entry("id", &self.id)?;
    283                 map.serialize_entry("amount", &self.amount)?;
    284                 map.end()
    285             }
    286         }
    287     }
    288 }
    289 
    290 #[test]
    291 fn sql_error_code_and_to_json_cover_all_variants() {
    292     let errors = vec![
    293         SqlError::InvalidArgument("a".to_string()),
    294         SqlError::NotFound("b".to_string()),
    295         SqlError::SerializationError("c".to_string()),
    296         SqlError::InvalidQuery("d".to_string()),
    297         SqlError::Internal,
    298         SqlError::UnsupportedPlatform,
    299     ];
    300     let expected = vec![
    301         "ERR_INVALID_ARGUMENT",
    302         "ERR_NOT_FOUND",
    303         "ERR_SERIALIZATION",
    304         "ERR_INVALID_QUERY",
    305         "ERR_INTERNAL",
    306         "ERR_UNSUPPORTED_PLATFORM",
    307     ];
    308 
    309     for (err, code) in errors.into_iter().zip(expected.into_iter()) {
    310         assert_eq!(err.code(), code);
    311         let json_value = err.to_json();
    312         assert_eq!(json_value.get("code").and_then(|v| v.as_str()), Some(code));
    313         assert!(json_value.get("message").and_then(|v| v.as_str()).is_some());
    314     }
    315 }
    316 
    317 #[test]
    318 fn parse_json_and_identifiers_work() {
    319     let parsed: Payload = parse_json(r#"{"id":"p1","amount":3}"#).expect("payload should parse");
    320     assert_eq!(
    321         parsed,
    322         Payload {
    323             id: "p1".to_string(),
    324             amount: Some(3),
    325         }
    326     );
    327 
    328     let err = parse_json::<Payload>("not-json").expect_err("invalid json should fail");
    329     assert!(matches!(err, SqlError::SerializationError(_)));
    330 
    331     let first = uuidv4();
    332     let second = uuidv4();
    333     assert_ne!(first, second);
    334     assert_eq!(first.len(), 36);
    335 
    336     let created_on = time_created_on();
    337     assert!(created_on.ends_with('Z'));
    338 }
    339 
    340 #[test]
    341 fn object_map_helpers_cover_success_and_error_paths() {
    342     let payload = FilterInput::object(Some("row-1"), Some(8));
    343     let object = to_object_map(payload).expect("to object map");
    344     assert_eq!(object.get("id"), Some(&Value::String("row-1".to_string())));
    345 
    346     let err = to_object_map(FilterInput::array()).expect_err("array should fail");
    347     assert!(matches!(err, SqlError::SerializationError(_)));
    348     let err = to_object_map(FilterInput::error()).expect_err("serialize fail should surface");
    349     assert!(matches!(err, SqlError::SerializationError(_)));
    350 
    351     let partial =
    352         to_partial_object_map(FilterInput::object(Some("row-2"), None)).expect("to partial map");
    353     assert_eq!(partial.get("id"), Some(&Value::String("row-2".to_string())));
    354     assert!(!partial.contains_key("amount"));
    355 
    356     let err_partial = to_partial_object_map(FilterInput::array()).expect_err("array should fail");
    357     assert!(matches!(err_partial, SqlError::SerializationError(_)));
    358     let err_partial =
    359         to_partial_object_map(FilterInput::error()).expect_err("serialize fail should surface");
    360     assert!(matches!(err_partial, SqlError::SerializationError(_)));
    361 }
    362 
    363 #[test]
    364 fn bind_value_helpers_cover_all_value_paths() {
    365     assert_eq!(to_db_bind_value(&Value::Bool(true)), Value::from(1));
    366     assert_eq!(to_db_bind_value(&Value::Bool(false)), Value::from(0));
    367     assert_eq!(to_db_bind_value(&json!(5_i64)), Value::from(5_u32));
    368     assert_eq!(to_db_bind_value(&json!(-5_i64)), Value::from(-5_i64));
    369     assert_eq!(to_db_bind_value(&json!(7.25_f64)), Value::from(7.25_f64));
    370     assert_eq!(
    371         to_db_bind_value(&json!(u32::MAX as u64)),
    372         Value::from(u32::MAX)
    373     );
    374     assert_eq!(
    375         to_db_bind_value(&json!((u32::MAX as u64) + 1)),
    376         Value::from((u32::MAX as u64) + 1)
    377     );
    378     assert_eq!(
    379         to_db_bind_value(&Value::String("x".to_string())),
    380         Value::String("x".to_string())
    381     );
    382     assert_eq!(to_db_bind_value(&json!({"x":1})), Value::Null);
    383 }
    384 
    385 #[test]
    386 fn query_builder_helpers_cover_empty_and_non_empty_paths() {
    387     let empty_filter = FilterInput::object(None, None);
    388     let (where_empty, binds_empty) = build_where_clause_eq(&empty_filter).expect("where empty");
    389     assert_eq!(where_empty, "");
    390     assert!(binds_empty.is_empty());
    391 
    392     let err_filter = FilterInput::error();
    393     let err = build_where_clause_eq(&err_filter).expect_err("where error");
    394     assert!(matches!(err, SqlError::SerializationError(_)));
    395 
    396     let mut fields = Map::new();
    397     fields.insert("name".to_string(), Value::String("alpha".to_string()));
    398     fields.insert("weight".to_string(), Value::from(12));
    399     let (insert_sql, insert_binds) = build_insert_query_with_meta(
    400         "items",
    401         &[("uuid", Value::String("u-1".to_string()))],
    402         &fields,
    403     );
    404     assert!(insert_sql.contains("INSERT INTO items"));
    405     assert_eq!(insert_binds.len(), 3);
    406 
    407     let (select_all, select_binds_all) = build_select_query_with_meta::<FilterInput>("items", None);
    408     assert_eq!(select_all, "SELECT * FROM items;");
    409     assert!(select_binds_all.is_empty());
    410 
    411     let filter = FilterInput::object(Some("row-3"), Some(10));
    412     let (select_filtered, select_binds_filtered) =
    413         build_select_query_with_meta("items", Some(&filter));
    414     assert!(select_filtered.contains(" WHERE "));
    415     assert_eq!(select_binds_filtered.len(), 2);
    416 
    417     let array_filter = FilterInput::array();
    418     let (select_error_path, select_error_binds) =
    419         build_select_query_with_meta("items", Some(&array_filter));
    420     assert_eq!(select_error_path, "SELECT * FROM items;");
    421     assert!(select_error_binds.is_empty());
    422 }
    423 
    424 #[test]
    425 fn parse_query_and_params_helpers_cover_success_and_error_paths() {
    426     assert_eq!(
    427         parse_query_value(&Value::Bool(true)).expect("bool true"),
    428         json!(1)
    429     );
    430     assert_eq!(
    431         parse_query_value(&Value::Bool(false)).expect("bool false"),
    432         json!(0)
    433     );
    434     assert_eq!(parse_query_value(&Value::Null).expect("null"), Value::Null);
    435     assert_eq!(parse_query_value(&json!(7)).expect("number"), json!(7));
    436     assert_eq!(
    437         parse_query_value(&Value::String("ok".to_string())).expect("string"),
    438         Value::String("ok".to_string())
    439     );
    440 
    441     let err = parse_query_value(&json!({"bad": true})).expect_err("object should fail");
    442     assert!(matches!(err, SqlError::InvalidArgument(_)));
    443 
    444     let params_json = to_params_json(FilterInput::object(Some("a"), Some(1))).expect("params json");
    445     let params_value: Value = serde_json::from_str(&params_json).expect("params json parse");
    446     assert_eq!(params_value, json!({"id":"a","amount":1}));
    447 
    448     let err_params =
    449         to_params_json(FilterInput::error()).expect_err("serialize fail should surface");
    450     assert!(matches!(err_params, SqlError::SerializationError(_)));
    451 }
    452 
    453 #[test]
    454 fn with_transaction_covers_commit_and_rollback_paths() {
    455     let ok_exec = MockExecutor::new();
    456     let value = with_transaction(&ok_exec, || Ok::<_, SqlError>(41)).expect("tx should commit");
    457     assert_eq!(value, 41);
    458     let ok_snapshot = ok_exec.snapshot();
    459     assert_eq!(ok_snapshot.begin_count, 1);
    460     assert_eq!(ok_snapshot.commit_count, 1);
    461     assert_eq!(ok_snapshot.rollback_count, 0);
    462 
    463     let err_exec = MockExecutor::new();
    464     let err = with_transaction(&err_exec, || {
    465         Err::<i32, SqlError>(SqlError::InvalidQuery("bad".to_string()))
    466     })
    467     .expect_err("tx should rollback");
    468     assert!(matches!(err, SqlError::InvalidQuery(_)));
    469     let err_snapshot = err_exec.snapshot();
    470     assert_eq!(err_snapshot.begin_count, 1);
    471     assert_eq!(err_snapshot.commit_count, 0);
    472     assert_eq!(err_snapshot.rollback_count, 1);
    473 
    474     let rollback_err_exec = MockExecutor::new();
    475     rollback_err_exec.set_fail_rollback(true);
    476     let _ = with_transaction(&rollback_err_exec, || {
    477         Err::<i32, SqlError>(SqlError::InvalidQuery("err".to_string()))
    478     })
    479     .expect_err("tx should still return original error");
    480     let rollback_snapshot = rollback_err_exec.snapshot();
    481     assert_eq!(rollback_snapshot.rollback_count, 1);
    482 }
    483 
    484 #[test]
    485 fn with_transaction_surfaces_begin_error() {
    486     let exec = MockExecutor::new();
    487     exec.set_fail_begin(true);
    488     let err = with_transaction(&exec, || Ok::<_, SqlError>(1)).expect_err("begin should fail");
    489     assert!(matches!(err, SqlError::Internal));
    490 }
    491 
    492 #[test]
    493 fn with_transaction_surfaces_commit_error() {
    494     let exec = MockExecutor::new();
    495     exec.set_fail_commit(true);
    496     let err = with_transaction(&exec, || Ok::<_, SqlError>(1)).expect_err("commit should fail");
    497     assert!(matches!(err, SqlError::Internal));
    498 }
    499 
    500 fn sample_migrations() -> Vec<Migration> {
    501     vec![
    502         Migration {
    503             name: "001",
    504             up_sql: "create table m1(x integer)",
    505             down_sql: "drop table m1",
    506         },
    507         Migration {
    508             name: "002",
    509             up_sql: "create table m2(y integer)",
    510             down_sql: "drop table m2",
    511         },
    512     ]
    513 }
    514 
    515 #[test]
    516 fn migrations_run_all_up_applies_pending_and_skips_existing() {
    517     let exec = MockExecutor::new();
    518     let migrations = sample_migrations();
    519 
    520     migrations_run_all_up(&exec, &migrations).expect("first run up");
    521     migrations_run_all_up(&exec, &migrations).expect("second run up");
    522 
    523     let snapshot = exec.snapshot();
    524     assert!(snapshot.applied.contains("001"));
    525     assert!(snapshot.applied.contains("002"));
    526     let up_calls = snapshot
    527         .exec_sql
    528         .iter()
    529         .filter(|sql| sql.starts_with("create table m"))
    530         .count();
    531     assert_eq!(up_calls, 2);
    532 }
    533 
    534 #[test]
    535 fn migrations_run_all_up_surfaces_ensure_table_error() {
    536     let exec = MockExecutor::new().with_fail_sql("create table if not exists __migrations");
    537     let migrations = sample_migrations();
    538     let err = migrations_run_all_up(&exec, &migrations).expect_err("ensure table should fail");
    539     assert!(matches!(err, SqlError::InvalidQuery(_)));
    540 }
    541 
    542 #[test]
    543 fn migrations_run_all_up_surfaces_begin_error() {
    544     let exec = MockExecutor::new();
    545     exec.set_fail_begin(true);
    546     let migrations = sample_migrations();
    547     let err = migrations_run_all_up(&exec, &migrations).expect_err("begin should fail");
    548     assert!(matches!(err, SqlError::Internal));
    549 }
    550 
    551 #[test]
    552 fn migrations_run_all_up_surfaces_commit_error() {
    553     let exec = MockExecutor::new();
    554     exec.set_fail_commit(true);
    555     let migrations = sample_migrations();
    556     let err = migrations_run_all_up(&exec, &migrations).expect_err("commit should fail");
    557     assert!(matches!(err, SqlError::Internal));
    558 }
    559 
    560 #[test]
    561 fn migrations_run_all_up_surfaces_mark_applied_error() {
    562     let exec = MockExecutor::new().with_fail_sql("insert or ignore into __migrations");
    563     let migrations = sample_migrations();
    564     let err = migrations_run_all_up(&exec, &migrations).expect_err("mark applied should fail");
    565     assert!(matches!(err, SqlError::InvalidQuery(_)));
    566 }
    567 
    568 #[test]
    569 fn migrations_run_all_up_rolls_back_on_failure() {
    570     let exec = MockExecutor::new().with_fail_sql("create table m2");
    571     let migrations = sample_migrations();
    572 
    573     let err = migrations_run_all_up(&exec, &migrations).expect_err("second migration should fail");
    574     assert!(matches!(err, SqlError::InvalidQuery(_)));
    575 
    576     let snapshot = exec.snapshot();
    577     assert!(snapshot.applied.contains("001"));
    578     assert!(!snapshot.applied.contains("002"));
    579     assert!(snapshot.rollback_count >= 1);
    580 }
    581 
    582 #[test]
    583 fn migrations_run_all_up_surfaces_query_parse_error() {
    584     let exec = MockExecutor::new();
    585     exec.set_query_override(Some(Ok("not-json".to_string())));
    586     let migrations = sample_migrations();
    587     let err = migrations_run_all_up(&exec, &migrations).expect_err("query parse should fail");
    588     assert!(matches!(err, SqlError::SerializationError(_)));
    589 }
    590 
    591 #[test]
    592 fn migrations_run_all_up_surfaces_query_error() {
    593     let exec = MockExecutor::new();
    594     exec.set_query_override(Some(Err(SqlError::Internal)));
    595     let migrations = sample_migrations();
    596     let err = migrations_run_all_up(&exec, &migrations).expect_err("query should fail");
    597     assert!(matches!(err, SqlError::Internal));
    598 }
    599 
    600 #[test]
    601 fn migrations_run_all_up_handles_empty_query_rows() {
    602     let exec = MockExecutor::new();
    603     exec.set_query_override(Some(Ok(String::new())));
    604     let migrations = sample_migrations();
    605     migrations_run_all_up(&exec, &migrations).expect("empty rows should count as not applied");
    606     let snapshot = exec.snapshot();
    607     assert!(snapshot.applied.contains("001"));
    608     assert!(snapshot.applied.contains("002"));
    609 }
    610 
    611 #[test]
    612 fn migrations_run_all_down_reverses_and_commits() {
    613     let exec = MockExecutor::new();
    614     exec.mark_applied("001");
    615     exec.mark_applied("002");
    616 
    617     let migrations = sample_migrations();
    618     migrations_run_all_down(&exec, &migrations).expect("run down");
    619 
    620     let snapshot = exec.snapshot();
    621     assert!(!snapshot.applied.contains("001"));
    622     assert!(!snapshot.applied.contains("002"));
    623     assert!(snapshot.commit_count >= 1);
    624     let down_calls: Vec<&String> = snapshot
    625         .exec_sql
    626         .iter()
    627         .filter(|sql| sql.starts_with("drop table"))
    628         .collect();
    629     assert_eq!(down_calls.len(), 2);
    630     assert_eq!(down_calls[0].as_str(), "drop table m2");
    631     assert_eq!(down_calls[1].as_str(), "drop table m1");
    632 }
    633 
    634 #[test]
    635 fn migrations_run_all_down_surfaces_ensure_table_error() {
    636     let exec = MockExecutor::new().with_fail_sql("create table if not exists __migrations");
    637     let migrations = sample_migrations();
    638     let err = migrations_run_all_down(&exec, &migrations).expect_err("ensure table should fail");
    639     assert!(matches!(err, SqlError::InvalidQuery(_)));
    640 }
    641 
    642 #[test]
    643 fn migrations_run_all_down_surfaces_delete_error() {
    644     let exec = MockExecutor::new().with_fail_sql("delete from __migrations");
    645     let migrations = sample_migrations();
    646     let err = migrations_run_all_down(&exec, &migrations).expect_err("delete should fail");
    647     assert!(matches!(err, SqlError::InvalidQuery(_)));
    648 }
    649 
    650 #[test]
    651 fn migrations_run_all_down_surfaces_down_sql_error() {
    652     let exec = MockExecutor::new().with_fail_sql("drop table m2");
    653     let migrations = sample_migrations();
    654     let err = migrations_run_all_down(&exec, &migrations).expect_err("down sql should fail");
    655     assert!(matches!(err, SqlError::InvalidQuery(_)));
    656 }
    657 
    658 #[test]
    659 fn migrations_run_all_down_surfaces_begin_error() {
    660     let exec = MockExecutor::new();
    661     exec.set_fail_begin(true);
    662     let migrations = sample_migrations();
    663     let err = migrations_run_all_down(&exec, &migrations).expect_err("begin should fail");
    664     assert!(matches!(err, SqlError::Internal));
    665 }
    666 
    667 #[test]
    668 fn migrations_run_all_down_surfaces_commit_error() {
    669     let exec = MockExecutor::new();
    670     exec.set_fail_commit(true);
    671     let migrations = sample_migrations();
    672     let err = migrations_run_all_down(&exec, &migrations).expect_err("commit should fail");
    673     assert!(matches!(err, SqlError::Internal));
    674 }