From e5898351861518fba28f1b0f7ff6ea8a1f5a94bb Mon Sep 17 00:00:00 2001
From: Amit Langote <amitlan@postgresql.org>
Date: Tue, 11 Nov 2025 21:47:46 +0900
Subject: [PATCH v5 2/6] Introduce ExecutorPrep and refactor executor startup

Factor permission checks, range table initialization, and initial
partition pruning out of InitPlan() into a new ExecutorPrep()
helper. ExecutorPrep() builds an EState containing the executor
metadata needed before plan execution, including partition
pruning state where partPruneInfos are present, and returns it
directly to the caller.

ExecutorStart() now checks if QueryDesc->estate is already set
(indicating ExecutorPrep() was called earlier). If so, it reuses
the EState to avoid redoing range table setup and pruning.
Otherwise, it invokes ExecutorPrep() itself and adopts the
resulting EState for the duration of the query. This keeps the
executor startup behavior unchanged while making the setup work
callable separately when needed.

CreateQueryDesc() grows a prep_estate argument to accept an
optionally pre-created EState and stores it in the QueryDesc.
Portals, SPI, SQL functions, and EXPLAIN are wired to carry
optional EState pointers alongside the PlannedStmt list, but most
callers still pass NULL and let ExecutorStart() perform the setup
lazily.

ExecutorPrep() requires the caller to have established an active
snapshot, as partition pruning expressions may call PL functions
that internally require one (e.g., via EnsurePortalSnapshotExists()).

Update executor/README and related comments to document the new
control flow and the separation between preparation and execution.

Note that as of this commit, ExecutorStart() is the only caller of
ExecutorPrep(), so there is no semantic change in behavior. Later
commits will add specialized callers that invoke ExecutorPrep()
earlier to enable pruning-aware locking in cached plans.
---
 src/backend/commands/copyto.c       |   2 +-
 src/backend/commands/createas.c     |   2 +-
 src/backend/commands/explain.c      |   8 +-
 src/backend/commands/extension.c    |   2 +-
 src/backend/commands/matview.c      |   2 +-
 src/backend/commands/portalcmds.c   |   1 +
 src/backend/commands/prepare.c      |   9 +-
 src/backend/executor/README         |  11 ++-
 src/backend/executor/execMain.c     | 123 +++++++++++++++++++++-------
 src/backend/executor/execParallel.c |   3 +-
 src/backend/executor/functions.c    |   3 +-
 src/backend/executor/spi.c          |   9 +-
 src/backend/tcop/postgres.c         |   2 +
 src/backend/tcop/pquery.c           |  24 ++++--
 src/backend/utils/mmgr/portalmem.c  |   2 +
 src/include/commands/explain.h      |   3 +-
 src/include/executor/execdesc.h     |   5 +-
 src/include/executor/executor.h     |  26 ++++++
 src/include/nodes/execnodes.h       |   1 -
 src/include/utils/portal.h          |   2 +
 20 files changed, 188 insertions(+), 52 deletions(-)

diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index 9ceeff6d99e..ef1ee2568c6 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -875,7 +875,7 @@ BeginCopyTo(ParseState *pstate,
 		cstate->queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext,
 											GetActiveSnapshot(),
 											InvalidSnapshot,
-											dest, NULL, NULL, 0);
+											dest, NULL, NULL, 0, NULL);
 
 		/*
 		 * Call ExecutorStart to prepare the plan for execution.
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index 270e9bf3110..b4a9808955a 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -336,7 +336,7 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
 		/* Create a QueryDesc, redirecting output to our tuple receiver */
 		queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext,
 									GetActiveSnapshot(), InvalidSnapshot,
-									dest, params, queryEnv, 0);
+									dest, params, queryEnv, 0, NULL);
 
 		/* call ExecutorStart to prepare the plan for execution */
 		ExecutorStart(queryDesc, GetIntoRelEFlags(into));
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index b7bb111688c..a5db3ed788e 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -370,7 +370,7 @@ standard_ExplainOneQuery(Query *query, int cursorOptions,
 	}
 
 	/* run it (if needed) and produce output */
-	ExplainOnePlan(plan, into, es, queryString, params, queryEnv,
+	ExplainOnePlan(plan, NULL, into, es, queryString, params, queryEnv,
 				   &planduration, (es->buffers ? &bufusage : NULL),
 				   es->memory ? &mem_counters : NULL);
 }
