From c22a500ee764ce1b25d1f6d9a05f9020a36cb23f Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <akorotkov@postgresql.org>
Date: Sun, 5 Apr 2026 03:53:13 +0300
Subject: [PATCH v20 4/6] Extract common Append/MergeAppend executor logic into
 execAppend.c

Extract shared non-async executor operations for Append and MergeAppend
nodes into a new execAppend.c file, reducing code duplication.

The extracted functions operate on the common AppendBaseState base type
introduced in the previous commit:

  - ExecInitAppendBase(): shared subplan initialization, partition pruning
    setup, and result tuple slot creation.
  - ExecEndAppendBase(): shut down all subplan nodes.
  - ExecReScanAppendBase(): propagate rescan to subplans and reset pruning.

Async subplan detection, setup, and execution remain in nodeAppend.c,
since MergeAppend does not yet support async.  The tuple-fetching logic
also remains specific to each node type, preserving their distinct
execution semantics (sequential iteration for Append, binary heap merge
for MergeAppend).

Discussion: https://postgr.es/m/59be194c5a409fb9fc9f2031581b8a44%40postgrespro.ru
Author: Matheus Alcantara <matheusssilv97@gmail.com>
Co-authored-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Alexander Pyhalov <a.pyhalov@postgrespro.ru>
Reviewed-by: Alena Rybakina <a.rybakina@postgrespro.ru>
---
 src/backend/executor/Makefile          |   1 +
 src/backend/executor/execAppend.c      | 208 ++++++++++++++++++++++++
 src/backend/executor/meson.build       |   1 +
 src/backend/executor/nodeAppend.c      | 216 ++++---------------------
 src/backend/executor/nodeMergeAppend.c | 151 ++---------------
 src/include/executor/execAppend.h      |  26 +++
 6 files changed, 282 insertions(+), 321 deletions(-)
 create mode 100644 src/backend/executor/execAppend.c
 create mode 100644 src/include/executor/execAppend.h

diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefile
index 11118d0ce02..2b12a1eb17e 100644
--- a/src/backend/executor/Makefile
+++ b/src/backend/executor/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	execAmi.o \
+	execAppend.o \
 	execAsync.o \
 	execCurrent.o \
 	execExpr.o \
