diff --git a/src/backend/catalog/pg_inherits.c b/src/backend/catalog/pg_inherits.c index 04214fc2031..d59027fd07a 100644 --- a/src/backend/catalog/pg_inherits.c +++ b/src/backend/catalog/pg_inherits.c @@ -273,6 +273,28 @@ has_subclass(Oid relationId) return result; } +/* + * has_superclass - does this relation inherit from another? + */ +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 diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c index 137b1ef42d9..839db726623 100644 --- a/src/backend/commands/copy.c +++ b/src/backend/commands/copy.c @@ -171,6 +171,7 @@ typedef struct CopyStateData ResultRelInfo *partitions; /* Per partition result relation */ TupleConversionMap **partition_tupconv_maps; TupleTableSlot *partition_tuple_slot; + TriggerTransitionState *transitions; /* * These variables are used to reduce overhead in textual COPY FROM. @@ -1436,6 +1437,13 @@ BeginCopy(ParseState *pstate, 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->transitions = MakeTriggerTransitionState(rel->trigdesc); } } else @@ -2292,6 +2300,7 @@ uint64 CopyFrom(CopyState cstate) { HeapTuple tuple; + HeapTuple original_tuple = NULL; TupleDesc tupDesc; Datum *values; bool *nulls; @@ -2595,6 +2604,7 @@ CopyFrom(CopyState cstate) * We might need to convert from the parent rowtype to the * partition rowtype. */ + original_tuple = tuple; map = cstate->partition_tupconv_maps[leaf_part_index]; if (map) { @@ -2686,9 +2696,20 @@ CopyFrom(CopyState cstate) NULL, NIL); + /* + * If there are transition tables and we've routed the + * insertion to a partition, make sure that the transition + * tables (if configured) also receive the tuple in the + * named relation's format without having to convert it + * back. + */ + if (cstate->transitions != NULL) + cstate->transitions->original_insert_tuple = + original_tuple; + /* AFTER ROW INSERT Triggers */ ExecARInsertTriggers(estate, resultRelInfo, tuple, - recheckIndexes); + recheckIndexes, cstate->transitions); list_free(recheckIndexes); } @@ -2841,7 +2862,7 @@ CopyFromInsertBatch(CopyState cstate, EState *estate, CommandId mycid, estate, false, NULL, NIL); ExecARInsertTriggers(estate, resultRelInfo, bufferedTuples[i], - recheckIndexes); + recheckIndexes, NULL); list_free(recheckIndexes); } } @@ -2858,7 +2879,7 @@ CopyFromInsertBatch(CopyState cstate, EState *estate, CommandId mycid, cstate->cur_lineno = firstBufferedLineNo + i; ExecARInsertTriggers(estate, resultRelInfo, bufferedTuples[i], - NIL); + NIL, NULL); } } diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index e2593780511..eb11b3fc309 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -10978,6 +10978,7 @@ ATExecAddInherit(Relation child_rel, RangeVar *parent, LOCKMODE lockmode) Relation parent_rel; List *children; ObjectAddress address; + const char *trigger_name; /* * A self-exclusive lock is needed here. See the similar case in @@ -11059,6 +11060,19 @@ ATExecAddInherit(Relation child_rel, RangeVar *parent, LOCKMODE lockmode) 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); @@ -13430,6 +13444,7 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd) TupleDesc tupleDesc; bool skip_validate = false; ObjectAddress address; + const char *trigger_name; attachRel = heap_openrv(cmd->name, AccessExclusiveLock); @@ -13559,6 +13574,19 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd) 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); diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c index 1566fb46074..9e20f0e833d 100644 --- a/src/backend/commands/trigger.c +++ b/src/backend/commands/trigger.c @@ -24,6 +24,7 @@ #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" @@ -96,7 +97,8 @@ static HeapTuple ExecCallTriggerFunc(TriggerData *trigdata, static void AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo, int event, bool row_trigger, HeapTuple oldtup, HeapTuple newtup, - List *recheckIndexes, Bitmapset *modifiedCols); + List *recheckIndexes, Bitmapset *modifiedCols, + TriggerTransitionState *transitions); static void AfterTriggerEnlargeQueryState(void); @@ -354,13 +356,6 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString, * 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), @@ -375,6 +370,27 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString, 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 support 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), @@ -2029,6 +2045,64 @@ equalTriggerDescs(TriggerDesc *trigdesc1, TriggerDesc *trigdesc2) #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 TriggerTransitionState 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 ttf_map or original_insert_tuple as appropriate when dealing + * with child tables. + */ +TriggerTransitionState * +MakeTriggerTransitionState(TriggerDesc *trigdesc) +{ + TriggerTransitionState *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 = (TriggerTransitionState *) + palloc0(sizeof(TriggerTransitionState)); + state->ttf_delete_old_table = trigdesc->trig_delete_old_table; + state->ttf_update_old_table = trigdesc->trig_update_old_table; + state->ttf_update_new_table = trigdesc->trig_update_new_table; + state->ttf_insert_new_table = trigdesc->trig_insert_new_table; + } + + return state; +} + +/* * Call a trigger function. * * trigdata: trigger descriptor. @@ -2192,7 +2266,7 @@ ExecASInsertTriggers(EState *estate, ResultRelInfo *relinfo) 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 * @@ -2263,14 +2337,18 @@ ExecBRInsertTriggers(EState *estate, ResultRelInfo *relinfo, void ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo, - HeapTuple trigtuple, List *recheckIndexes) + HeapTuple trigtuple, List *recheckIndexes, + TriggerTransitionState *transitions) { 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 && !transitions && trigdesc->trig_insert_new_table) || + (transitions && transitions->ttf_insert_new_table)) AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT, - true, NULL, trigtuple, recheckIndexes, NULL); + true, NULL, trigtuple, + recheckIndexes, NULL, + transitions); } TupleTableSlot * @@ -2398,7 +2476,7 @@ ExecASDeleteTriggers(EState *estate, ResultRelInfo *relinfo) 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 @@ -2473,12 +2551,14 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate, void ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo, ItemPointer tupleid, - HeapTuple fdw_trigtuple) + HeapTuple fdw_trigtuple, + TriggerTransitionState *transitions) { 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 && !transitions && trigdesc->trig_delete_old_table) || + (transitions && transitions->ttf_delete_old_table)) { HeapTuple trigtuple; @@ -2494,7 +2574,8 @@ ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo, trigtuple = fdw_trigtuple; AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE, - true, trigtuple, NULL, NIL, NULL); + true, trigtuple, NULL, NIL, NULL, + transitions); if (trigtuple != fdw_trigtuple) heap_freetuple(trigtuple); } @@ -2610,7 +2691,8 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo) 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 * @@ -2735,12 +2817,16 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo, ItemPointer tupleid, HeapTuple fdw_trigtuple, HeapTuple newtuple, - List *recheckIndexes) + List *recheckIndexes, + TriggerTransitionState *transitions) { 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 && !transitions && (trigdesc->trig_update_old_table || + trigdesc->trig_update_new_table)) || + (transitions && (transitions->ttf_update_old_table || + transitions->ttf_update_new_table))) { HeapTuple trigtuple; @@ -2757,7 +2843,8 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo, AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE, true, trigtuple, newtuple, recheckIndexes, - GetUpdatedColumns(relinfo, estate)); + GetUpdatedColumns(relinfo, estate), + transitions); if (trigtuple != fdw_trigtuple) heap_freetuple(trigtuple); } @@ -2888,7 +2975,7 @@ ExecASTruncateTriggers(EState *estate, ResultRelInfo *relinfo) if (trigdesc && trigdesc->trig_truncate_after_statement) AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_TRUNCATE, - false, NULL, NULL, NIL, NULL); + false, NULL, NULL, NIL, NULL, NULL); } @@ -5090,7 +5177,8 @@ static void AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo, int event, bool row_trigger, HeapTuple oldtup, HeapTuple newtup, - List *recheckIndexes, Bitmapset *modifiedCols) + List *recheckIndexes, Bitmapset *modifiedCols, + TriggerTransitionState *transitions) { Relation rel = relinfo->ri_RelationDesc; TriggerDesc *trigdesc = relinfo->ri_TrigDesc; @@ -5120,35 +5208,89 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo, */ 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 (transitions != NULL) + { + /* + * A TriggerTransitionState 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, or we'll need the original tuple (to skip a + * wasteful parent->child->parent conversion round trip). + */ + delete_old_table = transitions->ttf_delete_old_table; + update_old_table = transitions->ttf_update_old_table; + update_new_table = transitions->ttf_update_new_table; + insert_new_table = transitions->ttf_insert_new_table; + map = transitions->ttf_map; + original_insert_tuple = transitions->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 + * TriggerTransitionState 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; Assert(oldtup != NULL); old_tuplestore = GetTriggerTransitionTuplestore - (afterTriggers.old_tuplestores); - tuplestore_puttuple(old_tuplestore, oldtup); + (afterTriggers.old_tuplestores); + 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; Assert(newtup != NULL); new_tuplestore = GetTriggerTransitionTuplestore - (afterTriggers.new_tuplestores); - tuplestore_puttuple(new_tuplestore, newtup); + (afterTriggers.new_tuplestores); + 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; diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index fb2ba3302c0..9979be65278 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -3207,7 +3207,7 @@ EvalPlanQualEnd(EPQState *epqstate) * '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. diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c index 6af8018b711..f49d1db62d6 100644 --- a/src/backend/executor/execReplication.c +++ b/src/backend/executor/execReplication.c @@ -404,7 +404,7 @@ ExecSimpleRelationInsert(EState *estate, TupleTableSlot *slot) /* AFTER ROW INSERT Triggers */ ExecARInsertTriggers(estate, resultRelInfo, tuple, - recheckIndexes); + recheckIndexes, NULL); list_free(recheckIndexes); } @@ -466,7 +466,7 @@ ExecSimpleRelationUpdate(EState *estate, EPQState *epqstate, /* AFTER ROW UPDATE Triggers */ ExecARUpdateTriggers(estate, resultRelInfo, &searchslot->tts_tuple->t_self, - NULL, tuple, recheckIndexes); + NULL, tuple, recheckIndexes, NULL); list_free(recheckIndexes); } @@ -509,7 +509,7 @@ ExecSimpleRelationDelete(EState *estate, EPQState *epqstate, /* AFTER ROW DELETE Triggers */ ExecARDeleteTriggers(estate, resultRelInfo, - &searchslot->tts_tuple->t_self, NULL); + &searchslot->tts_tuple->t_self, NULL, NULL); list_free(recheckIndexes); } diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index 652cd975996..997e95852e3 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -258,6 +258,7 @@ ExecInsert(ModifyTableState *mtstate, bool canSetTag) { HeapTuple tuple; + HeapTuple original_tuple = NULL; ResultRelInfo *resultRelInfo; ResultRelInfo *saved_resultRelInfo = NULL; Relation resultRelationDesc; @@ -317,6 +318,7 @@ ExecInsert(ModifyTableState *mtstate, * We might need to convert from the parent rowtype to the partition * rowtype. */ + original_tuple = tuple; map = mtstate->mt_partition_tupconv_maps[leaf_part_index]; if (map) { @@ -570,8 +572,17 @@ ExecInsert(ModifyTableState *mtstate, setLastTid(&(tuple->t_self)); } + /* + * If we inserted into a partitioned table, then insert routing logic may + * have converted the tuple to a partition's format. Make the original + * unconverted tuple available for transition tables. + */ + if (mtstate->mt_transition_state != NULL) + mtstate->mt_transition_state->original_insert_tuple = original_tuple; + /* AFTER ROW INSERT Triggers */ - ExecARInsertTriggers(estate, resultRelInfo, tuple, recheckIndexes); + ExecARInsertTriggers(estate, resultRelInfo, tuple, recheckIndexes, + mtstate->mt_transition_state); list_free(recheckIndexes); @@ -619,7 +630,8 @@ ExecInsert(ModifyTableState *mtstate, * ---------------------------------------------------------------- */ static TupleTableSlot * -ExecDelete(ItemPointer tupleid, +ExecDelete(ModifyTableState *mtstate, + ItemPointer tupleid, HeapTuple oldtuple, TupleTableSlot *planSlot, EPQState *epqstate, @@ -796,7 +808,8 @@ ldelete:; (estate->es_processed)++; /* AFTER ROW DELETE Triggers */ - ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple); + ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple, + mtstate->mt_transition_state); /* Process RETURNING if present */ if (resultRelInfo->ri_projectReturning) @@ -877,7 +890,8 @@ ldelete:; * ---------------------------------------------------------------- */ static TupleTableSlot * -ExecUpdate(ItemPointer tupleid, +ExecUpdate(ModifyTableState *mtstate, + ItemPointer tupleid, HeapTuple oldtuple, TupleTableSlot *slot, TupleTableSlot *planSlot, @@ -1105,7 +1119,8 @@ lreplace:; /* AFTER ROW UPDATE Triggers */ ExecARUpdateTriggers(estate, resultRelInfo, tupleid, oldtuple, tuple, - recheckIndexes); + recheckIndexes, + mtstate->mt_transition_state); list_free(recheckIndexes); @@ -1312,7 +1327,7 @@ ExecOnConflictUpdate(ModifyTableState *mtstate, */ /* 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); @@ -1394,6 +1409,72 @@ fireASTriggers(ModifyTableState *node) } } +/* + * Set up the state needed for collecting transition tuples for AFTER + * triggers. + */ +static void +ExecSetupTriggerTransitionState(ModifyTableState *mtstate, EState *estate) +{ + ResultRelInfo *targetRelInfo; + int i; + + /* + * Find the ResultRelInfo corresponding to the relation that was + * explicitly named in the statement. + */ + if (mtstate->rootResultRelInfo != NULL) + { + /* Partitioned table. */ + targetRelInfo = mtstate->rootResultRelInfo; + } + else + { + /* + * Inheritance hierarchy or single relation. The named relation is + * the first subplan. + */ + targetRelInfo = &mtstate->resultRelInfo[0]; + } + + /* Check for transition tables on the directly targeted relation. */ + mtstate->mt_transition_state = + MakeTriggerTransitionState(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. Only needed for UPDATE and + * DELETE, because INSERT already has tuples in the appropriate format. + */ + if (mtstate->mt_transition_state != NULL && + (mtstate->operation == CMD_UPDATE || mtstate->operation == CMD_DELETE)) + { + /* + * 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 *) * mtstate->mt_nplans); + for (i = 0; i < mtstate->mt_nplans; ++i) + { + mtstate->mt_transition_tupconv_maps[i] = + convert_tuples_by_name(RelationGetDescr(mtstate->resultRelInfo[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. + */ + mtstate->mt_transition_state->ttf_map = + mtstate->mt_transition_tupconv_maps[0]; + } +} /* ---------------------------------------------------------------- * ExecModifyTable @@ -1492,6 +1573,13 @@ ExecModifyTable(ModifyTableState *node) estate->es_result_relation_info = resultRelInfo; EvalPlanQualSetPlan(&node->mt_epqstate, subplanstate->plan, node->mt_arowmarks[node->mt_whichplan]); + if (node->mt_transition_state != NULL) + { + /* Prepare to convert transition tuples from this child. */ + Assert(node->mt_transition_tupconv_maps != NULL); + node->mt_transition_state->ttf_map = + node->mt_transition_tupconv_maps[node->mt_whichplan]; + } continue; } else @@ -1602,11 +1690,11 @@ ExecModifyTable(ModifyTableState *node) 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: @@ -1788,6 +1876,9 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) mtstate->mt_partition_tuple_slot = partition_tuple_slot; } + /* Build state for collecting transition tuples */ + ExecSetupTriggerTransitionState(mtstate, estate); + /* * Initialize any WITH CHECK OPTION constraints if needed. */ diff --git a/src/backend/utils/misc/queryenvironment.c b/src/backend/utils/misc/queryenvironment.c index a0b10d402bd..dfb23d14f15 100644 --- a/src/backend/utils/misc/queryenvironment.c +++ b/src/backend/utils/misc/queryenvironment.c @@ -127,7 +127,8 @@ ENRMetadataGetTupDesc(EphemeralNamedRelationMetadata enrmd) TupleDesc tupdesc; /* One, and only one, of these fields must be filled. */ - Assert((enrmd->reliddesc == InvalidOid) != (enrmd->tupdesc == NULL)); + Assert((enrmd->reliddesc == InvalidOid) != + (enrmd->tupdesc == NULL)); if (enrmd->tupdesc != NULL) tupdesc = enrmd->tupdesc; diff --git a/src/include/catalog/pg_inherits_fn.h b/src/include/catalog/pg_inherits_fn.h index e33b39e71ec..6eb623d6fbd 100644 --- a/src/include/catalog/pg_inherits_fn.h +++ b/src/include/catalog/pg_inherits_fn.h @@ -21,6 +21,7 @@ extern List *find_inheritance_children(Oid parentrelId, LOCKMODE lockmode); 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 */ diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h index d73969c8747..bd43da9cc62 100644 --- a/src/include/commands/trigger.h +++ b/src/include/commands/trigger.h @@ -42,6 +42,39 @@ typedef struct TriggerData } TriggerData; /* + * Meta-data to control the capture of old and new tuples into transition + * tables from child tables. + */ +typedef struct TriggerTransitionState +{ + /* + * Is there at least one trigger specifying each transition relation on + * the relation explicitly named in the DML statement or COPY command? + */ + bool ttf_delete_old_table; + bool ttf_update_old_table; + bool ttf_update_new_table; + bool ttf_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 *ttf_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 original_insert_tuple; +} TriggerTransitionState; + +/* * TriggerEvent bit flags * * Note that we assume different event types (INSERT/DELETE/UPDATE/TRUNCATE) @@ -127,6 +160,9 @@ extern void RelationBuildTriggers(Relation relation); extern TriggerDesc *CopyTriggerDesc(TriggerDesc *trigdesc); +extern const char *FindTriggerIncompatibleWithInheritance(TriggerDesc *trigdesc); +extern TriggerTransitionState *MakeTriggerTransitionState(TriggerDesc *trigdesc); + extern void FreeTriggerDesc(TriggerDesc *trigdesc); extern void ExecBSInsertTriggers(EState *estate, @@ -139,7 +175,8 @@ extern TupleTableSlot *ExecBRInsertTriggers(EState *estate, extern void ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo, HeapTuple trigtuple, - List *recheckIndexes); + List *recheckIndexes, + TriggerTransitionState *transitions); extern TupleTableSlot *ExecIRInsertTriggers(EState *estate, ResultRelInfo *relinfo, TupleTableSlot *slot); @@ -155,7 +192,8 @@ extern bool ExecBRDeleteTriggers(EState *estate, extern void ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo, ItemPointer tupleid, - HeapTuple fdw_trigtuple); + HeapTuple fdw_trigtuple, + TriggerTransitionState *transitions); extern bool ExecIRDeleteTriggers(EState *estate, ResultRelInfo *relinfo, HeapTuple trigtuple); @@ -174,7 +212,8 @@ extern void ExecARUpdateTriggers(EState *estate, ItemPointer tupleid, HeapTuple fdw_trigtuple, HeapTuple newtuple, - List *recheckIndexes); + List *recheckIndexes, + TriggerTransitionState *transitions); extern TupleTableSlot *ExecIRUpdateTriggers(EState *estate, ResultRelInfo *relinfo, HeapTuple trigtuple, diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index f289f3c3c25..24359cafb1a 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -947,6 +947,10 @@ typedef struct ModifyTableState TupleConversionMap **mt_partition_tupconv_maps; /* Per partition tuple conversion map */ TupleTableSlot *mt_partition_tuple_slot; + struct TriggerTransitionState *mt_transition_state; + /* controls transition table population */ + TupleConversionMap **mt_transition_tupconv_maps; + /* Per subplan tuple conversion map */ } ModifyTableState; /* ---------------- diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out index 0d560fb3eed..95da4516066 100644 --- a/src/test/regress/expected/triggers.out +++ b/src/test/regress/expected/triggers.out @@ -1764,31 +1764,6 @@ drop table upsert; 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 -- @@ -1893,3 +1868,229 @@ copy parted_stmt_trig1(a) from stdin; 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_transition_tables() 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; +$$; +-- +-- 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_stmt_trig + after insert or update or delete on parent + referencing old table as old_table new table as new_table + for each statement + execute procedure dump_transition_tables(); +create trigger child1_stmt_trig + after insert or update or delete on child1 + referencing old table as old_table new table as new_table + for each statement + execute procedure dump_transition_tables(); +create trigger child2_stmt_trig + after insert or update or delete on child2 + referencing old table as old_table new table as new_table + for each statement + execute procedure dump_transition_tables(); +create trigger child3_stmt_trig + after insert or update or delete on child3 + referencing old table as old_table new table as new_table + for each statement + execute procedure dump_transition_tables(); +-- insert directly into children sees respective child-format tuples +insert into child1 values ('AAA', 42); +NOTICE: trigger = child1_stmt_trig, old table = , new table = (AAA,42) +insert into child2 values ('BBB', 42); +NOTICE: trigger = child2_stmt_trig, old table = , new table = (BBB,42) +insert into child3 values (42, 'CCC'); +NOTICE: trigger = child3_stmt_trig, old table = , new table = (42,CCC) +-- update via parent sees parent-format tuples +update parent set b = b + 1; +NOTICE: trigger = parent_stmt_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_stmt_trig, old table = (AAA,43), (BBB,43), (CCC,43), new table = +-- insert into parent sees parent-format tuples +insert into parent values ('AAA', 42); +NOTICE: trigger = parent_stmt_trig, old table = , new table = (AAA,42) +insert into parent values ('BBB', 42); +NOTICE: trigger = parent_stmt_trig, old table = , new table = (BBB,42) +insert into parent values ('CCC', 42); +NOTICE: trigger = parent_stmt_trig, old table = , new table = (CCC,42) +-- delete from children sees respective child-format tuples +delete from child1; +NOTICE: trigger = child1_stmt_trig, old table = (AAA,42), new table = +delete from child2; +NOTICE: trigger = child2_stmt_trig, old table = (BBB,42), new table = +delete from child3; +NOTICE: trigger = child3_stmt_trig, old table = (42,CCC), new table = +-- copy into parent sees parent-format tuples +copy parent (a, b) from stdin; +NOTICE: trigger = parent_stmt_trig, old table = , 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_stmt_trig on child1; +drop trigger child2_stmt_trig on child2; +drop trigger child3_stmt_trig on child3; +delete from parent; +NOTICE: trigger = parent_stmt_trig, old table = (AAA,42), (BBB,42), (CCC,42), new table = +-- 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_stmt_trig, old table = , new table = (AAA,42), (BBB,42), (CCC,42) +drop table child1, child2, child3, parent; +-- +-- 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 or update or delete on child + referencing old table as old_table new table as new_table + for each row + execute procedure dump_transition_tables(); +ERROR: ROW triggers with transition tables are not support on partitions +-- detaching it first works +alter table parent detach partition child; +create trigger child_row_trig + after insert or update or delete on child + referencing old table as old_table new table as new_table + for each row + execute procedure dump_transition_tables(); +-- 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_stmt_trig + after insert or update or delete on parent + referencing old table as old_table new table as new_table + for each statement + execute procedure dump_transition_tables(); +create trigger child1_stmt_trig + after insert or update or delete on child1 + referencing old table as old_table new table as new_table + for each statement + execute procedure dump_transition_tables(); +create trigger child2_stmt_trig + after insert or update or delete on child2 + referencing old table as old_table new table as new_table + for each statement + execute procedure dump_transition_tables(); +create trigger child3_stmt_trig + after insert or update or delete on child3 + referencing old table as old_table new table as new_table + for each statement + execute procedure dump_transition_tables(); +-- insert directly into children sees respective child-format tuples +insert into child1 values ('AAA', 42); +NOTICE: trigger = child1_stmt_trig, old table = , new table = (AAA,42) +insert into child2 values (42, 'BBB'); +NOTICE: trigger = child2_stmt_trig, old table = , new table = (42,BBB) +insert into child3 values ('CCC', 42, 'foo'); +NOTICE: trigger = child3_stmt_trig, old table = , new table = (CCC,42,foo) +-- update via parent sees parent-format tuples +update parent set b = b + 1; +NOTICE: trigger = parent_stmt_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_stmt_trig, old table = (AAA,43), (BBB,43), (CCC,43), new table = +-- reinsert values into children for next test... +insert into child1 values ('AAA', 42); +NOTICE: trigger = child1_stmt_trig, old table = , new table = (AAA,42) +insert into child2 values (42, 'BBB'); +NOTICE: trigger = child2_stmt_trig, old table = , new table = (42,BBB) +insert into child3 values ('CCC', 42, 'foo'); +NOTICE: trigger = child3_stmt_trig, old table = , new table = (CCC,42,foo) +-- delete from children sees respective child-format tuples +delete from child1; +NOTICE: trigger = child1_stmt_trig, old table = (AAA,42), new table = +delete from child2; +NOTICE: trigger = child2_stmt_trig, old table = (42,BBB), new table = +delete from child3; +NOTICE: trigger = child3_stmt_trig, old table = (CCC,42,foo), new table = +-- 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_stmt_trig, old table = , new table = +-- DML affecting parent sees tuples collected from children even if +-- there is no transition table trigger on the children +drop trigger child1_stmt_trig on child1; +drop trigger child2_stmt_trig on child2; +drop trigger child3_stmt_trig on child3; +delete from parent; +NOTICE: trigger = parent_stmt_trig, old table = (AAA,42), (BBB,42), (CCC,42), new table = +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 or update or delete on child + referencing old table as old_table new table as new_table + for each row + execute procedure dump_transition_tables(); +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 or update or delete on child + referencing old table as old_table new table as new_table + for each row + execute procedure dump_transition_tables(); +-- 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_transition_tables(); diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql index 5581fcb1648..7675df516dc 100644 --- a/src/test/regress/sql/triggers.sql +++ b/src/test/regress/sql/triggers.sql @@ -1242,30 +1242,6 @@ 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 -- @@ -1360,3 +1336,260 @@ copy parted_stmt_trig1(a) from stdin; \. 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_transition_tables() 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; +$$; + +-- +-- 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_stmt_trig + after insert or update or delete on parent + referencing old table as old_table new table as new_table + for each statement + execute procedure dump_transition_tables(); + +create trigger child1_stmt_trig + after insert or update or delete on child1 + referencing old table as old_table new table as new_table + for each statement + execute procedure dump_transition_tables(); + +create trigger child2_stmt_trig + after insert or update or delete on child2 + referencing old table as old_table new table as new_table + for each statement + execute procedure dump_transition_tables(); + +create trigger child3_stmt_trig + after insert or update or delete on child3 + referencing old table as old_table new table as new_table + for each statement + execute procedure dump_transition_tables(); + +-- 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_stmt_trig on child1; +drop trigger child2_stmt_trig on child2; +drop trigger child3_stmt_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 +\. + +drop table child1, child2, child3, parent; + +-- +-- 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 or update or delete on child + referencing old table as old_table new table as new_table + for each row + execute procedure dump_transition_tables(); + +-- detaching it first works +alter table parent detach partition child; + +create trigger child_row_trig + after insert or update or delete on child + referencing old table as old_table new table as new_table + for each row + execute procedure dump_transition_tables(); + +-- 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_stmt_trig + after insert or update or delete on parent + referencing old table as old_table new table as new_table + for each statement + execute procedure dump_transition_tables(); + +create trigger child1_stmt_trig + after insert or update or delete on child1 + referencing old table as old_table new table as new_table + for each statement + execute procedure dump_transition_tables(); + +create trigger child2_stmt_trig + after insert or update or delete on child2 + referencing old table as old_table new table as new_table + for each statement + execute procedure dump_transition_tables(); + +create trigger child3_stmt_trig + after insert or update or delete on child3 + referencing old table as old_table new table as new_table + for each statement + execute procedure dump_transition_tables(); + +-- 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_stmt_trig on child1; +drop trigger child2_stmt_trig on child2; +drop trigger child3_stmt_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 or update or delete on child + referencing old table as old_table new table as new_table + for each row + execute procedure dump_transition_tables(); + +-- disinheriting it first works +alter table child no inherit parent; + +create trigger child_row_trig + after insert or update or delete on child + referencing old table as old_table new table as new_table + for each row + execute procedure dump_transition_tables(); + +-- 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_transition_tables();