lib

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

coverage.rs (23119B)


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