diff --git a/src/db.rs b/src/db.rs index 61df977..900788c 100644 --- a/src/db.rs +++ b/src/db.rs @@ -5282,6 +5282,107 @@ mod tests { } } + #[tokio::test] + async fn change_column_type_int_to_bool_with_zero_one_succeeds() { + // ADR-0017 §3 matrix: (Int, Bool) is per-cell-classified. + // Values 0/1 are Clean (storage class doesn't change); the + // transformer returns Value::Integer(0)/(1) unchanged, so + // is_non_identity is false for every cell. No + // [client-side] note is expected. + let db = db(); + make_id_table(&db, "T").await; + db.add_column("T".to_string(), "Flag".to_string(), Type::Int, None) + .await + .unwrap(); + for v in ["0", "1", "0"] { + db.insert( + "T".to_string(), + Some(vec!["Flag".to_string()]), + vec![Value::Number(v.to_string())], + None, + ) + .await + .unwrap(); + } + let result = db + .change_column_type( + "T".to_string(), + "Flag".to_string(), + Type::Bool, + ChangeColumnMode::Default, + None, + ) + .await + .expect("int -> bool with all 0/1 values should succeed"); + let flag = result + .description + .columns + .iter() + .find(|c| c.name == "Flag") + .unwrap(); + assert_eq!(flag.user_type, Some(Type::Bool)); + assert!( + result.client_side.is_none(), + "int -> bool with values that map identity should not fire a client-side note: {:?}", + result.client_side + ); + // Data preserved. + let data = db.query_data("T".to_string(), None).await.unwrap(); + assert_eq!(data.rows.len(), 3); + } + + #[tokio::test] + async fn change_column_type_int_to_bool_refuses_other_values() { + // ADR-0017 §3 matrix: (Int, Bool) classifies any value + // other than 0/1 as Incompatible. A single offending row + // triggers a refusal, and --force-conversion does not + // help (incompatible is not lossy). + let db = db(); + make_id_table(&db, "T").await; + db.add_column("T".to_string(), "Flag".to_string(), Type::Int, None) + .await + .unwrap(); + for v in ["0", "1", "2"] { + db.insert( + "T".to_string(), + Some(vec!["Flag".to_string()]), + vec![Value::Number(v.to_string())], + None, + ) + .await + .unwrap(); + } + for mode in [ChangeColumnMode::Default, ChangeColumnMode::ForceConversion] { + let err = db + .change_column_type( + "T".to_string(), + "Flag".to_string(), + Type::Bool, + mode, + None, + ) + .await + .unwrap_err(); + match err { + DbError::Unsupported(message) => { + assert!( + message.contains("cannot be converted"), + "expected incompatible header for {mode:?}: {message}" + ); + assert!( + message.contains("not 0 or 1"), + "expected per-cell reason naming the offending value for {mode:?}: {message}" + ); + assert!( + !message.contains("--force-conversion"), + "incompatible refusal must NOT advertise --force-conversion for {mode:?}: {message}" + ); + } + other => panic!("unexpected ({mode:?}): {other:?}"), + } + } + } + #[tokio::test] async fn change_column_type_dont_convert_skips_client_side() { // text -> int: under the per-cell matrix, "1"/"2"/"3"