From e70de2b114bca5ad045ec64289e4d2575ff40c91 Mon Sep 17 00:00:00 2001 From: Andrey Rachitskiy 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 Reported-by: Vladimir Savin 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; + +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