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
+16
View File
@@ -277,6 +277,18 @@ pub enum Command {
on_update: ReferentialAction,
create_fk: bool,
},
/// Convenience: generate a junction table for a many-to-many
/// relationship between `t1` and `t2` (ADR-0045 / C4). The
/// executor builds a table with one FK column per parent PK
/// column (named `{table}_{pkcol}`, typed via `fk_target_type`),
/// a compound PK over all of them, and two `CASCADE` 1:n
/// relationships — all in one `create table` (one undo step).
/// `name` overrides the auto-generated junction name `{t1}_{t2}`.
CreateM2nRelationship {
t1: String,
t2: String,
name: Option<String>,
},
/// Drop a relationship by either user-given/auto-generated
/// name, or by positional reference to the FK endpoints.
DropRelationship {
@@ -915,6 +927,7 @@ impl Command {
Self::RenameColumn { .. } => "rename column",
Self::ChangeColumnType { .. } => "change column",
Self::AddRelationship { .. } => "add relationship",
Self::CreateM2nRelationship { .. } => "create m:n relationship",
Self::DropRelationship { .. } => "drop relationship",
Self::AddIndex { .. } => "add index",
Self::DropIndex { .. } => "drop index",
@@ -991,6 +1004,9 @@ impl Command {
// table's "Referenced by" entry, which is what the
// user looks at to confirm the relationship.
Self::AddRelationship { parent_table, .. } => parent_table,
// For m:n we focus on the first table; the executor builds
// and returns the junction's structure regardless.
Self::CreateM2nRelationship { t1, .. } => t1,
Self::DropRelationship { selector } => match selector {
RelationshipSelector::Endpoints { parent_table, .. } => parent_table,
// For a named drop we don't know the parent table