fix(fk): inline FK referencing a compound PK points at the table-level form
ADR-0043 D4 residual: an inline column-level FK (`<col> REFERENCES P(a,b)`)
is single-column by construction, so referencing a parent's compound PK
gave the generic arity error ("1 foreign-key column(s) on the child side,
but `P`'s key has 2..."). It now points the user at the table-level form:
"an inline column reference can only name one column ... Use the table-level
form instead: FOREIGN KEY (<columns>) REFERENCES P (a, b)".
- Adds `inline: bool` to SqlForeignKey, set by the grammar's single shared
builder consume_fk_reference (true for the inline path, false for the
table-level and ALTER paths).
- resolve_fk_parent_columns takes `inline` and tailors the arity-mismatch
message when an inline FK meets a compound key.
Tests: parse-layer (inline=true / table-level=false) + end-to-end worker
refusal wording. 2209 pass / 0 fail / 1 ignored. Clippy clean.
This commit is contained in:
@@ -137,6 +137,7 @@ fn sql_create_table_compound_fk_executes_and_enforces() {
|
||||
parent_columns: Some(vec!["country".to_string(), "code".to_string()]),
|
||||
on_delete: ReferentialAction::NoAction,
|
||||
on_update: ReferentialAction::NoAction,
|
||||
inline: false,
|
||||
}],
|
||||
false,
|
||||
None,
|
||||
@@ -363,6 +364,65 @@ fn compound_fk_arity_mismatch_is_refused() {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inline_fk_referencing_compound_pk_points_at_table_level_form() {
|
||||
// ADR-0043 D4 residual: an *inline* single-column FK cannot express a
|
||||
// multi-column reference, so referencing a parent's compound PK must
|
||||
// refuse with a pointer to the table-level `FOREIGN KEY (...)` form —
|
||||
// not the generic arity message. The grammar marks the FK `inline`.
|
||||
let (_p, db, _dir) = open_project_db();
|
||||
let rt = rt();
|
||||
rt.block_on(async {
|
||||
db.create_table(
|
||||
"Region".to_string(),
|
||||
vec![
|
||||
ColumnSpec::new("country", Type::Int),
|
||||
ColumnSpec::new("code", Type::Int),
|
||||
],
|
||||
vec!["country".to_string(), "code".to_string()],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("create Region");
|
||||
|
||||
// Parse the inline form so the `inline` flag is set by the grammar.
|
||||
let cmd = parse_command(
|
||||
"create table City (country int references Region(country, code))",
|
||||
)
|
||||
.expect("parses");
|
||||
let Command::SqlCreateTable {
|
||||
name,
|
||||
columns,
|
||||
primary_key,
|
||||
unique_constraints,
|
||||
check_constraints,
|
||||
foreign_keys,
|
||||
if_not_exists,
|
||||
} = cmd
|
||||
else {
|
||||
panic!("expected SqlCreateTable");
|
||||
};
|
||||
let err = db
|
||||
.sql_create_table(
|
||||
name,
|
||||
columns,
|
||||
primary_key,
|
||||
unique_constraints,
|
||||
check_constraints,
|
||||
foreign_keys,
|
||||
if_not_exists,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect_err("inline FK referencing a compound PK must be refused");
|
||||
let msg = format!("{err}");
|
||||
assert!(
|
||||
msg.contains("FOREIGN KEY"),
|
||||
"expected a pointer to the table-level `FOREIGN KEY (...)` form, got: {msg}"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compound_fk_type_mismatch_per_pair_is_refused() {
|
||||
let (_p, db, _dir) = open_project_db();
|
||||
|
||||
@@ -839,6 +839,7 @@ fn fk(child_column: &str, parent_table: &str, parent_column: Option<&str>) -> Sq
|
||||
parent_columns: parent_column.map(|c| vec![c.to_string()]),
|
||||
on_delete: ReferentialAction::NoAction,
|
||||
on_update: ReferentialAction::NoAction,
|
||||
inline: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -109,6 +109,7 @@ fn dropping_a_referenced_parent_is_refused() {
|
||||
parent_columns: Some(vec!["id".to_string()]),
|
||||
on_delete: rdbms_playground::dsl::ReferentialAction::NoAction,
|
||||
on_update: rdbms_playground::dsl::ReferentialAction::NoAction,
|
||||
inline: true,
|
||||
}],
|
||||
false,
|
||||
Some("create table child (id serial primary key, pid int references parent(id))".to_string()),
|
||||
|
||||
Reference in New Issue
Block a user