Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions internal/diff/constraint.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,13 @@ func generateConstraintSQL(constraint *ir.Constraint, targetSchema string) strin
}
return stmt
case ir.ConstraintTypeCheck:
// Generate CHECK constraint with proper NOT VALID placement
// The CheckClause is normalized to exclude NOT VALID (stripped in normalize.go)
// We append NOT VALID based on IsValid field, mimicking pg_dump behavior
// Generate CHECK constraint with proper NOT VALID / NO INHERIT placement
// The CheckClause is normalized to exclude NOT VALID and NO INHERIT (stripped in normalize.go)
// We append them based on IsValid/NoInherit fields, mimicking pg_dump behavior
result := fmt.Sprintf("CONSTRAINT %s %s", ir.QuoteIdentifier(constraint.Name), constraint.CheckClause)
if constraint.NoInherit {
result += " NO INHERIT"
}
if !constraint.IsValid {
result += " NOT VALID"
}
Expand Down Expand Up @@ -150,6 +153,9 @@ func constraintsEqual(old, new *ir.Constraint) bool {
if old.CheckClause != new.CheckClause {
return false
}
if old.NoInherit != new.NoInherit {
return false
}
if old.ExclusionDefinition != new.ExclusionDefinition {
return false
}
Expand Down
16 changes: 12 additions & 4 deletions internal/diff/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -892,8 +892,12 @@ func (td *tableDiff) generateAlterTableStatements(targetSchema string, collector
// Ensure CHECK clause has outer parentheses around the full expression
tableName := getTableNameWithSchema(td.Table.Schema, td.Table.Name, targetSchema)
clause := ensureCheckClauseParens(constraint.CheckClause)
canonicalSQL := fmt.Sprintf("ALTER TABLE %s\nADD CONSTRAINT %s %s;",
tableName, ir.QuoteIdentifier(constraint.Name), clause)
suffix := ""
if constraint.NoInherit {
suffix += " NO INHERIT"
}
canonicalSQL := fmt.Sprintf("ALTER TABLE %s\nADD CONSTRAINT %s %s%s;",
tableName, ir.QuoteIdentifier(constraint.Name), clause, suffix)

context := &diffContext{
Type: DiffTypeTableConstraint,
Expand Down Expand Up @@ -1005,8 +1009,12 @@ func (td *tableDiff) generateAlterTableStatements(targetSchema string, collector

case ir.ConstraintTypeCheck:
// Add CHECK constraint with ensured outer parentheses
addSQL = fmt.Sprintf("ALTER TABLE %s\nADD CONSTRAINT %s %s;",
tableName, ir.QuoteIdentifier(constraint.Name), ensureCheckClauseParens(constraint.CheckClause))
suffix := ""
if constraint.NoInherit {
suffix += " NO INHERIT"
}
addSQL = fmt.Sprintf("ALTER TABLE %s\nADD CONSTRAINT %s %s%s;",
tableName, ir.QuoteIdentifier(constraint.Name), ensureCheckClauseParens(constraint.CheckClause), suffix)

case ir.ConstraintTypeForeignKey:
// Sort columns by position
Expand Down
8 changes: 6 additions & 2 deletions internal/plan/rewrite.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,12 @@ func generateIndexChangeRewrite(indexDiff *diff.IndexDiff) []RewriteStep {
func generateConstraintRewrite(constraint *ir.Constraint) []RewriteStep {
tableName := getTableNameWithSchema(constraint.Schema, constraint.Table)

notValidSQL := fmt.Sprintf("ALTER TABLE %s\nADD CONSTRAINT %s %s NOT VALID;",
tableName, ir.QuoteIdentifier(constraint.Name), constraint.CheckClause)
noInheritSuffix := ""
if constraint.NoInherit {
noInheritSuffix = " NO INHERIT"
}
notValidSQL := fmt.Sprintf("ALTER TABLE %s\nADD CONSTRAINT %s %s%s NOT VALID;",
tableName, ir.QuoteIdentifier(constraint.Name), constraint.CheckClause, noInheritSuffix)
validateSQL := fmt.Sprintf("ALTER TABLE %s VALIDATE CONSTRAINT %s;",
tableName, ir.QuoteIdentifier(constraint.Name))

Expand Down
88 changes: 48 additions & 40 deletions ir/inspector.go
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,11 @@ func (i *Inspector) buildConstraints(ctx context.Context, schema *IR, targetSche
}

if columnName == "" || columnName == "<nil>" {
continue // Skip constraints without columns
// CHECK and EXCLUDE constraints may have no columns (e.g., CHECK (FALSE))
// Only skip non-CHECK/EXCLUDE constraints without columns
if constraintType != "CHECK" && constraintType != "EXCLUDE" {
continue
}
}

key := constraintKey{
Expand Down Expand Up @@ -487,6 +491,7 @@ func (i *Inspector) buildConstraints(ctx context.Context, schema *IR, targetSche
Name: constraintName,
Type: cType,
Columns: []*ConstraintColumn{},
NoInherit: constraint.NoInherit,
IsTemporal: constraint.IsPeriod, // PG18 temporal constraint (WITHOUT OVERLAPS / PERIOD)
}

Expand Down Expand Up @@ -537,52 +542,55 @@ func (i *Inspector) buildConstraints(ctx context.Context, schema *IR, targetSche
constraintGroups[key] = c
}

// Get column position in constraint
position := i.getConstraintColumnPosition(ctx, schemaName, constraintName, columnName)
// Add column to constraint (skip for column-less constraints like CHECK (FALSE))
if columnName != "" && columnName != "<nil>" {
// Get column position in constraint
position := i.getConstraintColumnPosition(ctx, schemaName, constraintName, columnName)

// Check if column already exists in constraint to avoid duplicates
columnExists := false
for _, existingCol := range c.Columns {
if existingCol.Name == columnName {
columnExists = true
break
// Check if column already exists in constraint to avoid duplicates
columnExists := false
for _, existingCol := range c.Columns {
if existingCol.Name == columnName {
columnExists = true
break
}
}
}

// Add column to constraint only if it doesn't exist
if !columnExists {
constraintCol := &ConstraintColumn{
Name: columnName,
Position: position,
// Add column to constraint only if it doesn't exist
if !columnExists {
constraintCol := &ConstraintColumn{
Name: columnName,
Position: position,
}
c.Columns = append(c.Columns, constraintCol)
}
c.Columns = append(c.Columns, constraintCol)
}

// Handle foreign key referenced columns
if c.Type == ConstraintTypeForeignKey {
if refColumnName := i.safeInterfaceToString(constraint.ForeignColumnName); refColumnName != "" && refColumnName != "<nil>" {
// Check if referenced column already exists to avoid duplicates
refColumnExists := false
for _, existingRefCol := range c.ReferencedColumns {
if existingRefCol.Name == refColumnName {
refColumnExists = true
break

// Handle foreign key referenced columns
if c.Type == ConstraintTypeForeignKey {
if refColumnName := i.safeInterfaceToString(constraint.ForeignColumnName); refColumnName != "" && refColumnName != "<nil>" {
// Check if referenced column already exists to avoid duplicates
refColumnExists := false
for _, existingRefCol := range c.ReferencedColumns {
if existingRefCol.Name == refColumnName {
refColumnExists = true
break
}
}
}

// Add referenced column only if it doesn't exist
if !refColumnExists {
// Use the local column's constraint position for the referenced column.
// The local and referenced columns are paired together in the FK definition,
// so they must have the same position to maintain correct ordering.
// Note: ForeignOrdinalPosition from the query is fa.attnum (column position
// in the foreign table's definition), which is wrong - we need the constraint
// array position, which is the same as the local column's position.
refConstraintCol := &ConstraintColumn{
Name: refColumnName,
Position: position, // Use local column's constraint position
// Add referenced column only if it doesn't exist
if !refColumnExists {
// Use the local column's constraint position for the referenced column.
// The local and referenced columns are paired together in the FK definition,
// so they must have the same position to maintain correct ordering.
// Note: ForeignOrdinalPosition from the query is fa.attnum (column position
// in the foreign table's definition), which is wrong - we need the constraint
// array position, which is the same as the local column's position.
refConstraintCol := &ConstraintColumn{
Name: refColumnName,
Position: position, // Use local column's constraint position
}
c.ReferencedColumns = append(c.ReferencedColumns, refConstraintCol)
}
c.ReferencedColumns = append(c.ReferencedColumns, refConstraintCol)
}
}
}
Expand Down
1 change: 1 addition & 0 deletions ir/ir.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ type Constraint struct {
Deferrable bool `json:"deferrable,omitempty"`
InitiallyDeferred bool `json:"initially_deferred,omitempty"`
IsValid bool `json:"is_valid,omitempty"`
NoInherit bool `json:"no_inherit,omitempty"` // CHECK constraint NO INHERIT modifier
IsTemporal bool `json:"is_temporal,omitempty"` // PG18: temporal constraint (WITHOUT OVERLAPS on PK/UNIQUE, PERIOD on FK)
Comment string `json:"comment,omitempty"`
}
Expand Down
12 changes: 9 additions & 3 deletions ir/normalize.go
Original file line number Diff line number Diff line change
Expand Up @@ -1158,6 +1158,8 @@ func normalizeConstraint(constraint *Constraint) {
// Only normalize CHECK and EXCLUDE constraints - other constraint types are already consistent
if constraint.Type == ConstraintTypeCheck && constraint.CheckClause != "" {
constraint.CheckClause = normalizeCheckClause(constraint.CheckClause)
// pg_get_constraintdef may include NO INHERIT suffix — strip it and use the NoInherit field instead
// (NoInherit is already set from connoinherit in the query)
}
if constraint.Type == ConstraintTypeExclusion && constraint.ExclusionDefinition != "" {
constraint.ExclusionDefinition = normalizeExclusionDefinition(constraint.ExclusionDefinition)
Expand All @@ -1179,10 +1181,14 @@ func normalizeExclusionDefinition(definition string) string {
// now come from the same PostgreSQL version via pg_get_constraintdef(), they produce identical
// output. We only need basic cleanup for PostgreSQL internal representations.
func normalizeCheckClause(checkClause string) string {
// Strip " NOT VALID" suffix if present (mimicking pg_dump behavior)
// PostgreSQL's pg_get_constraintdef may include NOT VALID at the end,
// but we want to control its placement via the IsValid field
// Strip " NOT VALID" and " NO INHERIT" suffixes if present
// PostgreSQL's pg_get_constraintdef may include these at the end,
// but we control their placement via the IsValid and NoInherit fields
clause := strings.TrimSpace(checkClause)
if strings.HasSuffix(clause, " NO INHERIT") {
clause = strings.TrimSuffix(clause, " NO INHERIT")
clause = strings.TrimSpace(clause)
}
if strings.HasSuffix(clause, " NOT VALID") {
clause = strings.TrimSuffix(clause, " NOT VALID")
clause = strings.TrimSpace(clause)
Expand Down
6 changes: 4 additions & 2 deletions ir/queries/queries.sql
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,8 @@ SELECT
c.condeferrable AS deferrable,
c.condeferred AS initially_deferred,
c.convalidated AS is_valid,
COALESCE((to_jsonb(c) ->> 'conperiod')::boolean, false) AS is_period
COALESCE((to_jsonb(c) ->> 'conperiod')::boolean, false) AS is_period,
c.connoinherit AS no_inherit
FROM pg_constraint c
JOIN pg_class cl ON c.conrelid = cl.oid
JOIN pg_namespace n ON cl.relnamespace = n.oid
Expand Down Expand Up @@ -917,7 +918,8 @@ SELECT
c.condeferrable AS deferrable,
c.condeferred AS initially_deferred,
c.convalidated AS is_valid,
COALESCE((to_jsonb(c) ->> 'conperiod')::boolean, false) AS is_period
COALESCE((to_jsonb(c) ->> 'conperiod')::boolean, false) AS is_period,
c.connoinherit AS no_inherit
FROM pg_constraint c
JOIN pg_class cl ON c.conrelid = cl.oid
JOIN pg_namespace n ON cl.relnamespace = n.oid
Expand Down
10 changes: 8 additions & 2 deletions ir/queries/queries.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion testdata/diff/comment/add_column_comments/plan.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"pgschema_version": "1.8.0",
"created_at": "1970-01-01T00:00:00Z",
"source_fingerprint": {
"hash": "395a31ea82eaee99933280fdb3f320487dd7dabef2dd772729c7e5f3b8ceff9e"
"hash": "1351e4ca7db945af39a49da2b23273d5b33a8b1b9bd3b6a45f3cb4cf2cfce1a2"
},
"groups": [
{
Expand Down
2 changes: 1 addition & 1 deletion testdata/diff/comment/add_index_comment/plan.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"pgschema_version": "1.8.0",
"created_at": "1970-01-01T00:00:00Z",
"source_fingerprint": {
"hash": "d5424ad774c4a220725ab96071453230e0817f6ed77df1c3d427be4c28d1d3e5"
"hash": "a859ebafe82f0638592346ffb79d2bb11c1f0748d86308a87ff66c51abb68592"
},
"groups": [
{
Expand Down
2 changes: 1 addition & 1 deletion testdata/diff/comment/add_table_comment/plan.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"pgschema_version": "1.8.0",
"created_at": "1970-01-01T00:00:00Z",
"source_fingerprint": {
"hash": "ecf6d758d162c619907a2ab40f21478757d500dcc5885194afc013992e700339"
"hash": "f2623b8934b586c1ae51649bdfdcc295015334ce0d0cd6b7f4d6e2bc077030b3"
},
"groups": [
{
Expand Down
2 changes: 1 addition & 1 deletion testdata/diff/comment/alter_table_comment/plan.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"pgschema_version": "1.8.0",
"created_at": "1970-01-01T00:00:00Z",
"source_fingerprint": {
"hash": "8a4456790d31384951589e1a5cfcdb72c2c13db9aa651fa67f69dd210a4df0f5"
"hash": "1f242a84c8de680321c9fc75dcc5a06760ac51cc74d2e6b6affe524e341745f9"
},
"groups": [
{
Expand Down
2 changes: 1 addition & 1 deletion testdata/diff/comment/drop_table_comment/plan.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"pgschema_version": "1.8.0",
"created_at": "1970-01-01T00:00:00Z",
"source_fingerprint": {
"hash": "22213fffa77d3732ce0dd017e5f6f54c8c1da4c2b052a2c82e06a119a491614f"
"hash": "2ae68cfa8f7248d127b54e8c0ba366176b4e6ba698ed57883686f134a287ef16"
},
"groups": [
{
Expand Down
2 changes: 1 addition & 1 deletion testdata/diff/comment/mixed_comments/plan.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"pgschema_version": "1.8.0",
"created_at": "1970-01-01T00:00:00Z",
"source_fingerprint": {
"hash": "4f1b7baaff86b8f65e5ed747a064727e12fb0aeedff84f6f74439d1e20759dad"
"hash": "84e9a9b7c080fc6d686f528f11d070d030b1ab82e66a2ad98050868d66d2f98a"
},
"groups": [
{
Expand Down
2 changes: 1 addition & 1 deletion testdata/diff/comment/noop_column_comments/plan.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"pgschema_version": "1.8.0",
"created_at": "1970-01-01T00:00:00Z",
"source_fingerprint": {
"hash": "0f6266823ea77f5c0d7e8fe9d4b61bc9a40e38d6e25e26dfdf99944e5c4be0d9"
"hash": "06d2c3351398ca9ab591c3985cf2791ac3ff5b960eadac98470abbc0b611734a"
},
"groups": null
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"pgschema_version": "1.8.0",
"created_at": "1970-01-01T00:00:00Z",
"source_fingerprint": {
"hash": "742d612cedabf2d80d87df6c7d9fac8911e008b29ef8e15d16a32d2edf43ddd9"
"hash": "29f02983bf9ecf6f5f1ec38377f7209ec60f4fe4051d371227ace7d93bddf381"
},
"groups": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"pgschema_version": "1.8.0",
"created_at": "1970-01-01T00:00:00Z",
"source_fingerprint": {
"hash": "f787a47486736a59da932aa3fc0cb21e0fc423cfa56d1b2d3de2b529ccae64b9"
"hash": "c080880eeed5c864d9039e5087e56335177c19b37ace103267da30a2ef36775b"
},
"groups": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"pgschema_version": "1.8.0",
"created_at": "1970-01-01T00:00:00Z",
"source_fingerprint": {
"hash": "f0ac3bb964fd207775142c2f9119442b29f04e10c74de4162cf6e468335e5835"
"hash": "d7265cc266dac8551a3b9f37cf2293f45c601b13dafb6bb301915976389a3927"
},
"groups": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"pgschema_version": "1.8.0",
"created_at": "1970-01-01T00:00:00Z",
"source_fingerprint": {
"hash": "6ab6529bbf7c84fb6b639938bdaa3ca5af18b75b3de3637421b43469e4342865"
"hash": "a90a90090750b18a9aaffb6de253bd12234fe30ccf9cfc35b82c34ac834f1360"
},
"groups": [
{
Expand Down
Loading