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:
@@ -132,9 +132,9 @@ fn add_relationship_refuses_internal_tables() {
|
||||
.block_on(db.add_relationship(
|
||||
None,
|
||||
internal.clone(),
|
||||
"name".to_string(),
|
||||
vec!["name".to_string()],
|
||||
"C".to_string(),
|
||||
"x".to_string(),
|
||||
vec!["x".to_string()],
|
||||
ReferentialAction::NoAction,
|
||||
ReferentialAction::NoAction,
|
||||
false,
|
||||
@@ -161,9 +161,9 @@ fn add_relationship_refuses_internal_tables() {
|
||||
.block_on(db.add_relationship(
|
||||
None,
|
||||
"P".to_string(),
|
||||
"id".to_string(),
|
||||
vec!["id".to_string()],
|
||||
internal,
|
||||
"x".to_string(),
|
||||
vec!["x".to_string()],
|
||||
ReferentialAction::NoAction,
|
||||
ReferentialAction::NoAction,
|
||||
false,
|
||||
|
||||
@@ -420,9 +420,9 @@ fn enrich_fk_insert_resolves_parent_table_column_and_value() {
|
||||
db.add_relationship(
|
||||
None,
|
||||
"Customers".to_string(),
|
||||
"id".to_string(),
|
||||
vec!["id".to_string()],
|
||||
"Orders".to_string(),
|
||||
"CustId".to_string(),
|
||||
vec!["CustId".to_string()],
|
||||
ReferentialAction::NoAction,
|
||||
ReferentialAction::NoAction,
|
||||
false,
|
||||
@@ -496,9 +496,9 @@ fn enrich_fk_insert_natural_order_multi_value_resolves_via_schema() {
|
||||
db.add_relationship(
|
||||
None,
|
||||
"Customers".to_string(),
|
||||
"id".to_string(),
|
||||
vec!["id".to_string()],
|
||||
"Orders".to_string(),
|
||||
"CustId".to_string(),
|
||||
vec!["CustId".to_string()],
|
||||
ReferentialAction::NoAction,
|
||||
ReferentialAction::NoAction,
|
||||
false,
|
||||
@@ -570,9 +570,9 @@ fn enrich_fk_delete_resolves_child_table() {
|
||||
db.add_relationship(
|
||||
None,
|
||||
"Customers".to_string(),
|
||||
"id".to_string(),
|
||||
vec!["id".to_string()],
|
||||
"Orders".to_string(),
|
||||
"CustId".to_string(),
|
||||
vec!["CustId".to_string()],
|
||||
ReferentialAction::NoAction,
|
||||
ReferentialAction::NoAction,
|
||||
false,
|
||||
|
||||
@@ -192,9 +192,9 @@ fn delete_with_cascade_rewrites_both_csvs() {
|
||||
db.add_relationship(
|
||||
None,
|
||||
"Customers".to_string(),
|
||||
"id".to_string(),
|
||||
vec!["id".to_string()],
|
||||
"Orders".to_string(),
|
||||
"CustId".to_string(),
|
||||
vec!["CustId".to_string()],
|
||||
ReferentialAction::Cascade,
|
||||
ReferentialAction::NoAction,
|
||||
false,
|
||||
@@ -424,9 +424,9 @@ fn project_yaml_carries_relationship_after_add() {
|
||||
db.add_relationship(
|
||||
None,
|
||||
"Customers".to_string(),
|
||||
"id".to_string(),
|
||||
vec!["id".to_string()],
|
||||
"Orders".to_string(),
|
||||
"CustId".to_string(),
|
||||
vec!["CustId".to_string()],
|
||||
ReferentialAction::Cascade,
|
||||
ReferentialAction::NoAction,
|
||||
false,
|
||||
|
||||
@@ -185,9 +185,9 @@ fn rebuild_restores_relationships_and_cascade_behaviour() {
|
||||
db.add_relationship(
|
||||
None,
|
||||
"Customers".to_string(),
|
||||
"id".to_string(),
|
||||
vec!["id".to_string()],
|
||||
"Orders".to_string(),
|
||||
"CustId".to_string(),
|
||||
vec!["CustId".to_string()],
|
||||
ReferentialAction::Cascade,
|
||||
ReferentialAction::NoAction,
|
||||
false,
|
||||
|
||||
@@ -144,9 +144,9 @@ async fn seed_schema(db: &Database) {
|
||||
db.add_relationship(
|
||||
Some("orders_customer".to_string()),
|
||||
"Customers".to_string(),
|
||||
"id".to_string(),
|
||||
vec!["id".to_string()],
|
||||
"Orders".to_string(),
|
||||
"customer_id".to_string(),
|
||||
vec!["customer_id".to_string()],
|
||||
ReferentialAction::Cascade,
|
||||
ReferentialAction::NoAction,
|
||||
false,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -100,9 +100,9 @@ fn cascade_fixture(db: &Database, rt: &tokio::runtime::Runtime) {
|
||||
rt.block_on(db.add_relationship(
|
||||
Some("places".to_string()),
|
||||
"Customers".to_string(),
|
||||
"id".to_string(),
|
||||
vec!["id".to_string()],
|
||||
"Orders".to_string(),
|
||||
"CustId".to_string(),
|
||||
vec!["CustId".to_string()],
|
||||
ReferentialAction::Cascade,
|
||||
ReferentialAction::NoAction,
|
||||
false,
|
||||
@@ -289,9 +289,9 @@ fn cascade_to_two_children_reports_both() {
|
||||
rt.block_on(db.add_relationship(
|
||||
Some(name.to_string()),
|
||||
"Customers".to_string(),
|
||||
"id".to_string(),
|
||||
vec!["id".to_string()],
|
||||
child.to_string(),
|
||||
"CustId".to_string(),
|
||||
vec!["CustId".to_string()],
|
||||
ReferentialAction::Cascade,
|
||||
ReferentialAction::NoAction,
|
||||
false,
|
||||
@@ -358,9 +358,9 @@ fn delete_violating_fk_fails_and_persists_nothing() {
|
||||
rt.block_on(db.add_relationship(
|
||||
Some("places".to_string()),
|
||||
"Customers".to_string(),
|
||||
"id".to_string(),
|
||||
vec!["id".to_string()],
|
||||
"Orders".to_string(),
|
||||
"CustId".to_string(),
|
||||
vec!["CustId".to_string()],
|
||||
ReferentialAction::NoAction, // on delete: reject if referenced
|
||||
ReferentialAction::NoAction,
|
||||
false,
|
||||
@@ -395,9 +395,9 @@ fn self_referential_cascade_counts_only_cascaded_rows() {
|
||||
rt.block_on(db.add_relationship(
|
||||
Some("parent_of".to_string()),
|
||||
"T".to_string(),
|
||||
"id".to_string(),
|
||||
vec!["id".to_string()],
|
||||
"T".to_string(),
|
||||
"ParentId".to_string(),
|
||||
vec!["ParentId".to_string()],
|
||||
ReferentialAction::Cascade,
|
||||
ReferentialAction::NoAction,
|
||||
false,
|
||||
|
||||
@@ -318,9 +318,9 @@ fn cascade_fixture(db: &Database, rt: &tokio::runtime::Runtime) {
|
||||
rt.block_on(db.add_relationship(
|
||||
Some("places".to_string()),
|
||||
"Customers".to_string(),
|
||||
"id".to_string(),
|
||||
vec!["id".to_string()],
|
||||
"Orders".to_string(),
|
||||
"CustId".to_string(),
|
||||
vec!["CustId".to_string()],
|
||||
ReferentialAction::Cascade,
|
||||
ReferentialAction::NoAction,
|
||||
false,
|
||||
|
||||
@@ -104,9 +104,9 @@ fn dropping_a_referenced_parent_is_refused() {
|
||||
vec![],
|
||||
vec![SqlForeignKey {
|
||||
name: None,
|
||||
child_column: "pid".to_string(),
|
||||
child_columns: vec!["pid".to_string()],
|
||||
parent_table: "parent".to_string(),
|
||||
parent_column: Some("id".to_string()),
|
||||
parent_columns: Some(vec!["id".to_string()]),
|
||||
on_delete: rdbms_playground::dsl::ReferentialAction::NoAction,
|
||||
on_update: rdbms_playground::dsl::ReferentialAction::NoAction,
|
||||
}],
|
||||
|
||||
@@ -420,9 +420,9 @@ fn add_relationship_flow_shows_parent_side_with_inbound_section() {
|
||||
&Command::AddRelationship {
|
||||
name: None,
|
||||
parent_table: "Customers".to_string(),
|
||||
parent_column: "Id".to_string(),
|
||||
parent_columns: vec!["Id".to_string()],
|
||||
child_table: "Orders".to_string(),
|
||||
child_column: "CustId".to_string(),
|
||||
child_columns: vec!["CustId".to_string()],
|
||||
on_delete: ReferentialAction::Cascade,
|
||||
on_update: ReferentialAction::NoAction,
|
||||
create_fk: false,
|
||||
@@ -449,8 +449,8 @@ fn add_relationship_flow_shows_parent_side_with_inbound_section() {
|
||||
inbound_relationships: vec![RelationshipEnd {
|
||||
name: "Customers_Id_to_Orders_CustId".to_string(),
|
||||
other_table: "Orders".to_string(),
|
||||
other_column: "CustId".to_string(),
|
||||
local_column: "Id".to_string(),
|
||||
other_columns: vec!["CustId".to_string()],
|
||||
local_columns: vec!["Id".to_string()],
|
||||
on_delete: ReferentialAction::Cascade,
|
||||
on_update: ReferentialAction::NoAction,
|
||||
}],
|
||||
@@ -462,9 +462,9 @@ fn add_relationship_flow_shows_parent_side_with_inbound_section() {
|
||||
command: Command::AddRelationship {
|
||||
name: None,
|
||||
parent_table: "Customers".to_string(),
|
||||
parent_column: "Id".to_string(),
|
||||
parent_columns: vec!["Id".to_string()],
|
||||
child_table: "Orders".to_string(),
|
||||
child_column: "CustId".to_string(),
|
||||
child_columns: vec!["CustId".to_string()],
|
||||
on_delete: ReferentialAction::Cascade,
|
||||
on_update: ReferentialAction::NoAction,
|
||||
create_fk: false,
|
||||
@@ -504,8 +504,8 @@ fn add_relationship_flow_shows_inbound_section_on_parent() {
|
||||
inbound_relationships: vec![RelationshipEnd {
|
||||
name: "Customers_Id_to_Orders_CustId".to_string(),
|
||||
other_table: "Orders".to_string(),
|
||||
other_column: "CustId".to_string(),
|
||||
local_column: "Id".to_string(),
|
||||
other_columns: vec!["CustId".to_string()],
|
||||
local_columns: vec!["Id".to_string()],
|
||||
on_delete: ReferentialAction::Cascade,
|
||||
on_update: ReferentialAction::NoAction,
|
||||
}],
|
||||
|
||||
Reference in New Issue
Block a user