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
@@ -0,0 +1,20 @@
---
source: tests/typing_surface/create_m2n.rs
assertion_line: 72
description: "input=\"create m:n relationship from Customers to Orders as \" cursor=52"
expression: "& a"
---
Assessment {
input: "create m:n relationship from Customers to Orders as ",
cursor: 52,
state: IncompleteAtEof,
hint: Some(
Prose(
"Type a name",
),
),
completion: None,
parse_result: Err(
"Invalid(at_eof)",
),
}
@@ -0,0 +1,52 @@
---
source: tests/typing_surface/create_m2n.rs
assertion_line: 16
description: "input=\"create \" cursor=7"
expression: "& a"
---
Assessment {
input: "create ",
cursor: 7,
state: IncompleteAtEof,
hint: Some(
Candidates {
items: [
Candidate {
text: "table",
kind: Keyword,
mode: Simple,
},
Candidate {
text: "m:n",
kind: Keyword,
mode: Both,
},
],
selected: None,
},
),
completion: Some(
Completion {
replaced_range: (
7,
7,
),
partial_prefix: "",
candidates: [
Candidate {
text: "table",
kind: Keyword,
mode: Simple,
},
Candidate {
text: "m:n",
kind: Keyword,
mode: Both,
},
],
},
),
parse_result: Err(
"Invalid(at_eof)",
),
}
@@ -0,0 +1,52 @@
---
source: tests/typing_surface/create_m2n.rs
assertion_line: 34
description: "input=\"create m:n relationship from \" cursor=29"
expression: "& a"
---
Assessment {
input: "create m:n relationship from ",
cursor: 29,
state: IncompleteAtEof,
hint: Some(
Candidates {
items: [
Candidate {
text: "Customers",
kind: Identifier,
mode: Both,
},
Candidate {
text: "Orders",
kind: Identifier,
mode: Both,
},
],
selected: None,
},
),
completion: Some(
Completion {
replaced_range: (
29,
29,
),
partial_prefix: "",
candidates: [
Candidate {
text: "Customers",
kind: Identifier,
mode: Both,
},
Candidate {
text: "Orders",
kind: Identifier,
mode: Both,
},
],
},
),
parse_result: Err(
"Invalid(at_eof)",
),
}
@@ -0,0 +1,52 @@
---
source: tests/typing_surface/create_m2n.rs
assertion_line: 43
description: "input=\"create m:n relationship from Customers to \" cursor=42"
expression: "& a"
---
Assessment {
input: "create m:n relationship from Customers to ",
cursor: 42,
state: IncompleteAtEof,
hint: Some(
Candidates {
items: [
Candidate {
text: "Customers",
kind: Identifier,
mode: Both,
},
Candidate {
text: "Orders",
kind: Identifier,
mode: Both,
},
],
selected: None,
},
),
completion: Some(
Completion {
replaced_range: (
42,
42,
),
partial_prefix: "",
candidates: [
Candidate {
text: "Customers",
kind: Identifier,
mode: Both,
},
Candidate {
text: "Orders",
kind: Identifier,
mode: Both,
},
],
},
),
parse_result: Err(
"Invalid(at_eof)",
),
}
@@ -0,0 +1,20 @@
---
source: tests/typing_surface/create_m2n.rs
assertion_line: 52
description: "input=\"create m:n relationship from Customers to Orders\" cursor=48"
expression: "& a"
---
Assessment {
input: "create m:n relationship from Customers to Orders",
cursor: 48,
state: Valid,
hint: Some(
Prose(
"Submit with Enter",
),
),
completion: None,
parse_result: Ok(
"CreateM2nRelationship",
),
}
@@ -0,0 +1,20 @@
---
source: tests/typing_surface/create_m2n.rs
assertion_line: 64
description: "input=\"create m:n relationship from Customers to Orders as CustomerOrders\" cursor=66"
expression: "& a"
---
Assessment {
input: "create m:n relationship from Customers to Orders as CustomerOrders",
cursor: 66,
state: Valid,
hint: Some(
Prose(
"Type a name",
),
),
completion: None,
parse_result: Ok(
"CreateM2nRelationship",
),
}
@@ -0,0 +1,42 @@
---
source: tests/typing_surface/create_m2n.rs
assertion_line: 25
description: "input=\"create m:n relationship \" cursor=24"
expression: "& a"
---
Assessment {
input: "create m:n relationship ",
cursor: 24,
state: IncompleteAtEof,
hint: Some(
Candidates {
items: [
Candidate {
text: "from",
kind: Keyword,
mode: Simple,
},
],
selected: None,
},
),
completion: Some(
Completion {
replaced_range: (
24,
24,
),
partial_prefix: "",
candidates: [
Candidate {
text: "from",
kind: Keyword,
mode: Simple,
},
],
},
),
parse_result: Err(
"Invalid(at_eof)",
),
}
@@ -1,5 +1,6 @@
---
source: tests/typing_surface/create_table.rs
assertion_line: 13
description: "input=\"create \" cursor=7"
expression: "& a"
---
@@ -13,6 +14,11 @@ Assessment {
Candidate {
text: "table",
kind: Keyword,
mode: Simple,
},
Candidate {
text: "m:n",
kind: Keyword,
mode: Both,
},
],
@@ -30,6 +36,11 @@ Assessment {
Candidate {
text: "table",
kind: Keyword,
mode: Simple,
},
Candidate {
text: "m:n",
kind: Keyword,
mode: Both,
},
],
@@ -1,5 +1,6 @@
---
source: tests/typing_surface/create_table.rs
assertion_line: 48
description: "input=\"create table Customers with \" cursor=28"
expression: "& a"
---
@@ -13,7 +14,7 @@ Assessment {
Candidate {
text: "pk",
kind: Keyword,
mode: Both,
mode: Simple,
},
],
selected: None,
@@ -30,7 +31,7 @@ Assessment {
Candidate {
text: "pk",
kind: Keyword,
mode: Both,
mode: Simple,
},
],
},