From 28ef6c33f7a110fe976821573072f035193b4a01 Mon Sep 17 00:00:00 2001
From: Andrey Borodin <amborodin@acm.org>
Date: Sun, 23 Aug 2026 15:20:16 +0300
Subject: [PATCH 3/4] Test SSI conflict detection after summarization

When SSI exhausts its transaction array, it preserves old SIREAD locks
by folding them into OldCommittedSxact.  Conflict detection must continue
to prevent write skew after that transition, but no automated test forces
this path.

Add an injection point that summarizes the oldest committed serializable
transaction, and use it to exercise the same write-skew schedule before
and after summarization.
---
 src/backend/storage/lmgr/predicate.c          |  8 ++
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/serializable-summarization.out   | 98 +++++++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/serializable-summarization.spec     | 70 +++++++++++++
 5 files changed, 178 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/serializable-summarization.out
 create mode 100644 src/test/modules/injection_points/specs/serializable-summarization.spec

diff --git a/src/backend/storage/lmgr/predicate.c b/src/backend/storage/lmgr/predicate.c
index 457a51fecc6..666c2dfff18 100644
--- a/src/backend/storage/lmgr/predicate.c
+++ b/src/backend/storage/lmgr/predicate.c
@@ -210,6 +210,7 @@
 #include "storage/shmem.h"
 #include "storage/subsystems.h"
 #include "utils/guc_hooks.h"
+#include "utils/injection_point.h"
 #include "utils/rel.h"
 #include "utils/snapmgr.h"
 #include "utils/wait_event.h"
