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:
claude@clouddev1
2026-06-10 11:49:33 +00:00
parent 0a7612efe2
commit 6985a43f31
7 changed files with 104 additions and 3 deletions
+7 -3
View File
@@ -1557,7 +1557,7 @@ fn build_sql_create_table(path: &MatchedPath, source: &str) -> Result<Command, V
// Inline FK is single-column (the column it sits on);
// a compound FK uses the table-level form (ADR-0043 D4).
let child_column = columns.last().map_or_else(String::new, |c| c.name.clone());
foreign_keys.push(consume_fk_reference(&mut items, None, vec![child_column]));
foreign_keys.push(consume_fk_reference(&mut items, None, vec![child_column], true));
}
// Table-level `[constraint <name>] foreign key (<col>)
// references <parent> [(<col>)] [on …]` (ADR-0035 §5, 4b).
@@ -1587,7 +1587,8 @@ fn build_sql_create_table(path: &MatchedPath, source: &str) -> Result<Command, V
if matches!(items.peek().map(|i| &i.kind), Some(MatchedKind::Word("references"))) {
items.next();
}
let fk = consume_fk_reference(&mut items, pending_fk_name.take(), child_columns);
let fk =
consume_fk_reference(&mut items, pending_fk_name.take(), child_columns, false);
foreign_keys.push(fk);
}
// Track paren depth for element-boundary detection. The
@@ -1704,6 +1705,7 @@ fn consume_fk_reference<'a, I>(
items: &mut std::iter::Peekable<I>,
name: Option<String>,
child_columns: Vec<String>,
inline: bool,
) -> SqlForeignKey
where
I: Iterator<Item = &'a crate::dsl::walker::outcome::MatchedItem>,
@@ -1752,6 +1754,7 @@ where
parent_columns,
on_delete,
on_update,
inline,
}
}
@@ -2454,7 +2457,8 @@ fn build_alter_fk(path: &MatchedPath) -> SqlForeignKey {
if matches!(items.peek().map(|i| &i.kind), Some(MatchedKind::Word("references"))) {
items.next();
}
consume_fk_reference(&mut items, None, child_columns)
// `ALTER TABLE … ADD FOREIGN KEY (…)` is the table-level form.
consume_fk_reference(&mut items, None, child_columns, false)
}
pub static SQL_ALTER_TABLE: CommandNode = CommandNode {