From 4f35e78a762e96c8a8411904f0b983ab7b18b16e Mon Sep 17 00:00:00 2001
From: Alexander Cheshev <alex.cheshev@gmail.com>
Date: Thu, 14 Mar 2024 21:53:29 +0100
Subject: [PATCH v4] Support wildcards in LISTEN command
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Notification channels can be composed of multiple levels in the form ‘a.b.c’ where ‘a’, ‘b’ and ‘c’ are identifiers.

Listen and unlisten channels can be composed of multiple levels in the form ‘a.b.c’ where ‘a’, ‘b’ and ‘c’ are identifiers which can contain the following wildcards:
* The wildcard ‘%’ matches everything until the end of a level. Can only appear at the end of a level. For example, the notification channels ‘a.b.c’, ‘a.bc.c’ match against the notification channel ‘a.b%.c’.
* The wildcard ‘*’ matches everything to the right. Can only appear at the end of the last level. For example, the notification channels ‘a.b’, ‘a.bc.d’ match against the notification channel ‘a.b*’.

Use binary trie to match notification channels against listen channels.
---
 doc/src/sgml/ref/listen.sgml                 |  41 +-
 doc/src/sgml/ref/notify.sgml                 |   9 +-
 doc/src/sgml/ref/unlisten.sgml               |  46 +-
 src/backend/commands/async.c                 | 527 ++++++++++++++++++-
 src/backend/parser/gram.y                    | 111 +++-
 src/backend/parser/parser.c                  |   4 -
 src/backend/parser/scan.l                    |  11 +-
 src/backend/parser/scansup.c                 |  14 +-
 src/backend/tcop/utility.c                   |   5 +-
 src/backend/utils/adt/misc.c                 |   2 +-
 src/include/parser/scansup.h                 |   3 +-
 src/pl/plpgsql/src/pl_scanner.c              |   9 +
 src/test/isolation/expected/async-notify.out | 184 ++++++-
 src/test/isolation/specs/async-notify.spec   |  50 ++
 src/test/regress/expected/async.out          | 122 ++++-
 src/test/regress/sql/async.sql               |  68 ++-
 16 files changed, 1126 insertions(+), 80 deletions(-)

diff --git a/doc/src/sgml/ref/listen.sgml b/doc/src/sgml/ref/listen.sgml
index 6c1f09bd45..72dde0943d 100644
--- a/doc/src/sgml/ref/listen.sgml
+++ b/doc/src/sgml/ref/listen.sgml
@@ -36,6 +36,16 @@ LISTEN <replaceable class="parameter">channel</replaceable>
    this notification channel, nothing is done.
   </para>
 
+  <para>
+   Channel names can be composed of multiple levels in the form
+   <literal>a.b.c</literal> which allows you to organise channel names in
+   hierarchy. Listen channels can contain wildcards which match multiple
+   notification channels. For example, the notification channels
+   <literal>a.b.c</literal>, <literal>a.bc.c</literal> match against the
+   listen channel <literal>a.b%.c</literal> (<literal>%</literal> matches
+   everything until the end of the level).
+  </para>
+
   <para>
    Whenever the command <command>NOTIFY <replaceable
    class="parameter">channel</replaceable></command> is invoked, either
@@ -73,8 +83,37 @@ LISTEN <replaceable class="parameter">channel</replaceable>
     <term><replaceable class="parameter">channel</replaceable></term>
     <listitem>
      <para>
-      Name of a notification channel (any identifier).
+      Name of a notification channel. It is a simple string composed of
+      multiple levels in the form <literal>a.b.c</literal> where
+      <literal>a</literal>, <literal>b</literal> and <literal>c</literal> are
+      identifiers which can contain the following wildcards:
      </para>
+
+     <variablelist>
+      <varlistentry>
+       <term><literal>%</literal></term>
+       <listitem>
+        <para>
+         Matches everything until the end of a level. Can only appear at
+         the end of a level. For example, <command>LISTEN a.b%.c</command>
+         registers on the notification channels named like
+         <literal>a.b.c</literal>, <literal>a.bc.c</literal>.
+        </para>
+       </listitem>
+      </varlistentry>
+      
+      <varlistentry>
+       <term><literal>*</literal></term>
+       <listitem>
+        <para>
+         Matches everything to the right. Can only appear at the end of
+         the last level. For example, <command>LISTEN a.b*</command>
+         registers on the notification channels named like
+         <literal>a.b</literal>, <literal>a.bc.d</literal>.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
     </listitem>
    </varlistentry>
   </variablelist>
diff --git a/doc/src/sgml/ref/notify.sgml b/doc/src/sgml/ref/notify.sgml
index fd6ed54e8f..05a1bc6c97 100644
--- a/doc/src/sgml/ref/notify.sgml
+++ b/doc/src/sgml/ref/notify.sgml
@@ -55,7 +55,9 @@ NOTIFY <replaceable class="parameter">channel</replaceable> [ , <replaceable cla
 
   <para>
    It is up to the database designer to define the channel names that will
-   be used in a given database and what each one means.
+   be used in a given database and what each one means. The channel names can be
+   composed of multiple levels in the form <literal>a.b.c</literal> which
+   allows you to organise them in hierarchy.
    Commonly, the channel name is the same as the name of some table in
    the database, and the notify event essentially means, <quote>I changed this table,
    take a look at it to see what's new</quote>.  But no such association is enforced by
@@ -131,7 +133,10 @@ NOTIFY <replaceable class="parameter">channel</replaceable> [ , <replaceable cla
     <term><replaceable class="parameter">channel</replaceable></term>
     <listitem>
      <para>
-      Name of the notification channel to be signaled (any identifier).
+      Name of the notification channel to be signaled. It is
+      a simple string composed of multiple levels in the form
+      <literal>a.b.c</literal> where <literal>a</literal>,
+      <literal>b</literal> and <literal>c</literal> are identifiers.
      </para>
     </listitem>
    </varlistentry>
diff --git a/doc/src/sgml/ref/unlisten.sgml b/doc/src/sgml/ref/unlisten.sgml
index 687bf485c9..0f37ec618f 100644
--- a/doc/src/sgml/ref/unlisten.sgml
+++ b/doc/src/sgml/ref/unlisten.sgml
@@ -34,10 +34,12 @@ UNLISTEN { <replaceable class="parameter">channel</replaceable> | * }
    <command>UNLISTEN</command> cancels any existing registration of
    the current <productname>PostgreSQL</productname> session as a
    listener on the notification channel named <replaceable
-   class="parameter">channel</replaceable>.  The special wildcard
-   <literal>*</literal> cancels all listener registrations for the
+   class="parameter">channel</replaceable>. Channel name
+   <replaceable class="parameter">channel</replaceable> can contain
+   wildcards which allows you to simultaneously cancel multiple
+   registrations for the current session. For example,
+   <command>UNLISTEN *</command> cancels all registrations for the
    current session.
-  </para>
 
   <para>
    <xref linkend="sql-notify"/>
@@ -55,17 +57,37 @@ UNLISTEN { <replaceable class="parameter">channel</replaceable> | * }
     <term><replaceable class="parameter">channel</replaceable></term>
     <listitem>
      <para>
-      Name of a notification channel (any identifier).
+      Name of a notification channel. It is a simple string composed of
+      multiple levels in the form <literal>a.b.c</literal> where
+      <literal>a</literal>, <literal>b</literal> and <literal>c</literal> are
+      identifiers which can contain the following wildcards:
      </para>
-    </listitem>
-   </varlistentry>
 
-   <varlistentry>
-    <term><literal>*</literal></term>
-    <listitem>
-     <para>
-      All current listen registrations for this session are cleared.
-     </para>
+     <variablelist>
+      <varlistentry>
+       <term><literal>%</literal></term>
+       <listitem>
+        <para>
+         Matches everything until the end of a level. Can only appear at
+         the end of a level. For example, <command>UNLISTEN a.b%.c</command>
+         cancels registrations on the notification channels named like
+         <literal>a.b.c</literal>, <literal>a.bc.c</literal>.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term><literal>*</literal></term>
+       <listitem>
+        <para>
+         Matches everything to the right. Can only appear at the end of
+         the last level. For example, <command>UNLISTEN a.b*</command>
+         cancels registrations on the notification channels named like
+         <literal>a.b</literal>, <literal>a.bc.d</literal>.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
     </listitem>
    </varlistentry>
   </variablelist>
diff --git a/src/backend/commands/async.c b/src/backend/commands/async.c
index ab4c72762d..e7a863c308 100644
--- a/src/backend/commands/async.c
+++ b/src/backend/commands/async.c
@@ -319,6 +319,54 @@ static SlruCtlData NotifyCtlData;
  */
 static List *listenChannels = NIL;	/* list of C strings */
 
+/*
+ * Channel names consist of multiple levels which are separated by the
+ * character '.'. For example, 'a.b.c'. Listen channels can contain the
+ * following wildcards to match against multiple notification channels:
+ * 1. The wildcard '%' matches everything until the end of the level. For
+ *    example, 'aa.b' matches against 'a%.b'.
+ * 2. The wildcard '*' matches everything until the end of the notification
+ *    channel. For example, 'a.b.c' matches against 'a.*'.
+ */
+#define MATCH_OP_LEVEL(s, c) \
+	((*(c) == '\0') || \
+	 ((s) != (c) && *(c) == '.' && *((c) + 1) != '.' && *((c) + 1) != '\0'))
+
+#define MATCH_OP_WILDRIGHT(c) \
+	(*(c) == '*' && *((c) + 1) == '\0')
+
+#define MATCH_OP_WILDLEVEL(c) \
+	(*(c) == '%' && MATCH_OP_LEVEL(c, (c) + 1))
+
+#define MATCH_OP_NOWILD(c) \
+	(!MATCH_OP_WILDRIGHT(c) && !MATCH_OP_WILDLEVEL(c))
+
+/*
+ * Returns the number of trailing 0-bits in char starting at the least
+ * significant bit position. If char is 0 the result is CHAR_BIT.
+ */
+#define MATCH_CTZC(c)			(pg_rightmost_one_pos32(1 << CHAR_BIT | (c)))
+
+/* Node in the binary trie of the listen channels */
+typedef struct TrieNode
+{
+	const char *channel;		/* Listen channel, C string */
+	unsigned int index;			/* The index of the least significant bit on
+								 * which the listen channel on the left
+								 * differs from the listen channel on the
+								 * right */
+	unsigned int length;		/* Number of bits in the listen channel
+								 * including the last character '\0' */
+	struct TrieNode *left;		/* The left child */
+	struct TrieNode *right;		/* The right child */
+}			TrieNode;
+
+/*
+ * The root of the binary trie which is used to match notification channels
+ * against the listen channels. It is allocated in TopMemoryContext.
+ */
+static TrieNode * matchingTrie = NULL;
+
 /*
  * State for pending LISTEN/UNLISTEN actions consists of an ordered list of
  * all actions requested in the current transaction.  As explained above,
@@ -457,6 +505,14 @@ static void AddEventToPendingNotifies(Notification *n);
 static uint32 notification_hash(const void *key, Size keysize);
 static int	notification_match(const void *key1, const void *key2, Size keysize);
 static void ClearPendingActionsAndNotifies(void);
+static bool IsMatchingOn(const char *channel);
+static void BuildMatchingTrie(void);
+static void DeleteMatchingTrie(void);
+static void FreeMatchingTrieRecursively(TrieNode * node);
+static bool IsTrieMatchingOnRecursively(const char *channel,
+										TrieNode * node,
+										size_t channelIndex,
+										size_t parentIndex);
 
 /*
  * Compute the difference between two queue page numbers.
@@ -764,7 +820,7 @@ Async_Unlisten(const char *channel)
 /*
  * Async_UnlistenAll
  *
- *		This is invoked by UNLISTEN * command, and also at backend exit.
+ *		This is invoked at backend exit.
  */
 void
 Async_UnlistenAll(void)
@@ -822,6 +878,7 @@ pg_listening_channels(PG_FUNCTION_ARGS)
 static void
 Async_UnlistenOnExit(int code, Datum arg)
 {
+	DeleteMatchingTrie();
 	Exec_UnlistenAllCommit();
 	asyncQueueUnregister();
 }
@@ -1001,6 +1058,13 @@ AtCommit_Notify(void)
 		}
 	}
 