@@ -1786,6 +1787,13 @@ GetSerializableTransactionSnapshotInt(Snapshot snapshot,
 	 */
 #ifdef TEST_SUMMARIZE_SERIAL
 	SummarizeOldestCommittedSxact();
+#endif
+#ifdef USE_INJECTION_POINTS
+	if (IS_INJECTION_POINT_ATTACHED("serializable-summarize"))
+	{
+		INJECTION_POINT("serializable-summarize", NULL);
+		SummarizeOldestCommittedSxact();
+	}
 #endif
 	LWLockAcquire(SerializableXactHashLock, LW_EXCLUSIVE);
 	do
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2463d6e0fb9..f67fe49cd41 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -32,6 +32,7 @@ ISOLATION = basic \
 	    repack_toast \
 	    ri_fastpath_reindex \
 	    ri_fastpath_snapshot \
+	    serializable-summarization \
 	    syscache-update-pruned \
 	    $(WAIT_CLEANUP_TST) \
 	    heap_lock_update \
diff --git a/src/test/modules/injection_points/expected/serializable-summarization.out b/src/test/modules/injection_points/expected/serializable-summarization.out
new file mode 100644
index 00000000000..0604ac670ed
--- /dev/null
+++ b/src/test/modules/injection_points/expected/serializable-summarization.out
@@ -0,0 +1,98 @@
+Parsed test spec with 3 sessions
+
+starting permutation: s1_begin s1_read s2_begin s2_read s2_write s2_commit s1_write s1_commit
+step s1_begin: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1_read: SELECT count(*) FROM summarized_write_skew WHERE v = 1;
+count
+-----
+    0
+(1 row)
+
+step s2_begin: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2_read: SELECT count(*) FROM summarized_write_skew WHERE v = 1;
+count
+-----
+    0
+(1 row)
+
+step s2_write: 
+	UPDATE summarized_write_skew
+	SET v = 1
+	WHERE id = 1
+	  AND (SELECT count(*) FROM summarized_write_skew WHERE v = 1) = 0;
+
+step s2_commit: COMMIT;
+step s1_write: 
+	UPDATE summarized_write_skew
+	SET v = 1
+	WHERE id = 2
+	  AND (SELECT count(*) FROM summarized_write_skew WHERE v = 1) = 0;
+
+ERROR:  could not serialize access due to read/write dependencies among transactions
+step s1_commit: COMMIT;
+
+starting permutation: s1_begin s1_read s2_begin s2_read s2_write s2_commit attach summarize detach s1_write s1_commit
+step s1_begin: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1_read: SELECT count(*) FROM summarized_write_skew WHERE v = 1;
+count
+-----
+    0
+(1 row)
+
+step s2_begin: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2_read: SELECT count(*) FROM summarized_write_skew WHERE v = 1;
+count
+-----
+    0
+(1 row)
+
+step s2_write: 
+	UPDATE summarized_write_skew
+	SET v = 1
+	WHERE id = 1
+	  AND (SELECT count(*) FROM summarized_write_skew WHERE v = 1) = 0;
+
+step s2_commit: COMMIT;
+step attach: 
+	SELECT injection_points_attach('serializable-summarize', 'notice');
+
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+s3: NOTICE:  notice triggered for injection point serializable-summarize
+step summarize: 
+	BEGIN ISOLATION LEVEL SERIALIZABLE;
+	SELECT 1;
+	COMMIT;
+
+?column?
+--------
+       1
+(1 row)
+
+step detach: 
+	SELECT count(*) > 0 AS locks_summarized
+	FROM pg_locks
+	WHERE mode = 'SIReadLock' AND virtualtransaction = '-1/0';
+	SELECT injection_points_detach('serializable-summarize');
+
+locks_summarized
+----------------
+t               
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
+step s1_write: 
+	UPDATE summarized_write_skew
+	SET v = 1
+	WHERE id = 2
+	  AND (SELECT count(*) FROM summarized_write_skew WHERE v = 1) = 0;
+
+ERROR:  could not serialize access due to read/write dependencies among transactions
+step s1_commit: COMMIT;
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index b4a0079484b..955e28e25a6 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -40,6 +40,7 @@ injection_points_isolation = [
   'repack_toast',
   'ri_fastpath_reindex',
   'ri_fastpath_snapshot',
+  'serializable-summarization',
   'syscache-update-pruned',
   'heap_lock_update',
   'on_conflict_probe_window',
diff --git a/src/test/modules/injection_points/specs/serializable-summarization.spec b/src/test/modules/injection_points/specs/serializable-summarization.spec
new file mode 100644
index 00000000000..2bc72184daf
--- /dev/null
+++ b/src/test/modules/injection_points/specs/serializable-summarization.spec
@@ -0,0 +1,70 @@
+# Test SSI conflict detection after summarization.
+
+setup
+{
+	CREATE EXTENSION injection_points;
+	CREATE TABLE summarized_write_skew (id int PRIMARY KEY, v int);
+	INSERT INTO summarized_write_skew VALUES (1, 0), (2, 0);
+}
+
+teardown
+{
+	DROP TABLE summarized_write_skew;
+	DROP EXTENSION injection_points;
+}
+
+session s1
+step s1_begin	{ BEGIN ISOLATION LEVEL SERIALIZABLE; }
+step s1_read	{ SELECT count(*) FROM summarized_write_skew WHERE v = 1; }
+step s1_write
+{
+	UPDATE summarized_write_skew
+	SET v = 1
+	WHERE id = 2
+	  AND (SELECT count(*) FROM summarized_write_skew WHERE v = 1) = 0;
+}
+step s1_commit	{ COMMIT; }
+
+session s2
+step s2_begin	{ BEGIN ISOLATION LEVEL SERIALIZABLE; }
+step s2_read	{ SELECT count(*) FROM summarized_write_skew WHERE v = 1; }
+step s2_write
+{
+	UPDATE summarized_write_skew
+	SET v = 1
+	WHERE id = 1
+	  AND (SELECT count(*) FROM summarized_write_skew WHERE v = 1) = 0;
+}
+step s2_commit	{ COMMIT; }
+
+session s3
+step attach
+{
+	SELECT injection_points_attach('serializable-summarize', 'notice');
+}
+step summarize
+{
+	BEGIN ISOLATION LEVEL SERIALIZABLE;
+	SELECT 1;
+	COMMIT;
+}
+step detach
+{
+	SELECT count(*) > 0 AS locks_summarized
+	FROM pg_locks
+	WHERE mode = 'SIReadLock' AND virtualtransaction = '-1/0';
+	SELECT injection_points_detach('serializable-summarize');
+}
+
+# The ordinary, unsummarized conflict is a control case.
+permutation
+	s1_begin s1_read
+	s2_begin s2_read s2_write s2_commit
+	s1_write s1_commit
+
+# Moving s2's SIREAD locks to OldCommittedSxact must preserve the conflict.
+permutation
+	s1_begin s1_read
+	s2_begin s2_read s2_write s2_commit
+	attach summarize detach
+	s1_write s1_commit
-- 
2.55.0

