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.
6.0 KiB
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 readsread_table_indexes, which filters toorigin='c'(explicitCREATE INDEX) and excludes the UNIQUE-constraint auto-index (origin='u'). Sodrop column cwhenunique (b, c)spanscskips the guard, reaches the engine, and is refused with an unhelpful generic message. Add an up-front guard detecting the column inschema.unique_constraints(composite only —read_unique_constraintsroutes single-column UNIQUEs to the column flag, multi-column tounique_constraints), refusing with the constraint's derived name (F3) + the drop command. Behaviour stays "refused"; only the message improves. Message-only — no--cascadeextension (the SQL drop-column has no--cascadespelling; dropping a constraint via cascade is a larger semantic change, out of scope unless the user asks). -
F2 — literal
{table}leak in contextlessfriendly_message().Verbositydefaults toVerbose, sofriendly_message()(which usesTranslateContext::default(), no table) renders the generic hint"…current state of{table}."with the literal placeholder viactx_table()'s"{table}"fallback. Hits every contextlessfriendly_message()callsite whose error lands in the generic bucket: replay, undo, rebuild-from-text, export. Fix: a tableless generic-hint variant selected whenctx.tableisNone. 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 — soDROP 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 existingDROP 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)
- Ambiguous derived names (e.g. a column literally named
b_cvsUNIQUE (b, c)): drop-by-name must detect ambiguity and refuse, never guess. In scope. - Collision with a user-named CHECK/FK of the same string: the
do_drop_constraint_by_nameorder is CHECK → FK → UNIQUE, so a CHECK/FK shadows a derived UNIQUE name. Acceptable given the distinctiveunique_prefix; document the order. - F1
--cascade: not extended to drop a covering UNIQUE (constraint, not index). Refuse-only. Flagged. - F2 breadth: the leak is broader than
error.generic.hint. Fix the documented generic case; surface the broader leak. Flagged. - 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)
- F3.
unique_constraint_name(cols) -> "unique_<cols>"helper (db.rs,pub(crate)). Extenddo_drop_constraint_by_namewith a third step: match each composite UNIQUE's derived name; >1 match → refuse (ambiguous); 1 match →rebuild_tablewith that entry removed fromunique_constraints(mirrorsdo_alter_add_uniquein reverse) +do_describe_table. Annotate the describe "Table constraints:" section:unique_b_c: UNIQUE (b, c). - F1. Up-front guard in
do_drop_columnafter the index-covering guard: column in anyschema.unique_constraintsentry → refuse with the derived name +alter table T drop constraint <name>. - F2.
error.generic.hint_no_tablecatalog entry; intranslate_generic, pick it whenctx.tableisNone.
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.