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:
claude@clouddev1
2026-05-25 22:07:50 +00:00
parent 5b76315d1e
commit 6ff97f6e20
16 changed files with 1747 additions and 84 deletions
+14 -4
View File
@@ -263,7 +263,10 @@ static TABLE_PK_NODES: &[Node] = &[
},
Node::Punct(')'),
];
const TABLE_PK: Node = Node::Seq(TABLE_PK_NODES);
// `pub(crate)` so `ALTER TABLE … ADD PRIMARY KEY (…)` (ADR-0035 §4g)
// reuses the body — it parses, then the ALTER builder refuses it with a
// specific message (adding a PK to an existing table is unsupported).
pub(crate) const TABLE_PK: Node = Node::Seq(TABLE_PK_NODES);
// Table-level `UNIQUE ( col, … )`. A single column normalises into
// that column's `unique` flag (round-trips via the existing
@@ -292,7 +295,9 @@ static TABLE_UNIQUE_NODES: &[Node] = &[
},
Node::Punct(')'),
];
const TABLE_UNIQUE: Node = Node::Seq(TABLE_UNIQUE_NODES);
// `pub(crate)` so `ALTER TABLE … ADD UNIQUE (…)` (ADR-0035 §4g) reuses
// the same composite-UNIQUE body.
pub(crate) const TABLE_UNIQUE: Node = Node::Seq(TABLE_UNIQUE_NODES);
// Table-level `CHECK ( <expr> )` (ADR-0035 §4a.3) — a multi-column
// CHECK referencing several columns. Same paren-bounded shape as the
@@ -306,7 +311,9 @@ static TABLE_CHECK_NODES: &[Node] = &[
Node::Subgrammar(&sql_expr::SQL_OR_EXPR),
Node::Punct(')'),
];
const TABLE_CHECK: Node = Node::Seq(TABLE_CHECK_NODES);
// `pub(crate)` so `ALTER TABLE … ADD [CONSTRAINT <name>] CHECK (…)`
// (ADR-0035 §4g) reuses the same table-CHECK body.
pub(crate) const TABLE_CHECK: Node = Node::Seq(TABLE_CHECK_NODES);
// Table-level foreign key (ADR-0035 §5, sub-phase 4b):
// `[CONSTRAINT <name>] FOREIGN KEY ( <child col> ) REFERENCES
@@ -356,7 +363,10 @@ static FOREIGN_KEY_BODY_NODES: &[Node] = &[
];
const FOREIGN_KEY_BODY: Node = Node::Seq(FOREIGN_KEY_BODY_NODES);
// `FOREIGN KEY (…) …` — the unnamed table-level FK (auto-named).
const TABLE_FK: Node = FOREIGN_KEY_BODY;
// `pub(crate)` so `ALTER TABLE … ADD [CONSTRAINT <name>] FOREIGN KEY
// (…)` (ADR-0035 §4g) reuses the FK body (the §4g `CONSTRAINT <name>`
// prefix is supplied by the ALTER grammar, not this body).
pub(crate) const TABLE_FK: Node = FOREIGN_KEY_BODY;
// `CONSTRAINT <name> FOREIGN KEY (…) …` — the named table-level FK.
static TABLE_FK_NAMED_NODES: &[Node] = &[
Node::Word(Word::keyword("constraint")),