From 80ae2ac293196066702d439ee34b90ff305cbcff Mon Sep 17 00:00:00 2001
From: Will Mortensen <will@extrahop.com>
Date: Mon, 16 Jan 2023 20:51:26 -0800
Subject: [PATCH v3] Add WAIT ONLY option to LOCK command

Rather than actually taking any locks on the table(s), it simply waits
for conflicting lockers using the existing WaitForLockersMultiple()
function in the lock manager.

Unlike when actually taking locks, this obtains the set of conflicting
locks once for all specified tables and then waits for each of those
locks, rather than obtaining conflicts for the first table and waiting
for those before obtaining conflicts for the second table.

Currently it's not supported with views, since they would require more
locking to gather the locktags.

The syntax allows combining it with NOWAIT, which would perhaps be
useful to simply check for conflicts, but this is not yet implemented.
(NOWAIT + immediately releasing the lock already accomplishes roughly
the same thing.)

Unlike other forms of LOCK, WAIT ONLY is allowed outside a transaction
block, since it makes perfect sense to wait and then e.g. SELECT new
data.

Regardless of the specified locking mode, only SELECT permissions are
required on the table(s).
---
 doc/src/sgml/ref/lock.sgml                    |  54 ++++++--
 src/backend/commands/lockcmds.c               |  83 +++++++++--
 src/backend/parser/gram.y                     |  14 +-
 src/backend/tcop/utility.c                    |  24 +++-
 src/include/nodes/parsenodes.h                |   1 +
 src/include/parser/kwlist.h                   |   1 +
 .../isolation/expected/deadlock-wait-only.out |  12 ++
 src/test/isolation/expected/wait-only.out     | 129 ++++++++++++++++++
 src/test/isolation/isolation_schedule         |   2 +
 .../isolation/specs/deadlock-wait-only.spec   |  23 ++++
 src/test/isolation/specs/wait-only.spec       |  61 +++++++++
 src/test/regress/expected/lock.out            |  40 ++++++
 src/test/regress/sql/lock.sql                 |  41 ++++++
 13 files changed, 458 insertions(+), 27 deletions(-)
 create mode 100644 src/test/isolation/expected/deadlock-wait-only.out
 create mode 100644 src/test/isolation/expected/wait-only.out
 create mode 100644 src/test/isolation/specs/deadlock-wait-only.spec
 create mode 100644 src/test/isolation/specs/wait-only.spec

diff --git a/doc/src/sgml/ref/lock.sgml b/doc/src/sgml/ref/lock.sgml
index 6ce2518de7..44c46bdbaa 100644
--- a/doc/src/sgml/ref/lock.sgml
+++ b/doc/src/sgml/ref/lock.sgml
@@ -21,7 +21,7 @@ PostgreSQL documentation
 
  <refsynopsisdiv>
 <synopsis>
-LOCK [ TABLE ] [ ONLY ] <replaceable class="parameter">name</replaceable> [ * ] [, ...] [ IN <replaceable class="parameter">lockmode</replaceable> MODE ] [ NOWAIT ]
+LOCK [ TABLE ] [ ONLY ] <replaceable class="parameter">name</replaceable> [ * ] [, ...] [ IN <replaceable class="parameter">lockmode</replaceable> MODE ] [ NOWAIT | WAIT ONLY ]
 
 <phrase>where <replaceable class="parameter">lockmode</replaceable> is one of:</phrase>
 
@@ -42,12 +42,15 @@ LOCK [ TABLE ] [ ONLY ] <replaceable class="parameter">name</replaceable> [ * ]
    error is emitted.  Once obtained, the lock is held for the
    remainder of the current transaction.  (There is no <command>UNLOCK
    TABLE</command> command; locks are always released at transaction
-   end.)
+   end.)  If <literal>WAIT ONLY</literal> is specified, <command>LOCK
+   TABLE</command> simply waits for already-held conflicting locks to be
+   released, and doesn't take any new table-level locks.
   </para>
 
   <para>
    When a view is locked, all relations appearing in the view definition
-   query are also locked recursively with the same lock mode.
+   query are also locked recursively with the same lock mode. Views are not
+   currently supported with <literal>WAIT ONLY</literal>.
   </para>
 
   <para>
@@ -101,6 +104,15 @@ LOCK [ TABLE ] [ ONLY ] <replaceable class="parameter">name</replaceable> [ * ]
    restrictive mode first.
   </para>
 
