From e70de2b114bca5ad045ec64289e4d2575ff40c91 Mon Sep 17 00:00:00 2001
From: Andrey Rachitskiy <pl0h0yp1@gmail.com>
Date: Thu, 1 Oct 2026 20:11:29 +0500
Subject: [PATCH] Don't rebuild exec partition prune state during EvalPlanQual.

EvalPlanQualStart() shares the parent's PartitionPruneState so that
EPQ initializes the same Append and MergeAppend subplans.  ExecInit
of those nodes called InitExecPartitionPruneContexts() again.  That
replaced exec prune ExprStates with ones allocated in the EPQ memory
context.  EvalPlanQualEnd() frees that context.

A later CTE that updates a partitioned table through Nested Loop plus
Append with run-time pruning then used the dangling ExprStates and
could SIGSEGV in partkey_datum_from_expr().

Skip InitExecPartitionPruneContexts() when es_epq_active is set.  A
case is added to eval-plan-qual.

Author: Andrey Rachitskiy <pl0h0yp1@gmail.com>
Reported-by: Vladimir Savin <vladimir@encord.com>
Discussion: https://www.postgresql.org/message-id/CAP2G_gGpV=rKMLuET4RW-qO41GvBvi6Czsjsj16eT9hAwHxG6w@mail.gmail.com
---
 src/backend/executor/execPartition.c          |  6 +++-
 .../isolation/expected/eval-plan-qual.out     | 25 ++++++++++++++
 src/test/isolation/specs/eval-plan-qual.spec  | 34 +++++++++++++++++++
 3 files changed, 64 insertions(+), 1 deletion(-)

diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 86fa0f0deaa..546708ce0be 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -2038,8 +2038,12 @@ ExecInitPartitionExecPruning(PlanState *planstate,
 	 * leave the maps to be in an invalid state, but that's ok since that data
 	 * won't be consulted again (cf initial Assert in
 	 * ExecFindMatchingSubPlans).
+	 *
+	 * EvalPlanQualStart() shares this state with the parent.  Exec prune
+	 * ExprStates must remain in the parent's context.  EvalPlanQualEnd()
+	 * destroys the EPQ estate.
 	 */
-	if (prunestate->do_exec_prune)
+	if (prunestate->do_exec_prune && estate->es_epq_active == NULL)
 		InitExecPartitionPruneContexts(prunestate, planstate,
 									   *initially_valid_subplans,
 									   n_total_subplans);
diff --git a/src/test/isolation/expected/eval-plan-qual.out b/src/test/isolation/expected/eval-plan-qual.out
index a122047fd2a..4f33000e05a 100644
--- a/src/test/isolation/expected/eval-plan-qual.out
+++ b/src/test/isolation/expected/eval-plan-qual.out
@@ -1474,6 +1474,31 @@ step c1: COMMIT;
 step s2pp4: <... completed>
 step c2: COMMIT;
 
+starting permutation: s1epqprune s2epqprune c1 c2
+step s1epqprune: UPDATE epq_lock SET n = n + 1 WHERE id = 1;
+step s2epqprune: 
+	SET LOCAL enable_hashjoin = off;
+	SET LOCAL enable_mergejoin = off;
+	SET LOCAL enable_seqscan = off;
+	WITH locked AS (
+	  SELECT * FROM epq_lock WHERE id = 1 FOR UPDATE
+	), upd AS (
+	  UPDATE part_epq SET n = n + 1 + (SELECT count(*) FROM locked)
+	  FROM (VALUES (1), (2)) v(id)
+	  WHERE part_epq.id = v.id
+	  RETURNING part_epq.id
+	)
+	SELECT count(*) FROM upd;
+ <waiting ...>
+step c1: COMMIT;
+step s2epqprune: <... completed>
+count
+-----
+    2
+(1 row)
+
+step c2: COMMIT;
+
 starting permutation: updateformergevalues mergevalues c1 c2 read
 step updateformergevalues: UPDATE accounts SET balance = balance + 100;
 step mergevalues: 
