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:
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user