diff --git a/src/backend/executor/nodeAppend.c b/src/backend/executor/nodeAppend.c
index c7059e7528..113217e607 100644
--- a/src/backend/executor/nodeAppend.c
+++ b/src/backend/executor/nodeAppend.c
@@ -288,8 +288,65 @@ static TupleTableSlot *
 ExecAppend(PlanState *pstate)
 {
 	AppendState *node = castNode(AppendState, pstate);
+	EState	   *estate = node->ps.state;
 	TupleTableSlot *result;
 
+	if (estate->es_epq_active != NULL)
+	{
+		/*
+		 * We are inside an EvalPlanQual recheck.  If there is a relevant
+		 * rowmark for the append relation, return the test tuple if one is
+		 * available.
+		 */
+		EPQState   *epqstate = estate->es_epq_active;
+		int			scanrelid;
+
+		if (bms_get_singleton_member(castNode(Append, node->ps.plan)->apprelids,
+									 &scanrelid))
+		{
+			if (epqstate->relsubs_done[scanrelid - 1])
+			{
+				/*
+				 * Return empty slot, as either there is no EPQ tuple for this
+				 * rel or we already returned it.
+				 */
+				TupleTableSlot *slot = node->ps.ps_ResultTupleSlot;
+
+				return ExecClearTuple(slot);
+			}
+			else if (epqstate->relsubs_slot[scanrelid - 1] != NULL)
+			{
+				/*
+				 * Return replacement tuple provided by the EPQ caller.
+				 */
+				TupleTableSlot *slot = epqstate->relsubs_slot[scanrelid - 1];
+
+				Assert(epqstate->relsubs_rowmark[scanrelid - 1] == NULL);
+
+				/* Mark to remember that we shouldn't return it again */
+				epqstate->relsubs_done[scanrelid - 1] = true;
+
+				return slot;
+			}
+			else if (epqstate->relsubs_rowmark[scanrelid - 1] != NULL)
+			{
+				/*
+				 * Fetch and return replacement tuple using a non-locking
+				 * rowmark.
+				 */
+				TupleTableSlot *slot = node->ps.ps_ResultTupleSlot;
+
+				/* Mark to remember that we shouldn't return more */
+				epqstate->relsubs_done[scanrelid - 1] = true;
+
+				if (!EvalPlanQualFetchRowMark(epqstate, scanrelid, slot))
+					return NULL;
+
+				return slot;
+			}
+		}
+	}
+
 	/*
 	 * If this is the first call after Init or ReScan, we need to do the
 	 * initialization work.
@@ -405,6 +462,7 @@ ExecEndAppend(AppendState *node)
 void
 ExecReScanAppend(AppendState *node)
 {
+	EState	   *estate = node->ps.state;
 	int			nasyncplans = node->as_nasyncplans;
 	int			i;
 
@@ -443,6 +501,23 @@ ExecReScanAppend(AppendState *node)
 			ExecReScan(subnode);
 	}
 
+	/*
+	 * Rescan EvalPlanQual tuple(s) if we're inside an EvalPlanQual recheck.
+	 * But don't lose the "blocked" status of blocked target relations.
+	 */
+	if (estate->es_epq_active != NULL)
+	{
+		EPQState   *epqstate = estate->es_epq_active;
+		int			scanrelid;
+
+		if (bms_get_singleton_member(castNode(Append, node->ps.plan)->apprelids,
+									 &scanrelid))
+		{
+			epqstate->relsubs_done[scanrelid - 1] =
+				epqstate->relsubs_blocked[scanrelid - 1];
+		}
+	}
+
 	/* Reset async state */
 	if (nasyncplans > 0)
 	{
diff --git a/src/backend/executor/nodeMergeAppend.c b/src/backend/executor/nodeMergeAppend.c
index 0817868452..f2c386b123 100644
--- a/src/backend/executor/nodeMergeAppend.c
+++ b/src/backend/executor/nodeMergeAppend.c
@@ -200,11 +200,68 @@ static TupleTableSlot *
 ExecMergeAppend(PlanState *pstate)
 {
 	MergeAppendState *node = castNode(MergeAppendState, pstate);
+	EState	   *estate = node->ps.state;
 	TupleTableSlot *result;
 	SlotNumber	i;
 
 	CHECK_FOR_INTERRUPTS();
 
+	if (estate->es_epq_active != NULL)
+	{
+		/*
+		 * We are inside an EvalPlanQual recheck.  If there is a relevant
+		 * rowmark for the append relation, return the test tuple if one is
+		 * available.
+		 */
+		EPQState   *epqstate = estate->es_epq_active;
+		int			scanrelid;
+
+		if (bms_get_singleton_member(castNode(MergeAppend, node->ps.plan)->apprelids,
+									 &scanrelid))
+		{
+			if (epqstate->relsubs_done[scanrelid - 1])
+			{
+				/*
+				 * Return empty slot, as either there is no EPQ tuple for this
+				 * rel or we already returned it.
+				 */
+				TupleTableSlot *slot = node->ps.ps_ResultTupleSlot;
+
+				return ExecClearTuple(slot);
+			}
+			else if (epqstate->relsubs_slot[scanrelid - 1] != NULL)
+			{
+				/*
+				 * Return replacement tuple provided by the EPQ caller.
+				 */
+				TupleTableSlot *slot = epqstate->relsubs_slot[scanrelid - 1];
+
+				Assert(epqstate->relsubs_rowmark[scanrelid - 1] == NULL);
+
+				/* Mark to remember that we shouldn't return it again */
+				epqstate->relsubs_done[scanrelid - 1] = true;
+
+				return slot;
+			}
+			else if (epqstate->relsubs_rowmark[scanrelid - 1] != NULL)
+			{
+				/*
+				 * Fetch and return replacement tuple using a non-locking
+				 * rowmark.
+				 */
+				TupleTableSlot *slot = node->ps.ps_ResultTupleSlot;
+
+				/* Mark to remember that we shouldn't return more */
+				epqstate->relsubs_done[scanrelid - 1] = true;
+
+				if (!EvalPlanQualFetchRowMark(epqstate, scanrelid, slot))
+					return NULL;
+
+				return slot;
+			}
+		}
+	}
+
 	if (!node->ms_initialized)
 	{
 		/* Nothing to do if all subplans were pruned */
@@ -339,6 +396,7 @@ ExecEndMergeAppend(MergeAppendState *node)
 void
 ExecReScanMergeAppend(MergeAppendState *node)
 {
+	EState	   *estate = node->ps.state;
 	int			i;
 
 	/*
@@ -372,6 +430,24 @@ ExecReScanMergeAppend(MergeAppendState *node)
 		if (subnode->chgParam == NULL)
 			ExecReScan(subnode);
 	}
+
+	/*
+	 * Rescan EvalPlanQual tuple(s) if we're inside an EvalPlanQual recheck.
+	 * But don't lose the "blocked" status of blocked target relations.
+	 */
+	if (estate->es_epq_active != NULL)
+	{
+		EPQState   *epqstate = estate->es_epq_active;
+		int			scanrelid;
+
+		if (bms_get_singleton_member(castNode(MergeAppend, node->ps.plan)->apprelids,
+									 &scanrelid))
+		{
+			epqstate->relsubs_done[scanrelid - 1] =
+				epqstate->relsubs_blocked[scanrelid - 1];
+		}
+	}
+
 	binaryheap_reset(node->ms_heap);
 	node->ms_initialized = false;
 }
