From 6336a028e201e0496c126d320b5d2943e6a5e43e Mon Sep 17 00:00:00 2001 From: Tianzhou Date: Sun, 15 Feb 2026 07:54:24 -0800 Subject: [PATCH 1/2] fix: EXCLUDE constraint incorrectly dumped as regular INDEX (#281) 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 --- cmd/dump/dump_integration_test.go | 7 +++ internal/diff/constraint.go | 16 ++++-- internal/diff/table.go | 18 +++++++ ir/inspector.go | 9 ++++ ir/ir.go | 31 +++++------ ir/normalize.go | 14 ++++- ir/queries/queries.sql | 6 ++- ir/queries/queries.sql.go | 10 +++- .../issue_281_exclude_constraint/diff.sql | 2 + .../issue_281_exclude_constraint/new.sql | 6 +++ .../issue_281_exclude_constraint/old.sql | 5 ++ .../issue_281_exclude_constraint/plan.json | 20 ++++++++ .../issue_281_exclude_constraint/plan.sql | 2 + .../issue_281_exclude_constraint/plan.txt | 14 +++++ .../manifest.json | 9 ++++ .../issue_281_exclude_constraint/pgdump.sql | 51 +++++++++++++++++++ .../issue_281_exclude_constraint/pgschema.sql | 19 +++++++ .../dump/issue_281_exclude_constraint/raw.sql | 14 +++++ 18 files changed, 230 insertions(+), 23 deletions(-) create mode 100644 testdata/diff/create_table/issue_281_exclude_constraint/diff.sql create mode 100644 testdata/diff/create_table/issue_281_exclude_constraint/new.sql create mode 100644 testdata/diff/create_table/issue_281_exclude_constraint/old.sql create mode 100644 testdata/diff/create_table/issue_281_exclude_constraint/plan.json create mode 100644 testdata/diff/create_table/issue_281_exclude_constraint/plan.sql create mode 100644 testdata/diff/create_table/issue_281_exclude_constraint/plan.txt create mode 100644 testdata/dump/issue_281_exclude_constraint/manifest.json create mode 100644 testdata/dump/issue_281_exclude_constraint/pgdump.sql create mode 100644 testdata/dump/issue_281_exclude_constraint/pgschema.sql create mode 100644 testdata/dump/issue_281_exclude_constraint/raw.sql diff --git a/cmd/dump/dump_integration_test.go b/cmd/dump/dump_integration_test.go index 3e0883cb..cd41fa66 100644 --- a/cmd/dump/dump_integration_test.go +++ b/cmd/dump/dump_integration_test.go @@ -102,6 +102,13 @@ func TestDumpCommand_Issue275TruncatedFunctionGrants(t *testing.T) { runExactMatchTest(t, "issue_275_truncated_function_grants") } +func TestDumpCommand_Issue281ExcludeConstraint(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + runExactMatchTest(t, "issue_281_exclude_constraint") +} + func runExactMatchTest(t *testing.T, testDataDir string) { runExactMatchTestWithContext(t, context.Background(), testDataDir) } diff --git a/internal/diff/constraint.go b/internal/diff/constraint.go index 49358411..826903e9 100644 --- a/internal/diff/constraint.go +++ b/internal/diff/constraint.go @@ -62,6 +62,9 @@ func generateConstraintSQL(constraint *ir.Constraint, targetSchema string) strin result += " NOT VALID" } return result + case ir.ConstraintTypeExclusion: + // Use the full definition from pg_get_constraintdef() + return fmt.Sprintf("CONSTRAINT %s %s", ir.QuoteIdentifier(constraint.Name), constraint.ExclusionDefinition) default: return "" } @@ -79,6 +82,7 @@ func getInlineConstraintsForTable(table *ir.Table) []*ir.Constraint { var uniques []*ir.Constraint var foreignKeys []*ir.Constraint var checkConstraints []*ir.Constraint + var exclusionConstraints []*ir.Constraint for _, constraintName := range constraintNames { constraint := table.Constraints[constraintName] @@ -95,14 +99,17 @@ func getInlineConstraintsForTable(table *ir.Table) []*ir.Constraint { foreignKeys = append(foreignKeys, constraint) case ir.ConstraintTypeCheck: checkConstraints = append(checkConstraints, constraint) + case ir.ConstraintTypeExclusion: + exclusionConstraints = append(exclusionConstraints, constraint) } } - // Add constraints in order: PRIMARY KEY, UNIQUE, FOREIGN KEY, CHECK + // Add constraints in order: PRIMARY KEY, UNIQUE, FOREIGN KEY, CHECK, EXCLUDE inlineConstraints = append(inlineConstraints, primaryKeys...) inlineConstraints = append(inlineConstraints, uniques...) inlineConstraints = append(inlineConstraints, foreignKeys...) inlineConstraints = append(inlineConstraints, checkConstraints...) + inlineConstraints = append(inlineConstraints, exclusionConstraints...) return inlineConstraints } @@ -125,6 +132,9 @@ func constraintsEqual(old, new *ir.Constraint) bool { if old.CheckClause != new.CheckClause { return false } + if old.ExclusionDefinition != new.ExclusionDefinition { + return false + } // Foreign key specific properties (this is the key fix!) if old.DeleteRule != new.DeleteRule { @@ -153,8 +163,8 @@ func constraintsEqual(old, new *ir.Constraint) bool { return false } - // Compare columns (skip for CHECK constraints as column detection may differ between parser and inspector) - if old.Type != ir.ConstraintTypeCheck { + // Compare columns (skip for CHECK and EXCLUDE constraints as column detection may differ) + if old.Type != ir.ConstraintTypeCheck && old.Type != ir.ConstraintTypeExclusion { if len(old.Columns) != len(new.Columns) { return false } diff --git a/internal/diff/table.go b/internal/diff/table.go index 4c90aa3a..1a549501 100644 --- a/internal/diff/table.go +++ b/internal/diff/table.go @@ -887,6 +887,20 @@ func (td *tableDiff) generateAlterTableStatements(targetSchema string, collector CanRunInTransaction: true, } collector.collect(context, sql) + + case ir.ConstraintTypeExclusion: + tableName := getTableNameWithSchema(td.Table.Schema, td.Table.Name, targetSchema) + sql := fmt.Sprintf("ALTER TABLE %s\nADD CONSTRAINT %s %s;", + tableName, ir.QuoteIdentifier(constraint.Name), constraint.ExclusionDefinition) + + context := &diffContext{ + Type: DiffTypeTableConstraint, + Operation: DiffOperationCreate, + Path: fmt.Sprintf("%s.%s.%s", td.Table.Schema, td.Table.Name, constraint.Name), + Source: constraint, + CanRunInTransaction: true, + } + collector.collect(context, sql) } } @@ -946,6 +960,10 @@ func (td *tableDiff) generateAlterTableStatements(targetSchema string, collector } addSQL = fmt.Sprintf("ALTER TABLE %s\nADD CONSTRAINT %s PRIMARY KEY (%s);", tableName, ir.QuoteIdentifier(constraint.Name), strings.Join(columnNames, ", ")) + + case ir.ConstraintTypeExclusion: + addSQL = fmt.Sprintf("ALTER TABLE %s\nADD CONSTRAINT %s %s;", + tableName, ir.QuoteIdentifier(constraint.Name), constraint.ExclusionDefinition) } addContext := &diffContext{ diff --git a/ir/inspector.go b/ir/inspector.go index ac535738..97650649 100644 --- a/ir/inspector.go +++ b/ir/inspector.go @@ -463,6 +463,8 @@ func (i *Inspector) buildConstraints(ctx context.Context, schema *IR, targetSche cType = ConstraintTypeForeignKey case "CHECK": cType = ConstraintTypeCheck + case "EXCLUDE": + cType = ConstraintTypeExclusion default: continue // Skip unknown constraint types } @@ -509,6 +511,13 @@ func (i *Inspector) buildConstraints(ctx context.Context, schema *IR, targetSche } } + // Handle exclusion constraints + if cType == ConstraintTypeExclusion { + if exclDef := i.safeInterfaceToString(constraint.ExclusionDefinition); exclDef != "" && exclDef != "" { + c.ExclusionDefinition = exclDef + } + } + // Set validation state from database c.IsValid = constraint.IsValid diff --git a/ir/ir.go b/ir/ir.go index 50497c41..c6ec0842 100644 --- a/ir/ir.go +++ b/ir/ir.go @@ -204,21 +204,22 @@ type Sequence struct { // Constraint represents a table constraint type Constraint struct { - Schema string `json:"schema"` - Table string `json:"table"` - Name string `json:"name"` - Type ConstraintType `json:"type"` - Columns []*ConstraintColumn `json:"columns"` - ReferencedSchema string `json:"referenced_schema,omitempty"` - ReferencedTable string `json:"referenced_table,omitempty"` - ReferencedColumns []*ConstraintColumn `json:"referenced_columns,omitempty"` - CheckClause string `json:"check_clause,omitempty"` - DeleteRule string `json:"delete_rule,omitempty"` - UpdateRule string `json:"update_rule,omitempty"` - Deferrable bool `json:"deferrable,omitempty"` - InitiallyDeferred bool `json:"initially_deferred,omitempty"` - IsValid bool `json:"is_valid,omitempty"` - Comment string `json:"comment,omitempty"` + Schema string `json:"schema"` + Table string `json:"table"` + Name string `json:"name"` + Type ConstraintType `json:"type"` + Columns []*ConstraintColumn `json:"columns"` + ReferencedSchema string `json:"referenced_schema,omitempty"` + ReferencedTable string `json:"referenced_table,omitempty"` + ReferencedColumns []*ConstraintColumn `json:"referenced_columns,omitempty"` + CheckClause string `json:"check_clause,omitempty"` + ExclusionDefinition string `json:"exclusion_definition,omitempty"` // Full EXCLUDE definition from pg_get_constraintdef() + DeleteRule string `json:"delete_rule,omitempty"` + UpdateRule string `json:"update_rule,omitempty"` + Deferrable bool `json:"deferrable,omitempty"` + InitiallyDeferred bool `json:"initially_deferred,omitempty"` + IsValid bool `json:"is_valid,omitempty"` + Comment string `json:"comment,omitempty"` } // ConstraintColumn represents a column within a constraint with its position diff --git a/ir/normalize.go b/ir/normalize.go index 0d3a2409..0e1e4af0 100644 --- a/ir/normalize.go +++ b/ir/normalize.go @@ -877,10 +877,22 @@ func normalizeConstraint(constraint *Constraint) { return } - // Only normalize CHECK constraints - other constraint types are already consistent + // Only normalize CHECK and EXCLUDE constraints - other constraint types are already consistent if constraint.Type == ConstraintTypeCheck && constraint.CheckClause != "" { constraint.CheckClause = normalizeCheckClause(constraint.CheckClause) } + if constraint.Type == ConstraintTypeExclusion && constraint.ExclusionDefinition != "" { + constraint.ExclusionDefinition = normalizeExclusionDefinition(constraint.ExclusionDefinition) + } +} + +// normalizeExclusionDefinition normalizes EXCLUDE constraint definitions. +// +// pg_get_constraintdef() returns the full definition like: +// "EXCLUDE USING gist (range_col WITH &&)" +// 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) } // normalizeCheckClause normalizes CHECK constraint expressions. diff --git a/ir/queries/queries.sql b/ir/queries/queries.sql index b5baeab5..a8433f24 100644 --- a/ir/queries/queries.sql +++ b/ir/queries/queries.sql @@ -312,6 +312,7 @@ SELECT COALESCE(fa.attname, '') AS foreign_column_name, COALESCE(fa.attnum, 0) AS foreign_ordinal_position, CASE WHEN c.contype = 'c' THEN pg_get_constraintdef(c.oid, true) ELSE NULL END AS check_clause, + CASE WHEN c.contype = 'x' THEN pg_get_constraintdef(c.oid, true) ELSE NULL END AS exclusion_definition, CASE c.confdeltype WHEN 'a' THEN 'NO ACTION' WHEN 'r' THEN 'RESTRICT' @@ -374,7 +375,7 @@ WITH index_base AS ( AND NOT EXISTS ( SELECT 1 FROM pg_constraint c WHERE c.conindid = idx.indexrelid - AND c.contype IN ('u', 'p') + AND c.contype IN ('u', 'p', 'x') ) AND n.nspname NOT IN ('information_schema', 'pg_catalog', 'pg_toast') AND n.nspname NOT LIKE 'pg_temp_%' @@ -460,7 +461,7 @@ WITH index_base AS ( AND NOT EXISTS ( SELECT 1 FROM pg_constraint c WHERE c.conindid = idx.indexrelid - AND c.contype IN ('u', 'p') + AND c.contype IN ('u', 'p', 'x') ) AND n.nspname = $1 ) @@ -887,6 +888,7 @@ SELECT COALESCE(fa.attname, '') AS foreign_column_name, COALESCE(fa.attnum, 0) AS foreign_ordinal_position, CASE WHEN c.contype = 'c' THEN pg_get_constraintdef(c.oid, true) ELSE NULL END AS check_clause, + CASE WHEN c.contype = 'x' THEN pg_get_constraintdef(c.oid, true) ELSE NULL END AS exclusion_definition, CASE c.confdeltype WHEN 'a' THEN 'NO ACTION' WHEN 'r' THEN 'RESTRICT' diff --git a/ir/queries/queries.sql.go b/ir/queries/queries.sql.go index 67fe7f2a..67e4d5fa 100644 --- a/ir/queries/queries.sql.go +++ b/ir/queries/queries.sql.go @@ -771,6 +771,7 @@ SELECT COALESCE(fa.attname, '') AS foreign_column_name, COALESCE(fa.attnum, 0) AS foreign_ordinal_position, CASE WHEN c.contype = 'c' THEN pg_get_constraintdef(c.oid, true) ELSE NULL END AS check_clause, + CASE WHEN c.contype = 'x' THEN pg_get_constraintdef(c.oid, true) ELSE NULL END AS exclusion_definition, CASE c.confdeltype WHEN 'a' THEN 'NO ACTION' WHEN 'r' THEN 'RESTRICT' @@ -815,6 +816,7 @@ type GetConstraintsRow struct { ForeignColumnName sql.NullString `db:"foreign_column_name" json:"foreign_column_name"` ForeignOrdinalPosition sql.NullInt32 `db:"foreign_ordinal_position" json:"foreign_ordinal_position"` CheckClause sql.NullString `db:"check_clause" json:"check_clause"` + ExclusionDefinition sql.NullString `db:"exclusion_definition" json:"exclusion_definition"` DeleteRule sql.NullString `db:"delete_rule" json:"delete_rule"` UpdateRule sql.NullString `db:"update_rule" json:"update_rule"` Deferrable bool `db:"deferrable" json:"deferrable"` @@ -844,6 +846,7 @@ func (q *Queries) GetConstraints(ctx context.Context) ([]GetConstraintsRow, erro &i.ForeignColumnName, &i.ForeignOrdinalPosition, &i.CheckClause, + &i.ExclusionDefinition, &i.DeleteRule, &i.UpdateRule, &i.Deferrable, @@ -883,6 +886,7 @@ SELECT COALESCE(fa.attname, '') AS foreign_column_name, COALESCE(fa.attnum, 0) AS foreign_ordinal_position, CASE WHEN c.contype = 'c' THEN pg_get_constraintdef(c.oid, true) ELSE NULL END AS check_clause, + CASE WHEN c.contype = 'x' THEN pg_get_constraintdef(c.oid, true) ELSE NULL END AS exclusion_definition, CASE c.confdeltype WHEN 'a' THEN 'NO ACTION' WHEN 'r' THEN 'RESTRICT' @@ -925,6 +929,7 @@ type GetConstraintsForSchemaRow struct { ForeignColumnName sql.NullString `db:"foreign_column_name" json:"foreign_column_name"` ForeignOrdinalPosition sql.NullInt32 `db:"foreign_ordinal_position" json:"foreign_ordinal_position"` CheckClause sql.NullString `db:"check_clause" json:"check_clause"` + ExclusionDefinition sql.NullString `db:"exclusion_definition" json:"exclusion_definition"` DeleteRule sql.NullString `db:"delete_rule" json:"delete_rule"` UpdateRule sql.NullString `db:"update_rule" json:"update_rule"` Deferrable bool `db:"deferrable" json:"deferrable"` @@ -954,6 +959,7 @@ func (q *Queries) GetConstraintsForSchema(ctx context.Context, dollar_1 sql.Null &i.ForeignColumnName, &i.ForeignOrdinalPosition, &i.CheckClause, + &i.ExclusionDefinition, &i.DeleteRule, &i.UpdateRule, &i.Deferrable, @@ -1564,7 +1570,7 @@ WITH index_base AS ( AND NOT EXISTS ( SELECT 1 FROM pg_constraint c WHERE c.conindid = idx.indexrelid - AND c.contype IN ('u', 'p') + AND c.contype IN ('u', 'p', 'x') ) AND n.nspname NOT IN ('information_schema', 'pg_catalog', 'pg_toast') AND n.nspname NOT LIKE 'pg_temp_%' @@ -1698,7 +1704,7 @@ WITH index_base AS ( AND NOT EXISTS ( SELECT 1 FROM pg_constraint c WHERE c.conindid = idx.indexrelid - AND c.contype IN ('u', 'p') + AND c.contype IN ('u', 'p', 'x') ) AND n.nspname = $1 ) diff --git a/testdata/diff/create_table/issue_281_exclude_constraint/diff.sql b/testdata/diff/create_table/issue_281_exclude_constraint/diff.sql new file mode 100644 index 00000000..f0af5d3a --- /dev/null +++ b/testdata/diff/create_table/issue_281_exclude_constraint/diff.sql @@ -0,0 +1,2 @@ +ALTER TABLE test_table +ADD CONSTRAINT excl_no_overlap EXCLUDE USING gist (range_col WITH &&); diff --git a/testdata/diff/create_table/issue_281_exclude_constraint/new.sql b/testdata/diff/create_table/issue_281_exclude_constraint/new.sql new file mode 100644 index 00000000..f478cac7 --- /dev/null +++ b/testdata/diff/create_table/issue_281_exclude_constraint/new.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS test_table ( + id integer, + range_col int4range NOT NULL, + CONSTRAINT test_table_pkey PRIMARY KEY (id), + CONSTRAINT excl_no_overlap EXCLUDE USING gist (range_col WITH &&) +); diff --git a/testdata/diff/create_table/issue_281_exclude_constraint/old.sql b/testdata/diff/create_table/issue_281_exclude_constraint/old.sql new file mode 100644 index 00000000..f66ca834 --- /dev/null +++ b/testdata/diff/create_table/issue_281_exclude_constraint/old.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS test_table ( + id integer, + range_col int4range NOT NULL, + CONSTRAINT test_table_pkey PRIMARY KEY (id) +); diff --git a/testdata/diff/create_table/issue_281_exclude_constraint/plan.json b/testdata/diff/create_table/issue_281_exclude_constraint/plan.json new file mode 100644 index 00000000..a54c721a --- /dev/null +++ b/testdata/diff/create_table/issue_281_exclude_constraint/plan.json @@ -0,0 +1,20 @@ +{ + "version": "1.0.0", + "pgschema_version": "1.7.0", + "created_at": "1970-01-01T00:00:00Z", + "source_fingerprint": { + "hash": "4fbb3d8c221ccc65b31f81678cb19ac44eccc901628273d00cc3e124a6ee6877" + }, + "groups": [ + { + "steps": [ + { + "sql": "ALTER TABLE test_table\nADD CONSTRAINT excl_no_overlap EXCLUDE USING gist (range_col WITH &&);", + "type": "table.constraint", + "operation": "create", + "path": "public.test_table.excl_no_overlap" + } + ] + } + ] +} diff --git a/testdata/diff/create_table/issue_281_exclude_constraint/plan.sql b/testdata/diff/create_table/issue_281_exclude_constraint/plan.sql new file mode 100644 index 00000000..f0af5d3a --- /dev/null +++ b/testdata/diff/create_table/issue_281_exclude_constraint/plan.sql @@ -0,0 +1,2 @@ +ALTER TABLE test_table +ADD CONSTRAINT excl_no_overlap EXCLUDE USING gist (range_col WITH &&); diff --git a/testdata/diff/create_table/issue_281_exclude_constraint/plan.txt b/testdata/diff/create_table/issue_281_exclude_constraint/plan.txt new file mode 100644 index 00000000..aa1c9cf3 --- /dev/null +++ b/testdata/diff/create_table/issue_281_exclude_constraint/plan.txt @@ -0,0 +1,14 @@ +Plan: 1 to modify. + +Summary by type: + tables: 1 to modify + +Tables: + ~ test_table + + excl_no_overlap (constraint) + +DDL to be executed: +-------------------------------------------------- + +ALTER TABLE test_table +ADD CONSTRAINT excl_no_overlap EXCLUDE USING gist (range_col WITH &&); diff --git a/testdata/dump/issue_281_exclude_constraint/manifest.json b/testdata/dump/issue_281_exclude_constraint/manifest.json new file mode 100644 index 00000000..c4dfbb5a --- /dev/null +++ b/testdata/dump/issue_281_exclude_constraint/manifest.json @@ -0,0 +1,9 @@ +{ + "name": "issue_281_exclude_constraint", + "description": "Test case for EXCLUDE constraint incorrectly dumped as regular INDEX (GitHub issue #281)", + "source": "https://github.com/pgplex/pgschema/issues/281", + "notes": [ + "Reproduces the bug where EXCLUDE USING gist constraints are converted to regular CREATE INDEX statements", + "Tests that EXCLUDE constraints are preserved as inline table constraints with proper operators" + ] +} diff --git a/testdata/dump/issue_281_exclude_constraint/pgdump.sql b/testdata/dump/issue_281_exclude_constraint/pgdump.sql new file mode 100644 index 00000000..a9c9a020 --- /dev/null +++ b/testdata/dump/issue_281_exclude_constraint/pgdump.sql @@ -0,0 +1,51 @@ +-- +-- PostgreSQL database dump +-- + +-- Dumped from database version 17.5 (Debian 17.5-1.pgdg120+1) +-- Dumped by pg_dump version 17.2 + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- Name: test_table; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.test_table ( + id integer NOT NULL, + range_col int4range NOT NULL +); + + +-- +-- Name: test_table test_table_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.test_table + ADD CONSTRAINT test_table_pkey PRIMARY KEY (id); + + +-- +-- Name: test_table excl_no_overlap; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.test_table + ADD CONSTRAINT excl_no_overlap EXCLUDE USING gist (range_col WITH &&); + + +-- +-- PostgreSQL database dump complete +-- diff --git a/testdata/dump/issue_281_exclude_constraint/pgschema.sql b/testdata/dump/issue_281_exclude_constraint/pgschema.sql new file mode 100644 index 00000000..0ad90f46 --- /dev/null +++ b/testdata/dump/issue_281_exclude_constraint/pgschema.sql @@ -0,0 +1,19 @@ +-- +-- pgschema database dump +-- + +-- Dumped from database version PostgreSQL 18.0 +-- Dumped by pgschema version 1.7.0 + + +-- +-- Name: test_table; Type: TABLE; Schema: -; Owner: - +-- + +CREATE TABLE IF NOT EXISTS test_table ( + id integer, + range_col int4range NOT NULL, + CONSTRAINT test_table_pkey PRIMARY KEY (id), + CONSTRAINT excl_no_overlap EXCLUDE USING gist (range_col WITH &&) +); + diff --git a/testdata/dump/issue_281_exclude_constraint/raw.sql b/testdata/dump/issue_281_exclude_constraint/raw.sql new file mode 100644 index 00000000..a7270877 --- /dev/null +++ b/testdata/dump/issue_281_exclude_constraint/raw.sql @@ -0,0 +1,14 @@ +-- +-- Test case for GitHub issue #281: EXCLUDE constraint incorrectly dumped as regular INDEX +-- +-- This test verifies that EXCLUDE USING gist constraints are preserved as proper +-- table-level constraints in dump output, not converted to CREATE INDEX statements. +-- +-- Uses int4range with && operator which has native GiST support (no btree_gist needed). +-- + +CREATE TABLE test_table ( + id integer PRIMARY KEY, + range_col int4range NOT NULL, + CONSTRAINT excl_no_overlap EXCLUDE USING gist (range_col WITH &&) +); From 3407b596004d3b82823d219e2c96737df5bd8eab Mon Sep 17 00:00:00 2001 From: Tianzhou Date: Sun, 15 Feb 2026 07:59:41 -0800 Subject: [PATCH 2/2] chore: remove redundant dump test for issue #281 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 --- cmd/dump/dump_integration_test.go | 7 --- .../manifest.json | 9 ---- .../issue_281_exclude_constraint/pgdump.sql | 51 ------------------- .../issue_281_exclude_constraint/pgschema.sql | 19 ------- .../dump/issue_281_exclude_constraint/raw.sql | 14 ----- 5 files changed, 100 deletions(-) delete mode 100644 testdata/dump/issue_281_exclude_constraint/manifest.json delete mode 100644 testdata/dump/issue_281_exclude_constraint/pgdump.sql delete mode 100644 testdata/dump/issue_281_exclude_constraint/pgschema.sql delete mode 100644 testdata/dump/issue_281_exclude_constraint/raw.sql diff --git a/cmd/dump/dump_integration_test.go b/cmd/dump/dump_integration_test.go index cd41fa66..3e0883cb 100644 --- a/cmd/dump/dump_integration_test.go +++ b/cmd/dump/dump_integration_test.go @@ -102,13 +102,6 @@ func TestDumpCommand_Issue275TruncatedFunctionGrants(t *testing.T) { runExactMatchTest(t, "issue_275_truncated_function_grants") } -func TestDumpCommand_Issue281ExcludeConstraint(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - runExactMatchTest(t, "issue_281_exclude_constraint") -} - func runExactMatchTest(t *testing.T, testDataDir string) { runExactMatchTestWithContext(t, context.Background(), testDataDir) } diff --git a/testdata/dump/issue_281_exclude_constraint/manifest.json b/testdata/dump/issue_281_exclude_constraint/manifest.json deleted file mode 100644 index c4dfbb5a..00000000 --- a/testdata/dump/issue_281_exclude_constraint/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "issue_281_exclude_constraint", - "description": "Test case for EXCLUDE constraint incorrectly dumped as regular INDEX (GitHub issue #281)", - "source": "https://github.com/pgplex/pgschema/issues/281", - "notes": [ - "Reproduces the bug where EXCLUDE USING gist constraints are converted to regular CREATE INDEX statements", - "Tests that EXCLUDE constraints are preserved as inline table constraints with proper operators" - ] -} diff --git a/testdata/dump/issue_281_exclude_constraint/pgdump.sql b/testdata/dump/issue_281_exclude_constraint/pgdump.sql deleted file mode 100644 index a9c9a020..00000000 --- a/testdata/dump/issue_281_exclude_constraint/pgdump.sql +++ /dev/null @@ -1,51 +0,0 @@ --- --- PostgreSQL database dump --- - --- Dumped from database version 17.5 (Debian 17.5-1.pgdg120+1) --- Dumped by pg_dump version 17.2 - -SET statement_timeout = 0; -SET lock_timeout = 0; -SET idle_in_transaction_session_timeout = 0; -SET client_encoding = 'UTF8'; -SET standard_conforming_strings = on; -SELECT pg_catalog.set_config('search_path', '', false); -SET check_function_bodies = false; -SET xmloption = content; -SET client_min_messages = warning; -SET row_security = off; - -SET default_tablespace = ''; - -SET default_table_access_method = heap; - --- --- Name: test_table; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.test_table ( - id integer NOT NULL, - range_col int4range NOT NULL -); - - --- --- Name: test_table test_table_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.test_table - ADD CONSTRAINT test_table_pkey PRIMARY KEY (id); - - --- --- Name: test_table excl_no_overlap; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.test_table - ADD CONSTRAINT excl_no_overlap EXCLUDE USING gist (range_col WITH &&); - - --- --- PostgreSQL database dump complete --- diff --git a/testdata/dump/issue_281_exclude_constraint/pgschema.sql b/testdata/dump/issue_281_exclude_constraint/pgschema.sql deleted file mode 100644 index 0ad90f46..00000000 --- a/testdata/dump/issue_281_exclude_constraint/pgschema.sql +++ /dev/null @@ -1,19 +0,0 @@ --- --- pgschema database dump --- - --- Dumped from database version PostgreSQL 18.0 --- Dumped by pgschema version 1.7.0 - - --- --- Name: test_table; Type: TABLE; Schema: -; Owner: - --- - -CREATE TABLE IF NOT EXISTS test_table ( - id integer, - range_col int4range NOT NULL, - CONSTRAINT test_table_pkey PRIMARY KEY (id), - CONSTRAINT excl_no_overlap EXCLUDE USING gist (range_col WITH &&) -); - diff --git a/testdata/dump/issue_281_exclude_constraint/raw.sql b/testdata/dump/issue_281_exclude_constraint/raw.sql deleted file mode 100644 index a7270877..00000000 --- a/testdata/dump/issue_281_exclude_constraint/raw.sql +++ /dev/null @@ -1,14 +0,0 @@ --- --- Test case for GitHub issue #281: EXCLUDE constraint incorrectly dumped as regular INDEX --- --- This test verifies that EXCLUDE USING gist constraints are preserved as proper --- table-level constraints in dump output, not converted to CREATE INDEX statements. --- --- Uses int4range with && operator which has native GiST support (no btree_gist needed). --- - -CREATE TABLE test_table ( - id integer PRIMARY KEY, - range_col int4range NOT NULL, - CONSTRAINT excl_no_overlap EXCLUDE USING gist (range_col WITH &&) -);