From b2734a0d792df710aeeab21242cfa21ab470c773 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Wed, 14 Oct 2009 22:14:25 +0000 Subject: [PATCH] Support SQL-compliant triggers on columns, ie fire only if certain columns are named in the UPDATE's SET list. Note: the schema of pg_trigger has not actually changed; we've just started to use a column that was there all along. catversion bumped anyway so that this commit is included in the history of potentially interesting changes to system catalog contents. Itagaki Takahiro --- doc/src/sgml/catalogs.sgml | 15 +- doc/src/sgml/ref/create_trigger.sgml | 44 ++++-- doc/src/sgml/trigger.sgml | 13 +- src/backend/catalog/index.c | 3 +- src/backend/commands/tablecmds.c | 5 +- src/backend/commands/trigger.c | 261 +++++++++++++++++++-------------- src/backend/nodes/copyfuncs.c | 3 +- src/backend/nodes/equalfuncs.c | 3 +- src/backend/optimizer/plan/setrefs.c | 5 +- src/backend/parser/gram.y | 41 ++++-- src/backend/utils/adt/ruleutils.c | 19 ++- src/bin/pg_dump/pg_dump.c | 14 +- src/include/catalog/catversion.h | 4 +- src/include/catalog/pg_trigger.h | 6 +- src/include/nodes/parsenodes.h | 8 +- src/test/regress/expected/triggers.out | 119 ++++++++++++--- src/test/regress/sql/triggers.sql | 53 ++++++- 17 files changed, 433 insertions(+), 183 deletions(-) diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 487dd7e169..174dd0ee81 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -1,4 +1,4 @@ - + @@ -4745,8 +4745,9 @@ tgattr int2vector - - Currently unused + pg_attribute.attnum + column numbers, if trigger is column-specific; otherwise an + empty array @@ -4759,6 +4760,14 @@ + + Currently, column-specific triggering is supported only for + UPDATE events, and so tgattr is relevant + only for that event type. tgtype might + contain bits for other event types as well, but those are presumed + to be table-wide regardless of what is in tgattr. + + When tgconstraint is nonzero, diff --git a/doc/src/sgml/ref/create_trigger.sgml b/doc/src/sgml/ref/create_trigger.sgml index 38036ee168..04f681f030 100644 --- a/doc/src/sgml/ref/create_trigger.sgml +++ b/doc/src/sgml/ref/create_trigger.sgml @@ -1,5 +1,5 @@ @@ -49,7 +49,7 @@ CREATE TRIGGER name { BEFORE | AFTE current row, or change the row being inserted (for INSERT and UPDATE operations only). If the trigger fires after the event, all changes, including - the last insertion, update, or deletion, are visible + the effects of other triggers, are visible to the trigger. @@ -122,6 +122,16 @@ CREATE TRIGGER name { BEFORE | AFTE this specifies the event that will fire the trigger. Multiple events can be specified using OR. + + + For UPDATE triggers, it is possible to + specify a list of columns using this syntax: + +UPDATE OF column_name1 [, column_name2 ... ] + + The trigger will only fire if at least one of the listed columns + is mentioned as a target of the update. + @@ -169,8 +179,8 @@ CREATE TRIGGER name { BEFORE | AFTE literal string constants. Simple names and numeric constants can be written here, too, but they will all be converted to strings. Please check the description of the implementation - language of the trigger function about how the trigger arguments - are accessible within the function; it might be different from + language of the trigger function to find out how these arguments + can be accessed within the function; it might be different from normal function arguments. @@ -192,6 +202,18 @@ CREATE TRIGGER name { BEFORE | AFTE + A column-specific trigger (FOR UPDATE OF + column_name) will fire when any + of its columns are listed as targets in the UPDATE + command's SET list. It is possible for a column's value + to change even when the trigger is not fired, because changes made to the + row's contents by BEFORE UPDATE triggers are not considered. + Conversely, a command such as UPDATE ... SET x = x ... + will fire a trigger on column x, even though the column's + value did not change. + + + In PostgreSQL versions before 7.3, it was necessary to declare trigger functions as returning the placeholder type opaque, rather than trigger. To support loading @@ -220,13 +242,6 @@ CREATE TRIGGER name { BEFORE | AFTE - SQL allows triggers to fire on updates to specific columns - (e.g., AFTER UPDATE OF col1, col2). - - - - - SQL allows you to define aliases for the old and new rows or tables for use in the definition of the triggered action (e.g., CREATE TRIGGER ... ON @@ -264,9 +279,10 @@ CREATE TRIGGER name { BEFORE | AFTE The PostgreSQL behavior is for BEFORE DELETE to always fire before the delete action, even a cascading one. This is considered more consistent. There is also unpredictable - behavior when BEFORE triggers modify rows that are later - to be modified by referential actions. This can lead to constraint violations - or stored data that does not honor the referential constraint. + behavior when BEFORE triggers modify rows or prevent + updates during an update that is caused by a referential action. This can + lead to constraint violations or stored data that does not honor the + referential constraint. diff --git a/doc/src/sgml/trigger.sgml b/doc/src/sgml/trigger.sgml index 50e5943401..60977cbd56 100644 --- a/doc/src/sgml/trigger.sgml +++ b/doc/src/sgml/trigger.sgml @@ -1,4 +1,4 @@ - + Triggers @@ -36,10 +36,13 @@ performed. Triggers can be defined to execute either before or after any INSERT, UPDATE, or DELETE operation, either once per modified row, - or once per SQL statement. Triggers can also fire - for TRUNCATE statements. If a trigger event occurs, - the trigger's function is called at the appropriate time to handle the - event. + or once per SQL statement. + UPDATE triggers can moreover be set to fire only if + certain columns are mentioned in the SET clause of the + UPDATE statement. + Triggers can also fire for TRUNCATE statements. + If a trigger event occurs, the trigger's function is called at the + appropriate time to handle the event. diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c index b43b2910e7..573b00216d 100644 --- a/src/backend/catalog/index.c +++ b/src/backend/catalog/index.c @@ -8,7 +8,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/catalog/index.c,v 1.322 2009/10/05 19:24:35 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/catalog/index.c,v 1.323 2009/10/14 22:14:21 tgl Exp $ * * * INTERFACE ROUTINES @@ -792,6 +792,7 @@ index_create(Oid heapRelationId, trigger->before = false; trigger->row = true; trigger->events = TRIGGER_TYPE_INSERT | TRIGGER_TYPE_UPDATE; + trigger->columns = NIL; trigger->isconstraint = true; trigger->deferrable = true; trigger->initdeferred = initdeferred; diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index ce7d14cee6..24cbe1dd94 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -8,7 +8,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/commands/tablecmds.c,v 1.303 2009/10/13 00:53:07 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/commands/tablecmds.c,v 1.304 2009/10/14 22:14:21 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -5441,6 +5441,7 @@ CreateFKCheckTrigger(RangeVar *myRel, Constraint *fkconstraint, fk_trigger->events = TRIGGER_TYPE_UPDATE; } + fk_trigger->columns = NIL; fk_trigger->isconstraint = true; fk_trigger->deferrable = fkconstraint->deferrable; fk_trigger->initdeferred = fkconstraint->initdeferred; @@ -5491,6 +5492,7 @@ createForeignKeyTriggers(Relation rel, Constraint *fkconstraint, fk_trigger->before = false; fk_trigger->row = true; fk_trigger->events = TRIGGER_TYPE_DELETE; + fk_trigger->columns = NIL; fk_trigger->isconstraint = true; fk_trigger->constrrel = myRel; switch (fkconstraint->fk_del_action) @@ -5543,6 +5545,7 @@ createForeignKeyTriggers(Relation rel, Constraint *fkconstraint, fk_trigger->before = false; fk_trigger->row = true; fk_trigger->events = TRIGGER_TYPE_UPDATE; + fk_trigger->columns = NIL; fk_trigger->isconstraint = true; fk_trigger->constrrel = myRel; switch (fkconstraint->fk_upd_action) diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c index 47cf0c470d..dd526f6db1 100644 --- a/src/backend/commands/trigger.c +++ b/src/backend/commands/trigger.c @@ -7,7 +7,7 @@ * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/commands/trigger.c,v 1.253 2009/10/10 01:43:45 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/commands/trigger.c,v 1.254 2009/10/14 22:14:21 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -30,8 +30,11 @@ #include "executor/executor.h" #include "executor/instrument.h" #include "miscadmin.h" +#include "nodes/bitmapset.h" #include "nodes/makefuncs.h" #include "parser/parse_func.h" +#include "parser/parse_relation.h" +#include "parser/parsetree.h" #include "pgstat.h" #include "storage/bufmgr.h" #include "tcop/utility.h" @@ -51,6 +54,9 @@ int SessionReplicationRole = SESSION_REPLICATION_ROLE_ORIGIN; +#define GetModifiedColumns(relinfo, estate) \ + (rt_fetch((relinfo)->ri_RangeTableIndex, (estate)->es_range_table)->modifiedCols) + /* Local function prototypes */ static void ConvertTriggerToFK(CreateTrigStmt *stmt, Oid funcoid); static void InsertTrigger(TriggerDesc *trigdesc, Trigger *trigger, int indx); @@ -59,6 +65,8 @@ static HeapTuple GetTupleForTrigger(EState *estate, ResultRelInfo *relinfo, ItemPointer tid, TupleTableSlot **newSlot); +static bool TriggerEnabled(Trigger *trigger, TriggerEvent event, + Bitmapset *modifiedCols); static HeapTuple ExecCallTriggerFunc(TriggerData *trigdata, int tgindx, FmgrInfo *finfo, @@ -66,7 +74,7 @@ static HeapTuple ExecCallTriggerFunc(TriggerData *trigdata, MemoryContext per_tuple_context); static void AfterTriggerSaveEvent(ResultRelInfo *relinfo, int event, bool row_trigger, HeapTuple oldtup, HeapTuple newtup, - List *recheckIndexes); + List *recheckIndexes, Bitmapset *modifiedCols); /* @@ -98,6 +106,8 @@ CreateTrigger(CreateTrigStmt *stmt, bool checkPermissions) { int16 tgtype; + int ncolumns; + int2 *columns; int2vector *tgattr; Datum values[Natts_pg_trigger]; bool nulls[Natts_pg_trigger]; @@ -337,8 +347,44 @@ CreateTrigger(CreateTrigStmt *stmt, CStringGetDatum("")); } - /* tgattr is currently always a zero-length array */ - tgattr = buildint2vector(NULL, 0); + /* build column number array if it's a column-specific trigger */ + ncolumns = list_length(stmt->columns); + if (ncolumns == 0) + columns = NULL; + else + { + ListCell *cell; + int i = 0; + + columns = (int2 *) palloc(ncolumns * sizeof(int2)); + foreach(cell, stmt->columns) + { + char *name = strVal(lfirst(cell)); + int2 attnum; + int j; + + /* Lookup column name. System columns are not allowed */ + attnum = attnameAttNum(rel, name, false); + if (attnum == InvalidAttrNumber) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("column \"%s\" of relation \"%s\" does not exist", + name, RelationGetRelationName(rel)))); + + /* Check for duplicates */ + for (j = i - 1; j >= 0; j--) + { + if (columns[j] == attnum) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_COLUMN), + errmsg("column \"%s\" specified more than once", + name))); + } + + columns[i++] = attnum; + } + } + tgattr = buildint2vector(columns, ncolumns); values[Anum_pg_trigger_tgattr - 1] = PointerGetDatum(tgattr); tuple = heap_form_tuple(tgrel->rd_att, values, nulls); @@ -358,6 +404,7 @@ CreateTrigger(CreateTrigStmt *stmt, pfree(DatumGetPointer(values[Anum_pg_trigger_tgname - 1])); pfree(DatumGetPointer(values[Anum_pg_trigger_tgargs - 1])); + pfree(DatumGetPointer(values[Anum_pg_trigger_tgattr - 1])); /* * Update relation's pg_class entry. Crucial side-effect: other backends @@ -434,6 +481,20 @@ CreateTrigger(CreateTrigStmt *stmt, Assert(!OidIsValid(indexOid)); } + /* If column-specific trigger, add normal dependencies on columns */ + if (columns != NULL) + { + int i; + + referenced.classId = RelationRelationId; + referenced.objectId = RelationGetRelid(rel); + for (i = 0; i < ncolumns; i++) + { + referenced.objectSubId = columns[i]; + recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); + } + } + /* Keep lock on target rel until end of xact */ heap_close(rel, NoLock); @@ -1626,18 +1687,9 @@ ExecBSInsertTriggers(EState *estate, ResultRelInfo *relinfo) Trigger *trigger = &trigdesc->triggers[tgindx[i]]; HeapTuple newtuple; - if (SessionReplicationRole == SESSION_REPLICATION_ROLE_REPLICA) - { - if (trigger->tgenabled == TRIGGER_FIRES_ON_ORIGIN || - trigger->tgenabled == TRIGGER_DISABLED) - continue; - } - else /* ORIGIN or LOCAL role */ - { - if (trigger->tgenabled == TRIGGER_FIRES_ON_REPLICA || - trigger->tgenabled == TRIGGER_DISABLED) - continue; - } + if (!TriggerEnabled(trigger, LocTriggerData.tg_event, NULL)) + continue; + LocTriggerData.tg_trigger = trigger; newtuple = ExecCallTriggerFunc(&LocTriggerData, tgindx[i], @@ -1659,7 +1711,7 @@ ExecASInsertTriggers(EState *estate, ResultRelInfo *relinfo) if (trigdesc && trigdesc->n_after_statement[TRIGGER_EVENT_INSERT] > 0) AfterTriggerSaveEvent(relinfo, TRIGGER_EVENT_INSERT, - false, NULL, NULL, NIL); + false, NULL, NULL, NIL, NULL); } HeapTuple @@ -1685,18 +1737,9 @@ ExecBRInsertTriggers(EState *estate, ResultRelInfo *relinfo, { Trigger *trigger = &trigdesc->triggers[tgindx[i]]; - if (SessionReplicationRole == SESSION_REPLICATION_ROLE_REPLICA) - { - if (trigger->tgenabled == TRIGGER_FIRES_ON_ORIGIN || - trigger->tgenabled == TRIGGER_DISABLED) - continue; - } - else /* ORIGIN or LOCAL role */ - { - if (trigger->tgenabled == TRIGGER_FIRES_ON_REPLICA || - trigger->tgenabled == TRIGGER_DISABLED) - continue; - } + if (!TriggerEnabled(trigger, LocTriggerData.tg_event, NULL)) + continue; + LocTriggerData.tg_trigtuple = oldtuple = newtuple; LocTriggerData.tg_trigtuplebuf = InvalidBuffer; LocTriggerData.tg_trigger = trigger; @@ -1721,7 +1764,7 @@ ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo, if (trigdesc && trigdesc->n_after_row[TRIGGER_EVENT_INSERT] > 0) AfterTriggerSaveEvent(relinfo, TRIGGER_EVENT_INSERT, - true, NULL, trigtuple, recheckIndexes); + true, NULL, trigtuple, recheckIndexes, NULL); } void @@ -1757,18 +1800,9 @@ ExecBSDeleteTriggers(EState *estate, ResultRelInfo *relinfo) Trigger *trigger = &trigdesc->triggers[tgindx[i]]; HeapTuple newtuple; - if (SessionReplicationRole == SESSION_REPLICATION_ROLE_REPLICA) - { - if (trigger->tgenabled == TRIGGER_FIRES_ON_ORIGIN || - trigger->tgenabled == TRIGGER_DISABLED) - continue; - } - else /* ORIGIN or LOCAL role */ - { - if (trigger->tgenabled == TRIGGER_FIRES_ON_REPLICA || - trigger->tgenabled == TRIGGER_DISABLED) - continue; - } + if (!TriggerEnabled(trigger, LocTriggerData.tg_event, NULL)) + continue; + LocTriggerData.tg_trigger = trigger; newtuple = ExecCallTriggerFunc(&LocTriggerData, tgindx[i], @@ -1790,7 +1824,7 @@ ExecASDeleteTriggers(EState *estate, ResultRelInfo *relinfo) if (trigdesc && trigdesc->n_after_statement[TRIGGER_EVENT_DELETE] > 0) AfterTriggerSaveEvent(relinfo, TRIGGER_EVENT_DELETE, - false, NULL, NULL, NIL); + false, NULL, NULL, NIL, NULL); } bool @@ -1824,18 +1858,9 @@ ExecBRDeleteTriggers(EState *estate, PlanState *subplanstate, { Trigger *trigger = &trigdesc->triggers[tgindx[i]]; - if (SessionReplicationRole == SESSION_REPLICATION_ROLE_REPLICA) - { - if (trigger->tgenabled == TRIGGER_FIRES_ON_ORIGIN || - trigger->tgenabled == TRIGGER_DISABLED) - continue; - } - else /* ORIGIN or LOCAL role */ - { - if (trigger->tgenabled == TRIGGER_FIRES_ON_REPLICA || - trigger->tgenabled == TRIGGER_DISABLED) - continue; - } + if (!TriggerEnabled(trigger, LocTriggerData.tg_event, NULL)) + continue; + LocTriggerData.tg_trigtuple = trigtuple; LocTriggerData.tg_trigtuplebuf = InvalidBuffer; LocTriggerData.tg_trigger = trigger; @@ -1869,7 +1894,7 @@ ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo, tupleid, NULL); AfterTriggerSaveEvent(relinfo, TRIGGER_EVENT_DELETE, - true, trigtuple, NULL, NIL); + true, trigtuple, NULL, NIL, NULL); heap_freetuple(trigtuple); } } @@ -1882,6 +1907,7 @@ ExecBSUpdateTriggers(EState *estate, ResultRelInfo *relinfo) int *tgindx; int i; TriggerData LocTriggerData; + Bitmapset *modifiedCols; trigdesc = relinfo->ri_TrigDesc; @@ -1894,6 +1920,8 @@ ExecBSUpdateTriggers(EState *estate, ResultRelInfo *relinfo) if (ntrigs == 0) return; + modifiedCols = GetModifiedColumns(relinfo, estate); + LocTriggerData.type = T_TriggerData; LocTriggerData.tg_event = TRIGGER_EVENT_UPDATE | TRIGGER_EVENT_BEFORE; @@ -1907,18 +1935,9 @@ ExecBSUpdateTriggers(EState *estate, ResultRelInfo *relinfo) Trigger *trigger = &trigdesc->triggers[tgindx[i]]; HeapTuple newtuple; - if (SessionReplicationRole == SESSION_REPLICATION_ROLE_REPLICA) - { - if (trigger->tgenabled == TRIGGER_FIRES_ON_ORIGIN || - trigger->tgenabled == TRIGGER_DISABLED) - continue; - } - else /* ORIGIN or LOCAL role */ - { - if (trigger->tgenabled == TRIGGER_FIRES_ON_REPLICA || - trigger->tgenabled == TRIGGER_DISABLED) - continue; - } + if (!TriggerEnabled(trigger, LocTriggerData.tg_event, modifiedCols)) + continue; + LocTriggerData.tg_trigger = trigger; newtuple = ExecCallTriggerFunc(&LocTriggerData, tgindx[i], @@ -1940,7 +1959,8 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo) if (trigdesc && trigdesc->n_after_statement[TRIGGER_EVENT_UPDATE] > 0) AfterTriggerSaveEvent(relinfo, TRIGGER_EVENT_UPDATE, - false, NULL, NULL, NIL); + false, NULL, NULL, NIL, + GetModifiedColumns(relinfo, estate)); } HeapTuple @@ -1957,6 +1977,7 @@ ExecBRUpdateTriggers(EState *estate, PlanState *subplanstate, HeapTuple intuple = newtuple; TupleTableSlot *newSlot; int i; + Bitmapset *modifiedCols; trigtuple = GetTupleForTrigger(estate, subplanstate, relinfo, tupleid, &newSlot); @@ -1971,6 +1992,8 @@ ExecBRUpdateTriggers(EState *estate, PlanState *subplanstate, if (newSlot != NULL) intuple = newtuple = ExecRemoveJunk(relinfo->ri_junkFilter, newSlot); + modifiedCols = GetModifiedColumns(relinfo, estate); + LocTriggerData.type = T_TriggerData; LocTriggerData.tg_event = TRIGGER_EVENT_UPDATE | TRIGGER_EVENT_ROW | @@ -1980,18 +2003,9 @@ ExecBRUpdateTriggers(EState *estate, PlanState *subplanstate, { Trigger *trigger = &trigdesc->triggers[tgindx[i]]; - if (SessionReplicationRole == SESSION_REPLICATION_ROLE_REPLICA) - { - if (trigger->tgenabled == TRIGGER_FIRES_ON_ORIGIN || - trigger->tgenabled == TRIGGER_DISABLED) - continue; - } - else /* ORIGIN or LOCAL role */ - { - if (trigger->tgenabled == TRIGGER_FIRES_ON_REPLICA || - trigger->tgenabled == TRIGGER_DISABLED) - continue; - } + if (!TriggerEnabled(trigger, LocTriggerData.tg_event, modifiedCols)) + continue; + LocTriggerData.tg_trigtuple = trigtuple; LocTriggerData.tg_newtuple = oldtuple = newtuple; LocTriggerData.tg_trigtuplebuf = InvalidBuffer; @@ -2024,7 +2038,8 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo, tupleid, NULL); AfterTriggerSaveEvent(relinfo, TRIGGER_EVENT_UPDATE, - true, trigtuple, newtuple, recheckIndexes); + true, trigtuple, newtuple, recheckIndexes, + GetModifiedColumns(relinfo, estate)); heap_freetuple(trigtuple); } } @@ -2062,18 +2077,9 @@ ExecBSTruncateTriggers(EState *estate, ResultRelInfo *relinfo) Trigger *trigger = &trigdesc->triggers[tgindx[i]]; HeapTuple newtuple; - if (SessionReplicationRole == SESSION_REPLICATION_ROLE_REPLICA) - { - if (trigger->tgenabled == TRIGGER_FIRES_ON_ORIGIN || - trigger->tgenabled == TRIGGER_DISABLED) - continue; - } - else /* ORIGIN or LOCAL role */ - { - if (trigger->tgenabled == TRIGGER_FIRES_ON_REPLICA || - trigger->tgenabled == TRIGGER_DISABLED) - continue; - } + if (!TriggerEnabled(trigger, LocTriggerData.tg_event, NULL)) + continue; + LocTriggerData.tg_trigger = trigger; newtuple = ExecCallTriggerFunc(&LocTriggerData, tgindx[i], @@ -2095,7 +2101,7 @@ ExecASTruncateTriggers(EState *estate, ResultRelInfo *relinfo) if (trigdesc && trigdesc->n_after_statement[TRIGGER_EVENT_TRUNCATE] > 0) AfterTriggerSaveEvent(relinfo, TRIGGER_EVENT_TRUNCATE, - false, NULL, NULL, NIL); + false, NULL, NULL, NIL, NULL); } @@ -2201,6 +2207,52 @@ ltrmark:; return result; } +/* + * Is trigger enabled to fire? + */ +static bool +TriggerEnabled(Trigger *trigger, TriggerEvent event, Bitmapset *modifiedCols) +{ + /* Check replication-role-dependent enable state */ + if (SessionReplicationRole == SESSION_REPLICATION_ROLE_REPLICA) + { + if (trigger->tgenabled == TRIGGER_FIRES_ON_ORIGIN || + trigger->tgenabled == TRIGGER_DISABLED) + return false; + } + else /* ORIGIN or LOCAL role */ + { + if (trigger->tgenabled == TRIGGER_FIRES_ON_REPLICA || + trigger->tgenabled == TRIGGER_DISABLED) + return false; + } + + /* + * Check for column-specific trigger (only possible for UPDATE, and in + * fact we *must* ignore tgattr for other event types) + */ + if (trigger->tgnattr > 0 && TRIGGER_FIRED_BY_UPDATE(event)) + { + int i; + bool modified; + + modified = false; + for (i = 0; i < trigger->tgnattr; i++) + { + if (bms_is_member(trigger->tgattr[i] - FirstLowInvalidHeapAttributeNumber, + modifiedCols)) + { + modified = true; + break; + } + } + if (!modified) + return false; + } + + return true; +} + /* ---------- * After-trigger stuff @@ -3825,7 +3877,7 @@ AfterTriggerPendingOnRel(Oid relid) static void AfterTriggerSaveEvent(ResultRelInfo *relinfo, int event, bool row_trigger, HeapTuple oldtup, HeapTuple newtup, - List *recheckIndexes) + List *recheckIndexes, Bitmapset *modifiedCols) { Relation rel = relinfo->ri_RelationDesc; TriggerDesc *trigdesc = relinfo->ri_TrigDesc; @@ -3927,26 +3979,15 @@ AfterTriggerSaveEvent(ResultRelInfo *relinfo, int event, bool row_trigger, { Trigger *trigger = &trigdesc->triggers[tgindx[i]]; - /* Ignore disabled triggers */ - if (SessionReplicationRole == SESSION_REPLICATION_ROLE_REPLICA) - { - if (trigger->tgenabled == TRIGGER_FIRES_ON_ORIGIN || - trigger->tgenabled == TRIGGER_DISABLED) - continue; - } - else /* ORIGIN or LOCAL role */ - { - if (trigger->tgenabled == TRIGGER_FIRES_ON_REPLICA || - trigger->tgenabled == TRIGGER_DISABLED) - continue; - } + if (!TriggerEnabled(trigger, event, modifiedCols)) + continue; /* * If this is an UPDATE of a PK table or FK table that does not change * the PK or FK respectively, we can skip queuing the event: there is * no need to fire the trigger. */ - if ((event & TRIGGER_EVENT_OPMASK) == TRIGGER_EVENT_UPDATE) + if (TRIGGER_FIRED_BY_UPDATE(event)) { switch (RI_FKey_trigger_type(trigger->tgfoid)) { diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c index 49360e506a..92da5324c2 100644 --- a/src/backend/nodes/copyfuncs.c +++ b/src/backend/nodes/copyfuncs.c @@ -15,7 +15,7 @@ * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/nodes/copyfuncs.c,v 1.447 2009/10/13 00:53:08 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/nodes/copyfuncs.c,v 1.448 2009/10/14 22:14:21 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -3160,6 +3160,7 @@ _copyCreateTrigStmt(CreateTrigStmt *from) COPY_SCALAR_FIELD(before); COPY_SCALAR_FIELD(row); COPY_SCALAR_FIELD(events); + COPY_NODE_FIELD(columns); COPY_SCALAR_FIELD(isconstraint); COPY_SCALAR_FIELD(deferrable); COPY_SCALAR_FIELD(initdeferred); diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c index cfa0a40de7..0c30d6aa9d 100644 --- a/src/backend/nodes/equalfuncs.c +++ b/src/backend/nodes/equalfuncs.c @@ -22,7 +22,7 @@ * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/nodes/equalfuncs.c,v 1.369 2009/10/13 00:53:08 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/nodes/equalfuncs.c,v 1.370 2009/10/14 22:14:21 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -1671,6 +1671,7 @@ _equalCreateTrigStmt(CreateTrigStmt *a, CreateTrigStmt *b) COMPARE_SCALAR_FIELD(before); COMPARE_SCALAR_FIELD(row); COMPARE_SCALAR_FIELD(events); + COMPARE_NODE_FIELD(columns); COMPARE_SCALAR_FIELD(isconstraint); COMPARE_SCALAR_FIELD(deferrable); COMPARE_SCALAR_FIELD(initdeferred); diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 20d40841ef..4fec749eba 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -9,7 +9,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/optimizer/plan/setrefs.c,v 1.152 2009/10/12 18:10:48 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/optimizer/plan/setrefs.c,v 1.153 2009/10/14 22:14:22 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -193,7 +193,8 @@ set_plan_references(PlannerGlobal *glob, Plan *plan, * needed by the executor; this reduces the storage space and copying cost * for cached plans. We keep only the alias and eref Alias fields, which * are needed by EXPLAIN, and the selectedCols and modifiedCols bitmaps, - * which are needed for executor-startup permissions checking. + * which are needed for executor-startup permissions checking and for + * trigger event checking. */ foreach(lc, rtable) { diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 17463ee3b8..2fe6ab1612 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -11,7 +11,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/parser/gram.y,v 2.685 2009/10/12 23:41:43 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/parser/gram.y,v 2.686 2009/10/14 22:14:22 tgl Exp $ * * HISTORY * AUTHOR DATE MAJOR EVENT @@ -251,7 +251,7 @@ static TypeName *TableFuncTypeName(List *columns); %type TriggerActionTime TriggerForSpec opt_trusted opt_restart_seqs -%type TriggerEvents TriggerOneEvent +%type TriggerEvents TriggerOneEvent %type TriggerFuncArg %type relation_name copy_file_name @@ -3240,7 +3240,8 @@ CreateTrigStmt: n->args = $13; n->before = $4; n->row = $8; - n->events = $5; + n->events = intVal(linitial($5)); + n->columns = (List *) lsecond($5); n->isconstraint = FALSE; n->deferrable = FALSE; n->initdeferred = FALSE; @@ -3260,7 +3261,8 @@ CreateTrigStmt: n->args = $18; n->before = FALSE; n->row = TRUE; - n->events = $6; + n->events = intVal(linitial($6)); + n->columns = (List *) lsecond($6); n->isconstraint = TRUE; n->deferrable = ($10 & 1) != 0; n->initdeferred = ($10 & 2) != 0; @@ -3279,17 +3281,36 @@ TriggerEvents: { $$ = $1; } | TriggerEvents OR TriggerOneEvent { - if ($1 & $3) + int events1 = intVal(linitial($1)); + int events2 = intVal(linitial($3)); + List *columns1 = (List *) lsecond($1); + List *columns2 = (List *) lsecond($3); + + if (events1 & events2) parser_yyerror("duplicate trigger events specified"); - $$ = $1 | $3; + /* + * concat'ing the columns lists loses information about + * which columns went with which event, but so long as + * only UPDATE carries columns and we disallow multiple + * UPDATE items, it doesn't matter. Command execution + * should just ignore the columns for non-UPDATE events. + */ + $$ = list_make2(makeInteger(events1 | events2), + list_concat(columns1, columns2)); } ; TriggerOneEvent: - INSERT { $$ = TRIGGER_TYPE_INSERT; } - | DELETE_P { $$ = TRIGGER_TYPE_DELETE; } - | UPDATE { $$ = TRIGGER_TYPE_UPDATE; } - | TRUNCATE { $$ = TRIGGER_TYPE_TRUNCATE; } + INSERT + { $$ = list_make2(makeInteger(TRIGGER_TYPE_INSERT), NIL); } + | DELETE_P + { $$ = list_make2(makeInteger(TRIGGER_TYPE_DELETE), NIL); } + | UPDATE + { $$ = list_make2(makeInteger(TRIGGER_TYPE_UPDATE), NIL); } + | UPDATE OF columnList + { $$ = list_make2(makeInteger(TRIGGER_TYPE_UPDATE), $3); } + | TRUNCATE + { $$ = list_make2(makeInteger(TRIGGER_TYPE_TRUNCATE), NIL); } ; TriggerForSpec: diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 35c530bbda..8538c74d12 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -9,7 +9,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/utils/adt/ruleutils.c,v 1.309 2009/10/10 01:43:49 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/utils/adt/ruleutils.c,v 1.310 2009/10/14 22:14:23 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -543,6 +543,23 @@ pg_get_triggerdef_worker(Oid trigid, bool pretty) appendStringInfo(&buf, " OR UPDATE"); else appendStringInfo(&buf, " UPDATE"); + /* tgattr is first var-width field, so OK to access directly */ + if (trigrec->tgattr.dim1 > 0) + { + int i; + + appendStringInfoString(&buf, " OF "); + for (i = 0; i < trigrec->tgattr.dim1; i++) + { + char *attname; + + if (i > 0) + appendStringInfoString(&buf, ", "); + attname = get_relid_attribute_name(trigrec->tgrelid, + trigrec->tgattr.values[i]); + appendStringInfoString(&buf, quote_identifier(attname)); + } + } } if (TRIGGER_FOR_TRUNCATE(trigrec->tgtype)) { diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index b0dfba9d6d..e25bf7ab0b 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -12,7 +12,7 @@ * by PostgreSQL * * IDENTIFICATION - * $PostgreSQL: pgsql/src/bin/pg_dump/pg_dump.c,v 1.551 2009/10/12 23:41:43 tgl Exp $ + * $PostgreSQL: pgsql/src/bin/pg_dump/pg_dump.c,v 1.552 2009/10/14 22:14:23 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -4419,6 +4419,18 @@ getTriggers(TableInfo tblinfo[], int numTables) if (i_tgdef >= 0) { tginfo[j].tgdef = strdup(PQgetvalue(res, j, i_tgdef)); + + /* remaining fields are not valid if we have tgdef */ + tginfo[j].tgfname = NULL; + tginfo[j].tgtype = 0; + tginfo[j].tgnargs = 0; + tginfo[j].tgargs = NULL; + tginfo[j].tgisconstraint = false; + tginfo[j].tgdeferrable = false; + tginfo[j].tginitdeferred = false; + tginfo[j].tgconstrname = NULL; + tginfo[j].tgconstrrelid = InvalidOid; + tginfo[j].tgconstrrelname = NULL; } else { diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h index abbcb006e1..ea053041bf 100644 --- a/src/include/catalog/catversion.h +++ b/src/include/catalog/catversion.h @@ -37,7 +37,7 @@ * Portions Copyright (c) 1996-2009, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * $PostgreSQL: pgsql/src/include/catalog/catversion.h,v 1.545 2009/10/12 18:10:51 tgl Exp $ + * $PostgreSQL: pgsql/src/include/catalog/catversion.h,v 1.546 2009/10/14 22:14:24 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -53,6 +53,6 @@ */ /* yyyymmddN */ -#define CATALOG_VERSION_NO 200910121 +#define CATALOG_VERSION_NO 200910141 #endif diff --git a/src/include/catalog/pg_trigger.h b/src/include/catalog/pg_trigger.h index 64730bc60f..eb6a583fd2 100644 --- a/src/include/catalog/pg_trigger.h +++ b/src/include/catalog/pg_trigger.h @@ -8,7 +8,7 @@ * Portions Copyright (c) 1996-2009, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * $PostgreSQL: pgsql/src/include/catalog/pg_trigger.h,v 1.34 2009/07/28 02:56:31 tgl Exp $ + * $PostgreSQL: pgsql/src/include/catalog/pg_trigger.h,v 1.35 2009/10/14 22:14:24 tgl Exp $ * * NOTES * the genbki.sh script reads this file and generates .bki @@ -52,8 +52,8 @@ CATALOG(pg_trigger,2620) bool tginitdeferred; /* constraint trigger is deferred initially */ int2 tgnargs; /* # of extra arguments in tgargs */ - /* VARIABLE LENGTH FIELDS: */ - int2vector tgattr; /* reserved for column-specific triggers */ + /* VARIABLE LENGTH FIELDS (note: these are not supposed to be null) */ + int2vector tgattr; /* column numbers, if trigger is on columns */ bytea tgargs; /* first\000second\000tgnargs\000 */ } FormData_pg_trigger; diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index ba05f157f5..adc870543b 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -13,7 +13,7 @@ * Portions Copyright (c) 1996-2009, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * $PostgreSQL: pgsql/src/include/nodes/parsenodes.h,v 1.409 2009/10/13 00:53:08 tgl Exp $ + * $PostgreSQL: pgsql/src/include/nodes/parsenodes.h,v 1.410 2009/10/14 22:14:24 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -1560,10 +1560,9 @@ typedef struct DropUserMappingStmt } DropUserMappingStmt; /* ---------------------- - * Create/Drop TRIGGER Statements + * Create TRIGGER Statement * ---------------------- */ - typedef struct CreateTrigStmt { NodeTag type; @@ -1575,6 +1574,7 @@ typedef struct CreateTrigStmt bool row; /* ROW/STATEMENT */ /* events uses the TRIGGER_TYPE bits defined in catalog/pg_trigger.h */ int16 events; /* INSERT/UPDATE/DELETE/TRUNCATE */ + List *columns; /* column names, or NIL for all columns */ /* The following are used for constraint triggers (RI and unique checks) */ bool isconstraint; /* This is a constraint trigger */ @@ -1584,7 +1584,7 @@ typedef struct CreateTrigStmt } CreateTrigStmt; /* ---------------------- - * Create/Drop PROCEDURAL LANGUAGE Statement + * Create/Drop PROCEDURAL LANGUAGE Statements * ---------------------- */ typedef struct CreatePLangStmt diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out index c0b7645600..b21c93f3b5 100644 --- a/src/test/regress/expected/triggers.out +++ b/src/test/regress/expected/triggers.out @@ -278,37 +278,37 @@ CREATE TABLE main_table (a int, b int); COPY main_table (a,b) FROM stdin; CREATE FUNCTION trigger_func() RETURNS trigger LANGUAGE plpgsql AS ' BEGIN - RAISE NOTICE ''trigger_func() called: action = %, when = %, level = %'', TG_OP, TG_WHEN, TG_LEVEL; + RAISE NOTICE ''trigger_func(%) called: action = %, when = %, level = %'', TG_ARGV[0], TG_OP, TG_WHEN, TG_LEVEL; RETURN NULL; END;'; CREATE TRIGGER before_ins_stmt_trig BEFORE INSERT ON main_table -FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func(); +FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('before_ins_stmt'); CREATE TRIGGER after_ins_stmt_trig AFTER INSERT ON main_table -FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func(); +FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('after_ins_stmt'); -- -- if neither 'FOR EACH ROW' nor 'FOR EACH STATEMENT' was specified, -- CREATE TRIGGER should default to 'FOR EACH STATEMENT' -- -CREATE TRIGGER before_upd_stmt_trig AFTER UPDATE ON main_table -EXECUTE PROCEDURE trigger_func(); -CREATE TRIGGER before_upd_row_trig AFTER UPDATE ON main_table -FOR EACH ROW EXECUTE PROCEDURE trigger_func(); +CREATE TRIGGER after_upd_stmt_trig AFTER UPDATE ON main_table +EXECUTE PROCEDURE trigger_func('after_upd_stmt'); +CREATE TRIGGER after_upd_row_trig AFTER UPDATE ON main_table +FOR EACH ROW EXECUTE PROCEDURE trigger_func('after_upd_row'); INSERT INTO main_table DEFAULT VALUES; -NOTICE: trigger_func() called: action = INSERT, when = BEFORE, level = STATEMENT -NOTICE: trigger_func() called: action = INSERT, when = AFTER, level = STATEMENT +NOTICE: trigger_func(before_ins_stmt) called: action = INSERT, when = BEFORE, level = STATEMENT +NOTICE: trigger_func(after_ins_stmt) called: action = INSERT, when = AFTER, level = STATEMENT UPDATE main_table SET a = a + 1 WHERE b < 30; -NOTICE: trigger_func() called: action = UPDATE, when = AFTER, level = ROW -NOTICE: trigger_func() called: action = UPDATE, when = AFTER, level = ROW -NOTICE: trigger_func() called: action = UPDATE, when = AFTER, level = ROW -NOTICE: trigger_func() called: action = UPDATE, when = AFTER, level = ROW -NOTICE: trigger_func() called: action = UPDATE, when = AFTER, level = STATEMENT +NOTICE: trigger_func(after_upd_row) called: action = UPDATE, when = AFTER, level = ROW +NOTICE: trigger_func(after_upd_row) called: action = UPDATE, when = AFTER, level = ROW +NOTICE: trigger_func(after_upd_row) called: action = UPDATE, when = AFTER, level = ROW +NOTICE: trigger_func(after_upd_row) called: action = UPDATE, when = AFTER, level = ROW +NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT -- UPDATE that effects zero rows should still call per-statement trigger UPDATE main_table SET a = a + 2 WHERE b > 100; -NOTICE: trigger_func() called: action = UPDATE, when = AFTER, level = STATEMENT +NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT -- COPY should fire per-row and per-statement INSERT triggers COPY main_table (a, b) FROM stdin; -NOTICE: trigger_func() called: action = INSERT, when = BEFORE, level = STATEMENT -NOTICE: trigger_func() called: action = INSERT, when = AFTER, level = STATEMENT +NOTICE: trigger_func(before_ins_stmt) called: action = INSERT, when = BEFORE, level = STATEMENT +NOTICE: trigger_func(after_ins_stmt) called: action = INSERT, when = AFTER, level = STATEMENT SELECT * FROM main_table ORDER BY a, b; a | b ----+---- @@ -322,6 +322,91 @@ SELECT * FROM main_table ORDER BY a, b; | (8 rows) +-- Test column-level triggers +DROP TRIGGER after_upd_row_trig ON main_table; +CREATE TRIGGER before_upd_a_row_trig BEFORE UPDATE OF a ON main_table +FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_upd_a_row'); +CREATE TRIGGER after_upd_b_row_trig AFTER UPDATE OF b ON main_table +FOR EACH ROW EXECUTE PROCEDURE trigger_func('after_upd_b_row'); +CREATE TRIGGER after_upd_a_b_row_trig AFTER UPDATE OF a, b ON main_table +FOR EACH ROW EXECUTE PROCEDURE trigger_func('after_upd_a_b_row'); +CREATE TRIGGER before_upd_a_stmt_trig BEFORE UPDATE OF a ON main_table +FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('before_upd_a_stmt'); +CREATE TRIGGER after_upd_b_stmt_trig AFTER UPDATE OF b ON main_table +FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('after_upd_b_stmt'); +SELECT pg_get_triggerdef(oid) FROM pg_trigger WHERE tgrelid = 'main_table'::regclass AND tgname = 'after_upd_a_b_row_trig'; + pg_get_triggerdef +------------------------------------------------------------------------------------------------------------------------------------------- + CREATE TRIGGER after_upd_a_b_row_trig AFTER UPDATE OF a, b ON main_table FOR EACH ROW EXECUTE PROCEDURE trigger_func('after_upd_a_b_row') +(1 row) + +SELECT pg_get_triggerdef(oid, true) FROM pg_trigger WHERE tgrelid = 'main_table'::regclass AND tgname = 'after_upd_a_b_row_trig'; + pg_get_triggerdef +--------------------------------------------------------- + CREATE TRIGGER after_upd_a_b_row_trig + AFTER UPDATE OF a, b ON main_table + FOR EACH ROW + EXECUTE PROCEDURE trigger_func('after_upd_a_b_row') +(1 row) + +UPDATE main_table SET a = 50; +NOTICE: trigger_func(before_upd_a_stmt) called: action = UPDATE, when = BEFORE, level = STATEMENT +NOTICE: trigger_func(before_upd_a_row) called: action = UPDATE, when = BEFORE, level = ROW +NOTICE: trigger_func(before_upd_a_row) called: action = UPDATE, when = BEFORE, level = ROW +NOTICE: trigger_func(before_upd_a_row) called: action = UPDATE, when = BEFORE, level = ROW +NOTICE: trigger_func(before_upd_a_row) called: action = UPDATE, when = BEFORE, level = ROW +NOTICE: trigger_func(before_upd_a_row) called: action = UPDATE, when = BEFORE, level = ROW +NOTICE: trigger_func(before_upd_a_row) called: action = UPDATE, when = BEFORE, level = ROW +NOTICE: trigger_func(before_upd_a_row) called: action = UPDATE, when = BEFORE, level = ROW +NOTICE: trigger_func(before_upd_a_row) called: action = UPDATE, when = BEFORE, level = ROW +NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT +UPDATE main_table SET b = 10; +NOTICE: trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER, level = ROW +NOTICE: trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW +NOTICE: trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER, level = ROW +NOTICE: trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW +NOTICE: trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER, level = ROW +NOTICE: trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW +NOTICE: trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER, level = ROW +NOTICE: trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW +NOTICE: trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER, level = ROW +NOTICE: trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW +NOTICE: trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER, level = ROW +NOTICE: trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW +NOTICE: trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER, level = ROW +NOTICE: trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW +NOTICE: trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER, level = ROW +NOTICE: trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW +NOTICE: trigger_func(after_upd_b_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT +NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT +-- bogus cases +CREATE TRIGGER error_upd_and_col BEFORE UPDATE OR UPDATE OF a ON main_table +FOR EACH ROW EXECUTE PROCEDURE trigger_func('error_upd_and_col'); +ERROR: duplicate trigger events specified at or near "ON" +LINE 1: ...ER error_upd_and_col BEFORE UPDATE OR UPDATE OF a ON main_ta... + ^ +CREATE TRIGGER error_upd_a_a BEFORE UPDATE OF a, a ON main_table +FOR EACH ROW EXECUTE PROCEDURE trigger_func('error_upd_a_a'); +ERROR: column "a" specified more than once +CREATE TRIGGER error_ins_a BEFORE INSERT OF a ON main_table +FOR EACH ROW EXECUTE PROCEDURE trigger_func('error_ins_a'); +ERROR: syntax error at or near "OF" +LINE 1: CREATE TRIGGER error_ins_a BEFORE INSERT OF a ON main_table + ^ +-- check dependency restrictions +ALTER TABLE main_table DROP COLUMN b; +ERROR: cannot drop table main_table column b because other objects depend on it +DETAIL: trigger after_upd_b_row_trig on table main_table depends on table main_table column b +trigger after_upd_a_b_row_trig on table main_table depends on table main_table column b +trigger after_upd_b_stmt_trig on table main_table depends on table main_table column b +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- this should succeed, but we'll roll it back to keep the triggers around +begin; +DROP TRIGGER after_upd_a_b_row_trig ON main_table; +DROP TRIGGER after_upd_b_row_trig ON main_table; +DROP TRIGGER after_upd_b_stmt_trig ON main_table; +ALTER TABLE main_table DROP COLUMN b; +rollback; -- Test enable/disable triggers create table trigtest (i serial primary key); NOTICE: CREATE TABLE will create implicit sequence "trigtest_i_seq" for serial column "trigtest.i" diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql index 878adbb0d4..70c563477c 100644 --- a/src/test/regress/sql/triggers.sql +++ b/src/test/regress/sql/triggers.sql @@ -220,25 +220,25 @@ COPY main_table (a,b) FROM stdin; CREATE FUNCTION trigger_func() RETURNS trigger LANGUAGE plpgsql AS ' BEGIN - RAISE NOTICE ''trigger_func() called: action = %, when = %, level = %'', TG_OP, TG_WHEN, TG_LEVEL; + RAISE NOTICE ''trigger_func(%) called: action = %, when = %, level = %'', TG_ARGV[0], TG_OP, TG_WHEN, TG_LEVEL; RETURN NULL; END;'; CREATE TRIGGER before_ins_stmt_trig BEFORE INSERT ON main_table -FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func(); +FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('before_ins_stmt'); CREATE TRIGGER after_ins_stmt_trig AFTER INSERT ON main_table -FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func(); +FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('after_ins_stmt'); -- -- if neither 'FOR EACH ROW' nor 'FOR EACH STATEMENT' was specified, -- CREATE TRIGGER should default to 'FOR EACH STATEMENT' -- -CREATE TRIGGER before_upd_stmt_trig AFTER UPDATE ON main_table -EXECUTE PROCEDURE trigger_func(); +CREATE TRIGGER after_upd_stmt_trig AFTER UPDATE ON main_table +EXECUTE PROCEDURE trigger_func('after_upd_stmt'); -CREATE TRIGGER before_upd_row_trig AFTER UPDATE ON main_table -FOR EACH ROW EXECUTE PROCEDURE trigger_func(); +CREATE TRIGGER after_upd_row_trig AFTER UPDATE ON main_table +FOR EACH ROW EXECUTE PROCEDURE trigger_func('after_upd_row'); INSERT INTO main_table DEFAULT VALUES; @@ -254,6 +254,45 @@ COPY main_table (a, b) FROM stdin; SELECT * FROM main_table ORDER BY a, b; +-- Test column-level triggers +DROP TRIGGER after_upd_row_trig ON main_table; + +CREATE TRIGGER before_upd_a_row_trig BEFORE UPDATE OF a ON main_table +FOR EACH ROW EXECUTE PROCEDURE trigger_func('before_upd_a_row'); +CREATE TRIGGER after_upd_b_row_trig AFTER UPDATE OF b ON main_table +FOR EACH ROW EXECUTE PROCEDURE trigger_func('after_upd_b_row'); +CREATE TRIGGER after_upd_a_b_row_trig AFTER UPDATE OF a, b ON main_table +FOR EACH ROW EXECUTE PROCEDURE trigger_func('after_upd_a_b_row'); + +CREATE TRIGGER before_upd_a_stmt_trig BEFORE UPDATE OF a ON main_table +FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('before_upd_a_stmt'); +CREATE TRIGGER after_upd_b_stmt_trig AFTER UPDATE OF b ON main_table +FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('after_upd_b_stmt'); + +SELECT pg_get_triggerdef(oid) FROM pg_trigger WHERE tgrelid = 'main_table'::regclass AND tgname = 'after_upd_a_b_row_trig'; +SELECT pg_get_triggerdef(oid, true) FROM pg_trigger WHERE tgrelid = 'main_table'::regclass AND tgname = 'after_upd_a_b_row_trig'; + +UPDATE main_table SET a = 50; +UPDATE main_table SET b = 10; + +-- bogus cases +CREATE TRIGGER error_upd_and_col BEFORE UPDATE OR UPDATE OF a ON main_table +FOR EACH ROW EXECUTE PROCEDURE trigger_func('error_upd_and_col'); +CREATE TRIGGER error_upd_a_a BEFORE UPDATE OF a, a ON main_table +FOR EACH ROW EXECUTE PROCEDURE trigger_func('error_upd_a_a'); +CREATE TRIGGER error_ins_a BEFORE INSERT OF a ON main_table +FOR EACH ROW EXECUTE PROCEDURE trigger_func('error_ins_a'); + +-- check dependency restrictions +ALTER TABLE main_table DROP COLUMN b; +-- this should succeed, but we'll roll it back to keep the triggers around +begin; +DROP TRIGGER after_upd_a_b_row_trig ON main_table; +DROP TRIGGER after_upd_b_row_trig ON main_table; +DROP TRIGGER after_upd_b_stmt_trig ON main_table; +ALTER TABLE main_table DROP COLUMN b; +rollback; + -- Test enable/disable triggers create table trigtest (i serial primary key); -- 2.11.0