@@ -492,7 +492,8 @@ ExplainOneUtility(Node *utilityStmt, IntoClause *into, ExplainState *es,
  * to call it.
  */
 void
-ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
+ExplainOnePlan(PlannedStmt *plannedstmt, EState *prep_estate,
+			   IntoClause *into, ExplainState *es,
 			   const char *queryString, ParamListInfo params,
 			   QueryEnvironment *queryEnv, const instr_time *planduration,
 			   const BufferUsage *bufusage,
@@ -550,7 +551,8 @@ ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
 	/* Create a QueryDesc for the query */
 	queryDesc = CreateQueryDesc(plannedstmt, queryString,
 								GetActiveSnapshot(), InvalidSnapshot,
-								dest, params, queryEnv, instrument_option);
+								dest, params, queryEnv, instrument_option,
+								prep_estate);
 
 	/* Select execution options */
 	if (es->analyze)
diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c
index 596105ee078..5743caa0506 100644
--- a/src/backend/commands/extension.c
+++ b/src/backend/commands/extension.c
@@ -1043,7 +1043,7 @@ execute_sql_string(const char *sql, const char *filename)
 				qdesc = CreateQueryDesc(stmt,
 										sql,
 										GetActiveSnapshot(), NULL,
-										dest, NULL, NULL, 0);
+										dest, NULL, NULL, 0, NULL);
 
 				ExecutorStart(qdesc, 0);
 				ExecutorRun(qdesc, ForwardScanDirection, 0);
diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index 81a55a33ef2..2cdfdcf984b 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -439,7 +439,7 @@ refresh_matview_datafill(DestReceiver *dest, Query *query,
 	/* Create a QueryDesc, redirecting output to our tuple receiver */
 	queryDesc = CreateQueryDesc(plan, queryString,
 								GetActiveSnapshot(), InvalidSnapshot,
-								dest, NULL, NULL, 0);
+								dest, NULL, NULL, 0, NULL);
 
 	/* call ExecutorStart to prepare the plan for execution */
 	ExecutorStart(queryDesc, 0);
diff --git a/src/backend/commands/portalcmds.c b/src/backend/commands/portalcmds.c
index 01efac3319e..1e880a6d7c9 100644
--- a/src/backend/commands/portalcmds.c
+++ b/src/backend/commands/portalcmds.c
@@ -118,6 +118,7 @@ PerformCursorOpen(ParseState *pstate, DeclareCursorStmt *cstmt, ParamListInfo pa
 					  queryString,
 					  CMDTAG_SELECT,	/* cursor's query is always a SELECT */
 					  list_make1(plan),
+					  NIL,
 					  NULL);
 
 	/*----------
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..005fbb48aa5 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -205,6 +205,7 @@ ExecuteQuery(ParseState *pstate,
 					  query_string,
 					  entry->plansource->commandTag,
 					  plan_list,
+					  NIL,
 					  cplan);
 
 	/*
@@ -575,7 +576,9 @@ ExplainExecuteQuery(ExecuteStmt *execstmt, IntoClause *into, ExplainState *es,
 	const char *query_string;
 	CachedPlan *cplan;
 	List	   *plan_list;
+	List	   *prep_estates;
 	ListCell   *p;
+	ListCell   *prep_lc;
 	ParamListInfo paramLI = NULL;
 	EState	   *estate = NULL;
 	instr_time	planstart;
@@ -650,14 +653,18 @@ ExplainExecuteQuery(ExecuteStmt *execstmt, IntoClause *into, ExplainState *es,
 	}
 
 	plan_list = cplan->stmt_list;
+	prep_estates = NIL;
 
 	/* Explain each query */
