Fix SQL-spec incompatibilities in new transition table feature.

The standard says that all changes of the same kind (insert, update, or
delete) caused in one table by a single SQL statement should be reported
in a single transition table; and by that, they mean to include foreign key
enforcement actions cascading from the statement's direct effects.  It's
also reasonable to conclude that if the standard had wCTEs, they would say
that effects of wCTEs applying to the same table as each other or the outer
statement should be merged into one transition table.  We weren't doing it
like that.

Hence, arrange to merge tuples from multiple update actions into a single
transition table as much as we can.  There is a problem, which is that if
the firing of FK enforcement triggers and after-row triggers with
transition tables is interspersed, we might need to report more tuples
after some triggers have already seen the transition table.  It seems like
a bad idea for the transition table to be mutable between trigger calls.
There's no good way around this without a major redesign of the FK logic,
so for now, resolve it by opening a new transition table each time this
happens.

Also, ensure that AFTER STATEMENT triggers fire just once per statement,
or once per transition table when we're forced to make more than one.
Previous versions of Postgres have allowed each FK enforcement query
to cause an additional firing of the AFTER STATEMENT triggers for the
referencing table, but that's certainly not per spec.  (We're still
doing multiple firings of BEFORE STATEMENT triggers, though; is that
something worth changing?)

Also, forbid using transition tables with column-specific UPDATE triggers.
The spec requires such transition tables to show only the tuples for which
the UPDATE trigger would have fired, which means maintaining multiple
transition tables or else somehow filtering the contents at readout.
Maybe someday we'll bother to support that option, but it looks like a
lot of trouble for a marginal feature.

The transition tables are now managed by the AfterTriggers data structures,
rather than being directly the responsibility of ModifyTable nodes.  This
removes a subtransaction-lifespan memory leak introduced by my previous
band-aid patch 3c4359521.

In passing, refactor the AfterTriggers data structures to reduce the
management overhead for them, by using arrays of structs rather than
several parallel arrays for per-query-level and per-subtransaction state.

I failed to resist the temptation to do some copy-editing on the SGML
docs about triggers, above and beyond merely documenting the effects
of this patch.

Back-patch to v10, because we don't want the semantics of transition
tables to change post-release.

Patch by me, with help and review from Thomas Munro.

Discussion: https://postgr.es/m/20170909064853.25630.12825@wrigleys.postgresql.org
This commit is contained in:
Tom Lane 2017-09-16 13:20:32 -04:00
parent 04b64b8ddf
commit 0f79440fb0
11 changed files with 804 additions and 391 deletions

View File

