Hi Emanuel,

I did a review on the new patch version and I observed that the identifier
> passed to the LISTEN command is handled differently between outer and
> inner
> levels.
>

We have the following grammar:

notify_channel:
                       ColId
                                       { $$ = $1; }
                       | notify_channel '.' ColId
                                       { $$ = psprintf("%s.%s", $1, $3); }

And ColId is truncated in core scanner:

     ident = downcase_truncate_identifier(yytext, yyleng, true);

So each level is truncated independently. For this reason we observe the
behaviour which you described above.

Another observation, probably not strictly related to this patch itself but
> the async-notify tests, is that there is no test for
> "payload too long". Probably there is a reason on why isn't in the specs?
>

I believe that simply because not all functionality is covered with tests.
But I have noticed a very interesting test "channel name too long":

SELECT
pg_notify('notify_async_channel_name_too_long______________________________','sample_message1');
ERROR:  channel name too long

But the behaviour is inconsistent with NOTIFY command:

NOTIFY notify_async_channel_name_too_long______________________________
NOTICE:  identifier
"notify_async_channel_name_too_long______________________________" will be
truncated to ...

I guess that the expected behavior would be that if the outer level is
> truncated, the rest of the
> channel name should be ignored, as there won't be possible to notify it
> anyway.
>
> In the case of the inner levels creating a channel name too long, it may
> probably sane to just
> check the length of the entire identifier, and truncate -- ensuring that
> channel name doesn't
> end with the level separator.
>
>
Well, I believe that we can forbid too long channel names at all. So it
provides consistent behaviour among different ways we can send
notifications, and I agree with you that "there won't be possible to notify
it anyway". I created a patch for that and attached it to the email. In the
patch I relocated truncation from core scanner to parser. And as the same
core scanner is also used in plsql I added three lines of code to its
scanner to basically truncate too long identifiers in there. Here is an
example of the new behaviour:

-- 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

Regards,
Alexander Cheshev


On Sun, 21 Jul 2024 at 21:36, Emanuel Calvo <3man...@gmail.com> wrote:

