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:
+54
@@ -15,6 +15,7 @@
|
||||
|
||||
use crate::app::EffectiveMode;
|
||||
use crate::dsl::ReferentialAction;
|
||||
use crate::dsl::types::Type;
|
||||
use crate::dsl::Command;
|
||||
use crate::dsl::command::{
|
||||
ColumnSpec, CompareOp, Constraint, ConstraintKind, Expr, Operand, Predicate, RowFilter,
|
||||
@@ -286,6 +287,31 @@ pub(crate) fn render_add_relationship(
|
||||
s
|
||||
}
|
||||
|
||||
/// The advanced-mode DSL→SQL teaching echo (ADR-0038) for `create m:n
|
||||
/// relationship` (ADR-0045): the single `CREATE TABLE` the junction
|
||||
/// expands to — every FK column, the compound primary key over them,
|
||||
/// and the two `CASCADE` foreign keys (m:n always cascades, D2). Built
|
||||
/// from the post-exec junction description (the resolved columns don't
|
||||
/// exist on the command), so it shows exactly what was created.
|
||||
pub(crate) fn render_create_m2n(
|
||||
junction: &str,
|
||||
columns: &[(String, Type)],
|
||||
primary_key: &[String],
|
||||
foreign_keys: &[(Vec<String>, String, Vec<String>)],
|
||||
) -> String {
|
||||
let mut parts: Vec<String> =
|
||||
columns.iter().map(|(n, ty)| format!("{n} {}", ty.keyword())).collect();
|
||||
parts.push(format!("PRIMARY KEY ({})", primary_key.join(", ")));
|
||||
for (child_columns, parent_table, parent_columns) in foreign_keys {
|
||||
parts.push(format!(
|
||||
"FOREIGN KEY ({}) REFERENCES {parent_table} ({}) ON DELETE CASCADE ON UPDATE CASCADE",
|
||||
child_columns.join(", "),
|
||||
parent_columns.join(", "),
|
||||
));
|
||||
}
|
||||
format!("CREATE TABLE {junction} ({})", parts.join(", "))
|
||||
}
|
||||
|
||||
/// `ALTER TABLE <C> DROP CONSTRAINT <name>` — the `drop relationship`
|
||||
/// echo (ADR-0038 §7 Bucket B). The runtime resolves both `name` (for an
|
||||
/// `Endpoints` selector) and `child_table` (for a `Named` selector) **pre-
|
||||
@@ -1077,6 +1103,34 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_m2n_echo_renders_junction_and_round_trips() {
|
||||
// The advanced-mode teaching echo for `create m:n relationship`
|
||||
// (ADR-0045): the single CREATE TABLE the junction expands to,
|
||||
// compound PK + the two CASCADE FKs — and it is valid SQL.
|
||||
let sql = render_create_m2n(
|
||||
"Students_Courses",
|
||||
&[
|
||||
("Students_id".to_string(), Type::Int),
|
||||
("Courses_id".to_string(), Type::Int),
|
||||
],
|
||||
&["Students_id".to_string(), "Courses_id".to_string()],
|
||||
&[
|
||||
(vec!["Students_id".to_string()], "Students".to_string(), vec!["id".to_string()]),
|
||||
(vec!["Courses_id".to_string()], "Courses".to_string(), vec!["id".to_string()]),
|
||||
],
|
||||
);
|
||||
assert_eq!(
|
||||
sql,
|
||||
"CREATE TABLE Students_Courses (Students_id int, Courses_id int, \
|
||||
PRIMARY KEY (Students_id, Courses_id), \
|
||||
FOREIGN KEY (Students_id) REFERENCES Students (id) ON DELETE CASCADE ON UPDATE CASCADE, \
|
||||
FOREIGN KEY (Courses_id) REFERENCES Courses (id) ON DELETE CASCADE ON UPDATE CASCADE)"
|
||||
);
|
||||
// The echoed SQL is valid advanced-mode SQL (round-trips).
|
||||
assert!(matches!(reparse(&sql), Ok(Command::SqlCreateTable { .. })));
|
||||
}
|
||||
|
||||
// --- expr / literal rendering ------------------------------------
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user