refactor: relationship model to column lists for compound FK (ADR-0043)
Move the FK column fields String->Vec<String> through all six layers (AddRelationship/SqlForeignKey AST, RelationshipSchema, metadata, project.yaml, ReadForeignKey, RelationshipEnd). Metadata stores comma-joined lists in the existing TEXT cells; project.yaml endpoints now columns: [a, b] (house style). Executor logic is multi-column ready: resolve_fk_parent_columns (full-PK F-A + auto-expand F-D), per-pair type-compat, schema_to_ddl multi-column emission, pragma FK read grouped by id, auto-name + --create-fk per-column, multi-column teaching echo. Single-column behaviour preserved (one-element vecs); all 2181 tests green. The grammar to parse multi-column input lands next.
This commit is contained in:
+22
-7
@@ -785,12 +785,24 @@ fn build_add_relationship(path: &MatchedPath, _source: &str) -> Result<Command,
|
||||
.iter()
|
||||
.any(|i| matches!(&i.kind, MatchedKind::Flag("create-fk")));
|
||||
|
||||
// Collect every matched `parent_column` / `child_column` ident, in
|
||||
// order — one each for the single-column `from P.col to C.col`
|
||||
// form, or the full lists for the parenthesized compound form
|
||||
// `from P.(a, b) to C.(x, y)` (ADR-0043).
|
||||
let parent_columns = collect_idents(path, "parent_column");
|
||||
let child_columns = collect_idents(path, "child_column");
|
||||
if parent_columns.is_empty() || child_columns.is_empty() {
|
||||
return Err(ValidationError {
|
||||
message_key: "parse.error_wrapper",
|
||||
args: vec![("detail", "a relationship needs both endpoints".to_string())],
|
||||
});
|
||||
}
|
||||
Ok(Command::AddRelationship {
|
||||
name: ident(path, "relationship_name").map(str::to_string),
|
||||
parent_table: require_ident(path, "parent_table")?,
|
||||
parent_column: require_ident(path, "parent_column")?,
|
||||
parent_columns,
|
||||
child_table: require_ident(path, "child_table")?,
|
||||
child_column: require_ident(path, "child_column")?,
|
||||
child_columns,
|
||||
on_delete: on_delete.unwrap_or_else(ReferentialAction::default_action),
|
||||
on_update: on_update.unwrap_or_else(ReferentialAction::default_action),
|
||||
create_fk,
|
||||
@@ -1680,9 +1692,12 @@ where
|
||||
}
|
||||
SqlForeignKey {
|
||||
name,
|
||||
child_column,
|
||||
// Single-column for now; the parenthesized multi-column parse
|
||||
// (`FOREIGN KEY (a, b) REFERENCES P(x, y)`) lands with the
|
||||
// grammar-node change (ADR-0043).
|
||||
child_columns: vec![child_column],
|
||||
parent_table,
|
||||
parent_column,
|
||||
parent_columns: parent_column.map(|c| vec![c]),
|
||||
on_delete,
|
||||
on_update,
|
||||
}
|
||||
@@ -3202,9 +3217,9 @@ mod sql_alter_table_tests {
|
||||
assert_eq!(name, None);
|
||||
match *constraint {
|
||||
TableConstraint::ForeignKey(fk) => {
|
||||
assert_eq!(fk.child_column, "pid");
|
||||
assert_eq!(fk.child_columns, vec!["pid".to_string()]);
|
||||
assert_eq!(fk.parent_table, "P");
|
||||
assert_eq!(fk.parent_column.as_deref(), Some("id"));
|
||||
assert_eq!(fk.parent_columns, Some(vec!["id".to_string()]));
|
||||
}
|
||||
other => panic!("expected ForeignKey, got {other:?}"),
|
||||
}
|
||||
@@ -3216,7 +3231,7 @@ mod sql_alter_table_tests {
|
||||
assert_eq!(name.as_deref(), Some("fk_p"));
|
||||
match *constraint {
|
||||
TableConstraint::ForeignKey(fk) => {
|
||||
assert_eq!(fk.parent_column, None, "bare reference resolves at execution");
|
||||
assert_eq!(fk.parent_columns, None, "bare reference resolves at execution");
|
||||
}
|
||||
other => panic!("expected ForeignKey, got {other:?}"),
|
||||
}
|
||||
|
||||
@@ -984,9 +984,9 @@ mod builder_tests {
|
||||
assert_eq!(fks.len(), 1);
|
||||
let fk = &fks[0];
|
||||
assert_eq!(fk.name, None, "inline FK is auto-named at execution");
|
||||
assert_eq!(fk.child_column, "pid");
|
||||
assert_eq!(fk.child_columns, vec!["pid".to_string()]);
|
||||
assert_eq!(fk.parent_table, "parent");
|
||||
assert_eq!(fk.parent_column.as_deref(), Some("id"));
|
||||
assert_eq!(fk.parent_columns, Some(vec!["id".to_string()]));
|
||||
assert_eq!(fk.on_delete, ReferentialAction::NoAction);
|
||||
assert_eq!(fk.on_update, ReferentialAction::NoAction);
|
||||
}
|
||||
@@ -994,9 +994,9 @@ mod builder_tests {
|
||||
#[test]
|
||||
fn bare_inline_reference_has_no_parent_column() {
|
||||
let fks = parse_sct_fks("create table t (id int, pid int references parent)");
|
||||
assert_eq!(fks[0].parent_column, None, "bare REFERENCES — resolved at execution");
|
||||
assert_eq!(fks[0].parent_columns, None, "bare REFERENCES — resolved at execution");
|
||||
assert_eq!(fks[0].parent_table, "parent");
|
||||
assert_eq!(fks[0].child_column, "pid");
|
||||
assert_eq!(fks[0].child_columns, vec!["pid".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1026,9 +1026,9 @@ mod builder_tests {
|
||||
parse_sct_fks("create table t (id int, pid int, foreign key (pid) references parent(id))");
|
||||
assert_eq!(fks.len(), 1);
|
||||
assert_eq!(fks[0].name, None);
|
||||
assert_eq!(fks[0].child_column, "pid");
|
||||
assert_eq!(fks[0].child_columns, vec!["pid".to_string()]);
|
||||
assert_eq!(fks[0].parent_table, "parent");
|
||||
assert_eq!(fks[0].parent_column.as_deref(), Some("id"));
|
||||
assert_eq!(fks[0].parent_columns, Some(vec!["id".to_string()]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1038,7 +1038,7 @@ mod builder_tests {
|
||||
constraint fk_parent foreign key (pid) references parent(id))",
|
||||
);
|
||||
assert_eq!(fks[0].name.as_deref(), Some("fk_parent"));
|
||||
assert_eq!(fks[0].child_column, "pid");
|
||||
assert_eq!(fks[0].child_columns, vec!["pid".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1048,8 +1048,8 @@ mod builder_tests {
|
||||
foreign key (a) references p(id), foreign key (b) references q(id))",
|
||||
);
|
||||
assert_eq!(fks.len(), 2);
|
||||
assert_eq!((fks[0].child_column.as_str(), fks[0].parent_table.as_str()), ("a", "p"));
|
||||
assert_eq!((fks[1].child_column.as_str(), fks[1].parent_table.as_str()), ("b", "q"));
|
||||
assert_eq!((fks[0].child_columns[0].as_str(), fks[0].parent_table.as_str()), ("a", "p"));
|
||||
assert_eq!((fks[1].child_columns[0].as_str(), fks[1].parent_table.as_str()), ("b", "q"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1057,8 +1057,8 @@ mod builder_tests {
|
||||
let fks =
|
||||
parse_sct_fks("create table emp (id int primary key, mgr int references emp(id))");
|
||||
assert_eq!(fks[0].parent_table, "emp", "self-reference");
|
||||
assert_eq!(fks[0].child_column, "mgr");
|
||||
assert_eq!(fks[0].parent_column.as_deref(), Some("id"));
|
||||
assert_eq!(fks[0].child_columns, vec!["mgr".to_string()]);
|
||||
assert_eq!(fks[0].parent_columns, Some(vec!["id".to_string()]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1080,7 +1080,7 @@ mod builder_tests {
|
||||
} => {
|
||||
assert_eq!(primary_key, vec!["id".to_string()]);
|
||||
assert_eq!(foreign_keys.len(), 1);
|
||||
assert_eq!(foreign_keys[0].child_column, "pid");
|
||||
assert_eq!(foreign_keys[0].child_columns, vec!["pid".to_string()]);
|
||||
// the column-level CHECK still attaches to `pid`
|
||||
assert_eq!(
|
||||
columns.iter().find(|c| c.name == "pid").unwrap().check_sql.as_deref(),
|
||||
|
||||
Reference in New Issue
Block a user