From b339d1fff6c2f14776af29a35c8550b222ca70b2 Mon Sep 17 00:00:00 2001 From: Tom Lane <tgl@sss.pgh.pa.us> Date: Fri, 10 Sep 2004 18:40:09 +0000 Subject: [PATCH] Fire non-deferred AFTER triggers immediately upon query completion, rather than when returning to the idle loop. This makes no particular difference for interactively-issued queries, but it makes a big difference for queries issued within functions: trigger execution now occurs before the calling function is allowed to proceed. This responds to numerous complaints about nonintuitive behavior of foreign key checking, such as http://archives.postgresql.org/pgsql-bugs/2004-09/msg00020.php, and appears to be required by the SQL99 spec. Also take the opportunity to simplify the data structures used for the pending-trigger list, rename them for more clarity, and squeeze out a bit of space. --- doc/src/sgml/ref/set_constraints.sgml | 31 +- doc/src/sgml/release.sgml | 26 +- src/backend/access/transam/xact.c | 82 +- src/backend/commands/copy.c | 12 +- src/backend/commands/explain.c | 15 +- src/backend/commands/portalcmds.c | 4 +- src/backend/commands/trigger.c | 1195 ++++++++++++--------- src/backend/executor/functions.c | 9 +- src/backend/executor/spi.c | 12 +- src/backend/tcop/postgres.c | 5 +- src/backend/tcop/pquery.c | 22 +- src/backend/tcop/utility.c | 4 +- src/backend/utils/adt/ri_triggers.c | 4 +- src/include/commands/trigger.h | 28 +- src/test/regress/expected/foreign_key.out | 1 + src/test/regress/expected/plpgsql.out | 71 ++ src/test/regress/sql/plpgsql.sql | 51 + 17 files changed, 954 insertions(+), 618 deletions(-) diff --git a/doc/src/sgml/ref/set_constraints.sgml b/doc/src/sgml/ref/set_constraints.sgml index 0c976c93e25..3bcde91f386 100644 --- a/doc/src/sgml/ref/set_constraints.sgml +++ b/doc/src/sgml/ref/set_constraints.sgml @@ -1,4 +1,4 @@ -<!-- $PostgreSQL: pgsql/doc/src/sgml/ref/set_constraints.sgml,v 1.11 2004/09/08 20:47:37 tgl Exp $ --> +<!-- $PostgreSQL: pgsql/doc/src/sgml/ref/set_constraints.sgml,v 1.12 2004/09/10 18:39:53 tgl Exp $ --> <refentry id="SQL-SET-CONSTRAINTS"> <refmeta> <refentrytitle id="SQL-SET-CONSTRAINTS-title">SET CONSTRAINTS</refentrytitle> @@ -34,13 +34,13 @@ SET CONSTRAINTS { ALL | <replaceable class="parameter">name</replaceable> [, ... <para> Upon creation, a constraint is given one of three - characteristics: <literal>INITIALLY DEFERRED</literal>, - <literal>INITIALLY IMMEDIATE DEFERRABLE</literal>, or - <literal>INITIALLY IMMEDIATE NOT DEFERRABLE</literal>. The third - class is not affected by the <command>SET CONSTRAINTS</command> - command. The first two classes start every transaction in the - indicated mode, but their behavior can be changed within a transaction - by <command>SET CONSTRAINTS</command>. + characteristics: <literal>DEFERRABLE INITIALLY DEFERRED</literal>, + <literal>DEFERRABLE INITIALLY IMMEDIATE</literal>, or + <literal>NOT DEFERRABLE</literal>. The third + class is always <literal>IMMEDIATE</literal> and is not affected by the + <command>SET CONSTRAINTS</command> command. The first two classes start + every transaction in the indicated mode, but their behavior can be changed + within a transaction by <command>SET CONSTRAINTS</command>. </para> <para> @@ -52,19 +52,22 @@ SET CONSTRAINTS { ALL | <replaceable class="parameter">name</replaceable> [, ... </para> <para> - When you change the mode of a constraint from <literal>DEFERRED</literal> + When <command>SET CONSTRAINTS</command> changes the mode of a constraint + from <literal>DEFERRED</literal> to <literal>IMMEDIATE</literal>, the new mode takes effect retroactively: any outstanding data modifications that would have been checked at the end of the transaction are instead checked during the execution of the <command>SET CONSTRAINTS</command> command. If any such constraint is violated, the <command>SET CONSTRAINTS</command> - fails (and does not change the constraint mode). + fails (and does not change the constraint mode). Thus, <command>SET + CONSTRAINTS</command> can be used to force checking of constraints to + occur at a specific point in a transaction. </para> <para> Currently, only foreign key constraints are affected by this setting. Check and unique constraints are always effectively - initially immediate not deferrable. + not deferrable. </para> </refsect1> @@ -76,11 +79,7 @@ SET CONSTRAINTS { ALL | <replaceable class="parameter">name</replaceable> [, ... current transaction. Thus, if you execute this command outside of a transaction block (<command>BEGIN</command>/<command>COMMIT</command> pair), it will - not appear to have any effect. If you wish to change the behavior - of a constraint without needing to issue a <command>SET - CONSTRAINTS</command> command in every transaction, specify - <literal>INITIALLY DEFERRED</literal> or <literal>INITIALLY - IMMEDIATE</literal> when you create the constraint. + not appear to have any effect. </para> </refsect1> diff --git a/doc/src/sgml/release.sgml b/doc/src/sgml/release.sgml index b742f675e30..ddf93a04ff9 100644 --- a/doc/src/sgml/release.sgml +++ b/doc/src/sgml/release.sgml @@ -1,5 +1,5 @@ <!-- -$PostgreSQL: pgsql/doc/src/sgml/release.sgml,v 1.294 2004/08/30 00:47:31 tgl Exp $ +$PostgreSQL: pgsql/doc/src/sgml/release.sgml,v 1.295 2004/09/10 18:39:54 tgl Exp $ --> <appendix id="release"> @@ -336,6 +336,16 @@ $PostgreSQL: pgsql/doc/src/sgml/release.sgml,v 1.294 2004/08/30 00:47:31 tgl Exp whitespace (which has always been ignored). </para> </listitem> + + <listitem> + <para> + Non-deferred AFTER triggers are now fired immediately after completion + of the triggering query, rather than upon finishing the current + interactive command. This makes a difference when the triggering query + occurred within a function: the trigger is invoked before the function + proceeds to its next operation. + </para> + </listitem> </itemizedlist> </para> </sect2> @@ -1424,6 +1434,18 @@ $PostgreSQL: pgsql/doc/src/sgml/release.sgml,v 1.294 2004/08/30 00:47:31 tgl Exp <title>Server-Side Language Changes</title> <itemizedlist> + <listitem> + <para> + Non-deferred AFTER triggers are now fired immediately after completion + of the triggering query, rather than upon finishing the current + interactive command. This makes a difference when the triggering query + occurred within a function: the trigger is invoked before the function + proceeds to its next operation. For example, if a function inserts + a new row into a table, any non-deferred foreign key checks occur + before proceeding with the function. + </para> + </listitem> + <listitem> <para> Allow function parameters to be declared with names (Dennis Bjorklund) @@ -1483,7 +1505,7 @@ $PostgreSQL: pgsql/doc/src/sgml/release.sgml,v 1.294 2004/08/30 00:47:31 tgl Exp <listitem> <para> - New plperl server-side language (Command Prompt, Andrew Dunstan) + Major overhaul of plperl server-side language (Command Prompt, Andrew Dunstan) </para> </listitem> diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 92c9de0ea75..47c501c393e 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -10,7 +10,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/access/transam/xact.c,v 1.186 2004/09/06 17:56:04 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/access/transam/xact.c,v 1.187 2004/09/10 18:39:55 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -138,7 +138,6 @@ static void CleanupSubTransaction(void); static void StartAbortedSubTransaction(void); static void PushTransaction(void); static void PopTransaction(void); -static void CommitTransactionToLevel(int level); static char *CleanupAbortedSubTransactions(bool returnName); static void AtSubAbort_Memory(void); @@ -1219,7 +1218,7 @@ StartTransaction(void) */ AtStart_Inval(); AtStart_Cache(); - DeferredTriggerBeginXact(); + AfterTriggerBeginXact(); /* * done with start processing, set current transaction state to "in @@ -1253,7 +1252,7 @@ CommitTransaction(void) * committed. He'll invoke all trigger deferred until XACT before we * really start on committing the transaction. */ - DeferredTriggerEndXact(); + AfterTriggerEndXact(); /* * Similarly, let ON COMMIT management do its thing before we start to @@ -1454,7 +1453,7 @@ AbortTransaction(void) /* * do abort processing */ - DeferredTriggerAbortXact(); + AfterTriggerAbortXact(); AtAbort_Portals(); AtEOXact_LargeObject(false); /* 'false' means it's abort */ AtAbort_Notify(); @@ -1672,12 +1671,6 @@ CommitTransactionCommand(void) * default state. */ case TBLOCK_END: - /* commit all open subtransactions */ - if (s->nestingLevel > 1) - CommitTransactionToLevel(2); - s = CurrentTransactionState; - Assert(s->parent == NULL); - /* and now the outer transaction */ CommitTransaction(); s->blockState = TBLOCK_DEFAULT; break; @@ -1732,11 +1725,10 @@ CommitTransactionCommand(void) break; /* - * We were issued a RELEASE command, so we end the current - * subtransaction and return to the parent transaction. - * - * Since RELEASE can exit multiple levels of subtransaction, we - * must loop here until we get out of all SUBEND'ed levels. + * We were issued a COMMIT or RELEASE command, so we end the + * current subtransaction and return to the parent transaction. + * Lather, rinse, and repeat until we get out of all SUBEND'ed + * subtransaction levels. */ case TBLOCK_SUBEND: do @@ -1745,6 +1737,13 @@ CommitTransactionCommand(void) PopTransaction(); s = CurrentTransactionState; /* changed by pop */ } while (s->blockState == TBLOCK_SUBEND); + /* If we had a COMMIT command, finish off the main xact too */ + if (s->blockState == TBLOCK_END) + { + Assert(s->parent == NULL); + CommitTransaction(); + s->blockState = TBLOCK_DEFAULT; + } break; /* @@ -2238,7 +2237,6 @@ EndTransactionBlock(void) * the default state. */ case TBLOCK_INPROGRESS: - case TBLOCK_SUBINPROGRESS: s->blockState = TBLOCK_END; result = true; break; @@ -2254,6 +2252,22 @@ EndTransactionBlock(void) s->blockState = TBLOCK_ENDABORT; break; + /* + * We are in a live subtransaction block. Set up to subcommit + * all open subtransactions and then commit the main transaction. + */ + case TBLOCK_SUBINPROGRESS: + while (s->parent != NULL) + { + Assert(s->blockState == TBLOCK_SUBINPROGRESS); + s->blockState = TBLOCK_SUBEND; + s = s->parent; + } + Assert(s->blockState == TBLOCK_INPROGRESS); + s->blockState = TBLOCK_END; + result = true; + break; + /* * Here we are inside an aborted subtransaction. Go to the * "abort the whole tree" state so that @@ -2699,8 +2713,12 @@ ReleaseCurrentSubTransaction(void) if (s->blockState != TBLOCK_SUBINPROGRESS) elog(ERROR, "ReleaseCurrentSubTransaction: unexpected state %s", BlockStateAsString(s->blockState)); + Assert(s->state == TRANS_INPROGRESS); MemoryContextSwitchTo(CurTransactionContext); - CommitTransactionToLevel(GetCurrentTransactionNestLevel()); + CommitSubTransaction(); + PopTransaction(); + s = CurrentTransactionState; /* changed by pop */ + Assert(s->state == TRANS_INPROGRESS); } /* @@ -2827,28 +2845,6 @@ AbortOutOfAnyTransaction(void) Assert(s->parent == NULL); } -/* - * CommitTransactionToLevel - * - * Commit everything from the current transaction level - * up to the specified level (inclusive). - */ -static void -CommitTransactionToLevel(int level) -{ - TransactionState s = CurrentTransactionState; - - Assert(s->state == TRANS_INPROGRESS); - - while (s->nestingLevel >= level) - { - CommitSubTransaction(); - PopTransaction(); - s = CurrentTransactionState; /* changed by pop */ - Assert(s->state == TRANS_INPROGRESS); - } -} - /* * IsTransactionBlock --- are we within a transaction block? */ @@ -2975,7 +2971,7 @@ StartSubTransaction(void) */ AtSubStart_Inval(); AtSubStart_Notify(); - DeferredTriggerBeginSubXact(); + AfterTriggerBeginSubXact(); s->state = TRANS_INPROGRESS; @@ -3011,7 +3007,7 @@ CommitSubTransaction(void) AtSubCommit_childXids(); /* Post-commit cleanup */ - DeferredTriggerEndSubXact(true); + AfterTriggerEndSubXact(true); AtSubCommit_Portals(s->parent->transactionIdData, s->parent->curTransactionOwner); AtEOSubXact_LargeObject(true, s->transactionIdData, @@ -3101,7 +3097,7 @@ AbortSubTransaction(void) */ AtSubAbort_Memory(); - DeferredTriggerEndSubXact(false); + AfterTriggerEndSubXact(false); AtSubAbort_Portals(s->parent->transactionIdData, s->parent->curTransactionOwner); AtEOSubXact_LargeObject(false, s->transactionIdData, diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c index 5793c0b2bbb..b25c8eee98c 100644 --- a/src/backend/commands/copy.c +++ b/src/backend/commands/copy.c @@ -8,7 +8,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/commands/copy.c,v 1.230 2004/08/29 05:06:41 momjian Exp $ + * $PostgreSQL: pgsql/src/backend/commands/copy.c,v 1.231 2004/09/10 18:39:56 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -1610,6 +1610,11 @@ CopyFrom(Relation rel, List *attnumlist, bool binary, bool oids, } } + /* + * Prepare to catch AFTER triggers. + */ + AfterTriggerBeginQuery(); + /* * Check BEFORE STATEMENT insertion triggers. It's debateable whether * we should do this for COPY, since it's not really an "INSERT" @@ -1974,6 +1979,11 @@ CopyFrom(Relation rel, List *attnumlist, bool binary, bool oids, */ ExecASInsertTriggers(estate, resultRelInfo); + /* + * Handle queued AFTER triggers + */ + AfterTriggerEndQuery(); + pfree(values); pfree(nulls); diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index 7ad3596fac6..9b7cf1ae491 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -7,7 +7,7 @@ * Portions Copyright (c) 1994-5, Regents of the University of California * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/commands/explain.c,v 1.124 2004/08/29 05:06:41 momjian Exp $ + * $PostgreSQL: pgsql/src/backend/commands/explain.c,v 1.125 2004/09/10 18:39:56 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -18,6 +18,7 @@ #include "catalog/pg_type.h" #include "commands/explain.h" #include "commands/prepare.h" +#include "commands/trigger.h" #include "executor/executor.h" #include "executor/instrument.h" #include "lib/stringinfo.h" @@ -206,6 +207,10 @@ ExplainOnePlan(QueryDesc *queryDesc, ExplainStmt *stmt, gettimeofday(&starttime, NULL); + /* If analyzing, we need to cope with queued triggers */ + if (stmt->analyze) + AfterTriggerBeginQuery(); + /* call ExecutorStart to prepare the plan for execution */ ExecutorStart(queryDesc, false, !stmt->analyze); @@ -255,12 +260,16 @@ ExplainOnePlan(QueryDesc *queryDesc, ExplainStmt *stmt, } /* - * Close down the query and free resources. Include time for this in - * the total runtime. + * Close down the query and free resources; also run any queued + * AFTER triggers. Include time for this in the total runtime. */ gettimeofday(&starttime, NULL); ExecutorEnd(queryDesc); + + if (stmt->analyze) + AfterTriggerEndQuery(); + FreeQueryDesc(queryDesc); CommandCounterIncrement(); diff --git a/src/backend/commands/portalcmds.c b/src/backend/commands/portalcmds.c index 08b14013547..390e20aa2e9 100644 --- a/src/backend/commands/portalcmds.c +++ b/src/backend/commands/portalcmds.c @@ -14,7 +14,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/commands/portalcmds.c,v 1.33 2004/08/29 05:06:41 momjian Exp $ + * $PostgreSQL: pgsql/src/backend/commands/portalcmds.c,v 1.34 2004/09/10 18:39:56 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -273,6 +273,7 @@ PortalCleanup(Portal portal) { CurrentResourceOwner = portal->resowner; ExecutorEnd(queryDesc); + /* we do not need AfterTriggerEndQuery() here */ } PG_CATCH(); { @@ -373,6 +374,7 @@ PersistHoldablePortal(Portal portal) */ portal->queryDesc = NULL; /* prevent double shutdown */ ExecutorEnd(queryDesc); + /* we do not need AfterTriggerEndQuery() here */ /* * Reset the position in the result set: ideally, this could be diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c index 0efc4afb58c..5480fce189f 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.171 2004/09/08 23:47:58 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/commands/trigger.c,v 1.172 2004/09/10 18:39:56 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -48,7 +48,7 @@ static HeapTuple GetTupleForTrigger(EState *estate, static HeapTuple ExecCallTriggerFunc(TriggerData *trigdata, FmgrInfo *finfo, MemoryContext per_tuple_context); -static void DeferredTriggerSaveEvent(ResultRelInfo *relinfo, int event, +static void AfterTriggerSaveEvent(ResultRelInfo *relinfo, int event, bool row_trigger, HeapTuple oldtup, HeapTuple newtup); @@ -1219,8 +1219,8 @@ ExecASInsertTriggers(EState *estate, ResultRelInfo *relinfo) TriggerDesc *trigdesc = relinfo->ri_TrigDesc; if (trigdesc && trigdesc->n_after_statement[TRIGGER_EVENT_INSERT] > 0) - DeferredTriggerSaveEvent(relinfo, TRIGGER_EVENT_INSERT, - false, NULL, NULL); + AfterTriggerSaveEvent(relinfo, TRIGGER_EVENT_INSERT, + false, NULL, NULL); } HeapTuple @@ -1272,8 +1272,8 @@ ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo, TriggerDesc *trigdesc = relinfo->ri_TrigDesc; if (trigdesc && trigdesc->n_after_row[TRIGGER_EVENT_INSERT] > 0) - DeferredTriggerSaveEvent(relinfo, TRIGGER_EVENT_INSERT, - true, NULL, trigtuple); + AfterTriggerSaveEvent(relinfo, TRIGGER_EVENT_INSERT, + true, NULL, trigtuple); } void @@ -1332,8 +1332,8 @@ ExecASDeleteTriggers(EState *estate, ResultRelInfo *relinfo) TriggerDesc *trigdesc = relinfo->ri_TrigDesc; if (trigdesc && trigdesc->n_after_statement[TRIGGER_EVENT_DELETE] > 0) - DeferredTriggerSaveEvent(relinfo, TRIGGER_EVENT_DELETE, - false, NULL, NULL); + AfterTriggerSaveEvent(relinfo, TRIGGER_EVENT_DELETE, + false, NULL, NULL); } bool @@ -1399,8 +1399,8 @@ ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo, (CommandId) 0, NULL); - DeferredTriggerSaveEvent(relinfo, TRIGGER_EVENT_DELETE, - true, trigtuple, NULL); + AfterTriggerSaveEvent(relinfo, TRIGGER_EVENT_DELETE, + true, trigtuple, NULL); heap_freetuple(trigtuple); } } @@ -1461,8 +1461,8 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo) TriggerDesc *trigdesc = relinfo->ri_TrigDesc; if (trigdesc && trigdesc->n_after_statement[TRIGGER_EVENT_UPDATE] > 0) - DeferredTriggerSaveEvent(relinfo, TRIGGER_EVENT_UPDATE, - false, NULL, NULL); + AfterTriggerSaveEvent(relinfo, TRIGGER_EVENT_UPDATE, + false, NULL, NULL); } HeapTuple @@ -1535,8 +1535,8 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo, (CommandId) 0, NULL); - DeferredTriggerSaveEvent(relinfo, TRIGGER_EVENT_UPDATE, - true, trigtuple, newtuple); + AfterTriggerSaveEvent(relinfo, TRIGGER_EVENT_UPDATE, + true, trigtuple, newtuple); heap_freetuple(trigtuple); } } @@ -1635,87 +1635,35 @@ ltrmark:; /* ---------- - * Deferred trigger stuff + * After-trigger stuff * - * The DeferredTriggersData struct holds data about pending deferred - * trigger events during the current transaction tree. The struct and - * most of its subsidiary data are kept in TopTransactionContext; however + * The AfterTriggersData struct holds data about pending AFTER trigger events + * during the current transaction tree. (BEFORE triggers are fired + * immediately so we don't need any persistent state about them.) The struct + * and most of its subsidiary data are kept in TopTransactionContext; however * the individual event records are kept in CurTransactionContext, so that * they will easily go away during subtransaction abort. * - * DeferredTriggersData has the following fields: - * - * state keeps track of the deferred state of each trigger - * (including the global state). This is saved and restored across - * failed subtransactions. - * - * events is the head of the list of events. - * - * tail_thisxact points to the tail of the list, for the current - * transaction (whether main transaction or subtransaction). We always - * append to the list using this pointer. - * - * events_imm points to the last element scanned by the last - * deferredTriggerInvokeEvents call. We can use this to avoid rescanning - * unnecessarily; if it's NULL, the scan should start at the head of the - * list. Its name comes from the fact that it's set to the last event fired - * by the last call to immediate triggers. - * - * tail_stack and imm_stack are stacks of pointer, which hold the pointers - * to the tail and the "immediate" events as of the start of a subtransaction. - * We use to revert them when aborting the subtransaction. - * - * state_stack is a stack of pointers to saved copies of the deferred-trigger - * state data; each subtransaction level that modifies that state first - * saves a copy, which we use to restore the state if we abort. - * - * We use GetCurrentTransactionNestLevel() to determine the correct array - * index in these stacks. numalloc is the number of allocated entries in - * each stack. (By not keeping our own stack pointer, we can avoid trouble - * in cases where errors during subxact abort cause multiple invocations - * of DeferredTriggerEndSubXact() at the same nesting depth.) + * Because the list of pending events can grow large, we go to some effort + * to minimize memory consumption. We do not use the generic List mechanism + * but thread the events manually. * * XXX We need to be able to save the per-event data in a file if it grows too * large. * ---------- */ -/* Per-item data */ -typedef struct DeferredTriggerEventItem -{ - Oid dti_tgoid; - TransactionId dti_done_xid; - int32 dti_state; -} DeferredTriggerEventItem; - -typedef struct DeferredTriggerEventData *DeferredTriggerEvent; - -/* Per-event data */ -typedef struct DeferredTriggerEventData -{ - DeferredTriggerEvent dte_next; /* list link */ - int32 dte_event; - Oid dte_relid; - TransactionId dte_done_xid; - ItemPointerData dte_oldctid; - ItemPointerData dte_newctid; - int32 dte_n_items; - /* dte_item is actually a variable-size array, of length dte_n_items */ - DeferredTriggerEventItem dte_item[1]; -} DeferredTriggerEventData; - -/* Per-trigger status data */ -typedef struct DeferredTriggerStatusData +/* Per-trigger SET CONSTRAINT status */ +typedef struct SetConstraintTriggerData { - Oid dts_tgoid; - bool dts_tgisdeferred; -} DeferredTriggerStatusData; - -typedef struct DeferredTriggerStatusData *DeferredTriggerStatus; + Oid sct_tgoid; + bool sct_tgisdeferred; +} SetConstraintTriggerData; +typedef struct SetConstraintTriggerData *SetConstraintTrigger; /* - * Trigger deferral status data. + * SET CONSTRAINT intra-transaction status. * * We make this a single palloc'd object so it can be copied and freed easily. * @@ -1724,62 +1672,148 @@ typedef struct DeferredTriggerStatusData *DeferredTriggerStatus; * * trigstates[] stores per-trigger tgisdeferred settings. */ -typedef struct DeferredTriggerStateData +typedef struct SetConstraintStateData { bool all_isset; bool all_isdeferred; int numstates; /* number of trigstates[] entries in use */ int numalloc; /* allocated size of trigstates[] */ - DeferredTriggerStatusData trigstates[1]; /* VARIABLE LENGTH ARRAY */ -} DeferredTriggerStateData; + SetConstraintTriggerData trigstates[1]; /* VARIABLE LENGTH ARRAY */ +} SetConstraintStateData; -typedef DeferredTriggerStateData *DeferredTriggerState; +typedef SetConstraintStateData *SetConstraintState; -/* Per-transaction data */ -typedef struct DeferredTriggersData -{ - DeferredTriggerState state; - DeferredTriggerEvent events; - DeferredTriggerEvent tail_thisxact; - DeferredTriggerEvent events_imm; - DeferredTriggerEvent *tail_stack; - DeferredTriggerEvent *imm_stack; - DeferredTriggerState *state_stack; - int numalloc; -} DeferredTriggersData; -typedef DeferredTriggersData *DeferredTriggers; +/* + * Per-trigger-event data + * + * Note: ate_firing_id is meaningful when either AFTER_TRIGGER_DONE + * or AFTER_TRIGGER_IN_PROGRESS is set. It indicates which trigger firing + * cycle the trigger was or will be fired in. + */ +typedef struct AfterTriggerEventData *AfterTriggerEvent; -static DeferredTriggers deferredTriggers; +typedef struct AfterTriggerEventData +{ + AfterTriggerEvent ate_next; /* list link */ + TriggerEvent ate_event; /* event type and status bits */ + CommandId ate_firing_id; /* ID for firing cycle */ + Oid ate_tgoid; /* the trigger's ID */ + Oid ate_relid; /* the relation it's on */ + ItemPointerData ate_oldctid; /* specific tuple(s) involved */ + ItemPointerData ate_newctid; +} AfterTriggerEventData; + +/* A list of events */ +typedef struct AfterTriggerEventList +{ + AfterTriggerEvent head; + AfterTriggerEvent tail; +} AfterTriggerEventList; -static void DeferredTriggerExecute(DeferredTriggerEvent event, int itemno, - Relation rel, TriggerDesc *trigdesc, FmgrInfo *finfo, - MemoryContext per_tuple_context); -static DeferredTriggerState DeferredTriggerStateCreate(int numalloc); -static DeferredTriggerState DeferredTriggerStateCopy(DeferredTriggerState state); -static DeferredTriggerState DeferredTriggerStateAddItem(DeferredTriggerState state, +/* + * All per-transaction data for the AFTER TRIGGERS module. + * + * AfterTriggersData has the following fields: + * + * firing_counter is incremented for each call of afterTriggerInvokeEvents. + * We mark firable events with the current firing cycle's ID so that we can + * tell which ones to work on. This ensures sane behavior if a trigger + * function chooses to do SET CONSTRAINTS: the inner SET CONSTRAINTS will + * only fire those events that weren't already scheduled for firing. + * + * state keeps track of the transaction-local effects of SET CONSTRAINTS. + * This is saved and restored across failed subtransactions. + * + * events is the current list of deferred events. This is global across + * all subtransactions of the current transaction. In a subtransaction + * abort, we know that the events added by the subtransaction are at the + * end of the list, so it is relatively easy to discard them. + * + * query_depth is the current depth of nested AfterTriggerBeginQuery calls + * (-1 when the stack is empty). + * + * query_stack[query_depth] is a list of AFTER trigger events queued by the + * current query (and the query_stack entries below it are lists of trigger + * events queued by calling queries). None of these are valid until the + * matching AfterTriggerEndQuery call occurs. At that point we fire + * immediate-mode triggers, and append any deferred events to the main events + * list. + * + * maxquerydepth is just the allocated length of query_stack. + * + * state_stack is a stack of pointers to saved copies of the SET CONSTRAINTS + * state data; each subtransaction level that modifies that state first + * saves a copy, which we use to restore the state if we abort. + * + * events_stack is a stack of copies of the events head/tail pointers, + * which we use to restore those values during subtransaction abort. + * + * depth_stack is a stack of copies of subtransaction-start-time query_depth, + * which we similarly use to clean up at subtransaction abort. + * + * firing_stack is a stack of copies of subtransaction-start-time + * firing_counter. We use this to recognize which deferred triggers were + * fired (or marked for firing) within an aborted subtransaction. + * + * We use GetCurrentTransactionNestLevel() to determine the correct array + * index in these stacks. maxtransdepth is the number of allocated entries in + * each stack. (By not keeping our own stack pointer, we can avoid trouble + * in cases where errors during subxact abort cause multiple invocations + * of AfterTriggerEndSubXact() at the same nesting depth.) + */ +typedef struct AfterTriggersData +{ + CommandId firing_counter; /* next firing ID to assign */ + SetConstraintState state; /* the active S C state */ + AfterTriggerEventList events; /* deferred-event list */ + int query_depth; /* current query list index */ + AfterTriggerEventList *query_stack; /* events pending from each query */ + int maxquerydepth; /* allocated len of above array */ + + /* these fields are just for resetting at subtrans abort: */ + + SetConstraintState *state_stack; /* stacked S C states */ + AfterTriggerEventList *events_stack; /* stacked list pointers */ + int *depth_stack; /* stacked query_depths */ + CommandId *firing_stack; /* stacked firing_counters */ + int maxtransdepth; /* allocated len of above arrays */ +} AfterTriggersData; + +typedef AfterTriggersData *AfterTriggers; + +static AfterTriggers afterTriggers; + + +static void AfterTriggerExecute(AfterTriggerEvent event, + Relation rel, TriggerDesc *trigdesc, + FmgrInfo *finfo, + MemoryContext per_tuple_context); +static SetConstraintState SetConstraintStateCreate(int numalloc); +static SetConstraintState SetConstraintStateCopy(SetConstraintState state); +static SetConstraintState SetConstraintStateAddItem(SetConstraintState state, Oid tgoid, bool tgisdeferred); /* ---------- - * deferredTriggerCheckState() + * afterTriggerCheckState() * * Returns true if the trigger identified by tgoid is actually * in state DEFERRED. * ---------- */ static bool -deferredTriggerCheckState(Oid tgoid, int32 itemstate) +afterTriggerCheckState(Oid tgoid, TriggerEvent eventstate) { - DeferredTriggerState state = deferredTriggers->state; + SetConstraintState state = afterTriggers->state; int i; /* * For not-deferrable triggers (i.e. normal AFTER ROW triggers and * constraints declared NOT DEFERRABLE), the state is always false. */ - if ((itemstate & TRIGGER_DEFERRED_DEFERRABLE) == 0) + if ((eventstate & AFTER_TRIGGER_DEFERRABLE) == 0) return false; /* @@ -1787,8 +1821,8 @@ deferredTriggerCheckState(Oid tgoid, int32 itemstate) */ for (i = 0; i < state->numstates; i++) { - if (state->trigstates[i].dts_tgoid == tgoid) - return state->trigstates[i].dts_tgisdeferred; + if (state->trigstates[i].sct_tgoid == tgoid) + return state->trigstates[i].sct_tgisdeferred; } /* @@ -1800,37 +1834,43 @@ deferredTriggerCheckState(Oid tgoid, int32 itemstate) /* * Otherwise return the default state for the trigger. */ - return ((itemstate & TRIGGER_DEFERRED_INITDEFERRED) != 0); + return ((eventstate & AFTER_TRIGGER_INITDEFERRED) != 0); } /* ---------- - * deferredTriggerAddEvent() + * afterTriggerAddEvent() * - * Add a new trigger event to the queue. + * Add a new trigger event to the current query's queue. * ---------- */ static void -deferredTriggerAddEvent(DeferredTriggerEvent event) +afterTriggerAddEvent(AfterTriggerEvent event) { - Assert(event->dte_next == NULL); + AfterTriggerEventList *events; + + Assert(event->ate_next == NULL); - if (deferredTriggers->tail_thisxact == NULL) + /* Must be inside a query */ + Assert(afterTriggers->query_depth >= 0); + + events = &afterTriggers->query_stack[afterTriggers->query_depth]; + if (events->tail == NULL) { /* first list entry */ - deferredTriggers->events = event; - deferredTriggers->tail_thisxact = event; + events->head = event; + events->tail = event; } else { - deferredTriggers->tail_thisxact->dte_next = event; - deferredTriggers->tail_thisxact = event; + events->tail->ate_next = event; + events->tail = event; } } /* ---------- - * DeferredTriggerExecute() + * AfterTriggerExecute() * * Fetch the required tuples back from the heap and fire one * single trigger function. @@ -1840,7 +1880,6 @@ deferredTriggerAddEvent(DeferredTriggerEvent event) * fmgr lookup cache space at the caller level. * * event: event currently being fired. - * itemno: item within event currently being fired. * rel: open relation for event. * trigdesc: working copy of rel's trigger info. * finfo: array of fmgr lookup cache entries (one per trigger in trigdesc). @@ -1848,11 +1887,11 @@ deferredTriggerAddEvent(DeferredTriggerEvent event) * ---------- */ static void -DeferredTriggerExecute(DeferredTriggerEvent event, int itemno, +AfterTriggerExecute(AfterTriggerEvent event, Relation rel, TriggerDesc *trigdesc, FmgrInfo *finfo, - MemoryContext per_tuple_context) + MemoryContext per_tuple_context) { - Oid tgoid = event->dte_item[itemno].dti_tgoid; + Oid tgoid = event->ate_tgoid; TriggerData LocTriggerData; HeapTupleData oldtuple; HeapTupleData newtuple; @@ -1864,26 +1903,26 @@ DeferredTriggerExecute(DeferredTriggerEvent event, int itemno, /* * Fetch the required OLD and NEW tuples. */ - if (ItemPointerIsValid(&(event->dte_oldctid))) + if (ItemPointerIsValid(&(event->ate_oldctid))) { - ItemPointerCopy(&(event->dte_oldctid), &(oldtuple.t_self)); + ItemPointerCopy(&(event->ate_oldctid), &(oldtuple.t_self)); if (!heap_fetch(rel, SnapshotAny, &oldtuple, &oldbuffer, false, NULL)) - elog(ERROR, "failed to fetch old tuple for deferred trigger"); + elog(ERROR, "failed to fetch old tuple for AFTER trigger"); } - if (ItemPointerIsValid(&(event->dte_newctid))) + if (ItemPointerIsValid(&(event->ate_newctid))) { - ItemPointerCopy(&(event->dte_newctid), &(newtuple.t_self)); + ItemPointerCopy(&(event->ate_newctid), &(newtuple.t_self)); if (!heap_fetch(rel, SnapshotAny, &newtuple, &newbuffer, false, NULL)) - elog(ERROR, "failed to fetch new tuple for deferred trigger"); + elog(ERROR, "failed to fetch new tuple for AFTER trigger"); } /* * Setup the trigger information */ LocTriggerData.type = T_TriggerData; - LocTriggerData.tg_event = (event->dte_event & TRIGGER_EVENT_OPMASK) | - (event->dte_event & TRIGGER_EVENT_ROW); + LocTriggerData.tg_event = + event->ate_event & (TRIGGER_EVENT_OPMASK | TRIGGER_EVENT_ROW); LocTriggerData.tg_relation = rel; LocTriggerData.tg_trigger = NULL; @@ -1898,7 +1937,7 @@ DeferredTriggerExecute(DeferredTriggerEvent event, int itemno, if (LocTriggerData.tg_trigger == NULL) elog(ERROR, "could not find trigger %u", tgoid); - switch (event->dte_event & TRIGGER_EVENT_OPMASK) + switch (event->ate_event & TRIGGER_EVENT_OPMASK) { case TRIGGER_EVENT_INSERT: LocTriggerData.tg_trigtuple = &newtuple; @@ -1916,6 +1955,8 @@ DeferredTriggerExecute(DeferredTriggerEvent event, int itemno, break; } + MemoryContextReset(per_tuple_context); + /* * Call the trigger and throw away any eventually returned updated * tuple. @@ -1929,290 +1970,409 @@ DeferredTriggerExecute(DeferredTriggerEvent event, int itemno, /* * Release buffers */ - if (ItemPointerIsValid(&(event->dte_oldctid))) + if (ItemPointerIsValid(&(event->ate_oldctid))) ReleaseBuffer(oldbuffer); - if (ItemPointerIsValid(&(event->dte_newctid))) + if (ItemPointerIsValid(&(event->ate_newctid))) ReleaseBuffer(newbuffer); } +/* + * afterTriggerMarkEvents() + * + * Scan the given event list for not yet invoked events. Mark the ones + * that can be invoked now with the current firing ID. + * + * If move_list isn't NULL, events that are not to be invoked now are + * removed from the given list and appended to move_list. + * + * When immediate_only is TRUE, do not invoke currently-deferred triggers. + * (This will be FALSE only at main transaction exit.) + * + * Returns TRUE if any invokable events were found. + */ +static bool +afterTriggerMarkEvents(AfterTriggerEventList *events, + AfterTriggerEventList *move_list, + bool immediate_only) +{ + bool found = false; + AfterTriggerEvent event, + prev_event; + + prev_event = NULL; + event = events->head; + + while (event != NULL) + { + bool defer_it = false; + AfterTriggerEvent next_event; + + if (!(event->ate_event & + (AFTER_TRIGGER_DONE | AFTER_TRIGGER_IN_PROGRESS))) + { + /* + * This trigger hasn't been called or scheduled yet. Check if we + * should call it now. + */ + if (immediate_only && + afterTriggerCheckState(event->ate_tgoid, event->ate_event)) + { + defer_it = true; + } + else + { + /* + * Mark it as to be fired in this firing cycle. + */ + event->ate_firing_id = afterTriggers->firing_counter; + event->ate_event |= AFTER_TRIGGER_IN_PROGRESS; + found = true; + } + } + + /* + * If it's deferred, move it to move_list, if requested. + */ + next_event = event->ate_next; + + if (defer_it && move_list != NULL) + { + /* Delink it from input list */ + if (prev_event) + prev_event->ate_next = next_event; + else + events->head = next_event; + /* and add it to move_list */ + event->ate_next = NULL; + if (move_list->tail == NULL) + { + /* first list entry */ + move_list->head = event; + move_list->tail = event; + } + else + { + move_list->tail->ate_next = event; + move_list->tail = event; + } + } + else + { + /* Keep it in input list */ + prev_event = event; + } + + event = next_event; + } + + /* Update list tail pointer in case we moved tail event */ + events->tail = prev_event; + + return found; +} + /* ---------- - * deferredTriggerInvokeEvents() + * afterTriggerInvokeEvents() * - * Scan the event queue for not yet invoked triggers. Check if they - * should be invoked now and do so. + * Scan the given event list for events that are marked as to be fired + * in the current firing cycle, and fire them. + * + * When delete_ok is TRUE, it's okay to delete fully-processed events. + * The events list pointers are updated. * ---------- */ static void -deferredTriggerInvokeEvents(bool immediate_only) +afterTriggerInvokeEvents(AfterTriggerEventList *events, + CommandId firing_id, + bool delete_ok) { - DeferredTriggerEvent event, + AfterTriggerEvent event, prev_event; MemoryContext per_tuple_context; Relation rel = NULL; TriggerDesc *trigdesc = NULL; FmgrInfo *finfo = NULL; - /* - * If immediate_only is true, we remove fully-processed events from - * the event queue to recycle space. If immediate_only is false, we - * are going to discard the whole event queue on return anyway, so no - * need to bother with "retail" pfree's. - * - * If immediate_only is true, we need only scan from where the end of the - * queue was at the previous deferredTriggerInvokeEvents call; any - * non-deferred events before that point are already fired. (But if - * the deferral state changes, we must reset the saved position to the - * beginning of the queue, so as to process all events once with the - * new states. See DeferredTriggerSetState.) - */ - /* Make a per-tuple memory context for trigger function calls */ per_tuple_context = AllocSetContextCreate(CurrentMemoryContext, - "DeferredTriggerTupleContext", + "AfterTriggerTupleContext", ALLOCSET_DEFAULT_MINSIZE, ALLOCSET_DEFAULT_INITSIZE, ALLOCSET_DEFAULT_MAXSIZE); - /* - * If immediate_only is true, then the only events that could need - * firing are those since events_imm. (But if events_imm is NULL, we - * must scan the entire list.) - */ - if (immediate_only && deferredTriggers->events_imm != NULL) - { - prev_event = deferredTriggers->events_imm; - event = prev_event->dte_next; - } - else - { - prev_event = NULL; - event = deferredTriggers->events; - } + prev_event = NULL; + event = events->head; while (event != NULL) { - bool still_deferred_ones = false; - DeferredTriggerEvent next_event; - int i; + AfterTriggerEvent next_event; /* - * Skip executing cancelled events, and events already done, - * unless they were done by a subtransaction that later aborted. + * Is it one for me to fire? */ - if (!(event->dte_event & TRIGGER_DEFERRED_CANCELED) && - !(event->dte_event & TRIGGER_DEFERRED_DONE && - TransactionIdIsValid(event->dte_done_xid) && - !TransactionIdDidAbort(event->dte_done_xid))) + if ((event->ate_event & AFTER_TRIGGER_IN_PROGRESS) && + event->ate_firing_id == firing_id) { - MemoryContextReset(per_tuple_context); - /* - * Check each trigger item in the event. + * So let's fire it... but first, open the correct + * relation if this is not the same relation as before. */ - for (i = 0; i < event->dte_n_items; i++) + if (rel == NULL || rel->rd_id != event->ate_relid) { - if (event->dte_item[i].dti_state & TRIGGER_DEFERRED_DONE && - TransactionIdIsValid(event->dte_item[i].dti_done_xid) && - !(TransactionIdDidAbort(event->dte_item[i].dti_done_xid))) - continue; + if (rel) + heap_close(rel, NoLock); + if (trigdesc) + FreeTriggerDesc(trigdesc); + if (finfo) + pfree(finfo); /* - * This trigger item hasn't been called yet. Check if we - * should call it now. + * We assume that an appropriate lock is still held by + * the executor, so grab no new lock here. */ - if (immediate_only && - deferredTriggerCheckState(event->dte_item[i].dti_tgoid, - event->dte_item[i].dti_state)) - { - still_deferred_ones = true; - continue; - } + rel = heap_open(event->ate_relid, NoLock); /* - * So let's fire it... but first, open the correct - * relation if this is not the same relation as before. + * Copy relation's trigger info so that we have a + * stable copy no matter what the called triggers do. */ - if (rel == NULL || rel->rd_id != event->dte_relid) - { - if (rel) - heap_close(rel, NoLock); - FreeTriggerDesc(trigdesc); - if (finfo) - pfree(finfo); - - /* - * We assume that an appropriate lock is still held by - * the executor, so grab no new lock here. - */ - rel = heap_open(event->dte_relid, NoLock); - - /* - * Copy relation's trigger info so that we have a - * stable copy no matter what the called triggers do. - */ - trigdesc = CopyTriggerDesc(rel->trigdesc); - - if (trigdesc == NULL) /* should not happen */ - elog(ERROR, "relation %u has no triggers", - event->dte_relid); - - /* - * Allocate space to cache fmgr lookup info for - * triggers. - */ - finfo = (FmgrInfo *) - palloc0(trigdesc->numtriggers * sizeof(FmgrInfo)); - } + trigdesc = CopyTriggerDesc(rel->trigdesc); + + if (trigdesc == NULL) /* should not happen */ + elog(ERROR, "relation %u has no triggers", + event->ate_relid); + + /* + * Allocate space to cache fmgr lookup info for + * triggers. + */ + finfo = (FmgrInfo *) + palloc0(trigdesc->numtriggers * sizeof(FmgrInfo)); + } - DeferredTriggerExecute(event, i, rel, trigdesc, finfo, - per_tuple_context); + /* + * Fire it. Note that the AFTER_TRIGGER_IN_PROGRESS flag is still + * set, so recursive examinations of the event list won't try + * to re-fire it. + */ + AfterTriggerExecute(event, rel, trigdesc, finfo, + per_tuple_context); - event->dte_item[i].dti_state |= TRIGGER_DEFERRED_DONE; - event->dte_item[i].dti_done_xid = GetCurrentTransactionId(); - } /* end loop over items within event */ + /* + * Mark the event as done. + */ + event->ate_event &= ~AFTER_TRIGGER_IN_PROGRESS; + event->ate_event |= AFTER_TRIGGER_DONE; } /* - * If it's now completely done, throw it away. + * If it's now done, throw it away, if allowed. * - * NB: it's possible the trigger calls above added more events to the + * NB: it's possible the trigger call above added more events to the * queue, or that calls we will do later will want to add more, so - * we have to be careful about maintaining list validity here. + * we have to be careful about maintaining list validity at all + * points here. */ - next_event = event->dte_next; + next_event = event->ate_next; - if (still_deferred_ones || !immediate_only) + if ((event->ate_event & AFTER_TRIGGER_DONE) && delete_ok) { - /* Not done, keep in list */ - prev_event = event; + /* Delink it from list and free it */ + if (prev_event) + prev_event->ate_next = next_event; + else + events->head = next_event; + pfree(event); } else { - /* - * We can drop an item if it's done, but only if we're not - * inside a subtransaction because it could abort later on. We - * will want to check the item again if it does. - */ - if (!IsSubTransaction()) - { - /* delink it from list and free it */ - if (prev_event) - prev_event->dte_next = next_event; - else - deferredTriggers->events = next_event; - pfree(event); - } - else - { - /* - * Mark the event-as-a-whole done, but keep it in the list. - */ - event->dte_event |= TRIGGER_DEFERRED_DONE; - event->dte_done_xid = GetCurrentTransactionId(); - prev_event = event; - } + /* Keep it in list */ + prev_event = event; } event = next_event; } /* Update list tail pointer in case we just deleted tail event */ - deferredTriggers->tail_thisxact = prev_event; - - /* Set the immediate event pointer for next time */ - deferredTriggers->events_imm = prev_event; + events->tail = prev_event; /* Release working resources */ if (rel) heap_close(rel, NoLock); - FreeTriggerDesc(trigdesc); + if (trigdesc) + FreeTriggerDesc(trigdesc); if (finfo) pfree(finfo); MemoryContextDelete(per_tuple_context); } + /* ---------- - * DeferredTriggerBeginXact() + * AfterTriggerBeginXact() * * Called at transaction start (either BEGIN or implicit for single * statement outside of transaction block). * ---------- */ void -DeferredTriggerBeginXact(void) +AfterTriggerBeginXact(void) { - Assert(deferredTriggers == NULL); + Assert(afterTriggers == NULL); + + /* + * Build empty after-trigger state structure + */ + afterTriggers = (AfterTriggers) + MemoryContextAlloc(TopTransactionContext, + sizeof(AfterTriggersData)); + + afterTriggers->firing_counter = FirstCommandId; + afterTriggers->state = SetConstraintStateCreate(8); + afterTriggers->events.head = NULL; + afterTriggers->events.tail = NULL; + afterTriggers->query_depth = -1; - deferredTriggers = (DeferredTriggers) + /* We initialize the query stack to a reasonable size */ + afterTriggers->query_stack = (AfterTriggerEventList *) MemoryContextAlloc(TopTransactionContext, - sizeof(DeferredTriggersData)); + 8 * sizeof(AfterTriggerEventList)); + afterTriggers->maxquerydepth = 8; + + /* Subtransaction stack is empty until/unless needed */ + afterTriggers->state_stack = NULL; + afterTriggers->events_stack = NULL; + afterTriggers->depth_stack = NULL; + afterTriggers->firing_stack = NULL; + afterTriggers->maxtransdepth = 0; +} + + +/* ---------- + * AfterTriggerBeginQuery() + * + * Called just before we start processing a single query within a + * transaction (or subtransaction). Set up to record AFTER trigger + * events queued by the query. Note that it is allowed to have + * nested queries within a (sub)transaction. + * ---------- + */ +void +AfterTriggerBeginQuery(void) +{ + /* Must be inside a transaction */ + Assert(afterTriggers != NULL); + + /* Increase the query stack depth */ + afterTriggers->query_depth++; /* - * If unspecified, constraints default to IMMEDIATE, per SQL + * Allocate more space in the query stack if needed. */ - deferredTriggers->state = DeferredTriggerStateCreate(8); - deferredTriggers->events = NULL; - deferredTriggers->events_imm = NULL; - deferredTriggers->tail_thisxact = NULL; - deferredTriggers->tail_stack = NULL; - deferredTriggers->imm_stack = NULL; - deferredTriggers->state_stack = NULL; - deferredTriggers->numalloc = 0; + if (afterTriggers->query_depth >= afterTriggers->maxquerydepth) + { + /* repalloc will keep the stack in the same context */ + int new_alloc = afterTriggers->maxquerydepth * 2; + + afterTriggers->query_stack = (AfterTriggerEventList *) + repalloc(afterTriggers->query_stack, + new_alloc * sizeof(AfterTriggerEventList)); + afterTriggers->maxquerydepth = new_alloc; + } + + /* Initialize this query's list to empty */ + afterTriggers->query_stack[afterTriggers->query_depth].head = NULL; + afterTriggers->query_stack[afterTriggers->query_depth].tail = NULL; } /* ---------- - * DeferredTriggerEndQuery() + * AfterTriggerEndQuery() * - * Called after one query sent down by the user has completely been - * processed. At this time we invoke all outstanding IMMEDIATE triggers. + * Called after one query has been completely processed. At this time + * we invoke all AFTER IMMEDIATE trigger events queued by the query, and + * transfer deferred trigger events to the global deferred-trigger list. * ---------- */ void -DeferredTriggerEndQuery(void) +AfterTriggerEndQuery(void) { + AfterTriggerEventList *events; + + /* Must be inside a transaction */ + Assert(afterTriggers != NULL); + + /* Must be inside a query, too */ + Assert(afterTriggers->query_depth >= 0); + /* - * Ignore call if we aren't in a transaction. + * Process all immediate-mode triggers queued by the query, and move + * the deferred ones to the main list of deferred events. + * + * Notice that we decide which ones will be fired, and put the deferred + * ones on the main list, before anything is actually fired. This + * ensures reasonably sane behavior if a trigger function does + * SET CONSTRAINTS ... IMMEDIATE: all events we have decided to defer + * will be available for it to fire. + * + * If we find no firable events, we don't have to increment firing_counter. */ - if (deferredTriggers == NULL) - return; + events = &afterTriggers->query_stack[afterTriggers->query_depth]; + if (afterTriggerMarkEvents(events, &afterTriggers->events, true)) + { + CommandId firing_id = afterTriggers->firing_counter++; + + /* OK to delete the immediate events after processing them */ + afterTriggerInvokeEvents(events, firing_id, true); + } - deferredTriggerInvokeEvents(true); + afterTriggers->query_depth--; } /* ---------- - * DeferredTriggerEndXact() + * AfterTriggerEndXact() * * Called just before the current transaction is committed. At this * time we invoke all DEFERRED triggers and tidy up. * ---------- */ void -DeferredTriggerEndXact(void) +AfterTriggerEndXact(void) { + AfterTriggerEventList *events; + + /* Must be inside a transaction */ + Assert(afterTriggers != NULL); + + /* ... but not inside a query */ + Assert(afterTriggers->query_depth == -1); + /* - * Ignore call if we aren't in a transaction. + * Run all the remaining triggers. Loop until they are all gone, + * just in case some trigger queues more for us to do. */ - if (deferredTriggers == NULL) - return; + events = &afterTriggers->events; + while (afterTriggerMarkEvents(events, NULL, false)) + { + CommandId firing_id = afterTriggers->firing_counter++; - deferredTriggerInvokeEvents(false); + afterTriggerInvokeEvents(events, firing_id, true); + } /* - * Forget everything we know about deferred triggers. + * Forget everything we know about AFTER triggers. * * Since all the info is in TopTransactionContext or children thereof, we * need do nothing special to reclaim memory. */ - deferredTriggers = NULL; + afterTriggers = NULL; } /* ---------- - * DeferredTriggerAbortXact() + * AfterTriggerAbortXact() * * The current transaction has entered the abort state. * All outstanding triggers are canceled so we simply throw @@ -2220,139 +2380,147 @@ DeferredTriggerEndXact(void) * ---------- */ void -DeferredTriggerAbortXact(void) +AfterTriggerAbortXact(void) { /* - * Ignore call if we aren't in a transaction. + * Ignore call if we aren't in a transaction. (Need this to survive + * repeat call in case of error during transaction abort.) */ - if (deferredTriggers == NULL) + if (afterTriggers == NULL) return; /* - * Forget everything we know about deferred triggers. + * Forget everything we know about AFTER triggers. * * Since all the info is in TopTransactionContext or children thereof, we * need do nothing special to reclaim memory. */ - deferredTriggers = NULL; + afterTriggers = NULL; } /* - * DeferredTriggerBeginSubXact() + * AfterTriggerBeginSubXact() * * Start a subtransaction. */ void -DeferredTriggerBeginSubXact(void) +AfterTriggerBeginSubXact(void) { int my_level = GetCurrentTransactionNestLevel(); /* - * Ignore call if the transaction is in aborted state. + * Ignore call if the transaction is in aborted state. (Probably + * shouldn't happen?) */ - if (deferredTriggers == NULL) + if (afterTriggers == NULL) return; /* * Allocate more space in the stacks if needed. */ - while (my_level >= deferredTriggers->numalloc) + while (my_level >= afterTriggers->maxtransdepth) { - if (deferredTriggers->numalloc == 0) + if (afterTriggers->maxtransdepth == 0) { MemoryContext old_cxt; old_cxt = MemoryContextSwitchTo(TopTransactionContext); #define DEFTRIG_INITALLOC 8 - deferredTriggers->tail_stack = (DeferredTriggerEvent *) - palloc(DEFTRIG_INITALLOC * sizeof(DeferredTriggerEvent)); - deferredTriggers->imm_stack = (DeferredTriggerEvent *) - palloc(DEFTRIG_INITALLOC * sizeof(DeferredTriggerEvent)); - deferredTriggers->state_stack = (DeferredTriggerState *) - palloc(DEFTRIG_INITALLOC * sizeof(DeferredTriggerState)); - deferredTriggers->numalloc = DEFTRIG_INITALLOC; + afterTriggers->state_stack = (SetConstraintState *) + palloc(DEFTRIG_INITALLOC * sizeof(SetConstraintState)); + afterTriggers->events_stack = (AfterTriggerEventList *) + palloc(DEFTRIG_INITALLOC * sizeof(AfterTriggerEventList)); + afterTriggers->depth_stack = (int *) + palloc(DEFTRIG_INITALLOC * sizeof(int)); + afterTriggers->firing_stack = (CommandId *) + palloc(DEFTRIG_INITALLOC * sizeof(CommandId)); + afterTriggers->maxtransdepth = DEFTRIG_INITALLOC; MemoryContextSwitchTo(old_cxt); } else { /* repalloc will keep the stacks in the same context */ - int new_alloc = deferredTriggers->numalloc * 2; - - deferredTriggers->tail_stack = (DeferredTriggerEvent *) - repalloc(deferredTriggers->tail_stack, - new_alloc * sizeof(DeferredTriggerEvent)); - deferredTriggers->imm_stack = (DeferredTriggerEvent *) - repalloc(deferredTriggers->imm_stack, - new_alloc * sizeof(DeferredTriggerEvent)); - deferredTriggers->state_stack = (DeferredTriggerState *) - repalloc(deferredTriggers->state_stack, - new_alloc * sizeof(DeferredTriggerState)); - deferredTriggers->numalloc = new_alloc; + int new_alloc = afterTriggers->maxtransdepth * 2; + + afterTriggers->state_stack = (SetConstraintState *) + repalloc(afterTriggers->state_stack, + new_alloc * sizeof(SetConstraintState)); + afterTriggers->events_stack = (AfterTriggerEventList *) + repalloc(afterTriggers->events_stack, + new_alloc * sizeof(AfterTriggerEventList)); + afterTriggers->depth_stack = (int *) + repalloc(afterTriggers->depth_stack, + new_alloc * sizeof(int)); + afterTriggers->firing_stack = (CommandId *) + repalloc(afterTriggers->firing_stack, + new_alloc * sizeof(CommandId)); + afterTriggers->maxtransdepth = new_alloc; } } /* - * Push the current information into the stack. + * Push the current information into the stack. The SET CONSTRAINTS + * state is not saved until/unless changed. */ - deferredTriggers->tail_stack[my_level] = deferredTriggers->tail_thisxact; - deferredTriggers->imm_stack[my_level] = deferredTriggers->events_imm; - /* State is not saved until/unless changed */ - deferredTriggers->state_stack[my_level] = NULL; + afterTriggers->state_stack[my_level] = NULL; + afterTriggers->events_stack[my_level] = afterTriggers->events; + afterTriggers->depth_stack[my_level] = afterTriggers->query_depth; + afterTriggers->firing_stack[my_level] = afterTriggers->firing_counter; } /* - * DeferredTriggerEndSubXact() + * AfterTriggerEndSubXact() * * The current subtransaction is ending. */ void -DeferredTriggerEndSubXact(bool isCommit) +AfterTriggerEndSubXact(bool isCommit) { int my_level = GetCurrentTransactionNestLevel(); - DeferredTriggerState state; + SetConstraintState state; + AfterTriggerEvent event; + CommandId subxact_firing_id; /* - * Ignore call if the transaction is in aborted state. + * Ignore call if the transaction is in aborted state. (Probably unneeded) */ - if (deferredTriggers == NULL) + if (afterTriggers == NULL) return; /* * Pop the prior state if needed. */ - Assert(my_level < deferredTriggers->numalloc); + Assert(my_level < afterTriggers->maxtransdepth); if (isCommit) { /* If we saved a prior state, we don't need it anymore */ - state = deferredTriggers->state_stack[my_level]; + state = afterTriggers->state_stack[my_level]; if (state != NULL) pfree(state); /* this avoids double pfree if error later: */ - deferredTriggers->state_stack[my_level] = NULL; + afterTriggers->state_stack[my_level] = NULL; + Assert(afterTriggers->query_depth == + afterTriggers->depth_stack[my_level]); } else { /* * Aborting --- restore the pointers from the stacks. */ - deferredTriggers->tail_thisxact = - deferredTriggers->tail_stack[my_level]; - deferredTriggers->events_imm = - deferredTriggers->imm_stack[my_level]; + afterTriggers->events = afterTriggers->events_stack[my_level]; + afterTriggers->query_depth = afterTriggers->depth_stack[my_level]; /* - * Cleanup the head and the tail of the list. + * Cleanup the tail of the list. */ - if (deferredTriggers->tail_thisxact == NULL) - deferredTriggers->events = NULL; - else - deferredTriggers->tail_thisxact->dte_next = NULL; + if (afterTriggers->events.tail != NULL) + afterTriggers->events.tail->ate_next = NULL; /* - * We don't need to free the items, since the + * We don't need to free the subtransaction's items, since the * CurTransactionContext will be reset shortly. */ @@ -2360,24 +2528,46 @@ DeferredTriggerEndSubXact(bool isCommit) * Restore the trigger state. If the saved state is NULL, then * this subxact didn't save it, so it doesn't need restoring. */ - state = deferredTriggers->state_stack[my_level]; + state = afterTriggers->state_stack[my_level]; if (state != NULL) { - pfree(deferredTriggers->state); - deferredTriggers->state = state; + pfree(afterTriggers->state); + afterTriggers->state = state; } /* this avoids double pfree if error later: */ - deferredTriggers->state_stack[my_level] = NULL; + afterTriggers->state_stack[my_level] = NULL; + + /* + * Scan for any remaining deferred events that were marked DONE + * or IN PROGRESS by this subxact or a child, and un-mark them. + * We can recognize such events because they have a firing ID + * greater than or equal to the firing_counter value we saved at + * subtransaction start. (This essentially assumes that the + * current subxact includes all subxacts started after it.) + */ + subxact_firing_id = afterTriggers->firing_stack[my_level]; + for (event = afterTriggers->events.head; + event != NULL; + event = event->ate_next) + { + if (event->ate_event & + (AFTER_TRIGGER_DONE | AFTER_TRIGGER_IN_PROGRESS)) + { + if (event->ate_firing_id >= subxact_firing_id) + event->ate_event &= + ~(AFTER_TRIGGER_DONE | AFTER_TRIGGER_IN_PROGRESS); + } + } } } /* - * Create an empty DeferredTriggerState with room for numalloc trigstates + * Create an empty SetConstraintState with room for numalloc trigstates */ -static DeferredTriggerState -DeferredTriggerStateCreate(int numalloc) +static SetConstraintState +SetConstraintStateCreate(int numalloc) { - DeferredTriggerState state; + SetConstraintState state; /* Behave sanely with numalloc == 0 */ if (numalloc <= 0) @@ -2386,10 +2576,10 @@ DeferredTriggerStateCreate(int numalloc) /* * We assume that zeroing will correctly initialize the state values. */ - state = (DeferredTriggerState) + state = (SetConstraintState) MemoryContextAllocZero(TopTransactionContext, - sizeof(DeferredTriggerStateData) + - (numalloc - 1) *sizeof(DeferredTriggerStatusData)); + sizeof(SetConstraintStateData) + + (numalloc - 1) *sizeof(SetConstraintTriggerData)); state->numalloc = numalloc; @@ -2397,67 +2587,67 @@ DeferredTriggerStateCreate(int numalloc) } /* - * Copy a DeferredTriggerState + * Copy a SetConstraintState */ -static DeferredTriggerState -DeferredTriggerStateCopy(DeferredTriggerState origstate) +static SetConstraintState +SetConstraintStateCopy(SetConstraintState origstate) { - DeferredTriggerState state; + SetConstraintState state; - state = DeferredTriggerStateCreate(origstate->numstates); + state = SetConstraintStateCreate(origstate->numstates); state->all_isset = origstate->all_isset; state->all_isdeferred = origstate->all_isdeferred; state->numstates = origstate->numstates; memcpy(state->trigstates, origstate->trigstates, - origstate->numstates * sizeof(DeferredTriggerStatusData)); + origstate->numstates * sizeof(SetConstraintTriggerData)); return state; } /* - * Add a per-trigger item to a DeferredTriggerState. Returns possibly-changed + * Add a per-trigger item to a SetConstraintState. Returns possibly-changed * pointer to the state object (it will change if we have to repalloc). */ -static DeferredTriggerState -DeferredTriggerStateAddItem(DeferredTriggerState state, - Oid tgoid, bool tgisdeferred) +static SetConstraintState +SetConstraintStateAddItem(SetConstraintState state, + Oid tgoid, bool tgisdeferred) { if (state->numstates >= state->numalloc) { int newalloc = state->numalloc * 2; newalloc = Max(newalloc, 8); /* in case original has size 0 */ - state = (DeferredTriggerState) + state = (SetConstraintState) repalloc(state, - sizeof(DeferredTriggerStateData) + - (newalloc - 1) *sizeof(DeferredTriggerStatusData)); + sizeof(SetConstraintStateData) + + (newalloc - 1) *sizeof(SetConstraintTriggerData)); state->numalloc = newalloc; Assert(state->numstates < state->numalloc); } - state->trigstates[state->numstates].dts_tgoid = tgoid; - state->trigstates[state->numstates].dts_tgisdeferred = tgisdeferred; + state->trigstates[state->numstates].sct_tgoid = tgoid; + state->trigstates[state->numstates].sct_tgisdeferred = tgisdeferred; state->numstates++; return state; } /* ---------- - * DeferredTriggerSetState() + * AfterTriggerSetState() * - * Called for the SET CONSTRAINTS ... utility command. + * Execute the SET CONSTRAINTS ... utility command. * ---------- */ void -DeferredTriggerSetState(ConstraintsSetStmt *stmt) +AfterTriggerSetState(ConstraintsSetStmt *stmt) { int my_level = GetCurrentTransactionNestLevel(); /* - * Ignore call if we aren't in a transaction. + * Ignore call if we aren't in a transaction. (Shouldn't happen?) */ - if (deferredTriggers == NULL) + if (afterTriggers == NULL) return; /* @@ -2466,10 +2656,10 @@ DeferredTriggerSetState(ConstraintsSetStmt *stmt) * aborts. */ if (my_level > 1 && - deferredTriggers->state_stack[my_level] == NULL) + afterTriggers->state_stack[my_level] == NULL) { - deferredTriggers->state_stack[my_level] = - DeferredTriggerStateCopy(deferredTriggers->state); + afterTriggers->state_stack[my_level] = + SetConstraintStateCopy(afterTriggers->state); } /* @@ -2480,13 +2670,13 @@ DeferredTriggerSetState(ConstraintsSetStmt *stmt) /* * Forget any previous SET CONSTRAINTS commands in this transaction. */ - deferredTriggers->state->numstates = 0; + afterTriggers->state->numstates = 0; /* * Set the per-transaction ALL state to known. */ - deferredTriggers->state->all_isset = true; - deferredTriggers->state->all_isdeferred = stmt->deferred; + afterTriggers->state->all_isset = true; + afterTriggers->state->all_isdeferred = stmt->deferred; } else { @@ -2569,29 +2759,28 @@ DeferredTriggerSetState(ConstraintsSetStmt *stmt) heap_close(tgrel, AccessShareLock); /* - * Inside of a transaction block set the trigger states of - * individual triggers on transaction level. + * Set the trigger states of individual triggers for this xact. */ foreach(l, oidlist) { Oid tgoid = lfirst_oid(l); - DeferredTriggerState state = deferredTriggers->state; + SetConstraintState state = afterTriggers->state; bool found = false; int i; for (i = 0; i < state->numstates; i++) { - if (state->trigstates[i].dts_tgoid == tgoid) + if (state->trigstates[i].sct_tgoid == tgoid) { - state->trigstates[i].dts_tgisdeferred = stmt->deferred; + state->trigstates[i].sct_tgisdeferred = stmt->deferred; found = true; break; } } if (!found) { - deferredTriggers->state = - DeferredTriggerStateAddItem(state, tgoid, stmt->deferred); + afterTriggers->state = + SetConstraintStateAddItem(state, tgoid, stmt->deferred); } } } @@ -2600,20 +2789,35 @@ DeferredTriggerSetState(ConstraintsSetStmt *stmt) * SQL99 requires that when a constraint is set to IMMEDIATE, any * deferred checks against that constraint must be made when the SET * CONSTRAINTS command is executed -- i.e. the effects of the SET - * CONSTRAINTS command applies retroactively. This happens "for free" - * since we have already made the necessary modifications to the - * constraints, and deferredTriggerEndQuery() is called by - * finish_xact_command(). But we must reset - * deferredTriggerInvokeEvents' tail pointer to make it rescan the - * entire list, in case some deferred events are now immediately - * invokable. + * CONSTRAINTS command apply retroactively. We've updated the + * constraints state, so scan the list of previously deferred events + * to fire any that have now become immediate. + * + * Obviously, if this was SET ... DEFERRED then it can't have converted + * any unfired events to immediate, so we need do nothing in that case. */ - deferredTriggers->events_imm = NULL; + if (!stmt->deferred) + { + AfterTriggerEventList *events = &afterTriggers->events; + + if (afterTriggerMarkEvents(events, NULL, true)) + { + CommandId firing_id = afterTriggers->firing_counter++; + + /* + * We can delete fired events if we are at top transaction + * level, but we'd better not if inside a subtransaction, since + * the subtransaction could later get rolled back. + */ + afterTriggerInvokeEvents(events, firing_id, + !IsSubTransaction()); + } + } } /* ---------- - * DeferredTriggerSaveEvent() + * AfterTriggerSaveEvent() * * Called by ExecA[RS]...Triggers() to add the event to the queue. * @@ -2622,23 +2826,20 @@ DeferredTriggerSetState(ConstraintsSetStmt *stmt) * ---------- */ static void -DeferredTriggerSaveEvent(ResultRelInfo *relinfo, int event, bool row_trigger, - HeapTuple oldtup, HeapTuple newtup) +AfterTriggerSaveEvent(ResultRelInfo *relinfo, int event, bool row_trigger, + HeapTuple oldtup, HeapTuple newtup) { Relation rel = relinfo->ri_RelationDesc; TriggerDesc *trigdesc = relinfo->ri_TrigDesc; - MemoryContext oldcxt; - DeferredTriggerEvent new_event; - int new_size; + AfterTriggerEvent new_event; int i; int ntriggers; - int n_enabled_triggers = 0; int *tgindx; ItemPointerData oldctid; ItemPointerData newctid; - if (deferredTriggers == NULL) - elog(ERROR, "DeferredTriggerSaveEvent() called outside of transaction"); + if (afterTriggers == NULL) + elog(ERROR, "AfterTriggerSaveEvent() called outside of transaction"); /* * Get the CTID's of OLD and NEW @@ -2652,6 +2853,9 @@ DeferredTriggerSaveEvent(ResultRelInfo *relinfo, int event, bool row_trigger, else ItemPointerSetInvalid(&(newctid)); + /* + * Scan the appropriate set of triggers + */ if (row_trigger) { ntriggers = trigdesc->n_after_row[event]; @@ -2663,137 +2867,80 @@ DeferredTriggerSaveEvent(ResultRelInfo *relinfo, int event, bool row_trigger, tgindx = trigdesc->tg_after_statement[event]; } - /* - * Count the number of triggers that are actually enabled. Since we - * only add enabled triggers to the queue, we only need allocate - * enough space to hold them (and not any disabled triggers that may - * be associated with the relation). - */ for (i = 0; i < ntriggers; i++) { Trigger *trigger = &trigdesc->triggers[tgindx[i]]; - if (trigger->tgenabled) - n_enabled_triggers++; - } - - /* - * If all the triggers on this relation are disabled, we're done. - */ - if (n_enabled_triggers == 0) - return; - - /* - * Create a new event. We use the CurTransactionContext so the event - * will automatically go away if the subtransaction aborts. - */ - oldcxt = MemoryContextSwitchTo(CurTransactionContext); - - new_size = offsetof(DeferredTriggerEventData, dte_item[0]) + - n_enabled_triggers * sizeof(DeferredTriggerEventItem); - - new_event = (DeferredTriggerEvent) palloc(new_size); - new_event->dte_next = NULL; - new_event->dte_event = event & TRIGGER_EVENT_OPMASK; - new_event->dte_done_xid = InvalidTransactionId; - if (row_trigger) - new_event->dte_event |= TRIGGER_EVENT_ROW; - new_event->dte_relid = rel->rd_id; - ItemPointerCopy(&oldctid, &(new_event->dte_oldctid)); - ItemPointerCopy(&newctid, &(new_event->dte_newctid)); - new_event->dte_n_items = ntriggers; - for (i = 0; i < ntriggers; i++) - { - DeferredTriggerEventItem *ev_item; - Trigger *trigger = &trigdesc->triggers[tgindx[i]]; - + /* Ignore disabled triggers */ if (!trigger->tgenabled) continue; - ev_item = &(new_event->dte_item[i]); - ev_item->dti_tgoid = trigger->tgoid; - ev_item->dti_done_xid = InvalidTransactionId; - ev_item->dti_state = - ((trigger->tgdeferrable) ? - TRIGGER_DEFERRED_DEFERRABLE : 0) | - ((trigger->tginitdeferred) ? - TRIGGER_DEFERRED_INITDEFERRED : 0); - - if (row_trigger && (trigdesc->n_before_row[event] > 0)) - ev_item->dti_state |= TRIGGER_DEFERRED_HAS_BEFORE; - else if (!row_trigger && (trigdesc->n_before_statement[event] > 0)) - ev_item->dti_state |= TRIGGER_DEFERRED_HAS_BEFORE; - } - - MemoryContextSwitchTo(oldcxt); + /* + * If it is an RI UPDATE trigger, and the referenced keys have + * not changed, short-circuit queuing of the event; there's no + * need to fire the trigger. + */ + if ((event & TRIGGER_EVENT_OPMASK) == TRIGGER_EVENT_UPDATE) + { + bool is_ri_trigger; - switch (event & TRIGGER_EVENT_OPMASK) - { - case TRIGGER_EVENT_INSERT: - /* nothing to do */ - break; + switch (trigger->tgfoid) + { + case F_RI_FKEY_NOACTION_UPD: + case F_RI_FKEY_CASCADE_UPD: + case F_RI_FKEY_RESTRICT_UPD: + case F_RI_FKEY_SETNULL_UPD: + case F_RI_FKEY_SETDEFAULT_UPD: + is_ri_trigger = true; + break; - case TRIGGER_EVENT_UPDATE: + default: + is_ri_trigger = false; + break; + } - /* - * Check if one of the referenced keys is changed. - */ - for (i = 0; i < ntriggers; i++) + if (is_ri_trigger) { - Trigger *trigger = &trigdesc->triggers[tgindx[i]]; - bool is_ri_trigger; - bool key_unchanged; TriggerData LocTriggerData; - /* - * We are interested in RI_FKEY triggers only. - */ - switch (trigger->tgfoid) - { - case F_RI_FKEY_NOACTION_UPD: - case F_RI_FKEY_CASCADE_UPD: - case F_RI_FKEY_RESTRICT_UPD: - case F_RI_FKEY_SETNULL_UPD: - case F_RI_FKEY_SETDEFAULT_UPD: - is_ri_trigger = true; - break; - - default: - is_ri_trigger = false; - break; - } - if (!is_ri_trigger) - continue; - LocTriggerData.type = T_TriggerData; - LocTriggerData.tg_event = TRIGGER_EVENT_UPDATE; + LocTriggerData.tg_event = + TRIGGER_EVENT_UPDATE | TRIGGER_EVENT_ROW; LocTriggerData.tg_relation = rel; LocTriggerData.tg_trigtuple = oldtup; LocTriggerData.tg_newtuple = newtup; LocTriggerData.tg_trigger = trigger; - key_unchanged = RI_FKey_keyequal_upd(&LocTriggerData); - - if (key_unchanged) + if (RI_FKey_keyequal_upd(&LocTriggerData)) { - /* - * The key hasn't changed, so no need later to invoke - * the trigger at all. - */ - new_event->dte_item[i].dti_state |= TRIGGER_DEFERRED_DONE; - new_event->dte_item[i].dti_done_xid = GetCurrentTransactionId(); + /* key unchanged, so skip queuing this event */ + continue; } } + } - break; + /* + * Create a new event. We use the CurTransactionContext so the event + * will automatically go away if the subtransaction aborts. + */ + new_event = (AfterTriggerEvent) + MemoryContextAlloc(CurTransactionContext, + sizeof(AfterTriggerEventData)); + new_event->ate_next = NULL; + new_event->ate_event = + (event & TRIGGER_EVENT_OPMASK) | + (row_trigger ? TRIGGER_EVENT_ROW : 0) | + (trigger->tgdeferrable ? AFTER_TRIGGER_DEFERRABLE : 0) | + (trigger->tginitdeferred ? AFTER_TRIGGER_INITDEFERRED : 0); + new_event->ate_firing_id = 0; + new_event->ate_tgoid = trigger->tgoid; + new_event->ate_relid = rel->rd_id; + ItemPointerCopy(&oldctid, &(new_event->ate_oldctid)); + ItemPointerCopy(&newctid, &(new_event->ate_newctid)); - case TRIGGER_EVENT_DELETE: - /* nothing to do */ - break; + /* + * Add the new event to the queue. + */ + afterTriggerAddEvent(new_event); } - - /* - * Add the new event to the queue. - */ - deferredTriggerAddEvent(new_event); } diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c index 2a9e5d88a9e..3611c85a5fc 100644 --- a/src/backend/executor/functions.c +++ b/src/backend/executor/functions.c @@ -8,7 +8,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/executor/functions.c,v 1.87 2004/09/06 18:10:38 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/executor/functions.c,v 1.88 2004/09/10 18:39:57 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -17,6 +17,7 @@ #include "access/heapam.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" +#include "commands/trigger.h" #include "executor/execdefs.h" #include "executor/executor.h" #include "executor/functions.h" @@ -273,7 +274,10 @@ postquel_start(execution_state *es, SQLFunctionCachePtr fcache) /* Utility commands don't need Executor. */ if (es->qd->operation != CMD_UTILITY) + { + AfterTriggerBeginQuery(); ExecutorStart(es->qd, false, false); + } es->status = F_EXEC_RUN; } @@ -316,7 +320,10 @@ postquel_end(execution_state *es) /* Utility commands don't need Executor. */ if (es->qd->operation != CMD_UTILITY) + { ExecutorEnd(es->qd); + AfterTriggerEndQuery(); + } FreeQueryDesc(es->qd); es->qd = NULL; diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c index 4ffb27b0139..636eed31eee 100644 --- a/src/backend/executor/spi.c +++ b/src/backend/executor/spi.c @@ -8,7 +8,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/executor/spi.c,v 1.125 2004/08/29 05:06:42 momjian Exp $ + * $PostgreSQL: pgsql/src/backend/executor/spi.c,v 1.126 2004/09/10 18:39:57 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -16,6 +16,7 @@ #include "access/printtup.h" #include "catalog/heap.h" +#include "commands/trigger.h" #include "executor/spi_priv.h" #include "tcop/tcopprot.h" #include "utils/lsyscache.h" @@ -1434,6 +1435,8 @@ _SPI_pquery(QueryDesc *queryDesc, bool runit, ResetUsage(); #endif + AfterTriggerBeginQuery(); + ExecutorStart(queryDesc, useCurrentSnapshot, false); ExecutorRun(queryDesc, ForwardScanDirection, (long) tcount); @@ -1447,6 +1450,11 @@ _SPI_pquery(QueryDesc *queryDesc, bool runit, elog(ERROR, "consistency check on SPI tuple count failed"); } + ExecutorEnd(queryDesc); + + /* Take care of any queued AFTER triggers */ + AfterTriggerEndQuery(); + if (queryDesc->dest->mydest == SPI) { SPI_processed = _SPI_current->processed; @@ -1459,8 +1467,6 @@ _SPI_pquery(QueryDesc *queryDesc, bool runit, res = SPI_OK_UTILITY; } - ExecutorEnd(queryDesc); - FreeQueryDesc(queryDesc); #ifdef SPI_EXECUTOR_STATS diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index a3a96efae9c..50364bd79d3 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -8,7 +8,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/tcop/postgres.c,v 1.430 2004/08/29 05:06:49 momjian Exp $ + * $PostgreSQL: pgsql/src/backend/tcop/postgres.c,v 1.431 2004/09/10 18:39:59 tgl Exp $ * * NOTES * this is the "main" module of the postgres backend and @@ -1823,9 +1823,6 @@ finish_xact_command(void) { if (xact_started) { - /* Invoke IMMEDIATE constraint triggers */ - DeferredTriggerEndQuery(); - /* Cancel any active statement timeout before committing */ disable_sig_alarm(true); diff --git a/src/backend/tcop/pquery.c b/src/backend/tcop/pquery.c index 5d9fe611f01..fca98fc0fa5 100644 --- a/src/backend/tcop/pquery.c +++ b/src/backend/tcop/pquery.c @@ -8,13 +8,14 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/tcop/pquery.c,v 1.85 2004/08/29 05:06:49 momjian Exp $ + * $PostgreSQL: pgsql/src/backend/tcop/pquery.c,v 1.86 2004/09/10 18:40:00 tgl Exp $ * *------------------------------------------------------------------------- */ #include "postgres.h" +#include "commands/trigger.h" #include "executor/executor.h" #include "miscadmin.h" #include "tcop/tcopprot.h" @@ -136,6 +137,11 @@ ProcessQuery(Query *parsetree, */ queryDesc = CreateQueryDesc(parsetree, plan, dest, params, false); + /* + * Set up to collect AFTER triggers + */ + AfterTriggerBeginQuery(); + /* * Call ExecStart to prepare the plan for execution */ @@ -185,6 +191,9 @@ ProcessQuery(Query *parsetree, */ ExecutorEnd(queryDesc); + /* And take care of any queued AFTER triggers */ + AfterTriggerEndQuery(); + FreeQueryDesc(queryDesc); } @@ -290,6 +299,13 @@ PortalStart(Portal portal, ParamListInfo params) params, false); + /* + * We do *not* call AfterTriggerBeginQuery() here. We + * assume that a SELECT cannot queue any triggers. It + * would be messy to support triggers since the execution + * of the portal may be interleaved with other queries. + */ + /* * Call ExecStart to prepare the plan for execution */ @@ -1144,8 +1160,8 @@ DoPortalRunFetch(Portal portal, return PortalRunSelect(portal, false, 1L, dest); } else -/* count == 0 */ { + /* count == 0 */ /* Rewind to start, return zero rows */ DoPortalRewind(portal); return PortalRunSelect(portal, true, 0L, dest); @@ -1173,8 +1189,8 @@ DoPortalRunFetch(Portal portal, return PortalRunSelect(portal, false, 1L, dest); } else -/* count == 0 */ { + /* count == 0 */ /* Same as FETCH FORWARD 0, so fall out of switch */ fdirection = FETCH_FORWARD; } diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 517266b649c..6aec17bf9a5 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -10,7 +10,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/tcop/utility.c,v 1.228 2004/08/29 05:06:49 momjian Exp $ + * $PostgreSQL: pgsql/src/backend/tcop/utility.c,v 1.229 2004/09/10 18:40:00 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -912,7 +912,7 @@ ProcessUtility(Node *parsetree, break; case T_ConstraintsSetStmt: - DeferredTriggerSetState((ConstraintsSetStmt *) parsetree); + AfterTriggerSetState((ConstraintsSetStmt *) parsetree); break; case T_CreateGroupStmt: diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c index 8aaa38ddb69..9c32d57c111 100644 --- a/src/backend/utils/adt/ri_triggers.c +++ b/src/backend/utils/adt/ri_triggers.c @@ -17,7 +17,7 @@ * * Portions Copyright (c) 1996-2004, PostgreSQL Global Development Group * - * $PostgreSQL: pgsql/src/backend/utils/adt/ri_triggers.c,v 1.71 2004/08/29 05:06:49 momjian Exp $ + * $PostgreSQL: pgsql/src/backend/utils/adt/ri_triggers.c,v 1.72 2004/09/10 18:40:04 tgl Exp $ * * ---------- */ @@ -2454,7 +2454,7 @@ RI_FKey_setdefault_upd(PG_FUNCTION_ARGS) * * Check if we have a key change on update. * - * This is not a real trigger procedure. It is used by the deferred + * This is not a real trigger procedure. It is used by the AFTER * trigger queue manager to detect "triggered data change violation". * ---------- */ diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h index f23889668c1..7d5569018ae 100644 --- a/src/include/commands/trigger.h +++ b/src/include/commands/trigger.h @@ -6,7 +6,7 @@ * Portions Copyright (c) 1996-2004, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * $PostgreSQL: pgsql/src/include/commands/trigger.h,v 1.48 2004/08/29 05:06:56 momjian Exp $ + * $PostgreSQL: pgsql/src/include/commands/trigger.h,v 1.49 2004/09/10 18:40:06 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -45,11 +45,12 @@ typedef struct TriggerData #define TRIGGER_EVENT_ROW 0x00000004 #define TRIGGER_EVENT_BEFORE 0x00000008 -#define TRIGGER_DEFERRED_DONE 0x00000010 -#define TRIGGER_DEFERRED_CANCELED 0x00000020 -#define TRIGGER_DEFERRED_DEFERRABLE 0x00000040 -#define TRIGGER_DEFERRED_INITDEFERRED 0x00000080 -#define TRIGGER_DEFERRED_HAS_BEFORE 0x00000100 +/* More TriggerEvent flags, used only within trigger.c */ + +#define AFTER_TRIGGER_DONE 0x00000010 +#define AFTER_TRIGGER_IN_PROGRESS 0x00000020 +#define AFTER_TRIGGER_DEFERRABLE 0x00000040 +#define AFTER_TRIGGER_INITDEFERRED 0x00000080 #define TRIGGER_FIRED_BY_INSERT(event) \ (((TriggerEvent) (event) & TRIGGER_EVENT_OPMASK) == \ @@ -151,14 +152,15 @@ extern void ExecARUpdateTriggers(EState *estate, ItemPointer tupleid, HeapTuple newtuple); -extern void DeferredTriggerBeginXact(void); -extern void DeferredTriggerEndQuery(void); -extern void DeferredTriggerEndXact(void); -extern void DeferredTriggerAbortXact(void); -extern void DeferredTriggerBeginSubXact(void); -extern void DeferredTriggerEndSubXact(bool isCommit); +extern void AfterTriggerBeginXact(void); +extern void AfterTriggerBeginQuery(void); +extern void AfterTriggerEndQuery(void); +extern void AfterTriggerEndXact(void); +extern void AfterTriggerAbortXact(void); +extern void AfterTriggerBeginSubXact(void); +extern void AfterTriggerEndSubXact(bool isCommit); -extern void DeferredTriggerSetState(ConstraintsSetStmt *stmt); +extern void AfterTriggerSetState(ConstraintsSetStmt *stmt); /* diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out index b291000add1..e954a232f1b 100644 --- a/src/test/regress/expected/foreign_key.out +++ b/src/test/regress/expected/foreign_key.out @@ -646,6 +646,7 @@ SELECT * from FKTABLE; UPDATE PKTABLE set ptest2=5 where ptest2=2; ERROR: insert or update on table "fktable" violates foreign key constraint "constrname3" DETAIL: Key (ftest1,ftest2,ftest3)=(1,-1,3) is not present in table "pktable". +CONTEXT: SQL query "UPDATE ONLY "public"."fktable" SET "ftest2" = DEFAULT WHERE "ftest1" = $1 AND "ftest2" = $2 AND "ftest3" = $3" -- Try to update something that will set default UPDATE PKTABLE set ptest1=0, ptest2=5, ptest3=10 where ptest2=2; UPDATE PKTABLE set ptest2=10 where ptest2=4; diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out index 73962dbb37a..50d72830fb4 100644 --- a/src/test/regress/expected/plpgsql.out +++ b/src/test/regress/expected/plpgsql.out @@ -1935,3 +1935,74 @@ select * from foo; 20 (2 rows) +-- +-- test foreign key error trapping +-- +create temp table master(f1 int primary key); +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "master_pkey" for table "master" +create temp table slave(f1 int references master deferrable); +insert into master values(1); +insert into slave values(1); +insert into slave values(2); -- fails +ERROR: insert or update on table "slave" violates foreign key constraint "slave_f1_fkey" +DETAIL: Key (f1)=(2) is not present in table "master". +create function trap_foreign_key(int) returns int as $$ +begin + begin -- start a subtransaction + insert into slave values($1); + exception + when foreign_key_violation then + raise notice 'caught foreign_key_violation'; + return 0; + end; + return 1; +end$$ language plpgsql; +create function trap_foreign_key_2() returns int as $$ +begin + begin -- start a subtransaction + set constraints all immediate; + exception + when foreign_key_violation then + raise notice 'caught foreign_key_violation'; + return 0; + end; + return 1; +end$$ language plpgsql; +select trap_foreign_key(1); + trap_foreign_key +------------------ + 1 +(1 row) + +select trap_foreign_key(2); -- detects FK violation +NOTICE: caught foreign_key_violation + trap_foreign_key +------------------ + 0 +(1 row) + +begin; + set constraints all deferred; + select trap_foreign_key(2); -- should not detect FK violation + trap_foreign_key +------------------ + 1 +(1 row) + + savepoint x; + set constraints all immediate; -- fails +ERROR: insert or update on table "slave" violates foreign key constraint "slave_f1_fkey" +DETAIL: Key (f1)=(2) is not present in table "master". + rollback to x; + select trap_foreign_key_2(); -- detects FK violation +NOTICE: caught foreign_key_violation + trap_foreign_key_2 +-------------------- + 0 +(1 row) + +commit; -- still fails +ERROR: insert or update on table "slave" violates foreign key constraint "slave_f1_fkey" +DETAIL: Key (f1)=(2) is not present in table "master". +drop function trap_foreign_key(int); +drop function trap_foreign_key_2(); diff --git a/src/test/regress/sql/plpgsql.sql b/src/test/regress/sql/plpgsql.sql index 948a02ac0e2..a8951cd6efa 100644 --- a/src/test/regress/sql/plpgsql.sql +++ b/src/test/regress/sql/plpgsql.sql @@ -1699,3 +1699,54 @@ select blockme(); reset statement_timeout; select * from foo; + +-- +-- test foreign key error trapping +-- + +create temp table master(f1 int primary key); + +create temp table slave(f1 int references master deferrable); + +insert into master values(1); +insert into slave values(1); +insert into slave values(2); -- fails + +create function trap_foreign_key(int) returns int as $$ +begin + begin -- start a subtransaction + insert into slave values($1); + exception + when foreign_key_violation then + raise notice 'caught foreign_key_violation'; + return 0; + end; + return 1; +end$$ language plpgsql; + +create function trap_foreign_key_2() returns int as $$ +begin + begin -- start a subtransaction + set constraints all immediate; + exception + when foreign_key_violation then + raise notice 'caught foreign_key_violation'; + return 0; + end; + return 1; +end$$ language plpgsql; + +select trap_foreign_key(1); +select trap_foreign_key(2); -- detects FK violation + +begin; + set constraints all deferred; + select trap_foreign_key(2); -- should not detect FK violation + savepoint x; + set constraints all immediate; -- fails + rollback to x; + select trap_foreign_key_2(); -- detects FK violation +commit; -- still fails + +drop function trap_foreign_key(int); +drop function trap_foreign_key_2(); -- GitLab