From 501ed02cf6f4f60c3357775eb07578aebc912d3a Mon Sep 17 00:00:00 2001 From: Andrew Gierth Date: Wed, 28 Jun 2017 18:55:03 +0100 Subject: [PATCH] Fix transition tables for partition/inheritance. We disallow row-level triggers with transition tables on child tables. Transition tables for triggers on the parent table contain only those columns present in the parent. (We can't mix tuple formats in a single transition table.) Patch by Thomas Munro Discussion: https://postgr.es/m/CA%2BTgmoZzTBBAsEUh4MazAN7ga%3D8SsMC-Knp-6cetts9yNZUCcg%40mail.gmail.com --- doc/src/sgml/ref/create_trigger.sgml | 14 + src/backend/catalog/pg_inherits.c | 24 ++ src/backend/commands/copy.c | 70 ++++- src/backend/commands/tablecmds.c | 28 ++ src/backend/commands/trigger.c | 218 ++++++++++++--- src/backend/executor/execMain.c | 2 +- src/backend/executor/execReplication.c | 6 +- src/backend/executor/nodeModifyTable.c | 150 +++++++++- src/include/catalog/pg_inherits_fn.h | 1 + src/include/commands/trigger.h | 45 ++- src/include/nodes/execnodes.h | 4 + src/test/regress/expected/triggers.out | 326 ++++++++++++++++++++-- src/test/regress/sql/triggers.sql | 365 +++++++++++++++++++++++-- 13 files changed, 1143 insertions(+), 110 deletions(-) diff --git a/doc/src/sgml/ref/create_trigger.sgml b/doc/src/sgml/ref/create_trigger.sgml index c5f7c75838..18efe6a9ed 100644 --- a/doc/src/sgml/ref/create_trigger.sgml +++ b/doc/src/sgml/ref/create_trigger.sgml @@ -458,6 +458,20 @@ UPDATE OF column_name1 [, column_name2 + + Modifying a partitioned table or a table with inheritance children fires + statement-level triggers directly attached to that table, but not + statement-level triggers for its partitions or child tables. In contrast, + row-level triggers are fired for all affected partitions or child tables. + If a statement-level trigger has been defined with transition relations + named by a REFERENCING clause, then before and after + images of rows are visible from all affected partitions or child tables. + In the case of inheritance children, the row images include only columns + that are present in the table that the trigger is attached to. Currently, + row-level triggers with transition relations cannot be defined on + partitions or inheritance child tables. + + In PostgreSQL versions before 7.3, it was necessary to declare trigger functions as returning the placeholder diff --git a/src/backend/catalog/pg_inherits.c b/src/backend/catalog/pg_inherits.c index e5fb52cfbf..245a374fc9 100644 --- a/src/backend/catalog/pg_inherits.c +++ b/src/backend/catalog/pg_inherits.c @@ -273,6 +273,30 @@ has_subclass(Oid relationId) return result; } +/* + * has_superclass - does this relation inherit from another? The caller + * should hold a lock on the given relation so that it can't be concurrently + * added to or removed from an inheritance hierarchy. + */ +bool +has_superclass(Oid relationId) +{ + Relation catalog; + SysScanDesc scan; + ScanKeyData skey; + bool result; + + catalog = heap_open(InheritsRelationId, AccessShareLock); + ScanKeyInit(&skey, Anum_pg_inherits_inhrelid, BTEqualStrategyNumber, + F_OIDEQ, ObjectIdGetDatum(relationId)); + scan = systable_beginscan(catalog, InheritsRelidSeqnoIndexId, true, + NULL, 1, &skey); + result = HeapTupleIsValid(systable_getnext(scan)); + systable_endscan(scan); + heap_close(catalog, AccessShareLock); + + return result; +} /* * Given two type OIDs, determine whether the first is a complex type diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c index 3c399e29db..a4c02e6b7c 100644 --- a/src/backend/commands/copy.c +++ b/src/backend/commands/copy.c @@ -171,6 +171,8 @@ typedef struct CopyStateData ResultRelInfo *partitions; /* Per partition result relation */ TupleConversionMap **partition_tupconv_maps; TupleTableSlot *partition_tuple_slot; + TransitionCaptureState *transition_capture; + TupleConversionMap **transition_tupconv_maps; /* * These variables are used to reduce overhead in textual COPY FROM. @@ -1436,6 +1438,36 @@ 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->transition_capture = + MakeTransitionCaptureState(rel->trigdesc); + + /* + * If we are capturing transition tuples, they may need to be + * converted from partition format back to partitioned table + * format (this is only ever necessary if a BEFORE trigger + * modifies the tuple). + */ + if (cstate->transition_capture != NULL) + { + int i; + + cstate->transition_tupconv_maps = (TupleConversionMap **) + palloc0(sizeof(TupleConversionMap *) * + cstate->num_partitions); + for (i = 0; i < cstate->num_partitions; ++i) + { + cstate->transition_tupconv_maps[i] = + convert_tuples_by_name(RelationGetDescr(cstate->partitions[i].ri_RelationDesc), + RelationGetDescr(rel), + gettext_noop("could not convert row type")); + } + } } } else @@ -2591,6 +2623,35 @@ CopyFrom(CopyState cstate) */ estate->es_result_relation_info = resultRelInfo; + /* + * If we're capturing transition tuples, we might need to convert + * from the partition rowtype to parent rowtype. + */ + if (cstate->transition_capture != NULL) + { + if (resultRelInfo->ri_TrigDesc && + (resultRelInfo->ri_TrigDesc->trig_insert_before_row || + resultRelInfo->ri_TrigDesc->trig_insert_instead_row)) + { + /* + * If there are any BEFORE or INSTEAD triggers on the + * partition, we'll have to be ready to convert their + * result back to tuplestore format. + */ + cstate->transition_capture->tcs_original_insert_tuple = NULL; + cstate->transition_capture->tcs_map = + cstate->transition_tupconv_maps[leaf_part_index]; + } + else + { + /* + * Otherwise, just remember the original unconverted + * tuple, to avoid a needless round trip conversion. + */ + cstate->transition_capture->tcs_original_insert_tuple = tuple; + cstate->transition_capture->tcs_map = NULL; + } + } /* * We might need to convert from the parent rowtype to the * partition rowtype. @@ -2703,7 +2764,7 @@ CopyFrom(CopyState cstate) /* AFTER ROW INSERT Triggers */ ExecARInsertTriggers(estate, resultRelInfo, tuple, - recheckIndexes); + recheckIndexes, cstate->transition_capture); list_free(recheckIndexes); } @@ -2856,7 +2917,7 @@ CopyFromInsertBatch(CopyState cstate, EState *estate, CommandId mycid, estate, false, NULL, NIL); ExecARInsertTriggers(estate, resultRelInfo, bufferedTuples[i], - recheckIndexes); + recheckIndexes, NULL); list_free(recheckIndexes); } } @@ -2866,14 +2927,15 @@ CopyFromInsertBatch(CopyState cstate, EState *estate, CommandId mycid, * anyway. */ else if (resultRelInfo->ri_TrigDesc != NULL && - resultRelInfo->ri_TrigDesc->trig_insert_after_row) + (resultRelInfo->ri_TrigDesc->trig_insert_after_row || + resultRelInfo->ri_TrigDesc->trig_insert_new_table)) { for (i = 0; i < nBufferedTuples; i++) { cstate->cur_lineno = firstBufferedLineNo + i; ExecARInsertTriggers(estate, resultRelInfo, bufferedTuples[i], - NIL); + NIL, NULL); } } diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 7d9c769b06..bb00858ad1 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -10933,6 +10933,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 @@ -11014,6 +11015,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); @@ -13418,6 +13432,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); @@ -13547,6 +13562,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 45d1f515eb..f902e0cdf5 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, + TransitionCaptureState *transition_capture); 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 supported on partitions"))); + else + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("ROW triggers with transition tables are not supported on inheritance children"))); + } + if (stmt->timing != TRIGGER_TYPE_AFTER) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), @@ -2028,6 +2044,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 TransitionCaptureState object from a given TriggerDesc. The + * resulting object holds the flags which control whether transition tuples + * are collected when tables are modified. This allows us to use the flags + * from a parent table to control the collection of transition tuples from + * child tables. + * + * If there are no triggers with transition tables configured for 'trigdesc', + * then return NULL. + * + * The resulting object can be passed to the ExecAR* functions. The caller + * should set tcs_map or tcs_original_insert_tuple as appropriate when dealing + * with child tables. + */ +TransitionCaptureState * +MakeTransitionCaptureState(TriggerDesc *trigdesc) +{ + TransitionCaptureState *state = NULL; + + if (trigdesc != NULL && + (trigdesc->trig_delete_old_table || trigdesc->trig_update_old_table || + trigdesc->trig_update_new_table || trigdesc->trig_insert_new_table)) + { + state = (TransitionCaptureState *) + palloc0(sizeof(TransitionCaptureState)); + state->tcs_delete_old_table = trigdesc->trig_delete_old_table; + state->tcs_update_old_table = trigdesc->trig_update_old_table; + state->tcs_update_new_table = trigdesc->trig_update_new_table; + state->tcs_insert_new_table = trigdesc->trig_insert_new_table; + } + + return state; +} + /* * Call a trigger function. * @@ -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, + TransitionCaptureState *transition_capture) { TriggerDesc *trigdesc = relinfo->ri_TrigDesc; - if (trigdesc && - (trigdesc->trig_insert_after_row || trigdesc->trig_insert_new_table)) + if ((trigdesc && trigdesc->trig_insert_after_row) || + (trigdesc && !transition_capture && trigdesc->trig_insert_new_table) || + (transition_capture && transition_capture->tcs_insert_new_table)) AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT, - true, NULL, trigtuple, recheckIndexes, NULL); + true, NULL, trigtuple, + recheckIndexes, NULL, + transition_capture); } TupleTableSlot * @@ -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, + TransitionCaptureState *transition_capture) { TriggerDesc *trigdesc = relinfo->ri_TrigDesc; - if (trigdesc && - (trigdesc->trig_delete_after_row || trigdesc->trig_delete_old_table)) + if ((trigdesc && trigdesc->trig_delete_after_row) || + (trigdesc && !transition_capture && trigdesc->trig_delete_old_table) || + (transition_capture && transition_capture->tcs_delete_old_table)) { HeapTuple trigtuple; @@ -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, + transition_capture); 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,18 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo, ItemPointer tupleid, HeapTuple fdw_trigtuple, HeapTuple newtuple, - List *recheckIndexes) + List *recheckIndexes, + TransitionCaptureState *transition_capture) { TriggerDesc *trigdesc = relinfo->ri_TrigDesc; - if (trigdesc && (trigdesc->trig_update_after_row || - trigdesc->trig_update_old_table || trigdesc->trig_update_new_table)) + if ((trigdesc && trigdesc->trig_update_after_row) || + (trigdesc && !transition_capture && + (trigdesc->trig_update_old_table || + trigdesc->trig_update_new_table)) || + (transition_capture && + (transition_capture->tcs_update_old_table || + transition_capture->tcs_update_new_table))) { HeapTuple trigtuple; @@ -2757,7 +2845,8 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo, AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE, true, trigtuple, newtuple, recheckIndexes, - GetUpdatedColumns(relinfo, estate)); + GetUpdatedColumns(relinfo, estate), + transition_capture); if (trigtuple != fdw_trigtuple) heap_freetuple(trigtuple); } @@ -2888,7 +2977,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 +5179,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, + TransitionCaptureState *transition_capture) { Relation rel = relinfo->ri_RelationDesc; TriggerDesc *trigdesc = relinfo->ri_TrigDesc; @@ -5120,10 +5210,49 @@ 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 (transition_capture != NULL) + { + /* + * A TransitionCaptureState object was provided to tell us which + * tuples to capture based on a parent table named in a DML + * statement. We may be dealing with a child table with an + * incompatible TupleDescriptor, in which case we'll need a map to + * convert them. As a small optimization, we may receive the + * original tuple from an insertion into a partitioned table to + * avoid a wasteful parent->child->parent round trip. + */ + delete_old_table = transition_capture->tcs_delete_old_table; + update_old_table = transition_capture->tcs_update_old_table; + update_new_table = transition_capture->tcs_update_new_table; + insert_new_table = transition_capture->tcs_insert_new_table; + map = transition_capture->tcs_map; + original_insert_tuple = + transition_capture->tcs_original_insert_tuple; + } + else if (trigdesc != NULL) + { + /* + * Check if we need to capture transition tuples for triggers + * defined on this relation directly. This case is useful for + * cases like execReplication.c which don't set up a + * TriggerCaptureState because they don't know how to work with + * partitions. + */ + delete_old_table = trigdesc->trig_delete_old_table; + update_old_table = trigdesc->trig_update_old_table; + update_new_table = trigdesc->trig_update_new_table; + insert_new_table = trigdesc->trig_insert_new_table; + } + + if ((event == TRIGGER_EVENT_DELETE && delete_old_table) || + (event == TRIGGER_EVENT_UPDATE && update_old_table)) { Tuplestorestate *old_tuplestore; @@ -5131,12 +5260,18 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo, old_tuplestore = GetTriggerTransitionTuplestore (afterTriggers.old_tuplestores); - tuplestore_puttuple(old_tuplestore, oldtup); + if (map != NULL) + { + HeapTuple converted = do_convert_tuple(oldtup, map); + + tuplestore_puttuple(old_tuplestore, converted); + pfree(converted); + } + else + tuplestore_puttuple(old_tuplestore, oldtup); } - if ((event == TRIGGER_EVENT_INSERT && - trigdesc->trig_insert_new_table) || - (event == TRIGGER_EVENT_UPDATE && - trigdesc->trig_update_new_table)) + if ((event == TRIGGER_EVENT_INSERT && insert_new_table) || + (event == TRIGGER_EVENT_UPDATE && update_new_table)) { Tuplestorestate *new_tuplestore; @@ -5144,11 +5279,22 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo, new_tuplestore = GetTriggerTransitionTuplestore (afterTriggers.new_tuplestores); - tuplestore_puttuple(new_tuplestore, newtup); + if (original_insert_tuple != NULL) + tuplestore_puttuple(new_tuplestore, original_insert_tuple); + else if (map != NULL) + { + HeapTuple converted = do_convert_tuple(newtup, map); + + tuplestore_puttuple(new_tuplestore, converted); + pfree(converted); + } + else + tuplestore_puttuple(new_tuplestore, newtup); } /* If transition tables are the only reason we're here, return. */ - if ((event == TRIGGER_EVENT_DELETE && !trigdesc->trig_delete_after_row) || + if (trigdesc == NULL || + (event == TRIGGER_EVENT_DELETE && !trigdesc->trig_delete_after_row) || (event == TRIGGER_EVENT_INSERT && !trigdesc->trig_insert_after_row) || (event == TRIGGER_EVENT_UPDATE && !trigdesc->trig_update_after_row)) return; diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 7f0d21f516..0f08283f81 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -3198,7 +3198,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 59f14e997f..36960eaa7e 100644 --- a/src/backend/executor/execReplication.c +++ b/src/backend/executor/execReplication.c @@ -417,7 +417,7 @@ ExecSimpleRelationInsert(EState *estate, TupleTableSlot *slot) /* AFTER ROW INSERT Triggers */ ExecARInsertTriggers(estate, resultRelInfo, tuple, - recheckIndexes); + recheckIndexes, NULL); list_free(recheckIndexes); } @@ -479,7 +479,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); } @@ -522,7 +522,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 5e43a06942..f2534f2062 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -313,6 +313,36 @@ ExecInsert(ModifyTableState *mtstate, /* For ExecInsertIndexTuples() to work on the partition's indexes */ estate->es_result_relation_info = resultRelInfo; + /* + * If we're capturing transition tuples, we might need to convert from + * the partition rowtype to parent rowtype. + */ + if (mtstate->mt_transition_capture != NULL) + { + if (resultRelInfo->ri_TrigDesc && + (resultRelInfo->ri_TrigDesc->trig_insert_before_row || + resultRelInfo->ri_TrigDesc->trig_insert_instead_row)) + { + /* + * If there are any BEFORE or INSTEAD triggers on the + * partition, we'll have to be ready to convert their result + * back to tuplestore format. + */ + mtstate->mt_transition_capture->tcs_original_insert_tuple = NULL; + mtstate->mt_transition_capture->tcs_map = + mtstate->mt_transition_tupconv_maps[leaf_part_index]; + } + else + { + /* + * Otherwise, just remember the original unconverted tuple, to + * avoid a needless round trip conversion. + */ + mtstate->mt_transition_capture->tcs_original_insert_tuple = tuple; + mtstate->mt_transition_capture->tcs_map = NULL; + } + } + /* * We might need to convert from the parent rowtype to the partition * rowtype. @@ -588,7 +618,8 @@ ExecInsert(ModifyTableState *mtstate, } /* AFTER ROW INSERT Triggers */ - ExecARInsertTriggers(estate, resultRelInfo, tuple, recheckIndexes); + ExecARInsertTriggers(estate, resultRelInfo, tuple, recheckIndexes, + mtstate->mt_transition_capture); list_free(recheckIndexes); @@ -636,7 +667,8 @@ ExecInsert(ModifyTableState *mtstate, * ---------------------------------------------------------------- */ static TupleTableSlot * -ExecDelete(ItemPointer tupleid, +ExecDelete(ModifyTableState *mtstate, + ItemPointer tupleid, HeapTuple oldtuple, TupleTableSlot *planSlot, EPQState *epqstate, @@ -813,7 +845,8 @@ ldelete:; (estate->es_processed)++; /* AFTER ROW DELETE Triggers */ - ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple); + ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple, + mtstate->mt_transition_capture); /* Process RETURNING if present */ if (resultRelInfo->ri_projectReturning) @@ -894,7 +927,8 @@ ldelete:; * ---------------------------------------------------------------- */ static TupleTableSlot * -ExecUpdate(ItemPointer tupleid, +ExecUpdate(ModifyTableState *mtstate, + ItemPointer tupleid, HeapTuple oldtuple, TupleTableSlot *slot, TupleTableSlot *planSlot, @@ -1122,7 +1156,8 @@ lreplace:; /* AFTER ROW UPDATE Triggers */ ExecARUpdateTriggers(estate, resultRelInfo, tupleid, oldtuple, tuple, - recheckIndexes); + recheckIndexes, + mtstate->mt_transition_capture); list_free(recheckIndexes); @@ -1329,7 +1364,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); @@ -1376,20 +1411,31 @@ fireBSTriggers(ModifyTableState *node) } /* - * Process AFTER EACH STATEMENT triggers + * Return the ResultRelInfo for which we will fire AFTER STATEMENT triggers. + * This is also the relation into whose tuple format all captured transition + * tuples must be converted. */ -static void -fireASTriggers(ModifyTableState *node) +static ResultRelInfo * +getASTriggerResultRelInfo(ModifyTableState *node) { - ResultRelInfo *resultRelInfo = node->resultRelInfo; - /* * If the node modifies a partitioned table, we must fire its triggers. * Note that in that case, node->resultRelInfo points to the first leaf * partition, not the root table. */ if (node->rootResultRelInfo != NULL) - resultRelInfo = node->rootResultRelInfo; + return node->rootResultRelInfo; + else + return node->resultRelInfo; +} + +/* + * Process AFTER EACH STATEMENT triggers + */ +static void +fireASTriggers(ModifyTableState *node) +{ + ResultRelInfo *resultRelInfo = getASTriggerResultRelInfo(node); switch (node->operation) { @@ -1411,6 +1457,72 @@ fireASTriggers(ModifyTableState *node) } } +/* + * Set up the state needed for collecting transition tuples for AFTER + * triggers. + */ +static void +ExecSetupTransitionCaptureState(ModifyTableState *mtstate, EState *estate) +{ + ResultRelInfo *targetRelInfo = getASTriggerResultRelInfo(mtstate); + int i; + + /* Check for transition tables on the directly targeted relation. */ + mtstate->mt_transition_capture = + MakeTransitionCaptureState(targetRelInfo->ri_TrigDesc); + + /* + * If we found that we need to collect transition tuples then we may also + * need tuple conversion maps for any children that have TupleDescs that + * aren't compatible with the tuplestores. + */ + if (mtstate->mt_transition_capture != NULL) + { + ResultRelInfo *resultRelInfos; + int numResultRelInfos; + + /* Find the set of partitions so that we can find their TupleDescs. */ + if (mtstate->mt_partition_dispatch_info != NULL) + { + /* + * For INSERT via partitioned table, so we need TupleDescs based + * on the partition routing table. + */ + resultRelInfos = mtstate->mt_partitions; + numResultRelInfos = mtstate->mt_num_partitions; + } + else + { + /* Otherwise we need the ResultRelInfo for each subplan. */ + resultRelInfos = mtstate->resultRelInfo; + numResultRelInfos = mtstate->mt_nplans; + } + + /* + * Build array of conversion maps from each child's TupleDesc to the + * one used in the tuplestore. The map pointers may be NULL when no + * conversion is necessary, which is hopefully a common case for + * partitions. + */ + mtstate->mt_transition_tupconv_maps = (TupleConversionMap **) + palloc0(sizeof(TupleConversionMap *) * numResultRelInfos); + for (i = 0; i < numResultRelInfos; ++i) + { + mtstate->mt_transition_tupconv_maps[i] = + convert_tuples_by_name(RelationGetDescr(resultRelInfos[i].ri_RelationDesc), + RelationGetDescr(targetRelInfo->ri_RelationDesc), + gettext_noop("could not convert row type")); + } + + /* + * Install the conversion map for the first plan for UPDATE and DELETE + * operations. It will be advanced each time we switch to the next + * plan. (INSERT operations set it every time.) + */ + mtstate->mt_transition_capture->tcs_map = + mtstate->mt_transition_tupconv_maps[0]; + } +} /* ---------------------------------------------------------------- * ExecModifyTable @@ -1509,6 +1621,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_capture != NULL) + { + /* Prepare to convert transition tuples from this child. */ + Assert(node->mt_transition_tupconv_maps != NULL); + node->mt_transition_capture->tcs_map = + node->mt_transition_tupconv_maps[node->mt_whichplan]; + } continue; } else @@ -1618,11 +1737,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: @@ -1804,6 +1923,9 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) mtstate->mt_partition_tuple_slot = partition_tuple_slot; } + /* Build state for collecting transition tuples */ + ExecSetupTransitionCaptureState(mtstate, estate); + /* * Initialize any WITH CHECK OPTION constraints if needed. */ diff --git a/src/include/catalog/pg_inherits_fn.h b/src/include/catalog/pg_inherits_fn.h index abfa4766a1..7743388899 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 10ac724feb..51a25c8ddc 100644 --- a/src/include/commands/trigger.h +++ b/src/include/commands/trigger.h @@ -41,6 +41,39 @@ typedef struct TriggerData Tuplestorestate *tg_newtable; } TriggerData; +/* + * Meta-data to control the capture of old and new tuples into transition + * tables from child tables. + */ +typedef struct TransitionCaptureState +{ + /* + * Is there at least one trigger specifying each transition relation on + * the relation explicitly named in the DML statement or COPY command? + */ + bool tcs_delete_old_table; + bool tcs_update_old_table; + bool tcs_update_new_table; + bool tcs_insert_new_table; + + /* + * For UPDATE and DELETE, AfterTriggerSaveEvent may need to convert the + * new and old tuples from a child table's format to the format of the + * relation named in a query so that it is compatible with the transition + * tuplestores. + */ + TupleConversionMap *tcs_map; + + /* + * For INSERT and COPY, it would be wasteful to convert tuples from child + * format to parent format after they have already been converted in the + * opposite direction during routing. In that case we bypass conversion + * and allow the inserting code (copy.c and nodeModifyTable.c) to provide + * the original tuple directly. + */ + HeapTuple tcs_original_insert_tuple; +} TransitionCaptureState; + /* * TriggerEvent bit flags * @@ -127,6 +160,9 @@ extern void RelationBuildTriggers(Relation relation); extern TriggerDesc *CopyTriggerDesc(TriggerDesc *trigdesc); +extern const char *FindTriggerIncompatibleWithInheritance(TriggerDesc *trigdesc); +extern TransitionCaptureState *MakeTransitionCaptureState(TriggerDesc *trigdesc); + extern void FreeTriggerDesc(TriggerDesc *trigdesc); extern void ExecBSInsertTriggers(EState *estate, @@ -139,7 +175,8 @@ extern TupleTableSlot *ExecBRInsertTriggers(EState *estate, extern void ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo, HeapTuple trigtuple, - List *recheckIndexes); + List *recheckIndexes, + TransitionCaptureState *transition_capture); extern TupleTableSlot *ExecIRInsertTriggers(EState *estate, ResultRelInfo *relinfo, TupleTableSlot *slot); @@ -155,7 +192,8 @@ extern bool ExecBRDeleteTriggers(EState *estate, extern void ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo, ItemPointer tupleid, - HeapTuple fdw_trigtuple); + HeapTuple fdw_trigtuple, + TransitionCaptureState *transition_capture); extern bool ExecIRDeleteTriggers(EState *estate, ResultRelInfo *relinfo, HeapTuple trigtuple); @@ -174,7 +212,8 @@ extern void ExecARUpdateTriggers(EState *estate, ItemPointer tupleid, HeapTuple fdw_trigtuple, HeapTuple newtuple, - List *recheckIndexes); + List *recheckIndexes, + TransitionCaptureState *transition_capture); extern TupleTableSlot *ExecIRUpdateTriggers(EState *estate, ResultRelInfo *relinfo, HeapTuple trigtuple, diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 54c5cf5f95..85fac8ab91 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -963,6 +963,10 @@ typedef struct ModifyTableState TupleConversionMap **mt_partition_tupconv_maps; /* Per partition tuple conversion map */ TupleTableSlot *mt_partition_tuple_slot; + struct TransitionCaptureState *mt_transition_capture; + /* controls transition table population */ + TupleConversionMap **mt_transition_tupconv_maps; + /* Per plan/partition tuple conversion */ } ModifyTableState; /* ---------------- diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out index 29b8adf1e2..995410f1aa 100644 --- a/src/test/regress/expected/triggers.out +++ b/src/test/regress/expected/triggers.out @@ -1793,31 +1793,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 -- @@ -1922,3 +1897,304 @@ 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_insert() returns trigger language plpgsql as +$$ + begin + raise notice 'trigger = %, new table = %', + TG_NAME, + (select string_agg(new_table::text, ', ' order by a) from new_table); + return null; + end; +$$; +create or replace function dump_update() returns trigger language plpgsql as +$$ + begin + raise notice 'trigger = %, old table = %, new table = %', + TG_NAME, + (select string_agg(old_table::text, ', ' order by a) from old_table), + (select string_agg(new_table::text, ', ' order by a) from new_table); + return null; + end; +$$; +create or replace function dump_delete() returns trigger language plpgsql as +$$ + begin + raise notice 'trigger = %, old table = %', + TG_NAME, + (select string_agg(old_table::text, ', ' order by a) from old_table); + return null; + end; +$$; +-- +-- Verify behavior of statement triggers on partition hierarchy with +-- transition tables. Tuples should appear to each trigger in the +-- format of the the relation the trigger is attached to. +-- +-- set up a partition hierarchy with some different TupleDescriptors +create table parent (a text, b int) partition by list (a); +-- a child matching parent +create table child1 partition of parent for values in ('AAA'); +-- a child with a dropped column +create table child2 (x int, a text, b int); +alter table child2 drop column x; +alter table parent attach partition child2 for values in ('BBB'); +-- a child with a different column order +create table child3 (b int, a text); +alter table parent attach partition child3 for values in ('CCC'); +create trigger parent_insert_trig + after insert on parent referencing new table as new_table + for each statement execute procedure dump_insert(); +create trigger parent_update_trig + after update on parent referencing old table as old_table new table as new_table + for each statement execute procedure dump_update(); +create trigger parent_delete_trig + after delete on parent referencing old table as old_table + for each statement execute procedure dump_delete(); +create trigger child1_insert_trig + after insert on child1 referencing new table as new_table + for each statement execute procedure dump_insert(); +create trigger child1_update_trig + after update on child1 referencing old table as old_table new table as new_table + for each statement execute procedure dump_update(); +create trigger child1_delete_trig + after delete on child1 referencing old table as old_table + for each statement execute procedure dump_delete(); +create trigger child2_insert_trig + after insert on child2 referencing new table as new_table + for each statement execute procedure dump_insert(); +create trigger child2_update_trig + after update on child2 referencing old table as old_table new table as new_table + for each statement execute procedure dump_update(); +create trigger child2_delete_trig + after delete on child2 referencing old table as old_table + for each statement execute procedure dump_delete(); +create trigger child3_insert_trig + after insert on child3 referencing new table as new_table + for each statement execute procedure dump_insert(); +create trigger child3_update_trig + after update on child3 referencing old table as old_table new table as new_table + for each statement execute procedure dump_update(); +create trigger child3_delete_trig + after delete on child3 referencing old table as old_table + for each statement execute procedure dump_delete(); +-- insert directly into children sees respective child-format tuples +insert into child1 values ('AAA', 42); +NOTICE: trigger = child1_insert_trig, new table = (AAA,42) +insert into child2 values ('BBB', 42); +NOTICE: trigger = child2_insert_trig, new table = (BBB,42) +insert into child3 values (42, 'CCC'); +NOTICE: trigger = child3_insert_trig, new table = (42,CCC) +-- update via parent sees parent-format tuples +update parent set b = b + 1; +NOTICE: trigger = parent_update_trig, old table = (AAA,42), (BBB,42), (CCC,42), new table = (AAA,43), (BBB,43), (CCC,43) +-- delete via parent sees parent-format tuples +delete from parent; +NOTICE: trigger = parent_delete_trig, old table = (AAA,43), (BBB,43), (CCC,43) +-- insert into parent sees parent-format tuples +insert into parent values ('AAA', 42); +NOTICE: trigger = parent_insert_trig, new table = (AAA,42) +insert into parent values ('BBB', 42); +NOTICE: trigger = parent_insert_trig, new table = (BBB,42) +insert into parent values ('CCC', 42); +NOTICE: trigger = parent_insert_trig, new table = (CCC,42) +-- delete from children sees respective child-format tuples +delete from child1; +NOTICE: trigger = child1_delete_trig, old table = (AAA,42) +delete from child2; +NOTICE: trigger = child2_delete_trig, old table = (BBB,42) +delete from child3; +NOTICE: trigger = child3_delete_trig, old table = (42,CCC) +-- copy into parent sees parent-format tuples +copy parent (a, b) from stdin; +NOTICE: trigger = parent_insert_trig, new table = (AAA,42), (BBB,42), (CCC,42) +-- DML affecting parent sees tuples collected from children even if +-- there is no transition table trigger on the children +drop trigger child1_insert_trig on child1; +drop trigger child1_update_trig on child1; +drop trigger child1_delete_trig on child1; +drop trigger child2_insert_trig on child2; +drop trigger child2_update_trig on child2; +drop trigger child2_delete_trig on child2; +drop trigger child3_insert_trig on child3; +drop trigger child3_update_trig on child3; +drop trigger child3_delete_trig on child3; +delete from parent; +NOTICE: trigger = parent_delete_trig, old table = (AAA,42), (BBB,42), (CCC,42) +-- copy into parent sees tuples collected from children even if there +-- is no transition-table trigger on the children +copy parent (a, b) from stdin; +NOTICE: trigger = parent_insert_trig, new table = (AAA,42), (BBB,42), (CCC,42) +-- insert into parent with a before trigger on a child tuple before +-- insertion, and we capture the newly modified row in parent format +create or replace function intercept_insert() returns trigger language plpgsql as +$$ + begin + new.b = new.b + 1000; + return new; + end; +$$; +create trigger intercept_insert_child3 + before insert on child3 + for each row execute procedure intercept_insert(); +-- insert, parent trigger sees post-modification parent-format tuple +insert into parent values ('AAA', 42), ('BBB', 42), ('CCC', 66); +NOTICE: trigger = parent_insert_trig, new table = (AAA,42), (BBB,42), (CCC,1066) +-- copy, parent trigger sees post-modification parent-format tuple +copy parent (a, b) from stdin; +NOTICE: trigger = parent_insert_trig, new table = (AAA,42), (BBB,42), (CCC,1234) +drop table child1, child2, child3, parent; +drop function intercept_insert(); +-- +-- Verify prohibition of row triggers with transition triggers on +-- partitions +-- +create table parent (a text, b int) partition by list (a); +create table child partition of parent for values in ('AAA'); +-- adding row trigger with transition table fails +create trigger child_row_trig + after insert on child referencing new table as new_table + for each row execute procedure dump_insert(); +ERROR: ROW triggers with transition tables are not supported on partitions +-- detaching it first works +alter table parent detach partition child; +create trigger child_row_trig + after insert on child referencing new table as new_table + for each row execute procedure dump_insert(); +-- but now we're not allowed to reattach it +alter table parent attach partition child for values in ('AAA'); +ERROR: trigger "child_row_trig" prevents table "child" from becoming a partition +DETAIL: ROW triggers with transition tables are not supported on partitions +-- drop the trigger, and now we're allowed to attach it again +drop trigger child_row_trig on child; +alter table parent attach partition child for values in ('AAA'); +drop table child, parent; +-- +-- Verify behavior of statement triggers on (non-partition) +-- inheritance hierarchy with transition tables; similar to the +-- partition case, except there is no rerouting on insertion and child +-- tables can have extra columns +-- +-- set up inheritance hierarchy with different TupleDescriptors +create table parent (a text, b int); +-- a child matching parent +create table child1 () inherits (parent); +-- a child with a different column order +create table child2 (b int, a text); +alter table child2 inherit parent; +-- a child with an extra column +create table child3 (c text) inherits (parent); +create trigger parent_insert_trig + after insert on parent referencing new table as new_table + for each statement execute procedure dump_insert(); +create trigger parent_update_trig + after update on parent referencing old table as old_table new table as new_table + for each statement execute procedure dump_update(); +create trigger parent_delete_trig + after delete on parent referencing old table as old_table + for each statement execute procedure dump_delete(); +create trigger child1_insert_trig + after insert on child1 referencing new table as new_table + for each statement execute procedure dump_insert(); +create trigger child1_update_trig + after update on child1 referencing old table as old_table new table as new_table + for each statement execute procedure dump_update(); +create trigger child1_delete_trig + after delete on child1 referencing old table as old_table + for each statement execute procedure dump_delete(); +create trigger child2_insert_trig + after insert on child2 referencing new table as new_table + for each statement execute procedure dump_insert(); +create trigger child2_update_trig + after update on child2 referencing old table as old_table new table as new_table + for each statement execute procedure dump_update(); +create trigger child2_delete_trig + after delete on child2 referencing old table as old_table + for each statement execute procedure dump_delete(); +create trigger child3_insert_trig + after insert on child3 referencing new table as new_table + for each statement execute procedure dump_insert(); +create trigger child3_update_trig + after update on child3 referencing old table as old_table new table as new_table + for each statement execute procedure dump_update(); +create trigger child3_delete_trig + after delete on child3 referencing old table as old_table + for each statement execute procedure dump_delete(); +-- insert directly into children sees respective child-format tuples +insert into child1 values ('AAA', 42); +NOTICE: trigger = child1_insert_trig, new table = (AAA,42) +insert into child2 values (42, 'BBB'); +NOTICE: trigger = child2_insert_trig, new table = (42,BBB) +insert into child3 values ('CCC', 42, 'foo'); +NOTICE: trigger = child3_insert_trig, new table = (CCC,42,foo) +-- update via parent sees parent-format tuples +update parent set b = b + 1; +NOTICE: trigger = parent_update_trig, old table = (AAA,42), (BBB,42), (CCC,42), new table = (AAA,43), (BBB,43), (CCC,43) +-- delete via parent sees parent-format tuples +delete from parent; +NOTICE: trigger = parent_delete_trig, old table = (AAA,43), (BBB,43), (CCC,43) +-- reinsert values into children for next test... +insert into child1 values ('AAA', 42); +NOTICE: trigger = child1_insert_trig, new table = (AAA,42) +insert into child2 values (42, 'BBB'); +NOTICE: trigger = child2_insert_trig, new table = (42,BBB) +insert into child3 values ('CCC', 42, 'foo'); +NOTICE: trigger = child3_insert_trig, new table = (CCC,42,foo) +-- delete from children sees respective child-format tuples +delete from child1; +NOTICE: trigger = child1_delete_trig, old table = (AAA,42) +delete from child2; +NOTICE: trigger = child2_delete_trig, old table = (42,BBB) +delete from child3; +NOTICE: trigger = child3_delete_trig, old table = (CCC,42,foo) +-- copy into parent sees parent-format tuples (no rerouting, so these +-- are really inserted into the parent) +copy parent (a, b) from stdin; +NOTICE: trigger = parent_insert_trig, new table = (AAA,42), (BBB,42), (CCC,42) +-- DML affecting parent sees tuples collected from children even if +-- there is no transition table trigger on the children +drop trigger child1_insert_trig on child1; +drop trigger child1_update_trig on child1; +drop trigger child1_delete_trig on child1; +drop trigger child2_insert_trig on child2; +drop trigger child2_update_trig on child2; +drop trigger child2_delete_trig on child2; +drop trigger child3_insert_trig on child3; +drop trigger child3_update_trig on child3; +drop trigger child3_delete_trig on child3; +delete from parent; +NOTICE: trigger = parent_delete_trig, old table = (AAA,42), (BBB,42), (CCC,42) +drop table child1, child2, child3, parent; +-- +-- Verify prohibition of row triggers with transition triggers on +-- inheritance children +-- +create table parent (a text, b int); +create table child () inherits (parent); +-- adding row trigger with transition table fails +create trigger child_row_trig + after insert on child referencing new table as new_table + for each row execute procedure dump_insert(); +ERROR: ROW triggers with transition tables are not supported on inheritance children +-- disinheriting it first works +alter table child no inherit parent; +create trigger child_row_trig + after insert on child referencing new table as new_table + for each row execute procedure dump_insert(); +-- but now we're not allowed to make it inherit anymore +alter table child inherit parent; +ERROR: trigger "child_row_trig" prevents table "child" from becoming an inheritance child +DETAIL: ROW triggers with transition tables are not supported in inheritance hierarchies +-- drop the trigger, and now we're allowed to make it inherit again +drop trigger child_row_trig on child; +alter table child inherit parent; +drop table child, parent; +-- cleanup +drop function dump_insert(); +drop function dump_update(); +drop function dump_delete(); diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql index 9f2ed88f20..683a5f1e5c 100644 --- a/src/test/regress/sql/triggers.sql +++ b/src/test/regress/sql/triggers.sql @@ -1272,30 +1272,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(); -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 @@ -1391,3 +1367,344 @@ 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_insert() returns trigger language plpgsql as +$$ + begin + raise notice 'trigger = %, new table = %', + TG_NAME, + (select string_agg(new_table::text, ', ' order by a) from new_table); + return null; + end; +$$; + +create or replace function dump_update() returns trigger language plpgsql as +$$ + begin + raise notice 'trigger = %, old table = %, new table = %', + TG_NAME, + (select string_agg(old_table::text, ', ' order by a) from old_table), + (select string_agg(new_table::text, ', ' order by a) from new_table); + return null; + end; +$$; + +create or replace function dump_delete() returns trigger language plpgsql as +$$ + begin + raise notice 'trigger = %, old table = %', + TG_NAME, + (select string_agg(old_table::text, ', ' order by a) from old_table); + return null; + end; +$$; + +-- +-- Verify behavior of statement triggers on partition hierarchy with +-- transition tables. Tuples should appear to each trigger in the +-- format of the the relation the trigger is attached to. +-- + +-- set up a partition hierarchy with some different TupleDescriptors +create table parent (a text, b int) partition by list (a); + +-- a child matching parent +create table child1 partition of parent for values in ('AAA'); + +-- a child with a dropped column +create table child2 (x int, a text, b int); +alter table child2 drop column x; +alter table parent attach partition child2 for values in ('BBB'); + +-- a child with a different column order +create table child3 (b int, a text); +alter table parent attach partition child3 for values in ('CCC'); + +create trigger parent_insert_trig + after insert on parent referencing new table as new_table + for each statement execute procedure dump_insert(); +create trigger parent_update_trig + after update on parent referencing old table as old_table new table as new_table + for each statement execute procedure dump_update(); +create trigger parent_delete_trig + after delete on parent referencing old table as old_table + for each statement execute procedure dump_delete(); + +create trigger child1_insert_trig + after insert on child1 referencing new table as new_table + for each statement execute procedure dump_insert(); +create trigger child1_update_trig + after update on child1 referencing old table as old_table new table as new_table + for each statement execute procedure dump_update(); +create trigger child1_delete_trig + after delete on child1 referencing old table as old_table + for each statement execute procedure dump_delete(); + +create trigger child2_insert_trig + after insert on child2 referencing new table as new_table + for each statement execute procedure dump_insert(); +create trigger child2_update_trig + after update on child2 referencing old table as old_table new table as new_table + for each statement execute procedure dump_update(); +create trigger child2_delete_trig + after delete on child2 referencing old table as old_table + for each statement execute procedure dump_delete(); + +create trigger child3_insert_trig + after insert on child3 referencing new table as new_table + for each statement execute procedure dump_insert(); +create trigger child3_update_trig + after update on child3 referencing old table as old_table new table as new_table + for each statement execute procedure dump_update(); +create trigger child3_delete_trig + after delete on child3 referencing old table as old_table + for each statement execute procedure dump_delete(); + +-- insert directly into children sees respective child-format tuples +insert into child1 values ('AAA', 42); +insert into child2 values ('BBB', 42); +insert into child3 values (42, 'CCC'); + +-- update via parent sees parent-format tuples +update parent set b = b + 1; + +-- delete via parent sees parent-format tuples +delete from parent; + +-- insert into parent sees parent-format tuples +insert into parent values ('AAA', 42); +insert into parent values ('BBB', 42); +insert into parent values ('CCC', 42); + +-- delete from children sees respective child-format tuples +delete from child1; +delete from child2; +delete from child3; + +-- copy into parent sees parent-format tuples +copy parent (a, b) from stdin; +AAA 42 +BBB 42 +CCC 42 +\. + +-- DML affecting parent sees tuples collected from children even if +-- there is no transition table trigger on the children +drop trigger child1_insert_trig on child1; +drop trigger child1_update_trig on child1; +drop trigger child1_delete_trig on child1; +drop trigger child2_insert_trig on child2; +drop trigger child2_update_trig on child2; +drop trigger child2_delete_trig on child2; +drop trigger child3_insert_trig on child3; +drop trigger child3_update_trig on child3; +drop trigger child3_delete_trig on child3; +delete from parent; + +-- copy into parent sees tuples collected from children even if there +-- is no transition-table trigger on the children +copy parent (a, b) from stdin; +AAA 42 +BBB 42 +CCC 42 +\. + +-- insert into parent with a before trigger on a child tuple before +-- insertion, and we capture the newly modified row in parent format +create or replace function intercept_insert() returns trigger language plpgsql as +$$ + begin + new.b = new.b + 1000; + return new; + end; +$$; + +create trigger intercept_insert_child3 + before insert on child3 + for each row execute procedure intercept_insert(); + + +-- insert, parent trigger sees post-modification parent-format tuple +insert into parent values ('AAA', 42), ('BBB', 42), ('CCC', 66); + +-- copy, parent trigger sees post-modification parent-format tuple +copy parent (a, b) from stdin; +AAA 42 +BBB 42 +CCC 234 +\. + +drop table child1, child2, child3, parent; +drop function intercept_insert(); + +-- +-- Verify prohibition of row triggers with transition triggers on +-- partitions +-- +create table parent (a text, b int) partition by list (a); +create table child partition of parent for values in ('AAA'); + +-- adding row trigger with transition table fails +create trigger child_row_trig + after insert on child referencing new table as new_table + for each row execute procedure dump_insert(); + +-- detaching it first works +alter table parent detach partition child; + +create trigger child_row_trig + after insert on child referencing new table as new_table + for each row execute procedure dump_insert(); + +-- but now we're not allowed to reattach it +alter table parent attach partition child for values in ('AAA'); + +-- drop the trigger, and now we're allowed to attach it again +drop trigger child_row_trig on child; +alter table parent attach partition child for values in ('AAA'); + +drop table child, parent; + +-- +-- Verify behavior of statement triggers on (non-partition) +-- inheritance hierarchy with transition tables; similar to the +-- partition case, except there is no rerouting on insertion and child +-- tables can have extra columns +-- + +-- set up inheritance hierarchy with different TupleDescriptors +create table parent (a text, b int); + +-- a child matching parent +create table child1 () inherits (parent); + +-- a child with a different column order +create table child2 (b int, a text); +alter table child2 inherit parent; + +-- a child with an extra column +create table child3 (c text) inherits (parent); + +create trigger parent_insert_trig + after insert on parent referencing new table as new_table + for each statement execute procedure dump_insert(); +create trigger parent_update_trig + after update on parent referencing old table as old_table new table as new_table + for each statement execute procedure dump_update(); +create trigger parent_delete_trig + after delete on parent referencing old table as old_table + for each statement execute procedure dump_delete(); + +create trigger child1_insert_trig + after insert on child1 referencing new table as new_table + for each statement execute procedure dump_insert(); +create trigger child1_update_trig + after update on child1 referencing old table as old_table new table as new_table + for each statement execute procedure dump_update(); +create trigger child1_delete_trig + after delete on child1 referencing old table as old_table + for each statement execute procedure dump_delete(); + +create trigger child2_insert_trig + after insert on child2 referencing new table as new_table + for each statement execute procedure dump_insert(); +create trigger child2_update_trig + after update on child2 referencing old table as old_table new table as new_table + for each statement execute procedure dump_update(); +create trigger child2_delete_trig + after delete on child2 referencing old table as old_table + for each statement execute procedure dump_delete(); + +create trigger child3_insert_trig + after insert on child3 referencing new table as new_table + for each statement execute procedure dump_insert(); +create trigger child3_update_trig + after update on child3 referencing old table as old_table new table as new_table + for each statement execute procedure dump_update(); +create trigger child3_delete_trig + after delete on child3 referencing old table as old_table + for each statement execute procedure dump_delete(); + +-- insert directly into children sees respective child-format tuples +insert into child1 values ('AAA', 42); +insert into child2 values (42, 'BBB'); +insert into child3 values ('CCC', 42, 'foo'); + +-- update via parent sees parent-format tuples +update parent set b = b + 1; + +-- delete via parent sees parent-format tuples +delete from parent; + +-- reinsert values into children for next test... +insert into child1 values ('AAA', 42); +insert into child2 values (42, 'BBB'); +insert into child3 values ('CCC', 42, 'foo'); + +-- delete from children sees respective child-format tuples +delete from child1; +delete from child2; +delete from child3; + +-- copy into parent sees parent-format tuples (no rerouting, so these +-- are really inserted into the parent) +copy parent (a, b) from stdin; +AAA 42 +BBB 42 +CCC 42 +\. + +-- DML affecting parent sees tuples collected from children even if +-- there is no transition table trigger on the children +drop trigger child1_insert_trig on child1; +drop trigger child1_update_trig on child1; +drop trigger child1_delete_trig on child1; +drop trigger child2_insert_trig on child2; +drop trigger child2_update_trig on child2; +drop trigger child2_delete_trig on child2; +drop trigger child3_insert_trig on child3; +drop trigger child3_update_trig on child3; +drop trigger child3_delete_trig on child3; +delete from parent; + +drop table child1, child2, child3, parent; + +-- +-- Verify prohibition of row triggers with transition triggers on +-- inheritance children +-- +create table parent (a text, b int); +create table child () inherits (parent); + +-- adding row trigger with transition table fails +create trigger child_row_trig + after insert on child referencing new table as new_table + for each row execute procedure dump_insert(); + +-- disinheriting it first works +alter table child no inherit parent; + +create trigger child_row_trig + after insert on child referencing new table as new_table + for each row execute procedure dump_insert(); + +-- but now we're not allowed to make it inherit anymore +alter table child inherit parent; + +-- drop the trigger, and now we're allowed to make it inherit again +drop trigger child_row_trig on child; +alter table child inherit parent; + +drop table child, parent; + +-- cleanup +drop function dump_insert(); +drop function dump_update(); +drop function dump_delete();