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
+5 -5
View File
@@ -834,9 +834,9 @@ fn dropping_a_column_a_table_check_references_fails_cleanly() {
fn fk(child_column: &str, parent_table: &str, parent_column: Option<&str>) -> SqlForeignKey {
SqlForeignKey {
name: None,
child_column: child_column.to_string(),
child_columns: vec![child_column.to_string()],
parent_table: parent_table.to_string(),
parent_column: parent_column.map(str::to_string),
parent_columns: parent_column.map(|c| vec![c.to_string()]),
on_delete: ReferentialAction::NoAction,
on_update: ReferentialAction::NoAction,
}
@@ -929,7 +929,7 @@ fn foreign_key_creates_named_relationship_visible_in_describe() {
let rel = &child.outbound_relationships[0];
assert_eq!(rel.name, "parent_id_to_child_pid", "auto-named per ADR-0013");
assert_eq!(rel.other_table, "parent");
assert_eq!(rel.local_column, "pid");
assert_eq!(rel.local_columns, vec!["pid".to_string()]);
let parent = r.block_on(db.describe_table("parent".to_string(), None)).expect("describe parent");
assert_eq!(parent.inbound_relationships.len(), 1, "parent is referenced by child");
@@ -974,7 +974,7 @@ fn bare_references_resolves_to_parent_single_column_pk() {
))
.expect("create child with bare REFERENCES");
let child = r.block_on(db.describe_table("child".to_string(), None)).expect("describe");
assert_eq!(child.outbound_relationships[0].other_column, "id", "resolved to parent PK");
assert_eq!(child.outbound_relationships[0].other_columns, vec!["id".to_string()], "resolved to parent PK");
}
#[test]
@@ -1341,7 +1341,7 @@ fn bare_self_reference_resolves_to_own_pk() {
))
.expect("create self-referential emp with a bare reference");
let emp = r.block_on(db.describe_table("emp".to_string(), None)).expect("describe");
assert_eq!(emp.outbound_relationships[0].other_column, "id", "bare self-ref resolved to own PK");
assert_eq!(emp.outbound_relationships[0].other_columns, vec!["id".to_string()], "bare self-ref resolved to own PK");
// Enforced: a non-existent manager is rejected.
r.block_on(db.insert(
"emp".to_string(),