From 99e0c6f3ff7f8546efc00a3a36921828bc93e3ab Mon Sep 17 00:00:00 2001 From: Tianzhou Date: Sat, 11 Oct 2025 01:07:38 +0800 Subject: [PATCH] feat: trigger old table --- internal/diff/trigger.go | 30 +++++++++++++++++-- ir/inspector.go | 18 +++++++++-- ir/ir.go | 2 ++ ir/parser.go | 15 ++++++++++ ir/queries/queries.sql | 4 ++- ir/queries/queries.sql.go | 16 ++++++---- .../add_trigger_old_table/diff.sql | 5 ++++ .../add_trigger_old_table/new.sql | 25 ++++++++++++++++ .../add_trigger_old_table/old.sql | 19 ++++++++++++ .../add_trigger_old_table/plan.json | 20 +++++++++++++ .../add_trigger_old_table/plan.sql | 5 ++++ .../add_trigger_old_table/plan.txt | 17 +++++++++++ 12 files changed, 166 insertions(+), 10 deletions(-) create mode 100644 testdata/diff/create_trigger/add_trigger_old_table/diff.sql create mode 100644 testdata/diff/create_trigger/add_trigger_old_table/new.sql create mode 100644 testdata/diff/create_trigger/add_trigger_old_table/old.sql create mode 100644 testdata/diff/create_trigger/add_trigger_old_table/plan.json create mode 100644 testdata/diff/create_trigger/add_trigger_old_table/plan.sql create mode 100644 testdata/diff/create_trigger/add_trigger_old_table/plan.txt diff --git a/internal/diff/trigger.go b/internal/diff/trigger.go index 8e9ba34d..859b7764 100644 --- a/internal/diff/trigger.go +++ b/internal/diff/trigger.go @@ -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 } @@ -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 @@ -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 diff --git a/ir/inspector.go b/ir/inspector.go index 17d590bd..95fca69b 100644 --- a/ir/inspector.go +++ b/ir/inspector.go @@ -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 == "" { + // Extract column name from sql.NullString + columnName := "" + if constraint.ColumnName.Valid { + columnName = constraint.ColumnName.String + } + + if columnName == "" || columnName == "" { continue // Skip constraints without columns } @@ -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 != "" { + trigger.NewTable = triggerRow.NewTable + } table.Triggers[triggerName] = trigger } } diff --git a/ir/ir.go b/ir/ir.go index 53cec557..63a71387 100644 --- a/ir/ir.go +++ b/ir/ir.go @@ -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 diff --git a/ir/parser.go b/ir/parser.go index 8cb08cc6..e1aac39c 100644 --- a/ir/parser.go +++ b/ir/parser.go @@ -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, @@ -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 diff --git a/ir/queries/queries.sql b/ir/queries/queries.sql index c9f7a95e..d7f71491 100644 --- a/ir/queries/queries.sql +++ b/ir/queries/queries.sql @@ -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 diff --git a/ir/queries/queries.sql.go b/ir/queries/queries.sql.go index 147933cc..84591917 100644 --- a/ir/queries/queries.sql.go +++ b/ir/queries/queries.sql.go @@ -615,8 +615,8 @@ type GetConstraintsRow struct { TableName string `db:"table_name" json:"table_name"` ConstraintName string `db:"constraint_name" json:"constraint_name"` ConstraintType sql.NullString `db:"constraint_type" json:"constraint_type"` - ColumnName string `db:"column_name" json:"column_name"` - OrdinalPosition int16 `db:"ordinal_position" json:"ordinal_position"` + ColumnName sql.NullString `db:"column_name" json:"column_name"` + OrdinalPosition sql.NullInt32 `db:"ordinal_position" json:"ordinal_position"` ForeignTableSchema sql.NullString `db:"foreign_table_schema" json:"foreign_table_schema"` ForeignTableName sql.NullString `db:"foreign_table_name" json:"foreign_table_name"` ForeignColumnName sql.NullString `db:"foreign_column_name" json:"foreign_column_name"` @@ -725,8 +725,8 @@ type GetConstraintsForSchemaRow struct { TableName string `db:"table_name" json:"table_name"` ConstraintName string `db:"constraint_name" json:"constraint_name"` ConstraintType sql.NullString `db:"constraint_type" json:"constraint_type"` - ColumnName string `db:"column_name" json:"column_name"` - OrdinalPosition int16 `db:"ordinal_position" json:"ordinal_position"` + ColumnName sql.NullString `db:"column_name" json:"column_name"` + OrdinalPosition sql.NullInt32 `db:"ordinal_position" json:"ordinal_position"` ForeignTableSchema sql.NullString `db:"foreign_table_schema" json:"foreign_table_schema"` ForeignTableName sql.NullString `db:"foreign_table_name" json:"foreign_table_name"` ForeignColumnName sql.NullString `db:"foreign_column_name" json:"foreign_column_name"` @@ -2274,7 +2274,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 @@ -2288,6 +2290,8 @@ type GetTriggersForSchemaRow struct { EventObjectTable string `db:"event_object_table" json:"event_object_table"` TriggerName string `db:"trigger_name" json:"trigger_name"` TriggerDefinition string `db:"trigger_definition" json:"trigger_definition"` + OldTable string `db:"old_table" json:"old_table"` + NewTable string `db:"new_table" json:"new_table"` } // GetTriggersForSchema retrieves all triggers for a specific schema @@ -2307,6 +2311,8 @@ func (q *Queries) GetTriggersForSchema(ctx context.Context, nspname string) ([]G &i.EventObjectTable, &i.TriggerName, &i.TriggerDefinition, + &i.OldTable, + &i.NewTable, ); err != nil { return nil, err } diff --git a/testdata/diff/create_trigger/add_trigger_old_table/diff.sql b/testdata/diff/create_trigger/add_trigger_old_table/diff.sql new file mode 100644 index 00000000..a13a5a0f --- /dev/null +++ b/testdata/diff/create_trigger/add_trigger_old_table/diff.sql @@ -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(); diff --git a/testdata/diff/create_trigger/add_trigger_old_table/new.sql b/testdata/diff/create_trigger/add_trigger_old_table/new.sql new file mode 100644 index 00000000..0c6ad204 --- /dev/null +++ b/testdata/diff/create_trigger/add_trigger_old_table/new.sql @@ -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(); diff --git a/testdata/diff/create_trigger/add_trigger_old_table/old.sql b/testdata/diff/create_trigger/add_trigger_old_table/old.sql new file mode 100644 index 00000000..360a024e --- /dev/null +++ b/testdata/diff/create_trigger/add_trigger_old_table/old.sql @@ -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; diff --git a/testdata/diff/create_trigger/add_trigger_old_table/plan.json b/testdata/diff/create_trigger/add_trigger_old_table/plan.json new file mode 100644 index 00000000..e5587ad6 --- /dev/null +++ b/testdata/diff/create_trigger/add_trigger_old_table/plan.json @@ -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" + } + ] + } + ] +} diff --git a/testdata/diff/create_trigger/add_trigger_old_table/plan.sql b/testdata/diff/create_trigger/add_trigger_old_table/plan.sql new file mode 100644 index 00000000..a13a5a0f --- /dev/null +++ b/testdata/diff/create_trigger/add_trigger_old_table/plan.sql @@ -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(); diff --git a/testdata/diff/create_trigger/add_trigger_old_table/plan.txt b/testdata/diff/create_trigger/add_trigger_old_table/plan.txt new file mode 100644 index 00000000..9db250ef --- /dev/null +++ b/testdata/diff/create_trigger/add_trigger_old_table/plan.txt @@ -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();