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:
claude@clouddev1
2026-06-09 18:25:40 +00:00
parent b688592b4c
commit b14f0199e9
23 changed files with 721 additions and 507 deletions
+12 -12
View File
@@ -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(),