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
30 changes: 28 additions & 2 deletions internal/diff/trigger.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ func triggersEqual(old, new *ir.Trigger) bool {
return false
}

// Compare transition table references
if old.OldTable != new.OldTable {
return false
}
if old.NewTable != new.NewTable {
return false
}

return true
}

Expand Down Expand Up @@ -151,6 +159,19 @@ func generateTriggerSQLWithMode(trigger *ir.Trigger, targetSchema string) string
// Only include table name without schema if it's in the target schema
tableName := qualifyEntityName(trigger.Schema, trigger.Table, targetSchema)

// Build REFERENCING clause if present (for transition tables)
var referencingParts []string
if trigger.OldTable != "" {
referencingParts = append(referencingParts, fmt.Sprintf("OLD TABLE AS %s", trigger.OldTable))
}
if trigger.NewTable != "" {
referencingParts = append(referencingParts, fmt.Sprintf("NEW TABLE AS %s", trigger.NewTable))
}
referencingClause := ""
if len(referencingParts) > 0 {
referencingClause = fmt.Sprintf("\n REFERENCING %s", strings.Join(referencingParts, " "))
}

// Build the trigger statement with proper formatting
// Use CREATE CONSTRAINT TRIGGER for constraint triggers (cannot use OR REPLACE)
var stmt string
Expand All @@ -167,10 +188,15 @@ func generateTriggerSQLWithMode(trigger *ir.Trigger, targetSchema string) string
}
}

// Add REFERENCING clause before FOR EACH
stmt += referencingClause
stmt += fmt.Sprintf("\n FOR EACH %s", trigger.Level)
} else {
stmt = fmt.Sprintf("CREATE OR REPLACE TRIGGER %s\n %s %s ON %s\n FOR EACH %s",
trigger.Name, trigger.Timing, eventList, tableName, trigger.Level)
stmt = fmt.Sprintf("CREATE OR REPLACE TRIGGER %s\n %s %s ON %s",
trigger.Name, trigger.Timing, eventList, tableName)
// Add REFERENCING clause before FOR EACH
stmt += referencingClause
stmt += fmt.Sprintf("\n FOR EACH %s", trigger.Level)
}