+	/*
+	 * Build the matching trie which is used to match notification channels
+	 * against the listen channels
+	 */
+	if (pendingActions != NULL)
+		BuildMatchingTrie();
+
 	/* If no longer listening to anything, get out of listener array */
 	if (amRegisteredListener && listenChannels == NIL)
 		asyncQueueUnregister();
@@ -1170,12 +1234,51 @@ Exec_UnlistenCommit(const char *channel)
 	foreach(q, listenChannels)
 	{
 		char	   *lchan = (char *) lfirst(q);
+		const char *s1 = channel;
+		const char *s2 = lchan;
 
-		if (strcmp(lchan, channel) == 0)
+		while (*s1)
+		{
+			if (MATCH_OP_WILDRIGHT(s1))
+			{
+				/* lchan is a subset of channel, remove lchan from the list */
+				s2 = s1;
+				break;
+			}
+			else if (MATCH_OP_WILDRIGHT(s2))
+			{
+				/*
+				 * channel is a subset of lchan, break the loop and consider
+				 * the next lchan
+				 */
+				break;
+			}
+			else if (MATCH_OP_WILDLEVEL(s1) &&
+					 MATCH_OP_LEVEL(lchan, s2))
+				s1++;
+			else if (MATCH_OP_WILDLEVEL(s1) &&
+					 MATCH_OP_NOWILD(s2))
+				s2++;
+			else
+			{
+				if (*s1 != *s2)
+				{
+					/*
+					 * lchan is not a subset of channel, break the loop and
+					 * consider the next lchan
+					 */
+					break;
+				}
+
+				s1++;
+				s2++;
+			}
+		}
+
+		if (*s1 == *s2)
 		{
 			listenChannels = foreach_delete_current(listenChannels, q);
 			pfree(lchan);
-			break;
 		}
 	}
 
@@ -1203,10 +1306,8 @@ Exec_UnlistenAllCommit(void)
 /*
  * Test whether we are actively listening on the given channel name.
  *
- * Note: this function is executed for every notification found in the queue.
- * Perhaps it is worth further optimization, eg convert the list to a sorted
- * array so we can binary-search it.  In practice the list is likely to be
- * fairly short, though.
+ * Note: this function is not used to match notification channels against
+ * the listen channels so there is not need to optimize it any further.
  */
 static bool
 IsListeningOn(const char *channel)
