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:
@@ -0,0 +1,73 @@
|
||||
//! 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);
|
||||
}
|
||||
@@ -35,6 +35,7 @@ pub mod create_table;
|
||||
pub mod drop_column;
|
||||
pub mod drop_relationship;
|
||||
pub mod add_relationship;
|
||||
pub mod create_m2n;
|
||||
pub mod index_ops;
|
||||
pub mod constraints;
|
||||
pub mod rename_change_column;
|
||||
@@ -224,6 +225,7 @@ fn command_kind_label(cmd: &rdbms_playground::dsl::Command) -> String {
|
||||
RenameColumn { .. } => "RenameColumn".into(),
|
||||
ChangeColumnType { .. } => "ChangeColumnType".into(),
|
||||
AddRelationship { .. } => "AddRelationship".into(),
|
||||
CreateM2nRelationship { .. } => "CreateM2nRelationship".into(),
|
||||
DropRelationship { .. } => "DropRelationship".into(),
|
||||
AddIndex { .. } => "AddIndex".into(),
|
||||
DropIndex { .. } => "DropIndex".into(),
|
||||
|
||||
+20
@@ -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)",
|
||||
),
|
||||
}
|
||||
+52
@@ -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)",
|
||||
),
|
||||
}
|
||||
+52
@@ -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)",
|
||||
),
|
||||
}
|
||||
+52
@@ -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)",
|
||||
),
|
||||
}
|
||||
+20
@@ -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",
|
||||
),
|
||||
}
|
||||
+20
@@ -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",
|
||||
),
|
||||
}
|
||||
+42
@@ -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)",
|
||||
),
|
||||
}
|
||||
+11
@@ -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,
|
||||
},
|
||||
],
|
||||
|
||||
+3
-2
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user