@ -52,7 +52,7 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="PARAMETER">name</replaceable>
trigger will be associated with the specified table, view, or foreign table
and will execute the specified
function <replaceable class="parameter">function_name</replaceable> when
certain events occur.
certain operations are performed on that table.
</para>
<para>
@ -82,10 +82,7 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="PARAMETER">name</replaceable>
executes once for any given operation, regardless of how many rows
it modifies (in particular, an operation that modifies zero rows
will still result in the execution of any applicable <literal>FOR
EACH STATEMENT</literal> triggers). Note that with an
<command>INSERT</command> with an <literal>ON CONFLICT DO UPDATE</>
clause, both <command>INSERT</command> and
<command>UPDATE</command> statement level trigger will be fired.
EACH STATEMENT</literal> triggers).
</para>
<para>
@ -174,7 +171,8 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="PARAMETER">name</replaceable>
<firstterm>constraint trigger</>. This is the same as a regular trigger
except that the timing of the trigger firing can be adjusted using
<xref linkend="SQL-SET-CONSTRAINTS">.
Constraint triggers must be <literal>AFTER ROW</> triggers on tables. They
Constraint triggers must be <literal>AFTER ROW</> triggers on plain
tables (not foreign tables). They
can be fired either at the end of the statement causing the triggering
event, or at the end of the containing transaction; in the latter case they
are said to be <firstterm>deferred</>. A pending deferred-trigger firing
@ -184,18 +182,29 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="PARAMETER">name</replaceable>
</para>
<para>
The <literal>REFERENCING</> option is only allowed for an <literal>AFTER</>
trigger which is not a constraint trigger. <literal>OLD TABLE</> may only
be specified once, and only on a trigger which can fire on
<literal>UPDATE</> or <literal>DELETE</>. <literal>NEW TABLE</> may only
be specified once, and only on a trigger which can fire on
<literal>UPDATE</> or <literal>INSERT</>.
The <literal>REFERENCING</> option enables collection
of <firstterm>transition relations</>, which are row sets that include all
of the rows inserted, deleted, or modified by the current SQL statement.
This feature lets the trigger see a global view of what the statement did,
not just one row at a time. This option is only allowed for
an <literal>AFTER</> trigger that is not a constraint trigger; also, if
the trigger is an <literal>UPDATE</> trigger, it must not specify
a <replaceable class="parameter">column_name</replaceable> list.
<literal>OLD TABLE</> may only be specified once, and only for a trigger
that can fire on <literal>UPDATE</> or <literal>DELETE</>; it creates a
transition relation containing the <firstterm>before-images</> of all rows
updated or deleted by the statement.
Similarly, <literal>NEW TABLE</> may only be specified once, and only for
a trigger that can fire on <literal>UPDATE</> or <literal>INSERT</>;
it creates a transition relation containing the <firstterm>after-images</>
of all rows updated or inserted by the statement.
</para>
<para>
<command>SELECT</command> does not modify any rows so you cannot
create <command>SELECT</command> triggers. Rules and views are more
appropriate in such cases.
create <command>SELECT</command> triggers. Rules and views may provide
workable solutions to problems that seem to need <command>SELECT</command>
triggers.
</para>
<para>
@ -300,12 +309,9 @@ UPDATE OF <replaceable>column_name1</replaceable> [, <replaceable>column_name2</
<term><literal>REFERENCING</literal></term>
<listitem>
<para>
This immediately precedes the declaration of one or two relations which
can be used to read the before and/or after images of all rows directly
affected by the triggering statement. An <literal>AFTER EACH ROW</>
trigger is allowed to use both these transition relation names and the
row names (<literal>OLD</> and <literal>NEW</>) which reference each
individual row for which the trigger fires.
This keyword immediately precedes the declaration of one or two
relation names that provide access to the transition relations of the
triggering statement.
</para>
</listitem>
</varlistentry>
@ -315,8 +321,9 @@ UPDATE OF <replaceable>column_name1</replaceable> [, <replaceable>column_name2</
<term><literal>NEW TABLE</literal></term>
<listitem>
<para>
This specifies whether the named relation contains the before or after
images for rows affected by the statement which fired the trigger.
This clause indicates whether the following relation name is for the
before-image transition relation or the after-image transition
relation.
</para>
</listitem>
</varlistentry>
@ -325,7 +332,8 @@ UPDATE OF <replaceable>column_name1</replaceable> [, <replaceable>column_name2</
<term><replaceable class="PARAMETER">transition_relation_name</replaceable></term>
<listitem>
<para>
The (unqualified) name to be used within the trigger for this relation.
The (unqualified) name to be used within the trigger for this
transition relation.
</para>
</listitem>
</varlistentry>
@ -458,6 +466,35 @@ UPDATE OF <replaceable>column_name1</replaceable> [, <replaceable>column_name2</
rows.
</para>
<para>
In some cases it is possible for a single SQL command to fire more than
one kind of trigger. For instance an <command>INSERT</command> with
an <literal>ON CONFLICT DO UPDATE</> clause may cause both insert and
update operations, so it will fire both kinds of triggers as needed.
The transition relations supplied to triggers are
specific to their event type; thus an <command>INSERT</command> trigger
will see only the inserted rows, while an <command>UPDATE</command>
trigger will see only the updated rows.
</para>
<para>
Row updates or deletions caused by foreign-key enforcement actions, such
as <literal>ON UPDATE CASCADE</> or <literal>ON DELETE SET NULL</>, are
treated as part of the SQL command that caused them (note that such
actions are never deferred). Relevant triggers on the affected table will
be fired, so that this provides another way in which a SQL command might
fire triggers not directly matching its type. In simple cases, triggers
that request transition relations will see all changes caused in their
table by a single original SQL command as a single transition relation.
However, there are cases in which the presence of an <literal>AFTER ROW</>
trigger that requests transition relations will cause the foreign-key
enforcement actions triggered by a single SQL command to be split into
multiple steps, each with its own transition relation(s). In such cases,
any <literal>AFTER STATEMENT</> triggers that are present will be fired
once per creation of a transition relation, ensuring that the triggers see
each affected row once and only once.
</para>
<para>
Modifying a partitioned table or a table with inheritance children fires
statement-level triggers directly attached to that table, but not
@ -589,19 +626,30 @@ CREATE TRIGGER paired_items_update
<itemizedlist>
<listitem>
<para>
While transition tables for <literal>AFTER</> triggers are specified
using the <literal>REFERENCING</> clause in the standard way, the row
variables used in <literal>FOR EACH ROW</> triggers may not be
specified in <literal>REFERENCING</> clause. They are available in a
manner which is dependent on the language in which the trigger function
is written. Some languages effectively behave as though there is a
<literal>REFERENCING</> clause containing <literal>OLD ROW AS OLD NEW
ROW AS NEW</>.
While transition table names for <literal>AFTER</> triggers are
specified using the <literal>REFERENCING</> clause in the standard way,
the row variables used in <literal>FOR EACH ROW</> triggers may not be
specified in a <literal>REFERENCING</> clause. They are available in a
manner that is dependent on the language in which the trigger function
is written, but is fixed for any one language. Some languages
effectively behave as though there is a <literal>REFERENCING</> clause
containing <literal>OLD ROW AS OLD NEW ROW AS NEW</>.
</para>
</listitem>
<listitem>
<para><productname>PostgreSQL</productname> only allows the execution
<para>
The standard allows transition tables to be used with
column-specific <literal>UPDATE</> triggers, but then the set of rows
that should be visible in the transition tables depends on the
trigger's column list. This is not currently implemented by
<productname>PostgreSQL</productname>.
</para>
</listitem>
<listitem>
<para>
<productname>PostgreSQL</productname> only allows the execution
of a user-defined function for the triggered action. The standard
allows the execution of a number of other SQL commands, such as
<command>CREATE TABLE</command>, as the triggered action. This

View File

@ -41,17 +41,13 @@
On tables and foreign tables, 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 an
<command>INSERT</command> contains an <literal>ON CONFLICT DO UPDATE</>
clause, it is possible that the effects of a BEFORE insert trigger and
a BEFORE update trigger can both be applied together, if a reference to
an <varname>EXCLUDED</> column appears. <command>UPDATE</command>
triggers can moreover be set to fire only if certain columns are
mentioned in the <literal>SET</literal> clause of the
<command>UPDATE</command> statement. Triggers can also fire for
<command>TRUNCATE</command> statements. If a trigger event occurs,
or once per <acronym>SQL</acronym> statement.
<command>UPDATE</command> triggers can moreover be set to fire only if
certain columns are mentioned in the <literal>SET</literal> clause of
the <command>UPDATE</command> 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. Foreign tables do not support the TRUNCATE statement at all.
event.
</para>
<para>
@ -97,10 +93,7 @@
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. On views, triggers that fire before or
after may only be defined at statement level, while triggers that fire
instead of an <command>INSERT</command>, <command>UPDATE</command>,
or <command>DELETE</command> may only be defined at row level.
defined at statement level, not per-row.
</para>
<para>
@ -117,9 +110,9 @@
operated on, while row-level <literal>AFTER</> triggers fire at the end of
the statement (but before any statement-level <literal>AFTER</> triggers).
These types of triggers may only be defined on non-partitioned tables and
foreign tables. Row-level <literal>INSTEAD OF</> triggers may only be
defined on views, and fire immediately as each row in the view is
identified as needing to be operated on.
foreign tables, not views. <literal>INSTEAD OF</> triggers may only be
defined on views, and only at row level; they fire immediately as each
row in the view is identified as needing to be operated on.
</para>
<para>
@ -132,18 +125,19 @@
<para>
If an <command>INSERT</command> contains an <literal>ON CONFLICT
DO UPDATE</> clause, it is possible that the effects of all
row-level <literal>BEFORE</> <command>INSERT</command> triggers
and all row-level <literal>BEFORE</literal> <command>UPDATE</command> triggers can
DO UPDATE</> clause, it is possible that the effects of
row-level <literal>BEFORE</> <command>INSERT</command> triggers and
row-level <literal>BEFORE</literal> <command>UPDATE</command> triggers can
both be applied in a way that is apparent from the final state of
the updated row, if an <varname>EXCLUDED</> column is referenced.
There need not be an <varname>EXCLUDED</> column reference for
both sets of row-level <literal>BEFORE</literal> triggers to execute, though. The
both sets of row-level <literal>BEFORE</literal> triggers to execute,
though. The
possibility of surprising outcomes should be considered when there
are both <literal>BEFORE</> <command>INSERT</command> and
<literal>BEFORE</> <command>UPDATE</command> row-level triggers
that both affect a row being inserted/updated (this can still be
problematic if the modifications are more or less equivalent if
that change a row being inserted/updated (this can be
problematic even if the modifications are more or less equivalent, if
they're not also idempotent). Note that statement-level
<command>UPDATE</command> triggers are executed when <literal>ON
CONFLICT DO UPDATE</> is specified, regardless of whether or not
@ -314,8 +308,18 @@
<varname>NEW</varname> row for <command>INSERT</command> and
<command>UPDATE</command> triggers, and/or the <varname>OLD</varname> row
for <command>UPDATE</command> and <command>DELETE</command> triggers.
Statement-level triggers do not currently have any way to examine the
individual row(s) modified by the statement.
</para>
<para>
By default, statement-level triggers do not have any way to examine the
individual row(s) modified by the statement. But an <literal>AFTER
STATEMENT</> trigger can request that <firstterm>transition tables</>
be created to make the sets of affected rows available to the trigger.
<literal>AFTER ROW</> triggers can also request transition tables, so
that they can see the total changes in the table as well as the change in
the individual row they are currently being fired for. The syntax for
examining the transition tables again depends on the programming language
that is being used.
</para>
</sect1>

View File

@ -2432,12 +2432,17 @@ CopyFrom(CopyState cstate)
/* Triggers might need a slot as well */
estate->es_trig_tuple_slot = ExecInitExtraTupleSlot(estate);
/* Prepare to catch AFTER triggers. */
AfterTriggerBeginQuery();
/*
* If there are any triggers with transition tables on the named relation,
* we need to be prepared to capture transition tuples.
*/
cstate->transition_capture =
MakeTransitionCaptureState(cstate->rel->trigdesc);
MakeTransitionCaptureState(cstate->rel->trigdesc,
RelationGetRelid(cstate->rel),
CMD_INSERT);
/*
* If the named relation is a partitioned table, initialize state for
@ -2513,9 +2518,6 @@ CopyFrom(CopyState cstate)
bufferedTuples = palloc(MAX_BUFFERED_TUPLES * sizeof(HeapTuple));
}
/* Prepare to catch AFTER triggers. */
AfterTriggerBeginQuery();
/*
* Check BEFORE STATEMENT insertion triggers. It's debatable whether we
* should do this for COPY, since it's not really an "INSERT" statement as

File diff suppressed because it is too large Load Diff

View File

@ -241,11 +241,11 @@ This is a sketch of control flow for full query processing:
CreateExecutorState
creates per-query context
switch to per-query context to run ExecInitNode
AfterTriggerBeginQuery
ExecInitNode --- recursively scans plan tree
CreateExprContext
creates per-tuple context
ExecInitExpr
AfterTriggerBeginQuery
ExecutorRun
ExecProcNode --- recursively called in per-query context

View File

@ -251,11 +251,6 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags)
estate->es_top_eflags = eflags;
estate->es_instrument = queryDesc->instrument_options;
/*
* Initialize the plan state tree
*/
InitPlan(queryDesc, eflags);
/*
* Set up an AFTER-trigger statement context, unless told not to, or
* unless it's EXPLAIN-only mode (when ExecutorFinish won't be called).
@ -263,6 +258,11 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags)
if (!(eflags & (EXEC_FLAG_SKIP_TRIGGERS | EXEC_FLAG_EXPLAIN_ONLY)))
AfterTriggerBeginQuery();
/*
* Initialize the plan state tree
*/
InitPlan(queryDesc, eflags);
MemoryContextSwitchTo(oldcontext);
}
@ -1174,6 +1174,7 @@ CheckValidResultRel(ResultRelInfo *resultRelInfo, CmdType operation)
switch (operation)
{
case CMD_INSERT:
/*
* If foreign partition to do tuple-routing for, skip the
* check; it's disallowed elsewhere.

View File

@ -343,6 +343,9 @@ ExecInsert(ModifyTableState *mtstate,
mtstate->mt_transition_capture->tcs_map = NULL;
}
}
if (mtstate->mt_oc_transition_capture != NULL)
mtstate->mt_oc_transition_capture->tcs_map =
mtstate->mt_transition_tupconv_maps[leaf_part_index];
/*
* We might need to convert from the parent rowtype to the partition
@ -1158,6 +1161,8 @@ lreplace:;
/* AFTER ROW UPDATE Triggers */
ExecARUpdateTriggers(estate, resultRelInfo, tupleid, oldtuple, tuple,
recheckIndexes,
mtstate->operation == CMD_INSERT ?
mtstate->mt_oc_transition_capture :
mtstate->mt_transition_capture);
list_free(recheckIndexes);
@ -1444,7 +1449,7 @@ fireASTriggers(ModifyTableState *node)
if (node->mt_onconflict == ONCONFLICT_UPDATE)
ExecASUpdateTriggers(node->ps.state,
resultRelInfo,
node->mt_transition_capture);
node->mt_oc_transition_capture);
ExecASInsertTriggers(node->ps.state, resultRelInfo,
node->mt_transition_capture);
break;
@ -1474,14 +1479,24 @@ ExecSetupTransitionCaptureState(ModifyTableState *mtstate, EState *estate)
/* Check for transition tables on the directly targeted relation. */
mtstate->mt_transition_capture =
MakeTransitionCaptureState(targetRelInfo->ri_TrigDesc);
MakeTransitionCaptureState(targetRelInfo->ri_TrigDesc,
RelationGetRelid(targetRelInfo->ri_RelationDesc),
mtstate->operation);
if (mtstate->operation == CMD_INSERT &&
mtstate->mt_onconflict == ONCONFLICT_UPDATE)
mtstate->mt_oc_transition_capture =
MakeTransitionCaptureState(targetRelInfo->ri_TrigDesc,
RelationGetRelid(targetRelInfo->ri_RelationDesc),
CMD_UPDATE);
/*
* If we found that we need to collect transition tuples then we may also
* need tuple conversion maps for any children that have TupleDescs that
* aren't compatible with the tuplestores.
* aren't compatible with the tuplestores. (We can share these maps
* between the regular and ON CONFLICT cases.)
*/
if (mtstate->mt_transition_capture != NULL)
if (mtstate->mt_transition_capture != NULL ||
mtstate->mt_oc_transition_capture != NULL)
{
ResultRelInfo *resultRelInfos;
int numResultRelInfos;
@ -1522,10 +1537,12 @@ ExecSetupTransitionCaptureState(ModifyTableState *mtstate, EState *estate)
/*
* Install the conversion map for the first plan for UPDATE and DELETE
* operations. It will be advanced each time we switch to the next
* plan. (INSERT operations set it every time.)
* plan. (INSERT operations set it every time, so we need not update
* mtstate->mt_oc_transition_capture here.)
*/
mtstate->mt_transition_capture->tcs_map =
mtstate->mt_transition_tupconv_maps[0];
if (mtstate->mt_transition_capture)
mtstate->mt_transition_capture->tcs_map =
mtstate->mt_transition_tupconv_maps[0];
}
}
@ -1629,13 +1646,19 @@ ExecModifyTable(PlanState *pstate)
estate->es_result_relation_info = resultRelInfo;
EvalPlanQualSetPlan(&node->mt_epqstate, subplanstate->plan,
node->mt_arowmarks[node->mt_whichplan]);
/* Prepare to convert transition tuples from this child. */
if (node->mt_transition_capture != NULL)
{
/* Prepare to convert transition tuples from this child. */
Assert(node->mt_transition_tupconv_maps != NULL);
node->mt_transition_capture->tcs_map =
node->mt_transition_tupconv_maps[node->mt_whichplan];
}
if (node->mt_oc_transition_capture != NULL)
{
Assert(node->mt_transition_tupconv_maps != NULL);
node->mt_oc_transition_capture->tcs_map =
node->mt_transition_tupconv_maps[node->mt_whichplan];
}
continue;
}
else
@ -1934,8 +1957,12 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
mtstate->mt_partition_tuple_slot = partition_tuple_slot;
}
/* Build state for collecting transition tuples */
ExecSetupTransitionCaptureState(mtstate, estate);
/*
* Build state for collecting transition tuples. This requires having a
* valid trigger query context, so skip it in explain-only mode.
*/
if (!(eflags & EXEC_FLAG_EXPLAIN_ONLY))
ExecSetupTransitionCaptureState(mtstate, estate);
/*
* Initialize any WITH CHECK OPTION constraints if needed.
@ -2318,16 +2345,6 @@ ExecEndModifyTable(ModifyTableState *node)
{
int i;
/*
* Free transition tables, unless this query is being run in
* EXEC_FLAG_SKIP_TRIGGERS mode, which means that it may have queued AFTER
* triggers that won't be run till later. In that case we'll just leak
* the transition tables till end of (sub)transaction.
*/
if (node->mt_transition_capture != NULL &&
!(node->ps.state->es_top_eflags & EXEC_FLAG_SKIP_TRIGGERS))
DestroyTransitionCaptureState(node->mt_transition_capture);
/*
* Allow any FDWs to shut down
*/

View File

@ -43,13 +43,21 @@ typedef struct TriggerData
/*
* The state for capturing old and new tuples into transition tables for a
* single ModifyTable node.
* single ModifyTable node (or other operation source, e.g. copy.c).
*
* This is per-caller to avoid conflicts in setting tcs_map or
* tcs_original_insert_tuple. Note, however, that the pointed-to
* private data may be shared across multiple callers.
*/
struct AfterTriggersTableData; /* private in trigger.c */
typedef struct TransitionCaptureState
{
/*
* Is there at least one trigger specifying each transition relation on
* the relation explicitly named in the DML statement or COPY command?
* Note: in current usage, these flags could be part of the private state,
* but it seems possibly useful to let callers see them.
*/
bool tcs_delete_old_table;
bool tcs_update_old_table;
@ -60,7 +68,7 @@ typedef struct TransitionCaptureState
* For UPDATE and DELETE, AfterTriggerSaveEvent may need to convert the
* new and old tuples from a child table's format to the format of the
* relation named in a query so that it is compatible with the transition
* tuplestores.
* tuplestores. The caller must store the conversion map here if so.
*/
TupleConversionMap *tcs_map;
@ -74,17 +82,9 @@ typedef struct TransitionCaptureState
HeapTuple tcs_original_insert_tuple;
/*
* The tuplestores backing the transition tables. We use separate
* tuplestores for INSERT and UPDATE, because INSERT ... ON CONFLICT ...
* DO UPDATE causes INSERT and UPDATE triggers to fire and needs a way to
* keep track of the new tuple images resulting from the two cases
* separately. We only need a single old image tuplestore, because there
* is no statement that can both update and delete at the same time.
* Private data including the tuplestore(s) into which to insert tuples.
*/
Tuplestorestate *tcs_old_tuplestore; /* for DELETE and UPDATE old
* images */
Tuplestorestate *tcs_insert_tuplestore; /* for INSERT new images */
Tuplestorestate *tcs_update_tuplestore; /* for UPDATE new images */
struct AfterTriggersTableData *tcs_private;
} TransitionCaptureState;
/*
@ -174,8 +174,9 @@ extern void RelationBuildTriggers(Relation relation);
extern TriggerDesc *CopyTriggerDesc(TriggerDesc *trigdesc);
extern const char *FindTriggerIncompatibleWithInheritance(TriggerDesc *trigdesc);
extern TransitionCaptureState *MakeTransitionCaptureState(TriggerDesc *trigdesc);
extern void DestroyTransitionCaptureState(TransitionCaptureState *tcs);
extern TransitionCaptureState *MakeTransitionCaptureState(TriggerDesc *trigdesc,
Oid relid, CmdType cmdType);
extern void FreeTriggerDesc(TriggerDesc *trigdesc);

View File

@ -983,7 +983,9 @@ typedef struct ModifyTableState
/* Per partition tuple conversion map */
TupleTableSlot *mt_partition_tuple_slot;
struct TransitionCaptureState *mt_transition_capture;
/* controls transition table population */
/* controls transition table population for specified operation */
struct TransitionCaptureState *mt_oc_transition_capture;
/* controls transition table population for INSERT...ON CONFLICT UPDATE */
TupleConversionMap **mt_transition_tupconv_maps;
/* Per plan/partition tuple conversion */
} ModifyTableState;

View File

@ -2217,6 +2217,23 @@ with wcte as (insert into table1 values (42))
insert into table2 values ('hello world');
NOTICE: trigger = table2_trig, new table = ("hello world")
NOTICE: trigger = table1_trig, new table = (42)
with wcte as (insert into table1 values (43))
insert into table1 values (44);
NOTICE: trigger = table1_trig, new table = (43), (44)
select * from table1;
a
----
42
44
43
(3 rows)
select * from table2;
a
-------------
hello world
(1 row)
drop table table1;
drop table table2;
--
@ -2256,6 +2273,14 @@ create trigger my_table_multievent_trig
after insert or update on my_table referencing new table as new_table
for each statement execute procedure dump_insert();
ERROR: transition tables cannot be specified for triggers with more than one event
--
-- Verify that you can't create a trigger with transition tables with
-- a column list.
--
create trigger my_table_col_update_trig
after update of b on my_table referencing new table as new_table
for each statement execute procedure dump_insert();
ERROR: transition tables cannot be specified for triggers with column lists
drop table my_table;
--
-- Test firing of triggers with transition tables by foreign key cascades
@ -2299,8 +2324,7 @@ select * from trig_table;
(6 rows)
delete from refd_table where length(b) = 3;
NOTICE: trigger = trig_table_delete_trig, old table = (2,"two a"), (2,"two b")
NOTICE: trigger = trig_table_delete_trig, old table = (11,"one a"), (11,"one b")
NOTICE: trigger = trig_table_delete_trig, old table = (2,"two a"), (2,"two b"), (11,"one a"), (11,"one b")
select * from trig_table;
a | b
---+---------
@ -2309,6 +2333,30 @@ select * from trig_table;
(2 rows)
drop table refd_table, trig_table;
--
-- self-referential FKs are even more fun
--
create table self_ref (a int primary key,
b int references self_ref(a) on delete cascade);
create trigger self_ref_r_trig
after delete on self_ref referencing old table as old_table
for each row execute procedure dump_delete();
create trigger self_ref_s_trig
after delete on self_ref referencing old table as old_table
for each statement execute procedure dump_delete();
insert into self_ref values (1, null), (2, 1), (3, 2);
delete from self_ref where a = 1;
NOTICE: trigger = self_ref_r_trig, old table = (1,), (2,1)
NOTICE: trigger = self_ref_r_trig, old table = (1,), (2,1)
NOTICE: trigger = self_ref_s_trig, old table = (1,), (2,1)
NOTICE: trigger = self_ref_r_trig, old table = (3,2)
NOTICE: trigger = self_ref_s_trig, old table = (3,2)
-- without AR trigger, cascaded deletes all end up in one transition table
drop trigger self_ref_r_trig on self_ref;
insert into self_ref values (1, null), (2, 1), (3, 2), (4, 3);
delete from self_ref where a = 1;
NOTICE: trigger = self_ref_s_trig, old table = (1,), (2,1), (3,2), (4,3)
drop table self_ref;
-- cleanup
drop function dump_insert();
drop function dump_update();

View File

@ -1729,6 +1729,12 @@ create trigger table2_trig
with wcte as (insert into table1 values (42))
insert into table2 values ('hello world');
with wcte as (insert into table1 values (43))
insert into table1 values (44);
select * from table1;
select * from table2;
drop table table1;
drop table table2;
@ -1769,6 +1775,15 @@ create trigger my_table_multievent_trig
after insert or update on my_table referencing new table as new_table
for each statement execute procedure dump_insert();
--
-- Verify that you can't create a trigger with transition tables with
-- a column list.
--
create trigger my_table_col_update_trig
after update of b on my_table referencing new table as new_table
for each statement execute procedure dump_insert();
drop table my_table;
--
@ -1812,6 +1827,33 @@ select * from trig_table;
drop table refd_table, trig_table;
--
-- self-referential FKs are even more fun
--
create table self_ref (a int primary key,
b int references self_ref(a) on delete cascade);
create trigger self_ref_r_trig
after delete on self_ref referencing old table as old_table
for each row execute procedure dump_delete();
create trigger self_ref_s_trig
after delete on self_ref referencing old table as old_table
for each statement execute procedure dump_delete();
insert into self_ref values (1, null), (2, 1), (3, 2);
delete from self_ref where a = 1;
-- without AR trigger, cascaded deletes all end up in one transition table
drop trigger self_ref_r_trig on self_ref;
insert into self_ref values (1, null), (2, 1), (3, 2), (4, 3);
delete from self_ref where a = 1;
drop table self_ref;
-- cleanup
drop function dump_insert();
drop function dump_update();