feat: ADR-0035 4g — ALTER TABLE add/drop constraint + add FK
ALTER TABLE <T> ADD [CONSTRAINT <name>] (CHECK | UNIQUE | FOREIGN KEY)
and DROP CONSTRAINT <name>. ADD = table-CHECK + composite UNIQUE + FK
(ADD PRIMARY KEY and a named UNIQUE refused — composite UNIQUE is
anonymous in our model). Each ADD reuses a low-level path with a dry-run
guard (table-CHECK/UNIQUE rebuild; FK -> add_relationship, bare
REFERENCES -> parent single PK). DROP CONSTRAINT resolves the name to a
named table-CHECK then a child-side FK, else refuses. One undo step each.
Named table-CHECKs round-trip: a nullable `name` column on
__rdbms_playground_table_checks (rebuild-only arrival; a named add on a
pre-4g project is refused with a "rebuild first" hint) plus a project.yaml
check_constraints {expr, name} extension (bare-string form still reads).
The internal-__rdbms_* guard was folded into do_add_constraint /
do_add_relationship, completing that guard class.
Grammar: the action Choice keeps one branch per verb (add/drop/rename/
alter) with an inner Choice fanning out on the distinct second keyword,
since the walker's Choice does not backtrack between same-led branches.
Tests: 7 Tier-1 parse + 2 yaml round-trip + 1 internal-guard + 9 Tier-3
e2e. Help/usage refreshed; ADR-0035 §13 4g + README + requirements.md in
lockstep.
This commit is contained in:
@@ -10,7 +10,8 @@
|
||||
//! rename-drift bug that would break a later rebuild).
|
||||
|
||||
use rdbms_playground::db::Database;
|
||||
use rdbms_playground::dsl::{ChangeColumnMode, ColumnSpec, Type};
|
||||
use rdbms_playground::dsl::command::Constraint;
|
||||
use rdbms_playground::dsl::{ChangeColumnMode, ColumnSpec, ReferentialAction, Type};
|
||||
use rdbms_playground::persistence::Persistence;
|
||||
use rdbms_playground::project;
|
||||
|
||||
@@ -89,7 +90,7 @@ fn simple_column_ops_refuse_internal_tables() {
|
||||
// happen to hit.
|
||||
let err = r
|
||||
.block_on(db.change_column_type(
|
||||
internal,
|
||||
internal.clone(),
|
||||
"table_name".to_string(),
|
||||
Type::Int,
|
||||
ChangeColumnMode::Default,
|
||||
@@ -100,6 +101,79 @@ fn simple_column_ops_refuse_internal_tables() {
|
||||
format!("{err:?}").contains("NoSuchTable"),
|
||||
"expected a no-such-table refusal from the internal-table guard, got: {err:?}"
|
||||
);
|
||||
// `add constraint` (the simple surface; also the SQL `ALTER TABLE …
|
||||
// ADD CONSTRAINT` decomposition target — ADR-0035 §4g) is refused:
|
||||
// the guard lives in `do_add_constraint`.
|
||||
let err = r
|
||||
.block_on(db.add_constraint(
|
||||
internal,
|
||||
"table_name".to_string(),
|
||||
Constraint::NotNull,
|
||||
None,
|
||||
))
|
||||
.expect_err("add constraint on an internal table is refused");
|
||||
assert!(
|
||||
format!("{err:?}").contains("NoSuchTable"),
|
||||
"expected a no-such-table refusal from the internal-table guard, got: {err:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_relationship_refuses_internal_tables() {
|
||||
// The guard lives in `do_add_relationship` (ADR-0035 §4g) and covers
|
||||
// both the parent and the child endpoint — so the simple `add 1:n
|
||||
// relationship` and the SQL `ALTER TABLE … ADD FOREIGN KEY` (which
|
||||
// reaches the same executor) cannot touch an internal table.
|
||||
let (_p, db, _d) = open();
|
||||
let r = rt();
|
||||
let internal = "__rdbms_playground_relationships".to_string();
|
||||
// Internal *parent* — refused up-front.
|
||||
let err = r
|
||||
.block_on(db.add_relationship(
|
||||
None,
|
||||
internal.clone(),
|
||||
"name".to_string(),
|
||||
"C".to_string(),
|
||||
"x".to_string(),
|
||||
ReferentialAction::NoAction,
|
||||
ReferentialAction::NoAction,
|
||||
false,
|
||||
None,
|
||||
))
|
||||
.expect_err("relationship with an internal parent is refused");
|
||||
assert!(
|
||||
format!("{err:?}").contains("NoSuchTable"),
|
||||
"expected a no-such-table refusal (internal parent), got: {err:?}"
|
||||
);
|
||||
// Internal *child* — also refused (a real parent exists).
|
||||
r.block_on(db.sql_create_table(
|
||||
"P".to_string(),
|
||||
vec![ColumnSpec::new("id", Type::Int)],
|
||||
vec!["id".to_string()],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
false,
|
||||
Some("create table P (id int primary key)".to_string()),
|
||||
))
|
||||
.expect("create P");
|
||||
let err = r
|
||||
.block_on(db.add_relationship(
|
||||
None,
|
||||
"P".to_string(),
|
||||
"id".to_string(),
|
||||
internal,
|
||||
"x".to_string(),
|
||||
ReferentialAction::NoAction,
|
||||
ReferentialAction::NoAction,
|
||||
false,
|
||||
None,
|
||||
))
|
||||
.expect_err("relationship with an internal child is refused");
|
||||
assert!(
|
||||
format!("{err:?}").contains("NoSuchTable"),
|
||||
"expected a no-such-table refusal (internal child), got: {err:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user