+	prep_lc = list_head(prep_estates);
 	foreach(p, plan_list)
 	{
 		PlannedStmt *pstmt = lfirst_node(PlannedStmt, p);
+		EState *prep_estate = next_prep_estate(prep_estates, &prep_lc);
 
 		if (pstmt->commandType != CMD_UTILITY)
-			ExplainOnePlan(pstmt, into, es, query_string, paramLI, pstate->p_queryEnv,
+			ExplainOnePlan(pstmt, prep_estate,
+						   into, es, query_string, paramLI, pstate->p_queryEnv,
 						   &planduration, (es->buffers ? &bufusage : NULL),
 						   es->memory ? &mem_counters : NULL);
 		else
diff --git a/src/backend/executor/README b/src/backend/executor/README
index 54f4782f31b..d749ceb6687 100644
--- a/src/backend/executor/README
+++ b/src/backend/executor/README
@@ -291,11 +291,18 @@ Query Processing Control Flow
 
 This is a sketch of control flow for full query processing:
 
+    ExecutorPrep
+		May be run before ExecutorStart (e.g., for plan validation), or
+		implicitly from ExecutorStart if not done earlier.  Creates EState,
+		performs range table initialization, permission checks, and initial
+		partition pruning.  Returns the EState that ExecutorStart() should
+		reuse.
+
 	CreateQueryDesc
 
 	ExecutorStart
-		CreateExecutorState
-			creates per-query context
+		ExecutorPrep (if not already done, indicated by NULL QueryDesc.estate)
+			creates EState and per-query context
 		switch to per-query context to run ExecInitNode
 		AfterTriggerBeginQuery
 		ExecInitNode --- recursively scans plan tree
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 654f9246ad0..8c50f45a5c5 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -145,7 +145,6 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags)
 
 	/* sanity checks: queryDesc must not be started already */
 	Assert(queryDesc != NULL);
-	Assert(queryDesc->estate == NULL);
 
 	/* caller must ensure the query's snapshot is active */
 	Assert(GetActiveSnapshot() == queryDesc->snapshot);
@@ -171,9 +170,19 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags)
 
 	/*
 	 * Build EState, switch into per-query memory context for startup.
-	 */
-	estate = CreateExecutorState();
-	queryDesc->estate = estate;
+	 *
+	 * If ExecutorPrep() ran earlier (e.g., to do initial pruning during plan
+	 * validity checking), reuse its EState to avoid redoing range table setup
+	 * and pruning. Otherwise, create a fresh EState as usual.
+	 */
+	if (queryDesc->estate == NULL)
+		queryDesc->estate = ExecutorPrep(queryDesc->plannedstmt,
+										 queryDesc->params,
+										 CurrentResourceOwner,
+										 true,
+										 eflags);
+	estate = queryDesc->estate;
+	Assert(estate);
 
 	oldcontext = MemoryContextSwitchTo(estate->es_query_cxt);
 
@@ -263,6 +272,84 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags)
 	MemoryContextSwitchTo(oldcontext);
 }
 