// Add WHEN clause if present
Expand Down
18 changes: 16 additions & 2 deletions ir/inspector.go
Original file line number Diff line number Diff line change
Expand Up @@ -405,9 +405,14 @@ func (i *Inspector) buildConstraints(ctx context.Context, schema *IR, targetSche
if constraint.ConstraintType.Valid {
constraintType = constraint.ConstraintType.String
}
columnName := constraint.ColumnName

if columnName == "<nil>" {
// Extract column name from sql.NullString
columnName := ""
if constraint.ColumnName.Valid {
columnName = constraint.ColumnName.String
}

if columnName == "" || columnName == "<nil>" {
continue // Skip constraints without columns
}

Expand Down Expand Up @@ -1465,6 +1470,15 @@ func (i *Inspector) buildTriggers(ctx context.Context, schema *IR, targetSchema
for _, dbSchema := range parsedSchema.Schemas {
for _, parsedTable := range dbSchema.Tables {
for triggerName, trigger := range parsedTable.Triggers {
// Set transition table names from the system catalog query
// The parser extracts these from CREATE TRIGGER DDL, but for existing triggers
// we get the definitive values from pg_trigger catalog
if triggerRow.OldTable != "" {
trigger.OldTable = triggerRow.OldTable
}
if triggerRow.NewTable != "" {
Comment on lines +1476 to +1479
Copy link

Copilot AI Oct 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The transition table assignment logic overwrites parsed values unconditionally. This could cause issues when the catalog data is empty but the parser correctly extracted values from DDL. Consider only setting these values when they're currently empty in the trigger object.

Suggested change
if triggerRow.OldTable != "" {
trigger.OldTable = triggerRow.OldTable
}
if triggerRow.NewTable != "" {
if triggerRow.OldTable != "" && trigger.OldTable == "" {
trigger.OldTable = triggerRow.OldTable
}
if triggerRow.NewTable != "" && trigger.NewTable == "" {

Copilot uses AI. Check for mistakes.
trigger.NewTable = triggerRow.NewTable
}
table.Triggers[triggerName] = trigger
}
}
Expand Down
2 changes: 2 additions & 0 deletions ir/ir.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,8 @@ type Trigger struct {
IsConstraint bool `json:"is_constraint,omitempty"` // Whether this is a constraint trigger
Deferrable bool `json:"deferrable,omitempty"` // Can be deferred until end of transaction
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
}

// TriggerTiming represents the timing of trigger execution
Expand Down
15 changes: 15 additions & 0 deletions ir/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -3237,6 +3237,19 @@ func (p *Parser) parseCreateTrigger(triggerStmt *pg_query.CreateTrigStmt) error
condition = p.extractExpressionText(triggerStmt.WhenClause)
}

// Extract transition table references (REFERENCING OLD TABLE AS / NEW TABLE AS)
var oldTable, newTable string
for _, transRel := range triggerStmt.TransitionRels {
if rel := transRel.GetTriggerTransition(); rel != nil {
// rel.IsNew indicates if this is NEW TABLE (true) or OLD TABLE (false)
if rel.IsNew {
newTable = rel.Name
} else {
oldTable = rel.Name
}
}
}

// Create trigger
trigger := &Trigger{
Schema: schemaName,
Expand All @@ -3250,6 +3263,8 @@ func (p *Parser) parseCreateTrigger(triggerStmt *pg_query.CreateTrigStmt) error
IsConstraint: triggerStmt.Isconstraint,
Deferrable: triggerStmt.Deferrable,
InitiallyDeferred: triggerStmt.Initdeferred,
OldTable: oldTable,
NewTable: newTable,
}

// Add trigger to table only
Expand Down
4 changes: 3 additions & 1 deletion ir/queries/queries.sql
Original file line number Diff line number Diff line change
Expand Up @@ -850,7 +850,9 @@ SELECT
n.nspname AS trigger_schema,
c.relname AS event_object_table,
t.tgname AS trigger_name,
pg_catalog.pg_get_triggerdef(t.oid, false) AS trigger_definition
pg_catalog.pg_get_triggerdef(t.oid, false) AS trigger_definition,
COALESCE(t.tgoldtable, '') AS old_table,
COALESCE(t.tgnewtable, '') AS new_table
FROM pg_catalog.pg_trigger t
JOIN pg_catalog.pg_class c ON t.tgrelid = c.oid
JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid
Expand Down
16 changes: 11 additions & 5 deletions ir/queries/queries.sql.go

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

5 changes: 5 additions & 0 deletions testdata/diff/create_trigger/add_trigger_old_table/diff.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
CREATE OR REPLACE TRIGGER orders_delete_trigger
AFTER DELETE ON orders
REFERENCING OLD TABLE AS old_orders
FOR EACH STATEMENT
EXECUTE FUNCTION archive_deleted_orders();
25 changes: 25 additions & 0 deletions testdata/diff/create_trigger/add_trigger_old_table/new.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
CREATE TABLE public.orders (
id serial PRIMARY KEY,
amount numeric(10,2)
);

CREATE TABLE public.orders_archive (
id integer,
amount numeric(10,2),
deleted_at timestamp DEFAULT CURRENT_TIMESTAMP
);

CREATE OR REPLACE FUNCTION public.archive_deleted_orders()
RETURNS trigger AS $$
BEGIN
INSERT INTO orders_archive (id, amount)
SELECT id, amount FROM old_orders;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER orders_delete_trigger
AFTER DELETE ON public.orders
REFERENCING OLD TABLE AS old_orders
FOR EACH STATEMENT
EXECUTE FUNCTION public.archive_deleted_orders();
19 changes: 19 additions & 0 deletions testdata/diff/create_trigger/add_trigger_old_table/old.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
CREATE TABLE public.orders (
id serial PRIMARY KEY,
amount numeric(10,2)
);

CREATE TABLE public.orders_archive (
id integer,
amount numeric(10,2),
deleted_at timestamp DEFAULT CURRENT_TIMESTAMP
);

CREATE OR REPLACE FUNCTION public.archive_deleted_orders()
RETURNS trigger AS $$
BEGIN
INSERT INTO orders_archive (id, amount)
SELECT id, amount FROM old_orders;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
20 changes: 20 additions & 0 deletions testdata/diff/create_trigger/add_trigger_old_table/plan.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"version": "1.0.0",
"pgschema_version": "1.2.1",
"created_at": "1970-01-01T00:00:00Z",
"source_fingerprint": {
"hash": "087a74a25f6a817a556750d274e732048ca5281379e6c0bedd1f5a35176daa2e"
},
"groups": [
{
"steps": [
{
"sql": "CREATE OR REPLACE TRIGGER orders_delete_trigger\n AFTER DELETE ON orders\n REFERENCING OLD TABLE AS old_orders\n FOR EACH STATEMENT\n EXECUTE FUNCTION archive_deleted_orders();",
"type": "table.trigger",
"operation": "create",
"path": "public.orders.orders_delete_trigger"
}
]
}
]
}
5 changes: 5 additions & 0 deletions testdata/diff/create_trigger/add_trigger_old_table/plan.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
CREATE OR REPLACE TRIGGER orders_delete_trigger
AFTER DELETE ON orders
REFERENCING OLD TABLE AS old_orders
FOR EACH STATEMENT
EXECUTE FUNCTION archive_deleted_orders();
17 changes: 17 additions & 0 deletions testdata/diff/create_trigger/add_trigger_old_table/plan.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Plan: 1 to modify.

Summary by type:
tables: 1 to modify

Tables:
~ orders
+ orders_delete_trigger (trigger)

DDL to be executed:
--------------------------------------------------

CREATE OR REPLACE TRIGGER orders_delete_trigger
AFTER DELETE ON orders
REFERENCING OLD TABLE AS old_orders
FOR EACH STATEMENT
EXECUTE FUNCTION archive_deleted_orders();