diff --git a/src/test/isolation/specs/eval-plan-qual.spec b/src/test/isolation/specs/eval-plan-qual.spec
index fb57fb237dd..ec9b80e3131 100644
--- a/src/test/isolation/specs/eval-plan-qual.spec
+++ b/src/test/isolation/specs/eval-plan-qual.spec
@@ -47,6 +47,13 @@ setup
  CREATE TABLE another_parttbl2 PARTITION OF another_parttbl FOR VALUES IN (2);
  INSERT INTO another_parttbl VALUES (1, 1, 1);
 
+ CREATE TABLE part_epq (id int PRIMARY KEY, n int) PARTITION BY LIST (id);
+ CREATE TABLE part_epq1 PARTITION OF part_epq FOR VALUES IN (1);
+ CREATE TABLE part_epq2 PARTITION OF part_epq FOR VALUES IN (2);
+ INSERT INTO part_epq VALUES (1, 0), (2, 0);
+ CREATE TABLE epq_lock (id int PRIMARY KEY, n int);
+ INSERT INTO epq_lock VALUES (1, 0);
+
  CREATE FUNCTION noisy_oper(p_comment text, p_a anynonarray, p_op text, p_b anynonarray)
  RETURNS bool LANGUAGE plpgsql AS $$
  DECLARE
@@ -67,6 +74,8 @@ teardown
  DROP TABLE table_a, table_b, jointest;
  DROP TABLE parttbl;
  DROP TABLE another_parttbl;
+ DROP TABLE part_epq;
+ DROP TABLE epq_lock;
  DROP FUNCTION noisy_oper(text, anynonarray, text, anynonarray)
 }
 
@@ -205,6 +214,7 @@ step sys1	{
 }
 
 step s1pp1 { UPDATE another_parttbl SET b = b + 1 WHERE a = 1; }
+step s1epqprune { UPDATE epq_lock SET n = n + 1 WHERE id = 1; }
 
 step updateformergevalues { UPDATE accounts SET balance = balance + 100; }
 
@@ -320,6 +330,27 @@ step s2pp2 { PREPARE epd AS DELETE FROM another_parttbl WHERE a = $1; }
 step s2pp3 { EXECUTE epd(1); }
 step s2pp4 { DELETE FROM another_parttbl WHERE a = (SELECT 1); }
 
+# Session 1 updates epq_lock, so the FOR UPDATE CTE does EvalPlanQual.
+# EvalPlanQualStart() initializes every planned subplan, not only the
+# EPQ target.  The sibling writable CTE UPDATEs part_epq through Nested
+# Loop plus Append with run-time pruning, and shares PartitionPruneState
+# with the parent.  EPQ must not rebuild those exec prune ExprStates.
+# Nested Loop still uses them after EvalPlanQualEnd().
+step s2epqprune {
+	SET LOCAL enable_hashjoin = off;
+	SET LOCAL enable_mergejoin = off;
+	SET LOCAL enable_seqscan = off;
+	WITH locked AS (
+	  SELECT * FROM epq_lock WHERE id = 1 FOR UPDATE
+	), upd AS (
+	  UPDATE part_epq SET n = n + 1 + (SELECT count(*) FROM locked)
+	  FROM (VALUES (1), (2)) v(id)
+	  WHERE part_epq.id = v.id
+	  RETURNING part_epq.id
+	)
+	SELECT count(*) FROM upd;
+}
+
 step mergevalues {
 	MERGE INTO accounts
 	USING (VALUES ('checking', 610), ('savings', 620)) v(accountid, balance)
@@ -436,5 +467,8 @@ permutation sys1 sysmerge2 c1 c2
 permutation s1pp1 s2pp1 s2pp2 s2pp3 c1 c2
 permutation s1pp1 s2pp4 c1 c2
 
+# EPQ of a FOR UPDATE CTE must not rebuild exec prune state of a sibling Append
+permutation s1epqprune s2epqprune c1 c2
+
 # test EPQ recheck in MERGE from VALUES_RTE, cf bug #19355
 permutation updateformergevalues mergevalues c1 c2 read
-- 
2.53.0
