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)?;
|
.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.
|
// 1. Recreate user tables with FK constraints inline.
|
||||||
for table in &snapshot.tables {
|
for table in &snapshot.tables {
|
||||||
let read_schema = build_read_schema(table, &snapshot.relationships);
|
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
|
// 4. Project metadata: overwrite the configure-time
|
||||||
// `created_at` with the YAML's authoritative value.
|
// `created_at` with the YAML's authoritative value.
|
||||||
tx.execute(
|
tx.execute(
|
||||||
|
|||||||
@@ -596,3 +596,59 @@ fn e2e_add_constraint_is_one_undo_step() {
|
|||||||
// After undo the CHECK is gone: qty = -1 is accepted.
|
// After undo the CHECK is gone: qty = -1 is accepted.
|
||||||
assert!(insert_t_qty_ok(&db, &r, 3, -1), "one undo removed the CHECK");
|
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