From cc318cc38614481cf5e95b7ddb30643e356b3f23 Mon Sep 17 00:00:00 2001
From: Sami Imseih <samimseih.pg@gmail.com>
Date: Wed, 9 Sep 2026 12:29:22 +0000
Subject: [PATCH v4 1/1] Fix WAIT FOR rejection errors

WAIT FOR is documented as unsupported in transactions above READ
COMMITTED, but the implementation only rejected that case after a
transaction snapshot had already been taken. As a result, higher
isolation levels could reach the generic snapshot-check error instead
of failing with an error indicating that the isolation level is not
supported.

The same "active or registered snapshot" error also included an
isolation-level explanation as DETAIL, which is wrong for READ
COMMITTED cases such as a held cursor.

Reject transaction-snapshot mode explicitly before checking for
registered or active snapshots, so REPEATABLE READ and SERIALIZABLE
fail with the right message and snapshot-holding READ COMMITTED cases
report the snapshot-specific error.

Add recovery coverage for both cases.
---
 src/backend/commands/wait.c             | 13 ++++++++++--
 src/test/recovery/t/049_wait_for_lsn.pl | 28 ++++++++++++++++++++-----
 2 files changed, 34 insertions(+), 7 deletions(-)

diff --git a/src/backend/commands/wait.c b/src/backend/commands/wait.c
index 9ba4c75021e..7240ce08bb3 100644
--- a/src/backend/commands/wait.c
+++ b/src/backend/commands/wait.c
@@ -18,6 +18,7 @@
 #include "access/xlog.h"
 #include "access/xlogrecovery.h"
 #include "access/xlogwait.h"
+#include "access/xact.h"
 #include "catalog/pg_type_d.h"
 #include "commands/defrem.h"
 #include "commands/wait.h"
@@ -147,6 +148,15 @@ ExecWaitStmt(ParseState *pstate, WaitStmt *stmt, bool isTopLevel,
 		}
 	}
 
+	/*
+	 * WAIT FOR must not be run in a transaction that uses a transaction
+	 * snapshot.
+	 */
+	if (IsolationUsesXactSnapshot())
+		ereport(ERROR,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("WAIT FOR cannot be executed within a transaction with an isolation level higher than READ COMMITTED"));
+
 	/*
 	 * We are going to wait for the LSN.  We should first care that we don't
 	 * hold a snapshot and correspondingly our MyProc->xmin is invalid.
@@ -171,8 +181,7 @@ ExecWaitStmt(ParseState *pstate, WaitStmt *stmt, bool isTopLevel,
 	if (HaveRegisteredOrActiveSnapshot())
 		ereport(ERROR,
 				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
-				errmsg("WAIT FOR must be called without an active or registered snapshot"),
-				errdetail("WAIT FOR cannot be executed within a transaction with an isolation level higher than READ COMMITTED."));
+				errmsg("WAIT FOR must be called without an active or registered snapshot"));
 
 	/*
 	 * As the result we should hold no snapshot, and correspondingly our xmin
diff --git a/src/test/recovery/t/049_wait_for_lsn.pl b/src/test/recovery/t/049_wait_for_lsn.pl
index cb7d4d461de..934b2b121e6 100644
--- a/src/test/recovery/t/049_wait_for_lsn.pl
+++ b/src/test/recovery/t/049_wait_for_lsn.pl
@@ -262,9 +262,9 @@ unlike(
 	"WAIT FOR LSN after savepoint rollback did not disconnect");
 
 # 5. Check mode validation: standby modes error on primary, primary mode errors
-# on standby, and primary_flush works on primary.  Also check that WAIT FOR
-# triggers an error if called within a function, procedure, anonymous DO block,
-# or inside a transaction with an isolation level higher than READ COMMITTED.
+# on standby.  Also check that WAIT FOR triggers an error if called within a
+# function, procedure, anonymous DO block, in a transaction with an isolation
+# level higher than READ COMMITTED, or with an active or registered snapshot.
 
 # Test standby_flush on primary - should error
 $node_primary->psql(
@@ -282,13 +282,31 @@ $node_standby->psql(
 ok($stderr =~ /recovery is in progress/,
 	"get an error when running primary_flush on the standby");
 
+$node_standby->psql(
+	'postgres',
+	"BEGIN ISOLATION LEVEL REPEATABLE READ; WAIT FOR LSN '${lsn3}';",
+	stderr => \$stderr);
+ok($stderr =~
+	  /ERROR:\s+WAIT FOR cannot be executed within a transaction with an isolation level higher than READ COMMITTED/,
+	"get an error when running WAIT FOR in a transaction with an isolation level higher than READ COMMITTED"
+);
+
 $node_standby->psql(
 	'postgres',
 	"BEGIN ISOLATION LEVEL REPEATABLE READ; SELECT 1; WAIT FOR LSN '${lsn3}';",
 	stderr => \$stderr);
-ok( $stderr =~
+ok($stderr =~
+	  /ERROR:\s+WAIT FOR cannot be executed within a transaction with an isolation level higher than READ COMMITTED/,
+	"get an isolation-level error when running WAIT FOR after taking a transaction snapshot"
+);
+
+$node_standby->psql(
+	'postgres',
+	"BEGIN ISOLATION LEVEL READ COMMITTED; DECLARE c CURSOR FOR SELECT 1; WAIT FOR LSN '${lsn3}';",
+	stderr => \$stderr);
+ok($stderr =~
 	  /WAIT FOR must be called without an active or registered snapshot/,
-	"get an error when running in a transaction with an isolation level higher than REPEATABLE READ"
+	"get an error when running WAIT FOR with an active or registered snapshot"
 );
 
 # Test wrapping WAIT FOR into function, procedure, and anonymous DO block --
-- 
2.50.1