@@ -2071,7 +2172,7 @@ asyncQueueProcessPageEntries(volatile QueuePosition *current,
 				/* qe->data is the null-terminated channel name */
 				char	   *channel = qe->data;
 
-				if (IsListeningOn(channel))
+				if (IsMatchingOn(channel))
 				{
 					/* payload follows channel name */
 					char	   *payload = qe->data + strlen(channel) + 1;
@@ -2395,3 +2496,413 @@ check_notify_buffers(int *newval, void **extra, GucSource source)
 {
 	return check_slru_buffers("notify_buffers", newval);
 }
+
+/*
+ * Match the notification channel against the listen channels
+ */
+static bool
+IsMatchingOn(const char *channel)
+{
+	return IsTrieMatchingOnRecursively(channel, matchingTrie, 0, 0);
+}
+
+/*
+ * Build the binary trie of the listen channels which is used to match
+ * notification channels against the listen channels. The time complexity can
+ * be estimated as O(nm) where n is the number of the listen channels and m
+ * is the maximum length among the listen channels. As space complexity is
+ * dominated by the leaf nodes it can be estimated as O(n) where n is the
+ * number of the listen channels. The function builds the matching trie as a
+ * usual binary trie except fo the following two cases:
+ * 1. If a parent node satisfies the following condition:
+ *    channel[parent->index / CHAR_BIT] == '%'
+ *    then the descendants on the left don't satisfy the condition and the
+ *    descendants on the right satisfy the condition.
+ * 2. If a parent node satisfies the following condition:
+ *    channel[parent->index / CHAR_BIT] == '*'
+ *    then either the parent node doesn't have children or it has only the
+ *    left child which we solely preserve to free the memory during the next
+ *    build of the matching trie.
+ */
+static void
+BuildMatchingTrie()
+{
+	ListCell   *p;
+	MemoryContext oldcontext;
+
+	DeleteMatchingTrie();
+
+	/*
+	 * Allocate the matching trie in the TopMemoryContext as the listen
+	 * channels are allocated in there
+	 */
+	oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+
+	foreach(p, listenChannels)
+	{
+		char	   *lchan = (char *) lfirst(p);
+		size_t		i = 0;
+		size_t		l = strlen(lchan) + 1;
+		size_t		n = l * CHAR_BIT;
+		size_t		k = 0;
+		size_t		r;
+		char		x;
+		TrieNode  **prev;
+		TrieNode   *next;
+
+		if (matchingTrie == NULL)
+		{
+			matchingTrie = (TrieNode *) palloc(sizeof(TrieNode));
+			matchingTrie->channel = lchan;
+			matchingTrie->index = n;
+			matchingTrie->length = l;
+			matchingTrie->left = NULL;
+			matchingTrie->right = NULL;
+			continue;
+		}
+
+		prev = &matchingTrie;
+		next = matchingTrie;
+		while (i < n)
+		{
+			if (MATCH_OP_WILDRIGHT(next->channel + k))
+			{
+				/* The listen channel is a subset of the trie channel */
+				break;
+			}
+			else if (MATCH_OP_WILDRIGHT(lchan + k))
+			{
+				/*
+				 * The trie channel is a subset of the listen channel. So
+				 * replace the trie channel with the listen channel. We solely
+				 * preserve the next node to free the memory during the next
+				 * build of the matching trie.
+				 */
+				TrieNode   *parent;
+
+				parent = palloc(sizeof(TrieNode));
+				parent->channel = lchan;
+				parent->index = n;
+				parent->length = l;
+				parent->left = next;
+				parent->right = NULL;
+
+				*prev = parent;
+
+				break;
+			}
+			else if (MATCH_OP_WILDLEVEL(next->channel + k) &&
+					 !MATCH_OP_WILDLEVEL(lchan + k))
+			{
+				if (i == next->index)
+				{
+					prev = &next->left;
+					next = next->left;
+				}
+				else
+				{
+					/*
+					 * The trie channel contains the wildcard '%' and the
+					 * listen channel doesn't. So create a parent node with
+					 * the listen channel on the left and the trie channel on
+					 * the right.
+					 */
+					TrieNode   *child;
+					TrieNode   *parent;
+
+					child = palloc(sizeof(TrieNode));
+					child->channel = lchan;
+					child->index = n;
+					child->length = l;
+					child->left = NULL;
+					child->right = NULL;
+
+					parent = palloc(sizeof(TrieNode));
+					parent->channel = next->channel;
+					parent->index = i;
+					parent->length = next->length;
+					parent->left = child;
+					parent->right = next;
+
+					*prev = parent;
+
+					break;
+				}
+			}
+			else if (!MATCH_OP_WILDLEVEL(next->channel + k) &&
+					 MATCH_OP_WILDLEVEL(lchan + k))
+			{
+				/*
+				 * The listen channel contains the wildcard '%' and the trie
+				 * channel doesn't. So create a parent node with the trie
+				 * channel on the left and the listen channel on the right.
+				 */
+				TrieNode   *child;
+				TrieNode   *parent;
+
+				child = palloc(sizeof(TrieNode));
+				child->channel = lchan;
+				child->index = n;
+				child->length = l;
+				child->left = NULL;
+				child->right = NULL;
+
+				parent = palloc(sizeof(TrieNode));
+				parent->channel = lchan;
+				parent->index = i;
+				parent->length = l;
+				parent->left = next;
+				parent->right = child;
+
+				*prev = parent;
+
+				break;
+			}
+			else if (MATCH_OP_WILDLEVEL(next->channel + k) &&
+					 MATCH_OP_WILDLEVEL(lchan + k))
+			{
+				if (i == next->index)
+				{
+					prev = &next->right;
+					next = next->right;
+				}
+				i += CHAR_BIT;
+				k++;
+			}
+			else
+			{
+				/*
+				 * Find the index of the least significant bit on which the
+				 * listen channel differs from the trie channel
+				 */
+				x = lchan[k] ^ next->channel[k];
+				i = k * CHAR_BIT + MATCH_CTZC(x);
+				if (i < next->index)
+				{
+					if (x != 0)
+					{
+						/*
+						 * Create a parent node with the index of the least
+						 * significant bit on which the listen channel differs
+						 * from the trie channel. If the least significant bit
+						 * of the listen channel equals 0 then locate the
+						 * listen channel on the left and the trie channel on
+						 * the right. In the other case locate the channels in
+						 * the reverse order.
+						 */
+						TrieNode   *child;
+						TrieNode   *parent;
+
+						child = palloc(sizeof(TrieNode));
+						child->channel = lchan;
+						child->index = n;
+						child->length = l;
+						child->left = NULL;
+						child->right = NULL;
+
+						parent = palloc(sizeof(TrieNode));
+						parent->channel = lchan;
+						parent->index = i;
+						parent->length = l;
+
+						k = i / CHAR_BIT;
+						r = i % CHAR_BIT;
+						if (((lchan[k] >> r) & 1) == 0)
+						{
+							parent->left = child;
+							parent->right = next;
+						}
+						else
+						{
+							parent->left = next;
+							parent->right = child;
+						}
+
+						*prev = parent;
+
+						break;
+					}
+
+					k++;
+				}
+				else
+				{
+					i = next->index;
+					k = i / CHAR_BIT;
+					if (i < n &&
+						MATCH_OP_NOWILD(next->channel + k) &&
+						MATCH_OP_NOWILD(lchan + k))
+					{
+						/*
+						 * Find the bit of the listen channel on which the
+						 * left child differs from the right child
+						 */
+						r = i % CHAR_BIT;
+						if (((lchan[k] >> r) & 1) == 0)
+						{
+							prev = &next->left;
+							next = next->left;
+						}
+						else
+						{
+							prev = &next->right;
+							next = next->right;
+						}
+					}
+				}
+			}
+		}
+	}
+
+	MemoryContextSwitchTo(oldcontext);
+}
+
+/*
+ * Delete the matching trie
+ */
+static void
+DeleteMatchingTrie()
+{
+	if (matchingTrie == NULL)
+		return;
+
+	FreeMatchingTrieRecursively(matchingTrie);
+	matchingTrie = NULL;
+}
+
+/*
+ * Free the memory allocated to the matching trie
+ */
+static void
+FreeMatchingTrieRecursively(TrieNode * node)
+{
+	/* Since this function recurses, it could be driven to stack overflow */
+	check_stack_depth();
+
+	if (!node)
+		return;
+	if (node->left)
+		FreeMatchingTrieRecursively(node->left);
+	if (node->right)
+		FreeMatchingTrieRecursively(node->right);
+	pfree(node);
+}
+
+/*
+ * Match the notification channel against the binary trie of the listen
+ * channels. If during the search in the mathching trie the function doesn't
+ * encounter the wildcard '%' then the time complexity can be estimated as
+ * O(n) where n is the length of the notification channel. The function
+ * matches the notification channel using a usual search in the binary trie
+ * except for the following two cases:
+ * 1. If the function encounters the wildcard '%' then the function matches
+ *    everything until the end of the level.
+ * 2. If the function encounters the wildcard '*' then a match is found.
+ */
+static bool
+IsTrieMatchingOnRecursively(const char *channel,
+							TrieNode * node,
+							size_t channelIndex,
+							size_t parentIndex)
+{
+	size_t		i = channelIndex;
+	size_t		j = parentIndex;
+	size_t		l = strlen(channel) + 1;
+	size_t		n = l * CHAR_BIT;
+	size_t		k = 0;
+	size_t		t = 0;
+	size_t		r;
+	size_t		d;
+	char		x;
+	TrieNode   *next = node;
+
+	/* Since this function recurses, it could be driven to stack overflow */
+	check_stack_depth();
+
+	if (!node)
+		return false;
+
+	while (i < n)
+	{
+		if (MATCH_OP_WILDRIGHT(next->channel + t))
+		{
+			/*
+			 * The trie channel contains the wildcard '*' which matches
+			 * everything until the end of the notification channel. So a
+			 * match is found, break the loop and return true.
+			 */
+			i = n;
+		}
+		else if (MATCH_OP_WILDLEVEL(next->channel + t))
+		{
+			if (j == next->index)
+			{
+				/*
+				 * At first the function goes to the right as if there is a
+				 * match then it is higher likely located on the right
+				 */
+				if (IsTrieMatchingOnRecursively(channel, next->right, i, j))
+					i = n;
+				else
+					next = next->left;
+			}
+			else
+			{
+				/*
+				 * The trie channel contains the wildcard '%' which matches
+				 * everything until the end of the level
+				 */
+				while (!MATCH_OP_LEVEL(channel, channel + k))
+				{
+					i += CHAR_BIT;
+					k++;
+				}
+				j += CHAR_BIT;
+				t++;
+			}
+		}
+		else
+		{
+			/*
+			 * Find the index of the least significant bit on which the
+			 * notification channel differs from the trie channel
+			 */
+			d = i - j;
+			x = channel[k] ^ next->channel[t];
+			j = t * CHAR_BIT + MATCH_CTZC(x);
+			if (j < next->index)
+			{
+				if (x != 0)
+				{
+					/* No match is found, break the loop and return false */
+					break;
+				}
+
+				i = j + d;
+				k++;
+				t++;
+			}
+			else
+			{
+				j = next->index;
+				i = j + d;
+				k = i / CHAR_BIT;
+				t = j / CHAR_BIT;
+				if (i < n && MATCH_OP_NOWILD(next->channel + t))
+				{
+					/*
+					 * Find the bit of the notification channel on which the
+					 * left child differs from the right child
+					 */
+					r = i % CHAR_BIT;
+					if (((channel[k] >> r) & 1) == 0)
+						next = next->left;
+					else
+						next = next->right;
+				}
+			}
+		}
+	}
+
+	return i == n;
+}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index a043fd4c66..93a94cf0b0 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -59,6 +59,7 @@
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
 #include "parser/parser.h"
+#include "parser/scansup.h"
 #include "storage/lmgr.h"
 #include "utils/date.h"
 #include "utils/datetime.h"
@@ -216,6 +217,7 @@ static PartitionStrategy parsePartitionStrategy(char *strategy);
 static void preprocess_pubobj_list(List *pubobjspec_list,
 								   core_yyscan_t yyscanner);
 static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
+static void validate_channel(char *channel);
 
 %}
 
@@ -579,6 +581,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <str>		extract_arg
 %type <boolean> opt_varying opt_timezone opt_no_inherit
 
+%type <str>		Ident
 %type <ival>	Iconst SignedIconst
 %type <str>		Sconst comment_text notify_payload
 %type <str>		RoleId opt_boolean_or_string
@@ -590,6 +593,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <node>	var_value zone_value
 %type <rolespec> auth_ident RoleSpec opt_granted_by
 %type <publicationobjectspec> PublicationObjSpec
+%type <str>		LevelId
 
 %type <keyword> unreserved_keyword type_func_name_keyword
 %type <keyword> col_name_keyword reserved_keyword
@@ -679,6 +683,12 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				json_object_constructor_null_clause_opt
 				json_array_constructor_null_clause_opt
 
+%type <str>		listen_channel
+				listen_channel_inner_levels
+				listen_channel_inner_level
+				listen_channel_outer_level
+%type <str>		notify_channel
+
 
 /*
  * Non-keyword token types.  These are hard-wired into the "flex" lexer.
@@ -1252,7 +1262,7 @@ AlterOptRoleElem:
 				{
 					$$ = makeDefElem("rolemembers", (Node *) $2, @1);
 				}
-			| IDENT
+			| Ident
 				{
 					/*
 					 * We handle identifiers that aren't parser keywords with
@@ -1838,14 +1848,14 @@ opt_boolean_or_string:
  * - an integer or floating point number
  * - a time interval per SQL99
  * ColId gives reduce/reduce errors against ConstInterval and LOCAL,
- * so use IDENT (meaning we reject anything that is a key word).
+ * so use Ident (meaning we reject anything that is a key word).
  */
 zone_value:
 			Sconst
 				{
 					$$ = makeStringConst($1, @1);
 				}
-			| IDENT
+			| Ident
 				{
 					$$ = makeStringConst($1, @1);
 				}
@@ -5882,7 +5892,7 @@ RowSecurityOptionalToRole:
 		;
 
 RowSecurityDefaultPermissive:
-			AS IDENT
+			AS Ident
 				{
 					if (strcmp($2, "permissive") == 0)
 						$$ = true;
@@ -6479,11 +6489,11 @@ old_aggr_list: old_aggr_elem						{ $$ = list_make1($1); }
 		;
 
 /*
- * Must use IDENT here to avoid reduce/reduce conflicts; fortunately none of
+ * Must use Ident here to avoid reduce/reduce conflicts; fortunately none of
  * the item names needed in old aggregate definitions are likely to become
  * SQL keywords.
  */
-old_aggr_elem:  IDENT '=' def_arg
+old_aggr_elem:  Ident '=' def_arg
 				{
 					$$ = makeDefElem($1, (Node *) $3, @1);
 				}
@@ -10979,45 +10989,76 @@ opt_instead:
  *
  *****************************************************************************/
 
-NotifyStmt: NOTIFY ColId notify_payload
+NotifyStmt: NOTIFY notify_channel notify_payload
 				{
 					NotifyStmt *n = makeNode(NotifyStmt);
 
+					validate_channel($2);
 					n->conditionname = $2;
 					n->payload = $3;
 					$$ = (Node *) n;
 				}
 		;
 
+notify_channel:
+			LevelId
+					{ $$ = $1; }
+			| notify_channel '.' LevelId
+					{ $$ = psprintf("%s.%s", $1, $3); }
+		;
+
 notify_payload:
 			',' Sconst							{ $$ = $2; }
 			| /*EMPTY*/							{ $$ = NULL; }
 		;
 
-ListenStmt: LISTEN ColId
+ListenStmt: LISTEN listen_channel
 				{
 					ListenStmt *n = makeNode(ListenStmt);
 
+					validate_channel($2);
 					n->conditionname = $2;
 					$$ = (Node *) n;
 				}
 		;
 
 UnlistenStmt:
-			UNLISTEN ColId
+			UNLISTEN listen_channel
 				{
 					UnlistenStmt *n = makeNode(UnlistenStmt);
 
+					validate_channel($2);
 					n->conditionname = $2;
 					$$ = (Node *) n;
 				}
-			| UNLISTEN '*'
-				{
-					UnlistenStmt *n = makeNode(UnlistenStmt);
+		;
 
-					n->conditionname = NULL;
-					$$ = (Node *) n;
-				}
+listen_channel:
+			listen_channel_outer_level
+					{ $$ = $1; }
+			| listen_channel_inner_levels '.' listen_channel_outer_level
+					{ $$ = psprintf("%s.%s", $1, $3); }
+		;
+
+listen_channel_inner_levels:
+			listen_channel_inner_level
+					{ $$ = $1; }
+			| listen_channel_inner_levels '.' listen_channel_inner_level
+					{ $$ = psprintf("%s.%s", $1, $3); }
+		;
+
+listen_channel_inner_level:
+			'%' 								{ $$ = "%"; }
+			| LevelId 							{ $$ = $1; }
+			| LevelId '%' 						{ $$ = psprintf("%s%%", $1); }
+		;
+
+listen_channel_outer_level:
+			'*'									{ $$ = "*"; }
+			| '%'								{ $$ = "%"; }
+			| LevelId							{ $$ = $1; }
+			| LevelId '*'						{ $$ = psprintf("%s*", $1); }
+			| LevelId '%'						{ $$ = psprintf("%s%%", $1); }
 		;
 
 
@@ -11357,7 +11398,7 @@ createdb_opt_item:
 /*
  * Ideally we'd use ColId here, but that causes shift/reduce conflicts against
  * the ALTER DATABASE SET/RESET syntaxes.  Instead call out specific keywords
- * we need, and allow IDENT so that database option names don't have to be
+ * we need, and allow Ident so that database option names don't have to be
  * parser keywords unless they are already keywords for other reasons.
  *
  * XXX this coding technique is fragile since if someone makes a formerly
@@ -11366,7 +11407,7 @@ createdb_opt_item:
  * exercising every such option, at least at the syntax level.
  */
 createdb_opt_name:
-			IDENT							{ $$ = $1; }
+			Ident							{ $$ = $1; }
 			| CONNECTION LIMIT				{ $$ = pstrdup("connection_limit"); }
 			| ENCODING						{ $$ = pstrdup($1); }
 			| LOCATION						{ $$ = pstrdup($1); }
@@ -14135,7 +14176,7 @@ xmltable_column_option_list:
 		;
 
 xmltable_column_option_el:
-			IDENT b_expr
+			Ident b_expr
 				{ $$ = makeDefElem($1, $2, @1); }
 			| DEFAULT b_expr
 				{ $$ = makeDefElem("default", $2, @1); }
@@ -16648,7 +16689,7 @@ extract_list:
  * - thomas 2001-04-12
  */
 extract_arg:
-			IDENT									{ $$ = $1; }
+			Ident									{ $$ = $1; }
 			| YEAR_P								{ $$ = "year"; }
 			| MONTH_P								{ $$ = "month"; }
 			| DAY_P									{ $$ = "day"; }
@@ -17508,23 +17549,27 @@ plassign_equals: COLON_EQUALS
  * is chosen in part to make keywords acceptable as names wherever possible.
  */
 
+Ident: IDENT
+				{ truncate_identifier($1, strlen($1), true); }
+		;
+
 /* Column identifier --- names that can be column, table, etc names.
  */
-ColId:		IDENT									{ $$ = $1; }
+ColId:		Ident									{ $$ = $1; }
 			| unreserved_keyword					{ $$ = pstrdup($1); }
 			| col_name_keyword						{ $$ = pstrdup($1); }
 		;
 
 /* Type/function identifier --- names that can be type or function names.
  */
-type_function_name:	IDENT							{ $$ = $1; }
+type_function_name:	Ident							{ $$ = $1; }
 			| unreserved_keyword					{ $$ = pstrdup($1); }
 			| type_func_name_keyword				{ $$ = pstrdup($1); }
 		;
 
 /* Any not-fully-reserved word --- these names can be, eg, role names.
  */
-NonReservedWord:	IDENT							{ $$ = $1; }
+NonReservedWord:	Ident							{ $$ = $1; }
 			| unreserved_keyword					{ $$ = pstrdup($1); }
 			| col_name_keyword						{ $$ = pstrdup($1); }
 			| type_func_name_keyword				{ $$ = pstrdup($1); }
@@ -17533,7 +17578,7 @@ NonReservedWord:	IDENT							{ $$ = $1; }
 /* Column label --- allowed labels in "AS" clauses.
  * This presently includes *all* Postgres keywords.
  */
-ColLabel:	IDENT									{ $$ = $1; }
+ColLabel:	Ident									{ $$ = $1; }
 			| unreserved_keyword					{ $$ = pstrdup($1); }
 			| col_name_keyword						{ $$ = pstrdup($1); }
 			| type_func_name_keyword				{ $$ = pstrdup($1); }
@@ -17543,10 +17588,17 @@ ColLabel:	IDENT									{ $$ = $1; }
 /* Bare column label --- names that can be column labels without writing "AS".
  * This classification is orthogonal to the other keyword categories.
  */
-BareColLabel:	IDENT								{ $$ = $1; }
+BareColLabel:	Ident								{ $$ = $1; }
 			| bare_label_keyword					{ $$ = pstrdup($1); }
 		;
 
+/* Level identifier --- the same as column identifier but we postpone truncation
+ * until we fully assemble channel name.
+ */
+LevelId:	IDENT									{ $$ = $1; }
+			| unreserved_keyword					{ $$ = pstrdup($1); }
+			| col_name_keyword						{ $$ = pstrdup($1); }
+		;
 
 /*
  * Keyword category lists.  Generally, every keyword present in
@@ -19552,6 +19604,17 @@ makeRecursiveViewSelect(char *relname, List *aliases, Node *query)
 	return (Node *) s;
 }
 
+/* Validate channel name in NOTIFY, LISTEN, UNLISTEN statements */
+static void
+validate_channel(char *channel)
+{
+	/* enforce length limits */
+	if (strlen(channel) >= NAMEDATALEN)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("channel name too long")));
+}
+
 /* parser_init()
  * Initialize to parse one query string
  */
