From 9d5a9b44b29fcbdf427fda7221d068e20dedf60b Mon Sep 17 00:00:00 2001
From: Will Mortensen <will@extrahop.com>
Date: Sat, 23 Dec 2023 01:42:57 -0800
Subject: [PATCH v5 3/3] Add WAIT FOR LOCKERS command

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

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

See docs and tests for more detail.
---
 doc/src/sgml/ref/allfiles.sgml                |   1 +
 doc/src/sgml/ref/lock.sgml                    |   8 +
 doc/src/sgml/ref/wait_for_lockers.sgml        | 305 ++++++++++++++++++
 doc/src/sgml/reference.sgml                   |   1 +
 src/backend/commands/lockcmds.c               |  63 ++++
 src/backend/parser/gram.y                     |  51 ++-
 src/backend/tcop/utility.c                    |  18 ++
 src/include/commands/lockcmds.h               |   5 +
 src/include/nodes/parsenodes.h                |  12 +
 src/include/parser/kwlist.h                   |   2 +
 src/include/tcop/cmdtaglist.h                 |   1 +
 .../expected/deadlock-wait-for-lockers.out    |  12 +
 .../isolation/expected/wait-for-lockers.out   | 144 +++++++++
 src/test/isolation/isolation_schedule         |   2 +
 .../specs/deadlock-wait-for-lockers.spec      |  23 ++
 .../isolation/specs/wait-for-lockers.spec     |  76 +++++
 .../regress/expected/wait_for_lockers.out     |  92 ++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/wait_for_lockers.sql     |  99 ++++++
 19 files changed, 912 insertions(+), 5 deletions(-)
 create mode 100644 doc/src/sgml/ref/wait_for_lockers.sgml
 create mode 100644 src/test/isolation/expected/deadlock-wait-for-lockers.out
 create mode 100644 src/test/isolation/expected/wait-for-lockers.out
 create mode 100644 src/test/isolation/specs/deadlock-wait-for-lockers.spec
 create mode 100644 src/test/isolation/specs/wait-for-lockers.spec
 create mode 100644 src/test/regress/expected/wait_for_lockers.out
 create mode 100644 src/test/regress/sql/wait_for_lockers.sql

diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index fda4690eab..f40be2fd0e 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -188,6 +188,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY update             SYSTEM "update.sgml">
 <!ENTITY vacuum             SYSTEM "vacuum.sgml">
 <!ENTITY values             SYSTEM "values.sgml">
+<!ENTITY waitForLockers     SYSTEM "wait_for_lockers.sgml">
 
 <!-- applications and utilities -->
 <!ENTITY clusterdb          SYSTEM "clusterdb.sgml">
diff --git a/doc/src/sgml/ref/lock.sgml b/doc/src/sgml/ref/lock.sgml
index 6ce2518de7..1893f94619 100644
--- a/doc/src/sgml/ref/lock.sgml
+++ b/doc/src/sgml/ref/lock.sgml
@@ -268,4 +268,12 @@ COMMIT WORK;
    present in <productname>Oracle</productname>.
   </para>
  </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-waitforlockers"/></member>
+  </simplelist>
+ </refsect1>
 </refentry>
