# Plan — ADR-0035 Phase-4 `/runda` follow-ups F1 / F2 / F3 (2026-05-26) Bundle of the three error-message / capability follow-ups surfaced by the whole-Phase-4 `/runda` (handoff-42 §3). All three live on the **safe** composite-UNIQUE edge (dropping a UNIQUE-covered column is correctly *refused* today — no corruption); the work improves messaging and adds a way to drop the constraint itself. ## Phase 1 — requirements - **F1 — friendly refusal for dropping a composite-UNIQUE column.** `do_drop_column`'s covering-index guard reads `read_table_indexes`, which filters to `origin='c'` (explicit `CREATE INDEX`) and excludes the UNIQUE-constraint auto-index (`origin='u'`). So `drop column c` when `unique (b, c)` spans `c` skips the guard, reaches the engine, and is refused with an unhelpful generic message. Add an up-front guard detecting the column in `schema.unique_constraints` (composite only — `read_unique_constraints` routes single-column UNIQUEs to the column flag, multi-column to `unique_constraints`), refusing with the constraint's **derived name** (F3) + the drop command. Behaviour stays "refused"; only the message improves. **Message-only — no `--cascade` extension** (the SQL drop-column has no `--cascade` spelling; dropping a constraint via cascade is a larger semantic change, out of scope unless the user asks). - **F2 — literal `{table}` leak in contextless `friendly_message()`.** `Verbosity` defaults to `Verbose`, so `friendly_message()` (which uses `TranslateContext::default()`, no table) renders the generic hint `"…current state of `{table}`."` with the literal placeholder via `ctx_table()`'s `"{table}"` fallback. Hits every contextless `friendly_message()` callsite whose error lands in the **generic bucket**: replay, undo, rebuild-from-text, export. Fix: a tableless generic-hint variant selected when `ctx.table` is `None`. **Broader finding** (DA): the same `{name}`-marker fallbacks leak in *other* templates (e.g. a replayed UNIQUE violation → `error.unique.*`) when reached contextless. The documented F2 is the generic case; the broader leak is surfaced for the user to scope, not silently expanded/narrowed. - **F3 — a way to drop an anonymous composite UNIQUE (user-raised).** By design (§4a.2/§4g) a composite `UNIQUE(a,b)` is anonymous — PRAGMA-detected, a bare column-list, no name — so `DROP CONSTRAINT ` can't target it and recreating the table is the only escape. Add a way to drop it. **(Amends ADR-0035 — see Amendment 1.)** Baseline: `cargo test` → 1917 pass / 0 fail / 0 skip / 1 ignored doctest; `cargo clippy --all-targets -- -D warnings` clean. ## Phase 2/3 — F3 design (the genuine fork; user-decided) Composite UNIQUE has no name. Options considered: - **A — name composite UNIQUEs (user-supplied):** reverse the §4g anonymity decision; needs a new `__rdbms_*` table + YAML round-trip + rebuild-arrival migration (the cost §4a.3 deliberately avoided). Most SQL-standard, largest. - **B — positional drop by column-list** (`drop … unique (cols)`): preserves anonymity, no metadata, but needs a *new* grammar form. - **C — auto-assigned, engine-neutral *derived* name (chosen).** The name is a deterministic function of the columns (`unique_`), recomputed live wherever shown or matched. Storage stays a bare column-list (anonymity preserved); the name is purely a presentation/addressing label. **Reuses the existing `DROP CONSTRAINT ` grammar — no new syntax at all.** Zero metadata, zero migration, round-trips for free. Tracks column renames. **User decisions (2026-05-26):** approach **C / derived (no storage)**; name format **`unique_`**; doc vehicle **amend ADR-0035**; scope **advanced-SQL only** (matching the 4g `ADD` form — no simple-mode verb). ### DA critique (written down) 1. **Ambiguous derived names** (e.g. a column literally named `b_c` vs `UNIQUE (b, c)`): drop-by-name must **detect ambiguity and refuse**, never guess. *In scope.* 2. **Collision with a user-named CHECK/FK** of the same string: the `do_drop_constraint_by_name` order is CHECK → FK → UNIQUE, so a CHECK/FK shadows a derived UNIQUE name. Acceptable given the distinctive `unique_` prefix; **document the order**. 3. **F1 `--cascade`**: not extended to drop a covering UNIQUE (constraint, not index). Refuse-only. *Flagged.* 4. **F2 breadth**: the leak is broader than `error.generic.hint`. Fix the documented generic case; **surface** the broader leak. *Flagged.* 5. **Single-column UNIQUE column drop**: a parallel gap (a single-column UNIQUE column drop also reaches the engine with a poor message) exists but is **outside the documented F1 scope** (different mechanism — ADR-0029 column-level `drop constraint`). Noted, not fixed here. ## Phase 4 — execution (order: F3 → F1 → F2) 1. **F3.** `unique_constraint_name(cols) -> "unique_"` helper (`db.rs`, `pub(crate)`). Extend `do_drop_constraint_by_name` with a third step: match each composite UNIQUE's derived name; >1 match → refuse (ambiguous); 1 match → `rebuild_table` with that entry removed from `unique_constraints` (mirrors `do_alter_add_unique` in reverse) + `do_describe_table`. Annotate the describe "Table constraints:" section: `unique_b_c: UNIQUE (b, c)`. 2. **F1.** Up-front guard in `do_drop_column` after the index-covering guard: column in any `schema.unique_constraints` entry → refuse with the derived name + `alter table T drop constraint `. 3. **F2.** `error.generic.hint_no_table` catalog entry; in `translate_generic`, pick it when `ctx.table` is `None`. Test-first for each (reproduce → fail → fix → pass), across the worker API (Tier-1/3) and the friendly-layer unit tests + insta snapshots. ## Phase 5 — verification Full `cargo test` + clippy; compare to baseline; every checklist item addressed; engine-neutral vocab held (no SQLite/STRICT/PRAGMA in new user-facing strings); ADR + README + this plan lockstep.