diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 16c2979f2d..5f6d0fa1bf 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -1879,7 +1879,7 @@ postgresBeginForeignModify(ModifyTableState *mtstate,
 									rte,
 									resultRelInfo,
 									mtstate->operation,
-									outerPlanState(mtstate)->plan,
+									outerPlan(mtstate->ps.plan),
 									query,
 									target_attrs,
 									values_end_len,
@@ -1983,7 +1983,7 @@ postgresGetForeignModifyBatchSize(ResultRelInfo *resultRelInfo)
 		batch_size = get_batch_size_option(resultRelInfo->ri_RelationDesc);
 
 	/* Disable batching when we have to use RETURNING. */
-	if (resultRelInfo->ri_projectReturning != NULL ||
+	if (resultRelInfo->ri_returningList != NULL ||
 		(resultRelInfo->ri_TrigDesc &&
 		 resultRelInfo->ri_TrigDesc->trig_insert_after_row))
 		return 1;
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index be2e3d7354..20e7d57d41 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -697,7 +697,7 @@ CopyFrom(CopyFromState cstate)
 	 * CopyFrom tuple routing.
 	 */
 	if (cstate->rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
-		proute = ExecSetupPartitionTupleRouting(estate, NULL, cstate->rel);
+		proute = ExecSetupPartitionTupleRouting(estate, cstate->rel);
 
 	if (cstate->whereClause)
 		cstate->qualexpr = ExecInitQual(castNode(List, cstate->whereClause),
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index a5ceb1698c..3421014e47 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -5479,7 +5479,7 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
 	if (row_trigger && transition_capture != NULL)
 	{
 		TupleTableSlot *original_insert_tuple = transition_capture->tcs_original_insert_tuple;
-		TupleConversionMap *map = relinfo->ri_ChildToRootMap;
+		TupleConversionMap *map = ExecGetChildToRootMap(relinfo);
 		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 163242f54e..bd457254d0 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1221,6 +1221,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 	resultRelInfo->ri_projectNew = NULL;
 	resultRelInfo->ri_newTupleSlot = NULL;
 	resultRelInfo->ri_oldTupleSlot = NULL;
+	resultRelInfo->ri_projectNewInfoValid = false;
 	resultRelInfo->ri_FdwState = NULL;
 	resultRelInfo->ri_usesFdwDirectModify = false;
 	resultRelInfo->ri_ConstraintExprs = NULL;
@@ -1231,11 +1232,17 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 	resultRelInfo->ri_ReturningSlot = NULL;
 	resultRelInfo->ri_TrigOldSlot = NULL;
 	resultRelInfo->ri_TrigNewSlot = NULL;
+	/*
+	 * Only ExecInitPartitionInfo() passes partition_root_rri.  For child
+	 * relations that are not tuple routing target relations, this is set in
+	 * ExecInitModifyTable().
+	 */
 	resultRelInfo->ri_RootResultRelInfo = partition_root_rri;
 	resultRelInfo->ri_RootToPartitionMap = NULL;	/* set by
 													 * ExecInitRoutingInfo */
 	resultRelInfo->ri_PartitionTupleSlot = NULL;	/* ditto */
 	resultRelInfo->ri_ChildToRootMap = NULL;
+	resultRelInfo->ri_ChildToRootMapValid = false;
 	resultRelInfo->ri_CopyMultiInsertBuffer = NULL;
 }
 
@@ -1923,7 +1930,8 @@ ExecConstraints(ResultRelInfo *resultRelInfo,
  */
 void
 ExecWithCheckOptions(WCOKind kind, ResultRelInfo *resultRelInfo,
-					 TupleTableSlot *slot, EState *estate)
+					 TupleTableSlot *slot, EState *estate,
+					 PlanState *parent)
 {
 	Relation	rel = resultRelInfo->ri_RelationDesc;
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -1931,6 +1939,22 @@ ExecWithCheckOptions(WCOKind kind, ResultRelInfo *resultRelInfo,
 	ListCell   *l1,
 			   *l2;
 
+	/* Initialize the quals if not already done. */
+	if (resultRelInfo->ri_WithCheckOptionExprs == NIL)
+	{
+		List   *wcoExprs = NIL;
+
+		foreach(l1, resultRelInfo->ri_WithCheckOptions)
+		{
+			WithCheckOption *wco = (WithCheckOption *) lfirst(l1);
+			ExprState  *wcoExpr = ExecInitQual((List *) wco->qual,
+											   parent);
+
+			wcoExprs = lappend(wcoExprs, wcoExpr);
+		}
+		resultRelInfo->ri_WithCheckOptionExprs = wcoExprs;
+	}
+
 	/*
 	 * We will use the EState's per-tuple context for evaluating constraint
 	 * expressions (creating it if it's not already there).
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 558060e080..ead630a367 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -68,9 +68,15 @@
  *		Array of 'max_partitions' elements containing a pointer to a
  *		ResultRelInfo for every leaf partitions touched by tuple routing.
  *		Some of these are pointers to ResultRelInfos which are borrowed out of
- *		'subplan_resultrel_htab'.  The remainder have been built especially
- *		for tuple routing.  See comment for PartitionDispatchData->indexes for
- *		details on how this array is indexed.
+ *		the owning ModifyTableState node.  The remainder have been built
+ *		especially for tuple routing.  See comment for
+ *		PartitionDispatchData->indexes for details on how this array is
+ *		indexed.
+ *
+ * is_update_rel
+ * 		Array of 'max_partitions' booleans recording whether a given entry
+ * 		in 'partitions' is a ResultRelInfo pointer borrowed from a matching
+ * 		UPDATE result relation in the owning ModifyTableState node
  *
  * num_partitions
  *		The current number of items stored in the 'partitions' array.  Also
@@ -80,12 +86,6 @@
  * max_partitions
  *		The current allocated size of the 'partitions' array.
  *
- * subplan_resultrel_htab
- *		Hash table to store subplan ResultRelInfos by Oid.  This is used to
- *		cache ResultRelInfos from targets of an UPDATE ModifyTable node;
- *		NULL in other cases.  Some of these may be useful for tuple routing
- *		to save having to build duplicates.
- *
  * memcxt
  *		Memory context used to allocate subsidiary structs.
  *-----------------------
@@ -98,9 +98,9 @@ struct PartitionTupleRouting
 	int			num_dispatch;
 	int			max_dispatch;
 	ResultRelInfo **partitions;
+	bool	   *is_update_rel;
 	int			num_partitions;
 	int			max_partitions;
-	HTAB	   *subplan_resultrel_htab;
 	MemoryContext memcxt;
 };
 
@@ -153,16 +153,7 @@ typedef struct PartitionDispatchData
 	int			indexes[FLEXIBLE_ARRAY_MEMBER];
 }			PartitionDispatchData;
 
-/* struct to hold result relations coming from UPDATE subplans */
-typedef struct SubplanResultRelHashElem
-{
-	Oid			relid;			/* hash key -- must be first */
-	ResultRelInfo *rri;
-} SubplanResultRelHashElem;
-
 
-static void ExecHashSubPlanResultRelsByOid(ModifyTableState *mtstate,
-										   PartitionTupleRouting *proute);
 static ResultRelInfo *ExecInitPartitionInfo(ModifyTableState *mtstate,
 											EState *estate, PartitionTupleRouting *proute,
 											PartitionDispatch dispatch,
@@ -173,7 +164,7 @@ static void ExecInitRoutingInfo(ModifyTableState *mtstate,
 								PartitionTupleRouting *proute,
 								PartitionDispatch dispatch,
 								ResultRelInfo *partRelInfo,
-								int partidx);
+								int partidx, bool is_update_rel);
 static PartitionDispatch ExecInitPartitionDispatchInfo(EState *estate,
 													   PartitionTupleRouting *proute,
 													   Oid partoid, PartitionDispatch parent_pd,
@@ -215,11 +206,9 @@ static void find_matching_subplans_recurse(PartitionPruningData *prunedata,
  * it should be estate->es_query_cxt.
  */
 PartitionTupleRouting *
-ExecSetupPartitionTupleRouting(EState *estate, ModifyTableState *mtstate,
-							   Relation rel)
+ExecSetupPartitionTupleRouting(EState *estate, Relation rel)
 {
 	PartitionTupleRouting *proute;
-	ModifyTable *node = mtstate ? (ModifyTable *) mtstate->ps.plan : NULL;
 
 	/*
 	 * Here we attempt to expend as little effort as possible in setting up
@@ -241,17 +230,6 @@ ExecSetupPartitionTupleRouting(EState *estate, ModifyTableState *mtstate,
 	ExecInitPartitionDispatchInfo(estate, proute, RelationGetRelid(rel),
 								  NULL, 0, NULL);
 
-	/*
-	 * If performing an UPDATE with tuple routing, we can reuse partition
-	 * sub-plan result rels.  We build a hash table to map the OIDs of
-	 * partitions present in mtstate->resultRelInfo to their ResultRelInfos.
-	 * Every time a tuple is routed to a partition that we've yet to set the
-	 * ResultRelInfo for, before we go to the trouble of making one, we check
-	 * for a pre-made one in the hash table.
-	 */
-	if (node && node->operation == CMD_UPDATE)
-		ExecHashSubPlanResultRelsByOid(mtstate, proute);
-
 	return proute;
 }
 
@@ -351,7 +329,6 @@ ExecFindPartition(ModifyTableState *mtstate,
 		is_leaf = partdesc->is_leaf[partidx];
 		if (is_leaf)
 		{
-
 			/*
 			 * We've reached the leaf -- hurray, we're done.  Look to see if
 			 * we've already got a ResultRelInfo for this partition.
@@ -368,20 +345,18 @@ ExecFindPartition(ModifyTableState *mtstate,
 
 				/*
 				 * We have not yet set up a ResultRelInfo for this partition,
-				 * but if we have a subplan hash table, we might have one
-				 * there.  If not, we'll have to create one.
+				 * but if the partition is also an UPDATE result relation, use
+				 * the one in mtstate->resultRelInfo instead of creating a new
+				 * one with ExecInitPartitionInfo().
 				 */
-				if (proute->subplan_resultrel_htab)
+				if (mtstate->operation == CMD_UPDATE && mtstate->ps.plan)
 				{
 					Oid			partoid = partdesc->oids[partidx];
-					SubplanResultRelHashElem *elem;
 
-					elem = hash_search(proute->subplan_resultrel_htab,
-									   &partoid, HASH_FIND, NULL);
-					if (elem)
+					rri = ExecLookupResultRelByOid(mtstate, partoid, true);
+					if (rri)
 					{
 						found = true;
-						rri = elem->rri;
 
 						/* Verify this ResultRelInfo allows INSERTs */
 						CheckValidResultRel(rri, CMD_INSERT);
@@ -391,7 +366,7 @@ ExecFindPartition(ModifyTableState *mtstate,
 						 * subsequent tuples routed to this partition.
 						 */
 						ExecInitRoutingInfo(mtstate, estate, proute, dispatch,
-											rri, partidx);
+											rri, partidx, true);
 					}
 				}
 
@@ -509,50 +484,6 @@ ExecFindPartition(ModifyTableState *mtstate,
 	return rri;
 }
 
-/*
- * ExecHashSubPlanResultRelsByOid
- *		Build a hash table to allow fast lookups of subplan ResultRelInfos by
- *		partition Oid.  We also populate the subplan ResultRelInfo with an
- *		ri_PartitionRoot.
- */
-static void
-ExecHashSubPlanResultRelsByOid(ModifyTableState *mtstate,
-							   PartitionTupleRouting *proute)
-{
-	HASHCTL		ctl;
-	HTAB	   *htab;
-	int			i;
-
-	ctl.keysize = sizeof(Oid);
-	ctl.entrysize = sizeof(SubplanResultRelHashElem);
-	ctl.hcxt = CurrentMemoryContext;
-
-	htab = hash_create("PartitionTupleRouting table", mtstate->mt_nrels,
-					   &ctl, HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
-	proute->subplan_resultrel_htab = htab;
-
-	/* Hash all subplans by their Oid */
-	for (i = 0; i < mtstate->mt_nrels; i++)
-	{
-		ResultRelInfo *rri = &mtstate->resultRelInfo[i];
-		bool		found;
-		Oid			partoid = RelationGetRelid(rri->ri_RelationDesc);
-		SubplanResultRelHashElem *elem;
-
-		elem = (SubplanResultRelHashElem *)
-			hash_search(htab, &partoid, HASH_ENTER, &found);
-		Assert(!found);
-		elem->rri = rri;
-
-		/*
-		 * This is required in order to convert the partition's tuple to be
-		 * compatible with the root partitioned table's tuple descriptor. When
-		 * generating the per-subplan result rels, this was not set.
-		 */
-		rri->ri_RootResultRelInfo = mtstate->rootResultRelInfo;
-	}
-}
-
 /*
  * ExecInitPartitionInfo
  *		Lock the partition and initialize ResultRelInfo.  Also setup other
@@ -613,7 +544,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
 	 * didn't build the withCheckOptionList for partitions within the planner,
 	 * but simple translation of varattnos will suffice.  This only occurs for
 	 * the INSERT case or in the case of UPDATE tuple routing where we didn't
-	 * find a result rel to reuse in ExecSetupPartitionTupleRouting().
+	 * find a result rel to reuse.
 	 */
 	if (node && node->withCheckOptionLists != NIL)
 	{
@@ -680,8 +611,6 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
 	 */
 	if (node && node->returningLists != NIL)
 	{
-		TupleTableSlot *slot;
-		ExprContext *econtext;
 		List	   *returningList;
 
 		/* See the comment above for WCO lists. */
@@ -716,25 +645,11 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
 		/* We ignore the value of found_whole_row. */
 
 		leaf_part_rri->ri_returningList = returningList;
-
-		/*
-		 * Initialize the projection itself.
-		 *
-		 * Use the slot and the expression context that would have been set up
-		 * in ExecInitModifyTable() for projection's output.
-		 */
-		Assert(mtstate->ps.ps_ResultTupleSlot != NULL);
-		slot = mtstate->ps.ps_ResultTupleSlot;
-		Assert(mtstate->ps.ps_ExprContext != NULL);
-		econtext = mtstate->ps.ps_ExprContext;
-		leaf_part_rri->ri_projectReturning =
-			ExecBuildProjectionInfo(returningList, econtext, slot,
-									&mtstate->ps, RelationGetDescr(partrel));
 	}
 
 	/* Set up information needed for routing tuples to the partition. */
 	ExecInitRoutingInfo(mtstate, estate, proute, dispatch,
-						leaf_part_rri, partidx);
+						leaf_part_rri, partidx, false);
 
 	/*
 	 * If there is an ON CONFLICT clause, initialize state for it.
@@ -910,15 +825,6 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
 		}
 	}
 
-	/*
-	 * Also, if transition capture is required, store a map to convert tuples
-	 * from partition's rowtype to the root partition table's.
-	 */
-	if (mtstate->mt_transition_capture || mtstate->mt_oc_transition_capture)
-		leaf_part_rri->ri_ChildToRootMap =
-			convert_tuples_by_name(RelationGetDescr(leaf_part_rri->ri_RelationDesc),
-								   RelationGetDescr(rootResultRelInfo->ri_RelationDesc));
-
 	/*
 	 * Since we've just initialized this ResultRelInfo, it's not in any list
 	 * attached to the estate as yet.  Add it, so that it can be found later.
@@ -949,7 +855,7 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 					PartitionTupleRouting *proute,
 					PartitionDispatch dispatch,
 					ResultRelInfo *partRelInfo,
-					int partidx)
+					int partidx, bool is_update_rel)
 {
 	ResultRelInfo *rootRelInfo = partRelInfo->ri_RootResultRelInfo;
 	MemoryContext oldcxt;
@@ -1029,6 +935,8 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 			proute->max_partitions = 8;
 			proute->partitions = (ResultRelInfo **)
 				palloc(sizeof(ResultRelInfo *) * proute->max_partitions);
+			proute->is_update_rel = (bool *)
+				palloc(sizeof(bool) * proute->max_partitions);
 		}
 		else
 		{
@@ -1036,10 +944,14 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 			proute->partitions = (ResultRelInfo **)
 				repalloc(proute->partitions, sizeof(ResultRelInfo *) *
 						 proute->max_partitions);
+			proute->is_update_rel = (bool *)
+				repalloc(proute->is_update_rel, sizeof(bool) *
+						 proute->max_partitions);
 		}
 	}
 
 	proute->partitions[rri_index] = partRelInfo;
+	proute->is_update_rel[rri_index] = is_update_rel;
 	dispatch->indexes[partidx] = rri_index;
 
 	MemoryContextSwitchTo(oldcxt);
@@ -1199,7 +1111,6 @@ void
 ExecCleanupTupleRouting(ModifyTableState *mtstate,
 						PartitionTupleRouting *proute)
 {
-	HTAB	   *htab = proute->subplan_resultrel_htab;
 	int			i;
 
 	/*
@@ -1230,20 +1141,11 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 														   resultRelInfo);
 
 		/*
-		 * Check if this result rel is one belonging to the node's subplans,
-		 * if so, let ExecEndPlan() clean it up.
+		 * Close it if not one of the result relations borrowed from the owning
+		 * ModifyTableState, because those are closed by ExecEndPlan().
 		 */
-		if (htab)
-		{
-			Oid			partoid;
-			bool		found;
-
-			partoid = RelationGetRelid(resultRelInfo->ri_RelationDesc);
-
-			(void) hash_search(htab, &partoid, HASH_FIND, &found);
-			if (found)
-				continue;
-		}
+		if (proute->is_update_rel[i])
+			continue;
 
 		ExecCloseIndices(resultRelInfo);
 		table_close(resultRelInfo->ri_RelationDesc, NoLock);
diff --git a/src/backend/executor/execUtils.c b/src/backend/executor/execUtils.c
index 42632cb4d8..dbaef76448 100644
--- a/src/backend/executor/execUtils.c
+++ b/src/backend/executor/execUtils.c
@@ -1323,3 +1323,26 @@ ExecGetAllUpdatedCols(ResultRelInfo *relinfo, EState *estate)
 	return bms_union(ExecGetUpdatedCols(relinfo, estate),
 					 ExecGetExtraUpdatedCols(relinfo, estate));
 }
+
+/*
+ * Returns the map needed to convert given child relation's tuples to the
+ * query's main target ("root") relation's format, possibly initializing it
+ * if not already done.
+ */
+TupleConversionMap *
+ExecGetChildToRootMap(ResultRelInfo *resultRelInfo)
+{
+	if (!resultRelInfo->ri_ChildToRootMapValid &&
+		resultRelInfo->ri_RootResultRelInfo)
+	{
+		ResultRelInfo *targetRelInfo;
+
+		targetRelInfo = resultRelInfo->ri_RootResultRelInfo;
+		resultRelInfo->ri_ChildToRootMap =
+			convert_tuples_by_name(RelationGetDescr(resultRelInfo->ri_RelationDesc),
+								   RelationGetDescr(targetRelInfo->ri_RelationDesc));
+		resultRelInfo->ri_ChildToRootMapValid = true;
+	}
+
+	return resultRelInfo->ri_ChildToRootMap;
+}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index bf65785e64..6d47344f3d 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -154,12 +154,38 @@ ExecCheckPlanOutput(Relation resultRel, List *targetList)
 				 errdetail("Query has too few columns.")));
 }
 
+/* Initializes the RETURNING projection for given result relation. */
+static void
+ExecInitReturningProjection(ResultRelInfo *resultRelInfo,
+							ModifyTableState *mtstate)
+{
+	TupleTableSlot *slot;
+	ExprContext *econtext;
+
+	/* should not get called twice */
+	Assert(resultRelInfo->ri_projectReturning == NULL);
+
+
+	/* ExecInitModifyTable() should've initialized these. */
+	Assert(mtstate->ps.ps_ExprContext != NULL);
+	econtext = mtstate->ps.ps_ExprContext;
+	Assert(mtstate->ps.ps_ResultTupleSlot != NULL);
+	slot = mtstate->ps.ps_ResultTupleSlot;
+
+	Assert(resultRelInfo->ri_returningList != NIL);
+	resultRelInfo->ri_projectReturning =
+		ExecBuildProjectionInfo(resultRelInfo->ri_returningList,
+								econtext, slot, &mtstate->ps,
+								resultRelInfo->ri_RelationDesc->rd_att);
+}
+
 /*
  * ExecProcessReturning --- evaluate a RETURNING list
  *
  * resultRelInfo: current result rel
  * tupleSlot: slot holding tuple actually inserted/updated/deleted
  * planSlot: slot holding tuple returned by top subplan node
+ * mtstate: query's plan state
  *
  * Note: If tupleSlot is NULL, the FDW should have already provided econtext's
  * scan tuple.
@@ -169,10 +195,18 @@ ExecCheckPlanOutput(Relation resultRel, List *targetList)
 static TupleTableSlot *
 ExecProcessReturning(ResultRelInfo *resultRelInfo,
 					 TupleTableSlot *tupleSlot,
-					 TupleTableSlot *planSlot)
+					 TupleTableSlot *planSlot,
+					 ModifyTableState *mtstate)
 {
-	ProjectionInfo *projectReturning = resultRelInfo->ri_projectReturning;
-	ExprContext *econtext = projectReturning->pi_exprContext;
+	ProjectionInfo *projectReturning;
+	ExprContext *econtext;
+
+	/* Initialize the projection if not already done. */
+	if (resultRelInfo->ri_projectReturning == NULL)
+		ExecInitReturningProjection(resultRelInfo, mtstate);
+
+	projectReturning = resultRelInfo->ri_projectReturning;
+	econtext = projectReturning->pi_exprContext;
 
 	/* Make tuple and any needed join variables available to ExecProject */
 	if (tupleSlot)
@@ -371,6 +405,110 @@ ExecComputeStoredGenerated(ResultRelInfo *resultRelInfo,
 	MemoryContextSwitchTo(oldContext);
 }
 
+/*
+ * Initialize projection to create tuples suitable for the given result rel.
+ * INSERT queries may need a projection to filter out junk attrs in the
+ * tlist.  UPDATE always needs a projection, because (1) there's always
+ * some junk attrs, and (2) we may need to merge values of not-updated
+ * columns from the old tuple into the final tuple.  In UPDATE, the tuple
+ * arriving from the subplan contains only new values for the changed
+ * columns, plus row identity info in the junk attrs.
+ *
+ * This is also a convenient place to verify that the output of an INSERT or
+ * UPDATE matches the target table.
+ */
+static inline void
+ExecInitNewTupleProjection(ResultRelInfo *resultRelInfo,
+						   ModifyTableState *mtstate,
+						   int whichrel)
+{
+	EState	   *estate = mtstate->ps.state;
+	ModifyTable *node = (ModifyTable *) mtstate->ps.plan;
+	Plan		*subplan = outerPlan(node);
+	CmdType		operation = mtstate->operation;
+	ListCell   *l;
+
+	Assert(!resultRelInfo->ri_projectNewInfoValid);
+
+	if (operation == CMD_INSERT)
+	{
+		List	   *insertTargetList = NIL;
+		bool		need_projection = false;
+
+		foreach(l, subplan->targetlist)
+		{
+			TargetEntry *tle = (TargetEntry *) lfirst(l);
+
+			if (!tle->resjunk)
+				insertTargetList = lappend(insertTargetList, tle);
+			else
+				need_projection = true;
+		}
+
+		/*
+		 * The junk-free list must produce a tuple suitable for the result
+		 * relation.
+		 */
+		ExecCheckPlanOutput(resultRelInfo->ri_RelationDesc, insertTargetList);
+
+		/* We'll need a slot matching the table's format. */
+		resultRelInfo->ri_newTupleSlot =
+			table_slot_create(resultRelInfo->ri_RelationDesc,
+							  &mtstate->ps.state->es_tupleTable);
+
+		/* Build ProjectionInfo if needed (it probably isn't). */
+		if (need_projection)
+		{
+			TupleDesc	relDesc = RelationGetDescr(resultRelInfo->ri_RelationDesc);
+
+			/* need an expression context to do the projection */
+			if (mtstate->ps.ps_ExprContext == NULL)
+				ExecAssignExprContext(estate, &mtstate->ps);
+
+			resultRelInfo->ri_projectNew =
+				ExecBuildProjectionInfo(insertTargetList,
+										mtstate->ps.ps_ExprContext,
+										resultRelInfo->ri_newTupleSlot,
+										&mtstate->ps,
+										relDesc);
+		}
+	}
+	else if (operation == CMD_UPDATE)
+	{
+		List	   *updateColnos;
+		TupleDesc	relDesc = RelationGetDescr(resultRelInfo->ri_RelationDesc);
+
+		Assert(whichrel >= 0 && whichrel < mtstate->mt_nrels);
+		updateColnos = (List *) list_nth(node->updateColnosLists, whichrel);
+
+		/*
+		 * For UPDATE, we use the old tuple to fill up missing values in
+		 * the tuple produced by the plan to get the new tuple.  We need
+		 * two slots, both matching the table's desired format.
+		 */
+		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 =
+			ExecBuildUpdateProjection(subplan->targetlist,
+									  updateColnos,
+									  relDesc,
+									  mtstate->ps.ps_ExprContext,
+									  resultRelInfo->ri_newTupleSlot,
+									  &mtstate->ps);
+	}
+
+	resultRelInfo->ri_projectNewInfoValid = true;
+}
+
 /*
  * ExecGetInsertNewTuple
  *		This prepares a "new" tuple ready to be inserted into given result
@@ -488,6 +626,9 @@ ExecInsert(ModifyTableState *mtstate,
 		resultRelInfo = partRelInfo;
 	}
 
+	if (resultRelInfo->ri_IndexRelationDescs == NULL)
+		ExecOpenIndices(resultRelInfo, onconflict != ONCONFLICT_NONE);
+
 	ExecMaterializeSlot(slot);
 
 	resultRelationDesc = resultRelInfo->ri_RelationDesc;
@@ -627,7 +768,8 @@ ExecInsert(ModifyTableState *mtstate,
 		 * we are looking for at this point.
 		 */
 		if (resultRelInfo->ri_WithCheckOptions != NIL)
-			ExecWithCheckOptions(wco_kind, resultRelInfo, slot, estate);
+			ExecWithCheckOptions(wco_kind, resultRelInfo, slot, estate,
+								 &mtstate->ps);
 
 		/*
 		 * Check the constraints of the tuple.
@@ -641,7 +783,7 @@ ExecInsert(ModifyTableState *mtstate,
 		 * if there's no BR trigger defined on the partition.
 		 */
 		if (resultRelationDesc->rd_rel->relispartition &&
-			(resultRelInfo->ri_RootResultRelInfo == NULL ||
+			(resultRelInfo->ri_RangeTableIndex != 0 ||
 			 (resultRelInfo->ri_TrigDesc &&
 			  resultRelInfo->ri_TrigDesc->trig_insert_before_row)))
 			ExecPartitionCheck(resultRelInfo, slot, estate, true);
@@ -822,11 +964,12 @@ ExecInsert(ModifyTableState *mtstate,
 	 * are looking for at this point.
 	 */
 	if (resultRelInfo->ri_WithCheckOptions != NIL)
-		ExecWithCheckOptions(WCO_VIEW_CHECK, resultRelInfo, slot, estate);
+		ExecWithCheckOptions(WCO_VIEW_CHECK, resultRelInfo, slot, estate,
+							 &mtstate->ps);
 
 	/* Process RETURNING if present */
-	if (resultRelInfo->ri_projectReturning)
-		result = ExecProcessReturning(resultRelInfo, slot, planSlot);
+	if (resultRelInfo->ri_returningList)
+		result = ExecProcessReturning(resultRelInfo, slot, planSlot, mtstate);
 
 	return result;
 }
@@ -882,7 +1025,8 @@ ExecBatchInsert(ModifyTableState *mtstate,
 		 * comment in ExecInsert.
 		 */
 		if (resultRelInfo->ri_WithCheckOptions != NIL)
-			ExecWithCheckOptions(WCO_VIEW_CHECK, resultRelInfo, slot, estate);
+			ExecWithCheckOptions(WCO_VIEW_CHECK, resultRelInfo, slot, estate,
+								 &mtstate->ps);
 	}
 
 	if (canSetTag && numInserted > 0)
@@ -1205,7 +1349,7 @@ ldelete:;
 						 ar_delete_trig_tcs);
 
 	/* Process RETURNING if present and if requested */
-	if (processReturning && resultRelInfo->ri_projectReturning)
+	if (processReturning && resultRelInfo->ri_returningList)
 	{
 		/*
 		 * We have to put the target tuple into a slot, which means first we
@@ -1233,7 +1377,7 @@ ldelete:;
 			}
 		}
 
-		rslot = ExecProcessReturning(resultRelInfo, slot, planSlot);
+		rslot = ExecProcessReturning(resultRelInfo, slot, planSlot, mtstate);
 
 		/*
 		 * Before releasing the target tuple again, make sure rslot has a
@@ -1276,7 +1420,6 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
 						 TupleTableSlot **inserted_tuple)
 {
 	EState	   *estate = mtstate->ps.state;
-	PartitionTupleRouting *proute = mtstate->mt_partition_tuple_routing;
 	TupleConversionMap *tupconv_map;
 	bool		tuple_deleted;
 	TupleTableSlot *epqslot = NULL;
@@ -1295,13 +1438,27 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
 				 errmsg("invalid ON UPDATE specification"),
 				 errdetail("The result tuple would appear in a different partition than the original tuple.")));
 
-	/*
-	 * When an UPDATE is run on a leaf partition, we will not have partition
-	 * tuple routing set up.  In that case, fail with partition constraint
-	 * violation error.
-	 */
-	if (proute == NULL)
-		ExecPartitionCheckEmitError(resultRelInfo, slot, estate);
+	/* Initialize tuple routing info if not already done. */
+	if (mtstate->mt_partition_tuple_routing == NULL)
+	{
+		Relation	targetRel = mtstate->rootResultRelInfo->ri_RelationDesc;
+		MemoryContext	oldcxt;
+
+		/* Things built here have to last for the query duration. */
+		oldcxt = MemoryContextSwitchTo(estate->es_query_cxt);
+
+		mtstate->mt_partition_tuple_routing =
+			ExecSetupPartitionTupleRouting(estate, targetRel);
+
+		/*
+		 * 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.
+		 */
+		Assert(mtstate->mt_root_tuple_slot == NULL);
+		mtstate->mt_root_tuple_slot = table_slot_create(targetRel, NULL);
+		MemoryContextSwitchTo(oldcxt);
+	}
 
 	/*
 	 * Row movement, part 1.  Delete the tuple, but skip RETURNING processing.
@@ -1364,7 +1521,7 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
 	 * convert the tuple into root's tuple descriptor if needed, since
 	 * ExecInsert() starts the search from root.
 	 */
-	tupconv_map = resultRelInfo->ri_ChildToRootMap;
+	tupconv_map = ExecGetChildToRootMap(resultRelInfo);
 	if (tupconv_map != NULL)
 		slot = execute_attr_map_slot(tupconv_map->attrMap,
 									 slot,
@@ -1434,6 +1591,9 @@ ExecUpdate(ModifyTableState *mtstate,
 	if (IsBootstrapProcessingMode())
 		elog(ERROR, "cannot UPDATE during bootstrap");
 
+	if (resultRelInfo->ri_IndexRelationDescs == NULL)
+		ExecOpenIndices(resultRelInfo, false);
+
 	ExecMaterializeSlot(slot);
 
 	/* BEFORE ROW UPDATE Triggers */
@@ -1534,7 +1694,8 @@ lreplace:;
 			 * kind we are looking for at this point.
 			 */
 			ExecWithCheckOptions(WCO_RLS_UPDATE_CHECK,
-								 resultRelInfo, slot, estate);
+								 resultRelInfo, slot, estate,
+								 &mtstate->ps);
 		}
 
 		/*
@@ -1547,6 +1708,13 @@ lreplace:;
 					   *retry_slot;
 			bool		retry;
 
+			/*
+			 * When an UPDATE is run directly on a leaf partition, simply fail
+			 * with partition constraint violation error.
+			 */
+			if (resultRelInfo == mtstate->rootResultRelInfo)
+				ExecPartitionCheckEmitError(resultRelInfo, slot, estate);
+
 			/*
 			 * ExecCrossPartitionUpdate will first DELETE the row from the
 			 * partition it's currently in and then insert it back into the
@@ -1757,11 +1925,12 @@ lreplace:;
 	 * are looking for at this point.
 	 */
 	if (resultRelInfo->ri_WithCheckOptions != NIL)
-		ExecWithCheckOptions(WCO_VIEW_CHECK, resultRelInfo, slot, estate);
+		ExecWithCheckOptions(WCO_VIEW_CHECK, resultRelInfo, slot, estate,
+							 &mtstate->ps);
 
 	/* Process RETURNING if present */
-	if (resultRelInfo->ri_projectReturning)
-		return ExecProcessReturning(resultRelInfo, slot, planSlot);
+	if (resultRelInfo->ri_returningList)
+		return ExecProcessReturning(resultRelInfo, slot, planSlot, mtstate);
 
 	return NULL;
 }
@@ -1955,7 +2124,8 @@ ExecOnConflictUpdate(ModifyTableState *mtstate,
 		 */
 		ExecWithCheckOptions(WCO_RLS_CONFLICT_CHECK, resultRelInfo,
 							 existing,
-							 mtstate->ps.state);
+							 mtstate->ps.state,
+							 &mtstate->ps);
 	}
 
 	/* Project the new tuple version */
@@ -2244,38 +2414,8 @@ ExecModifyTable(PlanState *pstate)
 
 			/* If it's not the same as last time, we need to locate the rel */
 			if (resultoid != node->mt_lastResultOid)
-			{
-				if (node->mt_resultOidHash)
-				{
-					/* Use the pre-built hash table to locate the rel */
-					MTTargetRelLookup *mtlookup;
-
-					mtlookup = (MTTargetRelLookup *)
-						hash_search(node->mt_resultOidHash, &resultoid,
-									HASH_FIND, NULL);
-					if (!mtlookup)
-						elog(ERROR, "incorrect result rel OID %u", resultoid);
-					node->mt_lastResultOid = resultoid;
-					node->mt_lastResultIndex = mtlookup->relationIndex;
-					resultRelInfo = node->resultRelInfo + mtlookup->relationIndex;
-				}
-				else
-				{
-					/* With few target rels, just do a simple search */
-					int			ndx;
-
-					for (ndx = 0; ndx < node->mt_nrels; ndx++)
-					{
-						resultRelInfo = node->resultRelInfo + ndx;
-						if (RelationGetRelid(resultRelInfo->ri_RelationDesc) == resultoid)
-							break;
-					}
-					if (ndx >= node->mt_nrels)
-						elog(ERROR, "incorrect result rel OID %u", resultoid);
-					node->mt_lastResultOid = resultoid;
-					node->mt_lastResultIndex = ndx;
-				}
-			}
+				resultRelInfo = ExecLookupResultRelByOid(node, resultoid,
+														 false);
 		}
 
 		/*
@@ -2292,7 +2432,7 @@ ExecModifyTable(PlanState *pstate)
 			 * ExecProcessReturning by IterateDirectModify, so no need to
 			 * provide it here.
 			 */
-			slot = ExecProcessReturning(resultRelInfo, NULL, planSlot);
+			slot = ExecProcessReturning(resultRelInfo, NULL, planSlot, node);
 
 			return slot;
 		}
@@ -2381,11 +2521,22 @@ ExecModifyTable(PlanState *pstate)
 		switch (operation)
 		{
 			case CMD_INSERT:
+				if (!resultRelInfo->ri_projectNewInfoValid)
+					ExecInitNewTupleProjection(resultRelInfo, node,
+											   node->mt_lastResultIndex);
+				Assert(resultRelInfo->ri_newTupleSlot != NULL);
+
 				slot = ExecGetInsertNewTuple(resultRelInfo, planSlot);
 				slot = ExecInsert(node, resultRelInfo, slot, planSlot,
 								  estate, node->canSetTag);
 				break;
 			case CMD_UPDATE:
+				if (!resultRelInfo->ri_projectNewInfoValid)
+					ExecInitNewTupleProjection(resultRelInfo, node,
+											   node->mt_lastResultIndex);
+				Assert(resultRelInfo->ri_projectNew != NULL &&
+					   resultRelInfo->ri_newTupleSlot != NULL &&
+					   resultRelInfo->ri_oldTupleSlot != NULL);
 
 				/*
 				 * Make the new tuple by combining plan's output tuple with
@@ -2408,6 +2559,7 @@ ExecModifyTable(PlanState *pstate)
 													   oldSlot))
 						elog(ERROR, "failed to fetch tuple being updated");
 				}
+
 				slot = ExecGetUpdateNewTuple(resultRelInfo, planSlot,
 											 oldSlot);
 
@@ -2482,7 +2634,6 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 	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)));
@@ -2542,8 +2693,37 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		ExecSetupTransitionCaptureState(mtstate, estate);
 
 	/*
-	 * Open all the result relations and initialize the ResultRelInfo structs.
-	 * (But root relation was initialized above, if it's part of the array.)
+	 * Initialize result tuple slot and assign its rowtype using the first
+	 * RETURNING list.  We assume the rest will look the same.
+	 */
+	if (node->returningLists)
+	{
+		mtstate->ps.plan->targetlist = (List *) linitial(node->returningLists);
+
+		/* Set up a slot for the output of the RETURNING projection(s) */
+		ExecInitResultTupleSlotTL(&mtstate->ps, &TTSOpsVirtual);
+
+		/* Need an econtext too */
+		if (mtstate->ps.ps_ExprContext == NULL)
+			ExecAssignExprContext(estate, &mtstate->ps);
+	}
+	else
+	{
+		/*
+		 * We still must construct a dummy result tuple type, because InitPlan
+		 * expects one (maybe should change that?).
+		 */
+		mtstate->ps.plan->targetlist = NIL;
+		ExecInitResultTypeTL(&mtstate->ps);
+
+		mtstate->ps.ps_ExprContext = NULL;
+	}
+
+	/*
+	 * Open all the result relations and initialize the ResultRelInfo structs,
+	 * initializing its fields as needed. (But root relation was initialized
+	 * above, if it's part of the array.)
+	 *
 	 * We must do this before initializing the subplan, because direct-modify
 	 * FDWs expect their ResultRelInfos to be available.
 	 */
@@ -2554,8 +2734,18 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		Index		resultRelation = lfirst_int(l);
 
 		if (resultRelInfo != mtstate->rootResultRelInfo)
+		{
 			ExecInitResultRelation(estate, resultRelInfo, resultRelation);
 
+			/*
+			 * For child result relations, store the root result relation
+			 * pointer.  We do so for the convenience of places that want to
+			 * look at the query's original target relation but don't have the
+			 * mtstate handy.
+			 */
+			resultRelInfo->ri_RootResultRelInfo = mtstate->rootResultRelInfo;
+		}
+
 		/* Initialize the usesFdwDirectModify flag */
 		resultRelInfo->ri_usesFdwDirectModify = bms_is_member(i,
 															  node->fdwDirectModifyPlans);
@@ -2565,47 +2755,6 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		 */
 		CheckValidResultRel(resultRelInfo, operation);
 
-		resultRelInfo++;
-		i++;
-	}
-
-	/*
-	 * Now we may initialize the subplan.
-	 */
-	outerPlanState(mtstate) = ExecInitNode(subplan, estate, eflags);
-
-	/*
-	 * Do additional per-result-relation initialization.
-	 */
-	for (i = 0; i < nrels; i++)
-	{
-		resultRelInfo = &mtstate->resultRelInfo[i];
-
-		/*
-		 * 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;
-
 		/* Also let FDWs init themselves for foreign-table result rels */
 		if (!resultRelInfo->ri_usesFdwDirectModify &&
 			resultRelInfo->ri_FdwRoutine != NULL &&
@@ -2621,129 +2770,132 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		}
 
 		/*
-		 * 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.
-		 *
-		 * For INSERT, the map is only initialized for a given partition when
-		 * the partition itself is first initialized by ExecFindPartition().
+		 * Initialize any WITH CHECK OPTION constraints if needed.
 		 */
-		if (update_tuple_routing_needed ||
-			(mtstate->mt_transition_capture &&
-			 mtstate->operation != CMD_INSERT))
-			resultRelInfo->ri_ChildToRootMap =
-				convert_tuples_by_name(RelationGetDescr(resultRelInfo->ri_RelationDesc),
-									   RelationGetDescr(mtstate->rootResultRelInfo->ri_RelationDesc));
-	}
-
-	/* Get the root target relation */
-	rel = mtstate->rootResultRelInfo->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.
-	 */
-	if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
-		(operation == CMD_INSERT || update_tuple_routing_needed))
-		mtstate->mt_partition_tuple_routing =
-			ExecSetupPartitionTupleRouting(estate, mtstate, rel);
-
-	/*
-	 * For update row movement we'll need a dedicated slot to store the tuples
-	 * that have been converted from partition format to the root table
-	 * format.
-	 */
-	if (update_tuple_routing_needed)
-		mtstate->mt_root_tuple_slot = table_slot_create(rel, NULL);
-
-	/*
-	 * Initialize any WITH CHECK OPTION constraints if needed.
-	 */
-	resultRelInfo = mtstate->resultRelInfo;
-	foreach(l, node->withCheckOptionLists)
-	{
-		List	   *wcoList = (List *) lfirst(l);
-		List	   *wcoExprs = NIL;
-		ListCell   *ll;
-
-		foreach(ll, wcoList)
+		if (node->withCheckOptionLists)
 		{
-			WithCheckOption *wco = (WithCheckOption *) lfirst(ll);
-			ExprState  *wcoExpr = ExecInitQual((List *) wco->qual,
-											   &mtstate->ps);
+			List   *wcoList = (List *) list_nth(node->withCheckOptionLists, i);
 
-			wcoExprs = lappend(wcoExprs, wcoExpr);
+			resultRelInfo->ri_WithCheckOptions = wcoList;
+			/*
+			 * ri_WithCheckOptionExprs is built the first time it needs to be
+			 * used (see ExecWithCheckOptions()).
+			 */
 		}
 
-		resultRelInfo->ri_WithCheckOptions = wcoList;
-		resultRelInfo->ri_WithCheckOptionExprs = wcoExprs;
-		resultRelInfo++;
-	}
-
-	/*
-	 * Initialize RETURNING projections if needed.
-	 */
-	if (node->returningLists)
-	{
-		TupleTableSlot *slot;
-		ExprContext *econtext;
-
 		/*
-		 * Initialize result tuple slot and assign its rowtype using the first
-		 * RETURNING list.  We assume the rest will look the same.
+		 * Initialize RETURNING projections if needed.
 		 */
-		mtstate->ps.plan->targetlist = (List *) linitial(node->returningLists);
-
-		/* Set up a slot for the output of the RETURNING projection(s) */
-		ExecInitResultTupleSlotTL(&mtstate->ps, &TTSOpsVirtual);
-		slot = mtstate->ps.ps_ResultTupleSlot;
+		if (node->returningLists)
+		{
+			List  *rlist = (List *) list_nth(node->returningLists, i);
 
-		/* Need an econtext too */
-		if (mtstate->ps.ps_ExprContext == NULL)
-			ExecAssignExprContext(estate, &mtstate->ps);
-		econtext = mtstate->ps.ps_ExprContext;
+			resultRelInfo->ri_returningList = rlist;
+			/*
+			 * ri_projectReturning is built the first time it needs to be used
+			 * (see ExecProcessReturning), unless the relation is going to be
+			 * "direct modified" by its FDW.
+			 */
+			if (resultRelInfo->ri_usesFdwDirectModify)
+				ExecInitReturningProjection(resultRelInfo, mtstate);
+		}
 
 		/*
-		 * Build a projection for each result rel.
+		 * For UPDATE/DELETE, find the appropriate junk attr now, either a
+		 * 'ctid' or 'wholerow' attribute depending on relkind.  For foreign
+		 * tables, the FDW might have created additional junk attr(s), but
+		 * those are no concern of ours.
 		 */
-		resultRelInfo = mtstate->resultRelInfo;
-		foreach(l, node->returningLists)
+		if (operation == CMD_UPDATE || operation == CMD_DELETE)
 		{
-			List	   *rlist = (List *) lfirst(l);
+			char		relkind;
 
-			resultRelInfo->ri_returningList = rlist;
-			resultRelInfo->ri_projectReturning =
-				ExecBuildProjectionInfo(rlist, econtext, slot, &mtstate->ps,
-										resultRelInfo->ri_RelationDesc->rd_att);
-			resultRelInfo++;
+			relkind = resultRelInfo->ri_RelationDesc->rd_rel->relkind;
+			if (relkind == RELKIND_RELATION ||
+				relkind == RELKIND_MATVIEW ||
+				relkind == RELKIND_PARTITIONED_TABLE)
+			{
+				resultRelInfo->ri_RowIdAttNo =
+					ExecFindJunkAttributeInTlist(subplan->targetlist, "ctid");
+				if (!AttributeNumberIsValid(resultRelInfo->ri_RowIdAttNo))
+					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.  We also require it to be present in
+				 * UPDATE, so we can get the values of unchanged columns.
+				 */
+				resultRelInfo->ri_RowIdAttNo =
+					ExecFindJunkAttributeInTlist(subplan->targetlist,
+												 "wholerow");
+				if (mtstate->operation == CMD_UPDATE &&
+					!AttributeNumberIsValid(resultRelInfo->ri_RowIdAttNo))
+					elog(ERROR, "could not find junk wholerow column");
+			}
+			else
+			{
+				/* Other valid target relkinds must provide wholerow */
+				resultRelInfo->ri_RowIdAttNo =
+					ExecFindJunkAttributeInTlist(subplan->targetlist,
+												 "wholerow");
+				if (!AttributeNumberIsValid(resultRelInfo->ri_RowIdAttNo))
+					elog(ERROR, "could not find junk wholerow column");
+			}
 		}
-	}
-	else
-	{
+
 		/*
-		 * We still must construct a dummy result tuple type, because InitPlan
-		 * expects one (maybe should change that?).
+		 * Determine if the FDW supports batch insert and determine the batch
+		 * size (a FDW may support batching, but it may be disabled for the
+		 * server/table).
+		 *
+		 * We only do this for INSERT, so that for UPDATE/DELETE the batch
+		 * size remains set to 0.
 		 */
-		mtstate->ps.plan->targetlist = NIL;
-		ExecInitResultTypeTL(&mtstate->ps);
+		if (operation == CMD_INSERT)
+		{
+			if (!resultRelInfo->ri_usesFdwDirectModify &&
+				resultRelInfo->ri_FdwRoutine != NULL &&
+				resultRelInfo->ri_FdwRoutine->GetForeignModifyBatchSize &&
+				resultRelInfo->ri_FdwRoutine->ExecForeignBatchInsert)
+				resultRelInfo->ri_BatchSize =
+					resultRelInfo->ri_FdwRoutine->GetForeignModifyBatchSize(resultRelInfo);
+			else
+				resultRelInfo->ri_BatchSize = 1;
+			Assert(resultRelInfo->ri_BatchSize >= 1);
+		}
 
-		mtstate->ps.ps_ExprContext = NULL;
+		resultRelInfo++;
+		i++;
 	}
 
+	/*
+	 * Initialize the subplan.
+	 */
+	outerPlanState(mtstate) = ExecInitNode(subplan, estate, eflags);
+
+	/* Get the root target relation */
+	rel = mtstate->rootResultRelInfo->ri_RelationDesc;
+
+	/*
+	 * Build state for tuple routing if it's an INSERT.  An UPDATE might need
+	 * it too, but it's initialized only when it actually ends up moving
+	 * tuples between partitions; see ExecCrossPartitionUpdate().
+	 */
+	if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
+		operation == CMD_INSERT)
+		mtstate->mt_partition_tuple_routing =
+			ExecSetupPartitionTupleRouting(estate, rel);
+
 	/* Set the list of arbiter indexes if needed for ON CONFLICT */
 	resultRelInfo = mtstate->resultRelInfo;
 	if (node->onConflictAction != ONCONFLICT_NONE)
+	{
+		/* insert may only have one relation, inheritance is not expanded */
+		Assert(nrels == 1);
 		resultRelInfo->ri_onConflictArbiterIndexes = node->arbiterIndexes;
+	}
 
 	/*
 	 * If needed, Initialize target list, projection and qual for ON CONFLICT
@@ -2827,151 +2979,6 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 
 	EvalPlanQualSetPlan(&mtstate->mt_epqstate, subplan, arowmarks);
 
-	/*
-	 * Initialize projection(s) to create tuples suitable for result rel(s).
-	 * INSERT queries may need a projection to filter out junk attrs in the
-	 * tlist.  UPDATE always needs a projection, because (1) there's always
-	 * some junk attrs, and (2) we may need to merge values of not-updated
-	 * columns from the old tuple into the final tuple.  In UPDATE, the tuple
-	 * arriving from the subplan contains only new values for the changed
-	 * columns, plus row identity info in the junk attrs.
-	 *
-	 * If there are multiple result relations, each one needs its own
-	 * projection.  Note multiple rels are only possible for UPDATE/DELETE, so
-	 * we can't be fooled by some needing a projection and some not.
-	 *
-	 * This section of code is also a convenient place to verify that the
-	 * output of an INSERT or UPDATE matches the target table(s).
-	 */
-	for (i = 0; i < nrels; i++)
-	{
-		resultRelInfo = &mtstate->resultRelInfo[i];
-
-		/*
-		 * Prepare to generate tuples suitable for the target relation.
-		 */
-		if (operation == CMD_INSERT)
-		{
-			List	   *insertTargetList = NIL;
-			bool		need_projection = false;
-
-			foreach(l, subplan->targetlist)
-			{
-				TargetEntry *tle = (TargetEntry *) lfirst(l);
-
-				if (!tle->resjunk)
-					insertTargetList = lappend(insertTargetList, tle);
-				else
-					need_projection = true;
-			}
-
-			/*
-			 * The junk-free list must produce a tuple suitable for the result
-			 * relation.
-			 */
-			ExecCheckPlanOutput(resultRelInfo->ri_RelationDesc,
-								insertTargetList);
-
-			/* We'll need a slot matching the table's format. */
-			resultRelInfo->ri_newTupleSlot =
-				table_slot_create(resultRelInfo->ri_RelationDesc,
-								  &mtstate->ps.state->es_tupleTable);
-
-			/* Build ProjectionInfo if needed (it probably isn't). */
-			if (need_projection)
-			{
-				TupleDesc	relDesc = RelationGetDescr(resultRelInfo->ri_RelationDesc);
-
-				/* need an expression context to do the projection */
-				if (mtstate->ps.ps_ExprContext == NULL)
-					ExecAssignExprContext(estate, &mtstate->ps);
-
-				resultRelInfo->ri_projectNew =
-					ExecBuildProjectionInfo(insertTargetList,
-											mtstate->ps.ps_ExprContext,
-											resultRelInfo->ri_newTupleSlot,
-											&mtstate->ps,
-											relDesc);
-			}
-		}
-		else if (operation == CMD_UPDATE)
-		{
-			List	   *updateColnos;
-			TupleDesc	relDesc = RelationGetDescr(resultRelInfo->ri_RelationDesc);
-
-			updateColnos = (List *) list_nth(node->updateColnosLists, i);
-
-			/*
-			 * For UPDATE, we use the old tuple to fill up missing values in
-			 * the tuple produced by the plan to get the new tuple.  We need
-			 * two slots, both matching the table's desired format.
-			 */
-			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 =
-				ExecBuildUpdateProjection(subplan->targetlist,
-										  updateColnos,
-										  relDesc,
-										  mtstate->ps.ps_ExprContext,
-										  resultRelInfo->ri_newTupleSlot,
-										  &mtstate->ps);
-		}
-
-		/*
-		 * For UPDATE/DELETE, find the appropriate junk attr now, either a
-		 * 'ctid' or 'wholerow' attribute depending on relkind.  For foreign
-		 * tables, the FDW might have created additional junk attr(s), but
-		 * those are no concern of ours.
-		 */
-		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_RowIdAttNo =
-					ExecFindJunkAttributeInTlist(subplan->targetlist, "ctid");
-				if (!AttributeNumberIsValid(resultRelInfo->ri_RowIdAttNo))
-					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.  We also require it to be present in
-				 * UPDATE, so we can get the values of unchanged columns.
-				 */
-				resultRelInfo->ri_RowIdAttNo =
-					ExecFindJunkAttributeInTlist(subplan->targetlist,
-												 "wholerow");
-				if (mtstate->operation == CMD_UPDATE &&
-					!AttributeNumberIsValid(resultRelInfo->ri_RowIdAttNo))
-					elog(ERROR, "could not find junk wholerow column");
-			}
-			else
-			{
-				/* Other valid target relkinds must provide wholerow */
-				resultRelInfo->ri_RowIdAttNo =
-					ExecFindJunkAttributeInTlist(subplan->targetlist,
-												 "wholerow");
-				if (!AttributeNumberIsValid(resultRelInfo->ri_RowIdAttNo))
-					elog(ERROR, "could not find junk wholerow column");
-			}
-		}
-	}
-
 	/*
 	 * If this is an inherited update/delete, there will be a junk attribute
 	 * named "tableoid" present in the subplan's targetlist.  It will be used
@@ -3026,34 +3033,6 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 	else
 		mtstate->mt_resultOidHash = NULL;
 
-	/*
-	 * Determine if the FDW supports batch insert and determine the batch
-	 * size (a FDW may support batching, but it may be disabled for the
-	 * server/table).
-	 *
-	 * We only do this for INSERT, so that for UPDATE/DELETE the batch
-	 * size remains set to 0.
-	 */
-	if (operation == CMD_INSERT)
-	{
-		resultRelInfo = mtstate->resultRelInfo;
-		for (i = 0; i < nrels; i++)
-		{
-			if (!resultRelInfo->ri_usesFdwDirectModify &&
-				resultRelInfo->ri_FdwRoutine != NULL &&
-				resultRelInfo->ri_FdwRoutine->GetForeignModifyBatchSize &&
-				resultRelInfo->ri_FdwRoutine->ExecForeignBatchInsert)
-				resultRelInfo->ri_BatchSize =
-					resultRelInfo->ri_FdwRoutine->GetForeignModifyBatchSize(resultRelInfo);
-			else
-				resultRelInfo->ri_BatchSize = 1;
-
-			Assert(resultRelInfo->ri_BatchSize >= 1);
-
-			resultRelInfo++;
-		}
-	}
-
 	/*
 	 * 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
@@ -3140,3 +3119,64 @@ ExecReScanModifyTable(ModifyTableState *node)
 	 */
 	elog(ERROR, "ExecReScanModifyTable is not implemented");
 }
