Split ExecUpdate and ExecDelete into reusable pieces
authorAlvaro Herrera <alvherre@alvh.no-ip.org>
Thu, 17 Mar 2022 10:47:04 +0000 (11:47 +0100)
committerAlvaro Herrera <alvherre@alvh.no-ip.org>
Thu, 17 Mar 2022 10:47:04 +0000 (11:47 +0100)
Create subroutines ExecUpdatePrologue / ExecUpdateAct /
ExecUpdateEpilogue, and similar for ExecDelete.

Introduce a new struct to be used internally in nodeModifyTable.c,
dubbed ModifyTableContext, which contains all context information needed
to perform these operations, as well as ExecInsert and others.

This allows using a different schedule and a different way of evaluating
the results of these operations, which can be exploited by a later
commit introducing support for MERGE.  It also makes ExecUpdate and
ExecDelete proper shorter and (hopefully) simpler.

Author: Álvaro Herrera <alvherre@alvh.no-ip.org>
Reviewed-by: Amit Langote <amitlangote09@gmail.com>
Reviewed-by: Japin Li <japinli@hotmail.com>
Reviewed-by: Zhihong Yu <zyu@yugabyte.com>
Discussion: https://postgr.es/m/202202271724.4z7xv3cf46kv@alvherre.pgsql

src/backend/executor/nodeModifyTable.c
src/tools/pgindent/typedefs.list

index 5ec699a9bd1c2e6af2b2dec77a7725ca0a5d9238..6239abae90451deae1ce215af475d5f0b04fcbe2 100644 (file)
@@ -20,8 +20,8 @@
  *
  *  NOTES
  *     The ModifyTable node receives input from its outerPlan, which is
- *     the data to insert for INSERT cases, or the changed columns' new
- *     values plus row-locating info for UPDATE cases, or just the
+ *     the data to insert for INSERT cases, the changed columns' new
+ *     values plus row-locating info for UPDATE and MERGE cases, or just the
  *     row-locating info for DELETE cases.
  *
  *     If the query specifies RETURNING, then the ModifyTable returns a
@@ -60,6 +60,61 @@ typedef struct MTTargetRelLookup
    int         relationIndex;  /* rel's index in resultRelInfo[] array */
 } MTTargetRelLookup;
 