diff --git a/src/backend/parser/parser.c b/src/backend/parser/parser.c
index 118488c3f3..b38cd28821 100644
--- a/src/backend/parser/parser.c
+++ b/src/backend/parser/parser.c
@@ -307,10 +307,6 @@ base_yylex(YYSTYPE *lvalp, YYLTYPE *llocp, core_yyscan_t yyscanner)
 
 			if (cur_token == UIDENT)
 			{
-				/* It's an identifier, so truncate as appropriate */
-				truncate_identifier(lvalp->core_yystype.str,
-									strlen(lvalp->core_yystype.str),
-									true);
 				cur_token = IDENT;
 			}
 			else if (cur_token == USCONST)
diff --git a/src/backend/parser/scan.l b/src/backend/parser/scan.l
index f74059e7b0..9ab8841917 100644
--- a/src/backend/parser/scan.l
+++ b/src/backend/parser/scan.l
@@ -803,8 +803,6 @@ other			.
 					if (yyextra->literallen == 0)
 						yyerror("zero-length delimited identifier");
 					ident = litbufdup(yyscanner);
-					if (yyextra->literallen >= NAMEDATALEN)
-						truncate_identifier(ident, yyextra->literallen, true);
 					yylval->str = ident;
 					return IDENT;
 				}
@@ -831,7 +829,7 @@ other			.
 					/* throw back all but the initial u/U */
 					yyless(1);
 					/* and treat it as {identifier} */
