Uh oh!
There was an error while loading. Please reload this page.
Fix prefixed migrations: schema-qualified index names + cross-schema existence checks - #628
Merged
Conversation
Postgres rejects CREATE INDEX schema.name outright - an index always lands in its table's schema, so only the table may be qualified (DROP INDEX schema.name is valid; those sites are untouched). With --prefix set to a non-public schema the whole migration chain died with a syntax error. Reported by an external user running the full chain with --prefix "companyplexus". Five sites fixed: ensure_fk_target_unique_indexes/2 and create_uuid_fk_index/4 in uuid_fk_columns.ex, add_uuid_unique_indexes in V56 and V57, and the V95 media-folders unique index.
Idempotency checks that match on conname alone see constraints from every schema in the database. When a prefixed install runs in a database that also carries a public install, the check finds public's constraint and silently skips creating it on the prefixed table - leaving the prefixed schema without its CHECK/FK constraints. Add the AND conrelid = '<prefixed table>'::regclass anchor (V51's existing idiom) to the 13 unanchored sites: V35, V102 (5), V113 (2), V115, and V118/V119 (up and down each). The V95 information_schema column check gained its table_schema filter in the previous commit; the other 43 pg_constraint sites were already anchored.
Nothing exercised the --prefix path before: every fleet app and the test boot migrate into public, so the qualification branches never ran. The test runs the full 142-version chain into a scratch schema on the test database, asserts the version marker reports the chain fully applied, spot-checks the indexes built by the once-buggy CREATE sites, and requires a full index complement in the prefixed schema. Because the test database also carries a public install, it covers the cross-schema existence-check family too - it fails on either bug class. The chain runs through the named repo with the sandbox flipped to :auto (the boot path test_helper uses), restored to :manual in on_exit even if cleanup raises. A sandbox checkout cannot host the migrator (it spawns its own runner with its own connections), and a second non-sandbox repo instance does not work either: V08's backfill calls Ecto.Adapters.SQL.query(RepoHelper.repo(), ...), which resolves the repo module name to the named instance and bypasses put_dynamic_repo/1.
ddon pushed a commit
that referenced
this pull request
Jul 11, 2026
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ddon pushed a commit
that referenced
this pull request
Aug 9, 2026
Root cause (empirically proven on the scratch DB): execute/1 only QUEUES DDL while the chain's existence guards are immediate repo().query reads — v56 called UUIDFKColumns.add_constraints with no flush() after UUIDFKColumns.up, and v57 had no flush() at all, so single-shot installs (fresh projects) never generated SET NOT NULL for 46 *_uuid columns and never created the comments FK at V56/57; v72 then filled the 'missing' FK with a guessed ON DELETE CASCADE while every incrementally-upgraded install carries V57's SET NULL. Fix: flush() discipline in v56/v57 (in-place hardening precedent PR #628/631); v72's comments entry aligned to the intended SET NULL (content anonymizes on user deletion — likes/dislikes stay CASCADE); new V161 repairs already-affected single-shot installs: SET NOT NULL only where zero NULL rows exist (warn+skip otherwise — never backfills live data), comments FK corrected metadata-only via name-anchored pg_constraint check. UUIDFKColumns gains the not_null_uuid_fks/0 accessor so V161 shares the canonical list. Verified: both modes now agree to V161 on all pairs + the FK; full generator run completes with NO shape mismatch and emits the first complete ExpectedSchema manifest (159 tables / 1869 columns / 417 constraints / 609 indexes / 114 seeds). Known follow-ups tracked for P3: prefix-embedded index NAMES still enter the manifest untemplated (12 uuid-unique indexes, :legacy_optional misclass), and UUIDFKColumns carries a dead symmetric subscriptions.plan_uuid entry.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
An external user running the full migration chain into a named schema (
--prefix "companyplexus") hit a hard failure. Investigating it surfaced two bug families in the versioned migrations, both invisible on the defaultpublicpath:1. Schema-qualified index names on CREATE INDEX (hard failure). Postgres rejects
CREATE INDEX schema.name ...outright — an index always lands in its table's schema, so only the table may be qualified. (DROP INDEX schema.nameis valid, which is why the drop helpers using the same idiom never failed — those are untouched.) With a non-public prefix the chain died withsyntax error at or near ".". Five sites fixed:ensure_fk_target_unique_indexes/2andcreate_uuid_fk_index/4inuuid_fk_columns.ex,add_uuid_unique_indexesin V56 and V57, and the V95 media-folders unique index. Note these errors surface at a later version'sflush()(V61), not the version that queued the SQL.2. Cross-schema-leaky idempotency checks (silent skips). Existence checks that don't anchor to a schema see objects from every schema. When a prefixed install runs in a database that also carries a public install, the check finds public's object and silently skips creating the prefixed one — the prefixed schema ends up missing columns/constraints (or hard-fails downstream, which is how the new test caught it). Fixed: V95's
information_schema.columnscheck gained atable_schemafilter, and 13pg_constraintconnamechecks gained theAND conrelid = '<prefixed table>'::regclassanchor (V51's existing idiom): V35, V102 ×5, V113 ×2, V115, V118 up+down, V119 up+down. The other 43pg_constraintsites were already anchored.3. Regression test.
test/integration/prefix_migration_test.exsruns the full 142-version chain into a scratch schema on the test database and asserts the version marker, the once-buggy indexes' placement, and a full index complement. Because the test DB also carries a public install, it fails on either bug family. It flips the sandbox to:autofor the run (the same boot pathtest_helper.exsuses) — a sandbox checkout can't hostEcto.Migrator's own runner connections, and a dynamic non-sandbox repo instance doesn't work either because V08's backfill resolves the repo by module name (Ecto.Adapters.SQL.query(RepoHelper.repo(), ...)), bypassingput_dynamic_repo/1. Details in the test moduledoc.Verification
prefix: "companyplexus": chain previously died at V61; now completes with all 730 phoenix_kit indexes in the prefixed schema, none leaked to publicmix precommit(format + compile + credo --strict + dialyzer): green::regclasscasts safe (every target table is guaranteed to exist at its check), and prefixed-vs-public semantics correct; its one finding (test cleanup could leave the sandbox in:autoif the schema drop raises) is fixed withtry/after