+  <para>
+   <literal>WAIT ONLY</literal> can be used with <literal>SHARE</literal> mode
+   to wait for in-progress writes to be committed or rolled back, while still
+   allowing other transactions to newly acquire conflicting locks. This can be
+   useful in conjunction with non-transactional communication between clients
+   about writes, such as through sequences. As above, clients must be mindful of
+   isolation levels when observing changes made by other transactions.
+  </para>
+
   <para>
    More information about the lock modes and locking strategies can be
    found in <xref linkend="explicit-locking"/>.
@@ -158,6 +170,24 @@ LOCK [ TABLE ] [ ONLY ] <replaceable class="parameter">name</replaceable> [ * ]
      </para>
     </listitem>
    </varlistentry>
+
+   <varlistentry>
+    <term><literal>WAIT ONLY</literal></term>
+    <listitem>
+     <para>
+      Specifies that <command>LOCK TABLE</command> should only wait for existing
+      conflicting locks to be released, and not take any new table-level locks.
+      Unlike when actually locking the tables, this first obtains the combined
+      set of conflicting locks for all specified tables, and then waits on all
+      of those locks (rather than first obtaining the conflicting locks for the
+      first table and waiting on those, then obtaining the conflicting locks for
+      the second table and waiting on those, and so on). Since no table-level
+      locks are taken, a table may be dropped by another transaction while
+      waiting. Views are not currently supported with
+      <literal>WAIT ONLY</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
   </variablelist>
  </refsect1>
 
@@ -175,7 +205,9 @@ LOCK [ TABLE ] [ ONLY ] <replaceable class="parameter">name</replaceable> [ * ]
     MODE</literal> (or a less-conflicting mode as described in <xref
     linkend="explicit-locking"/>) is permitted. If a user has
     <literal>SELECT</literal> privileges on the table, <literal>ACCESS SHARE
-    MODE</literal> is permitted.
+    MODE</literal> is permitted. If <literal>WAIT ONLY</literal> is specified,
+    only <literal>SELECT</literal> privileges are required regardless of
+    <replaceable class="parameter">lockmode</replaceable>.
    </para>
 
    <para>
@@ -191,10 +223,13 @@ LOCK [ TABLE ] [ ONLY ] <replaceable class="parameter">name</replaceable> [ * ]
    </para>
 
    <para>
-    <command>LOCK TABLE</command> is useless outside a transaction block: the lock
-    would remain held only to the completion of the statement.  Therefore
-    <productname>PostgreSQL</productname> reports an error if <command>LOCK</command>
-    is used outside a transaction block.
+    <command>LOCK TABLE</command> without <literal>WAIT ONLY</literal> is
+    useless outside a transaction block: the lock would remain held only to the
+    completion of the statement, and using <literal>WAIT ONLY</literal> is more
+    efficient if the goal is simply to wait for conflicting locks.  Therefore
+    <productname>PostgreSQL</productname> reports an error if
+    <command>LOCK</command> is used outside a transaction block without
+    <literal>WAIT ONLY</literal>.
     Use
     <link linkend="sql-begin"><command>BEGIN</command></link> and
     <link linkend="sql-commit"><command>COMMIT</command></link>
@@ -262,7 +297,8 @@ COMMIT WORK;
 
   <para>
    Except for <literal>ACCESS SHARE</literal>, <literal>ACCESS EXCLUSIVE</literal>,
-   and <literal>SHARE UPDATE EXCLUSIVE</literal> lock modes, the
+   and <literal>SHARE UPDATE EXCLUSIVE</literal> lock modes and the
+   <literal>WAIT ONLY</literal> option, the
    <productname>PostgreSQL</productname> lock modes and the
    <command>LOCK TABLE</command> syntax are compatible with those
    present in <productname>Oracle</productname>.
diff --git a/src/backend/commands/lockcmds.c b/src/backend/commands/lockcmds.c
index 40ef4ede26..893f2311b9 100644
--- a/src/backend/commands/lockcmds.c
+++ b/src/backend/commands/lockcmds.c
@@ -16,6 +16,7 @@
 
 #include "access/table.h"
 #include "access/xact.h"
+#include "catalog/catalog.h"
 #include "catalog/namespace.h"
 #include "catalog/pg_inherits.h"
 #include "commands/lockcmds.h"
@@ -28,7 +29,8 @@
 #include "utils/lsyscache.h"
 #include "utils/syscache.h"
 
-static void LockTableRecurse(Oid reloid, LOCKMODE lockmode, bool nowait);
+static void LockTableRecurse(Oid reloid, LOCKMODE lockmode, bool nowait,
+							 List **locktags_p);
 static AclResult LockTableAclCheck(Oid reloid, LOCKMODE lockmode, Oid userid);
 static void RangeVarCallbackForLockTable(const RangeVar *rv, Oid relid,
 										 Oid oldrelid, void *arg);
