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:
@@ -1362,6 +1362,75 @@ pub static CREATE: CommandNode = CommandNode {
|
||||
help_id: Some("ddl.create"),
|
||||
usage_ids: &["parse.usage.create_table"],};
|
||||
|
||||
// =================================================================
|
||||
// create_m2n — `create m:n relationship from <T1> to <T2> [as <name>]`
|
||||
// (ADR-0045 / C4). Generates an auto-named junction table with two FKs
|
||||
// + two 1:n relationships. A *separate* `CommandNode` under the shared
|
||||
// `create` entry word (the walker dispatches both); the `m` opener is a
|
||||
// `Literal` (not a keyword) so it never shadows an identifier, mirroring
|
||||
// the `1` in `add 1:n relationship`.
|
||||
// =================================================================
|
||||
|
||||
const M2N_T1: Node = Node::Ident {
|
||||
source: IdentSource::Tables,
|
||||
role: "m2n_t1",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
};
|
||||
const M2N_T2: Node = Node::Ident {
|
||||
source: IdentSource::Tables,
|
||||
role: "m2n_t2",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
};
|
||||
// Optional `as <junction name>` — a *new* table name (the junction),
|
||||
// so it reuses `TABLE_NAME_NEW` (role `table_name`, `NewName` source +
|
||||
// hint). The only `table_name` role in this path, so the builder reads
|
||||
// it directly as the junction name.
|
||||
const M2N_AS_NAME_NODES: &[Node] = &[Node::Word(Word::keyword("as")), TABLE_NAME_NEW];
|
||||
const M2N_AS_NAME_OPT: Node = Node::Optional(&Node::Seq(M2N_AS_NAME_NODES));
|
||||
|
||||
const CREATE_M2N_NODES: &[Node] = &[
|
||||
Node::Literal("m"),
|
||||
Node::Punct(':'),
|
||||
Node::Word(Word::keyword("n")),
|
||||
Node::Word(Word::keyword("relationship")),
|
||||
Node::Word(Word::keyword("from")),
|
||||
M2N_T1,
|
||||
Node::Word(Word::keyword("to")),
|
||||
M2N_T2,
|
||||
M2N_AS_NAME_OPT,
|
||||
];
|
||||
const CREATE_M2N_SHAPE: Node = Node::Seq(CREATE_M2N_NODES);
|
||||
|
||||
fn build_create_m2n(path: &MatchedPath, _source: &str) -> Result<Command, ValidationError> {
|
||||
Ok(Command::CreateM2nRelationship {
|
||||
t1: require_ident(path, "m2n_t1")?,
|
||||
t2: require_ident(path, "m2n_t2")?,
|
||||
name: ident(path, "table_name").map(str::to_string),
|
||||
})
|
||||
}
|
||||
|
||||
pub static CREATE_M2N: CommandNode = CommandNode {
|
||||
entry: Word::keyword("create"),
|
||||
shape: CREATE_M2N_SHAPE,
|
||||
ast_builder: build_create_m2n,
|
||||
help_id: Some("ddl.create_m2n"),
|
||||
usage_ids: &["parse.usage.create_m2n"],
|
||||
};
|
||||
|
||||
/// The friendly error for a column type without a preceding name —
|
||||
/// a structural impossibility given the grammar, defended anyway.
|
||||
fn sql_col_type_without_name() -> ValidationError {
|
||||
|
||||
Reference in New Issue
Block a user