fix: partition child PK/UNIQUE constraints cause perpetual plan drift (#495)#497
Conversation
…tion convergence (#495) connoinherit is only meaningful for CHECK constraints (NO INHERIT modifier). PostgreSQL sets it inconsistently for PK/UNIQUE on partition children: standalone constraint gets connoinherit=true while PARTITION OF gets false. This caused constraintsEqual to return false, producing perpetual no-op DROP+re-ADD on every plan for partitioned tables. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Fixes perpetual pgschema plan drift on partition children when PK/UNIQUE constraints are semantically identical but differ in PostgreSQL’s pg_constraint.connoinherit bookkeeping. The change narrows constraint equality comparison so NoInherit is only considered for CHECK constraints, and adds diff fixtures to ensure convergence for partition-child UNIQUE and PRIMARY KEY constraints.
Changes:
- Update
constraintsEqual()to ignoreNoInheritdifferences except for CHECK constraints (whereNO INHERITis semantically meaningful). - Add convergence fixtures for partition-child UNIQUE constraints (standalone+ATTACH vs
PARTITION OF). - Add convergence fixtures for partition-child PRIMARY KEY constraints (standalone+ATTACH vs
PARTITION OF).
Reviewed changes
Copilot reviewed 9 out of 13 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| internal/diff/constraint.go | Prevents false constraint diffs by only comparing NoInherit for CHECK constraints. |
| testdata/diff/create_table/issue_495_partition_constraint_convergence/old.sql | Fixture: “applied” state with standalone child UNIQUE constraints + ATTACH. |
| testdata/diff/create_table/issue_495_partition_constraint_convergence/new.sql | Fixture: desired state using PARTITION OF for children. |
| testdata/diff/create_table/issue_495_partition_constraint_convergence/plan.txt | Fixture expectation: plan converges with “No changes detected.” |
| testdata/diff/create_table/issue_495_partition_constraint_convergence/plan.json | Fixture expectation: empty plan JSON (no groups). |
| testdata/diff/create_table/issue_495_partition_constraint_convergence/plan.sql | Fixture placeholder for generated SQL output (no-op case). |
| testdata/diff/create_table/issue_495_partition_constraint_convergence/diff.sql | Fixture placeholder for human SQL diff output (no-op case). |
| testdata/diff/create_table/issue_495_partition_pk_convergence/old.sql | Fixture: “applied” state with standalone child PK constraints + ATTACH. |
| testdata/diff/create_table/issue_495_partition_pk_convergence/new.sql | Fixture: desired state using PARTITION OF for children. |
| testdata/diff/create_table/issue_495_partition_pk_convergence/plan.txt | Fixture expectation: plan converges with “No changes detected.” |
| testdata/diff/create_table/issue_495_partition_pk_convergence/plan.json | Fixture expectation: empty plan JSON (no groups). |
| testdata/diff/create_table/issue_495_partition_pk_convergence/plan.sql | Fixture placeholder for generated SQL output (no-op case). |
| testdata/diff/create_table/issue_495_partition_pk_convergence/diff.sql | Fixture placeholder for human SQL diff output (no-op case). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Greptile SummaryThis PR fixes a perpetual plan drift bug where
Confidence Score: 5/5Safe to merge. The change is a minimal one-line guard scoped to a well-understood PostgreSQL behavior difference, and the two new test cases directly reproduce the convergence failure. The fix is surgically correct: constraintsEqual() already checks type equality earlier (making the old.Type guard sufficient), the NoInherit field has no semantic meaning for PK/UNIQUE/FK/EXCLUDE constraints, and the test data accurately reflects the real PostgreSQL behavior difference between ATTACH PARTITION and PARTITION OF. No regressions are introduced for CHECK NO INHERIT (issue #386). No files require special attention. internal/diff/constraint.go is the only changed logic file and the change is straightforward. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant User as User (desired state)
participant Plan as pgschema plan
participant DB as Target DB (after apply)
participant Eq as constraintsEqual()
User->>Plan: "new.sql (PARTITION OF → connoinherit=false)"
DB->>Plan: "pg_constraint (ATTACH → connoinherit=true)"
Plan->>Eq: compare p_12.p_band_id_geom_id_key (old vs new)
Note over Eq: Before fix
Eq-->>Plan: NoInherit true≠false → NOT equal
Plan-->>User: DROP + ADD constraint (perpetual drift)
Note over Eq: After fix
Eq->>Eq: "old.Type == UNIQUE? Skip NoInherit check"
Eq-->>Plan: equal (all other fields match)
Plan-->>User: No changes detected
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant User as User (desired state)
participant Plan as pgschema plan
participant DB as Target DB (after apply)
participant Eq as constraintsEqual()
User->>Plan: "new.sql (PARTITION OF → connoinherit=false)"
DB->>Plan: "pg_constraint (ATTACH → connoinherit=true)"
Plan->>Eq: compare p_12.p_band_id_geom_id_key (old vs new)
Note over Eq: Before fix
Eq-->>Plan: NoInherit true≠false → NOT equal
Plan-->>User: DROP + ADD constraint (perpetual drift)
Note over Eq: After fix
Eq->>Eq: "old.Type == UNIQUE? Skip NoInherit check"
Eq-->>Plan: equal (all other fields match)
Plan-->>User: No changes detected
Reviews (1): Last reviewed commit: "fix: skip NoInherit comparison for non-C..." | Re-trigger Greptile |
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The UNIQUE test already covers the root cause (NoInherit comparison); a separate PK case exercises the same code path with no additional value. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
Fixes #495 —
pgschema plannever converges on partitioned tables because partition child PK/UNIQUE constraints are always detected as modified (perpetual DROP + re-ADD of identical constraints).Root cause:
constraintsEqual()comparedNoInherit(connoinherit) for all constraint types, but PostgreSQL setsconnoinheritinconsistently for PK/UNIQUE on partition children:connoinherit = truePARTITION OF(desired state):connoinherit = falseThis made the comparator always see a difference, even though the constraint definitions are byte-identical.
connoinheritonly has semantic meaning for CHECK constraints (theNO INHERITmodifier) — it's meaningless for PK/UNIQUE/FK/EXCLUDE.Fix: Only compare
NoInheritwhen both constraints are CHECK type.Test plan
CHECK (FALSE) NO INHERITconstraints silently dropped during plan/apply #386) still passesgo build ./...andgo vet ./...clean🤖 Generated with Claude Code