feat: ADR-0035 Amendment 1 follow-up — enrich replay errors + close message gaps
- F2-broad: replay failures now render with real schema context instead of
a contextless friendly_message(). Extract App::build_translate_context into
the shared App::translate_context_for(command, facts, verbosity); run_replay
enriches via enrich_dsl_failure + that builder. ctx_* fallbacks degrade to
neutral prose so the rare non-replay contextless callsites can't leak raw
{name} either. (SQL INSERT/UPDATE values aren't retained — ADR-0033 verbatim
— so those show real table/column + neutral "that value".)
- Gap C: SQL ALTER … ADD FOREIGN KEY on a missing child column refuses with an
SQL-appropriate "add it first", not the DSL-only --create-fk flag.
- Gap B: dropping a single-column-UNIQUE column refuses with a pointer to
`drop constraint unique from T.col` (was an opaque generic refusal).
- Gap D: 4e drop/rename CHECK-guard + 4f change-type FK-guard refusals reworded
to explain why; static_refusal reasons left as-is.
Tests: +4, 3 strengthened. 1926 pass / 0 fail / 0 skip; clippy clean.
This commit is contained in:
@@ -4379,6 +4379,19 @@ fn do_drop_column(
|
||||
)));
|
||||
}
|
||||
|
||||
// A single-column UNIQUE on this column (ADR-0029): the engine refuses
|
||||
// to drop a column carrying a UNIQUE constraint. Unlike a composite
|
||||
// UNIQUE (handled above), a single-column UNIQUE is removed by the
|
||||
// column-level `drop constraint` — point there (ADR-0035 Amendment 1,
|
||||
// gap B).
|
||||
if col_info.unique {
|
||||
return Err(DbError::Unsupported(format!(
|
||||
"cannot drop `{table}.{column}` — it has a UNIQUE constraint; \
|
||||
remove the constraint first (`drop constraint unique from \
|
||||
{table}.{column}`), then drop the column."
|
||||
)));
|
||||
}
|
||||
|
||||
// A CHECK (table-level, or a *different* column's column-level CHECK)
|
||||
// that references this column (ADR-0035 §4e, the 4a.3 deferral): a
|
||||
// deliberate up-front refusal — dropping the column would break that
|
||||
@@ -4387,8 +4400,10 @@ fn do_drop_column(
|
||||
// Friendly wording is H1. Guards both surfaces.
|
||||
if column_referenced_by_check(conn, table, &schema, column, false)? {
|
||||
return Err(DbError::Unsupported(format!(
|
||||
"cannot drop `{table}.{column}` while a CHECK references it; \
|
||||
drop the constraint first."
|
||||
"cannot drop `{table}.{column}` — a CHECK constraint refers to \
|
||||
it, and dropping the column would leave that rule pointing at \
|
||||
a column that no longer exists. Drop or change the CHECK \
|
||||
constraint first, then drop the column."
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -4466,8 +4481,10 @@ fn do_rename_column(
|
||||
// Deliberate refusal (friendly wording is H1); guards both surfaces.
|
||||
if column_referenced_by_check(conn, table, &schema, old, true)? {
|
||||
return Err(DbError::Unsupported(format!(
|
||||
"cannot rename `{table}.{old}` while a CHECK references it; \
|
||||
drop the constraint first."
|
||||
"cannot rename `{table}.{old}` — a CHECK constraint refers to \
|
||||
it by name, and the rename would leave that rule pointing at \
|
||||
the old name. Drop or change the CHECK constraint first, then \
|
||||
rename the column."
|
||||
)));
|
||||
}
|
||||
if old == new {
|
||||
@@ -4797,8 +4814,10 @@ fn do_change_column_type(
|
||||
.map_err(DbError::from_rusqlite)?;
|
||||
if outbound_count > 0 {
|
||||
return Err(DbError::Unsupported(format!(
|
||||
"cannot change type of `{table}.{column}` while a relationship \
|
||||
uses it as a foreign key; drop the relationship first."
|
||||
"cannot change the type of `{table}.{column}` — a relationship \
|
||||
uses it as a foreign key, and changing its type could break \
|
||||
the link to the table it references. Drop the relationship \
|
||||
first, then change the type."
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -7181,6 +7200,22 @@ fn do_alter_add_foreign_key(
|
||||
}
|
||||
}
|
||||
};
|
||||
// The child column must already exist for `ALTER … ADD FOREIGN KEY` —
|
||||
// there is no SQL spelling to auto-create it (the `--create-fk` option
|
||||
// is the simple-mode `add relationship` surface only). Pre-check here
|
||||
// so the refusal speaks SQL, not the DSL flag (ADR-0035 Amendment 1,
|
||||
// gap C). A missing child *table* is left to `do_add_relationship`'s
|
||||
// own "no such table".
|
||||
if let Ok(child_schema) = read_schema(conn, child_table)
|
||||
&& child_schema.columns.iter().all(|c| c.name != fk.child_column)
|
||||
{
|
||||
return Err(DbError::Unsupported(format!(
|
||||
"column `{child_table}.{child}` does not exist — add it first \
|
||||
(`alter table {child_table} add column {child} <type>`), then \
|
||||
add the foreign key.",
|
||||
child = fk.child_column,
|
||||
)));
|
||||
}
|
||||
do_add_relationship(
|
||||
conn,
|
||||
persistence,
|
||||
@@ -11269,7 +11304,15 @@ mod tests {
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, DbError::Unsupported(_)), "got {err:?}");
|
||||
let DbError::Unsupported(msg) = &err else {
|
||||
panic!("expected Unsupported, got {err:?}");
|
||||
};
|
||||
// The refusal explains the FK link, not just that it failed
|
||||
// (ADR-0035 Amendment 1, gap D).
|
||||
assert!(
|
||||
msg.contains("uses it as a foreign key"),
|
||||
"explains the FK link; got: {msg}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Reference in New Issue
Block a user