From 8fc6659411760c19ea3ebaea41d3c30bacdef7d7 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Sun, 26 Jul 2026 21:18:45 -0400
Subject: [PATCH v1 2/2] Read the in-progress set from subxip in
 pg_current_snapshot()

pg_current_snapshot() copied the active snapshot's xip array.  A snapshot
taken during recovery keeps all of its running XIDs in subxip and leaves xip
empty, so on a standby the function returned a pg_snapshot claiming that
nothing was in progress, and pg_visible_in_snapshot() answered "visible" for
transactions that were still running:

    primary  pg_current_snapshot() = 696:698:696   visible(696) = f
    standby  pg_current_snapshot() = 696:698:      visible(696) = t

while the row that transaction 696 had deleted was, correctly, still visible
on both nodes.  The documented caveat about subtransaction IDs does not cover
this: 696 is a top-level XID.  Unlike the export path this needs no subxid
overflow, since xip is empty on a standby either way.

Read the array the snapshot actually filled.  XIDs of subtransactions do
appear in it on a standby, because recovery does not distinguish them, but
that is within the existing caveat and reporting a running subtransaction as
still running is the better error.

pg_current_snapshot() and txid_current_snapshot() share this implementation.
---
 src/backend/utils/adt/xid8funcs.c             | 20 +++++++++++++++++--
 .../recovery/t/055_standby_snapshot_export.pl | 12 +++++++++++
 2 files changed, 30 insertions(+), 2 deletions(-)

diff --git a/src/backend/utils/adt/xid8funcs.c b/src/backend/utils/adt/xid8funcs.c
index c607e78d9..03053996b 100644
--- a/src/backend/utils/adt/xid8funcs.c
+++ b/src/backend/utils/adt/xid8funcs.c
@@ -374,14 +374,30 @@ pg_current_snapshot(PG_FUNCTION_ARGS)
 	uint32		nxip,
 				i;
 	Snapshot	cur;
+	TransactionId *xip;
 	FullTransactionId next_fxid = ReadNextFullTransactionId();
 
 	cur = GetActiveSnapshot();
 	if (cur == NULL)
 		elog(ERROR, "no active snapshot set");
 
+	/*
+	 * A snapshot taken during recovery stores all of its XIDs in subxip and
+	 * leaves xip empty, so read the in-progress set from there.  Reading xip
+	 * would report every running transaction as already completed.
+	 */
+	if (cur->takenDuringRecovery)
+	{
+		nxip = cur->subxcnt;
+		xip = cur->subxip;
+	}
+	else
+	{
+		nxip = cur->xcnt;
+		xip = cur->xip;
+	}
+
 	/* allocate */
-	nxip = cur->xcnt;
 	snap = palloc(PG_SNAPSHOT_SIZE(nxip));
 
 	/*
@@ -395,7 +411,7 @@ pg_current_snapshot(PG_FUNCTION_ARGS)
 	snap->nxip = nxip;
 	for (i = 0; i < nxip; i++)
 		snap->xip[i] =
-			FullTransactionIdFromAllowableAt(next_fxid, cur->xip[i]);
+			FullTransactionIdFromAllowableAt(next_fxid, xip[i]);
 
 	/*
 	 * We want them guaranteed to be in ascending order.  This also removes
diff --git a/src/test/recovery/t/055_standby_snapshot_export.pl b/src/test/recovery/t/055_standby_snapshot_export.pl
index 139190cf2..975b434df 100644
--- a/src/test/recovery/t/055_standby_snapshot_export.pl
+++ b/src/test/recovery/t/055_standby_snapshot_export.pl
@@ -82,6 +82,18 @@ $o->query_safe(
 $primary->safe_psql('postgres', 'INSERT INTO burner VALUES (0)');
 $primary->wait_for_replay_catchup($standby);
 
+# pg_current_snapshot() reads the same pair of arrays and has to make the same
+# distinction.  Reporting a running transaction as visible would contradict
+# what the very snapshot it came from shows.  Unlike the export path this
+# needs no overflow: on a standby xip is always empty.
+is($standby->safe_psql(
+		'postgres',
+		"SELECT pg_visible_in_snapshot('$u_xid'::xid8, pg_current_snapshot())"),
+	'f', 'pg_visible_in_snapshot agrees a running XID is not visible');
+
+is($standby->safe_psql('postgres', 'SELECT count(*) FROM victim WHERE k = 7'),
+	1, 'and its delete has not taken effect');
+
 my $s1 = $standby->background_psql('postgres');
 $s1->query_safe('BEGIN ISOLATION LEVEL REPEATABLE READ');
 my $snap = $s1->query_safe('SELECT pg_export_snapshot()');
-- 
2.53.0

