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
+22
View File
@@ -1832,6 +1832,24 @@ fn build_schema_echo(
.map(|(name, child_table)| {
vec![crate::echo::render_drop_relationship(name, child_table)]
}),
// `create m:n relationship` (ADR-0045): the resolved junction
// columns/FKs only exist on the post-exec description, so the
// teaching echo is rendered from it (not `command_to_sql`).
Command::CreateM2nRelationship { .. } => description.map(|desc| {
let columns: Vec<(String, crate::dsl::types::Type)> = desc
.columns
.iter()
.filter_map(|c| c.user_type.map(|ty| (c.name.clone(), ty)))
.collect();
let primary_key: Vec<String> =
desc.columns.iter().filter(|c| c.primary_key).map(|c| c.name.clone()).collect();
let foreign_keys: Vec<(Vec<String>, String, Vec<String>)> = desc
.outbound_relationships
.iter()
.map(|r| (r.local_columns.clone(), r.other_table.clone(), r.other_columns.clone()))
.collect();
vec![crate::echo::render_create_m2n(&desc.name, &columns, &primary_key, &foreign_keys)]
}),
// Everything else (Bucket A pure-Command, plus the no-echo Bucket C
// variants like `Sql*` / `ShowTable`) routes through the existing
// `echo::command_to_sql` — wrapping its `Option<String>` to fit the
@@ -2657,6 +2675,10 @@ async fn execute_command_typed(
)
.await
.map(|d| CommandOutcome::Schema(Some(d))),
Command::CreateM2nRelationship { t1, t2, name } => database
.create_m2n_relationship(t1, t2, name, src)
.await
.map(|d| CommandOutcome::Schema(Some(d))),
Command::DropRelationship { selector } => database
.drop_relationship(selector, src)
.await