+
+/*
+ * ExecLookupResultRelByOid
+ * 		If the table with given OID is among the result relations to be
+ * 		updated by the given ModifyTable node, return its ResultRelInfo, NULL
+ * 		otherwise.
+ */
+ResultRelInfo *
+ExecLookupResultRelByOid(ModifyTableState *node, Oid resultoid,
+						 bool missing_ok)
+{
+	bool	found;
+
+	if (node->mt_resultOidHash)
+	{
+		/* Use the pre-built hash table to locate the rel */
+		MTTargetRelLookup *mtlookup;
+
+		mtlookup = (MTTargetRelLookup *)
+			hash_search(node->mt_resultOidHash, &resultoid, HASH_FIND,
+						&found);
+		if (found)
+		{
+			Assert(mtlookup != NULL);
+			node->mt_lastResultOid = resultoid;
+			node->mt_lastResultIndex = mtlookup->relationIndex;
+		}
+		else if (!missing_ok)
+			elog(ERROR, "incorrect result rel OID %u", resultoid);
+	}
+	else
+	{
+		/* With few target rels, search in the pre-built OID array */
+		int			ndx;
+
+		found = false;
+		for (ndx = 0; ndx < node->mt_nrels; ndx++)
+		{
+			ResultRelInfo *rInfo = node->resultRelInfo + ndx;
+
+			if (RelationGetRelid(rInfo->ri_RelationDesc) == resultoid)
+			{
+				found = true;
+				break;
+			}
+		}
+		if (found)
+		{
+			Assert(ndx < node->mt_nrels);
+			node->mt_lastResultOid = resultoid;
+			node->mt_lastResultIndex = ndx;
+		}
+		else if (!missing_ok)
+			elog(ERROR, "incorrect result rel OID %u", resultoid);
+	}
+
+	if (!found)
+		return NULL;
+
+	return node->resultRelInfo + node->mt_lastResultIndex;
+}
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index 354fbe4b4b..e901823b63 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -1583,7 +1583,7 @@ apply_handle_tuple_routing(ResultRelInfo *relinfo,
 	mtstate->ps.state = estate;
 	mtstate->operation = operation;
 	mtstate->resultRelInfo = relinfo;
-	proute = ExecSetupPartitionTupleRouting(estate, mtstate, parentrel);
+	proute = ExecSetupPartitionTupleRouting(estate, parentrel);
 
 	/*
 	 * Find the partition to which the "search tuple" belongs.
diff --git a/src/include/executor/execPartition.h b/src/include/executor/execPartition.h
index d30ffde7d9..694e38b7dd 100644
--- a/src/include/executor/execPartition.h
+++ b/src/include/executor/execPartition.h
@@ -111,7 +111,6 @@ typedef struct PartitionPruneState
 } PartitionPruneState;
 
 extern PartitionTupleRouting *ExecSetupPartitionTupleRouting(EState *estate,
-															 ModifyTableState *mtstate,
 															 Relation rel);
 extern ResultRelInfo *ExecFindPartition(ModifyTableState *mtstate,
 										ResultRelInfo *rootResultRelInfo,
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 26dcc4485e..7c6ee53d4f 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -211,7 +211,8 @@ extern bool ExecPartitionCheck(ResultRelInfo *resultRelInfo,
 extern void ExecPartitionCheckEmitError(ResultRelInfo *resultRelInfo,
 										TupleTableSlot *slot, EState *estate);
 extern void ExecWithCheckOptions(WCOKind kind, ResultRelInfo *resultRelInfo,
-								 TupleTableSlot *slot, EState *estate);
+								 TupleTableSlot *slot, EState *estate,
+								 PlanState *parent);
 extern LockTupleMode ExecUpdateLockMode(EState *estate, ResultRelInfo *relinfo);
 extern ExecRowMark *ExecFindRowMark(EState *estate, Index rti, bool missing_ok);
 extern ExecAuxRowMark *ExecBuildAuxRowMark(ExecRowMark *erm, List *targetlist);
@@ -596,6 +597,7 @@ extern int	ExecCleanTargetListLength(List *targetlist);
 extern TupleTableSlot *ExecGetTriggerOldSlot(EState *estate, ResultRelInfo *relInfo);
 extern TupleTableSlot *ExecGetTriggerNewSlot(EState *estate, ResultRelInfo *relInfo);
 extern TupleTableSlot *ExecGetReturningSlot(EState *estate, ResultRelInfo *relInfo);
+extern TupleConversionMap *ExecGetChildToRootMap(ResultRelInfo *resultRelInfo);
 
 extern Bitmapset *ExecGetInsertedCols(ResultRelInfo *relinfo, EState *estate);
 extern Bitmapset *ExecGetUpdatedCols(ResultRelInfo *relinfo, EState *estate);
@@ -645,9 +647,11 @@ extern void CheckCmdReplicaIdentity(Relation rel, CmdType cmd);
 extern void CheckSubscriptionRelkind(char relkind, const char *nspname,
 									 const char *relname);
 
-/* needed by trigger.c */
+/* prototypes from nodeModifyTable.c */
 extern TupleTableSlot *ExecGetUpdateNewTuple(ResultRelInfo *relinfo,
 											 TupleTableSlot *planSlot,
 											 TupleTableSlot *oldSlot);
+extern ResultRelInfo *ExecLookupResultRelByOid(ModifyTableState *node, Oid resultoid,
+						 bool missing_ok);
 
 #endif							/* EXECUTOR_H  */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 52d1fa018b..a0b29745bb 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -431,6 +431,8 @@ typedef struct ResultRelInfo
 	TupleTableSlot *ri_newTupleSlot;
 	/* Slot to hold the old tuple being updated */
 	TupleTableSlot *ri_oldTupleSlot;
+	/* Have the projection and the slots above been initialized? */
+	bool			ri_projectNewInfoValid;
 
 	/* triggers to be fired, if any */
 	TriggerDesc *ri_TrigDesc;
@@ -516,6 +518,8 @@ typedef struct ResultRelInfo
 	 * transition tuple capture or update partition row movement is active.
 	 */
 	TupleConversionMap *ri_ChildToRootMap;
+	/* has the map been initialized? */
+	bool		ri_ChildToRootMapValid;
 
 	/* for use by copyfrom.c when performing multi-inserts */
 	struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out
index 1c703c351f..06f44287bc 100644
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -2492,7 +2492,7 @@ ERROR:  new row for relation "errtst_child_plaindef" violates check constraint "
 DETAIL:  Failing row contains (10, 1, 15).
 UPDATE errtst_parent SET data = data + 10 WHERE partid = 20;
 ERROR:  new row for relation "errtst_child_reorder" violates check constraint "errtst_child_reorder_data_check"
-DETAIL:  Failing row contains (15, 1, 20).
+DETAIL:  Failing row contains (20, 1, 15).
 -- direct leaf partition update, without partition id violation
 BEGIN;
 UPDATE errtst_child_fastdef SET partid = 1 WHERE partid = 0;
diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index 89f3d5da46..6372e0ed6a 100644
--- a/src/test/regress/expected/privileges.out
+++ b/src/test/regress/expected/privileges.out
@@ -685,7 +685,7 @@ DETAIL:  Failing row contains (a, b, c) = (aaa, null, null).
 -- simple update.
 UPDATE errtst SET b = NULL;
 ERROR:  null value in column "b" of relation "errtst_part_1" violates not-null constraint
-DETAIL:  Failing row contains (b) = (null).
+DETAIL:  Failing row contains (a, b, c) = (aaa, null, ccc).
 -- partitioning key is updated, doesn't move the row.
 UPDATE errtst SET a = 'aaa', b = NULL;
 ERROR:  null value in column "b" of relation "errtst_part_1" violates not-null constraint
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
index cdff914b93..dd309283be 100644
--- a/src/test/regress/expected/updatable_views.out
+++ b/src/test/regress/expected/updatable_views.out
@@ -1892,28 +1892,22 @@ UPDATE rw_view1 SET a = a + 5; -- should fail
 ERROR:  new row violates check option for view "rw_view1"
 DETAIL:  Failing row contains (15).
 EXPLAIN (costs off) INSERT INTO rw_view1 VALUES (5);
-                       QUERY PLAN                        
----------------------------------------------------------
+      QUERY PLAN      
+----------------------
  Insert on base_tbl b
    ->  Result
-   SubPlan 1
-     ->  Index Only Scan using ref_tbl_pkey on ref_tbl r
-           Index Cond: (a = b.a)
-(5 rows)
+(2 rows)
 
 EXPLAIN (costs off) UPDATE rw_view1 SET a = a + 5;
-                        QUERY PLAN                         
------------------------------------------------------------
+               QUERY PLAN                
+-----------------------------------------
  Update on base_tbl b
    ->  Hash Join
          Hash Cond: (b.a = r.a)
          ->  Seq Scan on base_tbl b
          ->  Hash
                ->  Seq Scan on ref_tbl r
-   SubPlan 1
-     ->  Index Only Scan using ref_tbl_pkey on ref_tbl r_1
-           Index Cond: (a = b.a)
-(9 rows)
+(6 rows)
 
 DROP TABLE base_tbl, ref_tbl CASCADE;
 NOTICE:  drop cascades to view rw_view1
diff --git a/src/test/regress/expected/update.out b/src/test/regress/expected/update.out
index dc34ac67b3..ad91e5aedb 100644
--- a/src/test/regress/expected/update.out
+++ b/src/test/regress/expected/update.out
@@ -342,8 +342,8 @@ DETAIL:  Failing row contains (105, 85, null, b, 15).
 -- fail, no partition key update, so no attempt to move tuple,
 -- but "a = 'a'" violates partition constraint enforced by root partition)
 UPDATE part_b_10_b_20 set a = 'a';
