Fix transition tables for partition/inheritance.
authorAndrew Gierth <rhodiumtoad@postgresql.org>
Wed, 28 Jun 2017 17:55:03 +0000 (18:55 +0100)
committerAndrew Gierth <rhodiumtoad@postgresql.org>
Wed, 28 Jun 2017 17:55:03 +0000 (18:55 +0100)
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

13 files changed:
doc/src/sgml/ref/create_trigger.sgml
src/backend/catalog/pg_inherits.c
src/backend/commands/copy.c
src/backend/commands/tablecmds.c
src/backend/commands/trigger.c
src/backend/executor/execMain.c
src/backend/executor/execReplication.c
src/backend/executor/nodeModifyTable.c
src/include/catalog/pg_inherits_fn.h
src/include/commands/trigger.h
src/include/nodes/execnodes.h
src/test/regress/expected/triggers.out
src/test/regress/sql/triggers.sql

index c5f7c7583819e3f002991e6a0898ad1b6255ade4..18efe6a9ed7e95551870f839a336bddc4da4106d 100644 (file)
@@ -458,6 +458,20 @@ UPDATE OF <replaceable>column_name1</replaceable> [, <replaceable>column_name2</
    rows.
   </para>
 
+  <para>
+    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 <literal>REFERENCING</literal> 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.
+  </para>
+
   <para>
    In <productname>PostgreSQL</productname> versions before 7.3, it was
    necessary to declare trigger functions as returning the placeholder
index e5fb52cfbf81aa6bb7b4d4b44dc03dfba9b97d9a..245a374fc915958d910fdb559b01860743250832 100644 (file)
@@ -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
index 3c399e29db5cd7653ae8a49e005dd904978b2fc5..a4c02e6b7c52d54cf484ac55eeb04fc607b16e61 100644 (file)
@@ -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);
        }
    }
 
index 7d9c769b062e189699eb179e13637ba348a4833a..bb00858ad13567859d8f1df08ac5220a29325706 100644 (file)
@@ -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);
 
index 45d1f515eb923e3a9670d38960b3687d6857b5a2..f902e0cdf5f3689b03cb3df45e01cfcb00fa3558 100644 (file)
@@ -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;
index 7f0d21f5166a33bd2ea3e47da627d3c29106e771..0f08283f81f151dd99f4cc2c2c06d10bc581d560 100644 (file)
@@ -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.
index 59f14e997f52f514e671800e15552b8691c1b228..36960eaa7e8d01dc848da2d0841741f0cf9519a5 100644 (file)
@@ -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);
    }
index 5e43a069426a43944fd181e13fbb3c1e23fbf235..f2534f2062297e86dc72419f10d8a07314e58221 100644 (file)
@@ -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.
     */
index abfa4766a12e973def4f2d0bf0494800b110c4d5..774338889920cad48539de6922dc80e78ce4d832 100644 (file)
@@ -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 */
index 10ac724febf52473e7d0e757fef3ad379182bf5e..51a25c8ddc2086c3b2e51ce8b65eb039ebe75856 100644 (file)
@@ -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,
index 54c5cf5f95b5c5a3cf40062e603c43c36216d81a..85fac8ab91b6f1f22d65f6c5e268af1d9acf38a9 100644 (file)
@@ -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;
 
 /* ----------------
index 29b8adf1e23cc85554f2acc2cbf6d346280a43bb..995410f1aaecb3704682580ed8fb890b4bb9d9ec 100644 (file)
@@ -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();
index 9f2ed88f20981c3f55da31c38da7d7c1d0c7bfe2..683a5f1e5c43b97fea501fcd83b2b004b7f511fd 100644 (file)
@@ -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();