Skip to content
Open
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
29 changes: 25 additions & 4 deletions internal/diff/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ type ddlDiff struct {
droppedTypes []*ir.Type
modifiedTypes []*typeDiff
addedSequences []*ir.Sequence
addedSerialSeqComments []*ir.Sequence // SERIAL-owned sequences skipped from addedSequences but with comments to emit
droppedSequences []*ir.Sequence
modifiedSequences []*sequenceDiff
addedDefaultPrivileges []*ir.DefaultPrivilege
Expand Down Expand Up @@ -478,6 +479,7 @@ func GenerateMigration(oldIR, newIR *ir.IR, targetSchema string) []Diff {
droppedTypes: []*ir.Type{},
modifiedTypes: []*typeDiff{},
addedSequences: []*ir.Sequence{},
addedSerialSeqComments: []*ir.Sequence{},
droppedSequences: []*ir.Sequence{},
modifiedSequences: []*sequenceDiff{},
addedDefaultPrivileges: []*ir.DefaultPrivilege{},
Expand Down Expand Up @@ -1041,6 +1043,11 @@ func GenerateMigration(oldIR, newIR *ir.IR, targetSchema string) []Diff {
// (created by SERIAL in CREATE TABLE). If the column already exists,
// we need to create the sequence explicitly for ALTER COLUMN to use.
if seq.OwnedByTable != "" && seq.OwnedByColumn != "" && !columnExistsInTables(oldTables, seq.Schema, seq.OwnedByTable, seq.OwnedByColumn) {
// Sequence is created implicitly by CREATE TABLE (SERIAL). Emit its
// comment separately after all tables are created.
if seq.Comment != "" {
diff.addedSerialSeqComments = append(diff.addedSerialSeqComments, seq)
}
continue
}
diff.addedSequences = append(diff.addedSequences, seq)
Expand All @@ -1064,12 +1071,20 @@ func GenerateMigration(oldIR, newIR *ir.IR, targetSchema string) []Diff {
for _, key := range seqKeys {
newSeq := newSequences[key]
if oldSeq, exists := oldSequences[key]; exists {
// Skip sequences owned by table columns (created by SERIAL)
if (oldSeq.OwnedByTable != "" && oldSeq.OwnedByColumn != "") ||
(newSeq.OwnedByTable != "" && newSeq.OwnedByColumn != "") {
// Skip sequences owned by table columns (created by SERIAL) for structural changes,
// but allow comment-only changes through so COMMENT ON SEQUENCE can be deployed.
isOwned := (oldSeq.OwnedByTable != "" && oldSeq.OwnedByColumn != "") ||
(newSeq.OwnedByTable != "" && newSeq.OwnedByColumn != "")
if isOwned {
if oldSeq.Comment != newSeq.Comment {
diff.modifiedSequences = append(diff.modifiedSequences, &sequenceDiff{
Old: oldSeq,
New: newSeq,
})
}
continue
}
if !sequencesEqual(oldSeq, newSeq) {
if !sequencesEqual(oldSeq, newSeq) || oldSeq.Comment != newSeq.Comment {
diff.modifiedSequences = append(diff.modifiedSequences, &sequenceDiff{
Old: oldSeq,
New: newSeq,
Expand Down Expand Up @@ -1818,6 +1833,12 @@ func (d *ddlDiff) generateCreateSQL(targetSchema string, collector *diffCollecto
// Create tables WITH function/domain dependencies (now that functions and deferred domains exist)
deferredPolicies2, deferredConstraints2 := generateCreateTablesSQL(tablesWithDeps, targetSchema, collector, existingTables, shouldDeferPolicy, d.suppressedInlineFKs)

// Emit COMMENT ON SEQUENCE for sequences created implicitly via CREATE TABLE (SERIAL/BIGSERIAL).
// These were skipped from addedSequences but their comments must still be deployed.
for _, seq := range d.addedSerialSeqComments {
generateSequenceComment(seq, targetSchema, DiffOperationCreate, collector)
}

// Add deferred foreign key constraints from BOTH batches AFTER all tables are created
// This ensures FK references to tables in the second batch (function-dependent tables) work correctly
allDeferredConstraints := append(deferredConstraints1, deferredConstraints2...)
Expand Down
29 changes: 29 additions & 0 deletions internal/diff/sequence.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,33 @@ func generateCreateSequencesSQL(sequences []*ir.Sequence, targetSchema string, c
}

collector.collect(context, sql)

// Emit COMMENT ON SEQUENCE if present
if seq.Comment != "" {
generateSequenceComment(seq, targetSchema, DiffOperationCreate, collector)
}
}
}

// generateSequenceComment emits a COMMENT ON SEQUENCE statement
func generateSequenceComment(seq *ir.Sequence, targetSchema string, operation DiffOperation, collector *diffCollector) {
seqName := qualifyEntityName(seq.Schema, seq.Name, targetSchema)
var sql string
if seq.Comment == "" {
sql = fmt.Sprintf("COMMENT ON SEQUENCE %s IS NULL;", seqName)
} else {
sql = fmt.Sprintf("COMMENT ON SEQUENCE %s IS %s;", seqName, quoteString(seq.Comment))
}
context := &diffContext{
Type: DiffTypeSequence,
Operation: operation,
Path: fmt.Sprintf("%s.%s", seq.Schema, seq.Name),
Source: seq,
CanRunInTransaction: true,
}
collector.collect(context, sql)
}

// generateDropSequencesSQL generates DROP SEQUENCE statements
func generateDropSequencesSQL(sequences []*ir.Sequence, targetSchema string, collector *diffCollector) {
// Process sequences in reverse order (already sorted)
Expand Down Expand Up @@ -71,6 +95,11 @@ func generateModifySequencesSQL(diffs []*sequenceDiff, targetSchema string, coll

collector.collect(context, stmt)
}

// Emit COMMENT ON SEQUENCE if comment changed
if diff.Old.Comment != diff.New.Comment {
generateSequenceComment(diff.New, targetSchema, DiffOperationAlter, collector)
}
}
}

Expand Down
89 changes: 54 additions & 35 deletions internal/diff/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,13 @@ func diffTriggers(oldTable, newTable *ir.Table, diff *tableDiff) {
}
}

// Find modified triggers
// Find modified triggers (structural changes, comment-only, or enabled-state-only)
for name, newTrigger := range newTriggers {
if oldTrigger, exists := oldTriggers[name]; exists {
if !triggersEqual(oldTrigger, newTrigger) {
structurallyEqual := triggersEqual(oldTrigger, newTrigger)
commentChanged := oldTrigger.Comment != newTrigger.Comment
enabledChanged := oldTrigger.Disabled != newTrigger.Disabled
if !structurallyEqual || commentChanged || enabledChanged {
diff.ModifiedTriggers = append(diff.ModifiedTriggers, &triggerDiff{
Old: oldTrigger,
New: newTrigger,
Expand Down Expand Up @@ -1383,43 +1386,59 @@ func (td *tableDiff) generateAlterTableStatements(targetSchema string, collector

// Modify triggers - already sorted by the Diff operation
for _, triggerDiff := range td.ModifiedTriggers {
// Constraint triggers don't support CREATE OR REPLACE, so we need to DROP and CREATE
if triggerDiff.New.IsConstraint {
tableName := getTableNameWithSchema(td.Table.Schema, td.Table.Name, targetSchema)
structurallyEqual := triggersEqual(triggerDiff.Old, triggerDiff.New)
commentChanged := triggerDiff.Old.Comment != triggerDiff.New.Comment
enabledChanged := triggerDiff.Old.Disabled != triggerDiff.New.Disabled

if !structurallyEqual {
// Constraint triggers don't support CREATE OR REPLACE, so we need to DROP and CREATE
if triggerDiff.New.IsConstraint {
tableName := getTableNameWithSchema(td.Table.Schema, td.Table.Name, targetSchema)

// Step 1: DROP the old trigger
dropSQL := fmt.Sprintf("DROP TRIGGER IF EXISTS %s ON %s;", ir.QuoteIdentifier(triggerDiff.Old.Name), tableName)
dropContext := &diffContext{
Type: DiffTypeTableTrigger,
Operation: DiffOperationDrop,
Path: fmt.Sprintf("%s.%s.%s", td.Table.Schema, td.Table.Name, triggerDiff.Old.Name),
Source: triggerDiff.Old,
CanRunInTransaction: true,
}
collector.collect(dropContext, dropSQL)

// Step 1: DROP the old trigger
dropSQL := fmt.Sprintf("DROP TRIGGER IF EXISTS %s ON %s;", ir.QuoteIdentifier(triggerDiff.Old.Name), tableName)
dropContext := &diffContext{
Type: DiffTypeTableTrigger,
Operation: DiffOperationDrop,
Path: fmt.Sprintf("%s.%s.%s", td.Table.Schema, td.Table.Name, triggerDiff.Old.Name),
Source: triggerDiff.Old,
CanRunInTransaction: true,
}
collector.collect(dropContext, dropSQL)
// Step 2: CREATE the new constraint trigger
createSQL := generateTriggerSQLWithMode(triggerDiff.New, targetSchema)
createContext := &diffContext{
Type: DiffTypeTableTrigger,
Operation: DiffOperationCreate,
Path: fmt.Sprintf("%s.%s.%s", td.Table.Schema, td.Table.Name, triggerDiff.New.Name),
Source: triggerDiff.New,
CanRunInTransaction: true,
}
collector.collect(createContext, createSQL)
} else {
// Use CREATE OR REPLACE for regular triggers
sql := generateTriggerSQLWithMode(triggerDiff.New, targetSchema)

// Step 2: CREATE the new constraint trigger
createSQL := generateTriggerSQLWithMode(triggerDiff.New, targetSchema)
createContext := &diffContext{
Type: DiffTypeTableTrigger,
Operation: DiffOperationCreate,
Path: fmt.Sprintf("%s.%s.%s", td.Table.Schema, td.Table.Name, triggerDiff.New.Name),
Source: triggerDiff.New,
CanRunInTransaction: true,
context := &diffContext{
Type: DiffTypeTableTrigger,
Operation: DiffOperationAlter,
Path: fmt.Sprintf("%s.%s.%s", td.Table.Schema, td.Table.Name, triggerDiff.New.Name),
Source: triggerDiff,
CanRunInTransaction: true,
}
collector.collect(context, sql)
}
collector.collect(createContext, createSQL)
} else {
// Use CREATE OR REPLACE for regular triggers
sql := generateTriggerSQLWithMode(triggerDiff.New, targetSchema)
}

context := &diffContext{
Type: DiffTypeTableTrigger,
Operation: DiffOperationAlter,
Path: fmt.Sprintf("%s.%s.%s", td.Table.Schema, td.Table.Name, triggerDiff.New.Name),
Source: triggerDiff,
CanRunInTransaction: true,
}
collector.collect(context, sql)
// Emit COMMENT ON TRIGGER for comment changes (including after structural recreate)
if commentChanged {
generateTriggerComment(triggerDiff.New, td.Table.Schema, td.Table.Name, targetSchema, DiffTypeTableTrigger, collector)
}

// Emit ENABLE/DISABLE TRIGGER for enabled state changes
if enabledChanged {
generateTriggerEnabledState(triggerDiff.New, td.Table.Schema, td.Table.Name, targetSchema, collector)
}
Comment on lines +1434 to 1442
}

Expand Down
53 changes: 53 additions & 0 deletions internal/diff/trigger.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,16 @@ func generateCreateTriggersSQL(triggers []*ir.Trigger, targetSchema string, coll
}

collector.collect(context, sql)

// Emit COMMENT ON TRIGGER if present
if trigger.Comment != "" {
generateTriggerComment(trigger, trigger.Schema, trigger.Table, targetSchema, DiffTypeTableTrigger, collector)
}

// Emit DISABLE TRIGGER if the trigger is disabled
if trigger.Disabled {
generateTriggerEnabledState(trigger, trigger.Schema, trigger.Table, targetSchema, collector)
}
}
}

Expand Down Expand Up @@ -319,7 +329,50 @@ func generateCreateViewTriggersSQL(triggers []*ir.Trigger, targetSchema string,
}

collector.collect(context, sql)

if trigger.Comment != "" {
generateTriggerComment(trigger, trigger.Schema, trigger.Table, targetSchema, DiffTypeViewTrigger, collector)
}
Comment thread
ayusssmaan marked this conversation as resolved.

if trigger.Disabled {
generateTriggerEnabledState(trigger, trigger.Schema, trigger.Table, targetSchema, collector)
}
}
}

// generateTriggerComment emits a COMMENT ON TRIGGER statement
func generateTriggerComment(trigger *ir.Trigger, schema, table, targetSchema string, diffType DiffType, collector *diffCollector) {
tableName := getTableNameWithSchema(schema, table, targetSchema)
var sql string
if trigger.Comment == "" {
sql = fmt.Sprintf("COMMENT ON TRIGGER %s ON %s IS NULL;", ir.QuoteIdentifier(trigger.Name), tableName)
} else {
sql = fmt.Sprintf("COMMENT ON TRIGGER %s ON %s IS %s;", ir.QuoteIdentifier(trigger.Name), tableName, quoteString(trigger.Comment))
}
context := &diffContext{
Type: diffType,
Operation: DiffOperationAlter,
Path: fmt.Sprintf("%s.%s.%s", schema, table, trigger.Name),
Source: trigger,
CanRunInTransaction: true,
}
collector.collect(context, sql)
}

// generateTriggerEnabledState emits ALTER TABLE DISABLE/ENABLE TRIGGER
func generateTriggerEnabledState(trigger *ir.Trigger, schema, table, targetSchema string, collector *diffCollector) {
tableName := getTableNameWithSchema(schema, table, targetSchema)
state := "ENABLE"
if trigger.Disabled {
state = "DISABLE"
}
sql := fmt.Sprintf("ALTER TABLE %s %s TRIGGER %s;", tableName, state, ir.QuoteIdentifier(trigger.Name))
context := &diffContext{
Type: DiffTypeTableTrigger,
Operation: DiffOperationAlter,
Path: fmt.Sprintf("%s.%s.%s", schema, table, trigger.Name),
Comment on lines +369 to +373
Source: trigger,
CanRunInTransaction: true,
}
collector.collect(context, sql)
}
12 changes: 12 additions & 0 deletions internal/diff/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,12 @@ func generateModifyViewsSQL(diffs []*viewDiff, targetSchema string, collector *d
CanRunInTransaction: true,
}
collector.collect(createContext, createSQL)
if triggerDiff.New.Comment != "" {
generateTriggerComment(triggerDiff.New, diff.New.Schema, diff.New.Name, targetSchema, DiffTypeViewTrigger, collector)
}
if triggerDiff.New.Disabled {
generateTriggerEnabledState(triggerDiff.New, diff.New.Schema, diff.New.Name, targetSchema, collector)
}
Comment on lines +439 to +443
} else {
sql := generateTriggerSQLWithMode(triggerDiff.New, targetSchema)
context := &diffContext{
Expand All @@ -445,6 +451,12 @@ func generateModifyViewsSQL(diffs []*viewDiff, targetSchema string, collector *d
CanRunInTransaction: true,
}
collector.collect(context, sql)
if triggerDiff.New.Comment != "" {
generateTriggerComment(triggerDiff.New, diff.New.Schema, diff.New.Name, targetSchema, DiffTypeViewTrigger, collector)
}
if triggerDiff.New.Disabled {
generateTriggerEnabledState(triggerDiff.New, diff.New.Schema, diff.New.Name, targetSchema, collector)
}
Comment on lines +455 to +459
}
}
}
Expand Down
16 changes: 16 additions & 0 deletions ir/inspector.go
Original file line number Diff line number Diff line change
Expand Up @@ -880,13 +880,19 @@ func (i *Inspector) buildSequences(ctx context.Context, schema *IR, targetSchema
}
}

seqComment := ""
if seq.SequenceComment.Valid {
seqComment = seq.SequenceComment.String
}

sequence := &Sequence{
Schema: schemaName,
Name: sequenceName,
DataType: dataType,
StartValue: seq.StartValue.Int64,
Increment: seq.Increment.Int64,
CycleOption: seq.CycleOption.Bool,
Comment: seqComment,
}

// Set default values if not valid
Expand Down Expand Up @@ -1724,6 +1730,15 @@ func (i *Inspector) buildTriggers(ctx context.Context, schema *IR, targetSchema
comment = triggerRow.TriggerComment.String
}

// Extract disabled state: tgenabled 'D' = disabled, anything else = enabled (Postgres default)
disabled := false
switch v := triggerRow.TriggerEnabled.(type) {
case string:
disabled = v == "D"
case []byte:
disabled = string(v) == "D"
}

// Determine if this is a constraint trigger
oid, ok := triggerRow.TriggerConstraintOid.(int64)
isConstraint := ok && oid != 0
Expand All @@ -1747,6 +1762,7 @@ func (i *Inspector) buildTriggers(ctx context.Context, schema *IR, targetSchema
Deferrable: deferrable,
InitiallyDeferred: initDeferred,
Comment: comment,
Disabled: disabled,
}

// Add trigger to the appropriate map
Expand Down
1 change: 1 addition & 0 deletions ir/ir.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ type Trigger struct {
InitiallyDeferred bool `json:"initially_deferred,omitempty"` // Whether deferred by default
OldTable string `json:"old_table,omitempty"` // REFERENCING OLD TABLE AS name
NewTable string `json:"new_table,omitempty"` // REFERENCING NEW TABLE AS name
Disabled bool `json:"disabled,omitempty"` // true = DISABLED (tgenabled='D'); omitted/false = enabled (Postgres default)
}

// TriggerTiming represents the timing of trigger execution
Expand Down
Loading