fix: ADR-0035 4g — reconstruct table-CHECK metadata on rebuild
do_rebuild_from_text re-emitted table-level CHECKs into the recreated DDL (so they stayed enforced) but never repopulated __rdbms_playground_ table_checks. A fresh rebuild (missing .db, reconstructed from project.yaml) therefore left the CHECK metadata empty: DROP CONSTRAINT, describe, and a later save would lose it — including a named CHECK's name. In-place rebuilds only worked because the wipe never touched the table. (Latent since 4a.3 for unnamed checks; exposed by 4g's named round-trip claim.) Rebuild now wipes and repopulates CHECK_TABLE from the yaml snapshot (name + seq + expr), like META/REL, and adds the 4g `name` column if a pre-4g table predates it (the rebuild-only migration). Regression test: a named CHECK's metadata survives a fresh rebuild (DROP CONSTRAINT by name resolves).
This commit is contained in:
@@ -8643,6 +8643,23 @@ fn do_rebuild_from_text(
|
||||
))
|
||||
.map_err(DbError::from_rusqlite)?;
|
||||
|
||||
// 0b. The table-level CHECK metadata is the source of truth that
|
||||
// the engine cannot report (ADR-0035 §4a.3), so — like
|
||||
// META/REL — it is wiped and repopulated from the YAML
|
||||
// snapshot here (step 3b). Without this a fresh rebuild
|
||||
// (missing `.db`) would enforce the CHECK via the recreated
|
||||
// DDL but leave `CHECK_TABLE` empty, so `describe` / `DROP
|
||||
// CONSTRAINT` / a later save would lose it. The rebuild also
|
||||
// **migrates** a pre-§4g table that predates the `name`
|
||||
// column (the rebuild-only migration, ADR-0035 §4g): add it
|
||||
// if absent before repopulating with names.
|
||||
if !check_table_has_name_column(&tx)? {
|
||||
tx.execute_batch(&format!("ALTER TABLE {CHECK_TABLE} ADD COLUMN name TEXT;"))
|
||||
.map_err(DbError::from_rusqlite)?;
|
||||
}
|
||||
tx.execute_batch(&format!("DELETE FROM {CHECK_TABLE};"))
|
||||
.map_err(DbError::from_rusqlite)?;
|
||||
|
||||
// 1. Recreate user tables with FK constraints inline.
|
||||
for table in &snapshot.tables {
|
||||
let read_schema = build_read_schema(table, &snapshot.relationships);
|
||||
@@ -8694,6 +8711,29 @@ fn do_rebuild_from_text(
|
||||
}
|
||||
}
|
||||
|
||||
// 3b. Table-level CHECK metadata (ADR-0035 §4a.3 / §4g) — in
|
||||
// declaration order (`seq`), carrying the optional name so a
|
||||
// named CHECK round-trips through the rebuild.
|
||||
{
|
||||
let mut stmt = tx
|
||||
.prepare(&format!(
|
||||
"INSERT INTO {CHECK_TABLE} (table_name, seq, check_expr, name) \
|
||||
VALUES (?1, ?2, ?3, ?4);"
|
||||
))
|
||||
.map_err(DbError::from_rusqlite)?;
|
||||
for table in &snapshot.tables {
|
||||
for (seq, check) in table.check_constraints.iter().enumerate() {
|
||||
stmt.execute(rusqlite::params![
|
||||
table.name,
|
||||
seq as i64,
|
||||
check.expr,
|
||||
check.name,
|
||||
])
|
||||
.map_err(DbError::from_rusqlite)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Project metadata: overwrite the configure-time
|
||||
// `created_at` with the YAML's authoritative value.
|
||||
tx.execute(
|
||||
|
||||
@@ -596,3 +596,59 @@ fn e2e_add_constraint_is_one_undo_step() {
|
||||
// After undo the CHECK is gone: qty = -1 is accepted.
|
||||
assert!(insert_t_qty_ok(&db, &r, 3, -1), "one undo removed the CHECK");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn e2e_named_check_metadata_survives_a_fresh_rebuild() {
|
||||
// A FRESH rebuild (deleted .db, reconstructed from project.yaml) must
|
||||
// repopulate the table-CHECK metadata — not just re-emit the CHECK
|
||||
// into the recreated DDL. Otherwise the CHECK is enforced but its
|
||||
// metadata (incl. the name) is lost: `describe` / `DROP CONSTRAINT` /
|
||||
// a later save would drop it (ADR-0035 §4g; fixes a latent 4a.3 gap).
|
||||
use rdbms_playground::dsl::ColumnSpec;
|
||||
use rdbms_playground::project::PLAYGROUND_DB;
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let r = rt();
|
||||
let project_path = {
|
||||
let project = project::open_or_create(None, Some(dir.path())).expect("open");
|
||||
let path = project.path().to_path_buf();
|
||||
let db = Database::open_with_persistence(project.db_path(), Persistence::new(path.clone()))
|
||||
.expect("db");
|
||||
r.block_on(db.sql_create_table(
|
||||
"T".to_string(),
|
||||
vec![ColumnSpec::new("id", Type::Int), ColumnSpec::new("qty", Type::Int)],
|
||||
vec!["id".to_string()],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
false,
|
||||
Some("create table T (id int primary key, qty int)".to_string()),
|
||||
))
|
||||
.expect("create");
|
||||
r.block_on(db.alter_add_table_check(
|
||||
"T".to_string(),
|
||||
Some("qty_positive".to_string()),
|
||||
"qty >= 0".to_string(),
|
||||
Some("alter table T add constraint qty_positive check (qty >= 0)".to_string()),
|
||||
))
|
||||
.expect("add named check");
|
||||
drop(db);
|
||||
path
|
||||
};
|
||||
// Delete the .db → the next open + rebuild reconstructs from yaml.
|
||||
std::fs::remove_file(project_path.join(PLAYGROUND_DB)).unwrap();
|
||||
let project = project::Project::open(&project_path).unwrap();
|
||||
let db = Database::open_with_persistence(
|
||||
project.db_path(),
|
||||
Persistence::new(project.path().to_path_buf()),
|
||||
)
|
||||
.unwrap();
|
||||
r.block_on(db.rebuild_from_text(project.path().to_path_buf(), None)).expect("rebuild");
|
||||
|
||||
// The named CHECK metadata survived: DROP CONSTRAINT by name resolves.
|
||||
r.block_on(db.alter_drop_constraint(
|
||||
"T".to_string(),
|
||||
"qty_positive".to_string(),
|
||||
Some("drop".to_string()),
|
||||
))
|
||||
.expect("DROP CONSTRAINT after a fresh rebuild — the CHECK metadata was reconstructed");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user