diff --git a/doc/src/sgml/ref/create_trigger.sgml b/doc/src/sgml/ref/create_trigger.sgml index 289dd1d..feb30ab 100644 --- a/doc/src/sgml/ref/create_trigger.sgml +++ b/doc/src/sgml/ref/create_trigger.sgml @@ -26,7 +26,7 @@ PostgreSQL documentation -CREATE [ CONSTRAINT ] TRIGGER name { BEFORE | AFTER | INSTEAD OF } { event [ OR ... ] } +CREATE [ OR REPLACE ] [ CONSTRAINT ] TRIGGER name { BEFORE | AFTER | INSTEAD OF } { event [ OR ... ] } ON table_name [ FROM referenced_table_name ] [ NOT DEFERRABLE | [ DEFERRABLE ] [ INITIALLY IMMEDIATE | INITIALLY DEFERRED ] ] @@ -48,14 +48,22 @@ CREATE [ CONSTRAINT ] TRIGGER name Description - CREATE TRIGGER creates a new trigger. The - trigger will be associated with the specified table, view, or foreign table + CREATE TRIGGER creates a new trigger. + CREATE OR REPLACE TRIGGER will either create a + new trigger, or replace an existing trigger. The trigger will be + associated with the specified table, view, or foreign table and will execute the specified function function_name when certain operations are performed on that table. + To replace the current definition of an existing trigger, use + CREATE OR REPLACE TRIGGER, by specifying the same + trigger's name and the corresponding table's name where the trigger belongs. + + + The trigger can be specified to fire before the operation is attempted on a row (before constraints are checked and the INSERT, UPDATE, or @@ -446,6 +454,15 @@ UPDATE OF column_name1 [, column_name2 + When replacing an existing trigger with CREATE OR + REPLACE TRIGGER, there are restrictions. You cannot replace + triggers with a different type of trigger, that means it is impossible + to replace regular trigger with constraint trigger and vise versa. + Also, in a transaction you cannot replace triggers that + are already fired and have pending events. + + + A column-specific trigger (one defined using the UPDATE OF column_name syntax) will fire when any of its columns are listed as targets in the UPDATE diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c index 67144aa..1a52fa3 100644 --- a/src/backend/catalog/heap.c +++ b/src/backend/catalog/heap.c @@ -2434,7 +2434,8 @@ StoreRelCheck(Relation rel, const char *ccname, Node *expr, * Create the Check Constraint */ constrOid = - CreateConstraintEntry(ccname, /* Constraint Name */ + CreateConstraintEntry(InvalidOid, + ccname, /* Constraint Name */ RelationGetNamespace(rel), /* namespace */ CONSTRAINT_CHECK, /* Constraint Type */ false, /* Is Deferrable */ diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c index 117e3fd..e546584 100644 --- a/src/backend/catalog/index.c +++ b/src/backend/catalog/index.c @@ -1862,7 +1862,8 @@ index_constraint_create(Relation heapRelation, /* * Construct a pg_constraint entry. */ - conOid = CreateConstraintEntry(constraintName, + conOid = CreateConstraintEntry(InvalidOid, + constraintName, namespaceId, constraintType, deferrable, @@ -1927,6 +1928,8 @@ index_constraint_create(Relation heapRelation, CreateTrigStmt *trigger; trigger = makeNode(CreateTrigStmt); + trigger->replace = false; + trigger->isconstraint = true; trigger->trigname = (constraintType == CONSTRAINT_PRIMARY) ? "PK_ConstraintTrigger" : "Unique_ConstraintTrigger"; @@ -1938,7 +1941,6 @@ index_constraint_create(Relation heapRelation, trigger->events = TRIGGER_TYPE_INSERT | TRIGGER_TYPE_UPDATE; trigger->columns = NIL; trigger->whenClause = NULL; - trigger->isconstraint = true; trigger->deferrable = true; trigger->initdeferred = initdeferred; trigger->constrrel = NULL; diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c index 0d70cb0..3f35e50 100644 --- a/src/backend/catalog/pg_constraint.c +++ b/src/backend/catalog/pg_constraint.c @@ -44,10 +44,12 @@ * constraint) are *not* created here. But we do make dependency links * from the constraint to the things it depends on. * - * The new constraint's OID is returned. + * The new constraint's OID is returned. (This will be the same as + * "existing_constraint_oid" if that is other than InvalidOid) */ Oid -CreateConstraintEntry(const char *constraintName, +CreateConstraintEntry(Oid existing_constraint_oid, + const char *constraintName, Oid constraintNamespace, char constraintType, bool isDeferrable, @@ -78,7 +80,7 @@ CreateConstraintEntry(const char *constraintName, bool is_internal) { Relation conDesc; - Oid conOid; + Oid conOid = InvalidOid; HeapTuple tup; bool nulls[Natts_pg_constraint]; Datum values[Natts_pg_constraint]; @@ -165,9 +167,12 @@ CreateConstraintEntry(const char *constraintName, values[i] = (Datum) NULL; } - conOid = GetNewOidWithIndex(conDesc, ConstraintOidIndexId, - Anum_pg_constraint_oid); - values[Anum_pg_constraint_oid - 1] = ObjectIdGetDatum(conOid); + if (!OidIsValid(existing_constraint_oid)) + { + conOid = GetNewOidWithIndex(conDesc, ConstraintOidIndexId, + Anum_pg_constraint_oid); + values[Anum_pg_constraint_oid - 1] = ObjectIdGetDatum(conOid); + } values[Anum_pg_constraint_conname - 1] = NameGetDatum(&cname); values[Anum_pg_constraint_connamespace - 1] = ObjectIdGetDatum(constraintNamespace); values[Anum_pg_constraint_contype - 1] = CharGetDatum(constraintType); @@ -221,9 +226,57 @@ CreateConstraintEntry(const char *constraintName, else nulls[Anum_pg_constraint_conbin - 1] = true; - tup = heap_form_tuple(RelationGetDescr(conDesc), values, nulls); + if (OidIsValid(existing_constraint_oid)) + { + /* + * Replace the existing constraint entry. + */ + SysScanDesc conscan; + ScanKeyData skey[2]; + HeapTuple tuple; + bool replaces[Natts_pg_constraint]; + Form_pg_constraint constrForm; + + ScanKeyInit(&skey[0], + Anum_pg_constraint_oid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(existing_constraint_oid)); + + conscan = systable_beginscan(conDesc, + ConstraintOidIndexId, + true, + NULL, + 1, + skey); - CatalogTupleInsert(conDesc, tup); + tuple = systable_getnext(conscan); + Assert(HeapTupleIsValid(tuple)); + constrForm = (Form_pg_constraint) GETSTRUCT(tuple); + conOid = constrForm->oid; + Assert(conOid == existing_constraint_oid); + + memset(replaces, true, sizeof(replaces)); + replaces[Anum_pg_constraint_oid - 1] = false; /* skip updating Oid + * data */ + replaces[Anum_pg_constraint_conname - 1] = false; + replaces[Anum_pg_constraint_confrelid - 1] = false; + + /* Modify the existing constraint entry */ + tup = heap_modify_tuple(tuple, RelationGetDescr(conDesc), values, nulls, replaces); + CatalogTupleUpdate(conDesc, &tuple->t_self, tup); + heap_freetuple(tup); + + /* Remove all old dependencies before registering new ones */ + deleteDependencyRecordsFor(ConstraintRelationId, conOid, true); + + systable_endscan(conscan); + } + else + { + tup = heap_form_tuple(RelationGetDescr(conDesc), values, nulls); + CatalogTupleInsert(conDesc, tup); + heap_freetuple(tup); + } ObjectAddressSet(conobject, ConstraintRelationId, conOid); diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 3e57c7f..a7c906e 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -8799,7 +8799,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel, /* * Record the FK constraint in pg_constraint. */ - constrOid = CreateConstraintEntry(conname, + constrOid = CreateConstraintEntry(InvalidOid, + conname, RelationGetNamespace(rel), CONSTRAINT_FOREIGN, fkconstraint->deferrable, @@ -9067,7 +9068,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel, else conname = fkconstraint->conname; constrOid = - CreateConstraintEntry(conname, + CreateConstraintEntry(InvalidOid, + conname, RelationGetNamespace(partition), CONSTRAINT_FOREIGN, fkconstraint->deferrable, @@ -9469,7 +9471,8 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel) indexOid = constrForm->conindid; constrOid = - CreateConstraintEntry(fkconstraint->conname, + CreateConstraintEntry(InvalidOid, + fkconstraint->conname, constrForm->connamespace, CONSTRAINT_FOREIGN, fkconstraint->deferrable, @@ -10480,6 +10483,8 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint, * and "RI_ConstraintTrigger_c_NNNN" for the check triggers. */ fk_trigger = makeNode(CreateTrigStmt); + fk_trigger->replace = false; + fk_trigger->isconstraint = true; fk_trigger->trigname = "RI_ConstraintTrigger_c"; fk_trigger->relation = NULL; fk_trigger->row = true; @@ -10500,7 +10505,6 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint, fk_trigger->columns = NIL; fk_trigger->transitionRels = NIL; fk_trigger->whenClause = NULL; - fk_trigger->isconstraint = true; fk_trigger->deferrable = fkconstraint->deferrable; fk_trigger->initdeferred = fkconstraint->initdeferred; fk_trigger->constrrel = NULL; @@ -10529,6 +10533,8 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr * DELETE action on the referenced table. */ fk_trigger = makeNode(CreateTrigStmt); + fk_trigger->replace = false; + fk_trigger->isconstraint = true; fk_trigger->trigname = "RI_ConstraintTrigger_a"; fk_trigger->relation = NULL; fk_trigger->row = true; @@ -10537,7 +10543,6 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr fk_trigger->columns = NIL; fk_trigger->transitionRels = NIL; fk_trigger->whenClause = NULL; - fk_trigger->isconstraint = true; fk_trigger->constrrel = NULL; switch (fkconstraint->fk_del_action) { @@ -10585,6 +10590,8 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr * UPDATE action on the referenced table. */ fk_trigger = makeNode(CreateTrigStmt); + fk_trigger->replace = false; + fk_trigger->isconstraint = true; fk_trigger->trigname = "RI_ConstraintTrigger_a"; fk_trigger->relation = NULL; fk_trigger->row = true; @@ -10593,7 +10600,6 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr fk_trigger->columns = NIL; fk_trigger->transitionRels = NIL; fk_trigger->whenClause = NULL; - fk_trigger->isconstraint = true; fk_trigger->constrrel = NULL; switch (fkconstraint->fk_upd_action) { diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c index 672fccf..27e72df 100644 --- a/src/backend/commands/trigger.c +++ b/src/backend/commands/trigger.c @@ -31,6 +31,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" +#include "catalog/pg_depend.h" #include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/trigger.h" @@ -175,7 +176,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString, Relation pgrel; HeapTuple tuple; Oid funcrettype; - Oid trigoid; + Oid trigoid = InvalidOid; char internaltrigname[NAMEDATALEN]; char *trigname; Oid constrrelid = InvalidOid; @@ -184,6 +185,10 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString, char *oldtablename = NULL; char *newtablename = NULL; bool partition_recurse; + bool is_update = false; + Oid existing_constraint_oid = InvalidOid; + bool trigger_exists = false; + bool trigger_deferrable = false; if (OidIsValid(relOid)) rel = table_open(relOid, ShareRowExclusiveLock); @@ -668,6 +673,83 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString, whenRtable = NIL; } + /* Check if there is a pre-existing trigger of the same name */ + tgrel = table_open(TriggerRelationId, RowExclusiveLock); + if (!isInternal) + { + ScanKeyInit(&key, + Anum_pg_trigger_tgrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(rel))); + tgscan = systable_beginscan(tgrel, TriggerRelidNameIndexId, true, + NULL, 1, &key); + while (HeapTupleIsValid(tuple = systable_getnext(tgscan))) + { + Form_pg_trigger pg_trigger = (Form_pg_trigger) GETSTRUCT(tuple); + + if (namestrcmp(&(pg_trigger->tgname), stmt->trigname) == 0) + { + /* Store values for replacement of this trigger */ + trigoid = pg_trigger->oid; + existing_constraint_oid = pg_trigger->tgconstraint; + trigger_exists = true; + trigger_deferrable = pg_trigger->tgdeferrable; + break; + } + } + systable_endscan(tgscan); + } + + /* Generate the trigger's oid because there was no same name trigger. */ + if (!trigger_exists) + trigoid = GetNewOidWithIndex(tgrel, TriggerOidIndexId, + Anum_pg_trigger_oid); + else + { + /* + * without OR REPLACE clause, can't override the trigger with the same + * name. + */ + if (!stmt->replace) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("trigger \"%s\" for relation \"%s\" already exists", + stmt->trigname, RelationGetRelationName(rel)))); + else + { + /* + * If this trigger has pending events, throw an error. + */ + if (trigger_deferrable && AfterTriggerPendingOnRel(RelationGetRelid(rel))) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_IN_USE), + errmsg("cannot replace \"%s\" on \"%s\" because it has pending trigger events", + stmt->trigname, RelationGetRelationName(rel)))); + + /* + * CREATE OR REPLACE CONSTRAINT TRIGGER command can't replace + * non-constraint trigger. + */ + if (stmt->isconstraint && !OidIsValid(existing_constraint_oid)) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("trigger \"%s\" for relation \"%s\" is a regular trigger", + stmt->trigname, RelationGetRelationName(rel)), + errhint("use CREATE OR REPLACE TRIGGER to replace a regular trigger"))); + + /* + * CREATE OR REPLACE TRIGGER command can't replace constraint + * trigger. + */ + if (!stmt->isconstraint && OidIsValid(existing_constraint_oid)) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("trigger \"%s\" for relation \"%s\" is a constraint trigger", + stmt->trigname, RelationGetRelationName(rel)), + errhint("use CREATE OR REPLACE CONSTRAINT TRIGGER to replace a constraint trigger"))); + } + } + /* * Find and validate the trigger function. */ @@ -695,7 +777,8 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString, { /* Internal callers should have made their own constraints */ Assert(!isInternal); - constraintOid = CreateConstraintEntry(stmt->trigname, + constraintOid = CreateConstraintEntry(existing_constraint_oid, + stmt->trigname, RelationGetNamespace(rel), CONSTRAINT_TRIGGER, stmt->deferrable, @@ -727,15 +810,6 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString, } /* - * Generate the trigger's OID now, so that we can use it in the name if - * needed. - */ - tgrel = table_open(TriggerRelationId, RowExclusiveLock); - - trigoid = GetNewOidWithIndex(tgrel, TriggerOidIndexId, - Anum_pg_trigger_oid); - - /* * If trigger is internally generated, modify the provided trigger name to * ensure uniqueness by appending the trigger OID. (Callers will usually * supply a simple constant trigger name in these cases.) @@ -753,37 +827,6 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString, } /* - * Scan pg_trigger for existing triggers on relation. We do this only to - * give a nice error message if there's already a trigger of the same - * name. (The unique index on tgrelid/tgname would complain anyway.) We - * can skip this for internally generated triggers, since the name - * modification above should be sufficient. - * - * NOTE that this is cool only because we have ShareRowExclusiveLock on - * the relation, so the trigger set won't be changing underneath us. - */ - if (!isInternal) - { - ScanKeyInit(&key, - Anum_pg_trigger_tgrelid, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(RelationGetRelid(rel))); - tgscan = systable_beginscan(tgrel, TriggerRelidNameIndexId, true, - NULL, 1, &key); - while (HeapTupleIsValid(tuple = systable_getnext(tgscan))) - { - Form_pg_trigger pg_trigger = (Form_pg_trigger) GETSTRUCT(tuple); - - if (namestrcmp(&(pg_trigger->tgname), trigname) == 0) - ereport(ERROR, - (errcode(ERRCODE_DUPLICATE_OBJECT), - errmsg("trigger \"%s\" for relation \"%s\" already exists", - trigname, RelationGetRelationName(rel)))); - } - systable_endscan(tgscan); - } - - /* * Build the new pg_trigger tuple. * * When we're creating a trigger in a partition, we mark it as internal, @@ -911,12 +954,56 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString, tuple = heap_form_tuple(tgrel->rd_att, values, nulls); - /* - * Insert tuple into pg_trigger. - */ - CatalogTupleInsert(tgrel, tuple); + if (!trigger_exists) + { + tuple = heap_form_tuple(tgrel->rd_att, values, nulls); + + /* + * Insert tuple into pg_trigger. + */ + CatalogTupleInsert(tgrel, tuple); + + heap_freetuple(tuple); + } + else + { + HeapTuple newtup; + TupleDesc tupDesc; + bool replaces[Natts_pg_trigger]; + + memset(replaces, true, sizeof(replaces)); + + ScanKeyInit(&key, + Anum_pg_trigger_tgrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(rel))); + tgscan = systable_beginscan(tgrel, TriggerRelidNameIndexId, true, + NULL, 1, &key); + while (HeapTupleIsValid(tuple = systable_getnext(tgscan))) + { + Form_pg_trigger pg_trigger = (Form_pg_trigger) GETSTRUCT(tuple); + + if (namestrcmp(&(pg_trigger->tgname), trigname) == 0) + { + tupDesc = RelationGetDescr(tgrel); + replaces[Anum_pg_trigger_oid - 1] = false; /* skip updating Oid + * data */ + replaces[Anum_pg_trigger_tgrelid - 1] = false; + replaces[Anum_pg_trigger_tgname - 1] = false; + trigoid = pg_trigger->oid; + newtup = heap_modify_tuple(tuple, tupDesc, values, nulls, replaces); + + /* Update tuple in pg_trigger */ + CatalogTupleUpdate(tgrel, &tuple->t_self, newtup); + + heap_freetuple(newtup); + is_update = true; + break; + } + } + systable_endscan(tgscan); + } - heap_freetuple(tuple); table_close(tgrel, RowExclusiveLock); pfree(DatumGetPointer(values[Anum_pg_trigger_tgname - 1])); @@ -959,6 +1046,15 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString, myself.objectId = trigoid; myself.objectSubId = 0; + /* + * In order to replace trigger, trigger should not be dependent on old + * referenced objects. Remove the old dependencies and then register new + * ones. In this case, while the old referenced object gets dropped, + * trigger will remain in the database. + */ + if (is_update) + deleteDependencyRecordsFor(myself.classId, myself.objectId, true); + referenced.classId = ProcedureRelationId; referenced.objectId = funcoid; referenced.objectSubId = 0; diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c index 483bb65..8e25349 100644 --- a/src/backend/commands/typecmds.c +++ b/src/backend/commands/typecmds.c @@ -3100,7 +3100,8 @@ domainAddConstraint(Oid domainOid, Oid domainNamespace, Oid baseTypeOid, * Store the constraint in pg_constraint */ ccoid = - CreateConstraintEntry(constr->conname, /* Constraint Name */ + CreateConstraintEntry(InvalidOid, + constr->conname, /* Constraint Name */ domainNamespace, /* namespace */ CONSTRAINT_CHECK, /* Constraint Type */ false, /* Is Deferrable */ diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c index 0409a40..d90c0bf 100644 --- a/src/backend/nodes/copyfuncs.c +++ b/src/backend/nodes/copyfuncs.c @@ -4315,6 +4315,8 @@ _copyCreateTrigStmt(const CreateTrigStmt *from) CreateTrigStmt *newnode = makeNode(CreateTrigStmt); COPY_STRING_FIELD(trigname); + COPY_SCALAR_FIELD(replace); + COPY_SCALAR_FIELD(isconstraint); COPY_NODE_FIELD(relation); COPY_NODE_FIELD(funcname); COPY_NODE_FIELD(args); @@ -4323,7 +4325,6 @@ _copyCreateTrigStmt(const CreateTrigStmt *from) COPY_SCALAR_FIELD(events); COPY_NODE_FIELD(columns); COPY_NODE_FIELD(whenClause); - COPY_SCALAR_FIELD(isconstraint); COPY_NODE_FIELD(transitionRels); COPY_SCALAR_FIELD(deferrable); COPY_SCALAR_FIELD(initdeferred); diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c index e2d1b98..6e261db 100644 --- a/src/backend/nodes/equalfuncs.c +++ b/src/backend/nodes/equalfuncs.c @@ -2020,6 +2020,8 @@ static bool _equalCreateTrigStmt(const CreateTrigStmt *a, const CreateTrigStmt *b) { COMPARE_STRING_FIELD(trigname); + COMPARE_SCALAR_FIELD(replace); + COMPARE_SCALAR_FIELD(isconstraint); COMPARE_NODE_FIELD(relation); COMPARE_NODE_FIELD(funcname); COMPARE_NODE_FIELD(args); @@ -2028,7 +2030,6 @@ _equalCreateTrigStmt(const CreateTrigStmt *a, const CreateTrigStmt *b) COMPARE_SCALAR_FIELD(events); COMPARE_NODE_FIELD(columns); COMPARE_NODE_FIELD(whenClause); - COMPARE_SCALAR_FIELD(isconstraint); COMPARE_NODE_FIELD(transitionRels); COMPARE_SCALAR_FIELD(deferrable); COMPARE_SCALAR_FIELD(initdeferred); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index c5154b8..533d48d 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -5214,48 +5214,50 @@ am_type: *****************************************************************************/ CreateTrigStmt: - CREATE TRIGGER name TriggerActionTime TriggerEvents ON + CREATE opt_or_replace TRIGGER name TriggerActionTime TriggerEvents ON qualified_name TriggerReferencing TriggerForSpec TriggerWhen EXECUTE FUNCTION_or_PROCEDURE func_name '(' TriggerFuncArgs ')' { CreateTrigStmt *n = makeNode(CreateTrigStmt); - n->trigname = $3; - n->relation = $7; - n->funcname = $13; - n->args = $15; - n->row = $9; - n->timing = $4; - n->events = intVal(linitial($5)); - n->columns = (List *) lsecond($5); - n->whenClause = $10; - n->transitionRels = $8; + n->replace = $2; + n->trigname = $4; + n->relation = $8; + n->funcname = $14; + n->args = $16; + n->row = $10; + n->timing = $5; + n->events = intVal(linitial($6)); + n->columns = (List *) lsecond($6); + n->whenClause = $11; + n->transitionRels = $9; n->isconstraint = false; n->deferrable = false; n->initdeferred = false; n->constrrel = NULL; $$ = (Node *)n; } - | CREATE CONSTRAINT TRIGGER name AFTER TriggerEvents ON + | CREATE opt_or_replace CONSTRAINT TRIGGER name AFTER TriggerEvents ON qualified_name OptConstrFromTable ConstraintAttributeSpec FOR EACH ROW TriggerWhen EXECUTE FUNCTION_or_PROCEDURE func_name '(' TriggerFuncArgs ')' { CreateTrigStmt *n = makeNode(CreateTrigStmt); - n->trigname = $4; - n->relation = $8; - n->funcname = $17; - n->args = $19; + n->replace = $2; + n->trigname = $5; + n->relation = $9; + n->funcname = $18; + n->args = $20; n->row = true; n->timing = TRIGGER_TYPE_AFTER; - n->events = intVal(linitial($6)); - n->columns = (List *) lsecond($6); - n->whenClause = $14; + n->events = intVal(linitial($7)); + n->columns = (List *) lsecond($7); + n->whenClause = $15; n->transitionRels = NIL; n->isconstraint = true; - processCASbits($10, @10, "TRIGGER", + processCASbits($11, @11, "TRIGGER", &n->deferrable, &n->initdeferred, NULL, NULL, yyscanner); - n->constrrel = $9; + n->constrrel = $10; $$ = (Node *)n; } ; diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h index 9600ece..0d938f1 100644 --- a/src/include/catalog/pg_constraint.h +++ b/src/include/catalog/pg_constraint.h @@ -182,7 +182,8 @@ typedef enum ConstraintCategory } ConstraintCategory; -extern Oid CreateConstraintEntry(const char *constraintName, +extern Oid CreateConstraintEntry(Oid existing_constraint_oid, + const char *constraintName, Oid constraintNamespace, char constraintType, bool isDeferrable, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index e83329f..c0ddf2a 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2428,6 +2428,9 @@ typedef struct CreateAmStmt typedef struct CreateTrigStmt { NodeTag type; + bool replace; /* when true, replace trigger if already + * exists */ + bool isconstraint; /* This is a constraint trigger */ char *trigname; /* TRIGGER's name */ RangeVar *relation; /* relation trigger is on */ List *funcname; /* qual. name of function to call */ @@ -2439,7 +2442,6 @@ typedef struct CreateTrigStmt int16 events; /* "OR" of INSERT/UPDATE/DELETE/TRUNCATE */ List *columns; /* column names, or NIL for all columns */ Node *whenClause; /* qual expression, or NULL if none */ - bool isconstraint; /* This is a constraint trigger */ /* explicitly named transition data */ List *transitionRels; /* TriggerTransition nodes, or NIL if none */ /* The remaining fields are only used for constraint triggers */ diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out index 5e76b3a..c8bbcdf 100644 --- a/src/test/regress/expected/triggers.out +++ b/src/test/regress/expected/triggers.out @@ -3052,3 +3052,222 @@ create trigger aft_row after insert or update on trigger_parted create table trigger_parted_p1 partition of trigger_parted for values in (1) partition by list (a); create table trigger_parted_p1_1 partition of trigger_parted_p1 for values in (1); +-- +-- Test case for CREATE OR REPLACE TRIGGER +-- +create table my_table (id integer); +create function funcA() returns trigger as $$ +begin + raise notice 'hello from funcA'; + return null; +end; $$ language plpgsql; +create function funcB() returns trigger as $$ +begin + raise notice 'hello from funcB'; + return null; +end; $$ language plpgsql; +create or replace trigger my_trig + before insert on my_table + for each row execute procedure funcA(); +insert into my_table (id) values (1); +NOTICE: hello from funcA +create or replace trigger my_trig + before insert on my_table + for each row execute procedure funcB(); +insert into my_table (id) values (2); +NOTICE: hello from funcB +drop trigger my_trig on my_table; +drop function funcA(); +drop function funcB(); +drop table my_table; +-- test trigger can't be replaced without OR REPLACE clause +create table my_table (id integer); +create function funcA() returns trigger as $$ +begin + raise notice 'hello from funcA'; + return null; +end; $$ language plpgsql; +create function funcB() returns trigger as $$ +begin + raise notice 'hello from funcB'; + return null; +end; $$ language plpgsql; +create trigger my_trig + before insert on my_table + for each row execute procedure funcA(); +create trigger my_trig + before insert on my_table + for each row execute procedure funcB(); --should fail +ERROR: trigger "my_trig" for relation "my_table" already exists +drop trigger my_trig on my_table; +create constraint trigger my_trig + after insert on my_table + for each row execute procedure funcA(); +create constraint trigger my_trig + after insert on my_table + for each row execute procedure funcB(); --should fail +ERROR: trigger "my_trig" for relation "my_table" already exists +drop trigger my_trig on my_table; +drop function funcA(); +drop function funcB(); +drop table my_table; +-- test for detecting incompatible replacement of trigger +create table my_table (id integer); +create function funcA() returns trigger as $$ +begin + raise notice 'hello from funcA'; + return null; +end; $$ language plpgsql; +create function funcB() returns trigger as $$ +begin + raise notice 'hello from funcB'; + return null; +end; $$ language plpgsql; +create or replace trigger my_trig + after insert on my_table + for each row execute procedure funcA(); +create or replace constraint trigger my_trig + after insert on my_table + for each row execute procedure funcB(); --should fail +ERROR: trigger "my_trig" for relation "my_table" is a regular trigger +HINT: use CREATE OR REPLACE TRIGGER to replace a regular trigger +drop trigger my_trig on my_table; +create or replace constraint trigger my_trig + after insert on my_table + for each row execute procedure funcB(); +create or replace trigger my_trig + after insert on my_table + for each row execute procedure funcB(); --should fail +ERROR: trigger "my_trig" for relation "my_table" is a constraint trigger +HINT: use CREATE OR REPLACE CONSTRAINT TRIGGER to replace a constraint trigger +drop table my_table; +drop function funcA(); +drop function funcB(); +-- test CREATE OR REPLACE TRIGGER on partition table +create table parted_trig (a int) partition by range (a); +create table parted_trig_1 partition of parted_trig for values from (0) to (1000) partition by range (a); +create table parted_trig_1_1 partition of parted_trig_1 for values from (0) to (100); +create table parted_trig_2 partition of parted_trig for values from (1000) to (2000); +create table default_parted_trig partition of parted_trig default; +create function funcA() returns trigger as $$ +begin + raise notice 'hello from funcA'; + return null; +end; $$ language plpgsql; +create function funcB() returns trigger as $$ +begin + raise notice 'hello from funcB'; + return null; +end; $$ language plpgsql; +-- trigger attached to the parent partition table is replaced by a new one to the parent partition table. +-- verify that all partitioned table share the latter trigger in this case. +create or replace trigger my_trig + after insert on parted_trig + for each row execute procedure funcA(); +insert into parted_trig (a) values (50); +NOTICE: hello from funcA +insert into parted_trig (a) values (1500); +NOTICE: hello from funcA +insert into parted_trig (a) values (2500); +NOTICE: hello from funcA +create or replace trigger my_trig + after insert on parted_trig + for each row execute procedure funcB(); +insert into parted_trig (a) values (50); +NOTICE: hello from funcB +insert into parted_trig (a) values (1500); +NOTICE: hello from funcB +insert into parted_trig (a) values (2500); +NOTICE: hello from funcB +truncate parted_trig; +drop trigger my_trig on parted_trig; +-- trigger attached to the parent partition table is replaced by a new one to the child partition table. +-- verify that only the child partition's trigger is replaced and other tables' triggers aren't. +create or replace trigger my_trig + after insert on parted_trig + for each row execute procedure funcA(); +insert into parted_trig (a) values (50); +NOTICE: hello from funcA +insert into parted_trig (a) values (1500); +NOTICE: hello from funcA +insert into parted_trig (a) values (2500); +NOTICE: hello from funcA +create or replace trigger my_trig + after insert on parted_trig_1 + for each row execute procedure funcB(); +insert into parted_trig (a) values (50); +NOTICE: hello from funcB +insert into parted_trig (a) values (1500); +NOTICE: hello from funcA +insert into parted_trig (a) values (2500); +NOTICE: hello from funcA +truncate parted_trig; +drop trigger my_trig on parted_trig; +-- trigger attached to the child partition table is replaced by a new one to the parent partition table. +-- verify that the child partition's trigger is replaced by the new one as well and other partition share the same one. +create or replace trigger my_trig + after insert on parted_trig_1 + for each row execute procedure funcA(); +insert into parted_trig (a) values (50); +NOTICE: hello from funcA +insert into parted_trig (a) values (1500); +insert into parted_trig (a) values (2500); +create or replace trigger my_trig + after insert on parted_trig + for each row execute procedure funcB(); +insert into parted_trig (a) values (50); +NOTICE: hello from funcB +insert into parted_trig (a) values (1500); +NOTICE: hello from funcB +insert into parted_trig (a) values (2500); +NOTICE: hello from funcB +truncate parted_trig; +drop trigger my_trig on parted_trig; +-- trigger attached to the child partition table is replaced by a new one to the child partition table. +-- verify that the trigger of the child partition is replaced and no other partition has the partition. +create or replace trigger my_trig + after insert on parted_trig_1 + for each row execute procedure funcA(); +insert into parted_trig (a) values (50); +NOTICE: hello from funcA +insert into parted_trig (a) values (1500); +insert into parted_trig (a) values (2500); +create or replace trigger my_trig + after insert on parted_trig_1 + for each row execute procedure funcB(); +insert into parted_trig (a) values (50); +NOTICE: hello from funcB +insert into parted_trig (a) values (1500); +insert into parted_trig (a) values (2500); +truncate parted_trig; +drop trigger my_trig on parted_trig_1; +drop table parted_trig; +drop function funcA(); +drop function funcB(); +-- test for protection of dropping a trigger that has pending events in the session +create table my_table (id integer); +create function funcA () returns trigger as $$ +begin + raise notice 'hello from funcA'; + return null; +end; $$ language plpgsql; +create function funcB() returns trigger as $$ +begin + raise notice 'hello from funcB'; + return null; +end; $$ language plpgsql; +begin; +create or replace constraint trigger my_trig + after insert on my_table + deferrable initially deferred + for each row execute procedure funcA(); +insert into my_table (id) values (1); -- make this trigger above pending +create or replace constraint trigger my_trig + after insert on my_table + deferrable initially deferred + for each row execute procedure funcB(); -- should fail +ERROR: cannot replace "my_trig" on "my_table" because it has pending trigger events +end; +drop table my_table; +drop function funcA(); +drop function funcB(); diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql index e228d0a..11c5df7 100644 --- a/src/test/regress/sql/triggers.sql +++ b/src/test/regress/sql/triggers.sql @@ -2295,3 +2295,204 @@ create trigger aft_row after insert or update on trigger_parted create table trigger_parted_p1 partition of trigger_parted for values in (1) partition by list (a); create table trigger_parted_p1_1 partition of trigger_parted_p1 for values in (1); + +-- +-- Test case for CREATE OR REPLACE TRIGGER +-- +create table my_table (id integer); +create function funcA() returns trigger as $$ +begin + raise notice 'hello from funcA'; + return null; +end; $$ language plpgsql; +create function funcB() returns trigger as $$ +begin + raise notice 'hello from funcB'; + return null; +end; $$ language plpgsql; +create or replace trigger my_trig + before insert on my_table + for each row execute procedure funcA(); +insert into my_table (id) values (1); +create or replace trigger my_trig + before insert on my_table + for each row execute procedure funcB(); +insert into my_table (id) values (2); +drop trigger my_trig on my_table; +drop function funcA(); +drop function funcB(); +drop table my_table; + +-- test trigger can't be replaced without OR REPLACE clause +create table my_table (id integer); +create function funcA() returns trigger as $$ +begin + raise notice 'hello from funcA'; + return null; +end; $$ language plpgsql; +create function funcB() returns trigger as $$ +begin + raise notice 'hello from funcB'; + return null; +end; $$ language plpgsql; +create trigger my_trig + before insert on my_table + for each row execute procedure funcA(); +create trigger my_trig + before insert on my_table + for each row execute procedure funcB(); --should fail +drop trigger my_trig on my_table; +create constraint trigger my_trig + after insert on my_table + for each row execute procedure funcA(); +create constraint trigger my_trig + after insert on my_table + for each row execute procedure funcB(); --should fail +drop trigger my_trig on my_table; +drop function funcA(); +drop function funcB(); +drop table my_table; + +-- test for detecting incompatible replacement of trigger +create table my_table (id integer); +create function funcA() returns trigger as $$ +begin + raise notice 'hello from funcA'; + return null; +end; $$ language plpgsql; +create function funcB() returns trigger as $$ +begin + raise notice 'hello from funcB'; + return null; +end; $$ language plpgsql; +create or replace trigger my_trig + after insert on my_table + for each row execute procedure funcA(); +create or replace constraint trigger my_trig + after insert on my_table + for each row execute procedure funcB(); --should fail +drop trigger my_trig on my_table; +create or replace constraint trigger my_trig + after insert on my_table + for each row execute procedure funcB(); +create or replace trigger my_trig + after insert on my_table + for each row execute procedure funcB(); --should fail +drop table my_table; +drop function funcA(); +drop function funcB(); + +-- test CREATE OR REPLACE TRIGGER on partition table +create table parted_trig (a int) partition by range (a); +create table parted_trig_1 partition of parted_trig for values from (0) to (1000) partition by range (a); +create table parted_trig_1_1 partition of parted_trig_1 for values from (0) to (100); +create table parted_trig_2 partition of parted_trig for values from (1000) to (2000); +create table default_parted_trig partition of parted_trig default; +create function funcA() returns trigger as $$ +begin + raise notice 'hello from funcA'; + return null; +end; $$ language plpgsql; +create function funcB() returns trigger as $$ +begin + raise notice 'hello from funcB'; + return null; +end; $$ language plpgsql; + +-- trigger attached to the parent partition table is replaced by a new one to the parent partition table. +-- verify that all partitioned table share the latter trigger in this case. +create or replace trigger my_trig + after insert on parted_trig + for each row execute procedure funcA(); +insert into parted_trig (a) values (50); +insert into parted_trig (a) values (1500); +insert into parted_trig (a) values (2500); +create or replace trigger my_trig + after insert on parted_trig + for each row execute procedure funcB(); +insert into parted_trig (a) values (50); +insert into parted_trig (a) values (1500); +insert into parted_trig (a) values (2500); +truncate parted_trig; +drop trigger my_trig on parted_trig; + +-- trigger attached to the parent partition table is replaced by a new one to the child partition table. +-- verify that only the child partition's trigger is replaced and other tables' triggers aren't. +create or replace trigger my_trig + after insert on parted_trig + for each row execute procedure funcA(); +insert into parted_trig (a) values (50); +insert into parted_trig (a) values (1500); +insert into parted_trig (a) values (2500); +create or replace trigger my_trig + after insert on parted_trig_1 + for each row execute procedure funcB(); +insert into parted_trig (a) values (50); +insert into parted_trig (a) values (1500); +insert into parted_trig (a) values (2500); +truncate parted_trig; +drop trigger my_trig on parted_trig; + +-- trigger attached to the child partition table is replaced by a new one to the parent partition table. +-- verify that the child partition's trigger is replaced by the new one as well and other partition share the same one. +create or replace trigger my_trig + after insert on parted_trig_1 + for each row execute procedure funcA(); +insert into parted_trig (a) values (50); +insert into parted_trig (a) values (1500); +insert into parted_trig (a) values (2500); +create or replace trigger my_trig + after insert on parted_trig + for each row execute procedure funcB(); +insert into parted_trig (a) values (50); +insert into parted_trig (a) values (1500); +insert into parted_trig (a) values (2500); +truncate parted_trig; +drop trigger my_trig on parted_trig; + +-- trigger attached to the child partition table is replaced by a new one to the child partition table. +-- verify that the trigger of the child partition is replaced and no other partition has the partition. +create or replace trigger my_trig + after insert on parted_trig_1 + for each row execute procedure funcA(); +insert into parted_trig (a) values (50); +insert into parted_trig (a) values (1500); +insert into parted_trig (a) values (2500); +create or replace trigger my_trig + after insert on parted_trig_1 + for each row execute procedure funcB(); +insert into parted_trig (a) values (50); +insert into parted_trig (a) values (1500); +insert into parted_trig (a) values (2500); +truncate parted_trig; +drop trigger my_trig on parted_trig_1; +drop table parted_trig; +drop function funcA(); +drop function funcB(); + +-- test for protection of dropping a trigger that has pending events in the session +create table my_table (id integer); +create function funcA () returns trigger as $$ +begin + raise notice 'hello from funcA'; + return null; +end; $$ language plpgsql; +create function funcB() returns trigger as $$ +begin + raise notice 'hello from funcB'; + return null; +end; $$ language plpgsql; +begin; +create or replace constraint trigger my_trig + after insert on my_table + deferrable initially deferred + for each row execute procedure funcA(); +insert into my_table (id) values (1); -- make this trigger above pending +create or replace constraint trigger my_trig + after insert on my_table + deferrable initially deferred + for each row execute procedure funcB(); -- should fail +end; +drop table my_table; +drop function funcA(); +drop function funcB();