feat: create m:n relationship convenience command (C4, ADR-0045)

`create m:n relationship from <T1> to <T2> [as <name>]` generates a
junction table with one FK column per parent PK column ({table}_{pkcol},
typed via fk_target_type), a compound PK over them, and two CASCADE 1:n
relationships -- all in one do_create_table call = one undo step.
Auto-named {T1}_{T2} (optional `as`), both modes, compound-parent PKs
supported (ADR-0043). Self-referential m:n / PK-less parent / internal
junction name / name collision all refused.

Wired across every surface: grammar (separate CREATE_M2N node), worker
executor, runtime dispatch, completion ("m:n" composite), hints,
highlighting, help + usage catalog + disambiguator, and the advanced-mode
DSL->SQL teaching echo (render_create_m2n, round-trips as valid SQL).

Generalized/fixed framework assumptions the build + two /runda passes
surfaced (all behaviour-preserving for existing commands):
- simple-mode dispatch committed simple.first() unconditionally -> tries
  candidates, so `create table` no longer shadows `create m:n`.
- the completion continuation-merge was advanced-only -> runs in simple
  mode too when an entry word has >1 DSL form (gated simple_count>1).
- do_create_table now rejects internal `__rdbms_*` names (closes a
  pre-existing hole on the DSL create-table path too, not just m:n).
- usage disambiguator now recognizes the `m:n` opener.

Tests: 14 integration (tests/it/m2n.rs), 7 typing-surface matrix, echo /
highlight / usage / internal-name units. Closes C4.
2237 pass / 0 fail / 1 ignored. Clippy clean.
This commit is contained in:
claude@clouddev1
2026-06-10 14:26:33 +00:00
parent e598008ecf
commit 8bd43ccadf
28 changed files with 1273 additions and 26 deletions
+32 -1
View File
@@ -2,7 +2,38 @@
## Status
Accepted (2026-06-10). Closes `requirements.md` **C4**. All four
Accepted (2026-06-10); **implemented 2026-06-10**. Closes
`requirements.md` **C4**.
**Implementation note — a corrected ADR premise.** The plan claimed
"the walker already dispatches multiple nodes per entry word" and used
that to justify a *separate* `CREATE_M2N` node. That is true only in
**advanced** mode. The build hit **two** places hard-coded to assume
**≤1 DSL form per entry word in simple mode**: (1) the dispatcher
(`decide`) committed `simple.first()` unconditionally, so `create
table` shadowed `create m:n`; (2) the completion continuation-merge was
gated `if mode == Advanced`, so simple mode never surfaced `m:n` as a
candidate. Both were generalized to support multiple DSL forms per
entry word — **behaviour-preserving for every existing single-form
command** (the dispatch reduces to the old single-candidate commit; the
completion merge is gated on `simple_count > 1`). Verified: zero ripple
beyond the new command's own surfaces. The teaching echo (advanced-mode
DSL→SQL, ADR-0038) was also wired: `render_create_m2n` emits the
generated `CREATE TABLE … FOREIGN KEY …` from the post-exec junction
description (round-trips as valid SQL).
A second `/runda` DA pass (pre-commit) closed five coverage gaps
(highlighting, persistence round-trip, junction rename, name-collision,
missing-parent — the first two had been wrongly claimed verified) and
found two more issues: (a) `create m:n … as __rdbms_*` was accepted —
a hidden-orphan hole the new `as` slot **exposed**, but rooted in the
simple-mode `TABLE_NAME_NEW` slot having no internal-name guard (so
plain `create table __rdbms_*` had it too). Fixed at the **root**
a `reject_internal_table_name` guard in the shared `do_create_table`,
closing every path (the advanced-SQL path already rejected at parse).
(b) the usage disambiguator (`usage_key_for_input`) handled the `1:n`
opener but not `m:n`, so `create m:n …` resolved to no usage form —
fixed with an explicit `m:n` branch. All four
design forks were escalated and user-confirmed at the recommended
option (compound-over-FKs junction PK; `CASCADE` actions; auto-name +
optional `as`; both modes). Two follow-up points were also confirmed