From 501ed02cf6f4f60c3357775eb07578aebc912d3a Mon Sep 17 00:00:00 2001 From: Andrew Gierth Date: Wed, 28 Jun 2017 18:55:03 +0100 Subject: 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 --- src/backend/commands/copy.c | 70 ++++++++++++- src/backend/commands/tablecmds.c | 28 +++++ src/backend/commands/trigger.c | 218 ++++++++++++++++++++++++++++++++------- 3 files changed, 276 insertions(+), 40 deletions(-) (limited to 'src/backend/commands') 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; -- cgit v1.2.3 From c46c0e5202e8cfe750c6629db7852fdb15d528f3 Mon Sep 17 00:00:00 2001 From: Andrew Gierth Date: Wed, 28 Jun 2017 18:59:01 +0100 Subject: Fix transition tables for wCTEs. The original coding didn't handle this case properly; each separate DML substatement needs its own set of transitions. Patch by Thomas Munro Discussion: https://postgr.es/m/CAL9smLCDQ%3D2o024rBgtD4WihzX8B3C6u_oSQ2K3%2BR5grJrV0bg%40mail.gmail.com --- src/backend/commands/copy.c | 18 ++- src/backend/commands/trigger.c | 223 ++++++++++++++------------------- src/backend/executor/execReplication.c | 6 + src/backend/executor/nodeModifyTable.c | 16 ++- src/include/commands/trigger.h | 18 ++- src/test/regress/expected/triggers.out | 20 +++ src/test/regress/sql/triggers.sql | 21 ++++ 7 files changed, 174 insertions(+), 148 deletions(-) (limited to 'src/backend/commands') diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c index a4c02e6b7c..f391828e74 100644 --- a/src/backend/commands/copy.c +++ b/src/backend/commands/copy.c @@ -1416,6 +1416,12 @@ BeginCopy(ParseState *pstate, errmsg("table \"%s\" does not have OIDs", RelationGetRelationName(cstate->rel)))); + /* + * If there are any triggers with transition tables on the named + * relation, we need to be prepared to capture transition tuples. + */ + cstate->transition_capture = MakeTransitionCaptureState(rel->trigdesc); + /* Initialize state for CopyFrom tuple routing. */ if (is_from && rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) { @@ -1439,14 +1445,6 @@ BeginCopy(ParseState *pstate, 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 @@ -2807,7 +2805,7 @@ CopyFrom(CopyState cstate) pq_endmsgread(); /* Execute AFTER STATEMENT insertion triggers */ - ExecASInsertTriggers(estate, resultRelInfo); + ExecASInsertTriggers(estate, resultRelInfo, cstate->transition_capture); /* Handle queued AFTER triggers */ AfterTriggerEndQuery(estate); @@ -2935,7 +2933,7 @@ CopyFromInsertBatch(CopyState cstate, EState *estate, CommandId mycid, cstate->cur_lineno = firstBufferedLineNo + i; ExecARInsertTriggers(estate, resultRelInfo, bufferedTuples[i], - NIL, NULL); + NIL, cstate->transition_capture); } } diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c index f902e0cdf5..54db16c909 100644 --- a/src/backend/commands/trigger.c +++ b/src/backend/commands/trigger.c @@ -2071,9 +2071,10 @@ FindTriggerIncompatibleWithInheritance(TriggerDesc *trigdesc) /* * 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. + * are collected when tables are modified, and the tuplestores themselves. + * Note that we copy the flags from a parent table into this struct (rather + * than using each relation's TriggerDesc directly) so that we can use it to + * control the collection of transition tuples from child tables. * * If there are no triggers with transition tables configured for 'trigdesc', * then return NULL. @@ -2091,17 +2092,68 @@ MakeTransitionCaptureState(TriggerDesc *trigdesc) (trigdesc->trig_delete_old_table || trigdesc->trig_update_old_table || trigdesc->trig_update_new_table || trigdesc->trig_insert_new_table)) { + MemoryContext oldcxt; + ResourceOwner saveResourceOwner; + + /* + * Normally DestroyTransitionCaptureState should be called after + * executing all AFTER triggers for the current statement. + * + * To handle error cleanup, TransitionCaptureState and the tuplestores + * it contains will live in the current [sub]transaction's memory + * context. Likewise for the current resource owner, because we also + * want to clean up temporary files spilled to disk by the tuplestore + * in that scenario. This scope is sufficient, because AFTER triggers + * with transition tables cannot be deferred (only constraint triggers + * can be deferred, and constraint triggers cannot have transition + * tables). The AFTER trigger queue may contain pointers to this + * TransitionCaptureState, but any such entries will be processed or + * discarded before the end of the current [sub]transaction. + * + * If a future release allows deferred triggers with transition + * tables, we'll need to reconsider the scope of the + * TransitionCaptureState object. + */ + oldcxt = MemoryContextSwitchTo(CurTransactionContext); + saveResourceOwner = CurrentResourceOwner; + 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; + PG_TRY(); + { + CurrentResourceOwner = CurTransactionResourceOwner; + if (trigdesc->trig_delete_old_table || trigdesc->trig_update_old_table) + state->tcs_old_tuplestore = tuplestore_begin_heap(false, false, work_mem); + if (trigdesc->trig_insert_new_table || trigdesc->trig_update_new_table) + state->tcs_new_tuplestore = tuplestore_begin_heap(false, false, work_mem); + } + PG_CATCH(); + { + CurrentResourceOwner = saveResourceOwner; + PG_RE_THROW(); + } + PG_END_TRY(); + CurrentResourceOwner = saveResourceOwner; + MemoryContextSwitchTo(oldcxt); } return state; } +void +DestroyTransitionCaptureState(TransitionCaptureState *tcs) +{ + if (tcs->tcs_new_tuplestore != NULL) + tuplestore_end(tcs->tcs_new_tuplestore); + if (tcs->tcs_old_tuplestore != NULL) + tuplestore_end(tcs->tcs_old_tuplestore); + pfree(tcs); +} + /* * Call a trigger function. * @@ -2260,13 +2312,14 @@ ExecBSInsertTriggers(EState *estate, ResultRelInfo *relinfo) } void -ExecASInsertTriggers(EState *estate, ResultRelInfo *relinfo) +ExecASInsertTriggers(EState *estate, ResultRelInfo *relinfo, + TransitionCaptureState *transition_capture) { TriggerDesc *trigdesc = relinfo->ri_TrigDesc; if (trigdesc && trigdesc->trig_insert_after_statement) AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT, - false, NULL, NULL, NIL, NULL, NULL); + false, NULL, NULL, NIL, NULL, transition_capture); } TupleTableSlot * @@ -2343,7 +2396,6 @@ ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo, TriggerDesc *trigdesc = relinfo->ri_TrigDesc; 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, @@ -2470,13 +2522,14 @@ ExecBSDeleteTriggers(EState *estate, ResultRelInfo *relinfo) } void -ExecASDeleteTriggers(EState *estate, ResultRelInfo *relinfo) +ExecASDeleteTriggers(EState *estate, ResultRelInfo *relinfo, + TransitionCaptureState *transition_capture) { TriggerDesc *trigdesc = relinfo->ri_TrigDesc; if (trigdesc && trigdesc->trig_delete_after_statement) AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE, - false, NULL, NULL, NIL, NULL, NULL); + false, NULL, NULL, NIL, NULL, transition_capture); } bool @@ -2557,7 +2610,6 @@ ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo, TriggerDesc *trigdesc = relinfo->ri_TrigDesc; 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; @@ -2684,7 +2736,8 @@ ExecBSUpdateTriggers(EState *estate, ResultRelInfo *relinfo) } void -ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo) +ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo, + TransitionCaptureState *transition_capture) { TriggerDesc *trigdesc = relinfo->ri_TrigDesc; @@ -2692,7 +2745,7 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo) AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE, false, NULL, NULL, NIL, GetUpdatedColumns(relinfo, estate), - NULL); + transition_capture); } TupleTableSlot * @@ -2823,9 +2876,6 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo, TriggerDesc *trigdesc = relinfo->ri_TrigDesc; 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))) @@ -3362,6 +3412,7 @@ typedef struct AfterTriggerSharedData Oid ats_tgoid; /* the trigger's ID */ Oid ats_relid; /* the relation it's on */ CommandId ats_firing_id; /* ID for firing cycle */ + TransitionCaptureState *ats_transition_capture; } AfterTriggerSharedData; typedef struct AfterTriggerEventData *AfterTriggerEvent; @@ -3467,9 +3518,6 @@ typedef struct AfterTriggerEventList * fdw_tuplestores[query_depth] is a tuplestore containing the foreign tuples * needed for the current query. * - * old_tuplestores[query_depth] and new_tuplestores[query_depth] hold the - * transition relations for the current query. - * * maxquerydepth is just the allocated length of query_stack and the * tuplestores. * @@ -3502,8 +3550,6 @@ typedef struct AfterTriggersData AfterTriggerEventList *query_stack; /* events pending from each query */ Tuplestorestate **fdw_tuplestores; /* foreign tuples for one row from * each query */ - Tuplestorestate **old_tuplestores; /* all old tuples from each query */ - Tuplestorestate **new_tuplestores; /* all new tuples from each query */ int maxquerydepth; /* allocated len of above array */ MemoryContext event_cxt; /* memory context for events, if any */ @@ -3524,7 +3570,8 @@ static void AfterTriggerExecute(AfterTriggerEvent event, Instrumentation *instr, MemoryContext per_tuple_context, TupleTableSlot *trig_tuple_slot1, - TupleTableSlot *trig_tuple_slot2); + TupleTableSlot *trig_tuple_slot2, + TransitionCaptureState *transition_capture); static SetConstraintState SetConstraintStateCreate(int numalloc); static SetConstraintState SetConstraintStateCopy(SetConstraintState state); static SetConstraintState SetConstraintStateAddItem(SetConstraintState state, @@ -3533,8 +3580,6 @@ static SetConstraintState SetConstraintStateAddItem(SetConstraintState state, /* * Gets a current query transition tuplestore and initializes it if necessary. - * This can be holding a single transition row tuple (in the case of an FDW) - * or a transition table (for an AFTER trigger). */ static Tuplestorestate * GetTriggerTransitionTuplestore(Tuplestorestate **tss) @@ -3714,6 +3759,7 @@ afterTriggerAddEvent(AfterTriggerEventList *events, if (newshared->ats_tgoid == evtshared->ats_tgoid && newshared->ats_relid == evtshared->ats_relid && newshared->ats_event == evtshared->ats_event && + newshared->ats_transition_capture == evtshared->ats_transition_capture && newshared->ats_firing_id == 0) break; } @@ -3825,7 +3871,8 @@ AfterTriggerExecute(AfterTriggerEvent event, FmgrInfo *finfo, Instrumentation *instr, MemoryContext per_tuple_context, TupleTableSlot *trig_tuple_slot1, - TupleTableSlot *trig_tuple_slot2) + TupleTableSlot *trig_tuple_slot2, + TransitionCaptureState *transition_capture) { AfterTriggerShared evtshared = GetTriggerSharedData(event); Oid tgoid = evtshared->ats_tgoid; @@ -3940,16 +3987,14 @@ AfterTriggerExecute(AfterTriggerEvent event, /* * Set up the tuplestore information. */ - if (LocTriggerData.tg_trigger->tgoldtable) - LocTriggerData.tg_oldtable = - GetTriggerTransitionTuplestore(afterTriggers.old_tuplestores); - else - LocTriggerData.tg_oldtable = NULL; - if (LocTriggerData.tg_trigger->tgnewtable) - LocTriggerData.tg_newtable = - GetTriggerTransitionTuplestore(afterTriggers.new_tuplestores); - else - LocTriggerData.tg_newtable = NULL; + LocTriggerData.tg_oldtable = LocTriggerData.tg_newtable = NULL; + if (transition_capture != NULL) + { + if (LocTriggerData.tg_trigger->tgoldtable) + LocTriggerData.tg_oldtable = transition_capture->tcs_old_tuplestore; + if (LocTriggerData.tg_trigger->tgnewtable) + LocTriggerData.tg_newtable = transition_capture->tcs_new_tuplestore; + } /* * Setup the remaining trigger information @@ -4157,7 +4202,8 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events, * won't try to re-fire it. */ AfterTriggerExecute(event, rel, trigdesc, finfo, instr, - per_tuple_context, slot1, slot2); + per_tuple_context, slot1, slot2, + evtshared->ats_transition_capture); /* * Mark the event as done. @@ -4231,8 +4277,6 @@ AfterTriggerBeginXact(void) Assert(afterTriggers.state == NULL); Assert(afterTriggers.query_stack == NULL); Assert(afterTriggers.fdw_tuplestores == NULL); - Assert(afterTriggers.old_tuplestores == NULL); - Assert(afterTriggers.new_tuplestores == NULL); Assert(afterTriggers.maxquerydepth == 0); Assert(afterTriggers.event_cxt == NULL); Assert(afterTriggers.events.head == NULL); @@ -4277,8 +4321,6 @@ AfterTriggerEndQuery(EState *estate) { AfterTriggerEventList *events; Tuplestorestate *fdw_tuplestore; - Tuplestorestate *old_tuplestore; - Tuplestorestate *new_tuplestore; /* Must be inside a query, too */ Assert(afterTriggers.query_depth >= 0); @@ -4337,18 +4379,6 @@ AfterTriggerEndQuery(EState *estate) tuplestore_end(fdw_tuplestore); afterTriggers.fdw_tuplestores[afterTriggers.query_depth] = NULL; } - old_tuplestore = afterTriggers.old_tuplestores[afterTriggers.query_depth]; - if (old_tuplestore) - { - tuplestore_end(old_tuplestore); - afterTriggers.old_tuplestores[afterTriggers.query_depth] = NULL; - } - new_tuplestore = afterTriggers.new_tuplestores[afterTriggers.query_depth]; - if (new_tuplestore) - { - tuplestore_end(new_tuplestore); - afterTriggers.new_tuplestores[afterTriggers.query_depth] = NULL; - } afterTriggerFreeEventList(&afterTriggers.query_stack[afterTriggers.query_depth]); afterTriggers.query_depth--; @@ -4462,8 +4492,6 @@ AfterTriggerEndXact(bool isCommit) */ afterTriggers.query_stack = NULL; afterTriggers.fdw_tuplestores = NULL; - afterTriggers.old_tuplestores = NULL; - afterTriggers.new_tuplestores = NULL; afterTriggers.maxquerydepth = 0; afterTriggers.state = NULL; @@ -4596,18 +4624,6 @@ AfterTriggerEndSubXact(bool isCommit) tuplestore_end(ts); afterTriggers.fdw_tuplestores[afterTriggers.query_depth] = NULL; } - ts = afterTriggers.old_tuplestores[afterTriggers.query_depth]; - if (ts) - { - tuplestore_end(ts); - afterTriggers.old_tuplestores[afterTriggers.query_depth] = NULL; - } - ts = afterTriggers.new_tuplestores[afterTriggers.query_depth]; - if (ts) - { - tuplestore_end(ts); - afterTriggers.new_tuplestores[afterTriggers.query_depth] = NULL; - } afterTriggerFreeEventList(&afterTriggers.query_stack[afterTriggers.query_depth]); } @@ -4687,12 +4703,6 @@ AfterTriggerEnlargeQueryState(void) afterTriggers.fdw_tuplestores = (Tuplestorestate **) MemoryContextAllocZero(TopTransactionContext, new_alloc * sizeof(Tuplestorestate *)); - afterTriggers.old_tuplestores = (Tuplestorestate **) - MemoryContextAllocZero(TopTransactionContext, - new_alloc * sizeof(Tuplestorestate *)); - afterTriggers.new_tuplestores = (Tuplestorestate **) - MemoryContextAllocZero(TopTransactionContext, - new_alloc * sizeof(Tuplestorestate *)); afterTriggers.maxquerydepth = new_alloc; } else @@ -4708,19 +4718,9 @@ AfterTriggerEnlargeQueryState(void) afterTriggers.fdw_tuplestores = (Tuplestorestate **) repalloc(afterTriggers.fdw_tuplestores, new_alloc * sizeof(Tuplestorestate *)); - afterTriggers.old_tuplestores = (Tuplestorestate **) - repalloc(afterTriggers.old_tuplestores, - new_alloc * sizeof(Tuplestorestate *)); - afterTriggers.new_tuplestores = (Tuplestorestate **) - repalloc(afterTriggers.new_tuplestores, - new_alloc * sizeof(Tuplestorestate *)); /* Clear newly-allocated slots for subsequent lazy initialization. */ memset(afterTriggers.fdw_tuplestores + old_alloc, 0, (new_alloc - old_alloc) * sizeof(Tuplestorestate *)); - memset(afterTriggers.old_tuplestores + old_alloc, - 0, (new_alloc - old_alloc) * sizeof(Tuplestorestate *)); - memset(afterTriggers.new_tuplestores + old_alloc, - 0, (new_alloc - old_alloc) * sizeof(Tuplestorestate *)); afterTriggers.maxquerydepth = new_alloc; } @@ -5205,51 +5205,17 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo, AfterTriggerEnlargeQueryState(); /* - * If the relation has AFTER ... FOR EACH ROW triggers, capture rows into - * transition tuplestores for this depth. + * If the directly named relation has any triggers with transition tables, + * then we need to capture transition tuples. */ - if (row_trigger) + if (row_trigger && transition_capture != NULL) { - 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; - } + HeapTuple original_insert_tuple = transition_capture->tcs_original_insert_tuple; + TupleConversionMap *map = transition_capture->tcs_map; + bool delete_old_table = transition_capture->tcs_delete_old_table; + bool update_old_table = transition_capture->tcs_update_old_table; + bool update_new_table = transition_capture->tcs_update_new_table; + bool insert_new_table = transition_capture->tcs_insert_new_table;; if ((event == TRIGGER_EVENT_DELETE && delete_old_table) || (event == TRIGGER_EVENT_UPDATE && update_old_table)) @@ -5257,9 +5223,8 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo, Tuplestorestate *old_tuplestore; Assert(oldtup != NULL); - old_tuplestore = - GetTriggerTransitionTuplestore - (afterTriggers.old_tuplestores); + old_tuplestore = transition_capture->tcs_old_tuplestore; + if (map != NULL) { HeapTuple converted = do_convert_tuple(oldtup, map); @@ -5276,9 +5241,8 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo, Tuplestorestate *new_tuplestore; Assert(newtup != NULL); - new_tuplestore = - GetTriggerTransitionTuplestore - (afterTriggers.new_tuplestores); + new_tuplestore = transition_capture->tcs_new_tuplestore; + if (original_insert_tuple != NULL) tuplestore_puttuple(new_tuplestore, original_insert_tuple); else if (map != NULL) @@ -5464,6 +5428,7 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo, new_shared.ats_tgoid = trigger->tgoid; new_shared.ats_relid = RelationGetRelid(rel); new_shared.ats_firing_id = 0; + new_shared.ats_transition_capture = transition_capture; afterTriggerAddEvent(&afterTriggers.query_stack[afterTriggers.query_depth], &new_event, &new_shared); diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c index 36960eaa7e..bc53d07c7d 100644 --- a/src/backend/executor/execReplication.c +++ b/src/backend/executor/execReplication.c @@ -419,6 +419,12 @@ ExecSimpleRelationInsert(EState *estate, TupleTableSlot *slot) ExecARInsertTriggers(estate, resultRelInfo, tuple, recheckIndexes, NULL); + /* + * XXX we should in theory pass a TransitionCaptureState object to the + * above to capture transition tuples, but after statement triggers + * don't actually get fired by replication yet anyway + */ + list_free(recheckIndexes); } } diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index f2534f2062..8d17425abe 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -1442,14 +1442,18 @@ fireASTriggers(ModifyTableState *node) case CMD_INSERT: if (node->mt_onconflict == ONCONFLICT_UPDATE) ExecASUpdateTriggers(node->ps.state, - resultRelInfo); - ExecASInsertTriggers(node->ps.state, resultRelInfo); + resultRelInfo, + node->mt_transition_capture); + ExecASInsertTriggers(node->ps.state, resultRelInfo, + node->mt_transition_capture); break; case CMD_UPDATE: - ExecASUpdateTriggers(node->ps.state, resultRelInfo); + ExecASUpdateTriggers(node->ps.state, resultRelInfo, + node->mt_transition_capture); break; case CMD_DELETE: - ExecASDeleteTriggers(node->ps.state, resultRelInfo); + ExecASDeleteTriggers(node->ps.state, resultRelInfo, + node->mt_transition_capture); break; default: elog(ERROR, "unknown operation"); @@ -2304,6 +2308,10 @@ ExecEndModifyTable(ModifyTableState *node) { int i; + /* Free transition tables */ + if (node->mt_transition_capture != NULL) + DestroyTransitionCaptureState(node->mt_transition_capture); + /* * Allow any FDWs to shut down */ diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h index 51a25c8ddc..06199953fe 100644 --- a/src/include/commands/trigger.h +++ b/src/include/commands/trigger.h @@ -42,8 +42,8 @@ typedef struct TriggerData } TriggerData; /* - * Meta-data to control the capture of old and new tuples into transition - * tables from child tables. + * The state for capturing old and new tuples into transition tables for a + * single ModifyTable node. */ typedef struct TransitionCaptureState { @@ -72,6 +72,10 @@ typedef struct TransitionCaptureState * the original tuple directly. */ HeapTuple tcs_original_insert_tuple; + + /* The tuplestores backing the transition tables. */ + Tuplestorestate *tcs_old_tuplestore; + Tuplestorestate *tcs_new_tuplestore; } TransitionCaptureState; /* @@ -162,13 +166,15 @@ extern TriggerDesc *CopyTriggerDesc(TriggerDesc *trigdesc); extern const char *FindTriggerIncompatibleWithInheritance(TriggerDesc *trigdesc); extern TransitionCaptureState *MakeTransitionCaptureState(TriggerDesc *trigdesc); +extern void DestroyTransitionCaptureState(TransitionCaptureState *tcs); extern void FreeTriggerDesc(TriggerDesc *trigdesc); extern void ExecBSInsertTriggers(EState *estate, ResultRelInfo *relinfo); extern void ExecASInsertTriggers(EState *estate, - ResultRelInfo *relinfo); + ResultRelInfo *relinfo, + TransitionCaptureState *transition_capture); extern TupleTableSlot *ExecBRInsertTriggers(EState *estate, ResultRelInfo *relinfo, TupleTableSlot *slot); @@ -183,7 +189,8 @@ extern TupleTableSlot *ExecIRInsertTriggers(EState *estate, extern void ExecBSDeleteTriggers(EState *estate, ResultRelInfo *relinfo); extern void ExecASDeleteTriggers(EState *estate, - ResultRelInfo *relinfo); + ResultRelInfo *relinfo, + TransitionCaptureState *transition_capture); extern bool ExecBRDeleteTriggers(EState *estate, EPQState *epqstate, ResultRelInfo *relinfo, @@ -200,7 +207,8 @@ extern bool ExecIRDeleteTriggers(EState *estate, extern void ExecBSUpdateTriggers(EState *estate, ResultRelInfo *relinfo); extern void ExecASUpdateTriggers(EState *estate, - ResultRelInfo *relinfo); + ResultRelInfo *relinfo, + TransitionCaptureState *transition_capture); extern TupleTableSlot *ExecBRUpdateTriggers(EState *estate, EPQState *epqstate, ResultRelInfo *relinfo, diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out index 995410f1aa..0261d66e5c 100644 --- a/src/test/regress/expected/triggers.out +++ b/src/test/regress/expected/triggers.out @@ -2194,6 +2194,26 @@ DETAIL: ROW triggers with transition tables are not supported in inheritance hi drop trigger child_row_trig on child; alter table child inherit parent; drop table child, parent; +-- +-- Verify behavior of queries with wCTEs, where multiple transition +-- tuplestores can be active at the same time because there are +-- multiple DML statements that might fire triggers with transition +-- tables +-- +create table table1 (a int); +create table table2 (a text); +create trigger table1_trig + after insert on table1 referencing new table as new_table + for each statement execute procedure dump_insert(); +create trigger table2_trig + after insert on table2 referencing new table as new_table + for each statement execute procedure dump_insert(); +with wcte as (insert into table1 values (42)) + insert into table2 values ('hello world'); +NOTICE: trigger = table2_trig, new table = ("hello world") +NOTICE: trigger = table1_trig, new table = (42) +drop table table1; +drop table table2; -- cleanup drop function dump_insert(); drop function dump_update(); diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql index 683a5f1e5c..128126a0f1 100644 --- a/src/test/regress/sql/triggers.sql +++ b/src/test/regress/sql/triggers.sql @@ -1704,6 +1704,27 @@ alter table child inherit parent; drop table child, parent; +-- +-- Verify behavior of queries with wCTEs, where multiple transition +-- tuplestores can be active at the same time because there are +-- multiple DML statements that might fire triggers with transition +-- tables +-- +create table table1 (a int); +create table table2 (a text); +create trigger table1_trig + after insert on table1 referencing new table as new_table + for each statement execute procedure dump_insert(); +create trigger table2_trig + after insert on table2 referencing new table as new_table + for each statement execute procedure dump_insert(); + +with wcte as (insert into table1 values (42)) + insert into table2 values ('hello world'); + +drop table table1; +drop table table2; + -- cleanup drop function dump_insert(); drop function dump_update(); -- cgit v1.2.3 From 8c55244ae379822d8bf62f6db0b5b1f7637eea3a Mon Sep 17 00:00:00 2001 From: Andrew Gierth Date: Wed, 28 Jun 2017 19:00:55 +0100 Subject: Fix transition tables for ON CONFLICT. We now disallow having triggers with both transition tables and ON INSERT OR UPDATE (which was a PG extension to the spec anyway), because in this case it's not at all clear how the transition tables should work for an INSERT ... ON CONFLICT query. Separate ON INSERT and ON UPDATE triggers with transition tables are allowed, and the transition tables for these reflect only the inserted and only the updated tuples respectively. Patch by Thomas Munro Discussion: https://postgr.es/m/CAEepm%3D11KHQ0JmETJQihSvhZB5mUZL2xrqHeXbCeLhDiqQ39%3Dw%40mail.gmail.com --- src/backend/commands/trigger.c | 58 ++++++++++++++++++++++++++++++---- src/include/commands/trigger.h | 14 ++++++-- src/test/regress/expected/plpgsql.out | 9 ++++-- src/test/regress/expected/triggers.out | 38 ++++++++++++++++++++++ src/test/regress/sql/plpgsql.sql | 10 ++++-- src/test/regress/sql/triggers.sql | 39 +++++++++++++++++++++++ 6 files changed, 155 insertions(+), 13 deletions(-) (limited to 'src/backend/commands') diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c index 54db16c909..b502941b08 100644 --- a/src/backend/commands/trigger.c +++ b/src/backend/commands/trigger.c @@ -401,6 +401,23 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("TRUNCATE triggers with transition tables are not supported"))); + /* + * We currently don't allow multi-event triggers ("INSERT OR + * UPDATE") with transition tables, because it's not clear how to + * handle INSERT ... ON CONFLICT statements which can fire both + * INSERT and UPDATE triggers. We show the inserted tuples to + * INSERT triggers and the updated tuples to UPDATE triggers, but + * it's not yet clear what INSERT OR UPDATE trigger should see. + * This restriction could be lifted if we can decide on the right + * semantics in a later release. + */ + if (((TRIGGER_FOR_INSERT(tgtype) ? 1 : 0) + + (TRIGGER_FOR_UPDATE(tgtype) ? 1 : 0) + + (TRIGGER_FOR_DELETE(tgtype) ? 1 : 0)) != 1) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("Transition tables cannot be specified for triggers with more than one event"))); + if (tt->isNew) { if (!(TRIGGER_FOR_INSERT(tgtype) || @@ -2128,8 +2145,10 @@ MakeTransitionCaptureState(TriggerDesc *trigdesc) CurrentResourceOwner = CurTransactionResourceOwner; if (trigdesc->trig_delete_old_table || trigdesc->trig_update_old_table) state->tcs_old_tuplestore = tuplestore_begin_heap(false, false, work_mem); - if (trigdesc->trig_insert_new_table || trigdesc->trig_update_new_table) - state->tcs_new_tuplestore = tuplestore_begin_heap(false, false, work_mem); + if (trigdesc->trig_insert_new_table) + state->tcs_insert_tuplestore = tuplestore_begin_heap(false, false, work_mem); + if (trigdesc->trig_update_new_table) + state->tcs_update_tuplestore = tuplestore_begin_heap(false, false, work_mem); } PG_CATCH(); { @@ -2147,8 +2166,10 @@ MakeTransitionCaptureState(TriggerDesc *trigdesc) void DestroyTransitionCaptureState(TransitionCaptureState *tcs) { - if (tcs->tcs_new_tuplestore != NULL) - tuplestore_end(tcs->tcs_new_tuplestore); + if (tcs->tcs_insert_tuplestore != NULL) + tuplestore_end(tcs->tcs_insert_tuplestore); + if (tcs->tcs_update_tuplestore != NULL) + tuplestore_end(tcs->tcs_update_tuplestore); if (tcs->tcs_old_tuplestore != NULL) tuplestore_end(tcs->tcs_old_tuplestore); pfree(tcs); @@ -3993,7 +4014,29 @@ AfterTriggerExecute(AfterTriggerEvent event, if (LocTriggerData.tg_trigger->tgoldtable) LocTriggerData.tg_oldtable = transition_capture->tcs_old_tuplestore; if (LocTriggerData.tg_trigger->tgnewtable) - LocTriggerData.tg_newtable = transition_capture->tcs_new_tuplestore; + { + /* + * Currently a trigger with transition tables may only be defined + * for a single event type (here AFTER INSERT or AFTER UPDATE, but + * not AFTER INSERT OR ...). + */ + Assert((TRIGGER_FOR_INSERT(LocTriggerData.tg_trigger->tgtype) != 0) ^ + (TRIGGER_FOR_UPDATE(LocTriggerData.tg_trigger->tgtype) != 0)); + + /* + * Show either the insert or update new tuple images, depending on + * which event type the trigger was registered for. A single + * statement may have produced both in the case of INSERT ... ON + * CONFLICT ... DO UPDATE, and in that case the event determines + * which tuplestore the trigger sees as the NEW TABLE. + */ + if (TRIGGER_FOR_INSERT(LocTriggerData.tg_trigger->tgtype)) + LocTriggerData.tg_newtable = + transition_capture->tcs_insert_tuplestore; + else + LocTriggerData.tg_newtable = + transition_capture->tcs_update_tuplestore; + } } /* @@ -5241,7 +5284,10 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo, Tuplestorestate *new_tuplestore; Assert(newtup != NULL); - new_tuplestore = transition_capture->tcs_new_tuplestore; + if (event == TRIGGER_EVENT_INSERT) + new_tuplestore = transition_capture->tcs_insert_tuplestore; + else + new_tuplestore = transition_capture->tcs_update_tuplestore; if (original_insert_tuple != NULL) tuplestore_puttuple(new_tuplestore, original_insert_tuple); diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h index 06199953fe..36c1134b64 100644 --- a/src/include/commands/trigger.h +++ b/src/include/commands/trigger.h @@ -73,9 +73,17 @@ typedef struct TransitionCaptureState */ HeapTuple tcs_original_insert_tuple; - /* The tuplestores backing the transition tables. */ - Tuplestorestate *tcs_old_tuplestore; - Tuplestorestate *tcs_new_tuplestore; + /* + * The tuplestores backing the transition tables. We use separate + * tuplestores for INSERT and UPDATE, because INSERT ... ON CONFLICT + * ... DO UPDATE causes INSERT and UPDATE triggers to fire and needs a way + * to keep track of the new tuple images resulting from the two cases + * separately. We only need a single old image tuplestore, because there + * is no statement that can both update and delete at the same time. + */ + Tuplestorestate *tcs_old_tuplestore; /* for DELETE and UPDATE old images */ + Tuplestorestate *tcs_insert_tuplestore; /* for INSERT new images */ + Tuplestorestate *tcs_update_tuplestore; /* for UPDATE new images */ } TransitionCaptureState; /* diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out index 35b83f7b00..71099969a4 100644 --- a/src/test/regress/expected/plpgsql.out +++ b/src/test/regress/expected/plpgsql.out @@ -5847,8 +5847,13 @@ AS $$ RETURN NULL; END; $$; -CREATE TRIGGER transition_table_level2_ri_child_insupd_trigger - AFTER INSERT OR UPDATE ON transition_table_level2 +CREATE TRIGGER transition_table_level2_ri_child_ins_trigger + AFTER INSERT ON transition_table_level2 + REFERENCING NEW TABLE AS i + FOR EACH STATEMENT EXECUTE PROCEDURE + transition_table_level2_ri_child_insupd_func(); +CREATE TRIGGER transition_table_level2_ri_child_upd_trigger + AFTER UPDATE ON transition_table_level2 REFERENCING NEW TABLE AS i FOR EACH STATEMENT EXECUTE PROCEDURE transition_table_level2_ri_child_insupd_func(); diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out index 0261d66e5c..aaee30219a 100644 --- a/src/test/regress/expected/triggers.out +++ b/src/test/regress/expected/triggers.out @@ -2214,6 +2214,44 @@ NOTICE: trigger = table2_trig, new table = ("hello world") NOTICE: trigger = table1_trig, new table = (42) drop table table1; drop table table2; +-- +-- Verify behavior of INSERT ... ON CONFLICT DO UPDATE ... with +-- transition tables. +-- +create table my_table (a int primary key, b text); +create trigger my_table_insert_trig + after insert on my_table referencing new table as new_table + for each statement execute procedure dump_insert(); +create trigger my_table_update_trig + after update on my_table referencing old table as old_table new table as new_table + for each statement execute procedure dump_update(); +-- inserts only +insert into my_table values (1, 'AAA'), (2, 'BBB') + on conflict (a) do + update set b = my_table.b || ':' || excluded.b; +NOTICE: trigger = my_table_update_trig, old table = , new table = +NOTICE: trigger = my_table_insert_trig, new table = (1,AAA), (2,BBB) +-- mixture of inserts and updates +insert into my_table values (1, 'AAA'), (2, 'BBB'), (3, 'CCC'), (4, 'DDD') + on conflict (a) do + update set b = my_table.b || ':' || excluded.b; +NOTICE: trigger = my_table_update_trig, old table = (1,AAA), (2,BBB), new table = (1,AAA:AAA), (2,BBB:BBB) +NOTICE: trigger = my_table_insert_trig, new table = (3,CCC), (4,DDD) +-- updates only +insert into my_table values (3, 'CCC'), (4, 'DDD') + on conflict (a) do + update set b = my_table.b || ':' || excluded.b; +NOTICE: trigger = my_table_update_trig, old table = (3,CCC), (4,DDD), new table = (3,CCC:CCC), (4,DDD:DDD) +NOTICE: trigger = my_table_insert_trig, new table = +-- +-- Verify that you can't create a trigger with transition tables for +-- more than one event. +-- +create trigger my_table_multievent_trig + after insert or update on my_table referencing new table as new_table + for each statement execute procedure dump_insert(); +ERROR: Transition tables cannot be specified for triggers with more than one event +drop table my_table; -- cleanup drop function dump_insert(); drop function dump_update(); diff --git a/src/test/regress/sql/plpgsql.sql b/src/test/regress/sql/plpgsql.sql index f957d70c5f..771d68282e 100644 --- a/src/test/regress/sql/plpgsql.sql +++ b/src/test/regress/sql/plpgsql.sql @@ -4639,8 +4639,14 @@ AS $$ END; $$; -CREATE TRIGGER transition_table_level2_ri_child_insupd_trigger - AFTER INSERT OR UPDATE ON transition_table_level2 +CREATE TRIGGER transition_table_level2_ri_child_ins_trigger + AFTER INSERT ON transition_table_level2 + REFERENCING NEW TABLE AS i + FOR EACH STATEMENT EXECUTE PROCEDURE + transition_table_level2_ri_child_insupd_func(); + +CREATE TRIGGER transition_table_level2_ri_child_upd_trigger + AFTER UPDATE ON transition_table_level2 REFERENCING NEW TABLE AS i FOR EACH STATEMENT EXECUTE PROCEDURE transition_table_level2_ri_child_insupd_func(); diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql index 128126a0f1..659a5a1422 100644 --- a/src/test/regress/sql/triggers.sql +++ b/src/test/regress/sql/triggers.sql @@ -1725,6 +1725,45 @@ with wcte as (insert into table1 values (42)) drop table table1; drop table table2; +-- +-- Verify behavior of INSERT ... ON CONFLICT DO UPDATE ... with +-- transition tables. +-- + +create table my_table (a int primary key, b text); +create trigger my_table_insert_trig + after insert on my_table referencing new table as new_table + for each statement execute procedure dump_insert(); +create trigger my_table_update_trig + after update on my_table referencing old table as old_table new table as new_table + for each statement execute procedure dump_update(); + +-- inserts only +insert into my_table values (1, 'AAA'), (2, 'BBB') + on conflict (a) do + update set b = my_table.b || ':' || excluded.b; + +-- mixture of inserts and updates +insert into my_table values (1, 'AAA'), (2, 'BBB'), (3, 'CCC'), (4, 'DDD') + on conflict (a) do + update set b = my_table.b || ':' || excluded.b; + +-- updates only +insert into my_table values (3, 'CCC'), (4, 'DDD') + on conflict (a) do + update set b = my_table.b || ':' || excluded.b; + +-- +-- Verify that you can't create a trigger with transition tables for +-- more than one event. +-- + +create trigger my_table_multievent_trig + after insert or update on my_table referencing new table as new_table + for each statement execute procedure dump_insert(); + +drop table my_table; + -- cleanup drop function dump_insert(); drop function dump_update(); -- cgit v1.2.3 From 54baa48139ae6b67347bea6a9183d494e625939b Mon Sep 17 00:00:00 2001 From: Peter Eisentraut Date: Fri, 30 Jun 2017 08:50:39 -0400 Subject: Copy collencoding in CREATE COLLATION / FROM This command used to compute the collencoding entry like when a completely new collation is created. But for example when copying the "C" collation, this would then result in a collation that has a collencoding entry for the current database encoding rather than -1, thus not making an exact copy. This has probably no practical impact, but making this change keeps the catalog contents neat. Reported-by: Tom Lane --- src/backend/commands/collationcmds.c | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) (limited to 'src/backend/commands') diff --git a/src/backend/commands/collationcmds.c b/src/backend/commands/collationcmds.c index 7f2ce4db4c..418f7342c7 100644 --- a/src/backend/commands/collationcmds.c +++ b/src/backend/commands/collationcmds.c @@ -64,7 +64,7 @@ DefineCollation(ParseState *pstate, List *names, List *parameters, bool if_not_e char *collcollate = NULL; char *collctype = NULL; char *collproviderstr = NULL; - int collencoding; + int collencoding = 0; char collprovider = 0; char *collversion = NULL; Oid newoid; @@ -126,6 +126,7 @@ DefineCollation(ParseState *pstate, List *names, List *parameters, bool if_not_e collcollate = pstrdup(NameStr(((Form_pg_collation) GETSTRUCT(tp))->collcollate)); collctype = pstrdup(NameStr(((Form_pg_collation) GETSTRUCT(tp))->collctype)); collprovider = ((Form_pg_collation) GETSTRUCT(tp))->collprovider; + collencoding = ((Form_pg_collation) GETSTRUCT(tp))->collencoding; ReleaseSysCache(tp); @@ -185,12 +186,15 @@ DefineCollation(ParseState *pstate, List *names, List *parameters, bool if_not_e (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), errmsg("parameter \"lc_ctype\" must be specified"))); - if (collprovider == COLLPROVIDER_ICU) - collencoding = -1; - else + if (!fromEl) { - collencoding = GetDatabaseEncoding(); - check_encoding_locale_matches(collencoding, collcollate, collctype); + if (collprovider == COLLPROVIDER_ICU) + collencoding = -1; + else + { + collencoding = GetDatabaseEncoding(); + check_encoding_locale_matches(collencoding, collcollate, collctype); + } } if (!collversion) -- cgit v1.2.3 From cb9079cd51a2df677dc182aec72d88383b9c2a79 Mon Sep 17 00:00:00 2001 From: Peter Eisentraut Date: Mon, 3 Jul 2017 22:47:06 -0400 Subject: Improve subscription locking This avoids "tuple concurrently updated" errors when a ALTER or DROP SUBSCRIPTION writes to pg_subscription_rel at the same time as a worker. Author: Petr Jelinek --- src/backend/catalog/pg_subscription.c | 4 ++++ src/backend/commands/subscriptioncmds.c | 3 +++ 2 files changed, 7 insertions(+) (limited to 'src/backend/commands') diff --git a/src/backend/catalog/pg_subscription.c b/src/backend/catalog/pg_subscription.c index c69c461b62..fb53d71cd6 100644 --- a/src/backend/catalog/pg_subscription.c +++ b/src/backend/catalog/pg_subscription.c @@ -28,6 +28,8 @@ #include "nodes/makefuncs.h" +#include "storage/lmgr.h" + #include "utils/array.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -246,6 +248,8 @@ SetSubscriptionRelState(Oid subid, Oid relid, char state, bool nulls[Natts_pg_subscription_rel]; Datum values[Natts_pg_subscription_rel]; + LockSharedObject(SubscriptionRelationId, subid, 0, AccessShareLock); + rel = heap_open(SubscriptionRelRelationId, RowExclusiveLock); /* Try finding existing mapping. */ diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c index 9cbd36f646..6dc3f6ee00 100644 --- a/src/backend/commands/subscriptioncmds.c +++ b/src/backend/commands/subscriptioncmds.c @@ -644,6 +644,9 @@ AlterSubscription(AlterSubscriptionStmt *stmt) subid = HeapTupleGetOid(tup); sub = GetSubscription(subid, false); + /* Lock the subscription so nobody else can do anything with it. */ + LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock); + /* Form a new tuple. */ memset(values, 0, sizeof(values)); memset(nulls, false, sizeof(nulls)); -- cgit v1.2.3 From 46e91519425c5e98380c672d1b5c3acf18c5e565 Mon Sep 17 00:00:00 2001 From: Alvaro Herrera Date: Fri, 7 Jul 2017 17:10:36 -0400 Subject: Fix typo Noticed while reviewing code. --- src/backend/commands/vacuumlazy.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/backend/commands') diff --git a/src/backend/commands/vacuumlazy.c b/src/backend/commands/vacuumlazy.c index 881c7356db..30a0050ae1 100644 --- a/src/backend/commands/vacuumlazy.c +++ b/src/backend/commands/vacuumlazy.c @@ -529,7 +529,7 @@ lazy_scan_heap(Relation onerel, int options, LVRelStats *vacrelstats, * safely set for relfrozenxid or relminmxid. * * Before entering the main loop, establish the invariant that - * next_unskippable_block is the next block number >= blkno that's not we + * next_unskippable_block is the next block number >= blkno that we * can't skip based on the visibility map, either all-visible for a * regular scan or all-frozen for an aggressive scan. We set it to * nblocks if there's no such block. We also set up the skipping_blocks -- cgit v1.2.3 From ec4073f64130b40fabaa1b38ad402babda3a48eb Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Sat, 8 Jul 2017 12:42:25 -0400 Subject: Avoid unreferenced-function warning on low-functionality platforms. On platforms lacking both locale_t and ICU, collationcmds.c failed to make any use of its static function is_all_ascii(), thus probably drawing a compiler warning. Oversight in my commit ddb5fdc06. Per buildfarm member gaur. --- src/backend/commands/collationcmds.c | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) (limited to 'src/backend/commands') diff --git a/src/backend/commands/collationcmds.c b/src/backend/commands/collationcmds.c index 418f7342c7..b6c14c920d 100644 --- a/src/backend/commands/collationcmds.c +++ b/src/backend/commands/collationcmds.c @@ -357,6 +357,12 @@ pg_collation_actual_version(PG_FUNCTION_ARGS) } +/* will we use "locale -a" in pg_import_system_collations? */ +#if defined(HAVE_LOCALE_T) && !defined(WIN32) +#define READ_LOCALE_A_OUTPUT +#endif + +#if defined(READ_LOCALE_A_OUTPUT) || defined(USE_ICU) /* * Check a string to see if it is pure ASCII */ @@ -371,11 +377,7 @@ is_all_ascii(const char *str) } return true; } - -/* will we use "locale -a" in pg_import_system_collations? */ -#if defined(HAVE_LOCALE_T) && !defined(WIN32) -#define READ_LOCALE_A_OUTPUT -#endif +#endif /* READ_LOCALE_A_OUTPUT || USE_ICU */ #ifdef READ_LOCALE_A_OUTPUT /* -- cgit v1.2.3 From 1add0b15f117769f619af12720bea2f73d4f7359 Mon Sep 17 00:00:00 2001 From: Andrew Gierth Date: Mon, 10 Jul 2017 11:40:08 +0100 Subject: Fix COPY's handling of transition tables with indexes. Commit c46c0e5202e8cfe750c6629db7852fdb15d528f3 failed to pass the TransitionCaptureState object to ExecARInsertTriggers() in the case where it's using heap_multi_insert and there are indexes. Repair. Thomas Munro, from a report by David Fetter Discussion: https://postgr.es/m/20170708084213.GA14720%40fetter.org --- src/backend/commands/copy.c | 2 +- src/test/regress/expected/triggers.out | 7 ++++++- src/test/regress/sql/triggers.sql | 7 +++++++ 3 files changed, 14 insertions(+), 2 deletions(-) (limited to 'src/backend/commands') diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c index f391828e74..fc5f4f66ea 100644 --- a/src/backend/commands/copy.c +++ b/src/backend/commands/copy.c @@ -2915,7 +2915,7 @@ CopyFromInsertBatch(CopyState cstate, EState *estate, CommandId mycid, estate, false, NULL, NIL); ExecARInsertTriggers(estate, resultRelInfo, bufferedTuples[i], - recheckIndexes, NULL); + recheckIndexes, cstate->transition_capture); list_free(recheckIndexes); } } diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out index aaee30219a..ac132b042d 100644 --- a/src/test/regress/expected/triggers.out +++ b/src/test/regress/expected/triggers.out @@ -2156,6 +2156,11 @@ NOTICE: trigger = child3_delete_trig, old table = (CCC,42,foo) -- 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) +-- same behavior for copy if there is an index (interesting because rows are +-- captured by a different code path in copy.c if there are indexes) +create index on parent(b); +copy parent (a, b) from stdin; +NOTICE: trigger = parent_insert_trig, new table = (DDD,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; @@ -2168,7 +2173,7 @@ 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) +NOTICE: trigger = parent_delete_trig, old table = (AAA,42), (BBB,42), (CCC,42), (DDD,42) drop table child1, child2, child3, parent; -- -- Verify prohibition of row triggers with transition triggers on diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql index 659a5a1422..b10159a1cf 100644 --- a/src/test/regress/sql/triggers.sql +++ b/src/test/regress/sql/triggers.sql @@ -1661,6 +1661,13 @@ BBB 42 CCC 42 \. +-- same behavior for copy if there is an index (interesting because rows are +-- captured by a different code path in copy.c if there are indexes) +create index on parent(b); +copy parent (a, b) from stdin; +DDD 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; -- cgit v1.2.3