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
+50
View File
@@ -193,6 +193,56 @@ fn drop_column_referenced_by_a_table_check_is_refused() {
.expect("dropping an unreferenced column succeeds");
}
/// `T (id int pk, a int, b int, c text)` with a composite UNIQUE (a, b)
/// (ADR-0035 Amendment 1).
fn make_t_with_composite_unique(db: &Database, r: &tokio::runtime::Runtime) {
r.block_on(db.sql_create_table(
"T".to_string(),
vec![
ColumnSpec::new("id", Type::Int),
ColumnSpec::new("a", Type::Int),
ColumnSpec::new("b", Type::Int),
ColumnSpec::new("c", Type::Text),
],
vec!["id".to_string()],
vec![],
vec![],
vec![],
false,
Some("create table T (id int primary key, a int, b int, c text)".to_string()),
))
.expect("create T");
r.block_on(db.alter_add_unique(
"T".to_string(),
vec!["a".to_string(), "b".to_string()],
Some("alter table T add unique (a, b)".to_string()),
))
.expect("add composite UNIQUE (a, b)");
}
#[test]
fn drop_column_covered_by_a_composite_unique_is_refused_with_the_derived_name() {
let (_p, db, _d) = open();
let r = rt();
make_t_with_composite_unique(&db, &r);
// `a` participates in UNIQUE (a, b) → refused up-front, naming the
// derived constraint and the drop command (ADR-0035 Amendment 1, F1).
// Without this guard the drop reaches the engine and surfaces an
// unhelpful generic refusal.
let err = r
.block_on(db.drop_column("T".to_string(), "a".to_string(), false, None))
.expect_err("dropping a composite-UNIQUE column is refused");
let msg = err.friendly_message();
assert!(msg.contains("unique_a_b"), "names the derived constraint; got: {msg}");
assert!(
msg.contains("drop constraint unique_a_b"),
"points at the actionable drop command; got: {msg}"
);
// `c` is in no UNIQUE → the drop succeeds.
r.block_on(db.drop_column("T".to_string(), "c".to_string(), false, None))
.expect("dropping an uncovered column succeeds");
}
#[test]
fn rename_column_referenced_by_a_table_check_is_refused() {
let (_p, db, _d) = open();