diff --git a/src/db.rs b/src/db.rs index e9cc700..5a9b61d 100644 --- a/src/db.rs +++ b/src/db.rs @@ -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( diff --git a/tests/sql_alter_table.rs b/tests/sql_alter_table.rs index 9f7b2c5..3db690b 100644 --- a/tests/sql_alter_table.rs +++ b/tests/sql_alter_table.rs @@ -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"); +}