@@ -42,6 +44,34 @@ void
 LockTableCommand(LockStmt *lockstmt)
 {
 	ListCell   *p;
+	LOCKMODE	lockmode;
+	LOCKMODE   	waitmode;
+	List	   *waitlocktags = NIL;
+	List	  **waitlocktags_p;
+
+	if (lockstmt->waitonly && lockstmt->nowait)
+		/*
+		 * this could be defined to check and error if there are conflicting
+		 * lockers, but it seems unclear if that would be useful, since
+		 * LOCK ... NOWAIT + immediate unlock would do nearly the same thing
+		 */
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("NOWAIT is not supported with WAIT ONLY")));
+
+
+	if (lockstmt->waitonly)
+	{
+		lockmode = NoLock;
+		waitmode = lockstmt->mode;
+		waitlocktags_p = &waitlocktags;
+	}
+	else
+	{
+		lockmode = lockstmt->mode;
+		waitmode = NoLock;
+		waitlocktags_p = NULL;
+	}
 
 	/*
 	 * Iterate over the list and process the named relations one at a time
@@ -52,16 +82,37 @@ LockTableCommand(LockStmt *lockstmt)
 		bool		recurse = rv->inh;
 		Oid			reloid;
 
-		reloid = RangeVarGetRelidExtended(rv, lockstmt->mode,
+		reloid = RangeVarGetRelidExtended(rv, lockmode,
 										  lockstmt->nowait ? RVR_NOWAIT : 0,
 										  RangeVarCallbackForLockTable,
-										  (void *) &lockstmt->mode);
+										  (void *) &lockmode);
+		if (waitmode != NoLock)
+		{
+			Oid			dbid;
+			LOCKTAG	   *heaplocktag = palloc_object(LOCKTAG);
+
+			if (IsSharedRelation(reloid))
+				dbid = InvalidOid;
+			else
+				dbid = MyDatabaseId;
+			SET_LOCKTAG_RELATION(*heaplocktag, dbid, reloid);
+			waitlocktags = lappend(waitlocktags, heaplocktag);
+		}
 
 		if (get_rel_relkind(reloid) == RELKIND_VIEW)
-			LockViewRecurse(reloid, lockstmt->mode, lockstmt->nowait, NIL);
+		{
+			if (lockstmt->waitonly || lockmode == NoLock)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						errmsg("WAIT ONLY is not supported with views")));
+			LockViewRecurse(reloid, lockmode, lockstmt->nowait, NIL);
+		}
 		else if (recurse)
-			LockTableRecurse(reloid, lockstmt->mode, lockstmt->nowait);
+			LockTableRecurse(reloid, lockmode, lockstmt->nowait,
+							 waitlocktags_p);
 	}
+	if (waitmode != NoLock)
+		WaitForLockersMultiple(waitlocktags, waitmode, false);
 }
 
 /*
@@ -115,7 +166,7 @@ RangeVarCallbackForLockTable(const RangeVar *rv, Oid relid, Oid oldrelid,
  * parent which is enough.
  */
 static void
