Skip to content

migrate/supabase: capture multi-column CREATE UNIQUE INDEX; gap partial/expression unique indexes instead of inventing or dropping them (M1b, stacks on #309) - #313

Open
Sorcecoder wants to merge 5 commits into
mainfrom
fix/m1b-migrate-unique-index
Open

Sorcecoder wants to merge 5 commits into
mainfrom
fix/m1b-migrate-unique-index

Conversation

@Sorcecoder

Copy link
Copy Markdown
Contributor

Stacks on #309 (M1) — base is fix/m1-migrate-composite-unique; retarget to main once #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's composite_uniques so it translates to the entity's composite unique.

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 setting column.unique = true unconditionally — 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 by unique_reproducible (all-plain-identifiers AND no predicate) and raised as a blocking gap naming the index (reusing PgRawObject); indexed is still set since the lookup index is real. Non-unique multi-column indexes stay ignored (deliberately not exploded into N single-column indexed flags).

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: --live catalog mode reads no unique constraints/indexes at all.

Sorcecoder and others added 4 commits September 1, 2026 19:02
…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).
@Sorcecoder
Sorcecoder changed the base branch from fix/m1-migrate-composite-unique to main September 1, 2026 21:11
@Sorcecoder

Copy link
Copy Markdown
Contributor Author

Held for 0.8.0 — not a 0.7.x patch. The semver gate correctly flags this: constructible_struct_adds_field on PgTable (pgmodel.rs). The migrate IR (PgDatabase/PgTable/PgColumn) is public, all-pub-field, Default-derived and not #[non_exhaustive], so any new field is a breaking change for a 0.x crate, and no existing field can carry composite-unique groups non-breakingly. Per the release policy (bug fixes stay 0.7.x; breaking/feature work goes to the next minor), this lands in the 0.8.0 breaking window — where the migrate IR should also be marked #[non_exhaustive] (the 0.7.0 convention already used by design.rs/mod.rs/realtimegen.rs) so every future IR addition is non-breaking. The fix itself is reviewed and correct; only its version placement changes. (Stacked on #309; retargeted to main, lands with it.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant