Full implementation of truncate triggers on CVS HEAD, for Postgres 8.4 Includes various docs, regression tests for PL/pgSQL trigger support, psql and pg_dump support.
Some further discussion on -hackers required around statement skipping. Prototype code for that aspect included in this patch, #ifdef'd out. Most interesting discussion point is the handling of multiple truncate triggers when we are performing a multi-table TRUNCATE. Please read docs in patch for details, plus comments. This includes changes to PL/tcl, PL/perl and PL/python but support in those languages have not yet been tested, not even a little bit. This will be performed after review. Provisional Slony support patch will also be posted to appropriate list. diffstat doc/src/sgml/ref/create_trigger.sgml | 32 +++! doc/src/sgml/ref/truncate.sgml | 4 doc/src/sgml/trigger.sgml | 32 ++!! src/backend/commands/tablecmds.c | 126 ++++++++++++++++++ src/backend/commands/trigger.c | 111 ++++++++++++++++ src/backend/parser/gram.y | 1 src/backend/utils/adt/ruleutils.c | 7 + src/bin/pg_dump/pg_dump.c | 7 + src/include/catalog/pg_trigger.h | 3 src/include/commands/trigger.h | 31 +!!! src/include/utils/rel.h | 2 src/pl/plperl/plperl.c | 4 src/pl/plpgsql/src/pl_exec.c | 6 src/pl/plpython/plpython.c | 2 src/pl/tcl/pltcl.c | 2 src/test/regress/expected/truncate.out | 226 ++++++++++++++++++++... src/test/regress/sql/truncate.sql | 127 ++++++++++++++++++ 17 files changed, 672 insertions(+), 51 modifications(!) -- Simon Riggs 2ndQuadrant http://www.2ndQuadrant.com
Index: doc/src/sgml/trigger.sgml =================================================================== RCS file: /home/sriggs/pg/REPOSITORY/pgsql/doc/src/sgml/trigger.sgml,v retrieving revision 1.51 diff -c -r1.51 trigger.sgml *** doc/src/sgml/trigger.sgml 3 Dec 2007 23:49:51 -0000 1.51 --- doc/src/sgml/trigger.sgml 30 Jan 2008 17:34:18 -0000 *************** *** 35,44 **** execute a particular function whenever a certain type of operation is performed. Triggers can be defined to execute either before or after any <command>INSERT</command>, <command>UPDATE</command>, or ! <command>DELETE</command> operation, either once per modified row, ! or once per <acronym>SQL</acronym> statement. ! If a trigger event occurs, the trigger's function is called ! at the appropriate time to handle the event. </para> <para> --- 35,45 ---- execute a particular function whenever a certain type of operation is performed. Triggers can be defined to execute either before or after any <command>INSERT</command>, <command>UPDATE</command>, or ! <command>DELETE</command> operation, either once per modified row, ! or once per <acronym>SQL</acronym> statement. Triggers can also fire ! for <command>TRUNCATE</command> statements. If a trigger event occurs, ! the trigger's function is called at the appropriate time to handle the ! event. </para> <para> *************** *** 69,75 **** in the execution of any applicable per-statement triggers. These two types of triggers are sometimes called <firstterm>row-level</> triggers and <firstterm>statement-level</> triggers, ! respectively. </para> <para> --- 70,77 ---- in the execution of any applicable per-statement triggers. These two types of triggers are sometimes called <firstterm>row-level</> triggers and <firstterm>statement-level</> triggers, ! respectively. Triggers on <command>TRUNCATE</command> may only be ! defined at statement-level. </para> <para> *************** *** 194,199 **** --- 196,212 ---- individual row(s) modified by the statement. </para> + <para> + <command>TRUNCATE</> allows multiple tables to be truncated in a single + command. If triggers are defined on more than one of the tables then there + are special rules for trigger execution. All <literal>BEFORE</literal> + triggers fire before any truncate action occurs on any of the tables + listed. <literal>AFTER</literal> triggers begin firing after all of the + listed tables have been truncated. The triggers will fire in the order + the tables are listed for that command, allowing some user control over + firing sequence of the triggers by altering the <command>TRUNCATE</> + command. + </para> </sect1> <sect1 id="trigger-datachanges"> *************** *** 398,403 **** --- 411,425 ---- </para> </listitem> </varlistentry> + + <varlistentry> + <term><literal>TRIGGER_FIRED_BY_TRUNCATE(tg_event)</literal></term> + <listitem> + <para> + Returns true if the trigger was fired by a <command>TRUNCATE</command> command. + </para> + </listitem> + </varlistentry> </variablelist> </para> </listitem> Index: doc/src/sgml/ref/create_trigger.sgml =================================================================== RCS file: /home/sriggs/pg/REPOSITORY/pgsql/doc/src/sgml/ref/create_trigger.sgml,v retrieving revision 1.47 diff -c -r1.47 create_trigger.sgml *** doc/src/sgml/ref/create_trigger.sgml 1 Feb 2007 19:10:24 -0000 1.47 --- doc/src/sgml/ref/create_trigger.sgml 30 Jan 2008 11:50:35 -0000 *************** *** 66,71 **** --- 66,77 ---- </para> <para> + In addition, triggers may be defined to fire for a + <command>TRUNCATE</command>, though only + <literal>FOR EACH STATEMENT</literal>. + </para> + + <para> If multiple triggers of the same kind are defined for the same event, they will be fired in alphabetical order by name. </para> *************** *** 110,119 **** <term><replaceable class="parameter">event</replaceable></term> <listitem> <para> ! One of <command>INSERT</command>, <command>UPDATE</command>, or ! <command>DELETE</command>; this specifies the event that will ! fire the trigger. Multiple events can be specified using ! <literal>OR</literal>. </para> </listitem> </varlistentry> --- 116,125 ---- <term><replaceable class="parameter">event</replaceable></term> <listitem> <para> ! One of <command>INSERT</command>, <command>UPDATE</command>, ! <command>DELETE</command>, or <command>TRUNCATE</command>; ! this specifies the event that will fire the trigger. Multiple ! events can be specified using <literal>OR</literal>. </para> </listitem> </varlistentry> *************** *** 189,194 **** --- 195,212 ---- </para> <para> + <command>TRUNCATE</> allows multiple tables to be truncated in a single + command. If triggers are defined on more than one of the tables then there + are special rules for trigger execution. All <literal>BEFORE</literal> + triggers fire before any truncate action occurs on any of the tables + listed. <literal>AFTER</literal> triggers begin firing after all of the + listed tables have been truncated. The triggers will fire in the order + the tables are listed for that command, allowing some user control over + firing sequence of the triggers by altering the <command>TRUNCATE</> + command. + </para> + + <para> Use <xref linkend="sql-droptrigger" endterm="sql-droptrigger-title"> to remove a trigger. </para> *************** *** 267,272 **** --- 285,296 ---- <literal>OR</literal> is a <productname>PostgreSQL</> extension of the SQL standard. </para> + + <para> + The ability to fire triggers for <command>TRUNCATE</command> is a + <productname>PostgreSQL</> extension of the SQL standard. + </para> + </refsect1> <refsect1> Index: doc/src/sgml/ref/truncate.sgml =================================================================== RCS file: /home/sriggs/pg/REPOSITORY/pgsql/doc/src/sgml/ref/truncate.sgml,v retrieving revision 1.24 diff -c -r1.24 truncate.sgml *** doc/src/sgml/ref/truncate.sgml 11 May 2007 19:40:08 -0000 1.24 --- doc/src/sgml/ref/truncate.sgml 30 Jan 2008 12:13:00 -0000 *************** *** 92,98 **** <para> <command>TRUNCATE</> will not run any <literal>ON DELETE</literal> ! triggers that might exist for the tables. </para> <warning> --- 92,100 ---- <para> <command>TRUNCATE</> will not run any <literal>ON DELETE</literal> ! triggers that might exist for the tables. <literal>ON TRUNCATE</literal> ! triggers will fire for each table if they exist, see ! <xref linkend="sql-createtrigger" endterm="sql-createtrigger-title">. </para> <warning> Index: src/backend/commands/tablecmds.c =================================================================== RCS file: /home/sriggs/pg/REPOSITORY/pgsql/src/backend/commands/tablecmds.c,v retrieving revision 1.240 diff -c -r1.240 tablecmds.c *** src/backend/commands/tablecmds.c 17 Jan 2008 18:56:54 -0000 1.240 --- src/backend/commands/tablecmds.c 30 Jan 2008 16:40:42 -0000 *************** *** 538,543 **** --- 538,546 ---- List *rels = NIL; List *relids = NIL; ListCell *cell; + #ifdef UNUSED_STATEMENT_SKIPPING_LOGIC + bool do_all_truncates = true; + #endif /* * Open, exclusive-lock, and check all the explicitly-specified relations *************** *** 600,605 **** --- 603,685 ---- #endif /* + * Process BEFORE STATEMENT triggers + * + * We may be processing multiple tables here, so we may have + * multiple truncate triggers to fire. Truncate triggers allow + * the statement to be skipped. If we allowed one table to skip + * TRUNCATE but not the others we might end up with a constraint + * violation. So we implement the rule that if one of the tables + * has a BEFORE STATEMENT trigger which skips the statement then + * all tables must also skip the statement, otherwise error. + */ + foreach(cell, rels) + { + Relation rel = (Relation) lfirst(cell); + ResultRelInfo *resultRelInfo; + EState *estate = CreateExecutorState(); + #ifdef UNUSED_STATEMENT_SKIPPING_LOGIC + bool do_truncate = true; + #endif + + /* + * We need an EState and ResultRelInfo for trigger execution + */ + resultRelInfo = makeNode(ResultRelInfo); + resultRelInfo->ri_RangeTableIndex = 1; /* dummy */ + resultRelInfo->ri_RelationDesc = rel; + resultRelInfo->ri_TrigDesc = CopyTriggerDesc(rel->trigdesc); + if (resultRelInfo->ri_TrigDesc) + resultRelInfo->ri_TrigFunctions = (FmgrInfo *) + palloc0(resultRelInfo->ri_TrigDesc->numtriggers * sizeof(FmgrInfo)); + + /* We cannot EXPLAIN a TRUNCATE command, so no need to instrument */ + resultRelInfo->ri_TrigInstrument = NULL; + + estate->es_result_relations = resultRelInfo; + estate->es_num_result_relations = 1; + estate->es_result_relation_info = resultRelInfo; + + /* + * Process BEFORE EACH STATEMENT triggers + */ + ExecBSTruncateTriggers(estate, estate->es_result_relation_info); + + #ifdef UNUSED_STATEMENT_SKIPPING_LOGIC + /* do_truncate is set via BS trigger execution like this: + + ExecBSTruncateTriggers(estate, estate->es_result_relation_info, + &do_truncate); + */ + + + if (!do_all_truncates && do_truncate) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot skip TRUNCATE for table \"%s\"" + "when other tables in this statement do not", + RelationGetRelationName(rel)))); + + if (!do_truncate) + do_all_truncates = false; + #endif + + if (resultRelInfo->ri_TrigDesc) + pfree(resultRelInfo->ri_TrigFunctions); + FreeExecutorState(estate); + } + + #ifdef UNUSED_STATEMENT_SKIPPING_LOGIC + /* + * If we skipped the TRUNCATE nothing left for us to do. + * + * Note: AFTER triggers do *not* fire in this case. + */ + if (!do_all_truncates) + return; + #endif + + /* * OK, truncate each table. */ foreach(cell, rels) *************** *** 635,640 **** --- 715,766 ---- */ reindex_relation(heap_relid, true); } + + /* + * Process AFTER STATEMENT triggers + * + * We may be processing multiple tables here, so we may have + * multiple truncate triggers to fire. Before Truncate triggers + * are all executed before we perform any TRUNCATE, so we choose + * to implement after triggers the same way to provide symmetry. + */ + foreach(cell, rels) + { + Relation rel = (Relation) lfirst(cell); + ResultRelInfo *resultRelInfo; + EState *estate = CreateExecutorState(); + + /* + * We need an EState and ResultRelInfo for trigger execution + */ + resultRelInfo = makeNode(ResultRelInfo); + resultRelInfo->ri_RangeTableIndex = 1; /* dummy */ + resultRelInfo->ri_RelationDesc = rel; + resultRelInfo->ri_TrigDesc = CopyTriggerDesc(rel->trigdesc); + if (resultRelInfo->ri_TrigDesc) + resultRelInfo->ri_TrigFunctions = (FmgrInfo *) + palloc0(resultRelInfo->ri_TrigDesc->numtriggers * sizeof(FmgrInfo)); + + /* We cannot EXPLAIN a TRUNCATE command, so no need to instrument */ + resultRelInfo->ri_TrigInstrument = NULL; + + estate->es_result_relations = resultRelInfo; + estate->es_num_result_relations = 1; + estate->es_result_relation_info = resultRelInfo; + + AfterTriggerBeginQuery(); + + /* + * Process AFTER EACH STATEMENT trigger events and then execute them + */ + ExecASTruncateTriggers(estate, estate->es_result_relation_info); + + AfterTriggerEndQuery(estate); + + if (resultRelInfo->ri_TrigDesc) + pfree(resultRelInfo->ri_TrigFunctions); + FreeExecutorState(estate); + } } /* Index: src/backend/commands/trigger.c =================================================================== RCS file: /home/sriggs/pg/REPOSITORY/pgsql/src/backend/commands/trigger.c,v retrieving revision 1.227 diff -c -r1.227 trigger.c *** src/backend/commands/trigger.c 2 Jan 2008 23:34:42 -0000 1.227 --- src/backend/commands/trigger.c 30 Jan 2008 16:40:40 -0000 *************** *** 177,182 **** --- 177,189 ---- errmsg("multiple UPDATE events specified"))); TRIGGER_SETT_UPDATE(tgtype); break; + case 't': + if (TRIGGER_FOR_TRUNCATE(tgtype)) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("multiple TRUNCATE events specified"))); + TRIGGER_SETT_TRUNCATE(tgtype); + break; default: elog(ERROR, "unrecognized trigger event: %d", (int) stmt->actions[i]); *************** *** 185,190 **** --- 192,205 ---- } /* + * Specifically disallow TRUNCATE triggers at ROW level + */ + if (TRIGGER_FOR_ROW(tgtype) && TRIGGER_FOR_TRUNCATE(tgtype)) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot fire TRUNCATE triggers FOR EACH ROW"))); + + /* * Find and validate the trigger function. */ funcoid = LookupFuncName(stmt->funcname, 0, fargtypes, false); *************** *** 1297,1302 **** --- 1312,1326 ---- (*tp)[n[TRIGGER_EVENT_UPDATE]] = indx; (n[TRIGGER_EVENT_UPDATE])++; } + + if (TRIGGER_FOR_TRUNCATE(trigger->tgtype)) + { + tp = &(t[TRIGGER_EVENT_TRUNCATE]); + if (*tp == NULL) + *tp = (int *) palloc(trigdesc->numtriggers * sizeof(int)); + (*tp)[n[TRIGGER_EVENT_TRUNCATE]] = indx; + (n[TRIGGER_EVENT_TRUNCATE])++; + } } /* *************** *** 2028,2033 **** --- 2052,2138 ---- } } + void + ExecBSTruncateTriggers(EState *estate, ResultRelInfo *relinfo) + { + TriggerDesc *trigdesc; + int ntrigs; + int *tgindx; + int i; + TriggerData LocTriggerData; + + trigdesc = relinfo->ri_TrigDesc; + + if (trigdesc == NULL) + return; + + ntrigs = trigdesc->n_before_statement[TRIGGER_EVENT_TRUNCATE]; + tgindx = trigdesc->tg_before_statement[TRIGGER_EVENT_TRUNCATE]; + + if (ntrigs == 0) + return; + + LocTriggerData.type = T_TriggerData; + LocTriggerData.tg_event = TRIGGER_EVENT_TRUNCATE | + TRIGGER_EVENT_BEFORE; + LocTriggerData.tg_relation = relinfo->ri_RelationDesc; + LocTriggerData.tg_trigtuple = NULL; + LocTriggerData.tg_newtuple = NULL; + LocTriggerData.tg_trigtuplebuf = InvalidBuffer; + LocTriggerData.tg_newtuplebuf = InvalidBuffer; + for (i = 0; i < ntrigs; i++) + { + 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; + } + LocTriggerData.tg_trigger = trigger; + newtuple = ExecCallTriggerFunc(&LocTriggerData, + tgindx[i], + relinfo->ri_TrigFunctions, + relinfo->ri_TrigInstrument, + GetPerTupleMemoryContext(estate)); + + #ifdef UNUSED_STATEMENT_SKIPPING_LOGIC + if (newtuple) + *do_trunc = false; + /* + * Note that we do *not* throw this error here + * ereport(ERROR, + * (errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED), + * errmsg("BEFORE STATEMENT trigger cannot return a value"))); + */ + #else + if (newtuple) + ereport(ERROR, + (errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED), + errmsg("BEFORE STATEMENT trigger cannot return a value"))); + + #endif + + } + } + + void + ExecASTruncateTriggers(EState *estate, ResultRelInfo *relinfo) + { + TriggerDesc *trigdesc = relinfo->ri_TrigDesc; + + if (trigdesc && trigdesc->n_after_statement[TRIGGER_EVENT_TRUNCATE] > 0) + AfterTriggerSaveEvent(relinfo, TRIGGER_EVENT_TRUNCATE, + false, NULL, NULL); + } static HeapTuple GetTupleForTrigger(EState *estate, ResultRelInfo *relinfo, *************** *** 3570,3575 **** --- 3675,3686 ---- elog(ERROR, "AfterTriggerSaveEvent() called outside of transaction"); /* + * event is used both as a bitmask and an array offset, + * so make sure we don't walk off the edge of our arrays + */ + Assert(event >= 0 && event < TRIGGER_NUM_EVENT_CLASSES); + + /* * Get the CTID's of OLD and NEW */ if (oldtup != NULL) Index: src/backend/parser/gram.y =================================================================== RCS file: /home/sriggs/pg/REPOSITORY/pgsql/src/backend/parser/gram.y,v retrieving revision 2.605 diff -c -r2.605 gram.y *** src/backend/parser/gram.y 1 Jan 2008 19:45:50 -0000 2.605 --- src/backend/parser/gram.y 29 Jan 2008 09:04:07 -0000 *************** *** 2717,2722 **** --- 2717,2723 ---- INSERT { $$ = 'i'; } | DELETE_P { $$ = 'd'; } | UPDATE { $$ = 'u'; } + | TRUNCATE { $$ = 't'; } ; TriggerForSpec: Index: src/backend/utils/adt/ruleutils.c =================================================================== RCS file: /home/sriggs/pg/REPOSITORY/pgsql/src/backend/utils/adt/ruleutils.c,v retrieving revision 1.269 diff -c -r1.269 ruleutils.c *** src/backend/utils/adt/ruleutils.c 6 Jan 2008 01:03:16 -0000 1.269 --- src/backend/utils/adt/ruleutils.c 29 Jan 2008 23:24:09 -0000 *************** *** 498,503 **** --- 498,510 ---- else appendStringInfo(&buf, " UPDATE"); } + if (TRIGGER_FOR_TRUNCATE(trigrec->tgtype)) + { + if (findx > 0) + appendStringInfo(&buf, " OR TRUNCATE"); + else + appendStringInfo(&buf, " TRUNCATE"); + } appendStringInfo(&buf, " ON %s ", generate_relation_name(trigrec->tgrelid)); Index: src/bin/pg_dump/pg_dump.c =================================================================== RCS file: /home/sriggs/pg/REPOSITORY/pgsql/src/bin/pg_dump/pg_dump.c,v retrieving revision 1.481 diff -c -r1.481 pg_dump.c *** src/bin/pg_dump/pg_dump.c 1 Jan 2008 19:45:55 -0000 1.481 --- src/bin/pg_dump/pg_dump.c 30 Jan 2008 17:00:09 -0000 *************** *** 9625,9630 **** --- 9625,9637 ---- else appendPQExpBuffer(query, " UPDATE"); } + if (TRIGGER_FOR_TRUNCATE(tginfo->tgtype)) + { + if (findx > 0) + appendPQExpBuffer(query, " OR TRUNCATE"); + else + appendPQExpBuffer(query, " TRUNCATE"); + } appendPQExpBuffer(query, " ON %s\n", fmtId(tbinfo->dobj.name)); Index: src/include/catalog/pg_trigger.h =================================================================== RCS file: /home/sriggs/pg/REPOSITORY/pgsql/src/include/catalog/pg_trigger.h,v retrieving revision 1.30 diff -c -r1.30 pg_trigger.h *** src/include/catalog/pg_trigger.h 1 Jan 2008 19:45:57 -0000 1.30 --- src/include/catalog/pg_trigger.h 30 Jan 2008 10:58:43 -0000 *************** *** 94,99 **** --- 94,100 ---- #define TRIGGER_TYPE_INSERT (1 << 2) #define TRIGGER_TYPE_DELETE (1 << 3) #define TRIGGER_TYPE_UPDATE (1 << 4) + #define TRIGGER_TYPE_TRUNCATE (1 << 5) /* patch note: no need for initdb */ /* Macros for manipulating tgtype */ #define TRIGGER_CLEAR_TYPE(type) ((type) = 0) *************** *** 103,113 **** --- 104,116 ---- #define TRIGGER_SETT_INSERT(type) ((type) |= TRIGGER_TYPE_INSERT) #define TRIGGER_SETT_DELETE(type) ((type) |= TRIGGER_TYPE_DELETE) #define TRIGGER_SETT_UPDATE(type) ((type) |= TRIGGER_TYPE_UPDATE) + #define TRIGGER_SETT_TRUNCATE(type) ((type) |= TRIGGER_TYPE_TRUNCATE) #define TRIGGER_FOR_ROW(type) ((type) & TRIGGER_TYPE_ROW) #define TRIGGER_FOR_BEFORE(type) ((type) & TRIGGER_TYPE_BEFORE) #define TRIGGER_FOR_INSERT(type) ((type) & TRIGGER_TYPE_INSERT) #define TRIGGER_FOR_DELETE(type) ((type) & TRIGGER_TYPE_DELETE) #define TRIGGER_FOR_UPDATE(type) ((type) & TRIGGER_TYPE_UPDATE) + #define TRIGGER_FOR_TRUNCATE(type) ((type) & TRIGGER_TYPE_TRUNCATE) #endif /* PG_TRIGGER_H */ Index: src/include/commands/trigger.h =================================================================== RCS file: /home/sriggs/pg/REPOSITORY/pgsql/src/include/commands/trigger.h,v retrieving revision 1.66 diff -c -r1.66 trigger.h *** src/include/commands/trigger.h 2 Jan 2008 23:34:42 -0000 1.66 --- src/include/commands/trigger.h 30 Jan 2008 13:22:55 -0000 *************** *** 38,48 **** Buffer tg_newtuplebuf; } TriggerData; ! /* TriggerEvent bit flags */ ! #define TRIGGER_EVENT_INSERT 0x00000000 #define TRIGGER_EVENT_DELETE 0x00000001 #define TRIGGER_EVENT_UPDATE 0x00000002 #define TRIGGER_EVENT_OPMASK 0x00000003 #define TRIGGER_EVENT_ROW 0x00000004 #define TRIGGER_EVENT_BEFORE 0x00000008 --- 38,66 ---- Buffer tg_newtuplebuf; } TriggerData; ! /* TriggerEvent bit flags ! * ! * There are two distinct sets of DEFINEs associated with ! * trigger processing: ! * ! * TRIGGER_EVENT_* ! * defined in include/commands/trigger.h ! * used both as bitmasks for tg_type ! * and as array offsets within relcache arrays: be warned! ! * ! * The bitmasking only works if we never OR events together, which we ! * don't ever do, since a trigger is only invoked by one specific ! * event and hence just one event type, even if it could be invoked ! * by multiple event types. ! * ! * TRIGGER_TYPE_* ! * defined in include/catalog/pg_trigger.h ! * used as bitmaskable integers within pg_trigger ! */ #define TRIGGER_EVENT_INSERT 0x00000000 #define TRIGGER_EVENT_DELETE 0x00000001 #define TRIGGER_EVENT_UPDATE 0x00000002 + #define TRIGGER_EVENT_TRUNCATE 0x00000003 #define TRIGGER_EVENT_OPMASK 0x00000003 #define TRIGGER_EVENT_ROW 0x00000004 #define TRIGGER_EVENT_BEFORE 0x00000008 *************** *** 66,71 **** --- 84,93 ---- (((TriggerEvent) (event) & TRIGGER_EVENT_OPMASK) == \ TRIGGER_EVENT_UPDATE) + #define TRIGGER_FIRED_BY_TRUNCATE(event) \ + (((TriggerEvent) (event) & TRIGGER_EVENT_OPMASK) == \ + TRIGGER_EVENT_TRUNCATE) + #define TRIGGER_FIRED_FOR_ROW(event) \ ((TriggerEvent) (event) & TRIGGER_EVENT_ROW) *************** *** 141,146 **** --- 163,173 ---- ItemPointer tupleid, HeapTuple newtuple); + extern void ExecBSTruncateTriggers(EState *estate, + ResultRelInfo *relinfo); + extern void ExecASTruncateTriggers(EState *estate, + ResultRelInfo *relinfo); + extern void AfterTriggerBeginXact(void); extern void AfterTriggerBeginQuery(void); extern void AfterTriggerEndQuery(EState *estate); Index: src/include/utils/rel.h =================================================================== RCS file: /home/sriggs/pg/REPOSITORY/pgsql/src/include/utils/rel.h,v retrieving revision 1.104 diff -c -r1.104 rel.h *** src/include/utils/rel.h 1 Jan 2008 19:45:59 -0000 1.104 --- src/include/utils/rel.h 30 Jan 2008 10:18:52 -0000 *************** *** 73,79 **** * can appear in more than one class, for each class we provide a list of * integer indexes into the triggers array. */ ! #define TRIGGER_NUM_EVENT_CLASSES 3 uint16 n_before_statement[TRIGGER_NUM_EVENT_CLASSES]; uint16 n_before_row[TRIGGER_NUM_EVENT_CLASSES]; --- 73,79 ---- * can appear in more than one class, for each class we provide a list of * integer indexes into the triggers array. */ ! #define TRIGGER_NUM_EVENT_CLASSES 4 uint16 n_before_statement[TRIGGER_NUM_EVENT_CLASSES]; uint16 n_before_row[TRIGGER_NUM_EVENT_CLASSES]; Index: src/pl/plperl/plperl.c =================================================================== RCS file: /home/sriggs/pg/REPOSITORY/pgsql/src/pl/plperl/plperl.c,v retrieving revision 1.136 diff -c -r1.136 plperl.c *** src/pl/plperl/plperl.c 23 Jan 2008 00:55:47 -0000 1.136 --- src/pl/plperl/plperl.c 30 Jan 2008 11:16:01 -0000 *************** *** 689,694 **** --- 689,696 ---- tupdesc)); } } + else if (TRIGGER_FIRED_BY_TRUNCATE(tdata->tg_event)) + event = "TRUNCATE"; else event = "UNKNOWN"; *************** *** 1395,1400 **** --- 1397,1404 ---- retval = (Datum) trigdata->tg_newtuple; else if (TRIGGER_FIRED_BY_DELETE(trigdata->tg_event)) retval = (Datum) trigdata->tg_trigtuple; + else if (TRIGGER_FIRED_BY_TRUNCATE(trigdata->tg_event)) + retval = (Datum) trigdata->tg_trigtuple; else retval = (Datum) 0; /* can this happen? */ } Index: src/pl/plpgsql/src/pl_exec.c =================================================================== RCS file: /home/sriggs/pg/REPOSITORY/pgsql/src/pl/plpgsql/src/pl_exec.c,v retrieving revision 1.202 diff -c -r1.202 pl_exec.c *** src/pl/plpgsql/src/pl_exec.c 1 Jan 2008 19:46:00 -0000 1.202 --- src/pl/plpgsql/src/pl_exec.c 30 Jan 2008 10:28:51 -0000 *************** *** 524,530 **** rec_old->tupdesc = trigdata->tg_relation->rd_att; } else ! elog(ERROR, "unrecognized trigger action: not INSERT, DELETE, or UPDATE"); /* * Assign the special tg_ variables --- 524,530 ---- rec_old->tupdesc = trigdata->tg_relation->rd_att; } else ! elog(ERROR, "unrecognized trigger action: not INSERT, DELETE, UPDATE or TRUNCATE"); /* * Assign the special tg_ variables *************** *** 537,544 **** var->value = DirectFunctionCall1(textin, CStringGetDatum("UPDATE")); else if (TRIGGER_FIRED_BY_DELETE(trigdata->tg_event)) var->value = DirectFunctionCall1(textin, CStringGetDatum("DELETE")); else ! elog(ERROR, "unrecognized trigger action: not INSERT, DELETE, or UPDATE"); var->isnull = false; var->freeval = true; --- 537,546 ---- var->value = DirectFunctionCall1(textin, CStringGetDatum("UPDATE")); else if (TRIGGER_FIRED_BY_DELETE(trigdata->tg_event)) var->value = DirectFunctionCall1(textin, CStringGetDatum("DELETE")); + else if (TRIGGER_FIRED_BY_TRUNCATE(trigdata->tg_event)) + var->value = DirectFunctionCall1(textin, CStringGetDatum("TRUNCATE")); else ! elog(ERROR, "unrecognized trigger action: not INSERT, DELETE, UPDATE or TRUNCATE"); var->isnull = false; var->freeval = true; Index: src/pl/plpython/plpython.c =================================================================== RCS file: /home/sriggs/pg/REPOSITORY/pgsql/src/pl/plpython/plpython.c,v retrieving revision 1.106 diff -c -r1.106 plpython.c *** src/pl/plpython/plpython.c 2 Jan 2008 03:10:27 -0000 1.106 --- src/pl/plpython/plpython.c 30 Jan 2008 11:17:56 -0000 *************** *** 714,719 **** --- 714,721 ---- pltevent = PyString_FromString("DELETE"); else if (TRIGGER_FIRED_BY_UPDATE(tdata->tg_event)) pltevent = PyString_FromString("UPDATE"); + else if (TRIGGER_FIRED_BY_TRUNCATE(tdata->tg_event)) + pltevent = PyString_FromString("TRUNCATE"); else { elog(ERROR, "unrecognized OP tg_event: %u", tdata->tg_event); Index: src/pl/tcl/pltcl.c =================================================================== RCS file: /home/sriggs/pg/REPOSITORY/pgsql/src/pl/tcl/pltcl.c,v retrieving revision 1.117 diff -c -r1.117 pltcl.c *** src/pl/tcl/pltcl.c 15 Nov 2007 21:14:46 -0000 1.117 --- src/pl/tcl/pltcl.c 30 Jan 2008 11:31:03 -0000 *************** *** 824,829 **** --- 824,831 ---- Tcl_DStringAppendElement(&tcl_cmd, "DELETE"); else if (TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event)) Tcl_DStringAppendElement(&tcl_cmd, "UPDATE"); + else if (TRIGGER_FIRED_BY_TRUNCATE(trigdata->tg_event)) + Tcl_DStringAppendElement(&tcl_cmd, "TRUNCATE"); else elog(ERROR, "unrecognized OP tg_event: %u", trigdata->tg_event); Index: src/test/regress/expected/truncate.out =================================================================== RCS file: /home/sriggs/pg/REPOSITORY/pgsql/src/test/regress/expected/truncate.out,v retrieving revision 1.12 diff -c -r1.12 truncate.out *** src/test/regress/expected/truncate.out 29 Jun 2006 16:07:29 -0000 1.12 --- src/test/regress/expected/truncate.out 30 Jan 2008 11:29:07 -0000 *************** *** 145,147 **** --- 145,373 ---- NOTICE: drop cascades to constraint trunc_b_a_fkey on table trunc_b NOTICE: drop cascades to constraint trunc_e_b_fkey on table trunc_e NOTICE: drop cascades to constraint trunc_d_a_fkey on table trunc_d + -- TRUNCATE test runs much later than TRIGGER test, so just add a + -- few additional checks to ensure truncate triggers work + CREATE TABLE trunc_trigger_test (f1 int, f2 text, f3 text); + CREATE TABLE trunc_trigger_log (tgop text, tglevel text, tgwhen text, tgargv text); + CREATE FUNCTION trunctrigger() RETURNS trigger as $$ + begin + insert into trunc_trigger_log values (TG_OP, TG_LEVEL, TG_WHEN, TG_ARGV[0]); + return null; + end; + $$ LANGUAGE plpgsql; + INSERT INTO trunc_trigger_test VALUES(1, 'foo', 'bar'); + INSERT INTO trunc_trigger_test VALUES(2, 'baz', 'quux'); + -- cannot have row level truncate triggers + CREATE TRIGGER t + BEFORE TRUNCATE ON trunc_trigger_test + FOR EACH ROW EXECUTE PROCEDURE trunctrigger(); + ERROR: cannot fire TRUNCATE triggers FOR EACH ROW + -- cannot have row level truncate triggers + CREATE TRIGGER t + AFTER TRUNCATE OR INSERT ON trunc_trigger_test + FOR EACH ROW EXECUTE PROCEDURE trunctrigger(); + ERROR: cannot fire TRUNCATE triggers FOR EACH ROW + -- duplicate event error + CREATE TRIGGER t + AFTER TRUNCATE OR TRUNCATE ON trunc_trigger_test + EXECUTE PROCEDURE trunctrigger(); + ERROR: multiple TRUNCATE events specified + -- basic before trigger + CREATE TRIGGER t + BEFORE TRUNCATE ON trunc_trigger_test + FOR EACH STATEMENT + EXECUTE PROCEDURE trunctrigger('before trigger truncate'); + \d trunc_trigger_test + Table "public.trunc_trigger_test" + Column | Type | Modifiers + --------+---------+----------- + f1 | integer | + f2 | text | + f3 | text | + Triggers: + t BEFORE TRUNCATE ON trunc_trigger_test FOR EACH STATEMENT EXECUTE PROCEDURE trunctrigger('before trigger truncate') + + SELECT count(*) as "Row count in test table" FROM trunc_trigger_test; + Row count in test table + ------------------------- + 2 + (1 row) + + SELECT * FROM trunc_trigger_log; + tgop | tglevel | tgwhen | tgargv + ------+---------+--------+-------- + (0 rows) + + TRUNCATE trunc_trigger_test; + SELECT count(*) as "Row count in test table" FROM trunc_trigger_test; + Row count in test table + ------------------------- + 0 + (1 row) + + SELECT * FROM trunc_trigger_log; + tgop | tglevel | tgwhen | tgargv + ----------+-----------+--------+------------------------- + TRUNCATE | STATEMENT | BEFORE | before trigger truncate + (1 row) + + DROP TRIGGER t ON trunc_trigger_test; + -- mixed before/after trigger + truncate trunc_trigger_log; + CREATE TRIGGER ti + BEFORE insert ON trunc_trigger_test + FOR EACH STATEMENT + EXECUTE PROCEDURE trunctrigger('before trigger insert'); + INSERT INTO trunc_trigger_test VALUES(3, 'baz', 'quux'); + SELECT * FROM trunc_trigger_log; + tgop | tglevel | tgwhen | tgargv + --------+-----------+--------+----------------------- + INSERT | STATEMENT | BEFORE | before trigger insert + (1 row) + + CREATE TRIGGER tt + AFTER TRUNCATE ON trunc_trigger_test + FOR EACH STATEMENT + EXECUTE PROCEDURE trunctrigger('after trigger truncate'); + \d trunc_trigger_test + Table "public.trunc_trigger_test" + Column | Type | Modifiers + --------+---------+----------- + f1 | integer | + f2 | text | + f3 | text | + Triggers: + ti BEFORE INSERT ON trunc_trigger_test FOR EACH STATEMENT EXECUTE PROCEDURE trunctrigger('before trigger insert') + tt AFTER TRUNCATE ON trunc_trigger_test FOR EACH STATEMENT EXECUTE PROCEDURE trunctrigger('after trigger truncate') + + SELECT count(*) as "Row count in test table" FROM trunc_trigger_test; + Row count in test table + ------------------------- + 1 + (1 row) + + SELECT * FROM trunc_trigger_log; + tgop | tglevel | tgwhen | tgargv + --------+-----------+--------+----------------------- + INSERT | STATEMENT | BEFORE | before trigger insert + (1 row) + + TRUNCATE trunc_trigger_test; + SELECT count(*) as "Row count in test table" FROM trunc_trigger_test; + Row count in test table + ------------------------- + 0 + (1 row) + + SELECT * FROM trunc_trigger_log; + tgop | tglevel | tgwhen | tgargv + ----------+-----------+--------+------------------------ + INSERT | STATEMENT | BEFORE | before trigger insert + TRUNCATE | STATEMENT | AFTER | after trigger truncate + (2 rows) + + DROP TRIGGER ti ON trunc_trigger_test; + DROP TRIGGER tt ON trunc_trigger_test; + -- multiple tables, multiple triggers + TRUNCATE trunc_trigger_log; + CREATE TABLE trunc_trigger_test2 (f1 int, f2 text); + CREATE TRIGGER t1b + BEFORE TRUNCATE ON trunc_trigger_test + EXECUTE PROCEDURE trunctrigger('t1b'); + CREATE TRIGGER t2b + BEFORE TRUNCATE ON trunc_trigger_test2 + EXECUTE PROCEDURE trunctrigger('t2b'); + CREATE TRIGGER t1a + AFTER TRUNCATE OR INSERT ON trunc_trigger_test + EXECUTE PROCEDURE trunctrigger('t1a'); + CREATE TRIGGER t2at + AFTER TRUNCATE ON trunc_trigger_test2 + EXECUTE PROCEDURE trunctrigger('t2at'); + CREATE TRIGGER t2ai + AFTER INSERT ON trunc_trigger_test2 + EXECUTE PROCEDURE trunctrigger('t2ai'); + \d trunc_trigger_test + Table "public.trunc_trigger_test" + Column | Type | Modifiers + --------+---------+----------- + f1 | integer | + f2 | text | + f3 | text | + Triggers: + t1a AFTER INSERT OR TRUNCATE ON trunc_trigger_test FOR EACH STATEMENT EXECUTE PROCEDURE trunctrigger('t1a') + t1b BEFORE TRUNCATE ON trunc_trigger_test FOR EACH STATEMENT EXECUTE PROCEDURE trunctrigger('t1b') + + \d trunc_trigger_test2 + Table "public.trunc_trigger_test2" + Column | Type | Modifiers + --------+---------+----------- + f1 | integer | + f2 | text | + Triggers: + t2ai AFTER INSERT ON trunc_trigger_test2 FOR EACH STATEMENT EXECUTE PROCEDURE trunctrigger('t2ai') + t2at AFTER TRUNCATE ON trunc_trigger_test2 FOR EACH STATEMENT EXECUTE PROCEDURE trunctrigger('t2at') + t2b BEFORE TRUNCATE ON trunc_trigger_test2 FOR EACH STATEMENT EXECUTE PROCEDURE trunctrigger('t2b') + + INSERT INTO trunc_trigger_test VALUES(1, 'foo', 'bar'); + INSERT INTO trunc_trigger_test VALUES(2, 'baz', 'quux'); + INSERT INTO trunc_trigger_test2 VALUES(1, 'foo'); + INSERT INTO trunc_trigger_test2 VALUES(2, 'bar'); + INSERT INTO trunc_trigger_test2 VALUES(3, 'baz'); + SELECT count(*) as "Row count in test table" FROM trunc_trigger_test; + Row count in test table + ------------------------- + 2 + (1 row) + + SELECT count(*) as "Row count in test table2" FROM trunc_trigger_test2; + Row count in test table2 + -------------------------- + 3 + (1 row) + + SELECT * FROM trunc_trigger_log; + tgop | tglevel | tgwhen | tgargv + --------+-----------+--------+-------- + INSERT | STATEMENT | AFTER | t1a + INSERT | STATEMENT | AFTER | t1a + INSERT | STATEMENT | AFTER | t2ai + INSERT | STATEMENT | AFTER | t2ai + INSERT | STATEMENT | AFTER | t2ai + (5 rows) + + TRUNCATE trunc_trigger_test, trunc_trigger_test2; + SELECT count(*) as "Row count in test table" FROM trunc_trigger_test; + Row count in test table + ------------------------- + 0 + (1 row) + + SELECT count(*) as "Row count in test table2" FROM trunc_trigger_test2; + Row count in test table2 + -------------------------- + 0 + (1 row) + + SELECT * FROM trunc_trigger_log; + tgop | tglevel | tgwhen | tgargv + ----------+-----------+--------+-------- + INSERT | STATEMENT | AFTER | t1a + INSERT | STATEMENT | AFTER | t1a + INSERT | STATEMENT | AFTER | t2ai + INSERT | STATEMENT | AFTER | t2ai + INSERT | STATEMENT | AFTER | t2ai + TRUNCATE | STATEMENT | BEFORE | t1b + TRUNCATE | STATEMENT | BEFORE | t2b + TRUNCATE | STATEMENT | AFTER | t1a + TRUNCATE | STATEMENT | AFTER | t2at + (9 rows) + + DROP TRIGGER t1b ON trunc_trigger_test; + DROP TRIGGER t2b ON trunc_trigger_test2; + DROP TRIGGER t1a ON trunc_trigger_test; + DROP TRIGGER t2at ON trunc_trigger_test2; + DROP TRIGGER t2ai ON trunc_trigger_test2; + DROP TABLE trunc_trigger_test; + DROP TABLE trunc_trigger_test2; + DROP TABLE trunc_trigger_log; + DROP FUNCTION trunctrigger(); Index: src/test/regress/sql/truncate.sql =================================================================== RCS file: /home/sriggs/pg/REPOSITORY/pgsql/src/test/regress/sql/truncate.sql,v retrieving revision 1.5 diff -c -r1.5 truncate.sql *** src/test/regress/sql/truncate.sql 29 Jun 2006 16:07:29 -0000 1.5 --- src/test/regress/sql/truncate.sql 30 Jan 2008 10:56:18 -0000 *************** *** 77,79 **** --- 77,206 ---- SELECT * FROM trunc_e; DROP TABLE truncate_a,trunc_c,trunc_b,trunc_d,trunc_e CASCADE; + + -- TRUNCATE test runs much later than TRIGGER test, so just add a + -- few additional checks to ensure truncate triggers work + + CREATE TABLE trunc_trigger_test (f1 int, f2 text, f3 text); + CREATE TABLE trunc_trigger_log (tgop text, tglevel text, tgwhen text, tgargv text); + + CREATE FUNCTION trunctrigger() RETURNS trigger as $$ + begin + insert into trunc_trigger_log values (TG_OP, TG_LEVEL, TG_WHEN, TG_ARGV[0]); + return null; + end; + $$ LANGUAGE plpgsql; + + INSERT INTO trunc_trigger_test VALUES(1, 'foo', 'bar'); + INSERT INTO trunc_trigger_test VALUES(2, 'baz', 'quux'); + + -- cannot have row level truncate triggers + CREATE TRIGGER t + BEFORE TRUNCATE ON trunc_trigger_test + FOR EACH ROW EXECUTE PROCEDURE trunctrigger(); + + -- cannot have row level truncate triggers + CREATE TRIGGER t + AFTER TRUNCATE OR INSERT ON trunc_trigger_test + FOR EACH ROW EXECUTE PROCEDURE trunctrigger(); + + -- duplicate event error + CREATE TRIGGER t + AFTER TRUNCATE OR TRUNCATE ON trunc_trigger_test + EXECUTE PROCEDURE trunctrigger(); + + -- basic before trigger + CREATE TRIGGER t + BEFORE TRUNCATE ON trunc_trigger_test + FOR EACH STATEMENT + EXECUTE PROCEDURE trunctrigger('before trigger truncate'); + + \d trunc_trigger_test + + SELECT count(*) as "Row count in test table" FROM trunc_trigger_test; + SELECT * FROM trunc_trigger_log; + TRUNCATE trunc_trigger_test; + SELECT count(*) as "Row count in test table" FROM trunc_trigger_test; + SELECT * FROM trunc_trigger_log; + + DROP TRIGGER t ON trunc_trigger_test; + + -- mixed before/after trigger + truncate trunc_trigger_log; + CREATE TRIGGER ti + BEFORE insert ON trunc_trigger_test + FOR EACH STATEMENT + EXECUTE PROCEDURE trunctrigger('before trigger insert'); + + INSERT INTO trunc_trigger_test VALUES(3, 'baz', 'quux'); + SELECT * FROM trunc_trigger_log; + + CREATE TRIGGER tt + AFTER TRUNCATE ON trunc_trigger_test + FOR EACH STATEMENT + EXECUTE PROCEDURE trunctrigger('after trigger truncate'); + + \d trunc_trigger_test + + SELECT count(*) as "Row count in test table" FROM trunc_trigger_test; + SELECT * FROM trunc_trigger_log; + TRUNCATE trunc_trigger_test; + SELECT count(*) as "Row count in test table" FROM trunc_trigger_test; + SELECT * FROM trunc_trigger_log; + + DROP TRIGGER ti ON trunc_trigger_test; + DROP TRIGGER tt ON trunc_trigger_test; + + -- multiple tables, multiple triggers + TRUNCATE trunc_trigger_log; + CREATE TABLE trunc_trigger_test2 (f1 int, f2 text); + + CREATE TRIGGER t1b + BEFORE TRUNCATE ON trunc_trigger_test + EXECUTE PROCEDURE trunctrigger('t1b'); + + CREATE TRIGGER t2b + BEFORE TRUNCATE ON trunc_trigger_test2 + EXECUTE PROCEDURE trunctrigger('t2b'); + + CREATE TRIGGER t1a + AFTER TRUNCATE OR INSERT ON trunc_trigger_test + EXECUTE PROCEDURE trunctrigger('t1a'); + + CREATE TRIGGER t2at + AFTER TRUNCATE ON trunc_trigger_test2 + EXECUTE PROCEDURE trunctrigger('t2at'); + + CREATE TRIGGER t2ai + AFTER INSERT ON trunc_trigger_test2 + EXECUTE PROCEDURE trunctrigger('t2ai'); + + \d trunc_trigger_test + \d trunc_trigger_test2 + + INSERT INTO trunc_trigger_test VALUES(1, 'foo', 'bar'); + INSERT INTO trunc_trigger_test VALUES(2, 'baz', 'quux'); + + INSERT INTO trunc_trigger_test2 VALUES(1, 'foo'); + INSERT INTO trunc_trigger_test2 VALUES(2, 'bar'); + INSERT INTO trunc_trigger_test2 VALUES(3, 'baz'); + + SELECT count(*) as "Row count in test table" FROM trunc_trigger_test; + SELECT count(*) as "Row count in test table2" FROM trunc_trigger_test2; + SELECT * FROM trunc_trigger_log; + TRUNCATE trunc_trigger_test, trunc_trigger_test2; + SELECT count(*) as "Row count in test table" FROM trunc_trigger_test; + SELECT count(*) as "Row count in test table2" FROM trunc_trigger_test2; + SELECT * FROM trunc_trigger_log; + + DROP TRIGGER t1b ON trunc_trigger_test; + DROP TRIGGER t2b ON trunc_trigger_test2; + DROP TRIGGER t1a ON trunc_trigger_test; + DROP TRIGGER t2at ON trunc_trigger_test2; + DROP TRIGGER t2ai ON trunc_trigger_test2; + + DROP TABLE trunc_trigger_test; + DROP TABLE trunc_trigger_test2; + DROP TABLE trunc_trigger_log; + + DROP FUNCTION trunctrigger();
---------------------------(end of broadcast)--------------------------- TIP 5: don't forget to increase your free space map settings