From bfa1aa7113cf63ab9fa9dc6ba67f517a579ae74e Mon Sep 17 00:00:00 2001
From: Andrey Borodin <amborodin@acm.org>
Date: Sun, 23 Aug 2026 15:20:16 +0300
Subject: [PATCH v1 1/2] 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 0ae85b7d5b4..bc9b5191a4c 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"
@@ -1730,6 +1731,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 3b136adf126..0147b1aba91 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -21,6 +21,7 @@ ISOLATION = basic \
 	    repack_toast \
 	    ri_fastpath_reindex \
 	    syscache-update-pruned \
+	    serializable-summarization \
 	    wait_cleanup \
 	    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 aff516b901a..da1ec91cb0a 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -51,6 +51,7 @@ tests += {
       'repack_temporal_multirange',
       'repack_toast',
       'ri_fastpath_reindex',
+      'serializable-summarization',
       'syscache-update-pruned',
       'wait_cleanup',
       'heap_lock_update',
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
-- 
That's all, folks. May the source be with you.