-					ident = downcase_truncate_identifier(yytext, yyleng, true);
+					ident = downcase_identifier(yytext, yyleng);
 					yylval->str = ident;
 					return IDENT;
 				}
@@ -1097,11 +1095,8 @@ other			.
 						return yyextra->keyword_tokens[kwnum];
 					}
 
-					/*
-					 * No.  Convert the identifier to lower case, and truncate
-					 * if necessary.
-					 */
-					ident = downcase_truncate_identifier(yytext, yyleng, true);
+					/* No.  Convert the identifier to lower case. */
+					ident = downcase_identifier(yytext, yyleng);
 					yylval->str = ident;
 					return IDENT;
 				}
diff --git a/src/backend/parser/scansup.c b/src/backend/parser/scansup.c
index 164e61ef8b..9743d1421c 100644
--- a/src/backend/parser/scansup.c
+++ b/src/backend/parser/scansup.c
@@ -36,14 +36,21 @@
 char *
 downcase_truncate_identifier(const char *ident, int len, bool warn)
 {
-	return downcase_identifier(ident, len, warn, true);
+	char	   *result;
+
+	result = downcase_identifier(ident, len);
+
+	if (len >= NAMEDATALEN)
+		truncate_identifier(result, len, warn);
+
+	return result;
 }
 
 /*
  * a workhorse for downcase_truncate_identifier
  */
 char *
-downcase_identifier(const char *ident, int len, bool warn, bool truncate)
+downcase_identifier(const char *ident, int len)
 {
 	char	   *result;
 	int			i;
@@ -73,9 +80,6 @@ downcase_identifier(const char *ident, int len, bool warn, bool truncate)
 	}
 	result[i] = '\0';
 
-	if (i >= NAMEDATALEN && truncate)
-		truncate_identifier(result, i, warn);
-
 	return result;
 }
 
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index fa66b8017e..a2ff0059b5 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -830,10 +830,7 @@ standard_ProcessUtility(PlannedStmt *pstmt,
 				UnlistenStmt *stmt = (UnlistenStmt *) parsetree;
 
 				CheckRestrictedOperation("UNLISTEN");
-				if (stmt->conditionname)
-					Async_Unlisten(stmt->conditionname);
-				else
-					Async_UnlistenAll();
+				Async_Unlisten(stmt->conditionname);
 			}
 			break;
 
diff --git a/src/backend/utils/adt/misc.c b/src/backend/utils/adt/misc.c
index 0e6c45807a..b1dc9b3cf2 100644
--- a/src/backend/utils/adt/misc.c
+++ b/src/backend/utils/adt/misc.c
@@ -934,7 +934,7 @@ parse_ident(PG_FUNCTION_ARGS)
 			 * being too long. It's easy enough for the user to get the
 			 * truncated names by casting our output to name[].
 			 */
-			downname = downcase_identifier(curname, len, false, false);
+			downname = downcase_identifier(curname, len);
 			part = cstring_to_text_with_len(downname, len);
 			astate = accumArrayResult(astate, PointerGetDatum(part), false,
 									  TEXTOID, CurrentMemoryContext);
diff --git a/src/include/parser/scansup.h b/src/include/parser/scansup.h
index 4b68ddc2d8..de7e539322 100644
--- a/src/include/parser/scansup.h
+++ b/src/include/parser/scansup.h
@@ -17,8 +17,7 @@
 extern char *downcase_truncate_identifier(const char *ident, int len,
 										  bool warn);
 
-extern char *downcase_identifier(const char *ident, int len,
-								 bool warn, bool truncate);
+extern char *downcase_identifier(const char *ident, int len);
 
 extern void truncate_identifier(char *ident, int len, bool warn);
 
diff --git a/src/pl/plpgsql/src/pl_scanner.c b/src/pl/plpgsql/src/pl_scanner.c
index 9407da51ef..fc6bccb2f4 100644
--- a/src/pl/plpgsql/src/pl_scanner.c
+++ b/src/pl/plpgsql/src/pl_scanner.c
@@ -17,6 +17,7 @@
 
 #include "mb/pg_wchar.h"
 #include "parser/scanner.h"
+#include "parser/scansup.h"
 
 #include "plpgsql.h"
 #include "pl_gram.h"			/* must be after parser/scanner.h */
@@ -359,6 +360,14 @@ internal_yylex(TokenAuxData *auxdata)
 		{
 			auxdata->lval.str = pstrdup(yytext);
 		}
+
+		else if (token == IDENT)
+		{
+			/* It's an identifier, so truncate as appropriate */
+			truncate_identifier(auxdata->lval.str,
+								strlen(auxdata->lval.str),
+								true);
+		}
 	}
 
 	return token;
diff --git a/src/test/isolation/expected/async-notify.out b/src/test/isolation/expected/async-notify.out
index 556e180589..c25586031e 100644
--- a/src/test/isolation/expected/async-notify.out
+++ b/src/test/isolation/expected/async-notify.out
@@ -1,4 +1,4 @@
-Parsed test spec with 3 sessions
+Parsed test spec with 4 sessions
 
 starting permutation: listenc notify1 notify2 notify3 notifyf
 step listenc: LISTEN c1; LISTEN c2;
@@ -125,3 +125,185 @@ nonzero
 t      
 (1 row)
 
