We disallow row-level triggers with transition tables on child tables.
Transition tables for triggers on the parent table contain only those
columns present in the parent. (We can't mix tuple formats in a
single transition table.)
Patch by Thomas Munro
Discussion: https://postgr.es/m/CA%2BTgmoZzTBBAsEUh4MazAN7ga%3D8SsMC-Knp-6cetts9yNZUCcg%40mail.gmail.com
rows.
</para>
+ <para>
+ Modifying a partitioned table or a table with inheritance children fires
+ statement-level triggers directly attached to that table, but not
+ statement-level triggers for its partitions or child tables. In contrast,
+ row-level triggers are fired for all affected partitions or child tables.
+ If a statement-level trigger has been defined with transition relations
+ named by a <literal>REFERENCING</literal> clause, then before and after
+ images of rows are visible from all affected partitions or child tables.
+ In the case of inheritance children, the row images include only columns
+ that are present in the table that the trigger is attached to. Currently,
+ row-level triggers with transition relations cannot be defined on
+ partitions or inheritance child tables.
+ </para>
+
<para>
In <productname>PostgreSQL</productname> versions before 7.3, it was
necessary to declare trigger functions as returning the placeholder
return result;
}
+/*
+ * has_superclass - does this relation inherit from another? The caller
+ * should hold a lock on the given relation so that it can't be concurrently
+ * added to or removed from an inheritance hierarchy.
+ */
+bool
+has_superclass(Oid relationId)
+{
+ Relation catalog;
+ SysScanDesc scan;
+ ScanKeyData skey;
+ bool result;
+
+ catalog = heap_open(InheritsRelationId, AccessShareLock);
+ ScanKeyInit(&skey, Anum_pg_inherits_inhrelid, BTEqualStrategyNumber,
+ F_OIDEQ, ObjectIdGetDatum(relationId));
+ scan = systable_beginscan(catalog, InheritsRelidSeqnoIndexId, true,
+ NULL, 1, &skey);
+ result = HeapTupleIsValid(systable_getnext(scan));
+ systable_endscan(scan);
+ heap_close(catalog, AccessShareLock);
+
+ return result;
+}
/*
* Given two type OIDs, determine whether the first is a complex type
ResultRelInfo *partitions; /* Per partition result relation */
TupleConversionMap **partition_tupconv_maps;
TupleTableSlot *partition_tuple_slot;
+ TransitionCaptureState *transition_capture;
+ TupleConversionMap **transition_tupconv_maps;
/*
* These variables are used to reduce overhead in textual COPY FROM.
cstate->num_partitions = num_partitions;
cstate->partition_tupconv_maps = partition_tupconv_maps;
cstate->partition_tuple_slot = partition_tuple_slot;
+
+ /*
+ * If there are any triggers with transition tables on the named
+ * relation, we need to be prepared to capture transition tuples
+ * from child relations too.
+ */
+ cstate->transition_capture =
+ MakeTransitionCaptureState(rel->trigdesc);
+
+ /*
+ * If we are capturing transition tuples, they may need to be
+ * converted from partition format back to partitioned table
+ * format (this is only ever necessary if a BEFORE trigger
+ * modifies the tuple).
+ */
+ if (cstate->transition_capture != NULL)
+ {
+ int i;
+
+ cstate->transition_tupconv_maps = (TupleConversionMap **)
+ palloc0(sizeof(TupleConversionMap *) *
+ cstate->num_partitions);
+ for (i = 0; i < cstate->num_partitions; ++i)
+ {
+ cstate->transition_tupconv_maps[i] =
+ convert_tuples_by_name(RelationGetDescr(cstate->partitions[i].ri_RelationDesc),
+ RelationGetDescr(rel),
+ gettext_noop("could not convert row type"));
+ }
+ }
}
}
else
*/
estate->es_result_relation_info = resultRelInfo;
+ /*
+ * If we're capturing transition tuples, we might need to convert
+ * from the partition rowtype to parent rowtype.
+ */
+ if (cstate->transition_capture != NULL)
+ {
+ if (resultRelInfo->ri_TrigDesc &&
+ (resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
+ resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
+ {
+ /*
+ * If there are any BEFORE or INSTEAD triggers on the
+ * partition, we'll have to be ready to convert their
+ * result back to tuplestore format.
+ */
+ cstate->transition_capture->tcs_original_insert_tuple = NULL;
+ cstate->transition_capture->tcs_map =
+ cstate->transition_tupconv_maps[leaf_part_index];
+ }
+ else
+ {
+ /*
+ * Otherwise, just remember the original unconverted
+ * tuple, to avoid a needless round trip conversion.
+ */
+ cstate->transition_capture->tcs_original_insert_tuple = tuple;
+ cstate->transition_capture->tcs_map = NULL;
+ }
+ }
/*
* We might need to convert from the parent rowtype to the
* partition rowtype.
/* AFTER ROW INSERT Triggers */
ExecARInsertTriggers(estate, resultRelInfo, tuple,
- recheckIndexes);
+ recheckIndexes, cstate->transition_capture);
list_free(recheckIndexes);
}
estate, false, NULL, NIL);
ExecARInsertTriggers(estate, resultRelInfo,
bufferedTuples[i],
- recheckIndexes);
+ recheckIndexes, NULL);
list_free(recheckIndexes);
}
}
* anyway.
*/
else if (resultRelInfo->ri_TrigDesc != NULL &&
- resultRelInfo->ri_TrigDesc->trig_insert_after_row)
+ (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+ resultRelInfo->ri_TrigDesc->trig_insert_new_table))
{
for (i = 0; i < nBufferedTuples; i++)
{
cstate->cur_lineno = firstBufferedLineNo + i;
ExecARInsertTriggers(estate, resultRelInfo,
bufferedTuples[i],
- NIL);
+ NIL, NULL);
}
}
Relation parent_rel;
List *children;
ObjectAddress address;
+ const char *trigger_name;
/*
* A self-exclusive lock is needed here. See the similar case in
RelationGetRelationName(child_rel),
RelationGetRelationName(parent_rel))));
+ /*
+ * If child_rel has row-level triggers with transition tables, we
+ * currently don't allow it to become an inheritance child. See also
+ * prohibitions in ATExecAttachPartition() and CreateTrigger().
+ */
+ trigger_name = FindTriggerIncompatibleWithInheritance(child_rel->trigdesc);
+ if (trigger_name != NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("trigger \"%s\" prevents table \"%s\" from becoming an inheritance child",
+ trigger_name, RelationGetRelationName(child_rel)),
+ errdetail("ROW triggers with transition tables are not supported in inheritance hierarchies")));
+
/* OK to create inheritance */
CreateInheritance(child_rel, parent_rel);
TupleDesc tupleDesc;
bool skip_validate = false;
ObjectAddress address;
+ const char *trigger_name;
attachRel = heap_openrv(cmd->name, AccessExclusiveLock);
errdetail("New partition should contain only the columns present in parent.")));
}
+ /*
+ * If child_rel has row-level triggers with transition tables, we
+ * currently don't allow it to become a partition. See also prohibitions
+ * in ATExecAddInherit() and CreateTrigger().
+ */
+ trigger_name = FindTriggerIncompatibleWithInheritance(attachRel->trigdesc);
+ if (trigger_name != NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("trigger \"%s\" prevents table \"%s\" from becoming a partition",
+ trigger_name, RelationGetRelationName(attachRel)),
+ errdetail("ROW triggers with transition tables are not supported on partitions")));
+
/* OK to create inheritance. Rest of the checks performed there */
CreateInheritance(attachRel, rel);
#include "catalog/objectaccess.h"
#include "catalog/pg_constraint.h"
#include "catalog/pg_constraint_fn.h"
+#include "catalog/pg_inherits_fn.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
static void AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
int event, bool row_trigger,
HeapTuple oldtup, HeapTuple newtup,
- List *recheckIndexes, Bitmapset *modifiedCols);
+ List *recheckIndexes, Bitmapset *modifiedCols,
+ TransitionCaptureState *transition_capture);
static void AfterTriggerEnlargeQueryState(void);
* adjustments will be needed below.
*/
- if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
- ereport(ERROR,
- (errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("\"%s\" is a partitioned table",
- RelationGetRelationName(rel)),
- errdetail("Triggers on partitioned tables cannot have transition tables.")));
-
if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
RelationGetRelationName(rel)),
errdetail("Triggers on views cannot have transition tables.")));
+ /*
+ * We currently don't allow row-level triggers with transition
+ * tables on partition or inheritance children. Such triggers
+ * would somehow need to see tuples converted to the format of the
+ * table they're attached to, and it's not clear which subset of
+ * tuples each child should see. See also the prohibitions in
+ * ATExecAttachPartition() and ATExecAddInherit().
+ */
+ if (TRIGGER_FOR_ROW(tgtype) && has_superclass(rel->rd_id))
+ {
+ /* Use appropriate error message. */
+ if (rel->rd_rel->relispartition)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("ROW triggers with transition tables are not supported on partitions")));
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("ROW triggers with transition tables are not supported on inheritance children")));
+ }
+
if (stmt->timing != TRIGGER_TYPE_AFTER)
ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
}
#endif /* NOT_USED */
+/*
+ * Check if there is a row-level trigger with transition tables that prevents
+ * a table from becoming an inheritance child or partition. Return the name
+ * of the first such incompatible trigger, or NULL if there is none.
+ */
+const char *
+FindTriggerIncompatibleWithInheritance(TriggerDesc *trigdesc)
+{
+ if (trigdesc != NULL)
+ {
+ int i;
+
+ for (i = 0; i < trigdesc->numtriggers; ++i)
+ {
+ Trigger *trigger = &trigdesc->triggers[i];
+
+ if (trigger->tgoldtable != NULL || trigger->tgnewtable != NULL)
+ return trigger->tgname;
+ }
+ }
+
+ return NULL;
+}
+
+/*
+ * Make a TransitionCaptureState object from a given TriggerDesc. The
+ * resulting object holds the flags which control whether transition tuples
+ * are collected when tables are modified. This allows us to use the flags
+ * from a parent table to control the collection of transition tuples from
+ * child tables.
+ *
+ * If there are no triggers with transition tables configured for 'trigdesc',
+ * then return NULL.
+ *
+ * The resulting object can be passed to the ExecAR* functions. The caller
+ * should set tcs_map or tcs_original_insert_tuple as appropriate when dealing
+ * with child tables.
+ */
+TransitionCaptureState *
+MakeTransitionCaptureState(TriggerDesc *trigdesc)
+{
+ TransitionCaptureState *state = NULL;
+
+ if (trigdesc != NULL &&
+ (trigdesc->trig_delete_old_table || trigdesc->trig_update_old_table ||
+ trigdesc->trig_update_new_table || trigdesc->trig_insert_new_table))
+ {
+ state = (TransitionCaptureState *)
+ palloc0(sizeof(TransitionCaptureState));
+ state->tcs_delete_old_table = trigdesc->trig_delete_old_table;
+ state->tcs_update_old_table = trigdesc->trig_update_old_table;
+ state->tcs_update_new_table = trigdesc->trig_update_new_table;
+ state->tcs_insert_new_table = trigdesc->trig_insert_new_table;
+ }
+
+ return state;
+}
+
/*
* Call a trigger function.
*
if (trigdesc && trigdesc->trig_insert_after_statement)
AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
- false, NULL, NULL, NIL, NULL);
+ false, NULL, NULL, NIL, NULL, NULL);
}
TupleTableSlot *
void
ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo,
- HeapTuple trigtuple, List *recheckIndexes)
+ HeapTuple trigtuple, List *recheckIndexes,
+ TransitionCaptureState *transition_capture)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
- if (trigdesc &&
- (trigdesc->trig_insert_after_row || trigdesc->trig_insert_new_table))
+ if ((trigdesc && trigdesc->trig_insert_after_row) ||
+ (trigdesc && !transition_capture && trigdesc->trig_insert_new_table) ||
+ (transition_capture && transition_capture->tcs_insert_new_table))
AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
- true, NULL, trigtuple, recheckIndexes, NULL);
+ true, NULL, trigtuple,
+ recheckIndexes, NULL,
+ transition_capture);
}
TupleTableSlot *
if (trigdesc && trigdesc->trig_delete_after_statement)
AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
- false, NULL, NULL, NIL, NULL);
+ false, NULL, NULL, NIL, NULL, NULL);
}
bool
void
ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
ItemPointer tupleid,
- HeapTuple fdw_trigtuple)
+ HeapTuple fdw_trigtuple,
+ TransitionCaptureState *transition_capture)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
- if (trigdesc &&
- (trigdesc->trig_delete_after_row || trigdesc->trig_delete_old_table))
+ if ((trigdesc && trigdesc->trig_delete_after_row) ||
+ (trigdesc && !transition_capture && trigdesc->trig_delete_old_table) ||
+ (transition_capture && transition_capture->tcs_delete_old_table))
{
HeapTuple trigtuple;
trigtuple = fdw_trigtuple;
AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
- true, trigtuple, NULL, NIL, NULL);
+ true, trigtuple, NULL, NIL, NULL,
+ transition_capture);
if (trigtuple != fdw_trigtuple)
heap_freetuple(trigtuple);
}
if (trigdesc && trigdesc->trig_update_after_statement)
AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
false, NULL, NULL, NIL,
- GetUpdatedColumns(relinfo, estate));
+ GetUpdatedColumns(relinfo, estate),
+ NULL);
}
TupleTableSlot *
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
HeapTuple newtuple,
- List *recheckIndexes)
+ List *recheckIndexes,
+ TransitionCaptureState *transition_capture)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
- if (trigdesc && (trigdesc->trig_update_after_row ||
- trigdesc->trig_update_old_table || trigdesc->trig_update_new_table))
+ if ((trigdesc && trigdesc->trig_update_after_row) ||
+ (trigdesc && !transition_capture &&
+ (trigdesc->trig_update_old_table ||
+ trigdesc->trig_update_new_table)) ||
+ (transition_capture &&
+ (transition_capture->tcs_update_old_table ||
+ transition_capture->tcs_update_new_table)))
{
HeapTuple trigtuple;
AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
true, trigtuple, newtuple, recheckIndexes,
- GetUpdatedColumns(relinfo, estate));
+ GetUpdatedColumns(relinfo, estate),
+ transition_capture);
if (trigtuple != fdw_trigtuple)
heap_freetuple(trigtuple);
}
if (trigdesc && trigdesc->trig_truncate_after_statement)
AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_TRUNCATE,
- false, NULL, NULL, NIL, NULL);
+ false, NULL, NULL, NIL, NULL, NULL);
}
AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
int event, bool row_trigger,
HeapTuple oldtup, HeapTuple newtup,
- List *recheckIndexes, Bitmapset *modifiedCols)
+ List *recheckIndexes, Bitmapset *modifiedCols,
+ TransitionCaptureState *transition_capture)
{
Relation rel = relinfo->ri_RelationDesc;
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
*/
if (row_trigger)
{
- if ((event == TRIGGER_EVENT_DELETE &&
- trigdesc->trig_delete_old_table) ||
- (event == TRIGGER_EVENT_UPDATE &&
- trigdesc->trig_update_old_table))
+ HeapTuple original_insert_tuple = NULL;
+ TupleConversionMap *map = NULL;
+ bool delete_old_table = false;
+ bool update_old_table = false;
+ bool update_new_table = false;
+ bool insert_new_table = false;
+
+ if (transition_capture != NULL)
+ {
+ /*
+ * A TransitionCaptureState object was provided to tell us which
+ * tuples to capture based on a parent table named in a DML
+ * statement. We may be dealing with a child table with an
+ * incompatible TupleDescriptor, in which case we'll need a map to
+ * convert them. As a small optimization, we may receive the
+ * original tuple from an insertion into a partitioned table to
+ * avoid a wasteful parent->child->parent round trip.
+ */
+ delete_old_table = transition_capture->tcs_delete_old_table;
+ update_old_table = transition_capture->tcs_update_old_table;
+ update_new_table = transition_capture->tcs_update_new_table;
+ insert_new_table = transition_capture->tcs_insert_new_table;
+ map = transition_capture->tcs_map;
+ original_insert_tuple =
+ transition_capture->tcs_original_insert_tuple;
+ }
+ else if (trigdesc != NULL)
+ {
+ /*
+ * Check if we need to capture transition tuples for triggers
+ * defined on this relation directly. This case is useful for
+ * cases like execReplication.c which don't set up a
+ * TriggerCaptureState because they don't know how to work with
+ * partitions.
+ */
+ delete_old_table = trigdesc->trig_delete_old_table;
+ update_old_table = trigdesc->trig_update_old_table;
+ update_new_table = trigdesc->trig_update_new_table;
+ insert_new_table = trigdesc->trig_insert_new_table;
+ }
+
+ if ((event == TRIGGER_EVENT_DELETE && delete_old_table) ||
+ (event == TRIGGER_EVENT_UPDATE && update_old_table))
{
Tuplestorestate *old_tuplestore;
old_tuplestore =
GetTriggerTransitionTuplestore
(afterTriggers.old_tuplestores);
- tuplestore_puttuple(old_tuplestore, oldtup);
+ if (map != NULL)
+ {
+ HeapTuple converted = do_convert_tuple(oldtup, map);
+
+ tuplestore_puttuple(old_tuplestore, converted);
+ pfree(converted);
+ }
+ else
+ tuplestore_puttuple(old_tuplestore, oldtup);
}
- if ((event == TRIGGER_EVENT_INSERT &&
- trigdesc->trig_insert_new_table) ||
- (event == TRIGGER_EVENT_UPDATE &&
- trigdesc->trig_update_new_table))
+ if ((event == TRIGGER_EVENT_INSERT && insert_new_table) ||
+ (event == TRIGGER_EVENT_UPDATE && update_new_table))
{
Tuplestorestate *new_tuplestore;
new_tuplestore =
GetTriggerTransitionTuplestore
(afterTriggers.new_tuplestores);
- tuplestore_puttuple(new_tuplestore, newtup);
+ if (original_insert_tuple != NULL)
+ tuplestore_puttuple(new_tuplestore, original_insert_tuple);
+ else if (map != NULL)
+ {
+ HeapTuple converted = do_convert_tuple(newtup, map);
+
+ tuplestore_puttuple(new_tuplestore, converted);
+ pfree(converted);
+ }
+ else
+ tuplestore_puttuple(new_tuplestore, newtup);
}
/* If transition tables are the only reason we're here, return. */
- if ((event == TRIGGER_EVENT_DELETE && !trigdesc->trig_delete_after_row) ||
+ if (trigdesc == NULL ||
+ (event == TRIGGER_EVENT_DELETE && !trigdesc->trig_delete_after_row) ||
(event == TRIGGER_EVENT_INSERT && !trigdesc->trig_insert_after_row) ||
(event == TRIGGER_EVENT_UPDATE && !trigdesc->trig_update_after_row))
return;
* 'tup_conv_maps' receives an array of TupleConversionMap objects with one
* entry for every leaf partition (required to convert input tuple based
* on the root table's rowtype to a leaf partition's rowtype after tuple
- * routing is done
+ * routing is done)
* 'partition_tuple_slot' receives a standalone TupleTableSlot to be used
* to manipulate any given leaf partition's rowtype after that partition
* is chosen by tuple-routing.
/* AFTER ROW INSERT Triggers */
ExecARInsertTriggers(estate, resultRelInfo, tuple,
- recheckIndexes);
+ recheckIndexes, NULL);
list_free(recheckIndexes);
}
/* AFTER ROW UPDATE Triggers */
ExecARUpdateTriggers(estate, resultRelInfo,
&searchslot->tts_tuple->t_self,
- NULL, tuple, recheckIndexes);
+ NULL, tuple, recheckIndexes, NULL);
list_free(recheckIndexes);
}
/* AFTER ROW DELETE Triggers */
ExecARDeleteTriggers(estate, resultRelInfo,
- &searchslot->tts_tuple->t_self, NULL);
+ &searchslot->tts_tuple->t_self, NULL, NULL);
list_free(recheckIndexes);
}
/* For ExecInsertIndexTuples() to work on the partition's indexes */
estate->es_result_relation_info = resultRelInfo;
+ /*
+ * If we're capturing transition tuples, we might need to convert from
+ * the partition rowtype to parent rowtype.
+ */
+ if (mtstate->mt_transition_capture != NULL)
+ {
+ if (resultRelInfo->ri_TrigDesc &&
+ (resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
+ resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
+ {
+ /*
+ * If there are any BEFORE or INSTEAD triggers on the
+ * partition, we'll have to be ready to convert their result
+ * back to tuplestore format.
+ */
+ mtstate->mt_transition_capture->tcs_original_insert_tuple = NULL;
+ mtstate->mt_transition_capture->tcs_map =
+ mtstate->mt_transition_tupconv_maps[leaf_part_index];
+ }
+ else
+ {
+ /*
+ * Otherwise, just remember the original unconverted tuple, to
+ * avoid a needless round trip conversion.
+ */
+ mtstate->mt_transition_capture->tcs_original_insert_tuple = tuple;
+ mtstate->mt_transition_capture->tcs_map = NULL;
+ }
+ }
+
/*
* We might need to convert from the parent rowtype to the partition
* rowtype.
}
/* AFTER ROW INSERT Triggers */
- ExecARInsertTriggers(estate, resultRelInfo, tuple, recheckIndexes);
+ ExecARInsertTriggers(estate, resultRelInfo, tuple, recheckIndexes,
+ mtstate->mt_transition_capture);
list_free(recheckIndexes);
* ----------------------------------------------------------------
*/
static TupleTableSlot *
-ExecDelete(ItemPointer tupleid,
+ExecDelete(ModifyTableState *mtstate,
+ ItemPointer tupleid,
HeapTuple oldtuple,
TupleTableSlot *planSlot,
EPQState *epqstate,
(estate->es_processed)++;
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple);
+ ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple,
+ mtstate->mt_transition_capture);
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
* ----------------------------------------------------------------
*/
static TupleTableSlot *
-ExecUpdate(ItemPointer tupleid,
+ExecUpdate(ModifyTableState *mtstate,
+ ItemPointer tupleid,
HeapTuple oldtuple,
TupleTableSlot *slot,
TupleTableSlot *planSlot,
/* AFTER ROW UPDATE Triggers */
ExecARUpdateTriggers(estate, resultRelInfo, tupleid, oldtuple, tuple,
- recheckIndexes);
+ recheckIndexes,
+ mtstate->mt_transition_capture);
list_free(recheckIndexes);
*/
/* Execute UPDATE with projection */
- *returning = ExecUpdate(&tuple.t_self, NULL,
+ *returning = ExecUpdate(mtstate, &tuple.t_self, NULL,
mtstate->mt_conflproj, planSlot,
&mtstate->mt_epqstate, mtstate->ps.state,
canSetTag);
}
/*
- * Process AFTER EACH STATEMENT triggers
+ * Return the ResultRelInfo for which we will fire AFTER STATEMENT triggers.
+ * This is also the relation into whose tuple format all captured transition
+ * tuples must be converted.
*/
-static void
-fireASTriggers(ModifyTableState *node)
+static ResultRelInfo *
+getASTriggerResultRelInfo(ModifyTableState *node)
{
- ResultRelInfo *resultRelInfo = node->resultRelInfo;
-
/*
* If the node modifies a partitioned table, we must fire its triggers.
* Note that in that case, node->resultRelInfo points to the first leaf
* partition, not the root table.
*/
if (node->rootResultRelInfo != NULL)
- resultRelInfo = node->rootResultRelInfo;
+ return node->rootResultRelInfo;
+ else
+ return node->resultRelInfo;
+}
+
+/*
+ * Process AFTER EACH STATEMENT triggers
+ */
+static void
+fireASTriggers(ModifyTableState *node)
+{
+ ResultRelInfo *resultRelInfo = getASTriggerResultRelInfo(node);
switch (node->operation)
{
}
}
+/*
+ * Set up the state needed for collecting transition tuples for AFTER
+ * triggers.
+ */
+static void
+ExecSetupTransitionCaptureState(ModifyTableState *mtstate, EState *estate)
+{
+ ResultRelInfo *targetRelInfo = getASTriggerResultRelInfo(mtstate);
+ int i;
+
+ /* Check for transition tables on the directly targeted relation. */
+ mtstate->mt_transition_capture =
+ MakeTransitionCaptureState(targetRelInfo->ri_TrigDesc);
+
+ /*
+ * 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.
+ */
+ if (mtstate->mt_transition_capture != NULL)
+ {
+ ResultRelInfo *resultRelInfos;
+ int numResultRelInfos;
+
+ /* Find the set of partitions so that we can find their TupleDescs. */
+ if (mtstate->mt_partition_dispatch_info != NULL)
+ {
+ /*
+ * For INSERT via partitioned table, so we need TupleDescs based
+ * on the partition routing table.
+ */
+ resultRelInfos = mtstate->mt_partitions;
+ numResultRelInfos = mtstate->mt_num_partitions;
+ }
+ else
+ {
+ /* Otherwise we need the ResultRelInfo for each subplan. */
+ resultRelInfos = mtstate->resultRelInfo;
+ numResultRelInfos = mtstate->mt_nplans;
+ }
+
+ /*
+ * Build array of conversion maps from each child's TupleDesc to the
+ * one used in the tuplestore. The map pointers may be NULL when no
+ * conversion is necessary, which is hopefully a common case for
+ * partitions.
+ */
+ mtstate->mt_transition_tupconv_maps = (TupleConversionMap **)
+ palloc0(sizeof(TupleConversionMap *) * numResultRelInfos);
+ for (i = 0; i < numResultRelInfos; ++i)
+ {
+ mtstate->mt_transition_tupconv_maps[i] =
+ convert_tuples_by_name(RelationGetDescr(resultRelInfos[i].ri_RelationDesc),
+ RelationGetDescr(targetRelInfo->ri_RelationDesc),
+ gettext_noop("could not convert row type"));
+ }
+
+ /*
+ * 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.)
+ */
+ mtstate->mt_transition_capture->tcs_map =
+ mtstate->mt_transition_tupconv_maps[0];
+ }
+}
/* ----------------------------------------------------------------
* ExecModifyTable
estate->es_result_relation_info = resultRelInfo;
EvalPlanQualSetPlan(&node->mt_epqstate, subplanstate->plan,
node->mt_arowmarks[node->mt_whichplan]);
+ 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];
+ }
continue;
}
else
estate, node->canSetTag);
break;
case CMD_UPDATE:
- slot = ExecUpdate(tupleid, oldtuple, slot, planSlot,
+ slot = ExecUpdate(node, tupleid, oldtuple, slot, planSlot,
&node->mt_epqstate, estate, node->canSetTag);
break;
case CMD_DELETE:
- slot = ExecDelete(tupleid, oldtuple, planSlot,
+ slot = ExecDelete(node, tupleid, oldtuple, planSlot,
&node->mt_epqstate, estate, node->canSetTag);
break;
default:
mtstate->mt_partition_tuple_slot = partition_tuple_slot;
}
+ /* Build state for collecting transition tuples */
+ ExecSetupTransitionCaptureState(mtstate, estate);
+
/*
* Initialize any WITH CHECK OPTION constraints if needed.
*/
extern List *find_all_inheritors(Oid parentrelId, LOCKMODE lockmode,
List **parents);
extern bool has_subclass(Oid relationId);
+extern bool has_superclass(Oid relationId);
extern bool typeInheritsFrom(Oid subclassTypeId, Oid superclassTypeId);
#endif /* PG_INHERITS_FN_H */
Tuplestorestate *tg_newtable;
} TriggerData;
+/*
+ * Meta-data to control the capture of old and new tuples into transition
+ * tables from child tables.
+ */
+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?
+ */
+ bool tcs_delete_old_table;
+ bool tcs_update_old_table;
+ bool tcs_update_new_table;
+ bool tcs_insert_new_table;
+
+ /*
+ * 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.
+ */
+ TupleConversionMap *tcs_map;
+
+ /*
+ * For INSERT and COPY, it would be wasteful to convert tuples from child
+ * format to parent format after they have already been converted in the
+ * opposite direction during routing. In that case we bypass conversion
+ * and allow the inserting code (copy.c and nodeModifyTable.c) to provide
+ * the original tuple directly.
+ */
+ HeapTuple tcs_original_insert_tuple;
+} TransitionCaptureState;
+
/*
* TriggerEvent bit flags
*
extern TriggerDesc *CopyTriggerDesc(TriggerDesc *trigdesc);
+extern const char *FindTriggerIncompatibleWithInheritance(TriggerDesc *trigdesc);
+extern TransitionCaptureState *MakeTransitionCaptureState(TriggerDesc *trigdesc);
+
extern void FreeTriggerDesc(TriggerDesc *trigdesc);
extern void ExecBSInsertTriggers(EState *estate,
extern void ExecARInsertTriggers(EState *estate,
ResultRelInfo *relinfo,
HeapTuple trigtuple,
- List *recheckIndexes);
+ List *recheckIndexes,
+ TransitionCaptureState *transition_capture);
extern TupleTableSlot *ExecIRInsertTriggers(EState *estate,
ResultRelInfo *relinfo,
TupleTableSlot *slot);
extern void ExecARDeleteTriggers(EState *estate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
- HeapTuple fdw_trigtuple);
+ HeapTuple fdw_trigtuple,
+ TransitionCaptureState *transition_capture);
extern bool ExecIRDeleteTriggers(EState *estate,
ResultRelInfo *relinfo,
HeapTuple trigtuple);
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
HeapTuple newtuple,
- List *recheckIndexes);
+ List *recheckIndexes,
+ TransitionCaptureState *transition_capture);
extern TupleTableSlot *ExecIRUpdateTriggers(EState *estate,
ResultRelInfo *relinfo,
HeapTuple trigtuple,
TupleConversionMap **mt_partition_tupconv_maps;
/* Per partition tuple conversion map */
TupleTableSlot *mt_partition_tuple_slot;
+ struct TransitionCaptureState *mt_transition_capture;
+ /* controls transition table population */
+ TupleConversionMap **mt_transition_tupconv_maps;
+ /* Per plan/partition tuple conversion */
} ModifyTableState;
/* ----------------
drop function upsert_before_func();
drop function upsert_after_func();
--
--- Verify that triggers are prevented on partitioned tables if they would
--- access row data (ROW and STATEMENT-with-transition-table)
---
-create table my_table (i int) partition by list (i);
-create table my_table_42 partition of my_table for values in (42);
-create function my_trigger_function() returns trigger as $$ begin end; $$ language plpgsql;
-create trigger my_trigger before update on my_table for each row execute procedure my_trigger_function();
-ERROR: "my_table" is a partitioned table
-DETAIL: Partitioned tables cannot have ROW triggers.
-create trigger my_trigger after update on my_table referencing old table as old_table
- for each statement execute procedure my_trigger_function();
-ERROR: "my_table" is a partitioned table
-DETAIL: Triggers on partitioned tables cannot have transition tables.
---
--- Verify that triggers are allowed on partitions
---
-create trigger my_trigger before update on my_table_42 for each row execute procedure my_trigger_function();
-drop trigger my_trigger on my_table_42;
-create trigger my_trigger after update on my_table_42 referencing old table as old_table
- for each statement execute procedure my_trigger_function();
-drop trigger my_trigger on my_table_42;
-drop function my_trigger_function();
-drop table my_table_42;
-drop table my_table;
---
-- Verify that triggers with transition tables are not allowed on
-- views
--
NOTICE: trigger on parted_stmt_trig1 BEFORE INSERT for ROW
NOTICE: trigger on parted_stmt_trig1 AFTER INSERT for ROW
drop table parted_stmt_trig, parted2_stmt_trig;
+--
+-- Test the interaction between transition tables and both kinds of
+-- inheritance. We'll dump the contents of the transition tables in a
+-- format that shows the attribute order, so that we can distinguish
+-- tuple formats (though not dropped attributes).
+--
+create or replace function dump_insert() returns trigger language plpgsql as
+$$
+ begin
+ raise notice 'trigger = %, new table = %',
+ TG_NAME,
+ (select string_agg(new_table::text, ', ' order by a) from new_table);
+ return null;
+ end;
+$$;
+create or replace function dump_update() returns trigger language plpgsql as
+$$
+ begin
+ raise notice 'trigger = %, old table = %, new table = %',
+ TG_NAME,
+ (select string_agg(old_table::text, ', ' order by a) from old_table),
+ (select string_agg(new_table::text, ', ' order by a) from new_table);
+ return null;
+ end;
+$$;
+create or replace function dump_delete() returns trigger language plpgsql as
+$$
+ begin
+ raise notice 'trigger = %, old table = %',
+ TG_NAME,
+ (select string_agg(old_table::text, ', ' order by a) from old_table);
+ return null;
+ end;
+$$;
+--
+-- Verify behavior of statement triggers on partition hierarchy with
+-- transition tables. Tuples should appear to each trigger in the
+-- format of the the relation the trigger is attached to.
+--
+-- set up a partition hierarchy with some different TupleDescriptors
+create table parent (a text, b int) partition by list (a);
+-- a child matching parent
+create table child1 partition of parent for values in ('AAA');
+-- a child with a dropped column
+create table child2 (x int, a text, b int);
+alter table child2 drop column x;
+alter table parent attach partition child2 for values in ('BBB');
+-- a child with a different column order
+create table child3 (b int, a text);
+alter table parent attach partition child3 for values in ('CCC');
+create trigger parent_insert_trig
+ after insert on parent referencing new table as new_table
+ for each statement execute procedure dump_insert();
+create trigger parent_update_trig
+ after update on parent referencing old table as old_table new table as new_table
+ for each statement execute procedure dump_update();
+create trigger parent_delete_trig
+ after delete on parent referencing old table as old_table
+ for each statement execute procedure dump_delete();
+create trigger child1_insert_trig
+ after insert on child1 referencing new table as new_table
+ for each statement execute procedure dump_insert();
+create trigger child1_update_trig
+ after update on child1 referencing old table as old_table new table as new_table
+ for each statement execute procedure dump_update();
+create trigger child1_delete_trig
+ after delete on child1 referencing old table as old_table
+ for each statement execute procedure dump_delete();
+create trigger child2_insert_trig
+ after insert on child2 referencing new table as new_table
+ for each statement execute procedure dump_insert();
+create trigger child2_update_trig
+ after update on child2 referencing old table as old_table new table as new_table
+ for each statement execute procedure dump_update();
+create trigger child2_delete_trig
+ after delete on child2 referencing old table as old_table
+ for each statement execute procedure dump_delete();
+create trigger child3_insert_trig
+ after insert on child3 referencing new table as new_table
+ for each statement execute procedure dump_insert();
+create trigger child3_update_trig
+ after update on child3 referencing old table as old_table new table as new_table
+ for each statement execute procedure dump_update();
+create trigger child3_delete_trig
+ after delete on child3 referencing old table as old_table
+ for each statement execute procedure dump_delete();
+-- insert directly into children sees respective child-format tuples
+insert into child1 values ('AAA', 42);
+NOTICE: trigger = child1_insert_trig, new table = (AAA,42)
+insert into child2 values ('BBB', 42);
+NOTICE: trigger = child2_insert_trig, new table = (BBB,42)
+insert into child3 values (42, 'CCC');
+NOTICE: trigger = child3_insert_trig, new table = (42,CCC)
+-- update via parent sees parent-format tuples
+update parent set b = b + 1;
+NOTICE: trigger = parent_update_trig, old table = (AAA,42), (BBB,42), (CCC,42), new table = (AAA,43), (BBB,43), (CCC,43)
+-- delete via parent sees parent-format tuples
+delete from parent;
+NOTICE: trigger = parent_delete_trig, old table = (AAA,43), (BBB,43), (CCC,43)
+-- insert into parent sees parent-format tuples
+insert into parent values ('AAA', 42);
+NOTICE: trigger = parent_insert_trig, new table = (AAA,42)
+insert into parent values ('BBB', 42);
+NOTICE: trigger = parent_insert_trig, new table = (BBB,42)
+insert into parent values ('CCC', 42);
+NOTICE: trigger = parent_insert_trig, new table = (CCC,42)
+-- delete from children sees respective child-format tuples
+delete from child1;
+NOTICE: trigger = child1_delete_trig, old table = (AAA,42)
+delete from child2;
+NOTICE: trigger = child2_delete_trig, old table = (BBB,42)
+delete from child3;
+NOTICE: trigger = child3_delete_trig, old table = (42,CCC)
+-- copy into parent sees parent-format tuples
+copy parent (a, b) from stdin;
+NOTICE: trigger = parent_insert_trig, new table = (AAA,42), (BBB,42), (CCC,42)
+-- DML affecting parent sees tuples collected from children even if
+-- there is no transition table trigger on the children
+drop trigger child1_insert_trig on child1;
+drop trigger child1_update_trig on child1;
+drop trigger child1_delete_trig on child1;
+drop trigger child2_insert_trig on child2;
+drop trigger child2_update_trig on child2;
+drop trigger child2_delete_trig on child2;
+drop trigger child3_insert_trig on child3;
+drop trigger child3_update_trig on child3;
+drop trigger child3_delete_trig on child3;
+delete from parent;
+NOTICE: trigger = parent_delete_trig, old table = (AAA,42), (BBB,42), (CCC,42)
+-- copy into parent sees tuples collected from children even if there
+-- is no transition-table trigger on the children
+copy parent (a, b) from stdin;
+NOTICE: trigger = parent_insert_trig, new table = (AAA,42), (BBB,42), (CCC,42)
+-- insert into parent with a before trigger on a child tuple before
+-- insertion, and we capture the newly modified row in parent format
+create or replace function intercept_insert() returns trigger language plpgsql as
+$$
+ begin
+ new.b = new.b + 1000;
+ return new;
+ end;
+$$;
+create trigger intercept_insert_child3
+ before insert on child3
+ for each row execute procedure intercept_insert();
+-- insert, parent trigger sees post-modification parent-format tuple
+insert into parent values ('AAA', 42), ('BBB', 42), ('CCC', 66);
+NOTICE: trigger = parent_insert_trig, new table = (AAA,42), (BBB,42), (CCC,1066)
+-- copy, parent trigger sees post-modification parent-format tuple
+copy parent (a, b) from stdin;
+NOTICE: trigger = parent_insert_trig, new table = (AAA,42), (BBB,42), (CCC,1234)
+drop table child1, child2, child3, parent;
+drop function intercept_insert();
+--
+-- Verify prohibition of row triggers with transition triggers on
+-- partitions
+--
+create table parent (a text, b int) partition by list (a);
+create table child partition of parent for values in ('AAA');
+-- adding row trigger with transition table fails
+create trigger child_row_trig
+ after insert on child referencing new table as new_table
+ for each row execute procedure dump_insert();
+ERROR: ROW triggers with transition tables are not supported on partitions
+-- detaching it first works
+alter table parent detach partition child;
+create trigger child_row_trig
+ after insert on child referencing new table as new_table
+ for each row execute procedure dump_insert();
+-- but now we're not allowed to reattach it
+alter table parent attach partition child for values in ('AAA');
+ERROR: trigger "child_row_trig" prevents table "child" from becoming a partition
+DETAIL: ROW triggers with transition tables are not supported on partitions
+-- drop the trigger, and now we're allowed to attach it again
+drop trigger child_row_trig on child;
+alter table parent attach partition child for values in ('AAA');
+drop table child, parent;
+--
+-- Verify behavior of statement triggers on (non-partition)
+-- inheritance hierarchy with transition tables; similar to the
+-- partition case, except there is no rerouting on insertion and child
+-- tables can have extra columns
+--
+-- set up inheritance hierarchy with different TupleDescriptors
+create table parent (a text, b int);
+-- a child matching parent
+create table child1 () inherits (parent);
+-- a child with a different column order
+create table child2 (b int, a text);
+alter table child2 inherit parent;
+-- a child with an extra column
+create table child3 (c text) inherits (parent);
+create trigger parent_insert_trig
+ after insert on parent referencing new table as new_table
+ for each statement execute procedure dump_insert();
+create trigger parent_update_trig
+ after update on parent referencing old table as old_table new table as new_table
+ for each statement execute procedure dump_update();
+create trigger parent_delete_trig
+ after delete on parent referencing old table as old_table
+ for each statement execute procedure dump_delete();
+create trigger child1_insert_trig
+ after insert on child1 referencing new table as new_table
+ for each statement execute procedure dump_insert();
+create trigger child1_update_trig
+ after update on child1 referencing old table as old_table new table as new_table
+ for each statement execute procedure dump_update();
+create trigger child1_delete_trig
+ after delete on child1 referencing old table as old_table
+ for each statement execute procedure dump_delete();
+create trigger child2_insert_trig
+ after insert on child2 referencing new table as new_table
+ for each statement execute procedure dump_insert();
+create trigger child2_update_trig
+ after update on child2 referencing old table as old_table new table as new_table
+ for each statement execute procedure dump_update();
+create trigger child2_delete_trig
+ after delete on child2 referencing old table as old_table
+ for each statement execute procedure dump_delete();
+create trigger child3_insert_trig
+ after insert on child3 referencing new table as new_table
+ for each statement execute procedure dump_insert();
+create trigger child3_update_trig
+ after update on child3 referencing old table as old_table new table as new_table
+ for each statement execute procedure dump_update();
+create trigger child3_delete_trig
+ after delete on child3 referencing old table as old_table
+ for each statement execute procedure dump_delete();
+-- insert directly into children sees respective child-format tuples
+insert into child1 values ('AAA', 42);
+NOTICE: trigger = child1_insert_trig, new table = (AAA,42)
+insert into child2 values (42, 'BBB');
+NOTICE: trigger = child2_insert_trig, new table = (42,BBB)
+insert into child3 values ('CCC', 42, 'foo');
+NOTICE: trigger = child3_insert_trig, new table = (CCC,42,foo)
+-- update via parent sees parent-format tuples
+update parent set b = b + 1;
+NOTICE: trigger = parent_update_trig, old table = (AAA,42), (BBB,42), (CCC,42), new table = (AAA,43), (BBB,43), (CCC,43)
+-- delete via parent sees parent-format tuples
+delete from parent;
+NOTICE: trigger = parent_delete_trig, old table = (AAA,43), (BBB,43), (CCC,43)
+-- reinsert values into children for next test...
+insert into child1 values ('AAA', 42);
+NOTICE: trigger = child1_insert_trig, new table = (AAA,42)
+insert into child2 values (42, 'BBB');
+NOTICE: trigger = child2_insert_trig, new table = (42,BBB)
+insert into child3 values ('CCC', 42, 'foo');
+NOTICE: trigger = child3_insert_trig, new table = (CCC,42,foo)
+-- delete from children sees respective child-format tuples
+delete from child1;
+NOTICE: trigger = child1_delete_trig, old table = (AAA,42)
+delete from child2;
+NOTICE: trigger = child2_delete_trig, old table = (42,BBB)
+delete from child3;
+NOTICE: trigger = child3_delete_trig, old table = (CCC,42,foo)
+-- copy into parent sees parent-format tuples (no rerouting, so these
+-- are really inserted into the parent)
+copy parent (a, b) from stdin;
+NOTICE: trigger = parent_insert_trig, new table = (AAA,42), (BBB,42), (CCC,42)
+-- DML affecting parent sees tuples collected from children even if
+-- there is no transition table trigger on the children
+drop trigger child1_insert_trig on child1;
+drop trigger child1_update_trig on child1;
+drop trigger child1_delete_trig on child1;
+drop trigger child2_insert_trig on child2;
+drop trigger child2_update_trig on child2;
+drop trigger child2_delete_trig on child2;
+drop trigger child3_insert_trig on child3;
+drop trigger child3_update_trig on child3;
+drop trigger child3_delete_trig on child3;
+delete from parent;
+NOTICE: trigger = parent_delete_trig, old table = (AAA,42), (BBB,42), (CCC,42)
+drop table child1, child2, child3, parent;
+--
+-- Verify prohibition of row triggers with transition triggers on
+-- inheritance children
+--
+create table parent (a text, b int);
+create table child () inherits (parent);
+-- adding row trigger with transition table fails
+create trigger child_row_trig
+ after insert on child referencing new table as new_table
+ for each row execute procedure dump_insert();
+ERROR: ROW triggers with transition tables are not supported on inheritance children
+-- disinheriting it first works
+alter table child no inherit parent;
+create trigger child_row_trig
+ after insert on child referencing new table as new_table
+ for each row execute procedure dump_insert();
+-- but now we're not allowed to make it inherit anymore
+alter table child inherit parent;
+ERROR: trigger "child_row_trig" prevents table "child" from becoming an inheritance child
+DETAIL: ROW triggers with transition tables are not supported in inheritance hierarchies
+-- drop the trigger, and now we're allowed to make it inherit again
+drop trigger child_row_trig on child;
+alter table child inherit parent;
+drop table child, parent;
+-- cleanup
+drop function dump_insert();
+drop function dump_update();
+drop function dump_delete();
drop function upsert_before_func();
drop function upsert_after_func();
---
--- Verify that triggers are prevented on partitioned tables if they would
--- access row data (ROW and STATEMENT-with-transition-table)
---
-
-create table my_table (i int) partition by list (i);
-create table my_table_42 partition of my_table for values in (42);
-create function my_trigger_function() returns trigger as $$ begin end; $$ language plpgsql;
-create trigger my_trigger before update on my_table for each row execute procedure my_trigger_function();
-create trigger my_trigger after update on my_table referencing old table as old_table
- for each statement execute procedure my_trigger_function();
-
---
--- Verify that triggers are allowed on partitions
---
-create trigger my_trigger before update on my_table_42 for each row execute procedure my_trigger_function();
-drop trigger my_trigger on my_table_42;
-create trigger my_trigger after update on my_table_42 referencing old table as old_table
- for each statement execute procedure my_trigger_function();
-drop trigger my_trigger on my_table_42;
-drop function my_trigger_function();
-drop table my_table_42;
-drop table my_table;
-
--
-- Verify that triggers with transition tables are not allowed on
-- views
\.
drop table parted_stmt_trig, parted2_stmt_trig;
+
+--
+-- Test the interaction between transition tables and both kinds of
+-- inheritance. We'll dump the contents of the transition tables in a
+-- format that shows the attribute order, so that we can distinguish
+-- tuple formats (though not dropped attributes).
+--
+
+create or replace function dump_insert() returns trigger language plpgsql as
+$$
+ begin
+ raise notice 'trigger = %, new table = %',
+ TG_NAME,
+ (select string_agg(new_table::text, ', ' order by a) from new_table);
+ return null;
+ end;
+$$;
+
+create or replace function dump_update() returns trigger language plpgsql as
+$$
+ begin
+ raise notice 'trigger = %, old table = %, new table = %',
+ TG_NAME,
+ (select string_agg(old_table::text, ', ' order by a) from old_table),
+ (select string_agg(new_table::text, ', ' order by a) from new_table);
+ return null;
+ end;
+$$;
+
+create or replace function dump_delete() returns trigger language plpgsql as
+$$
+ begin
+ raise notice 'trigger = %, old table = %',
+ TG_NAME,
+ (select string_agg(old_table::text, ', ' order by a) from old_table);
+ return null;
+ end;
+$$;
+
+--
+-- Verify behavior of statement triggers on partition hierarchy with
+-- transition tables. Tuples should appear to each trigger in the
+-- format of the the relation the trigger is attached to.
+--
+
+-- set up a partition hierarchy with some different TupleDescriptors
+create table parent (a text, b int) partition by list (a);
+
+-- a child matching parent
+create table child1 partition of parent for values in ('AAA');
+
+-- a child with a dropped column
+create table child2 (x int, a text, b int);
+alter table child2 drop column x;
+alter table parent attach partition child2 for values in ('BBB');
+
+-- a child with a different column order
+create table child3 (b int, a text);
+alter table parent attach partition child3 for values in ('CCC');
+
+create trigger parent_insert_trig
+ after insert on parent referencing new table as new_table
+ for each statement execute procedure dump_insert();
+create trigger parent_update_trig
+ after update on parent referencing old table as old_table new table as new_table
+ for each statement execute procedure dump_update();
+create trigger parent_delete_trig
+ after delete on parent referencing old table as old_table
+ for each statement execute procedure dump_delete();
+
+create trigger child1_insert_trig
+ after insert on child1 referencing new table as new_table
+ for each statement execute procedure dump_insert();
+create trigger child1_update_trig
+ after update on child1 referencing old table as old_table new table as new_table
+ for each statement execute procedure dump_update();
+create trigger child1_delete_trig
+ after delete on child1 referencing old table as old_table
+ for each statement execute procedure dump_delete();
+
+create trigger child2_insert_trig
+ after insert on child2 referencing new table as new_table
+ for each statement execute procedure dump_insert();
+create trigger child2_update_trig
+ after update on child2 referencing old table as old_table new table as new_table
+ for each statement execute procedure dump_update();
+create trigger child2_delete_trig
+ after delete on child2 referencing old table as old_table
+ for each statement execute procedure dump_delete();
+
+create trigger child3_insert_trig
+ after insert on child3 referencing new table as new_table
+ for each statement execute procedure dump_insert();
+create trigger child3_update_trig
+ after update on child3 referencing old table as old_table new table as new_table
+ for each statement execute procedure dump_update();
+create trigger child3_delete_trig
+ after delete on child3 referencing old table as old_table
+ for each statement execute procedure dump_delete();
+
+-- insert directly into children sees respective child-format tuples
+insert into child1 values ('AAA', 42);
+insert into child2 values ('BBB', 42);
+insert into child3 values (42, 'CCC');
+
+-- update via parent sees parent-format tuples
+update parent set b = b + 1;
+
+-- delete via parent sees parent-format tuples
+delete from parent;
+
+-- insert into parent sees parent-format tuples
+insert into parent values ('AAA', 42);
+insert into parent values ('BBB', 42);
+insert into parent values ('CCC', 42);
+
+-- delete from children sees respective child-format tuples
+delete from child1;
+delete from child2;
+delete from child3;
+
+-- copy into parent sees parent-format tuples
+copy parent (a, b) from stdin;
+AAA 42
+BBB 42
+CCC 42
+\.
+
+-- DML affecting parent sees tuples collected from children even if
+-- there is no transition table trigger on the children
+drop trigger child1_insert_trig on child1;
+drop trigger child1_update_trig on child1;
+drop trigger child1_delete_trig on child1;
+drop trigger child2_insert_trig on child2;
+drop trigger child2_update_trig on child2;
+drop trigger child2_delete_trig on child2;
+drop trigger child3_insert_trig on child3;
+drop trigger child3_update_trig on child3;
+drop trigger child3_delete_trig on child3;
+delete from parent;
+
+-- copy into parent sees tuples collected from children even if there
+-- is no transition-table trigger on the children
+copy parent (a, b) from stdin;
+AAA 42
+BBB 42
+CCC 42
+\.
+
+-- insert into parent with a before trigger on a child tuple before
+-- insertion, and we capture the newly modified row in parent format
+create or replace function intercept_insert() returns trigger language plpgsql as
+$$
+ begin
+ new.b = new.b + 1000;
+ return new;
+ end;
+$$;
+
+create trigger intercept_insert_child3
+ before insert on child3
+ for each row execute procedure intercept_insert();
+
+
+-- insert, parent trigger sees post-modification parent-format tuple
+insert into parent values ('AAA', 42), ('BBB', 42), ('CCC', 66);
+
+-- copy, parent trigger sees post-modification parent-format tuple
+copy parent (a, b) from stdin;
+AAA 42
+BBB 42
+CCC 234
+\.
+
+drop table child1, child2, child3, parent;
+drop function intercept_insert();
+
+--
+-- Verify prohibition of row triggers with transition triggers on
+-- partitions
+--
+create table parent (a text, b int) partition by list (a);
+create table child partition of parent for values in ('AAA');
+
+-- adding row trigger with transition table fails
+create trigger child_row_trig
+ after insert on child referencing new table as new_table
+ for each row execute procedure dump_insert();
+
+-- detaching it first works
+alter table parent detach partition child;
+
+create trigger child_row_trig
+ after insert on child referencing new table as new_table
+ for each row execute procedure dump_insert();
+
+-- but now we're not allowed to reattach it
+alter table parent attach partition child for values in ('AAA');
+
+-- drop the trigger, and now we're allowed to attach it again
+drop trigger child_row_trig on child;
+alter table parent attach partition child for values in ('AAA');
+
+drop table child, parent;
+
+--
+-- Verify behavior of statement triggers on (non-partition)
+-- inheritance hierarchy with transition tables; similar to the
+-- partition case, except there is no rerouting on insertion and child
+-- tables can have extra columns
+--
+
+-- set up inheritance hierarchy with different TupleDescriptors
+create table parent (a text, b int);
+
+-- a child matching parent
+create table child1 () inherits (parent);
+
+-- a child with a different column order
+create table child2 (b int, a text);
+alter table child2 inherit parent;
+
+-- a child with an extra column
+create table child3 (c text) inherits (parent);
+
+create trigger parent_insert_trig
+ after insert on parent referencing new table as new_table
+ for each statement execute procedure dump_insert();
+create trigger parent_update_trig
+ after update on parent referencing old table as old_table new table as new_table
+ for each statement execute procedure dump_update();
+create trigger parent_delete_trig
+ after delete on parent referencing old table as old_table
+ for each statement execute procedure dump_delete();
+
+create trigger child1_insert_trig
+ after insert on child1 referencing new table as new_table
+ for each statement execute procedure dump_insert();
+create trigger child1_update_trig
+ after update on child1 referencing old table as old_table new table as new_table
+ for each statement execute procedure dump_update();
+create trigger child1_delete_trig
+ after delete on child1 referencing old table as old_table
+ for each statement execute procedure dump_delete();
+
+create trigger child2_insert_trig
+ after insert on child2 referencing new table as new_table
+ for each statement execute procedure dump_insert();
+create trigger child2_update_trig
+ after update on child2 referencing old table as old_table new table as new_table
+ for each statement execute procedure dump_update();
+create trigger child2_delete_trig
+ after delete on child2 referencing old table as old_table
+ for each statement execute procedure dump_delete();
+
+create trigger child3_insert_trig
+ after insert on child3 referencing new table as new_table
+ for each statement execute procedure dump_insert();
+create trigger child3_update_trig
+ after update on child3 referencing old table as old_table new table as new_table
+ for each statement execute procedure dump_update();
+create trigger child3_delete_trig
+ after delete on child3 referencing old table as old_table
+ for each statement execute procedure dump_delete();
+
+-- insert directly into children sees respective child-format tuples
+insert into child1 values ('AAA', 42);
+insert into child2 values (42, 'BBB');
+insert into child3 values ('CCC', 42, 'foo');
+
+-- update via parent sees parent-format tuples
+update parent set b = b + 1;
+
+-- delete via parent sees parent-format tuples
+delete from parent;
+
+-- reinsert values into children for next test...
+insert into child1 values ('AAA', 42);
+insert into child2 values (42, 'BBB');
+insert into child3 values ('CCC', 42, 'foo');
+
+-- delete from children sees respective child-format tuples
+delete from child1;
+delete from child2;
+delete from child3;
+
+-- copy into parent sees parent-format tuples (no rerouting, so these
+-- are really inserted into the parent)
+copy parent (a, b) from stdin;
+AAA 42
+BBB 42
+CCC 42
+\.
+
+-- DML affecting parent sees tuples collected from children even if
+-- there is no transition table trigger on the children
+drop trigger child1_insert_trig on child1;
+drop trigger child1_update_trig on child1;
+drop trigger child1_delete_trig on child1;
+drop trigger child2_insert_trig on child2;
+drop trigger child2_update_trig on child2;
+drop trigger child2_delete_trig on child2;
+drop trigger child3_insert_trig on child3;
+drop trigger child3_update_trig on child3;
+drop trigger child3_delete_trig on child3;
+delete from parent;
+
+drop table child1, child2, child3, parent;
+
+--
+-- Verify prohibition of row triggers with transition triggers on
+-- inheritance children
+--
+create table parent (a text, b int);
+create table child () inherits (parent);
+
+-- adding row trigger with transition table fails
+create trigger child_row_trig
+ after insert on child referencing new table as new_table
+ for each row execute procedure dump_insert();
+
+-- disinheriting it first works
+alter table child no inherit parent;
+
+create trigger child_row_trig
+ after insert on child referencing new table as new_table
+ for each row execute procedure dump_insert();
+
+-- but now we're not allowed to make it inherit anymore
+alter table child inherit parent;
+
+-- drop the trigger, and now we're allowed to make it inherit again
+drop trigger child_row_trig on child;
+alter table child inherit parent;
+
+drop table child, parent;
+
+-- cleanup
+drop function dump_insert();
+drop function dump_update();
+drop function dump_delete();