fix: recreate dependent FKs when a referenced unique/PK constraint is replaced (#439)#470
Conversation
… replaced (#439) Replacing a UNIQUE constraint with a PRIMARY KEY (or otherwise dropping or recreating a unique/PK constraint) failed with SQLSTATE 2BP01 when a foreign key in another table was bound to that constraint: the plan emitted a bare ALTER TABLE ... DROP CONSTRAINT with no handling of the dependent FK. Detect desired-state foreign keys whose referenced column set matches a unique/PK constraint being dropped or recreated, and when the FK itself is unchanged, drop it before the table modifications and recreate it afterwards. The final state still matches the schema files exactly; changed or dropped FKs remain handled by their own table diff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Greptile SummaryThis PR adds handling for FKs that depend on replaced unique or primary-key constraints. The main changes are:
Confidence Score: 2/5These issues should be fixed before merging.
Important Files Changed
Reviews (1): Last reviewed commit: "fix: recreate dependent FKs when a refer..." | Re-trigger Greptile |
There was a problem hiding this comment.
Pull request overview
This PR fixes Postgres apply failures when replacing a referenced UNIQUE constraint with a PRIMARY KEY by explicitly dropping and recreating unchanged dependent foreign keys that are bound to the replaced unique/PK constraint (issue #439).
Changes:
- Detect unchanged foreign keys whose referenced column list matches a unique/PK constraint being dropped/recreated, and route them through a drop → table modification → recreate sequence.
- Emit pre-modify FK drops and post-modify FK recreations in the migration SQL generation.
- Add a regression fixture covering the unique→PK replacement with multiple dependent FKs (including
ON DELETE CASCADE) and expected plans.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
internal/diff/table.go |
Adds detection of dependent unchanged FKs and emits drop/recreate SQL around table modifications. |
internal/diff/diff.go |
Wires the new FK drop/recreate sequence into the modify phase of migration generation. |
testdata/diff/dependency/issue_439_unique_to_pk_fk_dependent/plan.txt |
New expected human-readable plan output for the regression scenario. |
testdata/diff/dependency/issue_439_unique_to_pk_fk_dependent/plan.sql |
New expected SQL plan output showing FK drop/recreate and online rewrite steps. |
testdata/diff/dependency/issue_439_unique_to_pk_fk_dependent/plan.json |
New expected JSON plan output for the same regression scenario. |
testdata/diff/dependency/issue_439_unique_to_pk_fk_dependent/old.sql |
Old-state schema reproducing the dependency problem (FKs reference a UNIQUE constraint). |
testdata/diff/dependency/issue_439_unique_to_pk_fk_dependent/new.sql |
New-state schema replacing the UNIQUE with a PRIMARY KEY while keeping FKs unchanged. |
testdata/diff/dependency/issue_439_unique_to_pk_fk_dependent/diff.sql |
New expected diff output showing FK drop/recreate around the constraint replacement. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
… constraints (#439) Extends the dependent-FK handling to two scenarios flagged in review: - An FK that itself changes in the same migration as the unique->PK swap was left on the normal table-diff path, where its recreate could run before the swap and bind to the old constraint. FKs whose old or new definition targets a replaced constraint are now routed through the pre-drop/post-add path and removed from their own table diff. - An FK on a newly added table referencing the replaced constraint was emitted inline in CREATE TABLE during the create phase, binding to the old constraint before the modify-phase swap. Such FKs are now kept out of CREATE TABLE and created after the replacement constraint exists. Verified against live PostgreSQL that FK-to-unique matching is by column set, not ordered list (FOREIGN KEY (x, y) REFERENCES t (b, a) is valid against UNIQUE (a, b)), so the order-insensitive matching is kept. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fold the changed-FK and new-table-FK scenarios into the existing issue_439_unique_to_pk_fk_dependent fixture: one UNIQUE->PK swap with four referencing tables covering an unchanged FK, an unchanged FK with ON DELETE CASCADE, an FK changed in the same migration, and a newly added table whose FK targets the replaced constraint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Apply cleanup-review findings to the issue 439 change: - Reuse generateDeferredConstraintsSQL for the post-add emission instead of the near-identical generateAddRecreatedFKsSQL; fkPostAdds is now a []*deferredConstraint, removing a duplicated copy of the FK ADD format. - Replace the post-sort mutation of tableDiff DroppedConstraints and ModifiedConstraints with a preDroppedFKSet skip-set consulted at emission time in generateAlterTableStatements, matching the existing skip-set pattern (constraintDroppedWithColumns, droppedTableSet, preDroppedViews) and deleting the bespoke list-rewrite helpers. - Share constraint identity keys via constraintPathKey. Generated plans are unchanged; all fixtures pass without regeneration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Summary
Replacing a
UNIQUEconstraint with aPRIMARY KEYfailed at apply withSQLSTATE 2BP01(cannot drop constraint ... because other objects depend on it) when a foreign key in another table referenced the unique constraint. Postgres binds an FK to the specific unique/PK constraint it was created against, but the plan emitted a bareALTER TABLE ... DROP CONSTRAINTwith no handling of the dependent FK.The diff now detects desired-state foreign keys whose referenced column set matches a unique/PK constraint being dropped or recreated, and — when the FK itself is unchanged between old and new state — emits a self-contained sequence:
The final state still matches the schema files exactly, so no referential-integrity constraint is silently lost (which is why a
DROP CONSTRAINT ... CASCADEmode was rejected on the issue). The apply-rewrite layer automatically upgrades the FK recreation to the onlineADD CONSTRAINT ... NOT VALID+VALIDATE CONSTRAINTpattern.Changed or dropped FKs remain handled by their own table diff; only unchanged FKs are routed through the pre-drop/recreate path.
Fixes #439
Test plan
New fixture
testdata/diff/dependency/issue_439_unique_to_pk_fk_dependent/reproduces the issue scenario with two dependent FKs (one withON DELETE CASCADEto verify clause preservation on recreation). Before the fix, apply fails with the exact error from the issue; after, apply succeeds and the second plan is empty (idempotent).Regression: full
TestDiffFromFilessuite plusdependency/,create_table/, andonline/TestPlanAndApplycategories all pass.🤖 Generated with Claude Code