+/*
+ * ExecutorPrep: prepare executor state for a PlannedStmt outside ExecutorStart.
+ *
+ * Performs range table initialization, permission checks, and initial
+ * partition pruning if partPruneInfos are present and do_initial_pruning is
+ * true.
+ *
+ * This is intended for callers that need executor metadata ahead of actual
+ * execution. Typical use cases include:
+ *	- determining which relations must be locked during plan cache validation;
+ *	- initializing unpruned relids and valid subplans in parallel workers
+ *	  using state copied from the leader.
+ *
+ * The executor can reuse the resulting state to avoid redundant setup during
+ * ExecutorStart().
+ *
+ * Returns an EState that can be reused later.
+ */
+EState *
+ExecutorPrep(PlannedStmt *pstmt, ParamListInfo params, ResourceOwner owner,
+			 bool do_initial_pruning, int eflags)
+{
+	ResourceOwner oldowner;
+	EState *estate;
+
+	if (pstmt->commandType == CMD_UTILITY)
+		return NULL;
+
+	/* Caller must have established an active snapshot. */
+	Assert(ActiveSnapshotSet());
+
+	estate = CreateExecutorState();
+	estate->es_plannedstmt = pstmt;
+	estate->es_part_prune_infos = pstmt->partPruneInfos;
+	estate->es_param_list_info = params;
+	estate->es_top_eflags = eflags;
+
+	/*
+	 * Do permissions checks.
+	 */
+	ExecCheckPermissions(pstmt->rtable, pstmt->permInfos, true);
+
+	/*
+	 * Initialize range table.
+	 */
+	ExecInitRangeTable(estate, pstmt->rtable, pstmt->permInfos,
+					   bms_copy(pstmt->unprunableRelids));
+
+	/*
+	 * Ensure locks taken during initial pruning are tracked under the given
+	 * ResourceOwner (e.g., one associated with CachedPlan validation).
+	 */
+	oldowner = CurrentResourceOwner;
+	CurrentResourceOwner = owner;
+
+	/*
+	 * Set up PartitionPruneState structures needed for both initial and
+	 * runtime partition pruning. These structures are built from the
+	 * PartitionPruneInfo entries in the plan tree.
+	 *
+	 * If do_initial_pruning is true, also perform initial pruning to compute
+	 * the subset of child subplans that will be executed. The results,
+	 * which are bitmapsets of selected child indexes, are saved in
+	 * es_part_prune_results. This list is parallel to es_part_prune_infos.
+	 *
+	 * In parallel workers, do_initial_pruning should be false -- they receive
+	 * es_part_prune_results from the leader process and should only initialize
+	 * the PartitionPruneStates.
+	 */
+	ExecCreatePartitionPruneStates(estate);
+	if (do_initial_pruning)
+		ExecDoInitialPruning(estate);
+
+	CurrentResourceOwner = oldowner;
+
+	return estate;
+}
+
 /* ----------------------------------------------------------------
  *		ExecutorRun
  *
@@ -838,38 +925,14 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 	CmdType		operation = queryDesc->operation;
 	PlannedStmt *plannedstmt = queryDesc->plannedstmt;
 	Plan	   *plan = plannedstmt->planTree;
-	List	   *rangeTable = plannedstmt->rtable;
 	EState	   *estate = queryDesc->estate;
 	PlanState  *planstate;
 	TupleDesc	tupType;
 	ListCell   *l;
 	int			i;
 
-	/*
-	 * Do permissions checks
-	 */
-	ExecCheckPermissions(rangeTable, plannedstmt->permInfos, true);
-
-	/*
-	 * initialize the node's execution state
-	 */
-	ExecInitRangeTable(estate, rangeTable, plannedstmt->permInfos,
-					   bms_copy(plannedstmt->unprunableRelids));
-
-	estate->es_plannedstmt = plannedstmt;
-	estate->es_part_prune_infos = plannedstmt->partPruneInfos;
-
-	/*
-	 * Perform runtime "initial" pruning to identify which child subplans,
-	 * corresponding to the children of plan nodes that contain
-	 * PartitionPruneInfo such as Append, will not be executed. The results,
-	 * which are bitmapsets of indexes of the child subplans that will be
-	 * executed, are saved in es_part_prune_results.  These results correspond
-	 * to each PartitionPruneInfo entry, and the es_part_prune_results list is
-	 * parallel to es_part_prune_infos.
-	 */
-	ExecCreatePartitionPruneStates(estate);
-	ExecDoInitialPruning(estate);
+	/* ExecutorPrep() must have been done. */
+	Assert(queryDesc->estate);
 
 	/*
 	 * Next, build the ExecRowMark array from the PlanRowMark(s), if any.
diff --git a/src/backend/executor/execParallel.c b/src/backend/executor/execParallel.c
index f87978c137e..2d3c5d6123e 100644
--- a/src/backend/executor/execParallel.c
+++ b/src/backend/executor/execParallel.c
@@ -1299,7 +1299,8 @@ ExecParallelGetQueryDesc(shm_toc *toc, DestReceiver *receiver,
 	return CreateQueryDesc(pstmt,
 						   queryString,
 						   GetActiveSnapshot(), InvalidSnapshot,
-						   receiver, paramLI, NULL, instrument_options);
+						   receiver, paramLI, NULL, instrument_options,
+						   NULL);
 }
 
 /*
diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
index 4ca342a43ef..c93e2664cfd 100644
--- a/src/backend/executor/functions.c
+++ b/src/backend/executor/functions.c
@@ -1368,7 +1368,8 @@ postquel_start(execution_state *es, SQLFunctionCachePtr fcache)
 							 dest,
 							 fcache->paramLI,
 							 es->qd ? es->qd->queryEnv : NULL,
-							 0);
+							 0,
+							 NULL);
 
 	/* Utility commands don't need Executor. */
 	if (es->qd->operation != CMD_UTILITY)
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
index 3019a3b2b97..994a69a1c8e 100644
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -1685,6 +1685,7 @@ SPI_cursor_open_internal(const char *name, SPIPlanPtr plan,
 					  query_string,
 					  plansource->commandTag,
 					  stmt_list,
+					  NIL,
 					  cplan);
 
 	/*
@@ -2499,6 +2500,8 @@ _SPI_execute_plan(SPIPlanPtr plan, const SPIExecuteOptions *options,
 		CachedPlanSource *plansource = (CachedPlanSource *) lfirst(lc1);
 		List	   *stmt_list;
 		ListCell   *lc2;
+		List	   *prep_estates;
+		ListCell   *prep_lc;
 
 		spicallbackarg.query = plansource->query_string;
 
@@ -2577,6 +2580,7 @@ _SPI_execute_plan(SPIPlanPtr plan, const SPIExecuteOptions *options,
 							  plan_owner, _SPI_current->queryEnv);
 
 		stmt_list = cplan->stmt_list;
+		prep_estates = NIL;
 
 		/*
 		 * If we weren't given a specific snapshot to use, and the statement
@@ -2614,9 +2618,11 @@ _SPI_execute_plan(SPIPlanPtr plan, const SPIExecuteOptions *options,
 			}
 		}
 
+		prep_lc = list_head(prep_estates);
 		foreach(lc2, stmt_list)
 		{
 			PlannedStmt *stmt = lfirst_node(PlannedStmt, lc2);
+			EState *prep_estate = next_prep_estate(prep_estates, &prep_lc);
 			bool		canSetTag = stmt->canSetTag;
 			DestReceiver *dest;
 
@@ -2694,7 +2700,8 @@ _SPI_execute_plan(SPIPlanPtr plan, const SPIExecuteOptions *options,
 										dest,
 										options->params,
 										_SPI_current->queryEnv,
-										0);
+										0,
+										prep_estate);
 				res = _SPI_pquery(qdesc, fire_triggers,
 								  canSetTag ? options->tcount : 0);
 				FreeQueryDesc(qdesc);
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 02e9aaa6bca..5541c574c8b 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -1230,6 +1230,7 @@ exec_simple_query(const char *query_string)
 						  query_string,
 						  commandTag,
 						  plantree_list,
+						  NIL,
 						  NULL);
 
 		/*
@@ -2029,6 +2030,7 @@ exec_bind_message(StringInfo input_message)
 					  query_string,
 					  psrc->commandTag,
 					  cplan->stmt_list,
+					  NIL,
 					  cplan);
 
 	/* Portal is defined, set the plan ID based on its contents. */
