migrate/supabase: capture multi-column CREATE UNIQUE INDEX; gap partial/expression unique indexes instead of inventing or dropping them (M1b, stacks on #309) - #313
Conversation
…tly dropping it apply_table_constraint only handled single-column UNIQUE; a table-level UNIQUE(a, b) fell through and vanished, so the migrated app permitted duplicate rows the source forbade — violating the migrator's never-silently-drop promise (docs/ai/19-migrate-supabase.md:7). Capture multi-column groups into PgTable.composite_uniques and emit them as the entity's composite `unique` (issue #115), mapping source columns to the entity's field / belongs_to fk-column names (the shape JC0559 accepts). If a group's column dropped out (e.g. an unmappable-type column), raise a blocking gap for that constraint instead of dropping it — the promise holds either way. Covers both the inline CREATE TABLE constraint and the pg_dump ALTER TABLE ... ADD CONSTRAINT ... UNIQUE(a,b) form (both route through apply_table_constraint).
…site unique; gap partial/expression unique indexes (M1b) The CreateIndex arm only handled single-column indexes, so a multi-column `CREATE UNIQUE INDEX idx ON t (a, b)` — a separate parse node from the table-level UNIQUE(a, b) fixed in the parent commit, and the form pg_dump commonly emits — fell through and vanished. Same silent data-integrity loss: the migrated app permitted duplicate rows the source forbade. Resolve the columns and push the group into PgTable.composite_uniques so the existing entities.rs emit translates it (and gaps it if a column didn't survive). Non-unique multi-column indexes are lookup hints, not constraints, and stay ignored. Also stop guessing on a unique index that cannot be reproduced as a plain composite `unique`: an expression index (no column to hang `unique` on) or a partial `WHERE` index. The partial case was worse than a drop — a single-column partial unique index set `unique` unconditionally, inventing a constraint the source doesn't have, so rows legal in Supabase would 409 in the migrated app. Both now raise a blocking gap naming the index and keep the plain lookup index, per the never-silently-drop / never-guess promise (docs/ai/19-migrate-supabase.md:7).
|
Held for 0.8.0 — not a 0.7.x patch. The semver gate correctly flags this: |
Stacks on #309 (M1) — base is
fix/m1-migrate-composite-unique; retarget tomainonce #309 merges.Sibling of M1: a multi-column
CREATE UNIQUE INDEX ON t(a,b)is a separate parse node (CreateIndex, single-column only) and silently dropped the same way. Now captured into M1'scomposite_uniquesso it translates to the entity's compositeunique.Also fixes a silent over-constraint found on the way: a single-column partial unique index (
WHERE deleted_at IS NULL, the soft-delete pattern common in Supabase dumps) was settingcolumn.unique = trueunconditionally — inventing a constraint the source doesn't have, so rows legal in Supabase would 409 in the migrated app. Both partial and expression unique indexes are now gated byunique_reproducible(all-plain-identifiers AND no predicate) and raised as a blocking gap naming the index (reusingPgRawObject);indexedis still set since the lookup index is real. Non-unique multi-column indexes stay ignored (deliberately not exploded into N single-columnindexedflags).Test:
multi_column_unique_index_survives_and_a_partial_one_gaps. 75 migrate lib + 6 integration tests pass. No version bump.Not in scope, filed separately:
--livecatalog mode reads no unique constraints/indexes at all.