diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c index bcaa58cae0e..1a8ea8e8888 100644 --- a/src/backend/commands/copy.c +++ b/src/backend/commands/copy.c @@ -2688,7 +2688,7 @@ CopyFrom(CopyState cstate) /* AFTER ROW INSERT Triggers */ ExecARInsertTriggers(estate, resultRelInfo, tuple, - recheckIndexes); + recheckIndexes, NULL); list_free(recheckIndexes); } @@ -2838,7 +2838,7 @@ CopyFromInsertBatch(CopyState cstate, EState *estate, CommandId mycid, estate, false, NULL, NIL); ExecARInsertTriggers(estate, resultRelInfo, bufferedTuples[i], - recheckIndexes); + recheckIndexes, NULL); list_free(recheckIndexes); } } @@ -2855,7 +2855,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/trigger.c b/src/backend/commands/trigger.c index 819395a9678..853dbf82e25 100644 --- a/src/backend/commands/trigger.c +++ b/src/backend/commands/trigger.c @@ -96,7 +96,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, + TriggerTransitionFilter *transitions); static void AfterTriggerEnlargeQueryState(void); @@ -354,13 +355,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), @@ -2029,6 +2023,87 @@ equalTriggerDescs(TriggerDesc *trigdesc1, TriggerDesc *trigdesc2) #endif /* NOT_USED */ /* + * Make a TriggerTransitionFilter object from a given TriggerDesc. The filter + * 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. Even if NULL is returned, the caller should call + * MergeTriggerTransitionFilter for the TriggerDesc of each child table to be + * modified. + * + * The resulting object can be passed to the ExecAR* functions. The caller + * should also set ttf_map as appropriate when dealing with child tables, and + * should set ttf_transition_relid. + */ +TriggerTransitionFilter * +MakeTriggerTransitionFilter(TriggerDesc *trigdesc) +{ + TriggerTransitionFilter *filter = NULL; + + if (trigdesc != NULL && + (trigdesc->trig_delete_old_table || trigdesc->trig_update_old_table || + trigdesc->trig_update_new_table || trigdesc->trig_insert_new_table)) + { + filter = (TriggerTransitionFilter *) + palloc0(sizeof(TriggerTransitionFilter)); + filter->ttf_delete_old_table = trigdesc->trig_delete_old_table; + filter->ttf_update_old_table = trigdesc->trig_update_old_table; + filter->ttf_update_new_table = trigdesc->trig_update_new_table; + filter->ttf_insert_new_table = trigdesc->trig_insert_new_table; + } + + return filter; +} + +/* + * Update a TriggerTransitionFilter to consider a given TriggerDesc. This + * should be called for each child table that is modified indirectly because + * it is part of a partition or inheritance hierarchy, to decide if there is + * any reason to collect transition tuples. + * + * Return the same TriggerTransitionFilter that was passed in, or allocate and + * return a new one if NULL was passed in. + */ +TriggerTransitionFilter * +MergeTriggerTransitionFilter(TriggerTransitionFilter *filter, + TriggerDesc *trigdesc) +{ + /* + * We only want to start collecting transition tuples if there is a + * row-level trigger and the corresponding kind of transition table on + * this child table. + * + * Note that this strategy generates a false positive if there is a + * row-level trigger AND a statement-level trigger but only the + * statement-level trigger has transition tables. TriggerDesc lacks the + * meta-data required to distinguish that case for now. + */ + if (trigdesc != NULL && + ((trigdesc->trig_delete_old_table && trigdesc->trig_delete_after_row) || + (trigdesc->trig_update_old_table && trigdesc->trig_update_after_row) || + (trigdesc->trig_update_new_table && trigdesc->trig_update_after_row) || + (trigdesc->trig_insert_new_table && trigdesc->trig_insert_after_row))) + { + /* Create on demand. */ + if (filter == NULL) + { + filter = (TriggerTransitionFilter *) + palloc0(sizeof(TriggerTransitionFilter)); + } + + /* Merge with existing flags. */ + filter->ttf_delete_old_table |= trigdesc->trig_delete_old_table; + filter->ttf_update_old_table |= trigdesc->trig_update_old_table; + filter->ttf_update_new_table |= trigdesc->trig_update_new_table; + filter->ttf_insert_new_table |= trigdesc->trig_insert_new_table; + } + + return filter; +} + +/* * Call a trigger function. * * trigdata: trigger descriptor. @@ -2192,7 +2267,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 +2338,17 @@ ExecBRInsertTriggers(EState *estate, ResultRelInfo *relinfo, void ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo, - HeapTuple trigtuple, List *recheckIndexes) + HeapTuple trigtuple, List *recheckIndexes, + TriggerTransitionFilter *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 && 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, + TriggerTransitionFilter *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 && 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, + TriggerTransitionFilter *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 && (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); } @@ -3273,6 +3360,8 @@ typedef struct AfterTriggerSharedData TriggerEvent ats_event; /* event type indicator, see trigger.h */ Oid ats_tgoid; /* the trigger's ID */ Oid ats_relid; /* the relation it's on */ + Oid ats_transition_relid; + /* determines TupleDesc of transition tables */ CommandId ats_firing_id; /* ID for firing cycle */ } AfterTriggerSharedData; @@ -3624,6 +3713,7 @@ afterTriggerAddEvent(AfterTriggerEventList *events, { if (newshared->ats_tgoid == evtshared->ats_tgoid && newshared->ats_relid == evtshared->ats_relid && + newshared->ats_transition_relid == evtshared->ats_transition_relid && newshared->ats_event == evtshared->ats_event && newshared->ats_firing_id == 0) break; @@ -3861,6 +3951,7 @@ AfterTriggerExecute(AfterTriggerEvent event, GetTriggerTransitionTuplestore(afterTriggers.new_tuplestores); else LocTriggerData.tg_newtable = NULL; + LocTriggerData.tg_transition_relid = evtshared->ats_transition_relid; /* * Setup the remaining trigger information @@ -5099,7 +5190,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, + TriggerTransitionFilter *transitions) { Relation rel = relinfo->ri_RelationDesc; TriggerDesc *trigdesc = relinfo->ri_TrigDesc; @@ -5129,35 +5221,81 @@ 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)) + TupleConversionMap *map = NULL; + bool delete_old_table = false; + bool update_old_table = false; + bool update_new_table = false; + bool insert_new_table = false; + + if (trigdesc != NULL) + { + /* + * Check if we need to capture transition tuples for triggers + * defined on this relation. + */ + 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 (transitions != NULL) + { + /* + * A TriggerTransitionFilter 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. + */ + 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; + } + + 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 (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; @@ -5326,6 +5464,9 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo, (trigger->tginitdeferred ? AFTER_TRIGGER_INITDEFERRED : 0); new_shared.ats_tgoid = trigger->tgoid; new_shared.ats_relid = RelationGetRelid(rel); + new_shared.ats_transition_relid = + (transitions ? + transitions->ttf_transition_relid : new_shared.ats_relid); new_shared.ats_firing_id = 0; afterTriggerAddEvent(&afterTriggers.query_stack[afterTriggers.query_depth], diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 2535d2ee695..b6e5b3f14ff 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -3204,7 +3204,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 327a0bad388..a70d48291f9 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..62e2133a359 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -571,7 +571,8 @@ ExecInsert(ModifyTableState *mtstate, } /* AFTER ROW INSERT Triggers */ - ExecARInsertTriggers(estate, resultRelInfo, tuple, recheckIndexes); + ExecARInsertTriggers(estate, resultRelInfo, tuple, recheckIndexes, + mtstate->mt_transition_filter); list_free(recheckIndexes); @@ -619,7 +620,8 @@ ExecInsert(ModifyTableState *mtstate, * ---------------------------------------------------------------- */ static TupleTableSlot * -ExecDelete(ItemPointer tupleid, +ExecDelete(ModifyTableState *mtstate, + ItemPointer tupleid, HeapTuple oldtuple, TupleTableSlot *planSlot, EPQState *epqstate, @@ -796,7 +798,8 @@ ldelete:; (estate->es_processed)++; /* AFTER ROW DELETE Triggers */ - ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple); + ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple, + mtstate->mt_transition_filter); /* Process RETURNING if present */ if (resultRelInfo->ri_projectReturning) @@ -877,7 +880,8 @@ ldelete:; * ---------------------------------------------------------------- */ static TupleTableSlot * -ExecUpdate(ItemPointer tupleid, +ExecUpdate(ModifyTableState *mtstate, + ItemPointer tupleid, HeapTuple oldtuple, TupleTableSlot *slot, TupleTableSlot *planSlot, @@ -1105,7 +1109,8 @@ lreplace:; /* AFTER ROW UPDATE Triggers */ ExecARUpdateTriggers(estate, resultRelInfo, tupleid, oldtuple, tuple, - recheckIndexes); + recheckIndexes, + mtstate->mt_transition_filter); list_free(recheckIndexes); @@ -1312,7 +1317,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); @@ -1492,6 +1497,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_filter != NULL) + { + /* Prepare to convert transition tuples from this child. */ + Assert(node->mt_transition_tupconv_maps != NULL); + node->mt_transition_filter->ttf_map = + node->mt_transition_tupconv_maps[node->mt_whichplan]; + } continue; } else @@ -1602,11 +1614,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: @@ -1650,11 +1662,12 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) int nplans = list_length(node->plans); ResultRelInfo *saved_resultRelInfo; ResultRelInfo *resultRelInfo; - TupleDesc tupDesc; + TupleDesc tupDesc ; Plan *subplan; ListCell *l; int i; Relation rel; + ResultRelInfo *targetRelInfo; /* check for unsupported flags */ Assert(!(eflags & (EXEC_FLAG_BACKWARD | EXEC_FLAG_MARK))); @@ -1788,6 +1801,83 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) mtstate->mt_partition_tuple_slot = partition_tuple_slot; } + /* Check if this is a multi-table update through a named target relation. */ + targetRelInfo = NULL; + if (estate->es_num_root_result_relations > 0) + { + /* Partitioned table. The named relation is the first root. */ + targetRelInfo = &estate->es_root_result_relations[0]; + + } + else if (mtstate->mt_nplans > 1) + { + /* Inheritance hierarchy. The named relation is the first plan. */ + targetRelInfo = &mtstate->resultRelInfo[0]; + } + + /* + * If this is a multi-table update, check if we need to build a + * TriggerTransitionFilter to control the capture of transition tuples + * from the whole set of tables. + */ + if (targetRelInfo != NULL) + { + int i; + + /* Check for transition tables on the directly targeted relation. */ + mtstate->mt_transition_filter = + MakeTriggerTransitionFilter(targetRelInfo->ri_TrigDesc); + + /* Check for transition tables on all child tables. */ + for (i = 0; i < mtstate->mt_nplans; ++i) + { + mtstate->mt_transition_filter = + MergeTriggerTransitionFilter(mtstate->mt_transition_filter, + mtstate->resultRelInfo[i].ri_TrigDesc); + } + + /* + * Note that mt_transition_filter may still be NULL here if no reason + * to collect transition tuples was found. + */ + } + + /* + * If we found that we need to collect transition tuples from multiple + * tables, then we'll also need tuple conversion maps for any children + * that have incompatible TupleDescriptors. + */ + if (mtstate->mt_transition_filter != NULL) + { + int i; + + /* + * Record the rel ID of the target table. This will be used to + * control the transition tuplestores' TupleDesc as visible to scans. + */ + mtstate->mt_transition_filter->ttf_transition_relid = + targetRelInfo->ri_RelationDesc->rd_id; + + /* + * 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. + */ + mtstate->mt_transition_tupconv_maps = (TupleConversionMap **) + palloc(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 conversion map for first plan. */ + mtstate->mt_transition_filter->ttf_map = + mtstate->mt_transition_tupconv_maps[0]; + } + /* * Initialize any WITH CHECK OPTION constraints if needed. */ diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c index 35021e1839b..e1c150a7cb0 100644 --- a/src/backend/executor/spi.c +++ b/src/backend/executor/spi.c @@ -2739,7 +2739,7 @@ SPI_register_trigger_data(TriggerData *tdata) int rc; enr->md.name = tdata->tg_trigger->tgnewtable; - enr->md.reliddesc = tdata->tg_relation->rd_id; + enr->md.reliddesc = tdata->tg_transition_relid; enr->md.tupdesc = NULL; enr->md.enrtype = ENR_NAMED_TUPLESTORE; enr->md.enrtuples = tuplestore_tuple_count(tdata->tg_newtable); @@ -2756,7 +2756,7 @@ SPI_register_trigger_data(TriggerData *tdata) int rc; enr->md.name = tdata->tg_trigger->tgoldtable; - enr->md.reliddesc = tdata->tg_relation->rd_id; + enr->md.reliddesc = tdata->tg_transition_relid; enr->md.tupdesc = NULL; enr->md.enrtype = ENR_NAMED_TUPLESTORE; enr->md.enrtuples = tuplestore_tuple_count(tdata->tg_oldtable); diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h index d73969c8747..16bc7759944 100644 --- a/src/include/commands/trigger.h +++ b/src/include/commands/trigger.h @@ -32,6 +32,7 @@ typedef struct TriggerData NodeTag type; TriggerEvent tg_event; Relation tg_relation; + Oid tg_transition_relid; HeapTuple tg_trigtuple; HeapTuple tg_newtuple; Trigger *tg_trigger; @@ -42,6 +43,22 @@ typedef struct TriggerData } TriggerData; /* + * Meta-data to control the capture of old and new tuples into transition + * tables for a trigger on a partitioned table or a parent in an inheritance + * hierarchy. + */ +typedef struct TriggerTransitionFilter +{ + /* Is there at least one trigger specifying each transition relation? */ + bool ttf_delete_old_table; + bool ttf_update_old_table; + bool ttf_update_new_table; + bool ttf_insert_new_table; + Oid ttf_transition_relid; + TupleConversionMap *ttf_map; +} TriggerTransitionFilter; + +/* * TriggerEvent bit flags * * Note that we assume different event types (INSERT/DELETE/UPDATE/TRUNCATE) @@ -127,6 +144,10 @@ extern void RelationBuildTriggers(Relation relation); extern TriggerDesc *CopyTriggerDesc(TriggerDesc *trigdesc); +extern TriggerTransitionFilter *MakeTriggerTransitionFilter(TriggerDesc *trigdesc); +extern TriggerTransitionFilter *MergeTriggerTransitionFilter(TriggerTransitionFilter *filter, + TriggerDesc *trigdesc); + extern void FreeTriggerDesc(TriggerDesc *trigdesc); extern void ExecBSInsertTriggers(EState *estate, @@ -139,7 +160,8 @@ extern TupleTableSlot *ExecBRInsertTriggers(EState *estate, extern void ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo, HeapTuple trigtuple, - List *recheckIndexes); + List *recheckIndexes, + TriggerTransitionFilter *transitions); extern TupleTableSlot *ExecIRInsertTriggers(EState *estate, ResultRelInfo *relinfo, TupleTableSlot *slot); @@ -155,7 +177,8 @@ extern bool ExecBRDeleteTriggers(EState *estate, extern void ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo, ItemPointer tupleid, - HeapTuple fdw_trigtuple); + HeapTuple fdw_trigtuple, + TriggerTransitionFilter *transitions); extern bool ExecIRDeleteTriggers(EState *estate, ResultRelInfo *relinfo, HeapTuple trigtuple); @@ -174,7 +197,8 @@ extern void ExecARUpdateTriggers(EState *estate, ItemPointer tupleid, HeapTuple fdw_trigtuple, HeapTuple newtuple, - List *recheckIndexes); + List *recheckIndexes, + TriggerTransitionFilter *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..5b7ce1e6cd4 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 TriggerTransitionFilter *mt_transition_filter; + /* 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 c300449f3aa..8efb199a6e2 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 -- @@ -1883,3 +1858,104 @@ delete from parted_stmt_trig; NOTICE: trigger on parted_stmt_trig BEFORE DELETE for STATEMENT NOTICE: trigger on parted_stmt_trig AFTER DELETE for STATEMENT drop table parted_stmt_trig, parted2_stmt_trig; +-- +-- Verify behavior of statement triggers on partition parent with +-- transition tables +-- +-- set up a partition hierarchy with some different TupleDescriptors +create table parent (a text, b int) partition by list (a); +create table child1 partition of parent for values in ('AAA'); +insert into child1 values ('AAA', 42); +-- a child with a dropped column +create table child2 (x int, a text, b int); +insert into child2 values (42, 'BBB', 42); +alter table child2 drop column x; +alter table parent attach partition child2 for values in ('BBB'); +-- a child with a different order +create table child3 (b int, a text); +insert into child3 values (42, 'CCC'); +alter table parent attach partition child3 for values in ('CCC'); +create or replace function dump_transition_tables() returns trigger language plpgsql as +$$ + begin + raise notice 'old table = %, new table = %', + (select json_agg(row_to_json(old_table) order by a) from old_table), + (select json_agg(row_to_json(new_table) order by a) from new_table); + return null; + end; +$$; +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(); +update parent set b = b + 1; +NOTICE: old table = [{"a":"AAA","b":42}, {"a":"BBB","b":42}, {"a":"CCC","b":42}], new table = [{"a":"AAA","b":43}, {"a":"BBB","b":43}, {"a":"CCC","b":43}] +drop table child1, child2, child3, parent; +-- +-- Verify behavior of row triggers on partition parent with transition +-- tables, when the row trigger is installed on a child +-- +create table parent (a text, b int) partition by list (a); +create table child1 partition of parent for values in ('AAA'); +insert into child1 values ('AAA', 42); +-- a child with a different order +create table child2 (b int, a text); +insert into child2 values (42, 'BBB'); +alter table parent attach partition child2 for values in ('BBB'); +create trigger child2_row_trig + after insert or update or delete on child2 + referencing old table as old_table new table as new_table + for each row + execute procedure dump_transition_tables(); +-- updating the parent should cause the transition tuples to be +-- captured from child1 and child2, and child2_row_trig should see +-- them and they should all be converted to the same format +update parent set b = b + 1; +NOTICE: old table = [{"a":"AAA","b":42}, {"a":"BBB","b":42}], new table = [{"a":"AAA","b":43}, {"a":"BBB","b":43}] +drop table child1, child2, parent; +-- +-- Verify behavior of statement triggers on inheritance parent with +-- transition tables +-- +-- set up inheritance hierarchy with different TupleDescriptors +create table parent (a text, b int); +create table child1 () inherits (parent); +insert into child1 values ('AAA', 42); +-- a child with a different order +create table child2 (b int, a text); +alter table child2 inherit parent; +insert into child2 values (42, 'BBB'); +-- a child with an extra column that should be sliced off +create table child3 (c text) inherits (parent); +insert into child3 values ('CCC', 42, 'foo'); +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(); +update parent set b = b + 1; +NOTICE: old table = [{"a":"AAA","b":42}, {"a":"BBB","b":42}, {"a":"CCC","b":42}], new table = [{"a":"AAA","b":43}, {"a":"BBB","b":43}, {"a":"CCC","b":43}] +drop table child1, child2, child3, parent; +-- +-- Verify behavior of row triggers on inheritance child tables with +-- transition tables +-- +create table animal (a text); +create table cat (whiskers text) inherits (animal); +create table kangaroo (pouch text) inherits (animal); +insert into cat values ('C', 'whiskers'); +insert into kangaroo values ('K', 'pouch'); +create trigger kangaroo_row_trig + after insert or update or delete on kangaroo + referencing old table as old_table new table as new_table + for each row + execute procedure dump_transition_tables(); +-- updating "animal" should fire kangaroo's row trigger, and it should +-- see the "cat" and "kangaroo" rows but they should be sliced down to +-- "animal" format, because that is the directly accessed relation and +-- the common subset of columns +update animal set a = a || 'x'; +NOTICE: old table = [{"a":"C"}, {"a":"K"}], new table = [{"a":"Cx"}, {"a":"Kx"}] +drop table cat, kangaroo, animal; +drop function dump_transition_tables(); diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql index e5dbcaeea36..50a52f51949 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 -- @@ -1348,3 +1324,125 @@ with upd as ( delete from parted_stmt_trig; drop table parted_stmt_trig, parted2_stmt_trig; + +-- +-- Verify behavior of statement triggers on partition parent with +-- transition tables +-- + +-- set up a partition hierarchy with some different TupleDescriptors +create table parent (a text, b int) partition by list (a); +create table child1 partition of parent for values in ('AAA'); +insert into child1 values ('AAA', 42); + +-- a child with a dropped column +create table child2 (x int, a text, b int); +insert into child2 values (42, 'BBB', 42); +alter table child2 drop column x; +alter table parent attach partition child2 for values in ('BBB'); + +-- a child with a different order +create table child3 (b int, a text); +insert into child3 values (42, 'CCC'); +alter table parent attach partition child3 for values in ('CCC'); + +create or replace function dump_transition_tables() returns trigger language plpgsql as +$$ + begin + raise notice 'old table = %, new table = %', + (select json_agg(row_to_json(old_table) order by a) from old_table), + (select json_agg(row_to_json(new_table) order by a) from new_table); + return null; + end; +$$; + +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(); + +update parent set b = b + 1; + +drop table child1, child2, child3, parent; + +-- +-- Verify behavior of row triggers on partition parent with transition +-- tables, when the row trigger is installed on a child +-- +create table parent (a text, b int) partition by list (a); +create table child1 partition of parent for values in ('AAA'); +insert into child1 values ('AAA', 42); + +-- a child with a different order +create table child2 (b int, a text); +insert into child2 values (42, 'BBB'); +alter table parent attach partition child2 for values in ('BBB'); + +create trigger child2_row_trig + after insert or update or delete on child2 + referencing old table as old_table new table as new_table + for each row + execute procedure dump_transition_tables(); + +-- updating the parent should cause the transition tuples to be +-- captured from child1 and child2, and child2_row_trig should see +-- them and they should all be converted to the same format +update parent set b = b + 1; + +drop table child1, child2, parent; + +-- +-- Verify behavior of statement triggers on inheritance parent with +-- transition tables +-- + +-- set up inheritance hierarchy with different TupleDescriptors +create table parent (a text, b int); +create table child1 () inherits (parent); +insert into child1 values ('AAA', 42); + +-- a child with a different order +create table child2 (b int, a text); +alter table child2 inherit parent; +insert into child2 values (42, 'BBB'); + +-- a child with an extra column that should be sliced off +create table child3 (c text) inherits (parent); +insert into child3 values ('CCC', 42, 'foo'); + +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(); + +update parent set b = b + 1; + +drop table child1, child2, child3, parent; + +-- +-- Verify behavior of row triggers on inheritance child tables with +-- transition tables +-- +create table animal (a text); +create table cat (whiskers text) inherits (animal); +create table kangaroo (pouch text) inherits (animal); +insert into cat values ('C', 'whiskers'); +insert into kangaroo values ('K', 'pouch'); + +create trigger kangaroo_row_trig + after insert or update or delete on kangaroo + referencing old table as old_table new table as new_table + for each row + execute procedure dump_transition_tables(); + +-- updating "animal" should fire kangaroo's row trigger, and it should +-- see the "cat" and "kangaroo" rows but they should be sliced down to +-- "animal" format, because that is the directly accessed relation and +-- the common subset of columns +update animal set a = a || 'x'; + +drop table cat, kangaroo, animal; + +drop function dump_transition_tables();