Skip to content

Commit 63a6400

Browse files
authored
feat: support CONSTRAINT TRIGGER (#63)
1 parent a033416 commit 63a6400

File tree

12 files changed

+181
-37
lines changed

12 files changed

+181
-37
lines changed

internal/diff/table.go

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -971,17 +971,44 @@ func (td *tableDiff) generateAlterTableStatements(targetSchema string, collector
971971

972972
// Modify triggers - already sorted by the Diff operation
973973
for _, triggerDiff := range td.ModifiedTriggers {
974-
// Use CREATE OR REPLACE for modified triggers
975-
sql := generateTriggerSQLWithMode(triggerDiff.New, targetSchema)
974+
// Constraint triggers don't support CREATE OR REPLACE, so we need to DROP and CREATE
975+
if triggerDiff.New.IsConstraint {
976+
tableName := getTableNameWithSchema(td.Table.Schema, td.Table.Name, targetSchema)
976977

977-
context := &diffContext{
978-
Type: DiffTypeTableTrigger,
979-
Operation: DiffOperationAlter,
980-
Path: fmt.Sprintf("%s.%s.%s", td.Table.Schema, td.Table.Name, triggerDiff.New.Name),
981-
Source: triggerDiff,
982-
CanRunInTransaction: true,
978+
// Step 1: DROP the old trigger
979+
dropSQL := fmt.Sprintf("DROP TRIGGER IF EXISTS %s ON %s;", triggerDiff.Old.Name, tableName)
980+
dropContext := &diffContext{
981+
Type: DiffTypeTableTrigger,
982+
Operation: DiffOperationDrop,
983+
Path: fmt.Sprintf("%s.%s.%s", td.Table.Schema, td.Table.Name, triggerDiff.Old.Name),
984+
Source: triggerDiff.Old,
985+
CanRunInTransaction: true,
986+
}
987+
collector.collect(dropContext, dropSQL)
988+
989+
// Step 2: CREATE the new constraint trigger
990+
createSQL := generateTriggerSQLWithMode(triggerDiff.New, targetSchema)
991+
createContext := &diffContext{
992+
Type: DiffTypeTableTrigger,
993+
Operation: DiffOperationCreate,
994+
Path: fmt.Sprintf("%s.%s.%s", td.Table.Schema, td.Table.Name, triggerDiff.New.Name),
995+
Source: triggerDiff.New,
996+
CanRunInTransaction: true,
997+
}
998+
collector.collect(createContext, createSQL)
999+
} else {
1000+
// Use CREATE OR REPLACE for regular triggers
1001+
sql := generateTriggerSQLWithMode(triggerDiff.New, targetSchema)
1002+
1003+
context := &diffContext{
1004+
Type: DiffTypeTableTrigger,
1005+
Operation: DiffOperationAlter,
1006+
Path: fmt.Sprintf("%s.%s.%s", td.Table.Schema, td.Table.Name, triggerDiff.New.Name),
1007+
Source: triggerDiff,
1008+
CanRunInTransaction: true,
1009+
}
1010+
collector.collect(context, sql)
9831011
}
984-
collector.collect(context, sql)
9851012
}
9861013

9871014
// Modify policies - already sorted by the Diff operation

internal/diff/trigger.go

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,17 @@ func triggersEqual(old, new *ir.Trigger) bool {
4646
}
4747
}
4848

49+
// Compare constraint trigger properties
50+
if old.IsConstraint != new.IsConstraint {
51+
return false
52+
}
53+
if old.Deferrable != new.Deferrable {
54+
return false
55+
}
56+
if old.InitiallyDeferred != new.InitiallyDeferred {
57+
return false
58+
}
59+
4960
return true
5061
}
5162

@@ -122,7 +133,7 @@ func generateCreateTriggersSQL(triggers []*ir.Trigger, targetSchema string, coll
122133
}
123134
}
124135

