Files
claude@clouddev1 8bd43ccadf 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.
2026-06-10 14:26:33 +00:00

74 lines
2.6 KiB
Rust

//! Matrix coverage for `create m:n relationship from <T1> to <T2>
//! [as <name>]` (C4 / ADR-0045). Exercises the full typing surface —
//! completion candidates, ambient hint, highlighting, and parse state —
//! at each stage, so a regression in any of those surfaces is caught.
use crate::typing_surface::*;
use rdbms_playground::input_render::InputState;
#[test]
fn after_create_offers_table_and_m2n() {
let schema = schema_multi_table();
let a = assess_at_end("create ", &schema);
assert!(matches!(a.state, InputState::IncompleteAtEof));
// `create` branches to `table` (create table) or the `m:n` composite.
assert_candidate_present(&a, &["table", "m:n"]);
crate::snap!("after_create", a);
}
#[test]
fn m2n_relationship_keyword_sequence_is_incomplete() {
let schema = schema_multi_table();
let a = assess_at_end("create m:n relationship ", &schema);
assert!(matches!(a.state, InputState::IncompleteAtEof));
assert_candidate_present(&a, &["from"]);
crate::snap!("after_relationship_keyword", a);
}
#[test]
fn after_from_offers_table_names() {
let schema = schema_multi_table();
let a = assess_at_end("create m:n relationship from ", &schema);
assert!(matches!(a.state, InputState::IncompleteAtEof));
assert_candidate_present(&a, &["Customers", "Orders"]);
crate::snap!("after_from", a);
}
#[test]
fn after_to_offers_table_names() {
let schema = schema_multi_table();
let a = assess_at_end("create m:n relationship from Customers to ", &schema);
assert!(matches!(a.state, InputState::IncompleteAtEof));
assert_candidate_present(&a, &["Customers", "Orders"]);
crate::snap!("after_to", a);
}
#[test]
fn complete_create_m2n_parses() {
let schema = schema_multi_table();
let a = assess_at_end("create m:n relationship from Customers to Orders", &schema);
assert!(matches!(a.state, InputState::Valid));
assert_eq!(a.parse_result.as_deref(), Ok("CreateM2nRelationship"));
crate::snap!("complete", a);
}
#[test]
fn create_m2n_with_as_name_parses() {
let schema = schema_multi_table();
let a = assess_at_end(
"create m:n relationship from Customers to Orders as CustomerOrders",
&schema,
);
assert!(matches!(a.state, InputState::Valid));
assert_eq!(a.parse_result.as_deref(), Ok("CreateM2nRelationship"));
crate::snap!("with_as_name", a);
}
#[test]
fn after_as_keyword_is_incomplete() {
let schema = schema_multi_table();
let a = assess_at_end("create m:n relationship from Customers to Orders as ", &schema);
assert!(matches!(a.state, InputState::IncompleteAtEof));
crate::snap!("after_as", a);
}