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 d05e51c8208..90216693eef 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 (stmt->timing != TRIGGER_TYPE_AFTER) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), @@ -2010,6 +2004,35 @@ equalTriggerDescs(TriggerDesc *trigdesc1, TriggerDesc *trigdesc2) #endif /* NOT_USED */ /* + * Make a TriggerTransitionFilter object based on a given TriggerDesc. This + * 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. The + * resulting object can be passed to the ExecAR* functions, but the caller + * should also set ttf_map as appropriate when dealing with child tables. If + * there are no triggers with transition tables, then return NULL. + */ +TriggerTransitionFilter * +MakeTriggerTransitionFilter(TriggerDesc *trigdesc) +{ + TriggerTransitionFilter *result = NULL; + + if (trigdesc != NULL && + (trigdesc->trig_delete_old_table || trigdesc->trig_update_old_table || + trigdesc->trig_update_new_table || trigdesc->trig_insert_new_table)) + { + result = (TriggerTransitionFilter *) + palloc0(sizeof(TriggerTransitionFilter)); + result->ttf_delete_old_table = trigdesc->trig_delete_old_table; + result->ttf_update_old_table = trigdesc->trig_update_old_table; + result->ttf_update_new_table = trigdesc->trig_update_new_table; + result->ttf_insert_new_table = trigdesc->trig_insert_new_table; + } + + return result; +} + +/* * Call a trigger function. * * trigdata: trigger descriptor. @@ -2173,7 +2196,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 * @@ -2244,14 +2267,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 * @@ -2379,7 +2405,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 @@ -2454,12 +2480,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; @@ -2475,7 +2503,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); } @@ -2591,7 +2620,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 * @@ -2716,12 +2746,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; @@ -2738,7 +2772,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); } @@ -2869,7 +2904,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); } @@ -5080,7 +5115,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; @@ -5110,35 +5146,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; diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index cdb1a6a5f5d..ab7384f2c86 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..b9f44bb2bad 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,11 @@ 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) + { + node->mt_transition_filter->ttf_map = + node->mt_transition_tupconv_maps[node->mt_whichplan]; + } continue; } else @@ -1602,11 +1612,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,7 +1660,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) int nplans = list_length(node->plans); ResultRelInfo *saved_resultRelInfo; ResultRelInfo *resultRelInfo; - TupleDesc tupDesc; + TupleDesc tupDesc = NULL; Plan *subplan; ListCell *l; int i; @@ -1788,6 +1798,48 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) mtstate->mt_partition_tuple_slot = partition_tuple_slot; } + /* Check if we need to capture transition tuples from child tables. */ + if (estate->es_num_root_result_relations > 0) + { + /* Partitioned table. The named relation is from the first root. */ + mtstate->mt_transition_filter = + MakeTriggerTransitionFilter(estate->es_root_result_relations[0].ri_TrigDesc); + tupDesc = RelationGetDescr(estate->es_root_result_relations[0].ri_RelationDesc); + } + else if (mtstate->mt_nplans > 1) + { + /* Inheritance hierarchy. The named relation is from the first plan. */ + mtstate->mt_transition_filter = + MakeTriggerTransitionFilter(mtstate->resultRelInfo[0].ri_TrigDesc); + tupDesc = RelationGetDescr(mtstate->resultRelInfo[0].ri_RelationDesc); + } + + if (mtstate->mt_transition_filter != NULL) + { + int i; + + /* + * If there are any partitioning or inheritance child tables, then + * we'll need to be able to convert their tuples to match the target + * table's TupleDescriptor before putting any new and old images into + * its tuplestores. So we'll need a list of TupleConversionMaps + * corresponding to the list of subplans. + */ + 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), + tupDesc, + 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/include/commands/trigger.h b/src/include/commands/trigger.h index d73969c8747..1db8f3d2d25 100644 --- a/src/include/commands/trigger.h +++ b/src/include/commands/trigger.h @@ -42,6 +42,21 @@ 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; + TupleConversionMap *ttf_map; +} TriggerTransitionFilter; + +/* * TriggerEvent bit flags * * Note that we assume different event types (INSERT/DELETE/UPDATE/TRUNCATE) @@ -127,6 +142,8 @@ extern void RelationBuildTriggers(Relation relation); extern TriggerDesc *CopyTriggerDesc(TriggerDesc *trigdesc); +extern TriggerTransitionFilter *MakeTriggerTransitionFilter(TriggerDesc *trigdesc); + extern void FreeTriggerDesc(TriggerDesc *trigdesc); extern void ExecBSInsertTriggers(EState *estate, @@ -139,7 +156,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 +173,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 +193,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 10a301310b4..fa47034b8e1 100644 --- a/src/test/regress/expected/triggers.out +++ b/src/test/regress/expected/triggers.out @@ -1764,30 +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 table my_table_42; -drop table my_table; --- -- Verify that per-statement triggers are fired for partitioned tables -- create table parted_stmt_trig (a int) partition by list (a); @@ -1868,3 +1844,61 @@ 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 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; +drop function dump_transition_tables(); diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql index 84b5ada5544..1ece6a3e74e 100644 --- a/src/test/regress/sql/triggers.sql +++ b/src/test/regress/sql/triggers.sql @@ -1242,29 +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 table my_table_42; -drop table my_table; - --- -- Verify that per-statement triggers are fired for partitioned tables -- create table parted_stmt_trig (a int) partition by list (a); @@ -1333,3 +1310,74 @@ 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 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; +drop function dump_transition_tables();