125-
// generateTriggerSQLWithMode generates CREATE [OR REPLACE] TRIGGER statement
136+
// generateTriggerSQLWithMode generates CREATE [OR REPLACE] TRIGGER or CREATE CONSTRAINT TRIGGER statement
126137
func generateTriggerSQLWithMode(trigger *ir.Trigger, targetSchema string) string {
127138
// Build event list in standard order: INSERT, UPDATE, DELETE, TRUNCATE
128139
var events []string
@@ -141,8 +152,26 @@ func generateTriggerSQLWithMode(trigger *ir.Trigger, targetSchema string) string
141152
tableName := qualifyEntityName(trigger.Schema, trigger.Table, targetSchema)
142153

143154
// Build the trigger statement with proper formatting
144-
stmt := fmt.Sprintf("CREATE OR REPLACE TRIGGER %s\n %s %s ON %s\n FOR EACH %s",
145-
trigger.Name, trigger.Timing, eventList, tableName, trigger.Level)
155+
// Use CREATE CONSTRAINT TRIGGER for constraint triggers (cannot use OR REPLACE)
156+
var stmt string
157+
if trigger.IsConstraint {
158+
stmt = fmt.Sprintf("CREATE CONSTRAINT TRIGGER %s\n %s %s ON %s",
159+
trigger.Name, trigger.Timing, eventList, tableName)
160+
161+
// Add deferrable clause for constraint triggers
162+
if trigger.Deferrable {
163+
if trigger.InitiallyDeferred {
164+
stmt += "\n DEFERRABLE INITIALLY DEFERRED"
165+
} else {
166+
stmt += "\n DEFERRABLE INITIALLY IMMEDIATE"
167+
}
168+
}
169+
170+
stmt += fmt.Sprintf("\n FOR EACH %s", trigger.Level)
171+
} else {
172+
stmt = fmt.Sprintf("CREATE OR REPLACE TRIGGER %s\n %s %s ON %s\n FOR EACH %s",
173+
trigger.Name, trigger.Timing, eventList, tableName, trigger.Level)
174+
}
146175

147176
// Add WHEN clause if present
148177
if trigger.Condition != "" {

ir/ir.go

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -227,15 +227,18 @@ const (
227227

228228
// Trigger represents a database trigger
229229
type Trigger struct {
230-
Schema string `json:"schema"`
231-
Table string `json:"table"`
232-
Name string `json:"name"`
233-
Timing TriggerTiming `json:"timing"` // BEFORE, AFTER, INSTEAD OF
234-
Events []TriggerEvent `json:"events"` // INSERT, UPDATE, DELETE
235-
Level TriggerLevel `json:"level"` // ROW, STATEMENT
236-
Function string `json:"function"`
237-
Condition string `json:"condition,omitempty"` // WHEN condition
238-
Comment string `json:"comment,omitempty"`
230+
Schema string `json:"schema"`
231+
Table string `json:"table"`
232+
Name string `json:"name"`
233+
Timing TriggerTiming `json:"timing"` // BEFORE, AFTER, INSTEAD OF
234+
Events []TriggerEvent `json:"events"` // INSERT, UPDATE, DELETE
235+
Level TriggerLevel `json:"level"` // ROW, STATEMENT
236+
Function string `json:"function"`
237+
Condition string `json:"condition,omitempty"` // WHEN condition
238+
Comment string `json:"comment,omitempty"`
239+
IsConstraint bool `json:"is_constraint,omitempty"` // Whether this is a constraint trigger
240+
Deferrable bool `json:"deferrable,omitempty"` // Can be deferred until end of transaction
241+
InitiallyDeferred bool `json:"initially_deferred,omitempty"` // Whether deferred by default
239242
}
240243

241244
// TriggerTiming represents the timing of trigger execution

ir/parser.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3239,14 +3239,17 @@ func (p *Parser) parseCreateTrigger(triggerStmt *pg_query.CreateTrigStmt) error
32393239

32403240
// Create trigger
32413241
trigger := &Trigger{
3242-
Schema: schemaName,
3243-
Table: tableName,
3244-
Name: triggerStmt.Trigname,
3245-
Timing: timing,
3246-
Events: events,
3247-
Level: level,
3248-
Function: function,
3249-
Condition: condition,
3242+
Schema: schemaName,
3243+
Table: tableName,
3244+
Name: triggerStmt.Trigname,
3245+
Timing: timing,
3246+
Events: events,
3247+
Level: level,
3248+
Function: function,
3249+
Condition: condition,
3250+
IsConstraint: triggerStmt.Isconstraint,
3251+
Deferrable: triggerStmt.Deferrable,
3252+
InitiallyDeferred: triggerStmt.Initdeferred,
32503253
}
32513254

32523255
// Add trigger to table only

ir/queries/queries.sql

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,8 @@ SELECT
176176
WHEN 'x' THEN 'EXCLUDE'
177177
ELSE 'UNKNOWN'
178178
END AS constraint_type,
179-
a.attname AS column_name,
180-
a.attnum AS ordinal_position,
179+
COALESCE(a.attname, '') AS column_name,
180+
COALESCE(a.attnum, 0) AS ordinal_position,
181181
COALESCE(fn.nspname, '') AS foreign_table_schema,
182182
COALESCE(fcl.relname, '') AS foreign_table_name,
183183
COALESCE(fa.attname, '') AS foreign_column_name,
@@ -663,8 +663,8 @@ SELECT
663663
WHEN 'x' THEN 'EXCLUDE'
664664
ELSE 'UNKNOWN'
665665
END AS constraint_type,
666-
a.attname AS column_name,
667-
a.attnum AS ordinal_position,
666+
COALESCE(a.attname, '') AS column_name,
667+
COALESCE(a.attnum, 0) AS ordinal_position,
668668
COALESCE(fn.nspname, '') AS foreign_table_schema,
669669
COALESCE(fcl.relname, '') AS foreign_table_name,
670670
COALESCE(fa.attname, '') AS foreign_column_name,

ir/queries/queries.sql.go

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
CREATE CONSTRAINT TRIGGER prevent_code_update_trigger
2+
AFTER UPDATE ON products
3+
DEFERRABLE INITIALLY IMMEDIATE
4+
FOR EACH ROW
5+
EXECUTE FUNCTION prevent_code_update();
6+
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
CREATE TABLE public.products (
2+
id integer PRIMARY KEY,
3+
code text NOT NULL
4+
);
5+
6+
CREATE OR REPLACE FUNCTION public.prevent_code_update()
7+
RETURNS trigger AS $$
8+
BEGIN
9+
IF OLD.code IS DISTINCT FROM NEW.code THEN
10+
RAISE EXCEPTION 'Product code cannot be updated';
11+
END IF;
12+
RETURN NEW;
13+
END;
14+
$$ LANGUAGE plpgsql;
15+
16+
CREATE CONSTRAINT TRIGGER prevent_code_update_trigger
17+
AFTER UPDATE ON public.products
18+
DEFERRABLE INITIALLY IMMEDIATE
19+
FOR EACH ROW
20+
EXECUTE FUNCTION public.prevent_code_update();
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
CREATE TABLE public.products (
2+
id integer PRIMARY KEY,
3+
code text NOT NULL
4+
);
5+
6+
CREATE OR REPLACE FUNCTION public.prevent_code_update()
7+
RETURNS trigger AS $$
8+
BEGIN
9+
IF OLD.code IS DISTINCT FROM NEW.code THEN
10+
RAISE EXCEPTION 'Product code cannot be updated';
11+
END IF;
12+
RETURN NEW;
13+
END;
14+
$$ LANGUAGE plpgsql;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"version": "1.0.0",
3+
"pgschema_version": "1.2.1",
4+
"created_at": "1970-01-01T00:00:00Z",
5+
"source_fingerprint": {
6+
"hash": "08f4c7258568484a7571fb1a332a97279e6dcb5336ddbbf021d777cc1b73730d"
7+
},
8+
"groups": [
9+
{
10+
"steps": [
11+
{
12+
"sql": "CREATE CONSTRAINT TRIGGER prevent_code_update_trigger\n AFTER UPDATE ON products\n DEFERRABLE INITIALLY IMMEDIATE\n FOR EACH ROW\n EXECUTE FUNCTION prevent_code_update();",
13+
"type": "table.trigger",
14+
"operation": "create",
15+
"path": "public.products.prevent_code_update_trigger"
16+
}
17+
]
18+
}
19+
]
20+
}

0 commit comments

Comments
 (0)