From 00d3415953879fdc4e89527ee6c4a6f2a9996a3c Mon Sep 17 00:00:00 2001
From: Will Mortensen <will@extrahop.com>
Date: Sat, 23 Dec 2023 01:42:57 -0800
Subject: [PATCH v4 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/wait_for_lockers.sgml        | 271 ++++++++++++++++++
 doc/src/sgml/reference.sgml                   |   1 +
 src/backend/commands/lockcmds.c               |  77 +++++
 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     |  64 +++++
 .../regress/expected/wait_for_lockers.out     |  87 ++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/wait_for_lockers.sql     |  93 ++++++
 18 files changed, 861 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/wait_for_lockers.sgml b/doc/src/sgml/ref/wait_for_lockers.sgml
new file mode 100644
index 0000000000..2ea49c64f3
--- /dev/null
+++ b/doc/src/sgml/ref/wait_for_lockers.sgml
@@ -0,0 +1,271 @@
+<!--
+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.  It does not wait for locks that are taken after it starts
+   waiting, even if the locker was already waiting to take the lock.  It does
+   not take any table-level locks.
+  </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.
+  </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. Once the
+   drop commits, there cannot be any remaining locks on the table to wait for.
+  </para>
+
+  <para>
+   As with any command that potentially waits for other transactions,
+   <command>WAIT FOR LOCKERS</command> can participate in deadlocks if it waits
+   for another transaction that transitively waits for its transaction, although
+   this is generally less likely than when actually taking a lock. Also note
+   that a transaction will not wait for its own locks, just as its own locks do
+   not conflict.
+  </para>
+
+  <para>
+   More information about the lock modes and how they conflict can be
+   found in <xref linkend="explicit-locking"/>.
+  </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 on which to
+      wait. If <literal>ONLY</literal> is specified before the table name, only
+      that table is waited for. If <literal>ONLY</literal> is not specified, 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>
+      With multiple tables, <command>WAIT FOR LOCKERS</command> first obtains
+      the combined set of matching locks for all tables, and then waits on all
+      of those locks. This differs from the behavior of
+      <xref linkend="sql-lock"/> with multiple tables.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>IN</literal></term>
+    <term><literal>IN CONFLICT WITH</literal></term>
+    <listitem>
+     <para>
+      Specifies whether to wait for locks only in exactly the following
+      <replaceable class="parameter">lockmode</replaceable>, or in all modes
+      that conflict with <replaceable class="parameter">lockmode</replaceable>
+      (note that <replaceable class="parameter">lockmode</replaceable> may or
+      may not conflict with itself).  Lock modes and their conflicts are
+      described in <xref linkend="explicit-locking"/>.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">lockmode</replaceable></term>
+    <listitem>
+     <para>
+      The lock mode (in conjunction with the previous clause) determines which
+      locks to wait for. If no lock mode is specified, then
+      <literal>IN CONFLICT WITH ACCESS EXCLUSIVE</literal>, the most restrictive
+      set of options, is used.
+     </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, imagine we have a table of page views:
+
+<programlisting>
+CREATE TABLE page_views (
+    id bigserial,
+    view_time timestamptz default now(),
+    domain text
+);
+</programlisting>
+  </para>
+
+  <para>
+   And say that we want to maintain a separate table of view counts rolled up by
+   <literal>domain</literal> for fast retrieval, but we don't want to update it
+   on every page view:
+
+<programlisting>
+CREATE TABLE page_view_rollup (
+    domain text primary key,
+    view_count bigint
+);
+</programlisting>
+  </para>
+
+  <para>
+   Assume that the <literal>page_views</literal> table is only ever modified by
+   <command>INSERT</command> commands that assign the default value to the
+   <literal>id</literal> column, which is taken from the automatically-created
+   <literal>page_views_id_seq</literal> sequence, which is uncached. Then we
+   know that the values of <literal>id</literal> in the rows being inserted are
+   obtained from the sequence after <command>INSERT</command> takes its
+   <literal>ROW EXCLUSIVE</literal> lock on <literal>page_views</literal>. When
+   generating a rollup, we can call the
+   <literal>pg_sequence_last_value</literal> function to read the current value
+   of the sequence, and then execute
+   <literal>WAIT FOR LOCKERS OF page_views IN ROW EXCLUSIVE MODE;</literal>
+   to wait for outstanding <command>INSERT</command> commands to commit or roll
+   back. After it finishes waiting, we know that all rows with
+   <literal>id</literal> values less than or equal to the sequence value that we
+   read have been committed or rolled back, i.e., any outstanding uncommitted
+   rows must have a greater value for <literal>id</literal>.
+  </para>
+
+  <para>
+   We can use this knowledge to process each row exactly once for our rollup
+   table. We update the rollup table by running the following function inside a
+   transaction at the <literal>READ COMMITTED</literal> isolation level:
+
+<programlisting>
+CREATE TABLE page_view_rollup_state (
+    last_id bigint
+);
+
+CREATE FUNCTION update_page_view_rollup()
+RETURNS VOID
+LANGUAGE plpgsql
+AS $$
+DECLARE
+    _last_id bigint;
+    _cur_id bigint;
+BEGIN
+    DELETE FROM page_view_rollup_state RETURNING last_id INTO _last_id;
+
+    SELECT pg_sequence_last_value('page_views_id_seq') INTO _cur_id;
+
+    WAIT FOR LOCKERS OF page_views IN ROW EXCLUSIVE MODE;
+
+    -- We know that any page_views where id &lt;= _cur_id have been either
+    -- committed or rolled back. For the same reason, we know that any
+    -- page_views where id &lt;= _last_id were processed in a previous invocation
+    -- (or rolled back). There may be some page_views where id &gt; cur_id that
+    -- have already been committed, but more may be committed later, so we'll
+    -- process them in a future invocation to avoid double-counting.
+    INSERT INTO page_view_rollup (domain, view_count)
+        SELECT domain, count(*) as view_count
+        FROM page_views
+        WHERE id &gt; coalesce(_last_id, 0) AND id &lt;= _cur_id
+        GROUP BY domain
+    ON CONFLICT (domain) DO UPDATE
+    SET view_count = page_view_rollup.view_count + excluded.view_count;
+
+    INSERT INTO page_view_rollup_state (last_id) VALUES (_cur_id);
+END;
+$$;
+</programlisting>
+  </para>
+
+  <para>
+   In this example, a transaction is needed because the function temporarily
+   deletes from <literal>page_view_rollup_state</literal>, and the transaction
+   must use the <literal>READ COMMITTED</literal> isolation level because it
+   runs <command>DELETE</command> and <command>SELECT</command> before
+   <command>WAIT FOR LOCKERS</command> but expects newly committed rows to be
+   visible to the sub-<command>SELECT</command> afterward.
+  </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 40ef4ede26..dacecc5f13 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 void GetLocktagsRecurse(Oid reloid, List **locktags_p);
 static AclResult LockTableAclCheck(Oid reloid, LOCKMODE lockmode, Oid userid);
 static void RangeVarCallbackForLockTable(const RangeVar *rv, Oid relid,
 										 Oid oldrelid, void *arg);
@@ -64,6 +66,47 @@ 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;
+		LOCKTAG	   *heaplocktag = palloc_object(LOCKTAG);
+		LOCKMODE	nolock = NoLock;
+		Oid			reloid;
+		Oid			dbid;
+
+		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 (IsSharedRelation(reloid))
+			dbid = InvalidOid;
+		else
+			dbid = MyDatabaseId;
+		SET_LOCKTAG_RELATION(*heaplocktag, dbid, reloid);
+		locktags = lappend(locktags, heaplocktag);
+		if (recurse)
+			GetLocktagsRecurse(reloid, &locktags);
+	}
+	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 +201,40 @@ LockTableRecurse(Oid reloid, LOCKMODE lockmode, bool nowait)
 	}
 }
 