diff --git a/doc/src/sgml/ref/wait_for_lockers.sgml b/doc/src/sgml/ref/wait_for_lockers.sgml
new file mode 100644
index 0000000000..f563984c87
--- /dev/null
+++ b/doc/src/sgml/ref/wait_for_lockers.sgml
@@ -0,0 +1,305 @@
+<!--
+doc/src/sgml/ref/wait_for_lockers.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-waitforlockers">
+ <indexterm zone="sql-waitforlockers">
+  <primary>WAIT FOR LOCKERS</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>WAIT FOR LOCKERS</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>WAIT FOR LOCKERS</refname>
+  <refpurpose>wait for table locks to be released</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+WAIT FOR LOCKERS OF [ TABLE ] [ ONLY ] <replaceable class="parameter">name</replaceable> [ * ] [, ...] [ IN [ CONFLICT WITH ] <replaceable class="parameter">lockmode</replaceable> MODE ]
+
+<phrase>where <replaceable class="parameter">lockmode</replaceable> is one of:</phrase>
+
+    ACCESS SHARE | ROW SHARE | ROW EXCLUSIVE | SHARE UPDATE EXCLUSIVE
+    | SHARE | SHARE ROW EXCLUSIVE | EXCLUSIVE | ACCESS EXCLUSIVE
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>WAIT FOR LOCKERS</command> waits for already-held table-level locks
+   to be released, and does not take any new table-level locks.
+  </para>
+
+  <para>
+   <command>WAIT FOR LOCKERS</command> first builds a set of transactions that
+   hold matching locks, and then waits for the transactions in the set to
+   release those locks. The set does not include any transaction that is only
+   waiting to take a matching lock but does not yet hold one, nor any
+   transaction that only takes a matching lock after
+   <command>WAIT FOR LOCKERS</command> finishes building the set. The set may or
+   may not include a transaction that only takes a matching lock while
+   <command>WAIT FOR LOCKERS</command> is building the set. The set never
+   includes the transaction that is building the set, even if it holds a
+   matching lock, because that would trivially deadlock.
+  </para>
+
+  <para>
+   <command>WAIT FOR LOCKERS</command> can be used either inside or outside a
+   transaction block. Within a transaction at the
+   <literal>REPEATABLE READ</literal> or <literal>SERIALIZABLE</literal>
+   isolation level, in order to observe any changes made by the waited-for
+   lockers, you have to execute the <command>WAIT FOR LOCKERS</command>
+   statement before executing any <command>SELECT</command> or data modification
+   statement.  A <literal>REPEATABLE READ</literal> or
+   <literal>SERIALIZABLE</literal> transaction's view of data will be frozen
+   when its first <command>SELECT</command> or data modification statement
+   begins.  A <command>WAIT FOR LOCKERS</command> later in the transaction will
+   still wait for outstanding writes &mdash; but it won't ensure that what the
+   transaction reads afterward corresponds to the latest committed values. More
+   information about transaction isolation levels can be found in
+   <xref linkend="transaction-iso"/>.
+  </para>
+
+  <para>
+   Since <command>WAIT FOR LOCKERS</command> does not take any table-level
+   locks, a table may be dropped by another transaction while waiting for
+   lockers of it. Once the drop commits, there are no more lockers of the table
+   to wait for.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of an existing table, whose lockers
+      are waited for. If <literal>ONLY</literal> is specified before the table
+      name, only lockers of that table are waited for. If
+      <literal>ONLY</literal> is not specified, lockers of the table and all its
+      descendant tables (if any) are waited for.  Optionally,
+      <literal>*</literal> can be specified after the table name to explicitly
+      indicate that descendant tables are included.
+     </para>
+
+     <para>
+      When multiple tables are specified and/or descendant tables are included
+      (either explicitly or implicitly), <command>WAIT FOR LOCKERS</command>
+      builds a single combined set of transactions that hold matching locks on
+      any of the tables, and then waits for the transactions in this combined
+      set to release those locks. This may produce a shorter wait than the
+      one-table-at-a-time approach used by <xref linkend="sql-lock"/>.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>IN</literal></term>
+    <term><literal>IN CONFLICT WITH</literal></term>
+    <listitem>
+     <para>
+      Specifies whether to wait for locks in the following
+      <replaceable class="parameter">lockmode</replaceable>, or locks in modes
+      that conflict with <replaceable class="parameter">lockmode</replaceable>.
+      Note that a lock mode may or may not conflict with itself.  More
+      information about the lock modes and how they conflict can be found in
+      <xref linkend="explicit-locking"/>.
+     </para>
+
+     <para>
+      If this clause is omitted, then
+      <literal>IN CONFLICT WITH ACCESS EXCLUSIVE MODE</literal>, which waits for
+      locks in all lock modes, is used.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">lockmode</replaceable></term>
+    <listitem>
+     <para>
+      The specified lock mode, in conjunction with the previous parameter,
+      determines which lock modes to wait for.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   To wait for locks on a table, the user must have <literal>SELECT</literal>
+   privileges on the table.
+  </para>
+
+  <para>
+   Views are not currently supported.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Example</title>
+
+  <para>
+   <literal>IN ROW EXCLUSIVE MODE</literal> or
+   <literal>IN CONFLICT WITH SHARE MODE</literal> can be used to wait for
+   in-progress writes to be committed or rolled back, while still allowing other
+   transactions to newly acquire conflicting locks and perform writes.  This can
+   be useful in conjunction with non-transactional communication about
+   in-progress writes, such as through sequences.
+  </para>
+
+  <para>
+   For example, given the following table:
+
+<programlisting>
+CREATE TABLE page_views (
+    id bigserial,
+    view_time timestamptz
+);
+</programlisting>
+  </para>
+
+  <para>
+   Assume that this table is never modified except by
+   <xref linkend="sql-insert"/> commands that assign the default value to the
+   <structfield>id</structfield> column. Each transaction executing such an
+   <command>INSERT</command> command will first take a
+   <literal>ROW EXCLUSIVE</literal> lock on <structname>page_views</structname>
+   and then obtain new values for <structfield>id</structfield> from the
+   automatically-created uncached sequence <literal>page_views_id_seq</literal>.
+  </para>
+
+  <para>
+   To observe the progress of insertions, we first use the
+   <literal>pg_sequence_last_value</literal> function to obtain the last (and
+   thus highest) value of <structfield>id</structfield> used by an
+   <command>INSERT</command> command so far:
+
+<programlisting>
+SELECT pg_sequence_last_value('page_views_id_seq');
+
+ pg_sequence_last_value
+------------------------
+                      4
+</programlisting>
+  </para>
+
+  <para>
+   Since some of the transactions that used <structfield>id</structfield> values
+   less than or equal to 4 may not have committed or rolled back yet, we wait
+   for them:
+
+<programlisting>
+WAIT FOR LOCKERS OF page_views IN ROW EXCLUSIVE MODE;
+</programlisting>
+  </para>
+
+  <para>
+   After <literal>WAIT FOR LOCKERS</literal> returns, we know that all rows
+   where <literal>id &lt;= 4</literal> have been committed or rolled back. Any
+   rows that are committed after this must have <literal>id &gt; 4</literal>.
+   Then we execute:
+
+<programlisting>
+SELECT FROM page_views WHERE id &lt;= 4;
+
+ id |           view_time
+----+-------------------------------
+  2 | 2024-01-01 12:34:01.000000-00
+  3 | 2024-01-01 12:34:00.000000-00
+</programlisting>
+  </para>
+
+  <para>
+   We know that, going forward, these two rows are the only rows with
+   <literal>id &lt;= 4</literal> that can ever exist. (We might also conclude
+   that some transaction(s) tried to insert rows with
+   <structfield>id</structfield> values of 1 and 4, but if so, they must have
+   rolled back.)
+  </para>
+
+  <para>
+   For this to work, the second <command>SELECT</command> command must see a
+   snapshot of the database taken after <literal>WAIT FOR LOCKERS</literal>
+   returned. For example, it cannot be in the same
+   <literal>REPEATABLE READ</literal> or <literal>SERIALIZABLE</literal>
+   transaction as the first <command>SELECT</command> command, because that
+   would have taken the transaction's snapshot too early.
+  </para>
+
+  <para>
+   Note that some rows with <literal>id &gt; 4</literal> might have been
+   committed already, such as while we were executing
+   <literal>WAIT FOR LOCKERS</literal>, and of course more may be committed in
+   the future.
+  </para>
+
+  <para>
+   We can continue to observe new rows by iterating again:
+
+<programlisting>
+SELECT pg_sequence_last_value('page_views_id_seq');
+
+ pg_sequence_last_value
+------------------------
+                      9
+</programlisting>
+
+<programlisting>
+WAIT FOR LOCKERS OF page_views IN ROW EXCLUSIVE MODE;
+</programlisting>
+  </para>
+
+  <para>
+   We already observed all of the rows where <literal>id &lt;= 4</literal>, so
+   this time we can filter them out:
+
+<programlisting>
+SELECT FROM page_views WHERE id &gt; 4 AND id &lt;= 9;
+
+ id |           view_time
+----+-------------------------------
+  5 | 2024-01-01 12:34:05.000000-00
+  8 | 2024-01-01 12:34:04.000000-00
+  9 | 2024-01-01 12:34:07.000000-00
+</programlisting>
+  </para>
+
+  <para>
+   These three rows and the two rows we observed above are the only rows with
+   <literal>id &lt;= 9</literal> that can ever exist going forward. We can
+   continue iterating like this to incrementally observe more newly inserted
+   rows.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>WAIT FOR LOCKERS</command> in the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-lock"/></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index a07d2b5e01..cd827a12ca 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -216,6 +216,7 @@
    &update;
    &vacuum;
    &values;