+
+starting permutation: mnotify
+step mnotify: NOTIFY a; NOTIFY bc; NOTIFY ab; NOTIFY cd.efg.ijk; NOTIFY ab.ef; NOTIFY abcd.eg; NOTIFY abcd.ehfg; NOTIFY abc.efg.ijk; NOTIFY ab.ef.ijk; NOTIFY ab.ee.l;
+
+starting permutation: mlisten1 mnotify
+step mlisten1: LISTEN a;
+step mnotify: NOTIFY a; NOTIFY bc; NOTIFY ab; NOTIFY cd.efg.ijk; NOTIFY ab.ef; NOTIFY abcd.eg; NOTIFY abcd.ehfg; NOTIFY abc.efg.ijk; NOTIFY ab.ef.ijk; NOTIFY ab.ee.l;
+matching: NOTIFY "a" with payload "" from matching
+
+starting permutation: mlisten2 mnotify
+step mlisten2: LISTEN %;
+step mnotify: NOTIFY a; NOTIFY bc; NOTIFY ab; NOTIFY cd.efg.ijk; NOTIFY ab.ef; NOTIFY abcd.eg; NOTIFY abcd.ehfg; NOTIFY abc.efg.ijk; NOTIFY ab.ef.ijk; NOTIFY ab.ee.l;
+matching: NOTIFY "a" with payload "" from matching
+matching: NOTIFY "bc" with payload "" from matching
+matching: NOTIFY "ab" with payload "" from matching
+
+starting permutation: mlisten3 mnotify
+step mlisten3: LISTEN ab%;
+step mnotify: NOTIFY a; NOTIFY bc; NOTIFY ab; NOTIFY cd.efg.ijk; NOTIFY ab.ef; NOTIFY abcd.eg; NOTIFY abcd.ehfg; NOTIFY abc.efg.ijk; NOTIFY ab.ef.ijk; NOTIFY ab.ee.l;
+matching: NOTIFY "ab" with payload "" from matching
+
+starting permutation: mlisten4 mnotify
+step mlisten4: LISTEN *;
+step mnotify: NOTIFY a; NOTIFY bc; NOTIFY ab; NOTIFY cd.efg.ijk; NOTIFY ab.ef; NOTIFY abcd.eg; NOTIFY abcd.ehfg; NOTIFY abc.efg.ijk; NOTIFY ab.ef.ijk; NOTIFY ab.ee.l;
+matching: NOTIFY "a" with payload "" from matching
+matching: NOTIFY "bc" with payload "" from matching
+matching: NOTIFY "ab" with payload "" from matching
+matching: NOTIFY "cd.efg.ijk" with payload "" from matching
+matching: NOTIFY "ab.ef" with payload "" from matching
+matching: NOTIFY "abcd.eg" with payload "" from matching
+matching: NOTIFY "abcd.ehfg" with payload "" from matching
+matching: NOTIFY "abc.efg.ijk" with payload "" from matching
+matching: NOTIFY "ab.ef.ijk" with payload "" from matching
+matching: NOTIFY "ab.ee.l" with payload "" from matching
+
+starting permutation: mlisten5 mnotify
+step mlisten5: LISTEN cd*;
+step mnotify: NOTIFY a; NOTIFY bc; NOTIFY ab; NOTIFY cd.efg.ijk; NOTIFY ab.ef; NOTIFY abcd.eg; NOTIFY abcd.ehfg; NOTIFY abc.efg.ijk; NOTIFY ab.ef.ijk; NOTIFY ab.ee.l;
+matching: NOTIFY "cd.efg.ijk" with payload "" from matching
+
+starting permutation: mlisten6 mnotify
+step mlisten6: LISTEN ab.ef;
+step mnotify: NOTIFY a; NOTIFY bc; NOTIFY ab; NOTIFY cd.efg.ijk; NOTIFY ab.ef; NOTIFY abcd.eg; NOTIFY abcd.ehfg; NOTIFY abc.efg.ijk; NOTIFY ab.ef.ijk; NOTIFY ab.ee.l;
+matching: NOTIFY "ab.ef" with payload "" from matching
+
+starting permutation: mlisten7 mnotify
+step mlisten7: LISTEN ab%.eg;
+step mnotify: NOTIFY a; NOTIFY bc; NOTIFY ab; NOTIFY cd.efg.ijk; NOTIFY ab.ef; NOTIFY abcd.eg; NOTIFY abcd.ehfg; NOTIFY abc.efg.ijk; NOTIFY ab.ef.ijk; NOTIFY ab.ee.l;
+matching: NOTIFY "abcd.eg" with payload "" from matching
+
+starting permutation: mlisten8 mnotify
+step mlisten8: LISTEN %.eh*;
+step mnotify: NOTIFY a; NOTIFY bc; NOTIFY ab; NOTIFY cd.efg.ijk; NOTIFY ab.ef; NOTIFY abcd.eg; NOTIFY abcd.ehfg; NOTIFY abc.efg.ijk; NOTIFY ab.ef.ijk; NOTIFY ab.ee.l;
+matching: NOTIFY "abcd.ehfg" with payload "" from matching
+
+starting permutation: mlisten9 mnotify
+step mlisten9: LISTEN ab.ef%.*;
+step mnotify: NOTIFY a; NOTIFY bc; NOTIFY ab; NOTIFY cd.efg.ijk; NOTIFY ab.ef; NOTIFY abcd.eg; NOTIFY abcd.ehfg; NOTIFY abc.efg.ijk; NOTIFY ab.ef.ijk; NOTIFY ab.ee.l;
+matching: NOTIFY "ab.ef.ijk" with payload "" from matching
+
+starting permutation: mlisten10 mnotify
+step mlisten10: LISTEN ab.ee.l;
+step mnotify: NOTIFY a; NOTIFY bc; NOTIFY ab; NOTIFY cd.efg.ijk; NOTIFY ab.ef; NOTIFY abcd.eg; NOTIFY abcd.ehfg; NOTIFY abc.efg.ijk; NOTIFY ab.ef.ijk; NOTIFY ab.ee.l;
+matching: NOTIFY "ab.ee.l" with payload "" from matching
+
+starting permutation: mlisten1 mlisten8 mnotify
+step mlisten1: LISTEN a;
+step mlisten8: LISTEN %.eh*;
+step mnotify: NOTIFY a; NOTIFY bc; NOTIFY ab; NOTIFY cd.efg.ijk; NOTIFY ab.ef; NOTIFY abcd.eg; NOTIFY abcd.ehfg; NOTIFY abc.efg.ijk; NOTIFY ab.ef.ijk; NOTIFY ab.ee.l;
+matching: NOTIFY "a" with payload "" from matching
+matching: NOTIFY "abcd.ehfg" with payload "" from matching
+
+starting permutation: mlisten6 mlisten7 mnotify
+step mlisten6: LISTEN ab.ef;
+step mlisten7: LISTEN ab%.eg;
+step mnotify: NOTIFY a; NOTIFY bc; NOTIFY ab; NOTIFY cd.efg.ijk; NOTIFY ab.ef; NOTIFY abcd.eg; NOTIFY abcd.ehfg; NOTIFY abc.efg.ijk; NOTIFY ab.ef.ijk; NOTIFY ab.ee.l;
+matching: NOTIFY "ab.ef" with payload "" from matching
+matching: NOTIFY "abcd.eg" with payload "" from matching
+
+starting permutation: mlisten6 mlisten7 mlisten8 mnotify
+step mlisten6: LISTEN ab.ef;
+step mlisten7: LISTEN ab%.eg;
+step mlisten8: LISTEN %.eh*;
+step mnotify: NOTIFY a; NOTIFY bc; NOTIFY ab; NOTIFY cd.efg.ijk; NOTIFY ab.ef; NOTIFY abcd.eg; NOTIFY abcd.ehfg; NOTIFY abc.efg.ijk; NOTIFY ab.ef.ijk; NOTIFY ab.ee.l;
+matching: NOTIFY "ab.ef" with payload "" from matching
+matching: NOTIFY "abcd.eg" with payload "" from matching
+matching: NOTIFY "abcd.ehfg" with payload "" from matching
+
+starting permutation: mlisten1 mlisten6 mlisten7 mnotify
+step mlisten1: LISTEN a;
+step mlisten6: LISTEN ab.ef;
+step mlisten7: LISTEN ab%.eg;
+step mnotify: NOTIFY a; NOTIFY bc; NOTIFY ab; NOTIFY cd.efg.ijk; NOTIFY ab.ef; NOTIFY abcd.eg; NOTIFY abcd.ehfg; NOTIFY abc.efg.ijk; NOTIFY ab.ef.ijk; NOTIFY ab.ee.l;
+matching: NOTIFY "a" with payload "" from matching
+matching: NOTIFY "ab.ef" with payload "" from matching
+matching: NOTIFY "abcd.eg" with payload "" from matching
+
+starting permutation: mlisten1 mlisten3 mlisten5 mlisten6 mlisten8 mlisten9 mnotify
+step mlisten1: LISTEN a;
+step mlisten3: LISTEN ab%;
+step mlisten5: LISTEN cd*;
+step mlisten6: LISTEN ab.ef;
+step mlisten8: LISTEN %.eh*;
+step mlisten9: LISTEN ab.ef%.*;
+step mnotify: NOTIFY a; NOTIFY bc; NOTIFY ab; NOTIFY cd.efg.ijk; NOTIFY ab.ef; NOTIFY abcd.eg; NOTIFY abcd.ehfg; NOTIFY abc.efg.ijk; NOTIFY ab.ef.ijk; NOTIFY ab.ee.l;
+matching: NOTIFY "a" with payload "" from matching
+matching: NOTIFY "ab" with payload "" from matching
+matching: NOTIFY "cd.efg.ijk" with payload "" from matching
+matching: NOTIFY "ab.ef" with payload "" from matching
+matching: NOTIFY "abcd.ehfg" with payload "" from matching
+matching: NOTIFY "ab.ef.ijk" with payload "" from matching
+
+starting permutation: mlisten2 mlisten4 mlisten7 mlisten8 mlisten9 mlisten10 mnotify
+step mlisten2: LISTEN %;
+step mlisten4: LISTEN *;
+step mlisten7: LISTEN ab%.eg;
+step mlisten8: LISTEN %.eh*;
+step mlisten9: LISTEN ab.ef%.*;
+step mlisten10: LISTEN ab.ee.l;
+step mnotify: NOTIFY a; NOTIFY bc; NOTIFY ab; NOTIFY cd.efg.ijk; NOTIFY ab.ef; NOTIFY abcd.eg; NOTIFY abcd.ehfg; NOTIFY abc.efg.ijk; NOTIFY ab.ef.ijk; NOTIFY ab.ee.l;
+matching: NOTIFY "a" with payload "" from matching
+matching: NOTIFY "bc" with payload "" from matching
+matching: NOTIFY "ab" with payload "" from matching
+matching: NOTIFY "cd.efg.ijk" with payload "" from matching
+matching: NOTIFY "ab.ef" with payload "" from matching
+matching: NOTIFY "abcd.eg" with payload "" from matching
+matching: NOTIFY "abcd.ehfg" with payload "" from matching
+matching: NOTIFY "abc.efg.ijk" with payload "" from matching
+matching: NOTIFY "ab.ef.ijk" with payload "" from matching
+matching: NOTIFY "ab.ee.l" with payload "" from matching
+
+starting permutation: mlisten1 munlisten1 mnotify
+step mlisten1: LISTEN a;
+step munlisten1: UNLISTEN a%;
+step mnotify: NOTIFY a; NOTIFY bc; NOTIFY ab; NOTIFY cd.efg.ijk; NOTIFY ab.ef; NOTIFY abcd.eg; NOTIFY abcd.ehfg; NOTIFY abc.efg.ijk; NOTIFY ab.ef.ijk; NOTIFY ab.ee.l;
+
+starting permutation: mlisten3 munlisten2 mnotify
+step mlisten3: LISTEN ab%;
+step munlisten2: UNLISTEN abc%;
+step mnotify: NOTIFY a; NOTIFY bc; NOTIFY ab; NOTIFY cd.efg.ijk; NOTIFY ab.ef; NOTIFY abcd.eg; NOTIFY abcd.ehfg; NOTIFY abc.efg.ijk; NOTIFY ab.ef.ijk; NOTIFY ab.ee.l;
+matching: NOTIFY "ab" with payload "" from matching
+
+starting permutation: mlisten7 munlisten3 mnotify
+step mlisten7: LISTEN ab%.eg;
+step munlisten3: UNLISTEN %.e*;
+step mnotify: NOTIFY a; NOTIFY bc; NOTIFY ab; NOTIFY cd.efg.ijk; NOTIFY ab.ef; NOTIFY abcd.eg; NOTIFY abcd.ehfg; NOTIFY abc.efg.ijk; NOTIFY ab.ef.ijk; NOTIFY ab.ee.l;
+
+starting permutation: mlisten5 munlisten4 mnotify
+step mlisten5: LISTEN cd*;
+step munlisten4: UNLISTEN cd*;
+step mnotify: NOTIFY a; NOTIFY bc; NOTIFY ab; NOTIFY cd.efg.ijk; NOTIFY ab.ef; NOTIFY abcd.eg; NOTIFY abcd.ehfg; NOTIFY abc.efg.ijk; NOTIFY ab.ef.ijk; NOTIFY ab.ee.l;
+
+starting permutation: mlisten9 munlisten5 mnotify
+step mlisten9: LISTEN ab.ef%.*;
+step munlisten5: UNLISTEN ab.%;
+step mnotify: NOTIFY a; NOTIFY bc; NOTIFY ab; NOTIFY cd.efg.ijk; NOTIFY ab.ef; NOTIFY abcd.eg; NOTIFY abcd.ehfg; NOTIFY abc.efg.ijk; NOTIFY ab.ef.ijk; NOTIFY ab.ee.l;
+matching: NOTIFY "ab.ef.ijk" with payload "" from matching
+
+starting permutation: mlisten7 mlisten8 munlisten5 mnotify
+step mlisten7: LISTEN ab%.eg;
+step mlisten8: LISTEN %.eh*;
+step munlisten5: UNLISTEN ab.%;
+step mnotify: NOTIFY a; NOTIFY bc; NOTIFY ab; NOTIFY cd.efg.ijk; NOTIFY ab.ef; NOTIFY abcd.eg; NOTIFY abcd.ehfg; NOTIFY abc.efg.ijk; NOTIFY ab.ef.ijk; NOTIFY ab.ee.l;
+matching: NOTIFY "abcd.eg" with payload "" from matching
+matching: NOTIFY "abcd.ehfg" with payload "" from matching
+
+starting permutation: mlisten5 mlisten6 munlisten2 munlisten3 mnotify
+step mlisten5: LISTEN cd*;
+step mlisten6: LISTEN ab.ef;
+step munlisten2: UNLISTEN abc%;
+step munlisten3: UNLISTEN %.e*;
+step mnotify: NOTIFY a; NOTIFY bc; NOTIFY ab; NOTIFY cd.efg.ijk; NOTIFY ab.ef; NOTIFY abcd.eg; NOTIFY abcd.ehfg; NOTIFY abc.efg.ijk; NOTIFY ab.ef.ijk; NOTIFY ab.ee.l;
+matching: NOTIFY "cd.efg.ijk" with payload "" from matching
+
+starting permutation: mlisten1 mlisten3 mlisten5 mlisten6 mlisten8 munlisten6 mnotify
+step mlisten1: LISTEN a;
+step mlisten3: LISTEN ab%;
+step mlisten5: LISTEN cd*;
+step mlisten6: LISTEN ab.ef;
+step mlisten8: LISTEN %.eh*;
+step munlisten6: UNLISTEN *;
+step mnotify: NOTIFY a; NOTIFY bc; NOTIFY ab; NOTIFY cd.efg.ijk; NOTIFY ab.ef; NOTIFY abcd.eg; NOTIFY abcd.ehfg; NOTIFY abc.efg.ijk; NOTIFY ab.ef.ijk; NOTIFY ab.ee.l;
diff --git a/src/test/isolation/specs/async-notify.spec b/src/test/isolation/specs/async-notify.spec
index 0b8cfd9108..b47fa1b0c5 100644
--- a/src/test/isolation/specs/async-notify.spec
+++ b/src/test/isolation/specs/async-notify.spec
@@ -53,6 +53,28 @@ step l2begin	{ BEGIN; }
 step l2commit	{ COMMIT; }
 step l2stop		{ UNLISTEN *; }
 