+/*
+ * Get locktags recursively over an inheritance tree
+ *
+ * This doesn't check permission on the child tables, because getting here means
+ * that the user has permission on the parent which is enough.
+ */
+static void
+GetLocktagsRecurse(Oid reloid, List **locktags_p)
+{
+	List	   *children;
+	ListCell   *lc;
+
+	children = find_all_inheritors(reloid, NoLock, NULL);
+
+	foreach(lc, children)
+	{
+		Oid			childreloid = lfirst_oid(lc);
+		Oid			dbid;
+		LOCKTAG	   *heaplocktag;
+
+		/* Parent already handled. */
+		if (childreloid == reloid)
+			continue;
+
+		heaplocktag = palloc_object(LOCKTAG);
+		if (IsSharedRelation(childreloid))
+			dbid = InvalidOid;
+		else
+			dbid = MyDatabaseId;
+		SET_LOCKTAG_RELATION(*heaplocktag, dbid, childreloid);
+		*locktags_p = lappend(*locktags_p, heaplocktag);
+	}
+}
+
 /*
  * Apply LOCK TABLE recursively over a view
  *
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 63f172e175..f24447429b 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; }
 		;
@@ -12257,6 +12259,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:
@@ -17255,6 +17294,7 @@ unreserved_keyword:
 			| LOCATION
 			| LOCK_P
 			| LOCKED
+			| LOCKERS
 			| LOGGED
 			| MAPPING
 			| MATCH
@@ -17414,6 +17454,7 @@ unreserved_keyword:
 			| VIEW
 			| VIEWS
 			| VOLATILE
+			| WAIT
 			| WHITESPACE_P
 			| WITHIN
 			| WITHOUT
@@ -17845,6 +17886,7 @@ bare_label_keyword:
 			| LOCATION
 			| LOCK_P
 			| LOCKED
+			| LOCKERS
 			| LOGGED
 			| MAPPING
 			| MATCH
@@ -18045,6 +18087,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 366a27ae8e..960d04c220 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 6c298c71b3..9807099aaa 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 e494309da8..9a1da11a72 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3797,6 +3797,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;	/* conflicting lock modes */
+} WaitForLockersStmt;
+
 /* ----------------------
  *		SET CONSTRAINTS Statement
  * ----------------------
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 5984dcfa4b..4be2b81f41 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 320ee91512..55317b98f9 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..e5e49c3f88
--- /dev/null
+++ b/src/test/isolation/expected/wait-for-lockers.out
@@ -0,0 +1,144 @@
+Parsed test spec with 3 sessions
+
+starting permutation: w1lae2 w2in1 rwfl w2c rsel1 w1c rc
+step w1lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2in1: INSERT INTO t1 VALUES (DEFAULT);
+step rwfl: WAIT FOR LOCKERS OF TABLE t1, t2 IN ROW EXCLUSIVE MODE; <waiting ...>
+step w2c: COMMIT;
+step rwfl: <... completed>
+step rsel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step w1c: COMMIT;
+step rc: COMMIT;
+
+starting permutation: w1lae2 w2in1 rwflic w2c w1c rsel1 rc
+step w1lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2in1: INSERT INTO t1 VALUES (DEFAULT);
+step rwflic: WAIT FOR LOCKERS OF TABLE t1, t2 IN CONFLICT WITH SHARE MODE; <waiting ...>
+step w2c: COMMIT;
+step w1c: COMMIT;
+step rwflic: <... completed>
+step rsel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step rc: COMMIT;
+
+starting permutation: w1lae2 w2in1 w2c w1c rwflic rsel1 rc
+step w1lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2c: COMMIT;
+step w1c: COMMIT;
+step rwflic: WAIT FOR LOCKERS OF TABLE t1, t2 IN CONFLICT WITH SHARE MODE;
+step rsel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step rc: COMMIT;
+
+starting permutation: w1in1 rwflic w2in1 w2c w1c rsel1 rc
+step w1in1: INSERT INTO t1 VALUES (DEFAULT);
+step rwflic: WAIT FOR LOCKERS OF TABLE t1, t2 IN CONFLICT WITH SHARE MODE; <waiting ...>
+step w2in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2c: COMMIT;
+step w1c: COMMIT;
+step rwflic: <... 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 rwflic w1lae2 w1in1 w2c rsel1 w1c rc
+step w2in1: INSERT INTO t1 VALUES (DEFAULT);
+step rwflic: WAIT FOR LOCKERS OF TABLE t1, t2 IN CONFLICT WITH SHARE MODE; <waiting ...>
+step w1lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w1in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2c: COMMIT;
+step rwflic: <... completed>
+step rsel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step w1c: COMMIT;
+step rc: COMMIT;
+
+starting permutation: w2in1 rsv rl w1lae2 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 w1lae2: 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: w1lae1 w2in1 rwflic w1c rsel1 w2c rc
+step w1lae1: LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE;
+step w2in1: INSERT INTO t1 VALUES (DEFAULT); <waiting ...>
+step rwflic: WAIT FOR LOCKERS OF TABLE t1, t2 IN CONFLICT WITH SHARE MODE; <waiting ...>
+step w1c: COMMIT;
+step w2in1: <... completed>
+step rwflic: <... completed>
+step rsel1: SELECT id from t1;
+id
+--
+(0 rows)
+
+step w2c: COMMIT;
+step rc: COMMIT;
+
+starting permutation: w1lae1 w2in1 rl w1c w2c rsel1 rc
+step w1lae1: 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 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..f6fbfcff38
--- /dev/null
+++ b/src/test/isolation/specs/wait-for-lockers.spec
@@ -0,0 +1,64 @@
+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 w1lae1	{ LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE; }
+step w1lae2	{ 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 rwfl	{ WAIT FOR LOCKERS OF TABLE t1, t2 IN ROW EXCLUSIVE MODE; }
+step rwflic	{ WAIT FOR LOCKERS OF TABLE t1, t2 IN CONFLICT WITH SHARE MODE; }
+step rsel1	{ SELECT id from t1; }
+step rc		{ COMMIT; }
+
+# reader waits only for writer in ROW EXCLUSIVE mode
+permutation w1lae2 w2in1 rwfl w2c rsel1 w1c rc
+# reader waits for both writers conflicting with SHARE
+permutation w1lae2 w2in1 rwflic w2c w1c rsel1 rc
+
+# no waiting if writers already committed (obviously)
+permutation w1lae2 w2in1 w2c w1c rwflic rsel1 rc
+
+# reader waiting for writer1 doesn't block writer2...
+permutation w1in1 rwflic 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 rwflic w1lae2 w1in1 w2c rsel1 w1c rc
+# ...while actually taking the locks is blocked by the later ACCESS EXCLUSIVE
+# lock and would deadlock with the subsequent insert w1in1 (removed here)
+permutation w2in1 rsv rl w1lae2 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 w1lae1 w2in1 rwflic w1c rsel1 w2c rc
+# ...while actually taking the lock also waits for writer2 to release its lock
+permutation w1lae1 w2in1 rl w1c w2c rsel1 rc
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..f1e324ba0c
--- /dev/null
+++ b/src/test/regress/expected/wait_for_lockers.out
@@ -0,0 +1,87 @@
+--
+-- 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 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..732302331e
--- /dev/null
+++ b/src/test/regress/sql/wait_for_lockers.sql
@@ -0,0 +1,93 @@
+--
+-- 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 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