>
> Hi Alexander,
>
> I did a review on the new patch version and I observed that the identifier
> passed to the LISTEN command is handled differently between outer and
> inner
> levels.
>
> When the outer level exceeds the 64 characters limitation, the outer level
> of the
> channel name is truncated, but leaves the inner levels in the channel name
> due
> that isn't parsed in the same way.
>
> Also, even if the outer level isn't truncated, it is allowed to add
> channels names
> that exceeds the allowed identifier size.
>
> It can be reproduced just by:
>
>       # LISTEN a.a.a.a.a.lot.of.levels..; -- this doesn't fail at LISTEN,
> but fails in NOTIFY due to channel name too long
>
> In the following, the outer level is truncated, but it doesn't cut out the
> inner levels. This leaves
> listening channels that cannot receive any notifications in the queue:
>
>       # LISTEN
> notify_async_channel_name_too_long____________________________________.a.a.
> ...
>       NOTICE: identifier .... will be truncated
>
>       # select substring(c.channel,0,66), length(c.channel) from
> pg_listening_channels() c(channel) where length(c.channel) > 64;
>       substring |
> notify_async_channel_name_too_long_____________________________.a
>       length    | 1393
>
>
> I guess that the expected behavior would be that if the outer level is
> truncated, the rest of the
> channel name should be ignored, as there won't be possible to notify it
> anyway.
>
> In the case of the inner levels creating a channel name too long, it may
> probably sane to just
> check the length of the entire identifier, and truncate -- ensuring that
> channel name doesn't
> end with the level separator.
>
> Another observation, probably not strictly related to this patch itself
> but the async-notify tests, is that there is no test for
> "payload too long". Probably there is a reason on why isn't in the specs?
>
>
> Regards,
>
>
> El lun, 15 jul 2024 a las 12:59, Alexander Cheshev (<
> alex.ches...@gmail.com>) escribió:
>
>> Hi Emanuel,
>>
>> Changed implementation of the function Exec_UnlistenCommit . v2 of the
>> path contained a bug in the function Exec_UnlistenCommit (added a test case
>> for that) and also it was not implemented in natural to C form using
>> pointers. Now it looks fine and works as expected.
>>
>> In the previous email I forgot to mention that the new implementation of
>> the function Exec_UnlistenCommit has the same space and time complexities
>> as the original implementation (which doesn't support wildcards).
>>
>> Regards,
>> Alexander Cheshev
>>
>>
>> On Sat, 13 Jul 2024 at 13:26, Alexander Cheshev <alex.ches...@gmail.com>
>> wrote:
>>
>>> Hi Emanuel,
>>>
>>> I did a test over the "UNLISTEN >" behavior, and I'm not sure if this is
>>>> expected.
>>>> This command I assume should free all the listening channels, however,
>>>> it doesn't
>>>> seem to do so:
>>>
>>>
>>> TODO “Allow LISTEN on patterns” [1] is a bit vague about that feature.
>>> So I didn't implement it in the first version of the patch. Also I see that
>>> I made a mistake in the documentation and mentioned that it is actually
>>> supported. Sorry for the confusion.
>>>
>>> Besides obvious reasons I think that your finding is especially
>>> attractive for the following reason. We have an UNLISTEN * command. If we
>>> replace > with * in the patch (which I actually did in the new version of
>>> the patch) then we have a generalisation of the above command. For example,
>>> UNLISTEN a* cancels registration on all channels which start with a.
>>>
>>> I attached to the email the new version of the patch which supports the
>>> requested feature. Instead of > I use * for the reason which I mentioned
>>> above. Also I added test cases, changed documentation, etc.
>>>
>>> I appreciate your work, Emanuel! If you have any further findings I will
>>> be glad to adjust the patch accordingly.
>>>
>>> [1]
>>> https://www.postgresql.org/message-id/flat/52693FC5.7070507%40gmail.com
>>>
>>> Regards,
>>> Alexander Cheshev
>>>
>>> Regards,
>>> Alexander Cheshev
>>>
>>>
>>> On Tue, 9 Jul 2024 at 11:01, Emanuel Calvo <3man...@gmail.com> wrote:
>>>
>>>>
>>>> Hello there,
>>>>
>>>>
>>>> El vie, 15 mar 2024 a las 9:01, Alexander Cheshev (<
>>>> alex.ches...@gmail.com>) escribió:
>>>>
>>>>> Hello Hackers,
>>>>>
>>>>> I have implemented TODO “Allow LISTEN on patterns” [1] and attached
>>>>> the patch to the email. The patch basically consists of the following
>>>>> two parts.
>>>>>
>>>>> 1. Support wildcards in LISTEN command
>>>>>
>>>>> Notification channels can be composed of multiple levels in the form
>>>>> ‘a.b.c’ where ‘a’, ‘b’ and ‘c’ are identifiers.
>>>>>
>>>>> Listen 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:
>>>>>  *  ‘%’ 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 listen channel ‘a.b%.c’.
>>>>>  * ‘>’ 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 listen channel ‘a.b>’.
>>>>>
>>>>>
>>>> I did a test over the "UNLISTEN >" behavior, and I'm not sure if this
>>>> is expected.
>>>> This command I assume should free all the listening channels, however,
>>>> it doesn't
>>>> seem to do so:
>>>>
>>>> postgres=# LISTEN device1.alerts.%;
>>>> LISTEN
>>>> postgres=# ;
>>>> Asynchronous notification "device1.alerts.temp" with payload "80"
>>>> received from server process with PID 237.
>>>> postgres=# UNLISTEN >;
>>>> UNLISTEN
>>>> postgres=# ; -- Here I send a notification over the same channel
>>>> Asynchronous notification "device1.alerts.temp" with payload "80"
>>>> received from server process with PID 237.
>>>>
>>>> The same happens with "UNLISTEN %;", although I'm not sure if this
>>>> should have
>>>> the same behavior.
>>>>
>>>> It stops listening correctly if I do explicit UNLISTEN (exact channel
>>>> matching).
>>>>
>>>> I'll be glad to conduct more tests or checks on this.
>>>>
>>>> Cheers,
>>>>
>>>>
>>>> --
>>>> --
>>>> Emanuel Calvo
>>>> Database Engineering
>>>> https://tr3s.ma/aobut
>>>>
>>>>
>
> --
> --
> Emanuel Calvo
> https://tr3s.ma/ <https://tr3s.ma/about>
>
>
From 4f35e78a762e96c8a8411904f0b983ab7b18b16e Mon Sep 17 00:00:00 2001
From: Alexander Cheshev <alex.ches...@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

Reply via email to