From 5a05e1fd940b1bad240672b48c45b3c91e008006 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 v3 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 only 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 | 22 ++++++++++++++++++++--
 2 files changed, 31 insertions(+), 4 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..4b6e5194579 100644
--- a/src/test/recovery/t/049_wait_for_lsn.pl
+++ b/src/test/recovery/t/049_wait_for_lsn.pl
@@ -264,7 +264,7 @@ unlike(
 # 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.
+# or inside a transaction that uses a transaction 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 =~
+	  /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 =~
+	  /WAIT FOR cannot be executed within a transaction with an isolation level higher than READ COMMITTED/,
+	"get the isolation-level error even after a transaction snapshot has been taken"
+);
+
+$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

