From f6e63d4b8b2ec5ff011c87f79f3efc8abe921263 Mon Sep 17 00:00:00 2001 From: Alexander Korotkov Date: Thu, 5 Mar 2026 19:47:20 +0200 Subject: [PATCH] Fix handling of updated tuples in the MERGE statement This branch missed the IsolationUsesXactSnapshot() check. That led to EPQ on repeatable read and serializable isolation levels. This commit fixes the issue and provides a simple isolation check for that. Backpatch through v15 where MERGE statement was introduced. Reported-by: Tender Wang Discussion: https://postgr.es/m/CAPpHfdvzZSaNYdj5ac-tYRi6MuuZnYHiUkZ3D-AoY-ny8v%2BS%2Bw%40mail.gmail.com Author: Tender Wang Reviewed-by: Dean Rasheed Backpatch-through: 15 --- src/backend/executor/nodeModifyTable.c | 5 ++++ src/test/isolation/expected/merge-update.out | 25 ++++++++++++++++++++ src/test/isolation/specs/merge-update.spec | 2 ++ 3 files changed, 32 insertions(+) diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index 0d8a66aa9f0..2005674838d 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -3077,6 +3077,11 @@ lmerge_matched: *inputslot; LockTupleMode lockmode; + if (IsolationUsesXactSnapshot()) + ereport(ERROR, + (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), + errmsg("could not serialize access due to concurrent update"))); + /* * The target tuple was concurrently updated by some other * transaction. Run EvalPlanQual() with the new version of diff --git a/src/test/isolation/expected/merge-update.out b/src/test/isolation/expected/merge-update.out index f5f7e3ba19b..5170f6ee019 100644 --- a/src/test/isolation/expected/merge-update.out +++ b/src/test/isolation/expected/merge-update.out @@ -355,3 +355,28 @@ step c1: COMMIT; step pa_merge2c_dup: <... completed> ERROR: MERGE command cannot affect row a second time step a2: ABORT; + +starting permutation: merge2a c1 s1beginrr merge1 c2 +step merge2a: + MERGE INTO target t + USING (SELECT 1 as key, 'merge2a' as val) s + ON s.key = t.key + WHEN NOT MATCHED THEN + INSERT VALUES (s.key, s.val) + WHEN MATCHED THEN + UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val; + +step c1: COMMIT; +step s1beginrr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step merge1: + MERGE INTO target t + USING (SELECT 1 as key, 'merge1' as val) s + ON s.key = t.key + WHEN NOT MATCHED THEN + INSERT VALUES (s.key, s.val) + WHEN MATCHED THEN + UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val; + +step c2: COMMIT; +step merge1: <... completed> +ERROR: could not serialize access due to concurrent update diff --git a/src/test/isolation/specs/merge-update.spec b/src/test/isolation/specs/merge-update.spec index 3ccd4664498..293cdb46b56 100644 --- a/src/test/isolation/specs/merge-update.spec +++ b/src/test/isolation/specs/merge-update.spec @@ -78,6 +78,7 @@ step "pa_merge3" } step "c1" { COMMIT; } step "a1" { ABORT; } +step "s1beginrr" { BEGIN ISOLATION LEVEL REPEATABLE READ; } session "s2" setup @@ -167,3 +168,4 @@ permutation "pa_merge2" "c1" "pa_merge2a" "pa_select2" "c2" # succeeds permutation "pa_merge3" "pa_merge2b_when" "c1" "pa_select2" "c2" # WHEN not satisfied by updated tuple permutation "pa_merge1" "pa_merge2b_when" "c1" "pa_select2" "c2" # WHEN satisfied by updated tuple permutation "pa_merge1" "pa_merge2c_dup" "c1" "a2" +permutation "merge2a" "c1" "s1beginrr" "merge1" "c2"