+/*
+ * Context struct for a ModifyTable operation, containing basic execution
+ * state and some output variables populated by ExecUpdateAct() and
+ * ExecDeleteAct() to report the result of their actions to callers.
+ */
+typedef struct ModifyTableContext
+{
+   /* Operation state */
+   ModifyTableState *mtstate;
+   EPQState   *epqstate;
+   EState     *estate;
+
+   /*
+    * Slot containing tuple obtained from ModifyTable's subplan.  Used to
+    * access "junk" columns that are not going to be stored.
+    */
+   TupleTableSlot *planSlot;
+
+
+   /*
+    * Information about the changes that were made concurrently to a tuple
+    * being updated or deleted
+    */
+   TM_FailureData tmfd;
+
+   /*
+    * The tuple produced by EvalPlanQual to retry from, if a cross-partition
+    * UPDATE requires it
+    */
+   TupleTableSlot *cpUpdateRetrySlot;
+
+   /*
+    * The tuple projected by the INSERT's RETURNING clause, when doing a
+    * cross-partition UPDATE
+    */
+   TupleTableSlot *cpUpdateReturningSlot;
+
+   /*
+    * Lock mode to acquire on the latest tuple version before performing
+    * EvalPlanQual on it
+    */
+   LockTupleMode lockmode;
+} ModifyTableContext;
+
+/*
+ * Context struct containing output data specific to UPDATE operations.
+ */
+typedef struct UpdateContext
+{
+   bool        updated;        /* did UPDATE actually occur? */
+   bool        updateIndexes;  /* index update required? */
+   bool        crossPartUpdate;    /* was it a cross-partition update? */
+} UpdateContext;
+
+
 static void ExecBatchInsert(ModifyTableState *mtstate,
                            ResultRelInfo *resultRelInfo,
                            TupleTableSlot **slots,
@@ -67,12 +122,10 @@ static void ExecBatchInsert(ModifyTableState *mtstate,
                            int numSlots,
                            EState *estate,
                            bool canSetTag);
-static bool ExecOnConflictUpdate(ModifyTableState *mtstate,
+static bool ExecOnConflictUpdate(ModifyTableContext *context,
                                 ResultRelInfo *resultRelInfo,
                                 ItemPointer conflictTid,
-                                TupleTableSlot *planSlot,
                                 TupleTableSlot *excludedSlot,
-                                EState *estate,
                                 bool canSetTag,
                                 TupleTableSlot **returning);
 static TupleTableSlot *ExecPrepareTupleRouting(ModifyTableState *mtstate,
@@ -580,8 +633,6 @@ ExecGetUpdateNewTuple(ResultRelInfo *relinfo,
  *     relations.
  *
  *     slot contains the new tuple value to be stored.
- *     planSlot is the output of the ModifyTable's subplan; we use it
- *     to access "junk" columns that are not going to be stored.
  *
  *     Returns RETURNING result if any, otherwise NULL.
  *
@@ -591,15 +642,16 @@ ExecGetUpdateNewTuple(ResultRelInfo *relinfo,
  * ----------------------------------------------------------------
  */
 static TupleTableSlot *
-ExecInsert(ModifyTableState *mtstate,
+ExecInsert(ModifyTableContext *context,
           ResultRelInfo *resultRelInfo,
           TupleTableSlot *slot,
-          TupleTableSlot *planSlot,
-          EState *estate,
           bool canSetTag)
 {
+   ModifyTableState *mtstate = context->mtstate;
+   EState     *estate = context->estate;
    Relation    resultRelationDesc;
    List       *recheckIndexes = NIL;
+   TupleTableSlot *planSlot = context->planSlot;
    TupleTableSlot *result = NULL;
    TransitionCaptureState *ar_insert_trig_tcs;
    ModifyTable *node = (ModifyTable *) mtstate->ps.plan;
@@ -850,9 +902,9 @@ ExecInsert(ModifyTableState *mtstate,
                     */
                    TupleTableSlot *returning = NULL;
 
-                   if (ExecOnConflictUpdate(mtstate, resultRelInfo,
-                                            &conflictTid, planSlot, slot,
-                                            estate, canSetTag, &returning))
+                   if (ExecOnConflictUpdate(context, resultRelInfo,
+                                            &conflictTid, slot, canSetTag,
+                                            &returning))
                    {
                        InstrCountTuples2(&mtstate->ps, 1);
                        return returning;
@@ -1055,6 +1107,90 @@ ExecBatchInsert(ModifyTableState *mtstate,
        estate->es_processed += numInserted;
 }
 
+/*
+ * ExecDeletePrologue -- subroutine for ExecDelete
+ *
+ * Prepare executor state for DELETE.  Actually, the only thing we have to do
+ * here is execute BEFORE ROW triggers.  We return false if one of them makes
+ * the delete a no-op; otherwise, return true.
+ */
+static bool
+ExecDeletePrologue(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
+                  ItemPointer tupleid, HeapTuple oldtuple,
+                  TupleTableSlot **epqreturnslot)
+{
+   /* BEFORE ROW DELETE triggers */
+   if (resultRelInfo->ri_TrigDesc &&
+       resultRelInfo->ri_TrigDesc->trig_delete_before_row)
+       return ExecBRDeleteTriggers(context->estate, context->epqstate,
+                                   resultRelInfo, tupleid, oldtuple,
+                                   epqreturnslot);
+
+   return true;
+}
+
+/*
+ * ExecDeleteAct -- subroutine for ExecDelete
+ *
+ * Actually delete the tuple from a plain table.
+ *
+ * Caller is in charge of doing EvalPlanQual as necessary
+ */
+static TM_Result
+ExecDeleteAct(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
+             ItemPointer tupleid, bool changingPart)
+{
+   EState     *estate = context->estate;
+
+   return table_tuple_delete(resultRelInfo->ri_RelationDesc, tupleid,
+                             estate->es_output_cid,
+                             estate->es_snapshot,
+                             estate->es_crosscheck_snapshot,
+                             true /* wait for commit */ ,
+                             &context->tmfd,
+                             changingPart);
+}
+
+/*
+ * ExecDeleteEpilogue -- subroutine for ExecDelete
+ *
+ * Closing steps of tuple deletion; this invokes AFTER FOR EACH ROW triggers,
+ * including the UPDATE triggers if the deletion is being done as part of a
+ * cross-partition tuple move.
+ */
+static void
+ExecDeleteEpilogue(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
+                  ItemPointer tupleid, HeapTuple oldtuple)
+{
+   ModifyTableState *mtstate = context->mtstate;
+   EState     *estate = context->estate;
+   TransitionCaptureState *ar_delete_trig_tcs;
+
+   /*
+    * If this delete is the result of a partition key update that moved the
+    * tuple to a new partition, put this row into the transition OLD TABLE,
+    * if there is one. We need to do this separately for DELETE and INSERT
+    * because they happen on different tables.
+    */
+   ar_delete_trig_tcs = mtstate->mt_transition_capture;
+   if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture &&
+       mtstate->mt_transition_capture->tcs_update_old_table)
+   {
+       ExecARUpdateTriggers(estate, resultRelInfo, tupleid, oldtuple,
+                            NULL, NULL, mtstate->mt_transition_capture);
+
+       /*
+        * We've already captured the NEW TABLE row, so make sure any AR
+        * DELETE trigger fired below doesn't capture it again.
+        */
+       ar_delete_trig_tcs = NULL;
+   }
+
+   /* AFTER ROW DELETE Triggers */
+   ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple,
+                        ar_delete_trig_tcs);
+}
+
 /* ----------------------------------------------------------------
  *     ExecDelete
  *
@@ -1072,46 +1208,37 @@ ExecBatchInsert(ModifyTableState *mtstate,
  *     whether the tuple is actually deleted, callers can use it to
  *     decide whether to continue the operation.  When this DELETE is a
  *     part of an UPDATE of partition-key, then the slot returned by
- *     EvalPlanQual() is passed back using output parameter epqslot.
+ *     EvalPlanQual() is passed back using output parameter epqreturnslot.
  *
  *     Returns RETURNING result if any, otherwise NULL.
  * ----------------------------------------------------------------
  */
 static TupleTableSlot *
-ExecDelete(ModifyTableState *mtstate,
+ExecDelete(ModifyTableContext *context,
           ResultRelInfo *resultRelInfo,
           ItemPointer tupleid,
           HeapTuple oldtuple,
-          TupleTableSlot *planSlot,
-          EPQState *epqstate,
-          EState *estate,
           bool processReturning,
-          bool canSetTag,
           bool changingPart,
+          bool canSetTag,
           bool *tupleDeleted,
           TupleTableSlot **epqreturnslot)
 {
+   EState     *estate = context->estate;
    Relation    resultRelationDesc = resultRelInfo->ri_RelationDesc;
-   TM_Result   result;
-   TM_FailureData tmfd;
    TupleTableSlot *slot = NULL;
-   TransitionCaptureState *ar_delete_trig_tcs;
+   TM_Result   result;
 
    if (tupleDeleted)
        *tupleDeleted = false;
 
-   /* BEFORE ROW DELETE Triggers */
-   if (resultRelInfo->ri_TrigDesc &&
-       resultRelInfo->ri_TrigDesc->trig_delete_before_row)
-   {
-       bool        dodelete;
-
-       dodelete = ExecBRDeleteTriggers(estate, epqstate, resultRelInfo,
-                                       tupleid, oldtuple, epqreturnslot);
-
-       if (!dodelete)          /* "do nothing" */
-           return NULL;
-   }
+   /*
+    * Prepare for the delete.  This includes BEFORE ROW triggers, so we're
+    * done if it says we are.
+    */
+   if (!ExecDeletePrologue(context, resultRelInfo, tupleid, oldtuple,
+                           epqreturnslot))
+       return NULL;
 
    /* INSTEAD OF ROW DELETE Triggers */
    if (resultRelInfo->ri_TrigDesc &&
@@ -1137,7 +1264,7 @@ ExecDelete(ModifyTableState *mtstate,
        slot = resultRelInfo->ri_FdwRoutine->ExecForeignDelete(estate,
                                                               resultRelInfo,
                                                               slot,
-                                                              planSlot);
+                                                              context->planSlot);
 
        if (slot == NULL)       /* "do nothing" */
            return NULL;
@@ -1156,20 +1283,14 @@ ExecDelete(ModifyTableState *mtstate,
        /*
         * delete the tuple
         *
-        * Note: if es_crosscheck_snapshot isn't InvalidSnapshot, we check
-        * that the row to be deleted is visible to that snapshot, and throw a
-        * can't-serialize error if not. This is a special-case behavior
-        * needed for referential integrity updates in transaction-snapshot
-        * mode transactions.
+        * Note: if context->estate->es_crosscheck_snapshot isn't
+        * InvalidSnapshot, we check that the row to be deleted is visible to
+        * that snapshot, and throw a can't-serialize error if not. This is a
+        * special-case behavior needed for referential integrity updates in
+        * transaction-snapshot mode transactions.
         */
 ldelete:;
-       result = table_tuple_delete(resultRelationDesc, tupleid,
-                                   estate->es_output_cid,
-                                   estate->es_snapshot,
-                                   estate->es_crosscheck_snapshot,
-                                   true /* wait for commit */ ,
-                                   &tmfd,
-                                   changingPart);
+       result = ExecDeleteAct(context, resultRelInfo, tupleid, changingPart);
 
        switch (result)
        {
@@ -1199,7 +1320,7 @@ ldelete:;
                 * can re-execute the DELETE and then return NULL to cancel
                 * the outer delete.
                 */
-               if (tmfd.cmax != estate->es_output_cid)
+               if (context->tmfd.cmax != estate->es_output_cid)
                    ereport(ERROR,
                            (errcode(ERRCODE_TRIGGERED_DATA_CHANGE_VIOLATION),
                             errmsg("tuple to be deleted was already modified by an operation triggered by the current command"),
@@ -1225,8 +1346,8 @@ ldelete:;
                     * Already know that we're going to need to do EPQ, so
                     * fetch tuple directly into the right slot.
                     */
-                   EvalPlanQualBegin(epqstate);
-                   inputslot = EvalPlanQualSlot(epqstate, resultRelationDesc,
+                   EvalPlanQualBegin(context->epqstate);
+                   inputslot = EvalPlanQualSlot(context->epqstate, resultRelationDesc,
                                                 resultRelInfo->ri_RangeTableIndex);
 
                    result = table_tuple_lock(resultRelationDesc, tupleid,
@@ -1234,13 +1355,13 @@ ldelete:;
                                              inputslot, estate->es_output_cid,
                                              LockTupleExclusive, LockWaitBlock,
                                              TUPLE_LOCK_FLAG_FIND_LAST_VERSION,
-                                             &tmfd);
+                                             &context->tmfd);
 
                    switch (result)
                    {
                        case TM_Ok:
-                           Assert(tmfd.traversed);
-                           epqslot = EvalPlanQual(epqstate,
+                           Assert(context->tmfd.traversed);
+                           epqslot = EvalPlanQual(context->epqstate,
                                                   resultRelationDesc,
                                                   resultRelInfo->ri_RangeTableIndex,
                                                   inputslot);
@@ -1273,7 +1394,7 @@ ldelete:;
                             * See also TM_SelfModified response to
                             * table_tuple_delete() above.
                             */
-                           if (tmfd.cmax != estate->es_output_cid)
+                           if (context->tmfd.cmax != estate->es_output_cid)
                                ereport(ERROR,
                                        (errcode(ERRCODE_TRIGGERED_DATA_CHANGE_VIOLATION),
                                         errmsg("tuple to be deleted was already modified by an operation triggered by the current command"),
@@ -1336,33 +1457,7 @@ ldelete:;
    if (tupleDeleted)
        *tupleDeleted = true;
 
-   /*
-    * If this delete is the result of a partition key update that moved the
-    * tuple to a new partition, put this row into the transition OLD TABLE,
-    * if there is one. We need to do this separately for DELETE and INSERT
-    * because they happen on different tables.
-    */
-   ar_delete_trig_tcs = mtstate->mt_transition_capture;
-   if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
-       && mtstate->mt_transition_capture->tcs_update_old_table)
-   {
-       ExecARUpdateTriggers(estate, resultRelInfo,
-                            tupleid,
-                            oldtuple,
-                            NULL,
-                            NULL,
-                            mtstate->mt_transition_capture);
-
-       /*
-        * We've already captured the NEW TABLE row, so make sure any AR
-        * DELETE trigger fired below doesn't capture it again.
-        */
-       ar_delete_trig_tcs = NULL;
-   }
-
-   /* AFTER ROW DELETE Triggers */
-   ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple,
-                        ar_delete_trig_tcs);
+   ExecDeleteEpilogue(context, resultRelInfo, tupleid, oldtuple);
 
    /* Process RETURNING if present and if requested */
    if (processReturning && resultRelInfo->ri_projectReturning)
@@ -1393,7 +1488,7 @@ ldelete:;
            }
        }
 
-       rslot = ExecProcessReturning(resultRelInfo, slot, planSlot);
+       rslot = ExecProcessReturning(resultRelInfo, slot, context->planSlot);
 
        /*
         * Before releasing the target tuple again, make sure rslot has a
@@ -1427,21 +1522,20 @@ ldelete:;
  * and call this function again or perform a regular update accordingly.
  */
 static bool
-ExecCrossPartitionUpdate(ModifyTableState *mtstate,
+ExecCrossPartitionUpdate(ModifyTableContext *context,
                         ResultRelInfo *resultRelInfo,
                         ItemPointer tupleid, HeapTuple oldtuple,
-                        TupleTableSlot *slot, TupleTableSlot *planSlot,
-                        EPQState *epqstate, bool canSetTag,
-                        TupleTableSlot **retry_slot,
-                        TupleTableSlot **inserted_tuple)
+                        TupleTableSlot *slot,
+                        bool canSetTag, UpdateContext *updateCxt)
 {
+   ModifyTableState *mtstate = context->mtstate;
    EState     *estate = mtstate->ps.state;
    TupleConversionMap *tupconv_map;
    bool        tuple_deleted;
    TupleTableSlot *epqslot = NULL;
 
-   *inserted_tuple = NULL;
-   *retry_slot = NULL;
+   context->cpUpdateReturningSlot = NULL;
+   context->cpUpdateRetrySlot = NULL;
 
    /*
     * Disallow an INSERT ON CONFLICT DO UPDATE that causes the original row
@@ -1488,11 +1582,11 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
     * Row movement, part 1.  Delete the tuple, but skip RETURNING processing.
     * We want to return rows from INSERT.
     */
-   ExecDelete(mtstate, resultRelInfo, tupleid, oldtuple, planSlot,
-              epqstate, estate,
+   ExecDelete(context, resultRelInfo,
+              tupleid, oldtuple,
               false,           /* processReturning */
-              false,           /* canSetTag */
               true,            /* changingPart */
+              false,           /* canSetTag */
               &tuple_deleted, &epqslot);
 
    /*
@@ -1538,8 +1632,9 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
                                               SnapshotAny,
                                               oldSlot))
                elog(ERROR, "failed to fetch tuple being updated");
-           *retry_slot = ExecGetUpdateNewTuple(resultRelInfo, epqslot,
-                                               oldSlot);
+           /* and project the new tuple to retry the UPDATE with */
+           context->cpUpdateRetrySlot =
+               ExecGetUpdateNewTuple(resultRelInfo, epqslot, oldSlot);
            return false;
        }
    }
@@ -1556,8 +1651,8 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
                                     mtstate->mt_root_tuple_slot);
 
    /* Tuple routing starts from the root table. */
-   *inserted_tuple = ExecInsert(mtstate, mtstate->rootResultRelInfo, slot,
-                                planSlot, estate, canSetTag);
+   context->cpUpdateReturningSlot =
+       ExecInsert(context, mtstate->rootResultRelInfo, slot, canSetTag);
 
    /*
     * Reset the transition state that may possibly have been written by
@@ -1570,6 +1665,233 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
    return true;
 }
 
+/*
+ * ExecUpdatePrologue -- subroutine for ExecUpdate
+ *
+ * Prepare executor state for UPDATE.  This includes running BEFORE ROW
+ * triggers.  We return false if one of them makes the update a no-op;
+ * otherwise, return true.
+ */
+static bool
+ExecUpdatePrologue(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
+                  ItemPointer tupleid, HeapTuple oldtuple, TupleTableSlot *slot)
+{
+   Relation    resultRelationDesc = resultRelInfo->ri_RelationDesc;
+
+   ExecMaterializeSlot(slot);
+
+   /*
+    * Open the table's indexes, if we have not done so already, so that we
+    * can add new index entries for the updated tuple.
+    */
+   if (resultRelationDesc->rd_rel->relhasindex &&
+       resultRelInfo->ri_IndexRelationDescs == NULL)
+       ExecOpenIndices(resultRelInfo, false);
+
+   /* BEFORE ROW UPDATE triggers */
+   if (resultRelInfo->ri_TrigDesc &&
+       resultRelInfo->ri_TrigDesc->trig_update_before_row)
+       return ExecBRUpdateTriggers(context->estate, context->epqstate,
+                                   resultRelInfo, tupleid, oldtuple, slot);
+
+   return true;
+}
+
+/*
+ * ExecUpdatePrepareSlot -- subroutine for ExecUpdate
+ *
+ * Apply the final modifications to the tuple slot before the update.
+ */
+static void
+ExecUpdatePrepareSlot(ResultRelInfo *resultRelInfo,
+                     TupleTableSlot *slot,
+                     EState *estate)
+{
+   Relation    resultRelationDesc = resultRelInfo->ri_RelationDesc;
+
+   /*
+    * Constraints and GENERATED expressions might reference the tableoid
+    * column, so (re-)initialize tts_tableOid before evaluating them.
+    */
+   slot->tts_tableOid = RelationGetRelid(resultRelationDesc);
+
+   /*
+    * Compute stored generated columns
+    */
+   if (resultRelationDesc->rd_att->constr &&
+       resultRelationDesc->rd_att->constr->has_generated_stored)
+       ExecComputeStoredGenerated(resultRelInfo, estate, slot,
+                                  CMD_UPDATE);
+}
+
+/*
+ * ExecUpdateAct -- subroutine for ExecUpdate
+ *
+ * Actually update the tuple, when operating on a plain table.  If the
+ * table is a partition, and the command was called referencing an ancestor
+ * partitioned table, this routine migrates the resulting tuple to another
+ * partition.
+ *
+ * The caller is in charge of keeping indexes current as necessary.  The
+ * caller is also in charge of doing EvalPlanQual if the tuple is found to
+ * be concurrently updated.  However, in case of a cross-partition update,
+ * this routine does it.
+ *
+ * Caller is in charge of doing EvalPlanQual as necessary, and of keeping
+ * indexes current for the update.
+ */
+static TM_Result
+ExecUpdateAct(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
+             ItemPointer tupleid, HeapTuple oldtuple, TupleTableSlot *slot,
+             bool canSetTag, UpdateContext *updateCxt)
+{
+   EState     *estate = context->estate;
+   Relation    resultRelationDesc = resultRelInfo->ri_RelationDesc;
+   bool        partition_constraint_failed;
+   TM_Result   result;
+
+   updateCxt->crossPartUpdate = false;
+
+   /*
+    * If we generate a new candidate tuple after EvalPlanQual testing, we
+    * must loop back here and recheck any RLS policies and constraints. (We
+    * don't need to redo triggers, however.  If there are any BEFORE triggers
+    * then trigger.c will have done table_tuple_lock to lock the correct
+    * tuple, so there's no need to do them again.)
+    */
+lreplace:;
+
+   /* ensure slot is independent, consider e.g. EPQ */
+   ExecMaterializeSlot(slot);
+
+   /*
+    * If partition constraint fails, this row might get moved to another
+    * partition, in which case we should check the RLS CHECK policy just
+    * before inserting into the new partition, rather than doing it here.
+    * This is because a trigger on that partition might again change the row.
+    * So skip the WCO checks if the partition constraint fails.
+    */
+   partition_constraint_failed =
+       resultRelationDesc->rd_rel->relispartition &&
+       !ExecPartitionCheck(resultRelInfo, slot, estate, false);
+
+   /* Check any RLS UPDATE WITH CHECK policies */
+   if (!partition_constraint_failed &&
+       resultRelInfo->ri_WithCheckOptions != NIL)
+   {
+       /*
+        * ExecWithCheckOptions() will skip any WCOs which are not of the kind
+        * we are looking for at this point.
+        */
+       ExecWithCheckOptions(WCO_RLS_UPDATE_CHECK,
+                            resultRelInfo, slot, estate);
+   }
+
+   /*
+    * If a partition check failed, try to move the row into the right
+    * partition.
+    */
+   if (partition_constraint_failed)
+   {
+       /*
+        * ExecCrossPartitionUpdate will first DELETE the row from the
+        * partition it's currently in and then insert it back into the root
+        * table, which will re-route it to the correct partition.  However,
+        * if the tuple has been concurrently updated, a retry is needed.
+        */
+       if (ExecCrossPartitionUpdate(context, resultRelInfo,
+                                    tupleid, oldtuple, slot,
+                                    canSetTag, updateCxt))
+       {
+           /* success! */
+           updateCxt->updated = true;
+           updateCxt->crossPartUpdate = true;
+           return TM_Ok;
+       }
+
+       /*
+        * ExecCrossPartitionUpdate installed an updated version of the new
+        * tuple in the retry slot; start over.
+        */
+       slot = context->cpUpdateRetrySlot;
+       goto lreplace;
+   }
+
+   /*
+    * Check the constraints of the tuple.  We've already checked the
+    * partition constraint above; however, we must still ensure the tuple
+    * passes all other constraints, so we will call ExecConstraints() and
+    * have it validate all remaining checks.
+    */
+   if (resultRelationDesc->rd_att->constr)
+       ExecConstraints(resultRelInfo, slot, estate);
+
+   /*
+    * replace the heap tuple
+    *
+    * Note: if es_crosscheck_snapshot isn't InvalidSnapshot, we check that
+    * the row to be updated is visible to that snapshot, and throw a
+    * can't-serialize error if not. This is a special-case behavior needed
+    * for referential integrity updates in transaction-snapshot mode
+    * transactions.
+    */
+   result = table_tuple_update(resultRelationDesc, tupleid, slot,
+                               estate->es_output_cid,
+                               estate->es_snapshot,
+                               estate->es_crosscheck_snapshot,
+                               true /* wait for commit */ ,
+                               &context->tmfd, &context->lockmode,
+                               &updateCxt->updateIndexes);
+   if (result == TM_Ok)
+       updateCxt->updated = true;
+
+   return result;
+}
+
+/*
+ * ExecUpdateEpilogue -- subroutine for ExecUpdate
+ *
+ * Closing steps of updating a tuple.  Must be called if ExecUpdateAct
+ * returns indicating that the tuple was updated.
+ */
+static void
+ExecUpdateEpilogue(ModifyTableContext *context, UpdateContext *updateCxt,
+                  ResultRelInfo *resultRelInfo, ItemPointer tupleid,
+                  HeapTuple oldtuple, TupleTableSlot *slot,
+                  List *recheckIndexes)
+{
+   ModifyTableState *mtstate = context->mtstate;
+
+   /* insert index entries for tuple if necessary */
+   if (resultRelInfo->ri_NumIndices > 0 && updateCxt->updateIndexes)
+       recheckIndexes = ExecInsertIndexTuples(resultRelInfo,
+                                              slot, context->estate,
+                                              true, false,
+                                              NULL, NIL);
+
+   /* AFTER ROW UPDATE Triggers */
+   ExecARUpdateTriggers(context->estate, resultRelInfo,
+                        tupleid, oldtuple, slot,
+                        recheckIndexes,
+                        mtstate->operation == CMD_INSERT ?
+                        mtstate->mt_oc_transition_capture :
+                        mtstate->mt_transition_capture);
+
+   /*
+    * Check any WITH CHECK OPTION constraints from parent views.  We are
+    * required to do this after testing all constraints and uniqueness
+    * violations per the SQL spec, so we do it after actually updating the
+    * record in the heap and all indexes.
+    *
+    * ExecWithCheckOptions() will skip any WCOs which are not of the kind we
+    * are looking for at this point.
+    */
+   if (resultRelInfo->ri_WithCheckOptions != NIL)
+       ExecWithCheckOptions(WCO_VIEW_CHECK, resultRelInfo,
+                            slot, context->estate);
+}
+
+
 /* ----------------------------------------------------------------
  *     ExecUpdate
  *
@@ -1598,20 +1920,15 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
  * ----------------------------------------------------------------
  */
 static TupleTableSlot *
-ExecUpdate(ModifyTableState *mtstate,
-          ResultRelInfo *resultRelInfo,
-          ItemPointer tupleid,
-          HeapTuple oldtuple,
-          TupleTableSlot *slot,
-          TupleTableSlot *planSlot,
-          EPQState *epqstate,
-          EState *estate,
+ExecUpdate(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
+          ItemPointer tupleid, HeapTuple oldtuple, TupleTableSlot *slot,
           bool canSetTag)
 {
+   EState     *estate = context->estate;
    Relation    resultRelationDesc = resultRelInfo->ri_RelationDesc;
-   TM_Result   result;
-   TM_FailureData tmfd;
+   UpdateContext updateCxt = {0};
    List       *recheckIndexes = NIL;
+   TM_Result   result;
 
    /*
     * abort the operation if not running transactions
@@ -1619,24 +1936,12 @@ ExecUpdate(ModifyTableState *mtstate,
    if (IsBootstrapProcessingMode())
        elog(ERROR, "cannot UPDATE during bootstrap");
 
-   ExecMaterializeSlot(slot);
-
    /*
-    * Open the table's indexes, if we have not done so already, so that we
-    * can add new index entries for the updated tuple.
+    * Prepare for the update.  This includes BEFORE ROW triggers, so we're
+    * done if it says we are.
     */
-   if (resultRelationDesc->rd_rel->relhasindex &&
-       resultRelInfo->ri_IndexRelationDescs == NULL)
-       ExecOpenIndices(resultRelInfo, false);
-
-   /* BEFORE ROW UPDATE Triggers */
-   if (resultRelInfo->ri_TrigDesc &&
-       resultRelInfo->ri_TrigDesc->trig_update_before_row)
-   {
-       if (!ExecBRUpdateTriggers(estate, epqstate, resultRelInfo,
-                                 tupleid, oldtuple, slot))
-           return NULL;        /* "do nothing" */
-   }
+   if (!ExecUpdatePrologue(context, resultRelInfo, tupleid, oldtuple, slot))
+       return NULL;
 
    /* INSTEAD OF ROW UPDATE Triggers */
    if (resultRelInfo->ri_TrigDesc &&
@@ -1648,19 +1953,7 @@ ExecUpdate(ModifyTableState *mtstate,
    }
    else if (resultRelInfo->ri_FdwRoutine)
    {
-       /*
-        * GENERATED expressions might reference the tableoid column, so
-        * (re-)initialize tts_tableOid before evaluating them.
-        */
-       slot->tts_tableOid = RelationGetRelid(resultRelInfo->ri_RelationDesc);
-
-       /*
-        * Compute stored generated columns
-        */
-       if (resultRelationDesc->rd_att->constr &&
-           resultRelationDesc->rd_att->constr->has_generated_stored)
-           ExecComputeStoredGenerated(resultRelInfo, estate, slot,
-                                      CMD_UPDATE);
+       ExecUpdatePrepareSlot(resultRelInfo, slot, estate);
 
        /*
         * update in foreign table: let the FDW do it
@@ -1668,7 +1961,7 @@ ExecUpdate(ModifyTableState *mtstate,
        slot = resultRelInfo->ri_FdwRoutine->ExecForeignUpdate(estate,
                                                               resultRelInfo,
                                                               slot,
-                                                              planSlot);
+                                                              context->planSlot);
 
        if (slot == NULL)       /* "do nothing" */
            return NULL;
@@ -1682,114 +1975,20 @@ ExecUpdate(ModifyTableState *mtstate,
    }
    else
    {
-       LockTupleMode lockmode;
-       bool        partition_constraint_failed;
-       bool        update_indexes;
-
-       /*
-        * Constraints and GENERATED expressions might reference the tableoid
-        * column, so (re-)initialize tts_tableOid before evaluating them.
-        */
-       slot->tts_tableOid = RelationGetRelid(resultRelationDesc);
-
-       /*
-        * Compute stored generated columns
-        */
-       if (resultRelationDesc->rd_att->constr &&
-           resultRelationDesc->rd_att->constr->has_generated_stored)
-           ExecComputeStoredGenerated(resultRelInfo, estate, slot,
-                                      CMD_UPDATE);
-
-       /*
-        * Check any RLS UPDATE WITH CHECK policies
-        *
-        * If we generate a new candidate tuple after EvalPlanQual testing, we
-        * must loop back here and recheck any RLS policies and constraints.
-        * (We don't need to redo triggers, however.  If there are any BEFORE
-        * triggers then trigger.c will have done table_tuple_lock to lock the
-        * correct tuple, so there's no need to do them again.)
-        */
-lreplace:;
-
-       /* ensure slot is independent, consider e.g. EPQ */
-       ExecMaterializeSlot(slot);
-
-       /*
-        * If partition constraint fails, this row might get moved to another
-        * partition, in which case we should check the RLS CHECK policy just
-        * before inserting into the new partition, rather than doing it here.
-        * This is because a trigger on that partition might again change the
-        * row.  So skip the WCO checks if the partition constraint fails.
-        */
-       partition_constraint_failed =
-           resultRelationDesc->rd_rel->relispartition &&
-           !ExecPartitionCheck(resultRelInfo, slot, estate, false);
-
-       if (!partition_constraint_failed &&
-           resultRelInfo->ri_WithCheckOptions != NIL)
-       {
-           /*
-            * ExecWithCheckOptions() will skip any WCOs which are not of the
-            * kind we are looking for at this point.
-            */
-           ExecWithCheckOptions(WCO_RLS_UPDATE_CHECK,
-                                resultRelInfo, slot, estate);
-       }
-
-       /*
-        * If a partition check failed, try to move the row into the right
-        * partition.
-        */
-       if (partition_constraint_failed)
-       {
-           TupleTableSlot *inserted_tuple,
-                      *retry_slot;
-           bool        retry;
+       /* Fill in the slot appropriately */
+       ExecUpdatePrepareSlot(resultRelInfo, slot, estate);
 
-           /*
-            * ExecCrossPartitionUpdate will first DELETE the row from the
-            * partition it's currently in and then insert it back into the
-            * root table, which will re-route it to the correct partition.
-            * The first part may have to be repeated if it is detected that
-            * the tuple we're trying to move has been concurrently updated.
-            */
-           retry = !ExecCrossPartitionUpdate(mtstate, resultRelInfo, tupleid,
-                                             oldtuple, slot, planSlot,
-                                             epqstate, canSetTag,
-                                             &retry_slot, &inserted_tuple);
-           if (retry)
-           {
-               slot = retry_slot;
-               goto lreplace;
-           }
-
-           return inserted_tuple;
-       }
+redo_act:
+       result = ExecUpdateAct(context, resultRelInfo, tupleid, oldtuple, slot,
+                              canSetTag, &updateCxt);
 
        /*
-        * Check the constraints of the tuple.  We've already checked the
-        * partition constraint above; however, we must still ensure the tuple
-        * passes all other constraints, so we will call ExecConstraints() and
-        * have it validate all remaining checks.
+        * If ExecUpdateAct reports that a cross-partition update was done,
+        * then the returning tuple has been projected and there's nothing
+        * else for us to do.
         */
-       if (resultRelationDesc->rd_att->constr)
-           ExecConstraints(resultRelInfo, slot, estate);
-
-       /*
-        * replace the heap tuple
-        *
-        * Note: if es_crosscheck_snapshot isn't InvalidSnapshot, we check
-        * that the row to be updated is visible to that snapshot, and throw a
-        * can't-serialize error if not. This is a special-case behavior
-        * needed for referential integrity updates in transaction-snapshot
-        * mode transactions.
-        */
-       result = table_tuple_update(resultRelationDesc, tupleid, slot,
-                                   estate->es_output_cid,
-                                   estate->es_snapshot,
-                                   estate->es_crosscheck_snapshot,
-                                   true /* wait for commit */ ,
-                                   &tmfd, &lockmode, &update_indexes);
+       if (updateCxt.crossPartUpdate)
+           return context->cpUpdateReturningSlot;
 
        switch (result)
        {
@@ -1818,7 +2017,7 @@ lreplace:;
                 * can re-execute the UPDATE (assuming it can figure out how)
                 * and then return NULL to cancel the outer update.
                 */
-               if (tmfd.cmax != estate->es_output_cid)
+               if (context->tmfd.cmax != estate->es_output_cid)
                    ereport(ERROR,
                            (errcode(ERRCODE_TRIGGERED_DATA_CHANGE_VIOLATION),
                             errmsg("tuple to be updated was already modified by an operation triggered by the current command"),
@@ -1845,22 +2044,22 @@ lreplace:;
                     * Already know that we're going to need to do EPQ, so
                     * fetch tuple directly into the right slot.
                     */
-                   inputslot = EvalPlanQualSlot(epqstate, resultRelationDesc,
+                   inputslot = EvalPlanQualSlot(context->epqstate, resultRelationDesc,
                                                 resultRelInfo->ri_RangeTableIndex);
 
                    result = table_tuple_lock(resultRelationDesc, tupleid,
                                              estate->es_snapshot,
                                              inputslot, estate->es_output_cid,
-                                             lockmode, LockWaitBlock,
+                                             context->lockmode, LockWaitBlock,
                                              TUPLE_LOCK_FLAG_FIND_LAST_VERSION,
-                                             &tmfd);
+                                             &context->tmfd);
 
                    switch (result)
                    {
                        case TM_Ok:
-                           Assert(tmfd.traversed);
+                           Assert(context->tmfd.traversed);
 
-                           epqslot = EvalPlanQual(epqstate,
+                           epqslot = EvalPlanQual(context->epqstate,
                                                   resultRelationDesc,
                                                   resultRelInfo->ri_RangeTableIndex,
                                                   inputslot);
@@ -1870,7 +2069,8 @@ lreplace:;
 
                            /* Make sure ri_oldTupleSlot is initialized. */
                            if (unlikely(!resultRelInfo->ri_projectNewInfoValid))
-                               ExecInitUpdateProjection(mtstate, resultRelInfo);
+                               ExecInitUpdateProjection(context->mtstate,
+                                                        resultRelInfo);
 
                            /* Fetch the most recent version of old tuple. */
                            oldSlot = resultRelInfo->ri_oldTupleSlot;
@@ -1881,7 +2081,7 @@ lreplace:;
                                elog(ERROR, "failed to fetch tuple being updated");
                            slot = ExecGetUpdateNewTuple(resultRelInfo,
                                                         epqslot, oldSlot);
-                           goto lreplace;
+                           goto redo_act;
 
                        case TM_Deleted:
                            /* tuple already deleted; nothing to do */
@@ -1900,7 +2100,7 @@ lreplace:;
                             * See also TM_SelfModified response to
                             * table_tuple_update() above.
                             */
-                           if (tmfd.cmax != estate->es_output_cid)
+                           if (context->tmfd.cmax != estate->es_output_cid)
                                ereport(ERROR,
                                        (errcode(ERRCODE_TRIGGERED_DATA_CHANGE_VIOLATION),
                                         errmsg("tuple to be updated was already modified by an operation triggered by the current command"),
@@ -1930,41 +2130,19 @@ lreplace:;
                     result);
                return NULL;
        }
-
-       /* insert index entries for tuple if necessary */
-       if (resultRelInfo->ri_NumIndices > 0 && update_indexes)
-           recheckIndexes = ExecInsertIndexTuples(resultRelInfo,
-                                                  slot, estate, true, false,
-                                                  NULL, NIL);
    }
 
    if (canSetTag)
        (estate->es_processed)++;
 
-   /* AFTER ROW UPDATE Triggers */
-   ExecARUpdateTriggers(estate, resultRelInfo, tupleid, oldtuple, slot,
-                        recheckIndexes,
-                        mtstate->operation == CMD_INSERT ?
-                        mtstate->mt_oc_transition_capture :
-                        mtstate->mt_transition_capture);
+   ExecUpdateEpilogue(context, &updateCxt, resultRelInfo, tupleid, oldtuple,
+                      slot, recheckIndexes);
 
    list_free(recheckIndexes);
 
-   /*
-    * Check any WITH CHECK OPTION constraints from parent views.  We are
-    * required to do this after testing all constraints and uniqueness
-    * violations per the SQL spec, so we do it after actually updating the
-    * record in the heap and all indexes.
-    *
-    * ExecWithCheckOptions() will skip any WCOs which are not of the kind we
-    * are looking for at this point.
-    */
-   if (resultRelInfo->ri_WithCheckOptions != NIL)
-       ExecWithCheckOptions(WCO_VIEW_CHECK, resultRelInfo, slot, estate);
-
    /* Process RETURNING if present */
    if (resultRelInfo->ri_projectReturning)
-       return ExecProcessReturning(resultRelInfo, slot, planSlot);
+       return ExecProcessReturning(resultRelInfo, slot, context->planSlot);
 
    return NULL;
 }
@@ -1981,15 +2159,14 @@ lreplace:;
  * the caller must retry the INSERT from scratch.
  */
 static bool
-ExecOnConflictUpdate(ModifyTableState *mtstate,
+ExecOnConflictUpdate(ModifyTableContext *context,
                     ResultRelInfo *resultRelInfo,
                     ItemPointer conflictTid,
-                    TupleTableSlot *planSlot,
                     TupleTableSlot *excludedSlot,
-                    EState *estate,
                     bool canSetTag,
                     TupleTableSlot **returning)
 {
+   ModifyTableState *mtstate = context->mtstate;
    ExprContext *econtext = mtstate->ps.ps_ExprContext;
    Relation    relation = resultRelInfo->ri_RelationDesc;
    ExprState  *onConflictSetWhere = resultRelInfo->ri_onConflict->oc_WhereClause;
@@ -2002,7 +2179,7 @@ ExecOnConflictUpdate(ModifyTableState *mtstate,
    bool        isnull;
 
    /* Determine lock mode to use */
-   lockmode = ExecUpdateLockMode(estate, resultRelInfo);
+   lockmode = ExecUpdateLockMode(context->estate, resultRelInfo);
 
    /*
     * Lock tuple for update.  Don't follow updates when tuple cannot be
@@ -2011,8 +2188,8 @@ ExecOnConflictUpdate(ModifyTableState *mtstate,
     * true anymore.
     */
    test = table_tuple_lock(relation, conflictTid,
-                           estate->es_snapshot,
-                           existing, estate->es_output_cid,
+                           context->estate->es_snapshot,
+                           existing, context->estate->es_output_cid,
                            lockmode, LockWaitBlock, 0,
                            &tmfd);
    switch (test)
@@ -2119,7 +2296,7 @@ ExecOnConflictUpdate(ModifyTableState *mtstate,
     * snapshot.  This is in line with the way UPDATE deals with newer tuple
     * versions.
     */
-   ExecCheckTupleVisible(estate, relation, existing);
+   ExecCheckTupleVisible(context->estate, relation, existing);
 
    /*
     * Make tuple and any needed join variables available to ExecQual and
@@ -2174,10 +2351,9 @@ ExecOnConflictUpdate(ModifyTableState *mtstate,
     */
 
    /* Execute UPDATE with projection */
-   *returning = ExecUpdate(mtstate, resultRelInfo, conflictTid, NULL,
+   *returning = ExecUpdate(context, resultRelInfo,
+                           conflictTid, NULL,
                            resultRelInfo->ri_onConflict->oc_ProjSlot,
-                           planSlot,
-                           &mtstate->mt_epqstate, mtstate->ps.state,
                            canSetTag);
 
    /*
@@ -2349,6 +2525,7 @@ static TupleTableSlot *
 ExecModifyTable(PlanState *pstate)
 {
    ModifyTableState *node = castNode(ModifyTableState, pstate);
+   ModifyTableContext context;
    EState     *estate = node->ps.state;
    CmdType     operation = node->operation;
    ResultRelInfo *resultRelInfo;
@@ -2356,10 +2533,10 @@ ExecModifyTable(PlanState *pstate)
    TupleTableSlot *slot;
    TupleTableSlot *planSlot;
    TupleTableSlot *oldSlot;
-   ItemPointer tupleid;
    ItemPointerData tuple_ctid;
    HeapTupleData oldtupdata;
    HeapTuple   oldtuple;
+   ItemPointer tupleid;
    PartitionTupleRouting *proute = node->mt_partition_tuple_routing;
    List       *relinfos = NIL;
    ListCell   *lc;
@@ -2400,6 +2577,11 @@ ExecModifyTable(PlanState *pstate)
    resultRelInfo = node->resultRelInfo + node->mt_lastResultIndex;
    subplanstate = outerPlanState(node);
 
+   /* Set global context */
+   context.mtstate = node;
+   context.epqstate = &node->mt_epqstate;
+   context.estate = estate;
+
    /*
     * Fetch rows from subplan, and execute the required table modification
     * for each row.
@@ -2551,6 +2733,10 @@ ExecModifyTable(PlanState *pstate)
            }
        }
 
+       /* complete context setup */
+       context.planSlot = planSlot;
+       context.lockmode = 0;
+
        switch (operation)
        {
            case CMD_INSERT:
@@ -2558,9 +2744,10 @@ ExecModifyTable(PlanState *pstate)
                if (unlikely(!resultRelInfo->ri_projectNewInfoValid))
                    ExecInitInsertProjection(node, resultRelInfo);
                slot = ExecGetInsertNewTuple(resultRelInfo, planSlot);
-               slot = ExecInsert(node, resultRelInfo, slot, planSlot,
-                                 estate, node->canSetTag);
+               slot = ExecInsert(&context, resultRelInfo, slot,
+                                 node->canSetTag);
                break;
+
            case CMD_UPDATE:
                /* Initialize projection info if first time for this table */
                if (unlikely(!resultRelInfo->ri_projectNewInfoValid))
@@ -2581,7 +2768,6 @@ ExecModifyTable(PlanState *pstate)
                    /* Fetch the most recent version of old tuple. */
                    Relation    relation = resultRelInfo->ri_RelationDesc;
 
-                   Assert(tupleid != NULL);
                    if (!table_tuple_fetch_row_version(relation, tupleid,
                                                       SnapshotAny,
                                                       oldSlot))
@@ -2591,18 +2777,15 @@ ExecModifyTable(PlanState *pstate)
                                             oldSlot);
 
                /* Now apply the update. */
-               slot = ExecUpdate(node, resultRelInfo, tupleid, oldtuple, slot,
-                                 planSlot, &node->mt_epqstate, estate,
-                                 node->canSetTag);
+               slot = ExecUpdate(&context, resultRelInfo, tupleid, oldtuple,
+                                 slot, node->canSetTag);
                break;
+
            case CMD_DELETE:
-               slot = ExecDelete(node, resultRelInfo, tupleid, oldtuple,
-                                 planSlot, &node->mt_epqstate, estate,
-                                 true, /* processReturning */
-                                 node->canSetTag,
-                                 false,    /* changingPart */
-                                 NULL, NULL);
+               slot = ExecDelete(&context, resultRelInfo, tupleid, oldtuple,
+                                 true, false, node->canSetTag, NULL, NULL);
                break;
+
            default:
                elog(ERROR, "unknown operation");
                break;
@@ -2800,8 +2983,8 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
        }
 
        /* Initialize the usesFdwDirectModify flag */
-       resultRelInfo->ri_usesFdwDirectModify = bms_is_member(i,
-                                                             node->fdwDirectModifyPlans);
+       resultRelInfo->ri_usesFdwDirectModify =
+           bms_is_member(i, node->fdwDirectModifyPlans);
 
        /*
         * Verify result relation is a valid target for the current operation
index eaf3e7a8d44ee08fe01f2fb8d90f9b04c8799ce9..6ffd4474bcf9e03adf775b05dfb973a64116a9b1 100644 (file)
@@ -1460,6 +1460,7 @@ MinimalTupleTableSlot
 MinmaxMultiOpaque
 MinmaxOpaque
 ModifyTable
+ModifyTableContext
 ModifyTablePath
 ModifyTableState
 MorphOpaque
@@ -2793,6 +2794,7 @@ UnlistenStmt
 UnpackTarState
 UnresolvedTup
 UnresolvedTupData
+UpdateContext
 UpdateStmt
 UpperRelationKind
 UpperUniquePath