+   &waitForLockers;
 
  </reference>
 
diff --git a/src/backend/commands/lockcmds.c b/src/backend/commands/lockcmds.c
index 29e9953bf4..c15db21ea7 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"
@@ -29,6 +30,7 @@
 #include "utils/syscache.h"
 
 static void LockTableRecurse(Oid reloid, LOCKMODE lockmode, bool nowait);
+static LOCKTAG *GetLocktag(Oid reloid);
 static AclResult LockTableAclCheck(Oid reloid, LOCKMODE lockmode, Oid userid);
 static void RangeVarCallbackForLockTable(const RangeVar *rv, Oid relid,
 										 Oid oldrelid, void *arg);
@@ -64,6 +66,49 @@ LockTableCommand(LockStmt *lockstmt)
 	}
 }
 
+/*
+ * WAIT FOR LOCKERS
+ */
+void
+WaitForLockersCommand(WaitForLockersStmt *waitstmt)
+{
+	ListCell   *p;
+	List	   *locktags = NIL;
+
+	/*
+	 * Iterate over the list and process the named relations one at a time
+	 */
+	foreach(p, waitstmt->relations)
+	{
+		RangeVar   *rv = (RangeVar *) lfirst(p);
+		bool		recurse = rv->inh;
+		LOCKMODE	nolock = NoLock;
+		Oid			reloid;
+
+		reloid = RangeVarGetRelidExtended(rv, NoLock, 0,
+										  RangeVarCallbackForLockTable,
+										  (void *) &nolock);
+		if (get_rel_relkind(reloid) == RELKIND_VIEW)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("WAIT FOR LOCKERS is not supported with views")));
+
+		if (recurse)
+		{
+			List	   *children;
+
+			children = find_all_inheritors(reloid, NoLock, NULL);
+
+			foreach_oid(childreloid, children)
+				locktags = lappend(locktags, GetLocktag(childreloid));
+		}
+		else
+			locktags = lappend(locktags, GetLocktag(reloid));
+	}
+	WaitForLockersMultiple(locktags, waitstmt->mode, waitstmt->conflicting,
+						   false);
+}
+
 /*
  * Before acquiring a table lock on the named table, check whether we have
  * permission to do so.
@@ -158,6 +203,24 @@ LockTableRecurse(Oid reloid, LOCKMODE lockmode, bool nowait)
 	}
 }
 
+/*
+ * Get locktag for a single rel
+ */
+static LOCKTAG *
+GetLocktag(Oid reloid)
+{
+	LOCKTAG	   *heaplocktag;
+	Oid			dbid;
+
+	heaplocktag = palloc_object(LOCKTAG);
+	if (IsSharedRelation(reloid))
+		dbid = InvalidOid;
+	else
+		dbid = MyDatabaseId;
+	SET_LOCKTAG_RELATION(*heaplocktag, dbid, reloid);
+	return heaplocktag;
+}
+
 /*
  * Apply LOCK TABLE recursively over a view
  *
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 6b88096e8e..ce14a1f1b3 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -319,6 +319,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 		CreateMatViewStmt RefreshMatViewStmt CreateAmStmt
 		CreatePublicationStmt AlterPublicationStmt
 		CreateSubscriptionStmt AlterSubscriptionStmt DropSubscriptionStmt
+		WaitForLockersStmt
 
 %type <node>	select_no_parens select_with_parens select_clause
 				simple_select values_clause
@@ -345,7 +346,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				transaction_mode_item
 				create_extension_opt_item alter_extension_opt_item
 
-%type <ival>	opt_lock lock_type cast_context
+%type <ival>	opt_lock lock_type opt_wait_lock_modes cast_context
 %type <str>		utility_option_name
 %type <defelt>	utility_option_elem
 %type <list>	utility_option_list
@@ -353,7 +354,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_conflict_with opt_if_exists opt_with_data
 				opt_transaction_chain
 %type <list>	grant_role_opt_list
 %type <defelt>	grant_role_opt
@@ -729,7 +730,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 	LABEL LANGUAGE LARGE_P LAST_P LATERAL_P
 	LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL
-	LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED
+	LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOCKERS LOGGED
 
 	MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD
 	MINUTE_P MINVALUE MODE MONTH_P MOVE
@@ -773,7 +774,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
@@ -1102,6 +1103,7 @@ stmt:
 			| VariableSetStmt
 			| VariableShowStmt
 			| ViewStmt
+			| WaitForLockersStmt
 			| /*EMPTY*/
 				{ $$ = NULL; }
 		;
