fix(fk): compound-FK violation message names every column pair

ADR-0043 residual: a compound-FK violation's friendly error named only the
first child->parent column pair (the ADR-0019 facts model is single-column).
enrich_fk_violation now gathers all pairs of the matched relationship and
carries them comma-joined in the existing single-column facts slots, so the
headline reads e.g. "no parent row in `Region` has `country, code` = `7, 8`."
instead of naming just `country`.

Single-column behaviour is unchanged (a one-element join is the element
itself). No facts-model or catalog change -- the joined strings flow through
the existing `{parent_column}` / `{value}` placeholders.

Tests: enrichment facts (compound names every pair, single-column
regression) + translate rendering (headline names both columns). 2211 pass
/ 0 fail / 1 ignored. Clippy clean.
This commit is contained in:
claude@clouddev1
2026-06-10 11:59:14 +00:00
parent 6985a43f31
commit 5a33f2aeea
3 changed files with 123 additions and 13 deletions
+24 -13
View File
@@ -2017,22 +2017,33 @@ async fn enrich_fk_violation(
};
facts.table = Some(table.clone());
for rel in outbound {
// The friendly FK-error facts model is single-column
// (ADR-0019); for a compound FK (ADR-0043) we enrich
// from the first column pair — the error still surfaces,
// richer multi-column enrichment is a later refinement.
let Some(local_col) = rel.local_columns.first().cloned() else {
// Identify the violated FK by the first local column the
// user supplied a value for (SQLite names no column in the
// error). The single-column facts slots then carry the
// comma-joined lists so a compound FK (ADR-0043) names
// *every* child->parent column pair, not just the first.
let Some(first_local) = rel.local_columns.first().cloned() else {
continue;
};
let value =
user_value_for_column_with_schema(database, command, table, &local_col).await;
if let Some(v) = value {
facts.column = Some(local_col);
facts.parent_table = Some(rel.other_table);
facts.parent_column = rel.other_columns.into_iter().next();
facts.value = Some(v.to_string());
break;
let Some(first_val) =
user_value_for_column_with_schema(database, command, table, &first_local).await
else {
continue;
};
// Matched. Gather the remaining pairs' values in order.
let mut values = vec![first_val.to_string()];
for local_col in rel.local_columns.iter().skip(1) {
if let Some(v) =
user_value_for_column_with_schema(database, command, table, local_col).await
{
values.push(v.to_string());
}
}
facts.column = Some(rel.local_columns.join(", "));
facts.parent_table = Some(rel.other_table);
facts.parent_column = Some(rel.other_columns.join(", "));
facts.value = Some(values.join(", "));
break;
}
// For UPDATE, if no outbound match was found we may
// be in the parent-side case (updating a column