From a6986597728c42c1fc6d717ac9cfcce772b3510f Mon Sep 17 00:00:00 2001
From: amitlan <amitlangote09@gmail.com>
Date: Thu, 18 Jun 2020 19:41:49 +0900
Subject: [PATCH v2 3/3] Delay initializing UPDATE/DELETE ResultRelInfos

Currently, InitPlan() makes one for each of the result relations in
PlannedStmt.resultRelations which may be a long list if the planner
couldn't perform any pruning, such as when making a generic plan.
Instead, make the ResultRelInfo of a given result relation only when
the plan actually produces a tuple to update/delete that belongs to
that relation.

As part of this, the tuple conversion map to convert from child
format to root format that used to be part of TransitionCaptureState
is now moved into ResultRelInfo so that it can also be initialized
lazily for a given child result relation along with its
ResultRelInfo.
---
 contrib/postgres_fdw/postgres_fdw.c           |   4 +-
 src/backend/commands/copy.c                   |  37 +-
 src/backend/commands/explain.c                |  41 +-
 src/backend/commands/tablecmds.c              |   2 +-
 src/backend/commands/trigger.c                |   9 +-
 src/backend/executor/execMain.c               | 125 ++--
 src/backend/executor/execPartition.c          |  26 +-
 src/backend/executor/execUtils.c              |   4 +-
 src/backend/executor/nodeModifyTable.c        | 936 +++++++++++++-------------
 src/backend/replication/logical/worker.c      |   2 +-
 src/include/commands/trigger.h                |  12 -
 src/include/executor/executor.h               |   4 +
 src/include/executor/nodeModifyTable.h        |   1 -
 src/include/nodes/execnodes.h                 |  15 +-
 src/test/regress/expected/partition_prune.out |  47 ++
 src/test/regress/sql/partition_prune.sql      |  12 +
 16 files changed, 659 insertions(+), 618 deletions(-)

diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 4a495bf..f606dee 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -1994,7 +1994,7 @@ postgresBeginForeignInsert(ModifyTableState *mtstate,
 		 */
 		if (plan && plan->operation == CMD_UPDATE &&
 			resultRelation == plan->rootRelation)
-			resultRelation = mtstate->resultRelInfo[0].ri_RangeTableIndex;
+			resultRelation = linitial_int(plan->resultRelations);
 	}
 
 	/* Construct the SQL command string. */
@@ -2472,7 +2472,7 @@ postgresIterateDirectModify(ForeignScanState *node)
 	{
 		ForeignScan *fscan = (ForeignScan *) node->ss.ps.plan;
 
-		resultRelInfo = &estate->es_result_relations[fscan->resultRelIndex];
+		resultRelInfo = estate->es_result_relations[fscan->resultRelIndex];
 		Assert(resultRelInfo != NULL);
 		dmstate->resultRelInfo = resultRelInfo;
 	}
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 3e199bd..581d28f 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -2784,7 +2784,7 @@ CopyFrom(CopyState cstate)
 
 	ExecOpenIndices(resultRelInfo, false);
 
-	estate->es_result_relations = resultRelInfo;
+	estate->es_result_relations = &resultRelInfo;
 	estate->es_num_result_relations = 1;
 	estate->es_result_relation_info = resultRelInfo;
 
@@ -2798,7 +2798,7 @@ CopyFrom(CopyState cstate)
 	mtstate->ps.plan = NULL;
 	mtstate->ps.state = estate;
 	mtstate->operation = CMD_INSERT;
-	mtstate->resultRelInfo = estate->es_result_relations;
+	mtstate->resultRelInfo = resultRelInfo;
 
 	if (resultRelInfo->ri_FdwRoutine != NULL &&
 		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
@@ -3063,32 +3063,15 @@ 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 root rowtype.
+			 * If we're capturing transition tuples and there are no BR
+			 * triggers to modify the row, we can simply put the original
+			 * tuple into the transition tuplestore.
 			 */
-			if (cstate->transition_capture != NULL)
-			{
-				if (has_before_insert_row_trig)
-				{
-					/*
-					 * If there are any BEFORE 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 =
-						resultRelInfo->ri_PartitionInfo->pi_PartitionToRootMap;
-				}
-				else
-				{
-					/*
-					 * Otherwise, just remember the original unconverted
-					 * tuple, to avoid a needless round trip conversion.
-					 */
-					cstate->transition_capture->tcs_original_insert_tuple = myslot;
-					cstate->transition_capture->tcs_map = NULL;
-				}
-			}
+			if (cstate->transition_capture != NULL &&
+				!has_before_insert_row_trig)
+				cstate->transition_capture->tcs_original_insert_tuple = myslot;
+			else if (cstate->transition_capture != NULL)
+				cstate->transition_capture->tcs_original_insert_tuple = NULL;
 
 			/*
 			 * We might need to convert from the root rowtype to the partition
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 37e7ae1..17a4baa 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -18,6 +18,7 @@
 #include "commands/createas.h"
 #include "commands/defrem.h"
 #include "commands/prepare.h"
+#include "executor/executor.h"
 #include "executor/nodeHash.h"
 #include "foreign/fdwapi.h"
 #include "jit/jit.h"
@@ -795,13 +796,21 @@ ExplainPrintTriggers(ExplainState *es, QueryDesc *queryDesc)
 
 	show_relname = (numrels > 1 || numrootrels > 0 ||
 					routerels != NIL || targrels != NIL);
-	rInfo = queryDesc->estate->es_result_relations;
-	for (nr = 0; nr < numrels; rInfo++, nr++)
-		report_triggers(rInfo, show_relname, es);
+	for (nr = 0; nr < numrels; nr++)
+	{
+		rInfo = queryDesc->estate->es_result_relations[nr];
 
-	rInfo = queryDesc->estate->es_root_result_relations;
-	for (nr = 0; nr < numrootrels; rInfo++, nr++)
-		report_triggers(rInfo, show_relname, es);
+		if (rInfo)
+			report_triggers(rInfo, show_relname, es);
+	}
+
+	for (nr = 0; nr < numrootrels; nr++)
+	{
+		rInfo = queryDesc->estate->es_root_result_relations[nr];
+
+		if (rInfo)
+			report_triggers(rInfo, show_relname, es);
+	}
 
 	foreach(l, routerels)
 	{
@@ -3669,15 +3678,29 @@ show_modifytable_info(ModifyTableState *mtstate, List *ancestors,
 	/* Should we explicitly label target relations? */
 	labeltargets = (mtstate->mt_nrels > 1 ||
 					(mtstate->mt_nrels == 1 &&
-					 mtstate->resultRelInfo->ri_RangeTableIndex != node->nominalRelation));
+					 linitial_int(node->resultRelations) != node->nominalRelation));
 
 	if (labeltargets)
 		ExplainOpenGroup("Target Tables", "Target Tables", false, es);
 
 	for (j = 0; j < mtstate->mt_nrels; j++)
 	{
-		ResultRelInfo *resultRelInfo = mtstate->resultRelInfo + j;
-		FdwRoutine *fdwroutine = resultRelInfo->ri_FdwRoutine;
+		/*
+		 * Get the ResultRelInfo of to show the information of this result
+		 * relation.  When the ModifyTable is actually performed (ANALYZE is
+		 * on), we pass false for create_it, so as to show only those that
+		 * were actually initialized during the execution due to some tuple
+		 * in them getting modified.
+		 */
+		ResultRelInfo *resultRelInfo =
+			ExecGetResultRelInfo(mtstate, node->resultRelIndex + j,
+								 !es->analyze);
+		FdwRoutine *fdwroutine;
+
+		if (resultRelInfo == NULL)
+			continue;
+
+		fdwroutine = resultRelInfo->ri_FdwRoutine;
 
 		if (labeltargets)
 		{
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index f79044f..0342429 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -1789,7 +1789,7 @@ ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged,
 						  0);
 		resultRelInfo++;
 	}