@@ -12267,6 +12269,43 @@ opt_nowait_or_skip:
 		;
 
 
+/*****************************************************************************
+ *
+ *		QUERY:
+ *				WAIT FOR LOCKERS
+ *
+ *****************************************************************************/
+
+WaitForLockersStmt:
+			WAIT FOR LOCKERS OF opt_table relation_expr_list opt_wait_lock_modes
+				{
+					WaitForLockersStmt *n = makeNode(WaitForLockersStmt);
+
+					n->relations = $6;
+					/* XXX: see opt_wait_lock_modes */
+					n->mode = $7 & ((1 << MaxLockMode) - 1);
+					n->conflicting = ($7 >> MaxLockMode) != 0;
+					$$ = (Node *) n;
+				}
+		;
+
+opt_wait_lock_modes:
+			/*
+			 * XXX: hackily store a bool and a lock mode in an int; should
+			 * probably make a new Node type?
+			 */
+			IN_P opt_conflict_with lock_type MODE
+					{ $$ = ((int)$2 << MaxLockMode) | $3; }
+			| /*EMPTY*/
+					{ $$ = (1 << MaxLockMode) | AccessExclusiveLock; }
+		;
+
+opt_conflict_with:
+			CONFLICT WITH					{ $$ = true; }
+			| /*EMPTY*/						{ $$ = false; }
+		;
+
+
 /*****************************************************************************
  *
  *		QUERY:
@@ -17265,6 +17304,7 @@ unreserved_keyword:
 			| LOCATION
 			| LOCK_P
 			| LOCKED
+			| LOCKERS
 			| LOGGED
 			| MAPPING
 			| MATCH
@@ -17424,6 +17464,7 @@ unreserved_keyword:
 			| VIEW
 			| VIEWS
 			| VOLATILE
+			| WAIT
 			| WHITESPACE_P
 			| WITHIN
 			| WITHOUT
@@ -17855,6 +17896,7 @@ bare_label_keyword:
 			| LOCATION
 			| LOCK_P
 			| LOCKED
+			| LOCKERS
 			| LOGGED
 			| MAPPING
 			| MATCH
@@ -18055,6 +18097,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 8de821f960..053c576440 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -360,6 +360,11 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 					return COMMAND_IS_STRICTLY_READ_ONLY;
 			}
 
+		case T_WaitForLockersStmt:
+			{
+				return COMMAND_IS_STRICTLY_READ_ONLY;
+			}
+
 		case T_TransactionStmt:
 			{
 				TransactionStmt *stmt = (TransactionStmt *) parsetree;
@@ -941,6 +946,11 @@ standard_ProcessUtility(PlannedStmt *pstmt,
 			LockTableCommand((LockStmt *) parsetree);
 			break;
 
+		case T_WaitForLockersStmt:
+
+			WaitForLockersCommand((WaitForLockersStmt *) parsetree);
+			break;
+
 		case T_ConstraintsSetStmt:
 			WarnNoTransactionBlock(isTopLevel, "SET CONSTRAINTS");
 			AfterTriggerSetState((ConstraintsSetStmt *) parsetree);
@@ -2993,6 +3003,10 @@ CreateCommandTag(Node *parsetree)
 			tag = CMDTAG_LOCK_TABLE;
 			break;
 
+		case T_WaitForLockersStmt:
+			tag = CMDTAG_WAIT_FOR_LOCKERS;
+			break;
+
 		case T_ConstraintsSetStmt:
 			tag = CMDTAG_SET_CONSTRAINTS;
 			break;
@@ -3614,6 +3628,10 @@ GetCommandLogLevel(Node *parsetree)
 			lev = LOGSTMT_ALL;
 			break;
 
+		case T_WaitForLockersStmt:
+			lev = LOGSTMT_ALL;
+			break;
+
 		case T_ConstraintsSetStmt:
 			lev = LOGSTMT_ALL;
 			break;
diff --git a/src/include/commands/lockcmds.h b/src/include/commands/lockcmds.h
index c3b2839f3f..53a5e662fc 100644
--- a/src/include/commands/lockcmds.h
+++ b/src/include/commands/lockcmds.h
@@ -21,4 +21,9 @@
  */
 extern void LockTableCommand(LockStmt *lockstmt);
 
+/*
+ * WAIT FOR LOCKERS
+ */
+extern void WaitForLockersCommand(WaitForLockersStmt *lockstmt);
+
 #endif							/* LOCKCMDS_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index b3181f34ae..9ae94cf46c 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3798,6 +3798,18 @@ typedef struct LockStmt
 	bool		nowait;			/* no wait mode */
 } LockStmt;
 
