diff --git a/doc/src/sgml/ref/wait.sgml b/doc/src/sgml/ref/wait.sgml
index cddcbf09695..2917309c0d5 100644
--- a/doc/src/sgml/ref/wait.sgml
+++ b/doc/src/sgml/ref/wait.sgml
@@ -260,10 +260,16 @@ WAIT FOR LSN 'lsn'
WAIT must be executed as a top-level command.
It cannot be executed from a function, procedure, or
- DO block. It also requires that no active or
- registered snapshot be held, and therefore cannot be used in contexts
- where such a snapshot must remain active, including transactions running
- at isolation levels higher than READ COMMITTED.
+ DO block. It also cannot be executed while the
+ current transaction holds a snapshot. WAIT itself
+ acquires none, so it can run before the first snapshot-taking statement
+ of a REPEATABLE READ or SERIALIZABLE
+ transaction, but not after it, and not while a cursor or an exported
+ snapshot holds one at any isolation level. A snapshot held here could
+ delay replay, which standby_replay waits for and which
+ other standby modes can end up waiting for too. That is also why
+ WAIT is a command rather than a function or a
+ procedure, which execute with one held.
diff --git a/src/backend/commands/wait.c b/src/backend/commands/wait.c
index 2223e91a3e3..86cb27ea2e1 100644
--- a/src/backend/commands/wait.c
+++ b/src/backend/commands/wait.c
@@ -13,6 +13,7 @@
*/
#include "postgres.h"
+#include "access/xact.h"
#include "access/xlog.h"
#include "access/xlogrecovery.h"
#include "access/xlogwait.h"
@@ -155,8 +156,9 @@ ExecWaitStmt(ParseState *pstate, WaitStmt *stmt, bool isTopLevel,
if (HaveRegisteredOrActiveSnapshot())
ereport(ERROR,
errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
- errmsg("WAIT must be called without an active or registered snapshot"),
- errdetail("WAIT cannot be executed within a transaction with an isolation level higher than READ COMMITTED."));
+ errmsg("WAIT cannot be executed while the current transaction holds a snapshot"),
+ IsolationUsesXactSnapshot() ?
+ errdetail("This transaction runs at an isolation level higher than READ COMMITTED, so it holds a snapshot from its first query until it ends.") : 0);
/*
* 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 3bc936e0f91..0fe5b9c6f67 100644
--- a/src/test/recovery/t/049_wait_for_lsn.pl
+++ b/src/test/recovery/t/049_wait_for_lsn.pl
@@ -282,13 +282,38 @@ $node_standby->psql(
ok($stderr =~ /recovery is in progress/,
"get an error when running primary_flush on the standby");
+# A transaction-snapshot-mode transaction has no snapshot until its first
+# query, so WAIT FOR is allowed before that point. The target is already
+# replayed here, so the command returns immediately.
+$output = $node_standby->safe_psql('postgres',
+ "BEGIN ISOLATION LEVEL REPEATABLE READ; WAIT FOR LSN '${lsn2}'; COMMIT;");
+ok($output eq "success",
+ "WAIT FOR is allowed before the transaction snapshot is taken");
+
$node_standby->psql(
'postgres',
"BEGIN ISOLATION LEVEL REPEATABLE READ; SELECT 1; WAIT FOR LSN '${lsn3}';",
stderr => \$stderr);
-ok( $stderr =~ /WAIT must be called without an active or registered snapshot/,
+ok( $stderr =~
+ /WAIT cannot be executed while the current transaction holds a snapshot/,
"get an error when running in a transaction with an isolation level higher than REPEATABLE READ"
);
+ok( $stderr =~
+ /isolation level higher than READ COMMITTED, so it holds a snapshot/,
+ "the isolation level is given as the reason when it is the reason");
+
+# The same error at READ COMMITTED must not blame the isolation level.
+$node_standby->psql(
+ 'postgres',
+ "BEGIN; DECLARE c CURSOR FOR SELECT 1; WAIT FOR LSN '${lsn3}';",
+ stderr => \$stderr);
+ok( $stderr =~
+ /WAIT cannot be executed while the current transaction holds a snapshot/,
+ "get an error when a cursor holds a snapshot");
+unlike(
+ $stderr,
+ qr/isolation level/,
+ "the isolation level is not blamed at READ COMMITTED");
# Test wrapping WAIT FOR into function, procedure, and anonymous DO block --
# should error