diff --git a/src/backend/executor/execAppend.c b/src/backend/executor/execAppend.c
new file mode 100644
index 00000000000..9599d10a952
--- /dev/null
+++ b/src/backend/executor/execAppend.c
@@ -0,0 +1,208 @@
+/*-------------------------------------------------------------------------
+ *
+ * execAppend.c
+ *	  This code provides support functions for executing MergeAppend and
+ *	  Append nodes.
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/backend/executor/execAppend.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "executor/execAppend.h"
+#include "executor/execPartition.h"
+#include "executor/executor.h"
+
+/*  Begin all of the subscans of an AppendBase node. */
+void
+ExecInitAppendBase(AppendBaseState *state,
+				   AppendBase *node,
+				   EState *estate,
+				   int eflags,
+				   int first_partial_plan,
+				   int *first_valid_partial_plan)
+{
+	PlanState **appendplanstates;
+	const TupleTableSlotOps *appendops;
+	Bitmapset  *validsubplans;
+	int			nplans;
+	int			firstvalid;
+	int			i,
+				j;
+
+	/* If run-time partition pruning is enabled, then set that up now */
+	if (node->part_prune_index >= 0)
+	{
+		PartitionPruneState *prunestate;
+
+		/*
+		 * Set up pruning data structure.  This also initializes the set of
+		 * subplans to initialize (validsubplans) by taking into account the
+		 * result of performing initial pruning if any.
+		 */
+		prunestate = ExecInitPartitionExecPruning(&state->ps,
+												  list_length(node->subplans),
+												  node->part_prune_index,
+												  node->apprelids,
+												  &validsubplans);
+		state->prune_state = prunestate;
+		nplans = bms_num_members(validsubplans);
+
+		/*
+		 * When no run-time pruning is required and there's at least one
+		 * subplan, we can fill valid_subplans immediately, preventing later
+		 * calls to ExecFindMatchingSubPlans.
+		 */
+		if (!prunestate->do_exec_prune && nplans > 0)
+		{
+			state->valid_subplans = bms_add_range(NULL, 0, nplans - 1);
+			state->valid_subplans_identified = true;
+		}
+	}
+	else
+	{
+		nplans = list_length(node->subplans);
+
+		/*
+		 * When run-time partition pruning is not enabled we can just mark all
+		 * subplans as valid; they must also all be initialized.
+		 */
+		Assert(nplans > 0);
+		state->valid_subplans = validsubplans =
+			bms_add_range(NULL, 0, nplans - 1);
+		state->valid_subplans_identified = true;
+		state->prune_state = NULL;
+	}
+
+	appendplanstates = palloc0_array(PlanState *, nplans);
+
+	/*
+	 * call ExecInitNode on each of the valid plans to be executed and save
+	 * the results into the appendplanstates array.
+	 *
+	 * While at it, find out the first valid partial plan.
+	 */
+	j = 0;
+	firstvalid = nplans;
+	i = -1;
+	while ((i = bms_next_member(validsubplans, i)) >= 0)
+	{
+		Plan	   *initNode = (Plan *) list_nth(node->subplans, i);
+
+		/*
+		 * Record the lowest appendplans index which is a valid partial plan.
+		 */
+		if (i >= first_partial_plan && j < firstvalid)
+			firstvalid = j;
+
+		appendplanstates[j++] = ExecInitNode(initNode, estate, eflags);
+	}
+
+	if (first_valid_partial_plan)
+		*first_valid_partial_plan = firstvalid;
+
+	state->plans = appendplanstates;
+	state->nplans = nplans;
+
+	/*
+	 * Initialize Append's result tuple type and slot.  If the child plans all
+	 * produce the same fixed slot type, we can use that slot type; otherwise
+	 * make a virtual slot.  (Note that the result slot itself is used only to
+	 * return a null tuple at end of execution; real tuples are returned to
+	 * the caller in the children's own result slots.  What we are doing here
+	 * is allowing the parent plan node to optimize if the Append will return
+	 * only one kind of slot.)
+	 */
+	appendops = ExecGetCommonSlotOps(appendplanstates, j);
+	if (appendops != NULL)
+	{
+		ExecInitResultTupleSlotTL(&state->ps, appendops);
+	}
+	else
+	{
+		ExecInitResultTupleSlotTL(&state->ps, &TTSOpsVirtual);
+		/* show that the output slot type is not fixed */
+		state->ps.resultopsset = true;
+		state->ps.resultopsfixed = false;
+	}
+
+	/* Initialize async state to safe defaults */
+	state->asyncplans = NULL;
+	state->nasyncplans = 0;
+	state->asyncrequests = NULL;
+	state->asyncresults = NULL;
+	state->needrequest = NULL;
+	state->eventset = NULL;
+	state->valid_asyncplans = NULL;
+
+	/*
+	 * Miscellaneous initialization
+	 */
+	state->ps.ps_ProjInfo = NULL;
+}
+
+void
+ExecReScanAppendBase(AppendBaseState *node)
+{
+	int			i;
+
+	/*
+	 * If any PARAM_EXEC Params used in pruning expressions have changed, then
+	 * we'd better unset the valid subplans so that they are reselected for
+	 * the new parameter values.
+	 */
+	if (node->prune_state &&
+		bms_overlap(node->ps.chgParam,
+					node->prune_state->execparamids))
+	{
+		node->valid_subplans_identified = false;
+		bms_free(node->valid_subplans);
+		node->valid_subplans = NULL;
+		bms_free(node->valid_asyncplans);
+		node->valid_asyncplans = NULL;
+	}
+
+	for (i = 0; i < node->nplans; i++)
+	{
+		PlanState  *subnode = node->plans[i];
+
+		/*
+		 * ExecReScan doesn't know about my subplans, so I have to do
+		 * changed-parameter signaling myself.
+		 */
+		if (node->ps.chgParam != NULL)
+			UpdateChangedParamSet(subnode, node->ps.chgParam);
+
+		/*
+		 * If chgParam of subnode is not null then plan will be re-scanned by
+		 * first ExecProcNode.
+		 */
+		if (subnode->chgParam == NULL)
+			ExecReScan(subnode);
+	}
+}
+
+/*  Shuts down the subplans of an AppendBase node. */
+void
+ExecEndAppendBase(AppendBaseState *node)
+{
+	PlanState **subplans;
+	int			nplans;
+	int			i;
+
+	/*
+	 * get information from the node
+	 */
+	subplans = node->plans;
+	nplans = node->nplans;
+
+	/*
+	 * shut down each of the subscans
+	 */
+	for (i = 0; i < nplans; i++)
+		ExecEndNode(subplans[i]);
+}
diff --git a/src/backend/executor/meson.build b/src/backend/executor/meson.build
index dc45be0b2ce..c2f261ff22d 100644
--- a/src/backend/executor/meson.build
+++ b/src/backend/executor/meson.build
@@ -2,6 +2,7 @@
 
 backend_sources += files(
   'execAmi.c',
+  'execAppend.c',
   'execAsync.c',
   'execCurrent.c',
   'execExpr.c',
diff --git a/src/backend/executor/nodeAppend.c b/src/backend/executor/nodeAppend.c
index f7044b6f28f..a9b23da3ad2 100644
--- a/src/backend/executor/nodeAppend.c
+++ b/src/backend/executor/nodeAppend.c
@@ -58,6 +58,7 @@
 
 #include "postgres.h"
 
+#include "executor/execAppend.h"
 #include "executor/execAsync.h"
 #include "executor/execPartition.h"
 #include "executor/executor.h"
@@ -113,15 +114,10 @@ AppendState *
 ExecInitAppend(Append *node, EState *estate, int eflags)
 {
 	AppendState *appendstate = makeNode(AppendState);
-	PlanState **appendplanstates;
-	const TupleTableSlotOps *appendops;
-	Bitmapset  *validsubplans;
 	Bitmapset  *asyncplans;
-	int			nplans;
 	int			nasyncplans;
-	int			firstvalid;
-	int			i,
-				j;
+	int			nplans;
+	int			i;
 
 	/* check for unsupported flags */
 	Assert(!(eflags & EXEC_FLAG_MARK));
@@ -138,124 +134,38 @@ ExecInitAppend(Append *node, EState *estate, int eflags)
 	appendstate->as_syncdone = false;
 	appendstate->as_begun = false;
 
-	/* If run-time partition pruning is enabled, then set that up now */
-	if (node->ab.part_prune_index >= 0)
-	{
-		PartitionPruneState *prunestate;
-
-		/*
-		 * Set up pruning data structure.  This also initializes the set of
-		 * subplans to initialize (validsubplans) by taking into account the
-		 * result of performing initial pruning if any.
-		 */
-		prunestate = ExecInitPartitionExecPruning(&appendstate->as.ps,
-												  list_length(node->ab.subplans),
-												  node->ab.part_prune_index,
-												  node->ab.apprelids,
-												  &validsubplans);
-		appendstate->as.prune_state = prunestate;
-		nplans = bms_num_members(validsubplans);
-
-		/*
-		 * When no run-time pruning is required and there's at least one
-		 * subplan, we can fill as_valid_subplans immediately, preventing
-		 * later calls to ExecFindMatchingSubPlans.
-		 */
-		if (!prunestate->do_exec_prune && nplans > 0)
-		{
-			appendstate->as.valid_subplans = bms_add_range(NULL, 0, nplans - 1);
-			appendstate->as.valid_subplans_identified = true;
-		}
-	}
-	else
-	{
-		nplans = list_length(node->ab.subplans);
-
-		/*
-		 * When run-time partition pruning is not enabled we can just mark all
-		 * subplans as valid; they must also all be initialized.
-		 */
-		Assert(nplans > 0);
-		appendstate->as.valid_subplans = validsubplans =
-			bms_add_range(NULL, 0, nplans - 1);
-		appendstate->as.valid_subplans_identified = true;
-		appendstate->as.prune_state = NULL;
-	}
+	/* Initialize common fields */
+	ExecInitAppendBase(&appendstate->as,
+					   &node->ab,
+					   estate,
+					   eflags,
+					   node->first_partial_plan,
+					   &appendstate->as_first_partial_plan);
 
-	appendplanstates = (PlanState **) palloc(nplans *
-											 sizeof(PlanState *));
+	nplans = appendstate->as.nplans;
 
 	/*
-	 * call ExecInitNode on each of the valid plans to be executed and save
-	 * the results into the appendplanstates array.
-	 *
-	 * While at it, find out the first valid partial plan.
+	 * Detect async-capable subplans.  When executing EvalPlanQual, we treat
+	 * them as sync ones; don't do this when initializing an EvalPlanQual plan
+	 * tree.
 	 */
-	j = 0;
 	asyncplans = NULL;
 	nasyncplans = 0;
-	firstvalid = nplans;
-	i = -1;
-	while ((i = bms_next_member(validsubplans, i)) >= 0)
+	for (i = 0; i < nplans; i++)
 	{
-		Plan	   *initNode = (Plan *) list_nth(node->ab.subplans, i);
-
-		/*
-		 * Record async subplans.  When executing EvalPlanQual, we treat them
-		 * as sync ones; don't do this when initializing an EvalPlanQual plan
-		 * tree.
-		 */
-		if (initNode->async_capable && estate->es_epq_active == NULL)
+		if (appendstate->as.plans[i]->plan->async_capable &&
+			estate->es_epq_active == NULL)
 		{
-			asyncplans = bms_add_member(asyncplans, j);
+			asyncplans = bms_add_member(asyncplans, i);
 			nasyncplans++;
 		}
-
-		/*
-		 * Record the lowest appendplans index which is a valid partial plan.
-		 */
-		if (i >= node->first_partial_plan && j < firstvalid)
-			firstvalid = j;
-
-		appendplanstates[j++] = ExecInitNode(initNode, estate, eflags);
-	}
-
-	appendstate->as_first_partial_plan = firstvalid;
-	appendstate->as.plans = appendplanstates;
-	appendstate->as.nplans = nplans;
-
-	/*
-	 * Initialize Append's result tuple type and slot.  If the child plans all
-	 * produce the same fixed slot type, we can use that slot type; otherwise
-	 * make a virtual slot.  (Note that the result slot itself is used only to
-	 * return a null tuple at end of execution; real tuples are returned to
-	 * the caller in the children's own result slots.  What we are doing here
-	 * is allowing the parent plan node to optimize if the Append will return
-	 * only one kind of slot.)
-	 */
-	appendops = ExecGetCommonSlotOps(appendplanstates, j);
-	if (appendops != NULL)
-	{
-		ExecInitResultTupleSlotTL(&appendstate->as.ps, appendops);
-	}
-	else
-	{
-		ExecInitResultTupleSlotTL(&appendstate->as.ps, &TTSOpsVirtual);
-		/* show that the output slot type is not fixed */
-		appendstate->as.ps.resultopsset = true;
-		appendstate->as.ps.resultopsfixed = false;
 	}
 
 	/* Initialize async state */
 	appendstate->as.asyncplans = asyncplans;
 	appendstate->as.nasyncplans = nasyncplans;
-	appendstate->as.asyncrequests = NULL;
-	appendstate->as.asyncresults = NULL;
 	appendstate->as_nasyncresults = 0;
 	appendstate->as_nasyncremain = 0;
-	appendstate->as.needrequest = NULL;
-	appendstate->as.eventset = NULL;
-	appendstate->as.valid_asyncplans = NULL;
 
 	if (nasyncplans > 0)
 	{
@@ -269,7 +179,7 @@ ExecInitAppend(Append *node, EState *estate, int eflags)
 
 			areq = palloc_object(AsyncRequest);
 			areq->requestor = (PlanState *) appendstate;
-			areq->requestee = appendplanstates[i];
+			areq->requestee = appendstate->as.plans[i];
 			areq->request_index = i;
 			areq->callback_pending = false;
 			areq->request_complete = false;
@@ -285,12 +195,6 @@ ExecInitAppend(Append *node, EState *estate, int eflags)
 			classify_matching_subplans(appendstate);
 	}
 
-	/*
-	 * Miscellaneous initialization
-	 */
-
-	appendstate->as.ps.ps_ProjInfo = NULL;
-
 	/* For parallel query, this will be overridden later. */
 	appendstate->choose_next_subplan = choose_next_subplan_locally;
 
@@ -404,70 +308,24 @@ ExecAppend(PlanState *pstate)
 void
 ExecEndAppend(AppendState *node)
 {
-	PlanState **appendplans;
-	int			nplans;
-	int			i;
-
-	/*
-	 * get information from the node
-	 */
-	appendplans = node->as.plans;
-	nplans = node->as.nplans;
-
-	/*
-	 * shut down each of the subscans
-	 */
-	for (i = 0; i < nplans; i++)
-		ExecEndNode(appendplans[i]);
+	ExecEndAppendBase(&node->as);
 }
 
 void
 ExecReScanAppend(AppendState *node)
 {
 	int			nasyncplans = node->as.nasyncplans;
-	int			i;
 
 	/* Process asynchronous requests still pending */
 	ExecAppendAsyncProcessPending(node);
 
-	/*
-	 * If any PARAM_EXEC Params used in pruning expressions have changed, then
-	 * we'd better unset the valid subplans so that they are reselected for
-	 * the new parameter values.
-	 */
-	if (node->as.prune_state &&
-		bms_overlap(node->as.ps.chgParam,
-					node->as.prune_state->execparamids))
-	{
-		node->as.valid_subplans_identified = false;
-		bms_free(node->as.valid_subplans);
-		node->as.valid_subplans = NULL;
-		bms_free(node->as.valid_asyncplans);
-		node->as.valid_asyncplans = NULL;
-	}
-
-	for (i = 0; i < node->as.nplans; i++)
-	{
-		PlanState  *subnode = node->as.plans[i];
-
-		/*
-		 * ExecReScan doesn't know about my subplans, so I have to do
-		 * changed-parameter signaling myself.
-		 */
-		if (node->as.ps.chgParam != NULL)
-			UpdateChangedParamSet(subnode, node->as.ps.chgParam);
-
-		/*
-		 * If chgParam of subnode is not null then plan will be re-scanned by
-		 * first ExecProcNode or by first ExecAsyncRequest.
-		 */
-		if (subnode->chgParam == NULL)
-			ExecReScan(subnode);
-	}
+	ExecReScanAppendBase(&node->as);
 
 	/* Reset async state */
 	if (nasyncplans > 0)
 	{
+		int			i;
+
 		i = -1;
 		while ((i = bms_next_member(node->as.asyncplans, i)) >= 0)
 		{
@@ -883,17 +741,6 @@ mark_invalid_subplans_as_finished(AppendState *node)
 static void
 ExecAppendAsyncBegin(AppendState *node)
 {
-	int			i;
-
-	/* Backward scan is not supported by async-aware Appends. */
-	Assert(ScanDirectionIsForward(node->as.ps.state->es_direction));
-
-	/* We should never be called when there are no subplans */
-	Assert(node->as.nplans > 0);
-
-	/* We should never be called when there are no async subplans. */
-	Assert(node->as.nasyncplans > 0);
-
 	/* If we've yet to determine the valid subplans then do so now. */
 	if (!node->as.valid_subplans_identified)
 	{
@@ -913,16 +760,19 @@ ExecAppendAsyncBegin(AppendState *node)
 		return;
 
 	/* Make a request for each of the valid async subplans. */
-	i = -1;
-	while ((i = bms_next_member(node->as.valid_asyncplans, i)) >= 0)
 	{
-		AsyncRequest *areq = node->as.asyncrequests[i];
+		int			i = -1;
 
-		Assert(areq->request_index == i);
-		Assert(!areq->callback_pending);
+		while ((i = bms_next_member(node->as.valid_asyncplans, i)) >= 0)
+		{
+			AsyncRequest *areq = node->as.asyncrequests[i];
 
-		/* Do the actual work. */
-		ExecAsyncRequest(areq);
+			Assert(areq->request_index == i);
+			Assert(!areq->callback_pending);
+
+			/* Do the actual work. */
+			ExecAsyncRequest(areq);
+		}
 	}
 }
 
diff --git a/src/backend/executor/nodeMergeAppend.c b/src/backend/executor/nodeMergeAppend.c
index e4cdbcc5eb2..2575d6a499f 100644
--- a/src/backend/executor/nodeMergeAppend.c
+++ b/src/backend/executor/nodeMergeAppend.c
@@ -39,6 +39,7 @@
 
 #include "postgres.h"
 
+#include "executor/execAppend.h"
 #include "executor/executor.h"
 #include "executor/execPartition.h"
 #include "executor/nodeMergeAppend.h"
@@ -67,12 +68,7 @@ MergeAppendState *
 ExecInitMergeAppend(MergeAppend *node, EState *estate, int eflags)
 {
 	MergeAppendState *mergestate = makeNode(MergeAppendState);
-	PlanState **mergeplanstates;
-	const TupleTableSlotOps *mergeops;
-	Bitmapset  *validsubplans;
-	int			nplans;
-	int			i,
-				j;
+	int			i;
 
 	/* check for unsupported flags */
 	Assert(!(eflags & (EXEC_FLAG_BACKWARD | EXEC_FLAG_MARK)));
@@ -84,94 +80,18 @@ ExecInitMergeAppend(MergeAppend *node, EState *estate, int eflags)
 	mergestate->as.ps.state = estate;
 	mergestate->as.ps.ExecProcNode = ExecMergeAppend;
 
-	/* If run-time partition pruning is enabled, then set that up now */
-	if (node->ab.part_prune_index >= 0)
-	{
-		PartitionPruneState *prunestate;
-
-		/*
-		 * Set up pruning data structure.  This also initializes the set of
-		 * subplans to initialize (validsubplans) by taking into account the
-		 * result of performing initial pruning if any.
-		 */
-		prunestate = ExecInitPartitionExecPruning(&mergestate->as.ps,
-												  list_length(node->ab.subplans),
-												  node->ab.part_prune_index,
-												  node->ab.apprelids,
-												  &validsubplans);
-		mergestate->as.prune_state = prunestate;
-		nplans = bms_num_members(validsubplans);
-
-		/*
-		 * When no run-time pruning is required and there's at least one
-		 * subplan, we can fill ms_valid_subplans immediately, preventing
-		 * later calls to ExecFindMatchingSubPlans.
-		 */
-		if (!prunestate->do_exec_prune && nplans > 0)
-			mergestate->as.valid_subplans = bms_add_range(NULL, 0, nplans - 1);
-	}
-	else
-	{
-		nplans = list_length(node->ab.subplans);
+	/* Initialize common fields */
+	ExecInitAppendBase(&mergestate->as,
+					   &node->ab,
+					   estate,
+					   eflags,
+					   -1,
+					   NULL);
 
-		/*
-		 * When run-time partition pruning is not enabled we can just mark all
-		 * subplans as valid; they must also all be initialized.
-		 */
-		Assert(nplans > 0);
-		mergestate->as.valid_subplans = validsubplans =
-			bms_add_range(NULL, 0, nplans - 1);
-		mergestate->as.prune_state = NULL;
-	}
-
-	mergeplanstates = palloc_array(PlanState *, nplans);
-	mergestate->as.plans = mergeplanstates;
-	mergestate->as.nplans = nplans;
-
-	mergestate->ms_slots = palloc0_array(TupleTableSlot *, nplans);
-	mergestate->ms_heap = binaryheap_allocate(nplans, heap_compare_slots,
+	mergestate->ms_slots = palloc0_array(TupleTableSlot *, mergestate->as.nplans);
+	mergestate->ms_heap = binaryheap_allocate(mergestate->as.nplans, heap_compare_slots,
 											  mergestate);
 
-	/*
-	 * call ExecInitNode on each of the valid plans to be executed and save
-	 * the results into the mergeplanstates array.
-	 */
-	j = 0;
-	i = -1;
-	while ((i = bms_next_member(validsubplans, i)) >= 0)
-	{
-		Plan	   *initNode = (Plan *) list_nth(node->ab.subplans, i);
-
-		mergeplanstates[j++] = ExecInitNode(initNode, estate, eflags);
-	}
-
-	/*
-	 * Initialize MergeAppend's result tuple type and slot.  If the child
-	 * plans all produce the same fixed slot type, we can use that slot type;
-	 * otherwise make a virtual slot.  (Note that the result slot itself is
-	 * used only to return a null tuple at end of execution; real tuples are
-	 * returned to the caller in the children's own result slots.  What we are
-	 * doing here is allowing the parent plan node to optimize if the
-	 * MergeAppend will return only one kind of slot.)
-	 */
-	mergeops = ExecGetCommonSlotOps(mergeplanstates, j);
-	if (mergeops != NULL)
-	{
-		ExecInitResultTupleSlotTL(&mergestate->as.ps, mergeops);
-	}
-	else
-	{
-		ExecInitResultTupleSlotTL(&mergestate->as.ps, &TTSOpsVirtual);
-		/* show that the output slot type is not fixed */
-		mergestate->as.ps.resultopsset = true;
-		mergestate->as.ps.resultopsfixed = false;
-	}
-
-	/*
-	 * Miscellaneous initialization
-	 */
-	mergestate->as.ps.ps_ProjInfo = NULL;
-
 	/*
 	 * initialize sort-key information
 	 */
@@ -336,59 +256,14 @@ heap_compare_slots(Datum a, Datum b, void *arg)
 void
 ExecEndMergeAppend(MergeAppendState *node)
 {
-	PlanState **mergeplans;
-	int			nplans;
-	int			i;
-
-	/*
-	 * get information from the node
-	 */
-	mergeplans = node->as.plans;
-	nplans = node->as.nplans;
-
-	/*
-	 * shut down each of the subscans
-	 */
-	for (i = 0; i < nplans; i++)
-		ExecEndNode(mergeplans[i]);
+	ExecEndAppendBase(&node->as);
 }
 
 void
 ExecReScanMergeAppend(MergeAppendState *node)
 {
-	int			i;
+	ExecReScanAppendBase(&node->as);
 
-	/*
-	 * If any PARAM_EXEC Params used in pruning expressions have changed, then
-	 * we'd better unset the valid subplans so that they are reselected for
-	 * the new parameter values.
-	 */
-	if (node->as.prune_state &&
-		bms_overlap(node->as.ps.chgParam,
-					node->as.prune_state->execparamids))
-	{
-		bms_free(node->as.valid_subplans);
-		node->as.valid_subplans = NULL;
-	}
-
-	for (i = 0; i < node->as.nplans; i++)
-	{
-		PlanState  *subnode = node->as.plans[i];
-
-		/*
-		 * ExecReScan doesn't know about my subplans, so I have to do
-		 * changed-parameter signaling myself.
-		 */
-		if (node->as.ps.chgParam != NULL)
-			UpdateChangedParamSet(subnode, node->as.ps.chgParam);
-
-		/*
-		 * If chgParam of subnode is not null then plan will be re-scanned by
-		 * first ExecProcNode.
-		 */
-		if (subnode->chgParam == NULL)
-			ExecReScan(subnode);
-	}
 	binaryheap_reset(node->ms_heap);
 	node->ms_initialized = false;
 }
diff --git a/src/include/executor/execAppend.h b/src/include/executor/execAppend.h
new file mode 100644
index 00000000000..a8f41bad921
--- /dev/null
+++ b/src/include/executor/execAppend.h
@@ -0,0 +1,26 @@
+/*-------------------------------------------------------------------------
+ * execAppend.h
+ *		Support functions for MergeAppend and Append nodes.
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/include/executor/execAppend.h
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef EXECAPPEND_H
+#define EXECAPPEND_H
+
+#include "nodes/execnodes.h"
+
+extern void ExecInitAppendBase(AppendBaseState *state,
+							   AppendBase *node,
+							   EState *estate,
+							   int eflags,
+							   int first_partial_plan,
+							   int *first_valid_partial_plan);
+extern void ExecEndAppendBase(AppendBaseState *node);
+extern void ExecReScanAppendBase(AppendBaseState *node);
+
+#endif							/* EXECAPPEND_H */
-- 
2.50.1 (Apple Git-155)