-	estate->es_result_relations = resultRelInfos;
+	estate->es_result_relations = &resultRelInfos;
 	estate->es_num_result_relations = list_length(rels);
 
 	/*
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 58a5111..98c83a2 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -34,6 +34,7 @@
 #include "commands/dbcommands.h"
 #include "commands/defrem.h"
 #include "commands/trigger.h"
+#include "executor/execPartition.h"
 #include "executor/executor.h"
 #include "miscadmin.h"
 #include "nodes/bitmapset.h"
@@ -4295,10 +4296,6 @@ GetAfterTriggersTableData(Oid relid, CmdType cmdType)
  * If there are no triggers in 'trigdesc' that request relevant transition
  * tables, 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.
- *
  * Note that we copy the flags from a parent table into this struct (rather
  * than subsequently using the relation's TriggerDesc directly) so that we can
  * use it to control collection of transition tuples from child tables.
@@ -5392,7 +5389,9 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
 	if (row_trigger && transition_capture != NULL)
 	{
 		TupleTableSlot *original_insert_tuple = transition_capture->tcs_original_insert_tuple;
-		TupleConversionMap *map = transition_capture->tcs_map;
+		PartitionRoutingInfo *pinfo = relinfo->ri_PartitionInfo;
+		TupleConversionMap *map = pinfo ? pinfo->pi_PartitionToRootMap :
+			relinfo->ri_childToRootMap;
 		bool		delete_old_table = transition_capture->tcs_delete_old_table;
 		bool		update_old_table = transition_capture->tcs_update_old_table;
 		bool		update_new_table = transition_capture->tcs_update_new_table;
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 166486d..5f1b560 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -828,35 +828,17 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 	estate->es_plannedstmt = plannedstmt;
 
 	/*
-	 * Initialize ResultRelInfo data structures, and open the result rels.
+	 * Allocate space for ResultRelInfo pointers that will be filled later.
+	 * See ExecGetResultRelInfo() and ExecGetRootResultRelInfo().
 	 */
 	if (plannedstmt->resultRelations)
 	{
 		List	   *resultRelations = plannedstmt->resultRelations;
 		int			numResultRelations = list_length(resultRelations);
-		ResultRelInfo *resultRelInfos;
-		ResultRelInfo *resultRelInfo;
 
-		resultRelInfos = (ResultRelInfo *)
-			palloc(numResultRelations * sizeof(ResultRelInfo));
-		resultRelInfo = resultRelInfos;
-		foreach(l, resultRelations)
-		{
-			Index		resultRelationIndex = lfirst_int(l);
-			Relation	resultRelation;
-
-			resultRelation = ExecGetRangeTableRelation(estate,
-													   resultRelationIndex);
-			InitResultRelInfo(resultRelInfo,
-							  resultRelation,
-							  resultRelationIndex,
-							  NULL,
-							  estate->es_instrument);
-			resultRelInfo++;
-		}
-		estate->es_result_relations = resultRelInfos;
+		estate->es_result_relations =
+			palloc0(numResultRelations * sizeof(ResultRelInfo *));
 		estate->es_num_result_relations = numResultRelations;
-
 		/* es_result_relation_info is NULL except when within ModifyTable */
 		estate->es_result_relation_info = NULL;
 
@@ -869,25 +851,8 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 		{
 			int			num_roots = list_length(plannedstmt->rootResultRelations);
 
-			resultRelInfos = (ResultRelInfo *)
-				palloc(num_roots * sizeof(ResultRelInfo));
-			resultRelInfo = resultRelInfos;
-			foreach(l, plannedstmt->rootResultRelations)
-			{
-				Index		resultRelIndex = lfirst_int(l);
-				Relation	resultRelDesc;
-
-				resultRelDesc = ExecGetRangeTableRelation(estate,
-														  resultRelIndex);
-				InitResultRelInfo(resultRelInfo,
-								  resultRelDesc,
-								  resultRelIndex,
-								  NULL,
-								  estate->es_instrument);
-				resultRelInfo++;
-			}
-
-			estate->es_root_result_relations = resultRelInfos;
+			estate->es_root_result_relations =
+				palloc0(num_roots * sizeof(ResultRelInfo *));
 			estate->es_num_root_result_relations = num_roots;
 		}
 		else
@@ -1376,24 +1341,18 @@ ExecGetTriggerResultRel(EState *estate, Oid relid)
 	MemoryContext oldcontext;
 
 	/* First, search through the query result relations */
-	rInfo = estate->es_result_relations;
-	nr = estate->es_num_result_relations;
-	while (nr > 0)
+	for (nr = 0; nr < estate->es_num_result_relations; nr++)
 	{
-		if (RelationGetRelid(rInfo->ri_RelationDesc) == relid)
+		rInfo = estate->es_result_relations[nr];
+		if (rInfo && RelationGetRelid(rInfo->ri_RelationDesc) == relid)
 			return rInfo;
-		rInfo++;
-		nr--;
 	}
 	/* Second, search through the root result relations, if any */
-	rInfo = estate->es_root_result_relations;
-	nr = estate->es_num_root_result_relations;
-	while (nr > 0)
+	for (nr = 0; nr < estate->es_num_root_result_relations; nr++)
 	{
-		if (RelationGetRelid(rInfo->ri_RelationDesc) == relid)
+		rInfo = estate->es_root_result_relations[nr];
+		if (rInfo && RelationGetRelid(rInfo->ri_RelationDesc) == relid)
 			return rInfo;
-		rInfo++;
-		nr--;
 	}
 
 	/*
@@ -1559,14 +1518,21 @@ ExecEndPlan(PlanState *planstate, EState *estate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/*
-	 * close indexes of result relation(s) if any.  (Rels themselves get
-	 * closed next.)
+	 * Close indexes of result relation(s) if any.  (Rels themselves get
+	 * closed next.)  Also, allow the result relation's FDWs to shut down.
 	 */
-	resultRelInfo = estate->es_result_relations;
-	for (i = estate->es_num_result_relations; i > 0; i--)
+	for (i = 0; i < estate->es_num_result_relations; i++)
 	{
-		ExecCloseIndices(resultRelInfo);
-		resultRelInfo++;
+		resultRelInfo = estate->es_result_relations[i];
+		if (resultRelInfo)
+		{
+			ExecCloseIndices(resultRelInfo);
+			if (!resultRelInfo->ri_usesFdwDirectModify &&
+				resultRelInfo->ri_FdwRoutine != NULL &&
+				resultRelInfo->ri_FdwRoutine->EndForeignModify != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignModify(estate,
+															   resultRelInfo);
+		}
 	}
 
 	/*
@@ -2795,23 +2761,42 @@ EvalPlanQualStart(EPQState *epqstate, Plan *planTree)
 	{
 		int			numResultRelations = parentestate->es_num_result_relations;
 		int			numRootResultRels = parentestate->es_num_root_result_relations;
-		ResultRelInfo *resultRelInfos;
+		int			i;
+		ResultRelInfo *resultRelInfo;
 
-		resultRelInfos = (ResultRelInfo *)
-			palloc(numResultRelations * sizeof(ResultRelInfo));
-		memcpy(resultRelInfos, parentestate->es_result_relations,
-			   numResultRelations * sizeof(ResultRelInfo));
-		rcestate->es_result_relations = resultRelInfos;
+		rcestate->es_result_relations =
+			palloc0(numResultRelations * sizeof(ResultRelInfo *));
+		for (i = 0; i < numResultRelations; i++)
+		{
+			if (parentestate->es_result_relations[i])
+			{
+				resultRelInfo = makeNode(ResultRelInfo);
+				memcpy(resultRelInfo, parentestate->es_result_relations[i],
+					   sizeof(ResultRelInfo));
+			}
+			else
+				resultRelInfo = NULL;
+			rcestate->es_result_relations[i] = resultRelInfo;
+		}
 		rcestate->es_num_result_relations = numResultRelations;
 
 		/* Also transfer partitioned root result relations. */
 		if (numRootResultRels > 0)
 		{
-			resultRelInfos = (ResultRelInfo *)
-				palloc(numRootResultRels * sizeof(ResultRelInfo));
-			memcpy(resultRelInfos, parentestate->es_root_result_relations,
-				   numRootResultRels * sizeof(ResultRelInfo));
-			rcestate->es_root_result_relations = resultRelInfos;
+			rcestate->es_root_result_relations =
+				palloc0(numRootResultRels * sizeof(ResultRelInfo *));
+			for (i = 0; i < numRootResultRels; i++)
+			{
+				if (parentestate->es_root_result_relations[i])
+				{
+					resultRelInfo = makeNode(ResultRelInfo);
+					memcpy(resultRelInfo, parentestate->es_root_result_relations[i],
+						   sizeof(ResultRelInfo));
+				}
+				else
+					resultRelInfo = NULL;
+				rcestate->es_root_result_relations[i] = resultRelInfo;
+			}
 			rcestate->es_num_root_result_relations = numRootResultRels;
 		}
 	}
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 24afcb2..b425859 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -447,7 +447,9 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
 	ModifyTable *node = (ModifyTable *) mtstate->ps.plan;
 	Relation	rootrel = rootResultRelInfo->ri_RelationDesc,
 				partrel;
-	Relation	firstResultRel = mtstate->resultRelInfo[0].ri_RelationDesc;
+	Index		firstVarno = node ? linitial_int(node->resultRelations) : 0;
+	Relation	firstResultRel = firstVarno > 0 ?
+						ExecGetRangeTableRelation(estate, firstVarno) : NULL;
 	ResultRelInfo *leaf_part_rri;
 	MemoryContext oldcxt;
 	AttrMap    *part_attmap = NULL;
@@ -495,7 +497,6 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
 		List	   *wcoList;
 		List	   *wcoExprs = NIL;
 		ListCell   *ll;
-		int			firstVarno = mtstate->resultRelInfo[0].ri_RangeTableIndex;
 
 		/*
 		 * In the case of INSERT on a partitioned table, there is only one
@@ -559,7 +560,6 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
 		TupleTableSlot *slot;
 		ExprContext *econtext;
 		List	   *returningList;
-		int			firstVarno = mtstate->resultRelInfo[0].ri_RangeTableIndex;
 
 		/* See the comment above for WCO lists. */
 		Assert((node->operation == CMD_INSERT &&
@@ -618,7 +618,6 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
 	 */
 	if (node && node->onConflictAction != ONCONFLICT_NONE)
 	{
-		int			firstVarno = mtstate->resultRelInfo[0].ri_RangeTableIndex;
 		TupleDesc	partrelDesc = RelationGetDescr(partrel);
 		ExprContext *econtext = mtstate->ps.ps_ExprContext;
 		ListCell   *lc;
@@ -864,9 +863,22 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	if (mtstate &&
 		(mtstate->mt_transition_capture || mtstate->mt_oc_transition_capture))
 	{
-		partrouteinfo->pi_PartitionToRootMap =
-			convert_tuples_by_name(RelationGetDescr(partRelInfo->ri_RelationDesc),
-								   RelationGetDescr(partRelInfo->ri_PartitionRoot));
+		ModifyTable *node = (ModifyTable *) mtstate->ps.plan;
+
+		/*
+		 * If the partition appears to be an UPDATE result relation, the map
+		 * would already have been initialized by ExecBuildResultRelInfo(); use
+		 * that one instead of building one from scratch.  To distinguish
+		 * UPDATE result relations from tuple-routing result relations, we rely
+		 * on the fact that each of the former has a distinct RT index.
+		 */
+		if (node && node->rootRelation != partRelInfo->ri_RangeTableIndex)
+			partrouteinfo->pi_PartitionToRootMap =
+				partRelInfo->ri_childToRootMap;
+		else
+			partrouteinfo->pi_PartitionToRootMap =
+				convert_tuples_by_name(RelationGetDescr(partRelInfo->ri_RelationDesc),
+									   RelationGetDescr(partRelInfo->ri_PartitionRoot));
 	}
 	else
 		partrouteinfo->pi_PartitionToRootMap = NULL;
diff --git a/src/backend/executor/execUtils.c b/src/backend/executor/execUtils.c
index d0e65b8..a67c023 100644
--- a/src/backend/executor/execUtils.c
+++ b/src/backend/executor/execUtils.c
@@ -711,13 +711,13 @@ ExecCreateScanSlotFromOuterPlan(EState *estate,
 bool
 ExecRelationIsTargetRelation(EState *estate, Index scanrelid)
 {
-	ResultRelInfo *resultRelInfos;
+	ResultRelInfo **resultRelInfos;
 	int			i;
 
 	resultRelInfos = estate->es_result_relations;
 	for (i = 0; i < estate->es_num_result_relations; i++)
 	{
-		if (resultRelInfos[i].ri_RangeTableIndex == scanrelid)
+		if (resultRelInfos[i]->ri_RangeTableIndex == scanrelid)
 			return true;
 	}
 	return false;
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 74c12be..3f611a7 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -72,11 +72,10 @@ static TupleTableSlot *ExecPrepareTupleRouting(ModifyTableState *mtstate,
 											   ResultRelInfo *targetRelInfo,
 											   TupleTableSlot *slot);
 static ResultRelInfo *getTargetResultRelInfo(ModifyTableState *node);
-static void ExecSetupChildParentMapForSubplan(ModifyTableState *mtstate);
-static TupleConversionMap *tupconv_map_for_subplan(ModifyTableState *node,
-												   int whichplan);
 static TupleTableSlot *ExecGetNewInsertTuple(ResultRelInfo *relinfo,
 					  TupleTableSlot *planSlot);
+static ResultRelInfo *ExecBuildResultRelInfo(ModifyTableState *mtstate, Index rti,
+					   int resultRelIndex);
 
 /*
  * Verify that the tuples to be produced by INSERT or UPDATE match the
@@ -1060,6 +1059,9 @@ typedef struct SubplanResultRelHashElem
 static void
 ExecHashSubPlanResultRelsByOid(ModifyTableState *mtstate)
 {
+	EState	   *estate = mtstate->ps.state;
+	ModifyTable *plan = (ModifyTable *) mtstate->ps.plan;
+	ListCell   *lc;
 	HASHCTL		ctl;
 	HTAB	   *htab;
 	int			i;
@@ -1075,18 +1077,19 @@ ExecHashSubPlanResultRelsByOid(ModifyTableState *mtstate)
 					   &ctl, HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
 	mtstate->mt_subplan_resultrel_hash = htab;
 
-	/* Hash all subplans by their Oid */
-	for (i = 0; i < mtstate->mt_nrels; i++)
+	/* Map result relation OIDs to their index in es_result_relations. */
+	i = 0;
+	foreach(lc, plan->resultRelations)
 	{
-		ResultRelInfo *rri = &mtstate->resultRelInfo[i];
+		Index		rti = lfirst_int(lc);
 		bool		found;
-		Oid			partoid = RelationGetRelid(rri->ri_RelationDesc);
+		Oid			partoid = exec_rt_fetch(rti, estate)->relid;
 		SubplanResultRelHashElem *elem;
 
 		elem = (SubplanResultRelHashElem *)
 			hash_search(htab, &partoid, HASH_ENTER, &found);
 		Assert(!found);
-		elem->whichrel = i;
+		elem->whichrel = i++;
 	}
 }
 
@@ -1105,6 +1108,7 @@ ResultRelInfo *
 ExecLookupModifyResultRelByOid(ModifyTableState *mtstate,
 							   Oid reloid, int *whichrel)
 {
+	ModifyTable *node = (ModifyTable *) mtstate->ps.plan;
 	SubplanResultRelHashElem *elem;
 
 	Assert(mtstate->mt_subplan_resultrel_hash != NULL);
@@ -1115,13 +1119,416 @@ ExecLookupModifyResultRelByOid(ModifyTableState *mtstate,
 	if (elem)
 	{
 		*whichrel = elem->whichrel;
-		return mtstate->resultRelInfo + elem->whichrel;
+		return ExecGetResultRelInfo(mtstate,
+									node->resultRelIndex + elem->whichrel,
+									true);
 	}
 
 	return NULL;
 }
 
 /*
+ * ExecGetResultRelInfo
+ *		Returns the result relation at a given offset within the global array
+ *		of ResultRelInfos
+ *
+ * If not present and 'create_it' is true, it is created and put at the given
+ * offset for subsequent calls to find.
+ *
+ * This, in conjunction with ExecLookupResultRelByOid, allows lazy
+ * initialization of ResultRelInfos.  That can be helpful in the case where
+ * there are multiple result relations due to inheritance but only one or
+ * few actually end up actually having any tuples to update.
+ *
+ * Note: only call from the executor proper or anything that possesses a valid
+ * execution context, that is an EState with a PlannedStmt, because this
+ * depends on finding a valid PlannedStmt to get result relation RT indexes
+ * from.
+ */
+ResultRelInfo *
+ExecGetResultRelInfo(ModifyTableState *mtstate, int resultRelIndex,
+					 bool create_it)
+{
+	EState   *estate = mtstate->ps.state;
+	ResultRelInfo *resultRelInfo = estate->es_result_relations[resultRelIndex];
+
+	if (resultRelInfo == NULL && create_it)
+	{
+		List   *resultRelations = estate->es_plannedstmt->resultRelations;
+		Index	rti = list_nth_int(resultRelations, resultRelIndex);
+
+		resultRelInfo = ExecBuildResultRelInfo(mtstate, rti, resultRelIndex);
+		estate->es_result_relations[resultRelIndex] = resultRelInfo;
+	}
+
+	return resultRelInfo;
+}
+
+/*
+ * ExecGetRootResultRelInfo
+ *		Like ExecGetResultRelInfo, but for "root" result relations
+ *		corresponding to partitioned tables, which are managed separately from
+ *		leaf result relations
+ *
+ * Root ResultRelInfos are never created lazily, although it seems better to
+ * have the same interface to avoid exposing ExecBuildResultRelInfo().
+ */
+ResultRelInfo *
+ExecGetRootResultRelInfo(ModifyTableState *mtstate, int rootRelIndex)
+{
+	EState   *estate = mtstate->ps.state;
+	ResultRelInfo *rootRelInfo = estate->es_root_result_relations[rootRelIndex];
+
+	if (rootRelInfo == NULL)
+	{
+		List   *rootRelations = estate->es_plannedstmt->rootResultRelations;
+		Index	rti = list_nth_int(rootRelations, rootRelIndex);
+
+		rootRelInfo = ExecBuildResultRelInfo(mtstate, rti, rootRelIndex);
+		estate->es_root_result_relations[rootRelIndex] = rootRelInfo;
+	}
+
+	return rootRelInfo;
+}
+
+/*
+ * ExecBuildResultRelInfo
+ *		Builds a ResultRelInfo for a result relation with given RT index
+ */
+static ResultRelInfo *
+ExecBuildResultRelInfo(ModifyTableState *mtstate, Index rti,
+					   int resultRelIndex)
+{
+	EState	   *estate = mtstate->ps.state;
+	ModifyTable *node = (ModifyTable *) mtstate->ps.plan;
+	Plan	   *subplan = linitial(node->plans);
+	CmdType		operation = node->operation;
+	int			firstRelIndex = node->resultRelIndex;
+	int			thisRelIndex = resultRelIndex - firstRelIndex;
+	Relation	relation = ExecGetRangeTableRelation(estate, rti);
+	ResultRelInfo *resultRelInfo;
+	List   *resultTargetList = NIL;
+	bool	need_projection = false;
+	bool	update_tuple_routing_needed = false;
+	ListCell *l;
+	int		eflags = estate->es_top_eflags;
+	MemoryContext oldcxt;
+
+	/* Things built here have to last for the query duration. */
+	oldcxt = MemoryContextSwitchTo(estate->es_query_cxt);
+
+	resultRelInfo = makeNode(ResultRelInfo);
+	InitResultRelInfo(resultRelInfo, relation, rti, NULL,
+					  estate->es_instrument);
+
+	/*
+	 * If this is the root result relation of an UPDATE/DELETE, we only need
+	 * a minimally valid result relation.
+	 */
+	if (rti == node->rootRelation && operation != CMD_INSERT)
+	{
+		MemoryContextSwitchTo(oldcxt);
+		return resultRelInfo;
+	}
+
+	/* Initialize the usesFdwDirectModify flag */
+	resultRelInfo->ri_usesFdwDirectModify = bms_is_member(thisRelIndex,
+														  node->fdwDirectModifyPlans);
+
+	/*
+	 * Verify result relation is a valid target for the current operation
+	 */
+	CheckValidResultRel(resultRelInfo, operation);
+
+	/*
+	 * If there are indices on the result relation, open them and save
+	 * descriptors in the result relation info, so that we can add new
+	 * index entries for the tuples we add/update.  We need not do this
+	 * for a DELETE, however, since deletion doesn't affect indexes. Also,
+	 * inside an EvalPlanQual operation, the indexes might be open
+	 * already, since we share the resultrel state with the original
+	 * query.
+	 */
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relhasindex &&
+		operation != CMD_DELETE &&
+		resultRelInfo->ri_IndexRelationDescs == NULL)
+		ExecOpenIndices(resultRelInfo,
+						node->onConflictAction != ONCONFLICT_NONE);
+
+	/* Result relation specific slot to store the plan's output tuple. */
+	mtstate->mt_scans[thisRelIndex] =
+			ExecInitExtraTupleSlot(mtstate->ps.state, mtstate->mt_plan_tupdesc,
+								   table_slot_callbacks(resultRelInfo->ri_RelationDesc));
+
+	/* Also let FDWs init themselves for foreign-table result rels */
+	if (!resultRelInfo->ri_usesFdwDirectModify &&
+		resultRelInfo->ri_FdwRoutine != NULL &&
+		resultRelInfo->ri_FdwRoutine->BeginForeignModify != NULL)
+	{
+		List	   *fdw_private = (List *) list_nth(node->fdwPrivLists,
+													thisRelIndex);
+
+		resultRelInfo->ri_FdwRoutine->BeginForeignModify(mtstate,
+														 resultRelInfo,
+														 fdw_private,
+														 thisRelIndex,
+														 eflags);
+	}
+
+	/*
+	 * Initialize any WITH CHECK OPTION constraints if needed.
+	 */
+	if (node->withCheckOptionLists)
+	{
+		List   *wcoList = (List *) list_nth(node->withCheckOptionLists,
+											thisRelIndex);
+		List   *wcoExprs = NIL;
+		ListCell   *ll;
+
+		foreach(ll, wcoList)
+		{
+			WithCheckOption *wco = (WithCheckOption *) lfirst(ll);
+			ExprState  *wcoExpr = ExecInitQual((List *) wco->qual,
+											   &mtstate->ps);
+
+			wcoExprs = lappend(wcoExprs, wcoExpr);
+		}
+
+		resultRelInfo->ri_WithCheckOptions = wcoList;
+		resultRelInfo->ri_WithCheckOptionExprs = wcoExprs;
+	}
+
+	/* RETURNING list */
+	if (node->returningLists)
+	{
+		List	*rlist = (List *) list_nth(node->returningLists,
+										   thisRelIndex);
+		TupleTableSlot *slot;
+		ExprContext *econtext;
+
+		slot = mtstate->ps.ps_ResultTupleSlot;
+		Assert(slot != NULL);
+		econtext = mtstate->ps.ps_ExprContext;
+		Assert(econtext != NULL);
+
+		resultRelInfo->ri_returningList = rlist;
+		resultRelInfo->ri_projectReturning =
+			ExecBuildProjectionInfo(rlist, econtext, slot, &mtstate->ps,
+									resultRelInfo->ri_RelationDesc->rd_att);
+	}
+
+	/* Set the list of arbiter indexes if needed for ON CONFLICT */
+	if (node->onConflictAction != ONCONFLICT_NONE)
+		resultRelInfo->ri_onConflictArbiterIndexes = node->arbiterIndexes;
+
+	/*
+	 * If needed, Initialize target list, projection and qual for ON CONFLICT
+	 * DO UPDATE.
+	 */
+	if (node->onConflictAction == ONCONFLICT_UPDATE)
+	{
+		ExprContext *econtext;
+		TupleDesc	relationDesc;
+		TupleDesc	tupDesc;
+
+		/* insert may only have one relation, inheritance is not expanded */
+		Assert(mtstate->mt_nrels == 1);
+
+		/* already exists if created by RETURNING processing above */
+		if (mtstate->ps.ps_ExprContext == NULL)
+			ExecAssignExprContext(estate, &mtstate->ps);
+
+		econtext = mtstate->ps.ps_ExprContext;
+		relationDesc = resultRelInfo->ri_RelationDesc->rd_att;
+
+		/* create state for DO UPDATE SET operation */
+		resultRelInfo->ri_onConflict = makeNode(OnConflictSetState);
+
+		/* initialize slot for the existing tuple */
+		resultRelInfo->ri_onConflict->oc_Existing =
+			table_slot_create(resultRelInfo->ri_RelationDesc,
+							  &mtstate->ps.state->es_tupleTable);
+
+		/*
+		 * Create the tuple slot for the UPDATE SET projection. We want a slot
+		 * of the table's type here, because the slot will be used to insert
+		 * into the table, and for RETURNING processing - which may access
+		 * system attributes.
+		 */
+		tupDesc = ExecTypeFromTL((List *) node->onConflictSet);
+		resultRelInfo->ri_onConflict->oc_ProjSlot =
+			ExecInitExtraTupleSlot(mtstate->ps.state, tupDesc,
+								   table_slot_callbacks(resultRelInfo->ri_RelationDesc));
+
+		/* build UPDATE SET projection state */
+		resultRelInfo->ri_onConflict->oc_ProjInfo =
+			ExecBuildProjectionInfo(node->onConflictSet, econtext,
+									resultRelInfo->ri_onConflict->oc_ProjSlot,
+									&mtstate->ps,
+									relationDesc);
+
+		/* initialize state to evaluate the WHERE clause, if any */
+		if (node->onConflictWhere)
+		{
+			ExprState  *qualexpr;
+
+			qualexpr = ExecInitQual((List *) node->onConflictWhere,
+									&mtstate->ps);
+			resultRelInfo->ri_onConflict->oc_WhereClause = qualexpr;
+		}
+	}
+
+	/*
+	 * Prepare to generate tuples suitable for the target relation.
+	 */
+	if (operation == CMD_INSERT || operation == CMD_UPDATE)
+	{
+		if (operation == CMD_INSERT)
+		{
+			foreach(l, subplan->targetlist)
+			{
+				TargetEntry *tle = (TargetEntry *) lfirst(l);
+
+				if (!tle->resjunk)
+					resultTargetList = lappend(resultTargetList, tle);
+				else
+					need_projection = true;
+			}
+		}
+		else
+		{
+			resultTargetList = (List *) list_nth(node->updateTargetLists,
+												 thisRelIndex);
+			need_projection = true;
+		}
+
+		/*
+		 * The clean list must produce a tuple suitable for the result
+		 * relation.
+		 */
+		ExecCheckPlanOutput(resultRelInfo->ri_RelationDesc, resultTargetList);
+	}
+
+	if (need_projection)
+	{
+		TupleDesc	relDesc = RelationGetDescr(resultRelInfo->ri_RelationDesc);
+
+		/*
+		 * For UPDATE, we use the old tuple to fill up missing values in
+		 * the tuple produced by the plan to get the new tuple.
+		 */
+		if (operation == CMD_UPDATE)
+			resultRelInfo->ri_oldTupleSlot =
+				table_slot_create(resultRelInfo->ri_RelationDesc,
+								  &mtstate->ps.state->es_tupleTable);
+		resultRelInfo->ri_newTupleSlot =
+			table_slot_create(resultRelInfo->ri_RelationDesc,
+							  &mtstate->ps.state->es_tupleTable);
+
+		/* need an expression context to do the projection */
+		if (mtstate->ps.ps_ExprContext == NULL)
+			ExecAssignExprContext(estate, &mtstate->ps);
+
+		resultRelInfo->ri_projectNew =
+			ExecBuildProjectionInfo(resultTargetList,
+									mtstate->ps.ps_ExprContext,
+									resultRelInfo->ri_newTupleSlot,
+									&mtstate->ps,
+									relDesc);
+	}
+
+	/*
+	 * For UPDATE/DELETE, find the appropriate junk attr now.
+	 */
+	if (operation == CMD_UPDATE || operation == CMD_DELETE)
+	{
+		char	relkind;
+
+		relkind = resultRelInfo->ri_RelationDesc->rd_rel->relkind;
+		if (relkind == RELKIND_RELATION ||
+			relkind == RELKIND_MATVIEW ||
+			relkind == RELKIND_PARTITIONED_TABLE)
+		{
+			resultRelInfo->ri_junkAttno =
+				ExecFindJunkAttributeInTlist(subplan->targetlist, "ctid");
+			if (!AttributeNumberIsValid(resultRelInfo->ri_junkAttno))
+				elog(ERROR, "could not find junk ctid column");
+		}
+		else if (relkind == RELKIND_FOREIGN_TABLE)
+		{
+			/*
+			 * When there is a row-level trigger, there should be
+			 * a wholerow attribute.
+			 */
+			resultRelInfo->ri_junkAttno =
+				ExecFindJunkAttributeInTlist(subplan->targetlist, "wholerow");
+			/* HACK: we require it to be present for updates. */
+			if (mtstate->operation == CMD_UPDATE &&
+				!AttributeNumberIsValid(resultRelInfo->ri_junkAttno))
+				elog(ERROR, "could not find junk wholerow column");
+		}
+		else
+		{
+			resultRelInfo->ri_junkAttno =
+				ExecFindJunkAttributeInTlist(subplan->targetlist, "wholerow");
+			if (!AttributeNumberIsValid(resultRelInfo->ri_junkAttno))
+				elog(ERROR, "could not find junk wholerow column");
+		}
+	}
+
+	/*
+	 * For UPDATE on a partitioned tables, tuple routing might be needed if
+	 * if the plan says so or a BEFORE UPDATE trigger is present on the
+	 * partition which might modify the partition-key values.
+	 */
+	if (mtstate->rootResultRelInfo &&
+		operation == CMD_UPDATE &&
+		(node->partColsUpdated ||
+		 (resultRelInfo->ri_TrigDesc &&
+		  resultRelInfo->ri_TrigDesc->trig_update_before_row)))
+		update_tuple_routing_needed = true;
+
+	/* Tuple routing state may already have been initialized. */
+	if (update_tuple_routing_needed &&
+		mtstate->mt_partition_tuple_routing == NULL)
+	{
+		Relation rootRel = mtstate->rootResultRelInfo->ri_RelationDesc;
+		mtstate->mt_partition_tuple_routing =
+			ExecSetupPartitionTupleRouting(estate, mtstate, rootRel);
+		/*
+		 * Before a partition's tuple can be re-routed, it must first
+		 * be converted to the root's format and we need a slot for
+		 * storing such tuple.
+		 */
+		mtstate->mt_root_tuple_slot = table_slot_create(rootRel, NULL);
+	}
+
+	/*
+	 * If needed, initialize a map to convert tuples in the child format
+	 * to the format of the table mentioned in the query (root relation).
+	 * It's needed for update tuple routing, because the routing starts
+	 * from the root relation.  It's also needed for capturing transition
+	 * tuples, because the transition tuple store can only store tuples
+	 * in the root table format.
+	 */
+	if (update_tuple_routing_needed ||
+		(mtstate->mt_transition_capture &&
+		 mtstate->operation != CMD_INSERT &&
+		 thisRelIndex > 0))
+	{
+		Relation targetRel = getTargetResultRelInfo(mtstate)->ri_RelationDesc;
+
+		resultRelInfo->ri_childToRootMap =
+			convert_tuples_by_name(RelationGetDescr(relation),
+								   RelationGetDescr(targetRel));
+	}
+
+	MemoryContextSwitchTo(oldcxt);
+
+	return resultRelInfo;
+}
+
+/*
  * ExecGetInsertNewTuple
  *		This prepares a "new" tuple ready to be put into a result relation
  *		by removing any junk columns of the tuple produced by a plan.
@@ -1217,7 +1624,6 @@ ExecUpdate(ModifyTableState *mtstate,
 	TM_Result	result;
 	TM_FailureData tmfd;
 	List	   *recheckIndexes = NIL;
-	TupleConversionMap *saved_tcs_map = NULL;
 
 	/*
 	 * abort the operation if not running transactions
@@ -1342,7 +1748,6 @@ lreplace:;
 			TupleTableSlot *ret_slot;
 			TupleTableSlot *epqslot = NULL;
 			PartitionTupleRouting *proute = mtstate->mt_partition_tuple_routing;
-			int			map_index;
 			TupleConversionMap *tupconv_map;
 
 			/*
@@ -1413,16 +1818,6 @@ lreplace:;
 			}
 
 			/*
-			 * Updates set the transition capture map only when a new subplan
-			 * is chosen.  But for inserts, it is set for each row. So after
-			 * INSERT, we need to revert back to the map created for UPDATE;
-			 * otherwise the next UPDATE will incorrectly use the one created
-			 * for INSERT.  So first save the one created for UPDATE.
-			 */
-			if (mtstate->mt_transition_capture)
-				saved_tcs_map = mtstate->mt_transition_capture->tcs_map;
-
-			/*
 			 * resultRelInfo is one of the per-subplan resultRelInfos.  So we
 			 * should convert the tuple into root's tuple descriptor, since
 			 * ExecInsert() starts the search from root.  The tuple conversion
@@ -1430,9 +1825,7 @@ lreplace:;
 			 * retrieve the one for this resultRel, we need to know the
 			 * position of the resultRel in mtstate->resultRelInfo[].
 			 */
-			map_index = resultRelInfo - mtstate->resultRelInfo;
-			Assert(map_index >= 0 && map_index < mtstate->mt_nrels);
-			tupconv_map = tupconv_map_for_subplan(mtstate, map_index);
+			tupconv_map = resultRelInfo->ri_childToRootMap;
 			if (tupconv_map != NULL)
 				slot = execute_attr_map_slot(tupconv_map->attrMap,
 											 slot,
@@ -1451,11 +1844,13 @@ lreplace:;
 
 			/* Revert ExecPrepareTupleRouting's node change. */
 			estate->es_result_relation_info = resultRelInfo;
+
+			/*
+			 * Reset the transition state that may possibly have been written
+			 * by INSERT.
+			 */
 			if (mtstate->mt_transition_capture)
-			{
 				mtstate->mt_transition_capture->tcs_original_insert_tuple = NULL;
-				mtstate->mt_transition_capture->tcs_map = saved_tcs_map;
-			}
 
 			return ret_slot;
 		}
@@ -1921,7 +2316,7 @@ static ResultRelInfo *
 getTargetResultRelInfo(ModifyTableState *node)
 {
 	/*
-	 * Note that if the node modifies a partitioned table, node->resultRelInfo
+	 * Note that if the node modifies a partitioned table, plan->resultRelInfo
 	 * points to the first leaf partition, not the root table.
 	 */
 	if (node->rootResultRelInfo != NULL)
@@ -1984,28 +2379,6 @@ ExecSetupTransitionCaptureState(ModifyTableState *mtstate, EState *estate)
 			MakeTransitionCaptureState(targetRelInfo->ri_TrigDesc,
 									   RelationGetRelid(targetRelInfo->ri_RelationDesc),
 									   CMD_UPDATE);
-
-	/*
-	 * 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.  (We can share these maps
-	 * between the regular and ON CONFLICT cases.)
-	 */
-	if (mtstate->mt_transition_capture != NULL ||
-		mtstate->mt_oc_transition_capture != NULL)
-	{
-		ExecSetupChildParentMapForSubplan(mtstate);
-
-		/*
-		 * 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, so we need not update
-		 * mtstate->mt_oc_transition_capture here.)
-		 */
-		if (mtstate->mt_transition_capture && mtstate->operation != CMD_INSERT)
-			mtstate->mt_transition_capture->tcs_map =
-				tupconv_map_for_subplan(mtstate, 0);
-	}
 }
 
 /*
@@ -2047,37 +2420,16 @@ ExecPrepareTupleRouting(ModifyTableState *mtstate,
 	estate->es_result_relation_info = partrel;
 
 	/*
-	 * If we're capturing transition tuples, we might need to convert from the
-	 * partition rowtype to root partitioned table's rowtype.
+	 * If we're capturing transition tuples and there are no BR triggers to
+	 * modify the row, we can simply put the original tuple into the
+	 * transition tuplestore.
 	 */
-	if (mtstate->mt_transition_capture != NULL)
-	{
-		if (partrel->ri_TrigDesc &&
-			partrel->ri_TrigDesc->trig_insert_before_row)
-		{
-			/*
-			 * If there are any BEFORE 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 =
-				partrouteinfo->pi_PartitionToRootMap;
-		}
-		else
-		{
-			/*
-			 * Otherwise, just remember the original unconverted tuple, to
-			 * avoid a needless round trip conversion.
-			 */
-			mtstate->mt_transition_capture->tcs_original_insert_tuple = slot;
-			mtstate->mt_transition_capture->tcs_map = NULL;
-		}
-	}
-	if (mtstate->mt_oc_transition_capture != NULL)
-	{
-		mtstate->mt_oc_transition_capture->tcs_map =
-			partrouteinfo->pi_PartitionToRootMap;
-	}
+	if (mtstate->mt_transition_capture != NULL &&
+		!(partrel->ri_TrigDesc &&
+		  partrel->ri_TrigDesc->trig_insert_before_row))
+		mtstate->mt_transition_capture->tcs_original_insert_tuple = slot;
+	else if (mtstate->mt_transition_capture != NULL)
+		mtstate->mt_transition_capture->tcs_original_insert_tuple = NULL;
 
 	/*
 	 * Convert the tuple, if necessary.
@@ -2093,58 +2445,6 @@ ExecPrepareTupleRouting(ModifyTableState *mtstate,
 	return slot;
 }
 
-/*
- * Initialize the child-to-root tuple conversion map array for UPDATE subplans.
- *
- * This map array is required to convert the tuple from the subplan result rel
- * to the target table descriptor. This requirement arises for two independent
- * scenarios:
- * 1. For update-tuple-routing.
- * 2. For capturing tuples in transition tables.
- */
-static void
-ExecSetupChildParentMapForSubplan(ModifyTableState *mtstate)
-{
-	ResultRelInfo *targetRelInfo = getTargetResultRelInfo(mtstate);
-	ResultRelInfo *resultRelInfos = mtstate->resultRelInfo;
-	TupleDesc	outdesc;
-	int			numResultRelInfos = mtstate->mt_nrels;
-	int			i;
-
-	/*
-	 * Build array of conversion maps from each child's TupleDesc to the one
-	 * used in the target relation.  The map pointers may be NULL when no
-	 * conversion is necessary, which is hopefully a common case.
-	 */
-
-	/* Get tuple descriptor of the target rel. */
-	outdesc = RelationGetDescr(targetRelInfo->ri_RelationDesc);
-
-	mtstate->mt_per_subplan_tupconv_maps = (TupleConversionMap **)
-		palloc(sizeof(TupleConversionMap *) * numResultRelInfos);
-
-	for (i = 0; i < numResultRelInfos; ++i)
-	{
-		mtstate->mt_per_subplan_tupconv_maps[i] =
-			convert_tuples_by_name(RelationGetDescr(resultRelInfos[i].ri_RelationDesc),
-								   outdesc);
-	}
-}
-
-/*
- * For a given subplan index, get the tuple conversion map.
- */
-static TupleConversionMap *
-tupconv_map_for_subplan(ModifyTableState *mtstate, int whichplan)
-{
-	/* If nobody else set the per-subplan array of maps, do so ourselves. */
-	if (mtstate->mt_per_subplan_tupconv_maps == NULL)
-		ExecSetupChildParentMapForSubplan(mtstate);
-
-	Assert(whichplan >= 0 && whichplan < mtstate->mt_nrels);
-	return mtstate->mt_per_subplan_tupconv_maps[whichplan];
-}
-
 /* ----------------------------------------------------------------
  *	   ExecModifyTable
  *
@@ -2159,8 +2459,10 @@ ExecModifyTable(PlanState *pstate)
 	PartitionTupleRouting *proute = node->mt_partition_tuple_routing;
 	EState	   *estate = node->ps.state;
 	CmdType		operation = node->operation;
+	int			firstRelIndex = ((ModifyTable *) node->ps.plan)->resultRelIndex;
 	ResultRelInfo *saved_resultRelInfo;
 	ResultRelInfo *resultRelInfo;
+	int			whichrel;
 	PlanState  *subplanstate;
 	TupleTableSlot *slot;
 	TupleTableSlot *planSlot;
@@ -2202,7 +2504,8 @@ ExecModifyTable(PlanState *pstate)
 	}
 
 	/* Preload local variables */
-	resultRelInfo = node->resultRelInfo + node->mt_whichrel;
+	whichrel = 0;	/* default result rel */
+	resultRelInfo = ExecGetResultRelInfo(node, firstRelIndex + whichrel, true);
 	subplanstate = node->mt_plans[node->mt_whichplan];
 
 	/*
@@ -2262,31 +2565,18 @@ ExecModifyTable(PlanState *pstate)
 
 			/* Table OID -> ResultRelInfo. */
 			resultRelInfo = ExecLookupModifyResultRelByOid(node, tableoid,
-														   &node->mt_whichrel);
-			Assert(node->mt_whichrel >= 0 &&
-				   node->mt_whichrel < node->mt_nrels);
+														   &whichrel);
+			Assert(whichrel >= 0 && whichrel < node->mt_nrels);
 			estate->es_result_relation_info = resultRelInfo;
-
-			/* Prepare to convert transition tuples from this child. */
-			if (node->mt_transition_capture != NULL)
-			{
-				node->mt_transition_capture->tcs_map =
-				tupconv_map_for_subplan(node, node->mt_whichrel);
-			}
-			if (node->mt_oc_transition_capture != NULL)
-			{
-				node->mt_oc_transition_capture->tcs_map =
-				tupconv_map_for_subplan(node, node->mt_whichrel);
-			}
 		}
 
 		/*
 		 * Ensure input tuple is the right format for the target relation.
 		 */
-		if (node->mt_scans[node->mt_whichrel]->tts_ops != planSlot->tts_ops)
+		if (node->mt_scans[whichrel]->tts_ops != planSlot->tts_ops)
 		{
-			ExecCopySlot(node->mt_scans[node->mt_whichrel], planSlot);
-			planSlot = node->mt_scans[node->mt_whichrel];
+			ExecCopySlot(node->mt_scans[whichrel], planSlot);
+			planSlot = node->mt_scans[whichrel];
 		}
 
 		/*
@@ -2450,14 +2740,9 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 	CmdType		operation = node->operation;
 	int			nplans = list_length(node->plans);
 	int			nrels = list_length(node->resultRelations);
-	ResultRelInfo *saved_resultRelInfo;
-	ResultRelInfo *resultRelInfo;
 	Plan	   *subplan;
-	TupleDesc	plan_result_type;
 	ListCell   *l;
-	int			i;
 	Relation	rel;
-	bool		update_tuple_routing_needed = node->partColsUpdated;
 
 	/* check for unsupported flags */
 	Assert(!(eflags & (EXEC_FLAG_BACKWARD | EXEC_FLAG_MARK)));
@@ -2475,14 +2760,8 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 	mtstate->mt_done = false;
 
 	mtstate->mt_plans = (PlanState **) palloc0(sizeof(PlanState *) * nplans);
-	mtstate->resultRelInfo = estate->es_result_relations + node->resultRelIndex;
 	mtstate->mt_scans = (TupleTableSlot **) palloc0(sizeof(TupleTableSlot *) * nrels);
 
-	/* If modifying a partitioned table, initialize the root table info */
-	if (node->rootResultRelIndex >= 0)
-		mtstate->rootResultRelInfo = estate->es_root_result_relations +
-			node->rootResultRelIndex;
-
 	mtstate->mt_arowmarks = (List **) palloc0(sizeof(List *) * nplans);
 	mtstate->mt_nplans = nplans;
 
@@ -2491,7 +2770,6 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 	mtstate->fireBSTriggers = true;
 
 	mtstate->mt_nrels = nrels;
-	mtstate->mt_whichrel = 0;
 
 	/*
 	 * Call ExecInitNode on the only plan to be executed and save the result
@@ -2502,12 +2780,9 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 	 * (see contrib/postgres_fdw/postgres_fdw.c: postgresBeginDirectModify()
 	 * as one example).
 	 */
-	saved_resultRelInfo = estate->es_result_relation_info;
-	estate->es_result_relation_info = mtstate->resultRelInfo;
 	subplan = linitial(node->plans);
 	mtstate->mt_plans[0] = ExecInitNode(subplan, estate, eflags);
-	estate->es_result_relation_info = saved_resultRelInfo;
-	plan_result_type = ExecGetResultType(mtstate->mt_plans[0]);
+	mtstate->mt_plan_tupdesc = ExecGetResultType(mtstate->mt_plans[0]);
 
 	if (mtstate->mt_nrels > 1)
 	{
@@ -2518,6 +2793,12 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		mtstate->mt_tableOidAttno =
 			ExecFindJunkAttributeInTlist(subplan->targetlist, "tableoid");
 		Assert(AttributeNumberIsValid(mtstate->mt_tableOidAttno));
+
+		/*
+		 * Also, initialize a hash table to look up UPDATE/DELETE result
+		 * relations.
+		 */
+		ExecHashSubPlanResultRelsByOid(mtstate);
 	}
 
 	/* Initialize some global state for RETURNING projections. */
@@ -2549,226 +2830,25 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 	}
 
 	/*
-	 * Per result relation initializations.
-	 * TODO: do this lazily.
+	 * If modifying a partitioned table, initialize the root table info.
 	 */
-	resultRelInfo = mtstate->resultRelInfo;
-	for (i = 0; i < nrels; i++)
-	{
-		List   *resultTargetList = NIL;
-		bool	need_projection = false;
-
-		/* Initialize the usesFdwDirectModify flag */
-		resultRelInfo->ri_usesFdwDirectModify = bms_is_member(i,
-															  node->fdwDirectModifyPlans);
-
-		/*
-		 * Verify result relation is a valid target for the current operation
-		 */
-		CheckValidResultRel(resultRelInfo, operation);
-
-		/*
-		 * If there are indices on the result relation, open them and save
-		 * descriptors in the result relation info, so that we can add new
-		 * index entries for the tuples we add/update.  We need not do this
-		 * for a DELETE, however, since deletion doesn't affect indexes. Also,
-		 * inside an EvalPlanQual operation, the indexes might be open
-		 * already, since we share the resultrel state with the original
-		 * query.
-		 */
-		if (resultRelInfo->ri_RelationDesc->rd_rel->relhasindex &&
-			operation != CMD_DELETE &&
-			resultRelInfo->ri_IndexRelationDescs == NULL)
-			ExecOpenIndices(resultRelInfo,
-							node->onConflictAction != ONCONFLICT_NONE);
-
-		/*
-		 * If this is an UPDATE and a BEFORE UPDATE trigger is present, the
-		 * trigger itself might modify the partition-key values. So arrange
-		 * for tuple routing.
-		 */
-		if (resultRelInfo->ri_TrigDesc &&
-			resultRelInfo->ri_TrigDesc->trig_update_before_row &&
-			operation == CMD_UPDATE)
-			update_tuple_routing_needed = true;
-
-		mtstate->mt_scans[i] =
-			ExecInitExtraTupleSlot(mtstate->ps.state, plan_result_type,
-								   table_slot_callbacks(resultRelInfo->ri_RelationDesc));
-
-		/* Now init the plan for this result rel */
-		estate->es_result_relation_info = resultRelInfo;
-
-		/* Also let FDWs init themselves for foreign-table result rels */
-		if (!resultRelInfo->ri_usesFdwDirectModify &&
-			resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->BeginForeignModify != NULL)
-		{
-			List	   *fdw_private = (List *) list_nth(node->fdwPrivLists, i);
-
-			resultRelInfo->ri_FdwRoutine->BeginForeignModify(mtstate,
-															 resultRelInfo,
-															 fdw_private,
-															 i,
-															 eflags);
-		}
-
-		/*
-		 * Initialize any WITH CHECK OPTION constraints if needed.
-		 */
-		if (node->withCheckOptionLists)
-		{
-			List   *wcoList = (List *) list_nth(node->withCheckOptionLists, i);
-			List   *wcoExprs = NIL;
-			ListCell   *ll;
-
-			foreach(ll, wcoList)
-			{
-				WithCheckOption *wco = (WithCheckOption *) lfirst(ll);
-				ExprState  *wcoExpr = ExecInitQual((List *) wco->qual,
-												   &mtstate->ps);
-
-				wcoExprs = lappend(wcoExprs, wcoExpr);
-			}
-
-			resultRelInfo->ri_WithCheckOptions = wcoList;
-			resultRelInfo->ri_WithCheckOptionExprs = wcoExprs;
-		}
-		if (node->returningLists)
-		{
-			List	*rlist = (List *) list_nth(node->returningLists, i);
-			TupleTableSlot *slot;
-			ExprContext *econtext;
-
-			slot = mtstate->ps.ps_ResultTupleSlot;
-			Assert(slot != NULL);
-			econtext = mtstate->ps.ps_ExprContext;
-			Assert(econtext != NULL);
-
-			resultRelInfo->ri_returningList = rlist;
-			resultRelInfo->ri_projectReturning =
-				ExecBuildProjectionInfo(rlist, econtext, slot, &mtstate->ps,
-										resultRelInfo->ri_RelationDesc->rd_att);
-		}
-
-		/*
-		 * Prepare to generate tuples suitable for the target relation.
-		 */
-		if (operation == CMD_INSERT || operation == CMD_UPDATE)
-		{
-			if (operation == CMD_INSERT)
-			{
-				foreach(l, subplan->targetlist)
-				{
-					TargetEntry *tle = (TargetEntry *) lfirst(l);
-
-					if (!tle->resjunk)
-						resultTargetList = lappend(resultTargetList, tle);
-					else
-						need_projection = true;
-				}
-			}
-			else
-			{
-				resultTargetList = (List *) list_nth(node->updateTargetLists,
-													 i);
-				need_projection = true;
-			}
-
-			/*
-			 * The clean list must produce a tuple suitable for the result
-			 * relation.
-			 */
-			ExecCheckPlanOutput(resultRelInfo->ri_RelationDesc,
-								resultTargetList);
-		}
-
-		if (need_projection)
-		{
-			TupleDesc	relDesc = RelationGetDescr(resultRelInfo->ri_RelationDesc);
-
-			/*
-			 * For UPDATE, we use the old tuple to fill up missing values in
-			 * the tuple produced by the plan to get the new tuple.
-			 */
-			if (operation == CMD_UPDATE)
-				resultRelInfo->ri_oldTupleSlot =
-					table_slot_create(resultRelInfo->ri_RelationDesc,
-									  &mtstate->ps.state->es_tupleTable);
-			resultRelInfo->ri_newTupleSlot =
-				table_slot_create(resultRelInfo->ri_RelationDesc,
-								  &mtstate->ps.state->es_tupleTable);
-
-			/* need an expression context to do the projection */
-			if (mtstate->ps.ps_ExprContext == NULL)
-				ExecAssignExprContext(estate, &mtstate->ps);
-			resultRelInfo->ri_projectNew =
-				ExecBuildProjectionInfo(resultTargetList,
-										mtstate->ps.ps_ExprContext,
-										resultRelInfo->ri_newTupleSlot,
-										&mtstate->ps,
-										relDesc);
-		}
-
-		/*
-		 * For UPDATE/DELETE, find the appropriate junk attr now.
-		 */
-		if (operation == CMD_UPDATE || operation == CMD_DELETE)
-		{
-			char	relkind;
-
-			relkind = resultRelInfo->ri_RelationDesc->rd_rel->relkind;
-			if (relkind == RELKIND_RELATION ||
-				relkind == RELKIND_MATVIEW ||
-				relkind == RELKIND_PARTITIONED_TABLE)
-			{
-				resultRelInfo->ri_junkAttno =
-					ExecFindJunkAttributeInTlist(subplan->targetlist, "ctid");
-				if (!AttributeNumberIsValid(resultRelInfo->ri_junkAttno))
-					elog(ERROR, "could not find junk ctid column");
-			}
-			else if (relkind == RELKIND_FOREIGN_TABLE)
-			{
-				/*
-				 * When there is a row-level trigger, there should be
-				 * a wholerow attribute.
-				 */
-				resultRelInfo->ri_junkAttno =
-					ExecFindJunkAttributeInTlist(subplan->targetlist,
-												 "wholerow");
-				/* HACK: we require it to be present for updates. */
-				if (mtstate->operation == CMD_UPDATE &&
-					!AttributeNumberIsValid(resultRelInfo->ri_junkAttno))
-					elog(ERROR, "could not find junk wholerow column");
-			}
-			else
-			{
-				resultRelInfo->ri_junkAttno =
-					ExecFindJunkAttributeInTlist(subplan->targetlist, "wholerow");
-				if (!AttributeNumberIsValid(resultRelInfo->ri_junkAttno))
-					elog(ERROR, "could not find junk wholerow column");
-			}
-		}
-
-		resultRelInfo++;
-	}
+	if (node->rootResultRelIndex >= 0)
+		mtstate->rootResultRelInfo =
+			ExecGetRootResultRelInfo(mtstate, node->rootResultRelIndex);
+	if (mtstate->rootResultRelInfo == NULL)
+		mtstate->resultRelInfo =
+			ExecGetResultRelInfo(mtstate, node->resultRelIndex, true);
 
 	/* Get the target relation */
 	rel = (getTargetResultRelInfo(mtstate))->ri_RelationDesc;
 
 	/*
-	 * If it's not a partitioned table after all, UPDATE tuple routing should
-	 * not be attempted.
-	 */
-	if (rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
-		update_tuple_routing_needed = false;
-
-	/*
-	 * Build state for tuple routing if it's an INSERT or if it's an UPDATE of
-	 * partition key.
+	 * Build state for tuple routing if it's an INSERT.  If an UPDATE might
+	 * need it, ExecBuildResultRelInfo will build it when initializing
+	 * a partition's ResultRelInfo.
 	 */
 	if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
-		(operation == CMD_INSERT || update_tuple_routing_needed))
+		operation == CMD_INSERT)
 		mtstate->mt_partition_tuple_routing =
 			ExecSetupPartitionTupleRouting(estate, mtstate, rel);
 
@@ -2780,83 +2860,6 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		ExecSetupTransitionCaptureState(mtstate, estate);
 
 	/*
-	 * Construct mapping from each of the per-subplan partition attnos to the
-	 * root attno.  This is required when during update row movement the tuple
-	 * descriptor of a source partition does not match the root partitioned
-	 * table descriptor.  In such a case we need to convert tuples to the root
-	 * tuple descriptor, because the search for destination partition starts
-	 * from the root.  We'll also need a slot to store these converted tuples.
-	 * We can skip this setup if it's not a partition key update.
-	 */
-	if (update_tuple_routing_needed)
-	{
-		ExecSetupChildParentMapForSubplan(mtstate);
-		mtstate->mt_root_tuple_slot = table_slot_create(rel, NULL);
-	}
-
-	/* Set the list of arbiter indexes if needed for ON CONFLICT */
-	resultRelInfo = mtstate->resultRelInfo;
-	if (node->onConflictAction != ONCONFLICT_NONE)
-		resultRelInfo->ri_onConflictArbiterIndexes = node->arbiterIndexes;
-
-	/*
-	 * If needed, Initialize target list, projection and qual for ON CONFLICT
-	 * DO UPDATE.
-	 */
-	if (node->onConflictAction == ONCONFLICT_UPDATE)
-	{
-		ExprContext *econtext;
-		TupleDesc	relationDesc;
-		TupleDesc	tupDesc;
-
-		/* insert may only have one relation, inheritance is not expanded */
-		Assert(nrels == 1);
-
-		/* already exists if created by RETURNING processing above */
-		if (mtstate->ps.ps_ExprContext == NULL)
-			ExecAssignExprContext(estate, &mtstate->ps);
-
-		econtext = mtstate->ps.ps_ExprContext;
-		relationDesc = resultRelInfo->ri_RelationDesc->rd_att;
-
-		/* create state for DO UPDATE SET operation */
-		resultRelInfo->ri_onConflict = makeNode(OnConflictSetState);
-
-		/* initialize slot for the existing tuple */
-		resultRelInfo->ri_onConflict->oc_Existing =
-			table_slot_create(resultRelInfo->ri_RelationDesc,
-							  &mtstate->ps.state->es_tupleTable);
-
-		/*
-		 * Create the tuple slot for the UPDATE SET projection. We want a slot
-		 * of the table's type here, because the slot will be used to insert
-		 * into the table, and for RETURNING processing - which may access
-		 * system attributes.
-		 */
-		tupDesc = ExecTypeFromTL((List *) node->onConflictSet);
-		resultRelInfo->ri_onConflict->oc_ProjSlot =
-			ExecInitExtraTupleSlot(mtstate->ps.state, tupDesc,
-								   table_slot_callbacks(resultRelInfo->ri_RelationDesc));
-
-		/* build UPDATE SET projection state */
-		resultRelInfo->ri_onConflict->oc_ProjInfo =
-			ExecBuildProjectionInfo(node->onConflictSet, econtext,
-									resultRelInfo->ri_onConflict->oc_ProjSlot,
-									&mtstate->ps,
-									relationDesc);
-
-		/* initialize state to evaluate the WHERE clause, if any */
-		if (node->onConflictWhere)
-		{
-			ExprState  *qualexpr;
-
-			qualexpr = ExecInitQual((List *) node->onConflictWhere,
-									&mtstate->ps);
-			resultRelInfo->ri_onConflict->oc_WhereClause = qualexpr;
-		}
-	}
-
-	/*
 	 * If we have any secondary relations in an UPDATE or DELETE, they need to
 	 * be treated like non-locked relations in SELECT FOR UPDATE, ie, the
 	 * EvalPlanQual mechanism needs to be told about them.  Locate the
@@ -2884,13 +2887,6 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 						mtstate->mt_arowmarks[0]);
 
 	/*
-	 * Initialize a hash table to look up UPDATE/DELETE result relations by
-	 * OID if there is more than one.
-	 */
-	if (mtstate->operation != CMD_INSERT && mtstate->mt_nrels > 1)
-		ExecHashSubPlanResultRelsByOid(mtstate);
-
-	/*
 	 * Lastly, if this is not the primary (canSetTag) ModifyTable node, add it
 	 * to estate->es_auxmodifytables so that it will be run to completion by
 	 * ExecPostprocessPlan.  (It'd actually work fine to add the primary
@@ -2920,20 +2916,6 @@ ExecEndModifyTable(ModifyTableState *node)
 	int			i;
 
 	/*
-	 * Allow any FDWs to shut down
-	 */
-	for (i = 0; i < node->mt_nrels; i++)
-	{
-		ResultRelInfo *resultRelInfo = node->resultRelInfo + i;
-
-		if (!resultRelInfo->ri_usesFdwDirectModify &&
-			resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignModify != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignModify(node->ps.state,
-														   resultRelInfo);
-	}
-
-	/*
 	 * Close all the partitioned tables, leaf partitions, and their indices
 	 * and release the slot used for tuple routing, if set.
 	 */
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index a752a12..42c4fc3 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -213,7 +213,7 @@ create_estate_for_relation(LogicalRepRelMapEntry *rel)
 	resultRelInfo = makeNode(ResultRelInfo);
 	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, 0);
 
-	estate->es_result_relations = resultRelInfo;
+	estate->es_result_relations = &resultRelInfo;
 	estate->es_num_result_relations = 1;
 	estate->es_result_relation_info = resultRelInfo;
 
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index a40ddf5..d1f436c 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -45,10 +45,6 @@ typedef struct TriggerData
 /*
  * The state for capturing old and new tuples into transition tables for a
  * single ModifyTable node (or other operation source, e.g. copy.c).
- *
- * This is per-caller to avoid conflicts in setting tcs_map or
- * tcs_original_insert_tuple.  Note, however, that the pointed-to
- * private data may be shared across multiple callers.
  */
 struct AfterTriggersTableData;	/* private in trigger.c */
 
@@ -66,14 +62,6 @@ typedef struct TransitionCaptureState
 	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.  The caller must store the conversion map here if so.
-	 */
-	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
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 1f4efd6..317e739 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -606,6 +606,10 @@ extern void CheckSubscriptionRelkind(char relkind, const char *nspname,
 /* prototypes from functions in nodeModifyTable.c */
 extern ResultRelInfo *ExecLookupModifyResultRelByOid(ModifyTableState *mtstate,
 							   Oid reloid, int *whichrel);
+extern ResultRelInfo *ExecGetResultRelInfo(ModifyTableState *mtstate, int resultRelIndex,
+					 bool create_it);
+extern ResultRelInfo *ExecGetRootResultRelInfo(ModifyTableState *mtstate, int rootRelIndex);
+
 /* needed by trigger.c */
 extern TupleTableSlot *ExecGetUpdateNewTuple(ResultRelInfo *relinfo,
 						  TupleTableSlot *planSlot,
diff --git a/src/include/executor/nodeModifyTable.h b/src/include/executor/nodeModifyTable.h
index 4ec4ebd..a30f5cf 100644
--- a/src/include/executor/nodeModifyTable.h
+++ b/src/include/executor/nodeModifyTable.h
@@ -20,5 +20,4 @@ extern void ExecComputeStoredGenerated(EState *estate, TupleTableSlot *slot, Cmd
 extern ModifyTableState *ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags);
 extern void ExecEndModifyTable(ModifyTableState *node);
 extern void ExecReScanModifyTable(ModifyTableState *node);
-
 #endif							/* NODEMODIFYTABLE_H */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 88ac0b2..a1d2b2b 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -494,6 +494,12 @@ typedef struct ResultRelInfo
 	TupleTableSlot *ri_oldTupleSlot;
 	TupleTableSlot *ri_newTupleSlot;
 	ProjectionInfo *ri_projectNew;
+
+	/*
+	 * Map to convert child sublan tuples to root parent format, set iff
+	 * either update row movement or transition tuple capture is active.
+	 */
+	TupleConversionMap *ri_childToRootMap;
 } ResultRelInfo;
 
 /* ----------------
@@ -525,7 +531,7 @@ typedef struct EState
 	CommandId	es_output_cid;
 
 	/* Info about target table(s) for insert/update/delete queries: */
-	ResultRelInfo *es_result_relations; /* array of ResultRelInfos */
+	ResultRelInfo **es_result_relations; /* array of ResultRelInfo pointers */
 	int			es_num_result_relations;	/* length of array */
 	ResultRelInfo *es_result_relation_info; /* currently active array elt */
 
@@ -535,7 +541,8 @@ typedef struct EState
 	 * es_result_relations, but we need access to the roots for firing
 	 * triggers and for runtime tuple routing.
 	 */
-	ResultRelInfo *es_root_result_relations;	/* array of ResultRelInfos */
+	ResultRelInfo **es_root_result_relations;	/* array of ResultRelInfo
+												 * pointers */
 	int			es_num_root_result_relations;	/* length of the array */
 	PartitionDirectory es_partition_directory;	/* for PartitionDesc lookup */
 
@@ -1173,7 +1180,7 @@ typedef struct ModifyTableState
 	int			mt_nplans;		/* number of plans in mt_plans (only 1!) */
 	int			mt_whichplan;	/* which one is being executed (always 0th!) */
 	int			mt_nrels;		/* number of result rels in the arrays */
-	int			mt_whichrel;	/* Array index of target rel being targeted */
+	TupleDesc	mt_plan_tupdesc;	/* TupleDesc of plan's output */
 	TupleTableSlot **mt_scans;  /* input tuple for each result relation */
 
 	/*
@@ -1182,7 +1189,7 @@ typedef struct ModifyTableState
 	 */
 	int			mt_tableOidAttno;
 
-	ResultRelInfo *resultRelInfo;	/* Target relations */
+	ResultRelInfo *resultRelInfo;	/* Target relation */
 	HTAB	   *mt_subplan_resultrel_hash;	/* hash table to look up result
 											 * relation by OID. */
 	ResultRelInfo *rootResultRelInfo;	/* root target relation (partitioned
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index a295c08..ea3200a 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -1837,6 +1837,53 @@ explain (analyze, costs off, summary off, timing off) execute ab_q3 (2, 2);
          Filter: ((b >= $1) AND (b <= $2) AND (a < $0))
 (10 rows)
 
+-- Runtime pruning for UPDATE/DELETE (mainly notice result relations listed)
+prepare upd_q (int, int) as
+update ab set a = a where a = $1 and b = $2;
+-- PARAM_EXTERN
+explain (analyze, costs off, summary off, timing off) execute upd_q (1, 1);
+                          QUERY PLAN                           
+---------------------------------------------------------------
+ Update on ab (actual rows=0 loops=1)
+   Update on ab_a1_b1 ab_1
+   ->  Append (actual rows=0 loops=1)
+         Subplans Removed: 8
+         ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
+               Filter: ((a = $1) AND (b = $2))
+(6 rows)
+
+-- PARAM_EXEC
+explain (analyze, costs off, summary off, timing off)
+update ab set a = a where a = (select 1) and b = (select 1);
+                          QUERY PLAN                           
+---------------------------------------------------------------
+ Update on ab (actual rows=0 loops=1)
+   Update on ab_a1_b1 ab_1
+   InitPlan 1 (returns $0)
+     ->  Result (actual rows=1 loops=1)
+   InitPlan 2 (returns $1)
+     ->  Result (actual rows=1 loops=1)
+   ->  Append (actual rows=0 loops=1)
+         ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
+               Filter: ((a = $0) AND (b = $1))
+         ->  Seq Scan on ab_a1_b2 ab_2 (never executed)
+               Filter: ((a = $0) AND (b = $1))
+         ->  Seq Scan on ab_a1_b3 ab_3 (never executed)
+               Filter: ((a = $0) AND (b = $1))
+         ->  Seq Scan on ab_a2_b1 ab_4 (never executed)
+               Filter: ((a = $0) AND (b = $1))
+         ->  Seq Scan on ab_a2_b2 ab_5 (never executed)
+               Filter: ((a = $0) AND (b = $1))
+         ->  Seq Scan on ab_a2_b3 ab_6 (never executed)
+               Filter: ((a = $0) AND (b = $1))
+         ->  Seq Scan on ab_a3_b1 ab_7 (never executed)
+               Filter: ((a = $0) AND (b = $1))
+         ->  Seq Scan on ab_a3_b2 ab_8 (never executed)
+               Filter: ((a = $0) AND (b = $1))
+         ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
+               Filter: ((a = $0) AND (b = $1))
+(25 rows)
+
 -- Test a backwards Append scan
 create table list_part (a int) partition by list (a);
 create table list_part1 partition of list_part for values in (1);
diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql
index 6658455..e7b617d 100644
--- a/src/test/regress/sql/partition_prune.sql
+++ b/src/test/regress/sql/partition_prune.sql
@@ -389,6 +389,18 @@ select a from ab where b between $1 and $2 and a < (select 3);
 
 explain (analyze, costs off, summary off, timing off) execute ab_q3 (2, 2);
 
+-- Runtime pruning for UPDATE/DELETE (mainly notice result relations listed)
+
+prepare upd_q (int, int) as
+update ab set a = a where a = $1 and b = $2;
+
+-- PARAM_EXTERN
+explain (analyze, costs off, summary off, timing off) execute upd_q (1, 1);
+
+-- PARAM_EXEC
+explain (analyze, costs off, summary off, timing off)
+update ab set a = a where a = (select 1) and b = (select 1);
+
 -- Test a backwards Append scan
 create table list_part (a int) partition by list (a);
 create table list_part1 partition of list_part for values in (1);
-- 
1.8.3.1