-LockTableRecurse(Oid reloid, LOCKMODE lockmode, bool nowait)
+LockTableRecurse(Oid reloid, LOCKMODE lockmode, bool nowait, List **locktags_p)
 {
 	List	   *children;
 	ListCell   *lc;
@@ -125,11 +176,26 @@ LockTableRecurse(Oid reloid, LOCKMODE lockmode, bool nowait)
 	foreach(lc, children)
 	{
 		Oid			childreloid = lfirst_oid(lc);
+		Oid			dbid;
+		LOCKTAG	   *heaplocktag;
 
-		/* Parent already locked. */
+		/* Parent already handled. */
 		if (childreloid == reloid)
 			continue;
 
+		if (locktags_p != NULL)
+		{
+			heaplocktag = palloc_object(LOCKTAG);
+			if (IsSharedRelation(childreloid))
+				dbid = InvalidOid;
+			else
+				dbid = MyDatabaseId;
+			SET_LOCKTAG_RELATION(*heaplocktag, dbid, childreloid);
+			*locktags_p = lappend(*locktags_p, heaplocktag);
+		}
+
+		if (lockmode == NoLock)
+			continue;
 		if (!nowait)
 			LockRelationOid(childreloid, lockmode);
 		else if (!ConditionalLockRelationOid(childreloid, lockmode))
@@ -228,7 +294,8 @@ LockViewRecurse_walker(Node *node, LockViewRecurse_context *context)
 				LockViewRecurse(relid, context->lockmode, context->nowait,
 								context->ancestor_views);
 			else if (rte->inh)
-				LockTableRecurse(relid, context->lockmode, context->nowait);
+				LockTableRecurse(relid, context->lockmode, context->nowait,
+								 NULL);
 		}
 
 		return query_tree_walker(query,
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 15ece871a0..c62693cf7a 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -353,7 +353,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <defelt>	drop_option
 %type <boolean>	opt_or_replace opt_no
 				opt_grant_grant_option
-				opt_nowait opt_if_exists opt_with_data
+				opt_nowait opt_waitonly opt_if_exists opt_with_data
 				opt_transaction_chain
 %type <list>	grant_role_opt_list
 %type <defelt>	grant_role_opt
@@ -773,7 +773,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING
 	VERBOSE VERSION_P VIEW VIEWS VOLATILE
 
-	WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE
+	WAIT WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE
 
 	XML_P XMLATTRIBUTES XMLCONCAT XMLELEMENT XMLEXISTS XMLFOREST XMLNAMESPACES
 	XMLPARSE XMLPI XMLROOT XMLSERIALIZE XMLTABLE
@@ -12146,13 +12146,14 @@ using_clause:
  *
  *****************************************************************************/
 
-LockStmt:	LOCK_P opt_table relation_expr_list opt_lock opt_nowait
+LockStmt:	LOCK_P opt_table relation_expr_list opt_lock opt_nowait opt_waitonly
 				{
 					LockStmt   *n = makeNode(LockStmt);
 
 					n->relations = $3;
 					n->mode = $4;
 					n->nowait = $5;
+					n->waitonly = $6;
 					$$ = (Node *) n;
 				}
 		;
@@ -12175,6 +12176,11 @@ opt_nowait:	NOWAIT							{ $$ = true; }
 			| /*EMPTY*/						{ $$ = false; }
 		;
 
+opt_waitonly:
+			WAIT ONLY						{ $$ = true; }
+			| /*EMPTY*/						{ $$ = false; }
+		;
+
 opt_nowait_or_skip:
 			NOWAIT							{ $$ = LockWaitError; }
 			| SKIP LOCKED					{ $$ = LockWaitSkip; }
@@ -17301,6 +17307,7 @@ unreserved_keyword:
 			| VIEW
 			| VIEWS
 			| VOLATILE
+			| WAIT
 			| WHITESPACE_P
 			| WITHIN
 			| WITHOUT
@@ -17932,6 +17939,7 @@ bare_label_keyword:
 			| VIEW
 			| VIEWS
 			| VOLATILE
+			| WAIT
 			| WHEN
 			| WHITESPACE_P
 			| WORK
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index e3ccf6c7f7..423731cd78 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -354,7 +354,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 				 * restrictions here must match those in
 				 * LockAcquireExtended().
 				 */
-				if (stmt->mode > RowExclusiveLock)
+				if (!stmt->waitonly && stmt->mode > RowExclusiveLock)
 					return COMMAND_OK_IN_READ_ONLY_TXN;
 				else
 					return COMMAND_IS_STRICTLY_READ_ONLY;
@@ -932,13 +932,23 @@ standard_ProcessUtility(PlannedStmt *pstmt,
 			break;
 
 		case T_LockStmt:
+			{
+				LockStmt *stmt = (LockStmt *) parsetree;
 
-			/*
-			 * Since the lock would just get dropped immediately, LOCK TABLE
-			 * outside a transaction block is presumed to be user error.
-			 */
-			RequireTransactionBlock(isTopLevel, "LOCK TABLE");
-			LockTableCommand((LockStmt *) parsetree);
+				if (!stmt->waitonly)
+				{
+					/*
+					 * Since the lock would just get dropped immediately, and
+					 * simply waiting is better done with WAIT ONLY, LOCK TABLE
+					 * without WAIT ONLY outside a transaction block is presumed
+					 * to be user error.
+					 *
+					 * XXX: the error should clarify that WAIT ONLY is allowed?
+					 */
+					RequireTransactionBlock(isTopLevel, "LOCK TABLE");
+				}
+				LockTableCommand(stmt);
+			}
 			break;
 
 		case T_ConstraintsSetStmt:
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index fe003ded50..47badd29ae 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3813,6 +3813,7 @@ typedef struct LockStmt
 	List	   *relations;		/* relations to lock */
 	int			mode;			/* lock mode */
 	bool		nowait;			/* no wait mode */
+	bool		waitonly;		/* wait only mode */
 } LockStmt;
 
 /* ----------------------
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 5984dcfa4b..9ae555d6b7 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -473,6 +473,7 @@ PG_KEYWORD("version", VERSION_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("view", VIEW, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("views", VIEWS, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("volatile", VOLATILE, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("wait", WAIT, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("when", WHEN, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("where", WHERE, RESERVED_KEYWORD, AS_LABEL)
 PG_KEYWORD("whitespace", WHITESPACE_P, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/test/isolation/expected/deadlock-wait-only.out b/src/test/isolation/expected/deadlock-wait-only.out
new file mode 100644
index 0000000000..78b4962fa1
--- /dev/null
+++ b/src/test/isolation/expected/deadlock-wait-only.out
@@ -0,0 +1,12 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1re s2as s2swo s1aewo s1c s2c
+step s1re: LOCK TABLE a1 IN ROW EXCLUSIVE MODE;
+step s2as: LOCK TABLE a1 IN ACCESS SHARE MODE;
+step s2swo: LOCK TABLE a1 IN SHARE MODE WAIT ONLY; <waiting ...>
+step s1aewo: LOCK TABLE a1 IN ACCESS EXCLUSIVE MODE WAIT ONLY; <waiting ...>
+step s1aewo: <... completed>
+step s2swo: <... completed>
+ERROR:  deadlock detected
+step s1c: COMMIT;
+step s2c: COMMIT;
diff --git a/src/test/isolation/expected/wait-only.out b/src/test/isolation/expected/wait-only.out
new file mode 100644
index 0000000000..3bbd9b68a3
--- /dev/null
+++ b/src/test/isolation/expected/wait-only.out
@@ -0,0 +1,129 @@
+Parsed test spec with 3 sessions
+
+starting permutation: w1ae2 w2in1 rlwo w2c w1c rsel1 rc
+step w1ae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2in1: INSERT INTO t1 VALUES (DEFAULT);
+step rlwo: LOCK TABLE t1, t2 IN SHARE MODE WAIT ONLY; <waiting ...>
+step w2c: COMMIT;
+step w1c: COMMIT;
+step rlwo: <... completed>
+step rsel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step rc: COMMIT;
+
+starting permutation: w1ae2 w2in1 w2c w1c rlwo rsel1 rc
+step w1ae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2c: COMMIT;
+step w1c: COMMIT;
+step rlwo: LOCK TABLE t1, t2 IN SHARE MODE WAIT ONLY;
+step rsel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step rc: COMMIT;
+
+starting permutation: w1in1 rlwo w2in1 w2c w1c rsel1 rc
+step w1in1: INSERT INTO t1 VALUES (DEFAULT);
+step rlwo: LOCK TABLE t1, t2 IN SHARE MODE WAIT ONLY; <waiting ...>
+step w2in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2c: COMMIT;
+step w1c: COMMIT;
+step rlwo: <... completed>
+step rsel1: SELECT id from t1;
+id
+--
+ 1
+ 2
+(2 rows)
+
+step rc: COMMIT;
+
+starting permutation: w1in1 rsv rl w2in1 w1c rrb w2c rsel1 rc
+step w1in1: INSERT INTO t1 VALUES (DEFAULT);
+step rsv: SAVEPOINT foo;
+step rl: LOCK TABLE t1, t2 IN SHARE MODE; <waiting ...>
+step w2in1: INSERT INTO t1 VALUES (DEFAULT); <waiting ...>
+step w1c: COMMIT;
+step rl: <... completed>
+step rrb: ROLLBACK TO foo;
+step w2in1: <... completed>
+step w2c: COMMIT;
+step rsel1: SELECT id from t1;
+id
+--
+ 1
+ 2
+(2 rows)
+
+step rc: COMMIT;
+
+starting permutation: w2in1 rlwo w1ae2 w1in1 w2c rsel1 w1c rc
+step w2in1: INSERT INTO t1 VALUES (DEFAULT);
+step rlwo: LOCK TABLE t1, t2 IN SHARE MODE WAIT ONLY; <waiting ...>
+step w1ae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w1in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2c: COMMIT;
+step rlwo: <... completed>
+step rsel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step w1c: COMMIT;
+step rc: COMMIT;
+
+starting permutation: w2in1 rsv rl w1ae2 w2c w1c rrb rsel1 rc
+step w2in1: INSERT INTO t1 VALUES (DEFAULT);
+step rsv: SAVEPOINT foo;
+step rl: LOCK TABLE t1, t2 IN SHARE MODE; <waiting ...>
+step w1ae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2c: COMMIT;
+step w1c: COMMIT;
+step rl: <... completed>
+step rrb: ROLLBACK TO foo;
+step rsel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step rc: COMMIT;
+
+starting permutation: w1ae1 w2in1 rlwo w1c rsel1 w2c rc
+step w1ae1: LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE;
+step w2in1: INSERT INTO t1 VALUES (DEFAULT); <waiting ...>
+step rlwo: LOCK TABLE t1, t2 IN SHARE MODE WAIT ONLY; <waiting ...>
+step w1c: COMMIT;
+step w2in1: <... completed>
+step rlwo: <... completed>
+step rsel1: SELECT id from t1;
+id
+--
+(0 rows)
+
+step w2c: COMMIT;
+step rc: COMMIT;
+
+starting permutation: w1ae1 w2in1 rl w1c w2c rsel1 rc
+step w1ae1: LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE;
+step w2in1: INSERT INTO t1 VALUES (DEFAULT); <waiting ...>
+step rl: LOCK TABLE t1, t2 IN SHARE MODE; <waiting ...>
+step w1c: COMMIT;
+step w2in1: <... completed>
+step w2c: COMMIT;
+step rl: <... completed>
+step rsel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step rc: COMMIT;
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index 4fc56ae99c..347cd20d6f 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -5,6 +5,7 @@ test: read-write-unique
 test: read-write-unique-2
 test: read-write-unique-3
 test: read-write-unique-4
+test: wait-only
 test: simple-write-skew
 test: receipt-report
 test: temporal-range-integrity
@@ -20,6 +21,7 @@ test: index-only-scan
 test: predicate-lock-hot-tuple
 test: update-conflict-out
 test: deadlock-simple
+test: deadlock-wait-only
 test: deadlock-hard
 test: deadlock-soft
 test: deadlock-soft-2
diff --git a/src/test/isolation/specs/deadlock-wait-only.spec b/src/test/isolation/specs/deadlock-wait-only.spec
new file mode 100644
index 0000000000..0efca38d60
--- /dev/null
+++ b/src/test/isolation/specs/deadlock-wait-only.spec
@@ -0,0 +1,23 @@
+setup
+{
+  CREATE TABLE a1 ();
+}
+
+teardown
+{
+  DROP TABLE a1;
+}
+
+session s1
+setup		{ BEGIN; }
+step s1re	  { LOCK TABLE a1 IN ROW EXCLUSIVE MODE; }
+step s1aewo	{ LOCK TABLE a1 IN ACCESS EXCLUSIVE MODE WAIT ONLY; }
+step s1c	  { COMMIT; }
+
+session s2
+setup		{ BEGIN; }
+step s2as		{ LOCK TABLE a1 IN ACCESS SHARE MODE; }
+step s2swo	{ LOCK TABLE a1 IN SHARE MODE WAIT ONLY; }
+step s2c		{ COMMIT; }
+
+permutation s1re s2as s2swo s1aewo s1c s2c
diff --git a/src/test/isolation/specs/wait-only.spec b/src/test/isolation/specs/wait-only.spec
new file mode 100644
index 0000000000..9a4e39cb9c
--- /dev/null
+++ b/src/test/isolation/specs/wait-only.spec
@@ -0,0 +1,61 @@
+setup
+{
+  CREATE TABLE t1 (id bigserial);
+  CREATE TABLE t2 (id bigserial);
+}
+
+teardown
+{
+  DROP TABLE t1;
+  DROP TABLE t2;
+}
+
+# use READ COMMITTED so we can observe the effects of a committed INSERT after
+# waiting
+
+session writer1
+setup		{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step w1in1	{ INSERT INTO t1 VALUES (DEFAULT); }
+step w1ae1	{ LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE; }
+step w1ae2	{ LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE; }
+step w1c	{ COMMIT; }
+
+session writer2
+setup		{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step w2in1	{ INSERT INTO t1 VALUES (DEFAULT); }
+step w2c	{ COMMIT; }
+
+session reader
+setup		{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step rsv	{ SAVEPOINT foo; }
+step rl		{ LOCK TABLE t1, t2 IN SHARE MODE; }
+step rrb	{ ROLLBACK TO foo; }
+step rlwo	{ LOCK TABLE t1, t2 IN SHARE MODE WAIT ONLY; }
+step rsel1	{ SELECT id from t1; }
+step rc		{ COMMIT; }
+
+# reader waits for both writers
+permutation w1ae2 w2in1 rlwo w2c w1c rsel1 rc
+
+# no waiting if writers already committed (obviously)
+permutation w1ae2 w2in1 w2c w1c rlwo rsel1 rc
+
+# reader waiting for writer1 doesn't block writer2...
+permutation w1in1 rlwo w2in1 w2c w1c rsel1 rc
+# ...while actually taking the lock does block writer2 (even if we release it
+# ASAP)
+permutation w1in1 rsv rl w2in1 w1c rrb w2c rsel1 rc
+
+# reader waiting for two tables with only t1 having a conflicting lock doesn't
+# prevent taking an ACCESS EXCLUSIVE lock on t2, or a lesser lock on t1, and the
+# reader doesn't wait for either later lock to be released...
+permutation w2in1 rlwo w1ae2 w1in1 w2c rsel1 w1c rc
+# ...while actually taking the locks is blocked by the ACCESS EXCLUSIVE lock and
+# would deadlock with the subsequent insert w1in1 (removed here)
+permutation w2in1 rsv rl w1ae2 w2c w1c rrb rsel1 rc
+
+# reader waits only for conflicting lock already held by writer1, not for
+# writer2 which was waiting to take a conflicting lock...
+permutation w1ae1 w2in1 rlwo w1c rsel1 w2c rc
+# ...while actually taking the lock also waits for writer2 to release its lock
+permutation w1ae1 w2in1 rl w1c w2c rsel1 rc
diff --git a/src/test/regress/expected/lock.out b/src/test/regress/expected/lock.out
index ad137d3645..ec5d3f5f4f 100644
--- a/src/test/regress/expected/lock.out
+++ b/src/test/regress/expected/lock.out
@@ -41,6 +41,22 @@ LOCK TABLE lock_tbl1 IN SHARE ROW EXCLUSIVE MODE NOWAIT;
 LOCK TABLE lock_tbl1 IN EXCLUSIVE MODE NOWAIT;
 LOCK TABLE lock_tbl1 IN ACCESS EXCLUSIVE MODE NOWAIT;
 ROLLBACK;
+-- Try using WAIT ONLY along with valid options.
+BEGIN TRANSACTION;
+LOCK TABLE lock_tbl1 IN ACCESS SHARE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN ROW SHARE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN ROW EXCLUSIVE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN SHARE UPDATE EXCLUSIVE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN SHARE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN SHARE ROW EXCLUSIVE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN EXCLUSIVE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN ACCESS EXCLUSIVE MODE WAIT ONLY;
+ROLLBACK;
+-- WAIT ONLY is allowed outside a transaction
+LOCK TABLE lock_tbl1 IN ACCESS EXCLUSIVE MODE WAIT ONLY;
+-- NOWAIT + WAIT ONLY is not supported (yet?)
+LOCK TABLE lock_tbl1 IN ACCESS EXCLUSIVE MODE NOWAIT WAIT ONLY;
+ERROR:  NOWAIT is not supported with WAIT ONLY
 -- Verify that we can lock views.
 BEGIN TRANSACTION;
 LOCK TABLE lock_view1 IN EXCLUSIVE MODE;
@@ -138,8 +154,22 @@ ROLLBACK;
 CREATE TABLE lock_tbl2 (b BIGINT) INHERITS (lock_tbl1);
 CREATE TABLE lock_tbl3 () INHERITS (lock_tbl2);
 BEGIN TRANSACTION;
+LOCK TABLE lock_tbl1 * IN ACCESS EXCLUSIVE MODE WAIT ONLY;
 LOCK TABLE lock_tbl1 * IN ACCESS EXCLUSIVE MODE;
 ROLLBACK;
+-- WAIT ONLY requires SELECT permissions regardless of lock mode
+-- fail without permissions
+SET ROLE regress_rol_lock1;
+BEGIN;
+LOCK TABLE ONLY lock_tbl1 IN ACCESS SHARE MODE WAIT ONLY;
+ERROR:  permission denied for table lock_tbl1
+ROLLBACK;
+RESET ROLE;
+-- succeed with only SELECT permissions and ACCESS EXCLUSIVE mode
+GRANT SELECT ON TABLE lock_tbl1 TO regress_rol_lock1;
+LOCK TABLE ONLY lock_tbl1 IN ACCESS EXCLUSIVE MODE WAIT ONLY;
+RESET ROLE;
+REVOKE SELECT ON TABLE lock_tbl1 FROM regress_rol_lock1;
 -- Child tables are locked without granting explicit permission to do so as
 -- long as we have permission to lock the parent.
 GRANT UPDATE ON TABLE lock_tbl1 TO regress_rol_lock1;
@@ -150,11 +180,21 @@ LOCK TABLE lock_tbl2;
 ERROR:  permission denied for table lock_tbl2
 ROLLBACK;
 BEGIN;
+LOCK TABLE lock_tbl2 WAIT ONLY;
+ERROR:  permission denied for table lock_tbl2
+ROLLBACK;
+BEGIN;
 LOCK TABLE lock_tbl1 * IN ACCESS EXCLUSIVE MODE;
 ROLLBACK;
 BEGIN;
+LOCK TABLE lock_tbl1 * IN ACCESS EXCLUSIVE MODE WAIT ONLY;
+ROLLBACK;
+BEGIN;
 LOCK TABLE ONLY lock_tbl1;
 ROLLBACK;
+BEGIN;
+LOCK TABLE ONLY lock_tbl1 WAIT ONLY;
+ROLLBACK;
 RESET ROLE;
 REVOKE UPDATE ON TABLE lock_tbl1 FROM regress_rol_lock1;
 -- Tables referred to by views are locked without explicit permission to do so
diff --git a/src/test/regress/sql/lock.sql b/src/test/regress/sql/lock.sql
index b88488c6d0..0a67bf9681 100644
--- a/src/test/regress/sql/lock.sql
+++ b/src/test/regress/sql/lock.sql
@@ -47,6 +47,24 @@ LOCK TABLE lock_tbl1 IN EXCLUSIVE MODE NOWAIT;
 LOCK TABLE lock_tbl1 IN ACCESS EXCLUSIVE MODE NOWAIT;
 ROLLBACK;
 
+-- Try using WAIT ONLY along with valid options.
+BEGIN TRANSACTION;
+LOCK TABLE lock_tbl1 IN ACCESS SHARE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN ROW SHARE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN ROW EXCLUSIVE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN SHARE UPDATE EXCLUSIVE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN SHARE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN SHARE ROW EXCLUSIVE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN EXCLUSIVE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN ACCESS EXCLUSIVE MODE WAIT ONLY;
+ROLLBACK;
+
+-- WAIT ONLY is allowed outside a transaction
+LOCK TABLE lock_tbl1 IN ACCESS EXCLUSIVE MODE WAIT ONLY;
+
+-- NOWAIT + WAIT ONLY is not supported (yet?)
+LOCK TABLE lock_tbl1 IN ACCESS EXCLUSIVE MODE NOWAIT WAIT ONLY;
+
 -- Verify that we can lock views.
 BEGIN TRANSACTION;
 LOCK TABLE lock_view1 IN EXCLUSIVE MODE;
@@ -104,9 +122,23 @@ ROLLBACK;
 CREATE TABLE lock_tbl2 (b BIGINT) INHERITS (lock_tbl1);
 CREATE TABLE lock_tbl3 () INHERITS (lock_tbl2);
 BEGIN TRANSACTION;
+LOCK TABLE lock_tbl1 * IN ACCESS EXCLUSIVE MODE WAIT ONLY;
 LOCK TABLE lock_tbl1 * IN ACCESS EXCLUSIVE MODE;
 ROLLBACK;
 
+-- WAIT ONLY requires SELECT permissions regardless of lock mode
+-- fail without permissions
+SET ROLE regress_rol_lock1;
+BEGIN;
+LOCK TABLE ONLY lock_tbl1 IN ACCESS SHARE MODE WAIT ONLY;
+ROLLBACK;
+RESET ROLE;
+-- succeed with only SELECT permissions and ACCESS EXCLUSIVE mode
+GRANT SELECT ON TABLE lock_tbl1 TO regress_rol_lock1;
+LOCK TABLE ONLY lock_tbl1 IN ACCESS EXCLUSIVE MODE WAIT ONLY;
+RESET ROLE;
+REVOKE SELECT ON TABLE lock_tbl1 FROM regress_rol_lock1;
+
 -- Child tables are locked without granting explicit permission to do so as
 -- long as we have permission to lock the parent.
 GRANT UPDATE ON TABLE lock_tbl1 TO regress_rol_lock1;
@@ -116,11 +148,20 @@ BEGIN;
 LOCK TABLE lock_tbl2;
 ROLLBACK;
 BEGIN;
+LOCK TABLE lock_tbl2 WAIT ONLY;
+ROLLBACK;
+BEGIN;
 LOCK TABLE lock_tbl1 * IN ACCESS EXCLUSIVE MODE;
 ROLLBACK;
 BEGIN;
+LOCK TABLE lock_tbl1 * IN ACCESS EXCLUSIVE MODE WAIT ONLY;
+ROLLBACK;
+BEGIN;
 LOCK TABLE ONLY lock_tbl1;
 ROLLBACK;
+BEGIN;
+LOCK TABLE ONLY lock_tbl1 WAIT ONLY;
+ROLLBACK;
 RESET ROLE;
 REVOKE UPDATE ON TABLE lock_tbl1 FROM regress_rol_lock1;
 
-- 
2.34.1

