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:
@@ -0,0 +1,110 @@
|
||||
# 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
|
||||
<name>` 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_<cols>`),
|
||||
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
|
||||
<name>` 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_<cols>`**; 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_<cols>"` 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 <name>`.
|
||||
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.
|
||||
Reference in New Issue
Block a user