+# A separate session to check wildcards in LISTEN and UNLISTEN commands
+
+session matching
+step mlisten1	{ LISTEN a; }
+step mlisten2	{ LISTEN %; }
+step mlisten3	{ LISTEN ab%; }
+step mlisten4	{ LISTEN *; }
+step mlisten5	{ LISTEN cd*; }
+step mlisten6	{ LISTEN ab.ef; }
+step mlisten7	{ LISTEN ab%.eg; }
+step mlisten8	{ LISTEN %.eh*; }
+step mlisten9	{ LISTEN ab.ef%.*; }
+step mlisten10	{ LISTEN ab.ee.l; }
+step munlisten1	{ UNLISTEN a%; }
+step munlisten2	{ UNLISTEN abc%; }
+step munlisten3	{ UNLISTEN %.e*; }
+step munlisten4	{ UNLISTEN cd*; }
+step munlisten5	{ UNLISTEN ab.%; }
+step munlisten6	{ UNLISTEN *; }
+step mnotify	{ NOTIFY a; NOTIFY bc; NOTIFY ab; NOTIFY cd.efg.ijk; NOTIFY ab.ef; NOTIFY abcd.eg; NOTIFY abcd.ehfg; NOTIFY abc.efg.ijk; NOTIFY ab.ef.ijk; NOTIFY ab.ee.l; }
+teardown		{ UNLISTEN *; }
+
 
 # Trivial cases.
 permutation listenc notify1 notify2 notify3 notifyf
@@ -82,3 +104,31 @@ permutation l2listen l2begin notify1 lbegins llisten lcommit l2commit l2stop
 # Hence, this should be the last test in this script.
 
 permutation llisten lbegin usage bignotify usage