-ERROR:  new row for relation "part_c_1_100" violates partition constraint
-DETAIL:  Failing row contains (null, 1, 96, 12, a).
+ERROR:  new row for relation "part_b_10_b_20" violates partition constraint
+DETAIL:  Failing row contains (null, 96, a, 12, 1).
 -- ok, partition key update, no constraint violation
 UPDATE range_parted set d = d - 10 WHERE d > 10;
 -- ok, no partition key update, no constraint violation
@@ -373,8 +373,8 @@ UPDATE part_b_10_b_20 set c = c + 20 returning c, b, a;
 
 -- fail, row movement happens only within the partition subtree.
 UPDATE part_b_10_b_20 set b = b - 6 WHERE c > 116 returning *;
-ERROR:  new row for relation "part_d_1_15" violates partition constraint
-DETAIL:  Failing row contains (2, 117, 2, b, 7).
+ERROR:  new row for relation "part_b_10_b_20" violates partition constraint
+DETAIL:  Failing row contains (2, 117, b, 7, 2).
 -- ok, row movement, with subset of rows moved into different partition.
 UPDATE range_parted set b = b - 6 WHERE c > 116 returning a, b + c;
  a | ?column? 
@@ -815,8 +815,8 @@ INSERT into sub_parted VALUES (1,2,10);
 -- Test partition constraint violation when intermediate ancestor is used and
 -- constraint is inherited from upper root.
 UPDATE sub_parted set a = 2 WHERE c = 10;
-ERROR:  new row for relation "sub_part2" violates partition constraint
-DETAIL:  Failing row contains (2, 10, 2).
+ERROR:  new row for relation "sub_parted" violates partition constraint
+DETAIL:  Failing row contains (2, 2, 10).
 -- Test update-partition-key, where the unpruned partitions do not have their
 -- partition keys updated.
 SELECT tableoid::regclass::text, * FROM list_parted WHERE a = 2 ORDER BY 1;
