feat: ADR-0035 Amendment 1 — drop composite UNIQUE; friendlier drop-column + generic-error wording

F1/F2/F3 from the whole-Phase-4 /runda (handoff-42 §3):

- F3: drop an anonymous composite UNIQUE via a derived, engine-neutral
  name `unique_<cols>` — recomputed live, nothing persisted, reusing the
  existing `DROP CONSTRAINT <name>` grammar (no new syntax/metadata, the
  §4g anonymity decision intact). A name matching more than one UNIQUE is
  refused as ambiguous, never guessed. One undo step. `describe`
  annotates each composite UNIQUE with its name.
- F1: dropping a column a composite UNIQUE covers is refused up-front
  with the derived name + the actionable drop command (was an unhelpful
  generic engine refusal).
- F2: contextless friendly_message() no longer leaks a literal `{table}`
  in the generic hint (new `error.generic.hint_no_table`, selected when
  no table is in context). The table-ful path is unchanged.

Docs: ADR-0035 Amendment 1 + Status + README index + plan
docs/plans/20260526-adr-0035-composite-unique-drop-f1f2f3.md.
Tests: +5 (drop-by-name, ambiguous-refused, one-undo-step, F1 guard,
F2 no-leak) + a describe-render assertion. 1922 pass / 0 fail / 0 skip;
clippy clean.
This commit is contained in:
claude@clouddev1
2026-05-26 16:20:08 +00:00
parent 60d30dd54e
commit cb8ff8a7c2
10 changed files with 475 additions and 6 deletions
+1
View File
@@ -109,6 +109,7 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[
// ---- Generic engine refusal ----
("error.generic.headline", &["operation"]),
("error.generic.hint", &["table"]),
("error.generic.hint_no_table", &[]),
// ---- Invalid-value errors (pre-engine, single-line) ----
(
"error.invalid_value.arity.headline",
+4
View File
@@ -142,6 +142,10 @@ error:
generic:
headline: "the database refused this `{operation}`."
hint: "The operation could not be completed against the current state of `{table}`."
# Used when no table is in context (e.g. contextless `friendly_message()`
# callsites: replay, undo, rebuild, export) so the hint never leaks a
# literal `{table}` placeholder.
hint_no_table: "The operation could not be completed against the current database state."
# Errors that are specifically about value validation
# (DbError::InvalidValue) — wrong arity, wrong literal
+31 -2
View File
@@ -659,10 +659,17 @@ fn translate_generic(message: &str, ctx: &TranslateContext) -> FriendlyError {
let operation = ctx
.operation
.map_or("operation", Operation::keyword);
let table = ctx_table(ctx);
// F2 (ADR-0035 Amendment 1): when no table is in context, use the
// table-less hint so a contextless `friendly_message()` (replay, undo,
// rebuild, export) never renders a literal `{table}` placeholder.
let hint = if ctx.table.is_some() {
t!("error.generic.hint", table = ctx_table(ctx))
} else {
t!("error.generic.hint_no_table")
};
fe(
t!("error.generic.headline", operation = operation),
verbose_hint(ctx, t!("error.generic.hint", table = table)),
verbose_hint(ctx, hint),
)
}
@@ -1034,6 +1041,28 @@ mod tests {
);
}
#[test]
fn generic_hint_has_no_unsubstituted_table_without_context() {
// F2 (ADR-0035 Amendment 1 plan): `friendly_message()` renders
// with a default, table-less context at the default Verbose
// verbosity, so a generic-bucket error must not leak a literal
// `{table}` in its hint.
let err = sqlite("some unclassified engine failure", SqliteErrorKind::Other);
let rendered = translate(&err, &TranslateContext::default()).render();
assert!(
!rendered.contains("{table}"),
"no unsubstituted placeholder in the table-less generic hint; got:\n{rendered}"
);
// The table-ful path is unchanged: a table in context still names it.
let mut ctx = TranslateContext::for_op(Operation::Delete);
ctx.table = Some("Orders".to_string());
let with_table = translate(&err, &ctx).render();
assert!(
with_table.contains("Orders"),
"the table-ful generic hint still names the table; got:\n{with_table}"
);
}
// ---- passthrough variants ----
#[test]