+
+# Check wildcards in LISTEN and UNLISTEN commands
+
+permutation mnotify
+permutation mlisten1 mnotify
+permutation mlisten2 mnotify
+permutation mlisten3 mnotify
+permutation mlisten4 mnotify
+permutation mlisten5 mnotify
+permutation mlisten6 mnotify
+permutation mlisten7 mnotify
+permutation mlisten8 mnotify
+permutation mlisten9 mnotify
+permutation mlisten10 mnotify
+permutation mlisten1 mlisten8 mnotify
+permutation mlisten6 mlisten7 mnotify
+permutation mlisten6 mlisten7 mlisten8 mnotify
+permutation mlisten1 mlisten6 mlisten7 mnotify
+permutation mlisten1 mlisten3 mlisten5 mlisten6 mlisten8 mlisten9 mnotify
+permutation mlisten2 mlisten4 mlisten7 mlisten8 mlisten9 mlisten10 mnotify
+permutation mlisten1 munlisten1 mnotify
+permutation mlisten3 munlisten2 mnotify
+permutation mlisten7 munlisten3 mnotify
+permutation mlisten5 munlisten4 mnotify
+permutation mlisten9 munlisten5 mnotify
+permutation mlisten7 mlisten8 munlisten5 mnotify
+permutation mlisten5 mlisten6 munlisten2 munlisten3 mnotify
+permutation mlisten1 mlisten3 mlisten5 mlisten6 mlisten8 munlisten6 mnotify
diff --git a/src/test/regress/expected/async.out b/src/test/regress/expected/async.out
index 19cbe38e63..489f01b930 100644
--- a/src/test/regress/expected/async.out
+++ b/src/test/regress/expected/async.out
@@ -27,11 +27,125 @@ SELECT pg_notify(NULL,'sample message1');
 ERROR:  channel name cannot be empty
 SELECT pg_notify('notify_async_channel_name_too_long______________________________','sample_message1');
 ERROR:  channel name too long
---Should work. Valid NOTIFY/LISTEN/UNLISTEN commands
-NOTIFY notify_async2;
-LISTEN notify_async2;
-UNLISTEN notify_async2;
+-- Should work. Valid NOTIFY commands, multiple levels
+NOTIFY a;
+NOTIFY a.b;
+NOTIFY a.b.c;
+-- Should fail. Invalid NOTIFY commands, empty levels
+NOTIFY a.b.;
+ERROR:  syntax error at or near ";"
+LINE 1: NOTIFY a.b.;
+                   ^
+NOTIFY .b.c;
+ERROR:  syntax error at or near "."
+LINE 1: NOTIFY .b.c;
+               ^
+NOTIFY a..c;
+ERROR:  syntax error at or near ".."
+LINE 1: NOTIFY a..c;
+                ^
+-- Should work. Valid LISTEN/UNLISTEN commands, multiple levels and wildcards
+LISTEN a;
+LISTEN %;
+LISTEN a%;
+LISTEN *;
+LISTEN a*;
+LISTEN a.b;
+LISTEN %.b;
+LISTEN %.b%;
+LISTEN a.*;
+LISTEN a.b*;
+LISTEN a.b.c;
+LISTEN a.b%.a*;
+UNLISTEN a;
+UNLISTEN %;
+UNLISTEN a%;
 UNLISTEN *;
+UNLISTEN a*;
+UNLISTEN a.b;
+UNLISTEN %.b;
+UNLISTEN %.b%;
+UNLISTEN a.*;
+UNLISTEN a.b*;
+UNLISTEN a.b.c;
+UNLISTEN a.b%.a*;
+UNLISTEN *;
+-- Should fail. Invalid LISTEN/UNLISTEN commands, empty levels
+LISTEN a.b%.;
+ERROR:  syntax error at or near ";"
+LINE 1: LISTEN a.b%.;
+                    ^
+LISTEN .b%.c*;
+ERROR:  syntax error at or near "."
+LINE 1: LISTEN .b%.c*;
+               ^
+LISTEN a..%;
+ERROR:  syntax error at or near ".."
+LINE 1: LISTEN a..%;
+                ^
+UNLISTEN a.b%.;
+ERROR:  syntax error at or near ";"
+LINE 1: UNLISTEN a.b%.;
+                      ^
+UNLISTEN .b%.c*;
+ERROR:  syntax error at or near "."
+LINE 1: UNLISTEN .b%.c*;
+                 ^
+UNLISTEN a..%;
+ERROR:  syntax error at or near ".."
+LINE 1: UNLISTEN a..%;
+                  ^
+-- Should fail. Invalid LISTEN/UNLISTEN commands, the wildcard '%' can only be
+-- located at the end of a level
+LISTEN %a;
+ERROR:  syntax error at or near "a"
+LINE 1: LISTEN %a;
+                ^
+LISTEN %*;
+ERROR:  syntax error at or near "%*"
+LINE 1: LISTEN %*;
+               ^
+UNLISTEN %a;
+ERROR:  syntax error at or near "a"
+LINE 1: UNLISTEN %a;
+                  ^
+UNLISTEN %*;
+ERROR:  syntax error at or near "%*"
+LINE 1: UNLISTEN %*;
+                 ^
+-- Should fail. Invalid LISTEN/UNLISTEN commands, the wildcard '*' can only be
+-- located at the end of a channel name
+LISTEN *.;
+ERROR:  syntax error at or near "."
+LINE 1: LISTEN *.;
+                ^
+LISTEN *a;
+ERROR:  syntax error at or near "a"
+LINE 1: LISTEN *a;
+                ^
+LISTEN *%;
+ERROR:  syntax error at or near "*%"
+LINE 1: LISTEN *%;
+               ^
+UNLISTEN *.;
+ERROR:  syntax error at or near "."
+LINE 1: UNLISTEN *.;
+                  ^
+UNLISTEN *a;
+ERROR:  syntax error at or near "a"
+LINE 1: UNLISTEN *a;
+                  ^
+UNLISTEN *%;
+ERROR:  syntax error at or near "*%"
+LINE 1: UNLISTEN *%;
+                 ^
+-- Should fail. Too long channel names
+NOTIFY notify_async_channel_name_too_long_________._____________________;
+ERROR:  channel name too long
+LISTEN notify_async_channel_name_too_long_________%._____________________;
+ERROR:  channel name too long
+UNLISTEN notify_async_channel_name_too_long_________%._____________________;
+ERROR:  channel name too long
 -- Should return zero while there are no pending notifications.
 -- src/test/isolation/specs/async-notify.spec tests for actual usage.
 SELECT pg_notification_queue_usage();
diff --git a/src/test/regress/sql/async.sql b/src/test/regress/sql/async.sql
index 40f6e01538..ecc583a225 100644
--- a/src/test/regress/sql/async.sql
+++ b/src/test/regress/sql/async.sql
@@ -12,11 +12,71 @@ SELECT pg_notify('','sample message1');
 SELECT pg_notify(NULL,'sample message1');
 SELECT pg_notify('notify_async_channel_name_too_long______________________________','sample_message1');
 
---Should work. Valid NOTIFY/LISTEN/UNLISTEN commands
-NOTIFY notify_async2;
-LISTEN notify_async2;
-UNLISTEN notify_async2;
+-- Should work. Valid NOTIFY commands, multiple levels
+NOTIFY a;
+NOTIFY a.b;
+NOTIFY a.b.c;
+
+-- Should fail. Invalid NOTIFY commands, empty levels
+NOTIFY a.b.;
+NOTIFY .b.c;
+NOTIFY a..c;
+
+-- Should work. Valid LISTEN/UNLISTEN commands, multiple levels and wildcards
+LISTEN a;
+LISTEN %;
+LISTEN a%;
+LISTEN *;
+LISTEN a*;
+LISTEN a.b;
+LISTEN %.b;
+LISTEN %.b%;
+LISTEN a.*;
+LISTEN a.b*;
+LISTEN a.b.c;
+LISTEN a.b%.a*;
+UNLISTEN a;
+UNLISTEN %;
+UNLISTEN a%;
 UNLISTEN *;
+UNLISTEN a*;
+UNLISTEN a.b;
+UNLISTEN %.b;
+UNLISTEN %.b%;
+UNLISTEN a.*;
+UNLISTEN a.b*;
+UNLISTEN a.b.c;
+UNLISTEN a.b%.a*;
+UNLISTEN *;
+
+-- Should fail. Invalid LISTEN/UNLISTEN commands, empty levels
+LISTEN a.b%.;
+LISTEN .b%.c*;
+LISTEN a..%;
+UNLISTEN a.b%.;
+UNLISTEN .b%.c*;
+UNLISTEN a..%;
+
+-- Should fail. Invalid LISTEN/UNLISTEN commands, the wildcard '%' can only be
+-- located at the end of a level
+LISTEN %a;
+LISTEN %*;
+UNLISTEN %a;
+UNLISTEN %*;
+
+-- Should fail. Invalid LISTEN/UNLISTEN commands, the wildcard '*' can only be
+-- located at the end of a channel name
+LISTEN *.;
+LISTEN *a;
+LISTEN *%;
+UNLISTEN *.;
+UNLISTEN *a;
+UNLISTEN *%;
+
+-- Should fail. Too long channel names
+NOTIFY notify_async_channel_name_too_long_________._____________________;
+LISTEN notify_async_channel_name_too_long_________%._____________________;
+UNLISTEN notify_async_channel_name_too_long_________%._____________________;
 
 -- Should return zero while there are no pending notifications.
 -- src/test/isolation/specs/async-notify.spec tests for actual usage.
-- 
2.25.1

