db: end-to-end tests for change_column int -> bool (B2)

The (Int, Bool) entry of the ADR-0017 §3 matrix was already
covered at the per-cell unit-test level in `type_change.rs`,
but the end-to-end change_column path through `db.rs` had no
test exercising it. This closes that gap with the two cases
called out in the handoff:

- `change_column_type_int_to_bool_with_zero_one_succeeds`:
  Rows 0/1/0 succeed, no [client-side] note. The matrix
  returns the same Value::Integer for 0 and 1, so
  is_non_identity reports false for every cell and
  ClientSideNote.transformed stays at 0 — the
  `transformed > 0 || auto_filled > 0` filter therefore
  drops the note.
- `change_column_type_int_to_bool_refuses_other_values`:
  Row with 2 → Incompatible. Verified under both Default
  and ForceConversion modes (per ADR-0017 §5: incompatible
  is not lossy, --force-conversion must not advertise).

No production code change; tests only. 534 -> 536 passing,
clippy clean with nursery lints enabled.
This commit is contained in:
claude@clouddev1
2026-05-08 14:49:34 +00:00
parent dcfeef5d3c
commit 0d7a7bcd49
+101
View File
@@ -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] #[tokio::test]
async fn change_column_type_dont_convert_skips_client_side() { async fn change_column_type_dont_convert_skips_client_side() {
// text -> int: under the per-cell matrix, "1"/"2"/"3" // text -> int: under the per-cell matrix, "1"/"2"/"3"