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
+31 -6
View File
@@ -4,14 +4,15 @@
Accepted. Design agreed with the user (2026-05-24); the approach is
**validated end-to-end by sub-phases 4a / 4a.2 / 4a.3 / 4b / 4c / 4d /
4e / 4f** (`CREATE TABLE` with column- and table-level constraints and
foreign keys, `DROP TABLE [IF EXISTS]`, `CREATE [UNIQUE] INDEX` /
`DROP INDEX [IF EXISTS]`, `ALTER TABLE` add/drop/rename column, and
`ALTER TABLE … ALTER COLUMN TYPE`, implemented 2026-05-25 — plans
4e / 4f / 4g** (`CREATE TABLE` with column- and table-level constraints
and foreign keys, `DROP TABLE [IF EXISTS]`, `CREATE [UNIQUE] INDEX` /
`DROP INDEX [IF EXISTS]`, `ALTER TABLE` add/drop/rename column,
`ALTER TABLE … ALTER COLUMN TYPE`, and `ALTER TABLE` add/drop constraint
+ add foreign key, implemented 2026-05-25 — plans
`docs/plans/20260524-adr-0035-sql-ddl-4a.md`, `…-4a2.md`, `…-4a3.md`,
`docs/plans/20260525-adr-0035-sql-ddl-4b.md`, `…-4c.md`, `…-4d.md`,
`…-4e.md`, `…-4f.md`), so the decision is accepted while the remaining
sub-phases (**4g4i**, §13) continue. This is **Phase 4** of the ADR-0030 roadmap (the
`…-4e.md`, `…-4f.md`, `…-4g.md`), so the decision is accepted while the
remaining sub-phases (**4h4i**, §13) continue. This is **Phase 4** of the ADR-0030 roadmap (the
advanced-mode SQL surface), the peer of ADR-0031 (expression grammar),
ADR-0032 (`SELECT`), and ADR-0033 (DML). It **clarifies ADR-0030 §4**
on how DDL is represented and executed.
@@ -456,6 +457,30 @@ ADR-0033's structure:
exposure too. *(The remaining internal-table guard on
`do_add_constraint` / `do_add_relationship` rides in 4g.)*
- **4g — `ALTER TABLE` add/drop constraint, add foreign key.**
*(Implemented 2026-05-25 — plan
`docs/plans/20260525-adr-0035-sql-ddl-4g.md`.)* `ALTER TABLE <T> ADD
[CONSTRAINT <name>] (CHECK (…) | UNIQUE (…) | FOREIGN KEY (…)
REFERENCES …)` and `DROP CONSTRAINT <name>`. **ADD scope (user-
confirmed):** CHECK + composite UNIQUE + FK; `ADD PRIMARY KEY` is
refused (every table already has a PK) and a **named UNIQUE** is
refused (composite UNIQUE is anonymous in our model — PRAGMA-detected,
§4a.2). Each ADD reuses an existing low-level path: table-CHECK and
composite-UNIQUE rebuild the table (dry-run guards reject existing
rows that would violate), FK decomposes to `add_relationship` (the
same machinery `add 1:n relationship` uses — bare `REFERENCES <P>`
resolves to the parent's single PK; `create_fk = false` as the column
must exist). **DROP CONSTRAINT (user-confirmed)** resolves the name to
a named table-CHECK then a named FK whose child is `<T>`, else refuses.
**Named table-CHECK round-trip (user-confirmed):** the `CHECK_TABLE`
metadata gains a nullable `name` column (**rebuild-only** arrival — a
pre-4g project gains it on `rebuild`; a named CHECK add on an
un-upgraded project is refused with a friendly "rebuild first"
message), and `project.yaml`'s `check_constraints` is **extended** to
carry the name (`{expr, name}` mapping; the bare-string form still
reads, name = `None`) so a named CHECK survives a rebuild — `rebuild`
reconstructs from the yaml. The internal-`__rdbms_*` guard was folded
into `do_add_constraint` / `do_add_relationship`, completing the
4d/4e/4f guard class. One undo step per statement.
- **4h — `ALTER TABLE … RENAME TO`** (the §6 new low-level op).
- **4i — Verification sweep.** Typing-surface + matrix coverage,
engine-neutral error pass, undo-parity check (one step per