fix: EXCLUDE constraint incorrectly dumped as regular INDEX (#281)#289
fix: EXCLUDE constraint incorrectly dumped as regular INDEX (#281)#289
Conversation
EXCLUDE USING gist constraints were being converted to regular CREATE INDEX statements during dump, losing exclusion semantics (WITH operators). Now they are properly recognized as constraints and rendered inline in CREATE TABLE. - Add ConstraintTypeExclusion and ExclusionDefinition to IR - Query pg_get_constraintdef() for exclusion definitions (contype='x') - Exclude exclusion constraint indexes from index queries - Handle EXCLUDE in diff generation (inline, add, modify) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The diff integration test (TestPlanAndApply) already exercises the full inspector + diff pipeline, making the separate dump test unnecessary. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes a bug where EXCLUDE constraints were incorrectly dumped as regular CREATE INDEX statements, losing their exclusion semantics (the WITH operators that define the exclusion behavior). The fix ensures EXCLUDE constraints are properly recognized via contype='x' in PostgreSQL catalog queries and rendered as inline table constraints using the full definition from pg_get_constraintdef().
Changes:
- Added support for EXCLUDE constraint type throughout the IR and diff generation pipeline
- Modified index queries to exclude indexes backing EXCLUDE constraints (preventing duplicates)
- Added comprehensive test coverage including dump, diff, and integration tests
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| testdata/dump/issue_281_exclude_constraint/* | Test data for dump command validation |
| testdata/diff/create_table/issue_281_exclude_constraint/* | Test data for diff and plan command validation |
| ir/queries/queries.sql | Added exclusion_definition field to constraint queries; excluded 'x' type from index queries |
| ir/queries/queries.sql.go | Generated Go code from SQL query changes |
| ir/ir.go | Added ExclusionDefinition field to Constraint struct and ConstraintTypeExclusion constant |
| ir/inspector.go | Added EXCLUDE constraint type handling and population of ExclusionDefinition field |
| ir/normalize.go | Added normalizeExclusionDefinition function for constraint normalization |
| internal/diff/constraint.go | Added EXCLUDE constraint case in generateConstraintSQL and getInlineConstraintsForTable; updated column comparison logic |
| internal/diff/table.go | Added EXCLUDE constraint handling in ALTER TABLE statement generation |
| cmd/dump/dump_integration_test.go | Added integration test for issue #281 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Use the full definition from pg_get_constraintdef() | ||
| return fmt.Sprintf("CONSTRAINT %s %s", ir.QuoteIdentifier(constraint.Name), constraint.ExclusionDefinition) |
There was a problem hiding this comment.
The EXCLUDE constraint generation does not handle the NOT VALID state. While pg_get_constraintdef() returns the constraint definition, it does not include the validation state. Similar to CHECK constraints (lines 56-64), EXCLUDE constraints should append " NOT VALID" when constraint.IsValid is false to properly represent invalidated exclusion constraints.
| // Use the full definition from pg_get_constraintdef() | |
| return fmt.Sprintf("CONSTRAINT %s %s", ir.QuoteIdentifier(constraint.Name), constraint.ExclusionDefinition) | |
| // Use the full definition from pg_get_constraintdef() and append NOT VALID if needed | |
| result := fmt.Sprintf("CONSTRAINT %s %s", ir.QuoteIdentifier(constraint.Name), constraint.ExclusionDefinition) | |
| if !constraint.IsValid { | |
| result += " NOT VALID" | |
| } | |
| return result |
There was a problem hiding this comment.
PostgreSQL does not support NOT VALID for EXCLUDE constraints — only CHECK, FOREIGN KEY, and NOT NULL constraints support it (docs). pg_get_constraintdef() will never include "NOT VALID" for exclusion constraints, so this handling is unnecessary.
| // We keep it as-is since both desired and current state come from pg_get_constraintdef(). | ||
| func normalizeExclusionDefinition(definition string) string { | ||
| return strings.TrimSpace(definition) |
There was a problem hiding this comment.
The normalizeExclusionDefinition function should strip the " NOT VALID" suffix from the definition, similar to how normalizeCheckClause handles it (lines 904-911). PostgreSQL's pg_get_constraintdef() may include " NOT VALID" at the end for invalidated constraints, but the validation state should be controlled via the IsValid field for consistency with other constraint types.
| // We keep it as-is since both desired and current state come from pg_get_constraintdef(). | |
| func normalizeExclusionDefinition(definition string) string { | |
| return strings.TrimSpace(definition) | |
| // We keep it as-is since both desired and current state come from pg_get_constraintdef(), | |
| // except for stripping a trailing " NOT VALID" suffix to match normalizeCheckClause behavior. | |
| func normalizeExclusionDefinition(definition string) string { | |
| normalized := strings.TrimSpace(definition) | |
| // Strip " NOT VALID" suffix if present (mimicking pg_dump behavior) | |
| // PostgreSQL's pg_get_constraintdef may include NOT VALID at the end, | |
| // but the validation state should be represented via the IsValid field. | |
| const notValidSuffix = " NOT VALID" | |
| if strings.HasSuffix(normalized, notValidSuffix) { | |
| normalized = strings.TrimSpace(strings.TrimSuffix(normalized, notValidSuffix)) | |
| } | |
| return normalized |
There was a problem hiding this comment.
Same as above — PostgreSQL does not support NOT VALID for EXCLUDE constraints, so pg_get_constraintdef() will never return a "NOT VALID" suffix for them. Stripping it would be dead code.
Summary
CREATE INDEXstatements during dump, losing the exclusion semantics (WITH =,WITH &&operators)contype='x'in pg_catalog queries and rendered as inline table constraints usingpg_get_constraintdef()Fixes #281
Test plan
go test -v ./cmd/dump -run TestDumpCommand_Issue281PGSCHEMA_TEST_FILTER="create_table/issue_281_exclude_constraint" go test -v ./internal/diff -run TestDiffFromFilesPGSCHEMA_TEST_FILTER="create_table/issue_281_exclude_constraint" go test -v ./cmd -run TestPlanAndApplygo test ./...🤖 Generated with Claude Code