diff --git a/src/backend/tcop/pquery.c b/src/backend/tcop/pquery.c
index d8fc75d0bb9..b18266487bb 100644
--- a/src/backend/tcop/pquery.c
+++ b/src/backend/tcop/pquery.c
@@ -37,6 +37,7 @@ Portal		ActivePortal = NULL;
 
 
 static void ProcessQuery(PlannedStmt *plan,
+						 EState *prep_estate,
 						 const char *sourceText,
 						 ParamListInfo params,
 						 QueryEnvironment *queryEnv,
@@ -72,7 +73,8 @@ CreateQueryDesc(PlannedStmt *plannedstmt,
 				DestReceiver *dest,
 				ParamListInfo params,
 				QueryEnvironment *queryEnv,
-				int instrument_options)
+				int instrument_options,
+				EState *prep_estate)
 {
 	QueryDesc  *qd = palloc_object(QueryDesc);
 
@@ -93,6 +95,9 @@ CreateQueryDesc(PlannedStmt *plannedstmt,
 	qd->planstate = NULL;
 	qd->totaltime = NULL;
 
+	/* Use the EState created by ExecutorPrep() if already done. */
+	qd->estate = prep_estate;
+
 	/* not yet executed */
 	qd->already_executed = false;
 
@@ -123,6 +128,7 @@ FreeQueryDesc(QueryDesc *qdesc)
  *		PORTAL_ONE_RETURNING, or PORTAL_ONE_MOD_WITH portal
  *
  *	plan: the plan tree for the query
+ *	prep_estate: EState created in ExecutorPrep() for the query, if any
  *	sourceText: the source text of the query
  *	params: any parameters needed
  *	dest: where to send results
@@ -135,6 +141,7 @@ FreeQueryDesc(QueryDesc *qdesc)
  */
 static void
 ProcessQuery(PlannedStmt *plan,
+			 EState *prep_estate,
 			 const char *sourceText,
 			 ParamListInfo params,
 			 QueryEnvironment *queryEnv,
@@ -148,7 +155,8 @@ ProcessQuery(PlannedStmt *plan,
 	 */
 	queryDesc = CreateQueryDesc(plan, sourceText,
 								GetActiveSnapshot(), InvalidSnapshot,
-								dest, params, queryEnv, 0);
+								dest, params, queryEnv, 0,
+								prep_estate);
 
 	/*
 	 * Call ExecutorStart to prepare the plan for execution
@@ -495,7 +503,10 @@ PortalStart(Portal portal, ParamListInfo params,
 											None_Receiver,
 											params,
 											portal->queryEnv,
-											0);
+											0,
+											portal->prep_estates ?
+											(EState *) linitial(portal->prep_estates) :
+											NULL);
 
 				/*
 				 * If it's a scrollable cursor, executor needs to support
@@ -1185,6 +1196,7 @@ PortalRunMulti(Portal portal,
 {
 	bool		active_snapshot_set = false;
 	ListCell   *stmtlist_item;
+	ListCell   *prep_lc;
 
 	/*
 	 * If the destination is DestRemoteExecute, change to DestNone.  The
@@ -1205,9 +1217,11 @@ PortalRunMulti(Portal portal,
 	 * Loop to handle the individual queries generated from a single parsetree
 	 * by analysis and rewrite.
 	 */
+	prep_lc = list_head(portal->prep_estates);
 	foreach(stmtlist_item, portal->stmts)
 	{
 		PlannedStmt *pstmt = lfirst_node(PlannedStmt, stmtlist_item);
+		EState *prep_estate = next_prep_estate(portal->prep_estates, &prep_lc);
 
 		/*
 		 * If we got a cancel signal in prior command, quit
@@ -1265,7 +1279,7 @@ PortalRunMulti(Portal portal,
 			if (pstmt->canSetTag)
 			{
 				/* statement can set tag string */
-				ProcessQuery(pstmt,
+				ProcessQuery(pstmt, prep_estate,
 							 portal->sourceText,
 							 portal->portalParams,
 							 portal->queryEnv,
@@ -1274,7 +1288,7 @@ PortalRunMulti(Portal portal,
 			else
 			{
 				/* stmt added by rewrite cannot set tag */
-				ProcessQuery(pstmt,
+				ProcessQuery(pstmt, prep_estate,
 							 portal->sourceText,
 							 portal->portalParams,
 							 portal->queryEnv,
diff --git a/src/backend/utils/mmgr/portalmem.c b/src/backend/utils/mmgr/portalmem.c
index c1a53e658cb..941e95010c3 100644
--- a/src/backend/utils/mmgr/portalmem.c
+++ b/src/backend/utils/mmgr/portalmem.c
@@ -284,6 +284,7 @@ PortalDefineQuery(Portal portal,
 				  const char *sourceText,
 				  CommandTag commandTag,
 				  List *stmts,
+				  List *prep_estates,
 				  CachedPlan *cplan)
 {
 	Assert(PortalIsValid(portal));
@@ -297,6 +298,7 @@ PortalDefineQuery(Portal portal,
 	portal->commandTag = commandTag;
 	SetQueryCompletion(&portal->qc, commandTag, 0);
 	portal->stmts = stmts;
+	portal->prep_estates = prep_estates;
 	portal->cplan = cplan;
 	portal->status = PORTAL_DEFINED;
 }
diff --git a/src/include/commands/explain.h b/src/include/commands/explain.h
index 86226f8db70..3756a11345f 100644
--- a/src/include/commands/explain.h
+++ b/src/include/commands/explain.h
@@ -63,7 +63,8 @@ extern void ExplainOneUtility(Node *utilityStmt, IntoClause *into,
 							  ExplainState *es, ParseState *pstate,
 							  ParamListInfo params);
 
-extern void ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into,
+extern void ExplainOnePlan(PlannedStmt *plannedstmt, EState *prep_estate,
+						   IntoClause *into,
 						   ExplainState *es, const char *queryString,
 						   ParamListInfo params, QueryEnvironment *queryEnv,
 						   const instr_time *planduration,
diff --git a/src/include/executor/execdesc.h b/src/include/executor/execdesc.h
index d3a57242844..3a2169c9613 100644
--- a/src/include/executor/execdesc.h
+++ b/src/include/executor/execdesc.h
@@ -43,7 +43,7 @@ typedef struct QueryDesc
 	QueryEnvironment *queryEnv; /* query environment passed in */
 	int			instrument_options; /* OR of InstrumentOption flags */
 
-	/* These fields are set by ExecutorStart */
+	/* These fields are set by ExecutorStart or ExecutorPrep */
 	TupleDesc	tupDesc;		/* descriptor for result tuples */
 	EState	   *estate;			/* executor's query-wide state */
 	PlanState  *planstate;		/* tree of per-plan-node state */
@@ -63,7 +63,8 @@ extern QueryDesc *CreateQueryDesc(PlannedStmt *plannedstmt,
 								  DestReceiver *dest,
 								  ParamListInfo params,
 								  QueryEnvironment *queryEnv,
-								  int instrument_options);
+								  int instrument_options,
+								  EState *prep_estate);
 
 extern void FreeQueryDesc(QueryDesc *qdesc);
 
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 55a7d930d26..f7f922bfaa3 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -20,6 +20,7 @@
 #include "nodes/lockoptions.h"
 #include "nodes/parsenodes.h"
 #include "utils/memutils.h"
+#include "utils/resowner.h"
 
 
 /*
@@ -234,6 +235,31 @@ ExecGetJunkAttribute(TupleTableSlot *slot, AttrNumber attno, bool *isNull)
  */
 extern void ExecutorStart(QueryDesc *queryDesc, int eflags);
 extern void standard_ExecutorStart(QueryDesc *queryDesc, int eflags);
+
+extern EState *ExecutorPrep(PlannedStmt *pstmt,
+							ParamListInfo params,
+							ResourceOwner owner,
+							bool do_initial_pruning,
+							int eflags);
+
+/*
+ * Walk a prep_estates list in step with a parallel stmt_list iteration.
+ * Returns the next EState (or NULL) and advances *lc.  Safe when
+ * prep_estates is NIL; just pass list_head(NIL) which is NULL.
+ */
+static inline EState *
+next_prep_estate(List *prep_estates, ListCell **lc)
+{
+	EState *result = NULL;
+
+	if (*lc != NULL)
+	{
+		result = (EState *) lfirst(*lc);
+		*lc = lnext(prep_estates, *lc);
+	}
+	return result;
+}
+
 extern void ExecutorRun(QueryDesc *queryDesc,
 						ScanDirection direction, uint64 count);
 extern void standard_ExecutorRun(QueryDesc *queryDesc,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index f8053d9e572..70acfe3ad90 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -774,7 +774,6 @@ typedef struct EState
 	List	   *es_insert_pending_modifytables;
 } EState;
 
-
 /*
  * ExecRowMark -
  *	   runtime representation of FOR [KEY] UPDATE/SHARE clauses
diff --git a/src/include/utils/portal.h b/src/include/utils/portal.h
index a7bedb12c18..f69b4b9b479 100644
--- a/src/include/utils/portal.h
+++ b/src/include/utils/portal.h
@@ -137,6 +137,7 @@ typedef struct PortalData
 	CommandTag	commandTag;		/* command tag for original query */
 	QueryCompletion qc;			/* command completion data for executed query */
 	List	   *stmts;			/* list of PlannedStmts */
+	List	   *prep_estates;	/* list of EStates where needed */
 	CachedPlan *cplan;			/* CachedPlan, if stmts are from one */
 
 	ParamListInfo portalParams; /* params to pass to query */
@@ -240,6 +241,7 @@ extern void PortalDefineQuery(Portal portal,
 							  const char *sourceText,
 							  CommandTag commandTag,
 							  List *stmts,
+							  List *prep_estates,
 							  CachedPlan *cplan);
 extern PlannedStmt *PortalGetPrimaryStmt(Portal portal);
 extern void PortalCreateHoldStore(Portal portal);
-- 
2.47.3