+/* ----------------------
+ *		WAIT FOR LOCKERS Statement
+ * ----------------------
+ */
+typedef struct WaitForLockersStmt
+{
+	NodeTag		type;
+	List	   *relations;		/* relations to wait for */
+	int			mode;			/* lock mode */
+	bool		conflicting;	/* wait for locks conflicting with mode? */
+} WaitForLockersStmt;
+
 /* ----------------------
  *		SET CONSTRAINTS Statement
  * ----------------------
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 2331acac09..019e66d3e8 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -259,6 +259,7 @@ PG_KEYWORD("localtimestamp", LOCALTIMESTAMP, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("location", LOCATION, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("lock", LOCK_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("locked", LOCKED, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("lockers", LOCKERS, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("logged", LOGGED, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("mapping", MAPPING, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("match", MATCH, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -473,6 +474,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/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 7fdcec6dd9..f0925a98db 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -217,3 +217,4 @@ PG_CMDTAG(CMDTAG_TRUNCATE_TABLE, "TRUNCATE TABLE", false, false, false)
 PG_CMDTAG(CMDTAG_UNLISTEN, "UNLISTEN", false, false, false)
 PG_CMDTAG(CMDTAG_UPDATE, "UPDATE", false, false, true)
 PG_CMDTAG(CMDTAG_VACUUM, "VACUUM", false, false, false)
+PG_CMDTAG(CMDTAG_WAIT_FOR_LOCKERS, "WAIT FOR LOCKERS", false, false, false)
diff --git a/src/test/isolation/expected/deadlock-wait-for-lockers.out b/src/test/isolation/expected/deadlock-wait-for-lockers.out
new file mode 100644
index 0000000000..2241a7e999
--- /dev/null
+++ b/src/test/isolation/expected/deadlock-wait-for-lockers.out
@@ -0,0 +1,12 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1lre s2las s2wfl s1wfl s1c s2c
+step s1lre: LOCK TABLE a1 IN ROW EXCLUSIVE MODE;
+step s2las: LOCK TABLE a1 IN ACCESS SHARE MODE;
+step s2wfl: WAIT FOR LOCKERS OF TABLE a1 IN CONFLICT WITH SHARE MODE; <waiting ...>
+step s1wfl: WAIT FOR LOCKERS OF TABLE a1 IN CONFLICT WITH ACCESS EXCLUSIVE MODE; <waiting ...>
+step s1wfl: <... completed>
+step s2wfl: <... completed>
+ERROR:  deadlock detected
+step s1c: COMMIT;
+step s2c: COMMIT;
diff --git a/src/test/isolation/expected/wait-for-lockers.out b/src/test/isolation/expected/wait-for-lockers.out
new file mode 100644
index 0000000000..55e556d964
--- /dev/null
+++ b/src/test/isolation/expected/wait-for-lockers.out
@@ -0,0 +1,144 @@
+Parsed test spec with 3 sessions
+
+starting permutation: w1_lae2 w2_in1 w2_c w1_c r_wflic r_sel1 r_c
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2_c: COMMIT;
+step w1_c: COMMIT;
+step r_wflic: WAIT FOR LOCKERS OF TABLE t1, t2 IN CONFLICT WITH SHARE MODE;
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step r_c: COMMIT;
+
+starting permutation: w1_lae2 w2_in1 r_wfl w2_c r_sel1 w1_c r_c
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_wfl: WAIT FOR LOCKERS OF TABLE t1, t2 IN ROW EXCLUSIVE MODE; <waiting ...>
+step w2_c: COMMIT;
+step r_wfl: <... completed>
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step w1_c: COMMIT;
+step r_c: COMMIT;
+
+starting permutation: w1_lae2 w2_in1 r_wflic w2_c w1_c r_sel1 r_c
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_wflic: WAIT FOR LOCKERS OF TABLE t1, t2 IN CONFLICT WITH SHARE MODE; <waiting ...>
+step w2_c: COMMIT;
+step w1_c: COMMIT;
+step r_wflic: <... completed>
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step r_c: COMMIT;
+
+starting permutation: w1_in1 r_wflic w2_in1 w2_c w1_c r_sel1 r_c
+step w1_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_wflic: WAIT FOR LOCKERS OF TABLE t1, t2 IN CONFLICT WITH SHARE MODE; <waiting ...>
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2_c: COMMIT;
+step w1_c: COMMIT;
+step r_wflic: <... completed>
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+ 2
+(2 rows)
+
+step r_c: COMMIT;
+
+starting permutation: w1_in1 r_sv r_l w2_in1 w1_c r_rb w2_c r_sel1 r_c
+step w1_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_sv: SAVEPOINT foo;
+step r_l: LOCK TABLE t1, t2 IN SHARE MODE; <waiting ...>
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT); <waiting ...>
+step w1_c: COMMIT;
+step r_l: <... completed>
+step r_rb: ROLLBACK TO foo;
+step w2_in1: <... completed>
+step w2_c: COMMIT;
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+ 2
+(2 rows)
+
+step r_c: COMMIT;
+
+starting permutation: w2_in1 r_wflic w1_lae2 w1_in1 w2_c r_sel1 w1_c r_c
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_wflic: WAIT FOR LOCKERS OF TABLE t1, t2 IN CONFLICT WITH SHARE MODE; <waiting ...>
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w1_in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2_c: COMMIT;
+step r_wflic: <... completed>
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step w1_c: COMMIT;
+step r_c: COMMIT;
+
+starting permutation: w2_in1 r_sv r_l w1_lae2 w2_c w1_c r_rb r_sel1 r_c
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_sv: SAVEPOINT foo;
+step r_l: LOCK TABLE t1, t2 IN SHARE MODE; <waiting ...>
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2_c: COMMIT;
+step w1_c: COMMIT;
+step r_l: <... completed>
+step r_rb: ROLLBACK TO foo;
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step r_c: COMMIT;
+
+starting permutation: w1_lae1 w2_in1 r_wflic w1_c r_sel1 w2_c r_c
+step w1_lae1: LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT); <waiting ...>
+step r_wflic: WAIT FOR LOCKERS OF TABLE t1, t2 IN CONFLICT WITH SHARE MODE; <waiting ...>
+step w1_c: COMMIT;
+step w2_in1: <... completed>
+step r_wflic: <... completed>
+step r_sel1: SELECT id from t1;
+id
+--
+(0 rows)
+
+step w2_c: COMMIT;
+step r_c: COMMIT;
+
+starting permutation: w1_lae1 w2_in1 r_l w1_c w2_c r_sel1 r_c
+step w1_lae1: LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT); <waiting ...>
+step r_l: LOCK TABLE t1, t2 IN SHARE MODE; <waiting ...>
+step w1_c: COMMIT;
+step w2_in1: <... completed>
+step w2_c: COMMIT;
+step r_l: <... completed>
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step r_c: COMMIT;
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b2be88ead1..b7380627d7 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-for-lockers
 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-for-lockers
 test: deadlock-hard
 test: deadlock-soft
 test: deadlock-soft-2
diff --git a/src/test/isolation/specs/deadlock-wait-for-lockers.spec b/src/test/isolation/specs/deadlock-wait-for-lockers.spec
new file mode 100644
index 0000000000..1b34e3cb9b
--- /dev/null
+++ b/src/test/isolation/specs/deadlock-wait-for-lockers.spec
@@ -0,0 +1,23 @@
+setup
+{
+	CREATE TABLE a1 ();
+}
+
+teardown
+{
+	DROP TABLE a1;
+}
+
+session s1
+setup		{ BEGIN; }
+step s1lre	{ LOCK TABLE a1 IN ROW EXCLUSIVE MODE; }
+step s1wfl	{ WAIT FOR LOCKERS OF TABLE a1 IN CONFLICT WITH ACCESS EXCLUSIVE MODE; }
+step s1c	{ COMMIT; }
+
+session s2
+setup		{ BEGIN; }
+step s2las	{ LOCK TABLE a1 IN ACCESS SHARE MODE; }
+step s2wfl	{ WAIT FOR LOCKERS OF TABLE a1 IN CONFLICT WITH SHARE MODE; }
+step s2c	{ COMMIT; }
+
+permutation s1lre s2las s2wfl s1wfl s1c s2c
diff --git a/src/test/isolation/specs/wait-for-lockers.spec b/src/test/isolation/specs/wait-for-lockers.spec
new file mode 100644
index 0000000000..afdb6afcb8
--- /dev/null
+++ b/src/test/isolation/specs/wait-for-lockers.spec
@@ -0,0 +1,76 @@
+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 w1_in1		{ INSERT INTO t1 VALUES (DEFAULT); }
+step w1_lae1	{ LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE; }
+step w1_lae2	{ LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE; }
+step w1_c	{ COMMIT; }
+
+session writer2
+setup		{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step w2_in1	{ INSERT INTO t1 VALUES (DEFAULT); }
+step w2_c	{ COMMIT; }
+
+session reader
+setup			{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step r_sv		{ SAVEPOINT foo; }
+step r_l		{ LOCK TABLE t1, t2 IN SHARE MODE; }
+step r_rb		{ ROLLBACK TO foo; }
+step r_wfl		{ WAIT FOR LOCKERS OF TABLE t1, t2 IN ROW EXCLUSIVE MODE; }
+step r_wflic	{ WAIT FOR LOCKERS OF TABLE t1, t2 IN CONFLICT WITH SHARE MODE; }
+step r_sel1		{ SELECT id from t1; }
+step r_c		{ COMMIT; }
+
+
+# Basic sanity checks of WAIT FOR LOCKERS:
+
+# no waiting if no lockers (writers already committed)
+permutation w1_lae2 w2_in1 w2_c w1_c r_wflic r_sel1 r_c
+
+# reader waits only for writer2 holding a lock in ROW EXCLUSIVE mode, not for
+# writer1 holding a lock in ACCESS EXCLUSIVE mode
+permutation w1_lae2 w2_in1 r_wfl w2_c r_sel1 w1_c r_c
+
+# reader waits for both writers conflicting with SHARE mode
+permutation w1_lae2 w2_in1 r_wflic w2_c w1_c r_sel1 r_c
+
+
+# Comparisons between WAIT FOR LOCKERS and nearest equivalent LOCK + ROLLBACK:
+
+# reader waiting for writer1 allows writer2 to take a matching lock...
+permutation w1_in1 r_wflic w2_in1 w2_c w1_c r_sel1 r_c
+# ...whereas reader actually taking a conflicting lock blocks writer2 until
+# writer1 releases its lock (even if reader releases ASAP)
+permutation w1_in1 r_sv r_l w2_in1 w1_c r_rb w2_c r_sel1 r_c
+
+# reader waiting for two tables, with only writer2 holding a matching ROW
+# EXCLUSIVE lock, allows writer1 to then take an ACCESS EXCLUSIVE lock on t2 and
+# another ROW EXCLUSIVE lock on t1, and reader doesn't wait for writer1's later
+# locks...
+permutation w2_in1 r_wflic w1_lae2 w1_in1 w2_c r_sel1 w1_c r_c
+# ...whereas reader actually taking conflicting locks on the two tables first
+# waits for writer2's ROW EXCLUSIVE lock (same as above), and then for writer1's
+# *later* ACCESS EXCLUSIVE lock (due to LOCK's one-by-one locking); note that
+# writer1's later insert w1_in1 would deadlock so it's omitted altogether
+permutation w2_in1 r_sv r_l w1_lae2 w2_c w1_c r_rb r_sel1 r_c
+
+# reader waits only for matching lock already held by writer1, not for writer2
+# which was waiting to take a matching lock...
+permutation w1_lae1 w2_in1 r_wflic w1_c r_sel1 w2_c r_c
+# ...whereas actually taking a conflicting lock also waits for writer2 to
+# release its lock
+permutation w1_lae1 w2_in1 r_l w1_c w2_c r_sel1 r_c
diff --git a/src/test/regress/expected/wait_for_lockers.out b/src/test/regress/expected/wait_for_lockers.out
new file mode 100644
index 0000000000..91d3502267
--- /dev/null
+++ b/src/test/regress/expected/wait_for_lockers.out
@@ -0,0 +1,92 @@
+--
+-- Test the WAIT FOR LOCKERS statement
+--
+-- directory paths and dlsuffix are passed to us in environment variables
+\getenv libdir PG_LIBDIR
+\getenv dlsuffix PG_DLSUFFIX
+\set regresslib :libdir '/regress' :dlsuffix
+-- Setup
+CREATE SCHEMA wfl_schema1;
+SET search_path = wfl_schema1;
+CREATE TABLE wfl_tbl1 (a BIGINT);
+CREATE ROLE regress_rol_wfl1;
+ALTER ROLE regress_rol_wfl1 SET search_path = wfl_schema1;
+GRANT USAGE ON SCHEMA wfl_schema1 TO regress_rol_wfl1;
+-- Try all valid options; also try omitting the optional TABLE keyword.
+BEGIN TRANSACTION;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN ACCESS SHARE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN ROW SHARE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN ROW EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN SHARE UPDATE EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN SHARE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN SHARE ROW EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF wfl_tbl1 IN EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN ACCESS EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN CONFLICT WITH ACCESS SHARE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN CONFLICT WITH ROW SHARE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN CONFLICT WITH ROW EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF wfl_tbl1 IN CONFLICT WITH SHARE UPDATE EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN CONFLICT WITH SHARE MODE ;
+WAIT FOR LOCKERS OF wfl_tbl1 IN CONFLICT WITH SHARE ROW EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN CONFLICT WITH EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF wfl_tbl1 IN CONFLICT WITH ACCESS EXCLUSIVE MODE;
+ROLLBACK;
+-- WAIT FOR LOCKERS does nothing if the transaction itself is the only locker
+BEGIN TRANSACTION;
+LOCK TABLE wfl_tbl1 IN ACCESS EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN CONFLICT WITH ACCESS EXCLUSIVE MODE;
+ROLLBACK;
+-- WAIT FOR LOCKERS is allowed outside a transaction
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN ACCESS EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN CONFLICT WITH ACCESS EXCLUSIVE MODE;
+-- Verify that we can wait for a table with inheritance children.
+CREATE TABLE wfl_tbl2 (b BIGINT) INHERITS (wfl_tbl1);
+CREATE TABLE wfl_tbl3 () INHERITS (wfl_tbl2);
+BEGIN TRANSACTION;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 * IN ACCESS EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 * IN CONFLICT WITH ACCESS EXCLUSIVE MODE;
+ROLLBACK;
+-- WAIT FOR LOCKERS requires SELECT permissions regardless of lock mode
+-- fail without permissions
+SET ROLE regress_rol_wfl1;
+BEGIN;
+WAIT FOR LOCKERS OF TABLE ONLY wfl_tbl1 IN ACCESS SHARE MODE;
+ERROR:  permission denied for table wfl_tbl1
+ROLLBACK;
+BEGIN;
+WAIT FOR LOCKERS OF TABLE ONLY wfl_tbl1 IN CONFLICT WITH ACCESS SHARE MODE;
+ERROR:  permission denied for table wfl_tbl1
+ROLLBACK;
+RESET ROLE;
+-- succeed with only SELECT permissions and ACCESS EXCLUSIVE mode
+GRANT SELECT ON TABLE wfl_tbl1 TO regress_rol_wfl1;
+WAIT FOR LOCKERS OF TABLE ONLY wfl_tbl1 IN ACCESS EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE ONLY wfl_tbl1 IN CONFLICT WITH ACCESS EXCLUSIVE MODE;
+RESET ROLE;
+REVOKE SELECT ON TABLE wfl_tbl1 FROM regress_rol_wfl1;
+-- Child tables can be waited on without granting explicit permission to do so
+-- as long as we have permission to lock the parent.
+GRANT UPDATE ON TABLE wfl_tbl1 TO regress_rol_wfl1;
+SET ROLE regress_rol_wfl1;
+-- fail when child waited for directly
+BEGIN;
+WAIT FOR LOCKERS OF TABLE wfl_tbl2;
+ERROR:  permission denied for table wfl_tbl2
+ROLLBACK;
+BEGIN;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 *;
+ROLLBACK;
+BEGIN;
+WAIT FOR LOCKERS OF TABLE ONLY wfl_tbl1;
+ROLLBACK;
+RESET ROLE;
+REVOKE UPDATE ON TABLE wfl_tbl1 FROM regress_rol_wfl1;
+--
+-- Clean up
+--
+DROP TABLE wfl_tbl3;
+DROP TABLE wfl_tbl2;
+DROP TABLE wfl_tbl1;
+DROP SCHEMA wfl_schema1 CASCADE;
+DROP ROLE regress_rol_wfl1;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index f0987ff537..d2ec0a6a86 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -48,7 +48,7 @@ test: create_index create_index_spgist create_view index_including index_includi
 # ----------
 # Another group of parallel tests
 # ----------
-test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse
+test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse wait_for_lockers
 
 # ----------
 # sanity_check does a vacuum, affecting the sort order of SELECT *
diff --git a/src/test/regress/sql/wait_for_lockers.sql b/src/test/regress/sql/wait_for_lockers.sql
new file mode 100644
index 0000000000..695ac32de0
--- /dev/null
+++ b/src/test/regress/sql/wait_for_lockers.sql
@@ -0,0 +1,99 @@
+--
+-- Test the WAIT FOR LOCKERS statement
+--
+
+-- directory paths and dlsuffix are passed to us in environment variables
+\getenv libdir PG_LIBDIR
+\getenv dlsuffix PG_DLSUFFIX
+
+\set regresslib :libdir '/regress' :dlsuffix
+
+-- Setup
+CREATE SCHEMA wfl_schema1;
+SET search_path = wfl_schema1;
+CREATE TABLE wfl_tbl1 (a BIGINT);
+CREATE ROLE regress_rol_wfl1;
+ALTER ROLE regress_rol_wfl1 SET search_path = wfl_schema1;
+GRANT USAGE ON SCHEMA wfl_schema1 TO regress_rol_wfl1;
+
+-- Try all valid options; also try omitting the optional TABLE keyword.
+BEGIN TRANSACTION;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN ACCESS SHARE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN ROW SHARE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN ROW EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN SHARE UPDATE EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN SHARE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN SHARE ROW EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF wfl_tbl1 IN EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN ACCESS EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN CONFLICT WITH ACCESS SHARE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN CONFLICT WITH ROW SHARE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN CONFLICT WITH ROW EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF wfl_tbl1 IN CONFLICT WITH SHARE UPDATE EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN CONFLICT WITH SHARE MODE ;
+WAIT FOR LOCKERS OF wfl_tbl1 IN CONFLICT WITH SHARE ROW EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN CONFLICT WITH EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF wfl_tbl1 IN CONFLICT WITH ACCESS EXCLUSIVE MODE;
+ROLLBACK;
+
+-- WAIT FOR LOCKERS does nothing if the transaction itself is the only locker
+BEGIN TRANSACTION;
+LOCK TABLE wfl_tbl1 IN ACCESS EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN CONFLICT WITH ACCESS EXCLUSIVE MODE;
+ROLLBACK;
+
+-- WAIT FOR LOCKERS is allowed outside a transaction
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN ACCESS EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN CONFLICT WITH ACCESS EXCLUSIVE MODE;
+
+-- Verify that we can wait for a table with inheritance children.
+CREATE TABLE wfl_tbl2 (b BIGINT) INHERITS (wfl_tbl1);
+CREATE TABLE wfl_tbl3 () INHERITS (wfl_tbl2);
+BEGIN TRANSACTION;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 * IN ACCESS EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 * IN CONFLICT WITH ACCESS EXCLUSIVE MODE;
+ROLLBACK;
+
+-- WAIT FOR LOCKERS requires SELECT permissions regardless of lock mode
+-- fail without permissions
+SET ROLE regress_rol_wfl1;
+BEGIN;
+WAIT FOR LOCKERS OF TABLE ONLY wfl_tbl1 IN ACCESS SHARE MODE;
+ROLLBACK;
+BEGIN;
+WAIT FOR LOCKERS OF TABLE ONLY wfl_tbl1 IN CONFLICT WITH ACCESS SHARE MODE;
+ROLLBACK;
+RESET ROLE;
+-- succeed with only SELECT permissions and ACCESS EXCLUSIVE mode
+GRANT SELECT ON TABLE wfl_tbl1 TO regress_rol_wfl1;
+WAIT FOR LOCKERS OF TABLE ONLY wfl_tbl1 IN ACCESS EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE ONLY wfl_tbl1 IN CONFLICT WITH ACCESS EXCLUSIVE MODE;
+RESET ROLE;
+REVOKE SELECT ON TABLE wfl_tbl1 FROM regress_rol_wfl1;
+
+-- Child tables can be waited on without granting explicit permission to do so
+-- as long as we have permission to lock the parent.
+GRANT UPDATE ON TABLE wfl_tbl1 TO regress_rol_wfl1;
+SET ROLE regress_rol_wfl1;
+-- fail when child waited for directly
+BEGIN;
+WAIT FOR LOCKERS OF TABLE wfl_tbl2;
+ROLLBACK;
+BEGIN;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 *;
+ROLLBACK;
+BEGIN;
+WAIT FOR LOCKERS OF TABLE ONLY wfl_tbl1;
+ROLLBACK;
+RESET ROLE;
+REVOKE UPDATE ON TABLE wfl_tbl1 FROM regress_rol_wfl1;
+
+--
+-- Clean up
+--
+DROP TABLE wfl_tbl3;
+DROP TABLE wfl_tbl2;
+DROP TABLE wfl_tbl1;
+DROP SCHEMA wfl_schema1 CASCADE;
+DROP ROLE regress_rol_wfl1;
-- 
2.34.1

