From 3136d4fe27052146782f28bf176898277306abc0 Mon Sep 17 00:00:00 2001
From: Mark Dilger <mark.dilger@enterprisedb.com>
Date: Fri, 15 Oct 2021 09:07:15 -0700
Subject: [PATCH v1] Reject patterns with too many parts or wrong db

Object name patterns used by pg_dump and psql potentially contain
multiple parts (dotted names), and nothing prevents users from
specifying a name with too many parts, nor specifying a
database-qualified name for a database other than the currently
connected database.  Prior to PostgreSQL version 14, pg_dump,
pg_dumpall and psql quietly discarded extra parts of the name on the
left.  For example, `pg_dump -t` only expected a possibly schema
qualified table name, not a database name, and the following command

  pg_dump -t production.marketing.customers

quietly ignored the "production" database name with neither warning
nor error.  Commit 2c8726c4b0a496608919d1f78a5abc8c9b6e0868 changed
the behavior of name parsing.  Where names contain more than the
maximum expected number of dots, the extra dots on the right were
interpreted as part of the name, such that the above example was
interpreted as schema=production, relation=marketing.customers.
This turns out to be highly unintuitive to users.

We've had reports that users sometimes copy-and-paste database- and
schema-qualified relation names from the logs.
https://www.postgresql.org/message-id/20211013165426.GD27491%40telsasoft.com

There is no support for cross database references, but allowing a
database qualified pattern when the database portion matches the
current database, as in the above report, seems more friendly than
rejecting it, so do that.  We don't allow the database portion
itself to be a pattern, because if it matched more than one database
(including the current one), there would be confusion about which
database(s) were processed.

Consistent with how we allow db.schemapat.relpat in pg_dump and psql,
also allow db.schemapat for specifying schemas, as:

	\dn mydb.myschema

in psql and

	pg_dump --schema=mydb.myschema

Fix the pre-v14 behavior of ignoring leading portions of patterns
containing too many dotted names, and the v14.0 misfeature of
combining trailing portions of such patterns, and instead reject
such patterns in all cases by raising an error.
---
 doc/src/sgml/ref/psql-ref.sgml       |  17 +-
 src/bin/pg_amcheck/pg_amcheck.c      |  27 +-
 src/bin/pg_amcheck/t/002_nonesuch.pl |  40 ++-
 src/bin/pg_dump/pg_dump.c            |  77 +++-
 src/bin/pg_dump/pg_dumpall.c         |  13 +-
 src/bin/pg_dump/t/002_pg_dump.pl     |  61 +++-
 src/bin/psql/describe.c              | 510 ++++++++++++++++++---------
 src/fe_utils/string_utils.c          | 158 ++++++---
 src/include/fe_utils/string_utils.h  |   8 +-
 src/test/regress/expected/psql.out   | 221 ++++++++++++
 src/test/regress/sql/psql.sql        | 113 ++++++
 11 files changed, 1010 insertions(+), 235 deletions(-)

diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 14e0a4dbe3..7c00a65966 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -3605,14 +3605,27 @@ select 1\; select 2\; select 3;
   </para>
 
   <para>
-   A pattern that contains a dot (<literal>.</literal>) is interpreted as a schema
+   A relation pattern that contains a dot (<literal>.</literal>) is interpreted as a schema
    name pattern followed by an object name pattern.  For example,
    <literal>\dt foo*.*bar*</literal> displays all tables whose table name
    includes <literal>bar</literal> that are in schemas whose schema name
    starts with <literal>foo</literal>.  When no dot appears, then the pattern
    matches only objects that are visible in the current schema search path.
    Again, a dot within double quotes loses its special meaning and is matched
-   literally.
+   literally.  A relation pattern that contains two dots (<literal>.</literal>)
+   is interpreted as a database name followed by a schema name pattern followed
+   by an object name pattern.  The database name portion will not be treated as
+   a pattern and must match the name of the currently connected database, else
+   an error will be raised.
+  </para>
+
+  <para>
+   A schema pattern that contains a dot (<literal>.</literal>) is interpreted
+   as a database name followed by a schema name pattern.  For example,
+   <literal>\dn mydb.*foo*</literal> displays all schemas whose schema name
+   includes <literal>foo</literal>.  The database name portion will not be
+   treated as a pattern and must match the name of the currently connected
+   database, else an error will be raised.
   </para>
 
   <para>
diff --git a/src/bin/pg_amcheck/pg_amcheck.c b/src/bin/pg_amcheck/pg_amcheck.c
index d4a53c8e63..cb2945f9fb 100644
--- a/src/bin/pg_amcheck/pg_amcheck.c
+++ b/src/bin/pg_amcheck/pg_amcheck.c
@@ -1334,10 +1334,17 @@ static void
 append_database_pattern(PatternInfoArray *pia, const char *pattern, int encoding)
 {
 	PQExpBufferData buf;
+	int			dotcnt;
 	PatternInfo *info = extend_pattern_info_array(pia);
 
 	initPQExpBuffer(&buf);
-	patternToSQLRegex(encoding, NULL, NULL, &buf, pattern, false);
+	patternToSQLRegex(encoding, NULL, NULL, &buf, pattern, false, false,
+					  &dotcnt, NULL);
+	if (dotcnt > 0)
+	{
+		pg_log_error("improper qualified name (too many dotted names): %s", pattern);
+		exit(2);
+	}
 	info->pattern = pattern;
 	info->db_regex = pstrdup(buf.data);
 
@@ -1358,12 +1365,19 @@ append_schema_pattern(PatternInfoArray *pia, const char *pattern, int encoding)
 {
 	PQExpBufferData dbbuf;
 	PQExpBufferData nspbuf;
+	int			dotcnt;
 	PatternInfo *info = extend_pattern_info_array(pia);
 
 	initPQExpBuffer(&dbbuf);
 	initPQExpBuffer(&nspbuf);
 
-	patternToSQLRegex(encoding, NULL, &dbbuf, &nspbuf, pattern, false);
+	patternToSQLRegex(encoding, NULL, &dbbuf, &nspbuf, pattern, false, false,
+					  &dotcnt, NULL);
+	if (dotcnt > 1)
+	{
+		pg_log_error("improper qualified name (too many dotted names): %s", pattern);
+		exit(2);
+	}
 	info->pattern = pattern;
 	if (dbbuf.data[0])
 	{
@@ -1395,13 +1409,20 @@ append_relation_pattern_helper(PatternInfoArray *pia, const char *pattern,
 	PQExpBufferData dbbuf;
 	PQExpBufferData nspbuf;
 	PQExpBufferData relbuf;
+	int			dotcnt;
 	PatternInfo *info = extend_pattern_info_array(pia);
 
 	initPQExpBuffer(&dbbuf);
 	initPQExpBuffer(&nspbuf);
 	initPQExpBuffer(&relbuf);
 
-	patternToSQLRegex(encoding, &dbbuf, &nspbuf, &relbuf, pattern, false);
+	patternToSQLRegex(encoding, &dbbuf, &nspbuf, &relbuf, pattern, false,
+					  false, &dotcnt, NULL);
+	if (dotcnt > 2)
+	{
+		pg_log_error("improper relation name (too many dotted names): %s", pattern);
+		exit(2);
+	}
 	info->pattern = pattern;
 	if (dbbuf.data[0])
 	{
diff --git a/src/bin/pg_amcheck/t/002_nonesuch.pl b/src/bin/pg_amcheck/t/002_nonesuch.pl
index e30c1cc546..800203886f 100644
--- a/src/bin/pg_amcheck/t/002_nonesuch.pl
+++ b/src/bin/pg_amcheck/t/002_nonesuch.pl
@@ -6,7 +6,7 @@ use warnings;
 
 use PostgresNode;
 use TestLib;
-use Test::More tests => 76;
+use Test::More tests => 82;
 
 # Test set-up
 my ($node, $port);
@@ -147,6 +147,39 @@ $node->command_checks_all(
 	[qr/pg_amcheck: error: no heap tables to check matching "\."/],
 	'checking table pattern "."');
 
+# Check that a multipart database name is rejected
+$node->command_checks_all(
+	[ 'pg_amcheck', '-d', 'localhost.postgres' ],
+	2,
+	[qr/^$/],
+	[
+		qr/pg_amcheck: error: improper qualified name \(too many dotted names\): localhost\.postgres/
+	],
+	'multipart database patterns are rejected'
+);
+
+# Check that a three-part schema name is rejected
+$node->command_checks_all(
+	[ 'pg_amcheck', '-s', 'localhost.postgres.pg_catalog' ],
+	2,
+	[qr/^$/],
+	[
+		qr/pg_amcheck: error: improper qualified name \(too many dotted names\): localhost\.postgres\.pg_catalog/
+	],
+	'three part schema patterns are rejected'
+);
+
+# Check that a four-part table name is rejected
+$node->command_checks_all(
+	[ 'pg_amcheck', '-t', 'localhost.postgres.pg_catalog.pg_class' ],
+	2,
+	[qr/^$/],
+	[
+		qr/pg_amcheck: error: improper relation name \(too many dotted names\): localhost\.postgres\.pg_catalog\.pg_class/
+	],
+	'four part table patterns are rejected'
+);
+
 #########################################
 # Test checking non-existent databases, schemas, tables, and indexes
 
@@ -165,9 +198,7 @@ $node->command_checks_all(
 		'-d',         'no*such*database',
 		'-r',         'none.none',
 		'-r',         'none.none.none',
-		'-r',         'this.is.a.really.long.dotted.string',
 		'-r',         'postgres.none.none',
-		'-r',         'postgres.long.dotted.string',
 		'-r',         'postgres.pg_catalog.none',
 		'-r',         'postgres.none.pg_class',
 		'-t',         'postgres.pg_catalog.pg_class',          # This exists
@@ -186,15 +217,12 @@ $node->command_checks_all(
 		qr/pg_amcheck: warning: no connectable databases to check matching "no\*such\*database"/,
 		qr/pg_amcheck: warning: no relations to check matching "none\.none"/,
 		qr/pg_amcheck: warning: no connectable databases to check matching "none\.none\.none"/,
-		qr/pg_amcheck: warning: no connectable databases to check matching "this\.is\.a\.really\.long\.dotted\.string"/,
 		qr/pg_amcheck: warning: no relations to check matching "postgres\.none\.none"/,
-		qr/pg_amcheck: warning: no relations to check matching "postgres\.long\.dotted\.string"/,
 		qr/pg_amcheck: warning: no relations to check matching "postgres\.pg_catalog\.none"/,
 		qr/pg_amcheck: warning: no relations to check matching "postgres\.none\.pg_class"/,
 		qr/pg_amcheck: warning: no connectable databases to check matching "no_such_database"/,
 		qr/pg_amcheck: warning: no connectable databases to check matching "no\*such\*database"/,
 		qr/pg_amcheck: warning: no connectable databases to check matching "none\.none\.none"/,
-		qr/pg_amcheck: warning: no connectable databases to check matching "this\.is\.a\.really\.long\.dotted\.string"/,
 	],
 	'many unmatched patterns and one matched pattern under --no-strict-names'
 );
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index ed8ed2f266..dc577a6d1d 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -167,6 +167,9 @@ static void expand_table_name_patterns(Archive *fout,
 									   SimpleStringList *patterns,
 									   SimpleOidList *oids,
 									   bool strict_names);
+static void prohibit_crossdb_refs(PGconn *conn, const char *dbname,
+								  const char *pattern);
+
 static NamespaceInfo *findNamespace(Oid nsoid);
 static void dumpTableData(Archive *fout, const TableDataInfo *tdinfo);
 static void refreshMatViewData(Archive *fout, const TableDataInfo *tdinfo);
@@ -1341,10 +1344,26 @@ expand_schema_name_patterns(Archive *fout,
 
 	for (cell = patterns->head; cell; cell = cell->next)
 	{
+		PQExpBufferData	dbbuf;
+		int		dotcnt;
+		bool	dbname_is_literal;
+
 		appendPQExpBufferStr(query,
 							 "SELECT oid FROM pg_catalog.pg_namespace n\n");
+		initPQExpBuffer(&dbbuf);
 		processSQLNamePattern(GetConnection(fout), query, cell->val, false,
-							  false, NULL, "n.nspname", NULL, NULL);
+							  false, NULL, "n.nspname", NULL, NULL, &dbbuf,
+							  &dotcnt, &dbname_is_literal);
+		if (dotcnt > 1)
+			fatal("improper qualified name (too many dotted names): %s",
+				  cell->val);
+		else if (dotcnt == 1)
+		{
+			if (!dbname_is_literal)
+				fatal("database name must be literal: %s", cell->val);
+			prohibit_crossdb_refs(GetConnection(fout), dbbuf.data, cell->val);
+		}
+		termPQExpBuffer(&dbbuf);
 
 		res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
 		if (strict_names && PQntuples(res) == 0)
@@ -1388,10 +1407,17 @@ expand_extension_name_patterns(Archive *fout,
 	 */
 	for (cell = patterns->head; cell; cell = cell->next)
 	{
+		int		dotcnt;
+		bool	dbname_is_literal;
+
 		appendPQExpBufferStr(query,
 							 "SELECT oid FROM pg_catalog.pg_extension e\n");
 		processSQLNamePattern(GetConnection(fout), query, cell->val, false,
-							  false, NULL, "e.extname", NULL, NULL);
+							  false, NULL, "e.extname", NULL, NULL, NULL,
+							  &dotcnt, &dbname_is_literal);
+		if (dotcnt > 0)
+			fatal("improper qualified name (too many dotted names): %s",
+				  cell->val);
 
 		res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
 		if (strict_names && PQntuples(res) == 0)
@@ -1435,10 +1461,17 @@ expand_foreign_server_name_patterns(Archive *fout,
 
 	for (cell = patterns->head; cell; cell = cell->next)
 	{
+		int		dotcnt;
+		bool	dbname_is_literal;
+
 		appendPQExpBufferStr(query,
 							 "SELECT oid FROM pg_catalog.pg_foreign_server s\n");
 		processSQLNamePattern(GetConnection(fout), query, cell->val, false,
-							  false, NULL, "s.srvname", NULL, NULL);
+							  false, NULL, "s.srvname", NULL, NULL, NULL,
+							  &dotcnt, &dbname_is_literal);
+		if (dotcnt > 0)
+			fatal("improper qualified name (too many dotted names): %s",
+				  cell->val);
 
 		res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
 		if (PQntuples(res) == 0)
@@ -1481,6 +1514,10 @@ expand_table_name_patterns(Archive *fout,
 
 	for (cell = patterns->head; cell; cell = cell->next)
 	{
+		PQExpBufferData	dbbuf;
+		int		dotcnt;
+		bool	dbname_is_literal;
+
 		/*
 		 * Query must remain ABSOLUTELY devoid of unqualified names.  This
 		 * would be unnecessary given a pg_table_is_visible() variant taking a
@@ -1496,9 +1533,21 @@ expand_table_name_patterns(Archive *fout,
 						  RELKIND_RELATION, RELKIND_SEQUENCE, RELKIND_VIEW,
 						  RELKIND_MATVIEW, RELKIND_FOREIGN_TABLE,
 						  RELKIND_PARTITIONED_TABLE);
+		initPQExpBuffer(&dbbuf);
 		processSQLNamePattern(GetConnection(fout), query, cell->val, true,
 							  false, "n.nspname", "c.relname", NULL,
-							  "pg_catalog.pg_table_is_visible(c.oid)");
+							  "pg_catalog.pg_table_is_visible(c.oid)", &dbbuf,
+							  &dotcnt, &dbname_is_literal);
+		if (dotcnt > 2)
+			fatal("improper relation name (too many dotted names): %s",
+				  cell->val);
+		else if (dotcnt == 2)
+		{
+			if (!dbname_is_literal)
+				fatal("database name must be literal: %s", cell->val);
+			prohibit_crossdb_refs(GetConnection(fout), dbbuf.data, cell->val);
+		}
+		termPQExpBuffer(&dbbuf);
 
 		ExecuteSqlStatement(fout, "RESET search_path");
 		res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
@@ -1519,6 +1568,26 @@ expand_table_name_patterns(Archive *fout,
 	destroyPQExpBuffer(query);
 }
 
+/*
+ * Verifies that the connected database name matches the given database name,
+ * and if not, dies with an error about the given pattern.
+ *
+ * The 'dbname' argument should be a literal name parsed from 'pattern'.
+ */
+static void
+prohibit_crossdb_refs(PGconn *conn, const char *dbname, const char *pattern)
+{
+	const char *db;
+
+	db = PQdb(conn);
+	if (db == NULL)
+		fatal("You are currently not connected to a database.");
+
+	if (strcmp(db, dbname) != 0)
+		fatal("cross-database references are not implemented: %s",
+			  pattern);
+}
+
 /*
  * checkExtensionMembership
  *		Determine whether object is an extension member, and if so,
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index c29101704a..42820adcac 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -1438,10 +1438,21 @@ expand_dbname_patterns(PGconn *conn,
 
 	for (SimpleStringListCell *cell = patterns->head; cell; cell = cell->next)
 	{
+		int		dotcnt;
+
 		appendPQExpBufferStr(query,
 							 "SELECT datname FROM pg_catalog.pg_database n\n");
 		processSQLNamePattern(conn, query, cell->val, false,
-							  false, NULL, "datname", NULL, NULL);
+							  false, NULL, "datname", NULL, NULL, NULL,
+							  &dotcnt, NULL);
+
+		if (dotcnt > 0)
+		{
+			pg_log_error("improper qualified name (too many dotted names): %s",
+						 cell->val);
+			PQfinish(conn);
+			exit_nicely(1);
+		}
 
 		res = executeQuery(conn, query->data);
 		for (int i = 0; i < PQntuples(res); i++)
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index c61d95e817..126be5f4e4 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -3588,7 +3588,7 @@ $node->psql('postgres', 'create database regress_public_owner;');
 
 # Start with number of command_fails_like()*2 tests below (each
 # command_fails_like is actually 2 tests)
-my $num_tests = 12;
+my $num_tests = 27;
 
 foreach my $run (sort keys %pgdump_runs)
 {
@@ -3761,6 +3761,65 @@ command_fails_like(
 	qr/\Qpg_dump: error: no matching tables were found for pattern\E/,
 	'no matching tables');
 
+#########################################
+# Test invalid multipart database names
+
+$node->command_fails_like(
+	[ 'pg_dumpall', '--exclude-database', 'myhost.mydb' ],
+	qr/pg_dumpall: error: improper qualified name \(too many dotted names\): myhost\.mydb/,
+	'pg_dumpall: option --exclude-database rejects multipart database names'
+);
+
+#########################################
+# Test valid database exclusion patterns
+$node->command_ok(
+	[ 'pg_dumpall', '--exclude-database', '??*' ],
+	'pg_dumpall: option --exclude-database handles database name patterns'
+);
+
+
+#########################################
+# Test invalid multipart schema names
+
+$node->command_fails_like(
+	[ 'pg_dump', '--schema', 'myhost.mydb.myschema' ],
+	qr/pg_dump: error: improper qualified name \(too many dotted names\): myhost\.mydb\.myschema/,
+	'pg_dump: option --schema rejects three-part schema names'
+);
+
+$node->command_fails_like(
+	[ 'pg_dump', '--schema', 'otherdb.myschema' ],
+	qr/pg_dump: error: cross-database references are not implemented: otherdb\.myschema/,
+	'pg_dump: option --schema rejects cross-database multipart schema names'
+);
+
+$node->command_fails_like(
+	[ 'pg_dump', '--schema', 'otherdb.myschema' ],
+	qr/pg_dump: error: cross-database references are not implemented: otherdb\.myschema/,
+	'pg_dump: option --schema rejects cross-database multipart schema names'
+);
+
+#########################################
+# Test invalid multipart relation names
+
+$node->command_fails_like(
+	[ 'pg_dump', '--table', 'myhost.mydb.myschema.mytable' ],
+	qr/pg_dump: error: improper relation name \(too many dotted names\): myhost\.mydb\.myschema\.mytable/,
+	'pg_dump: option --table rejects four-part table names'
+);
+
+$node->command_fails_like(
+	[ 'pg_dump', '--table', 'otherdb.pg_catalog.pg_class' ],
+	qr/pg_dump: error: cross-database references are not implemented: otherdb\.pg_catalog\.pg_class/,
+	'pg_dump: option --table rejects cross-database three part table names'
+);
+
+$node->command_fails_like(
+	[ 'pg_dump', '--table', 'ma??.pg_catalog.pg_class' ],
+	qr/pg_dump: error: database name must be literal: ma\?\?\.pg_catalog\.pg_class/,
+	'pg_dump: option --table rejects non-literal database name'
+);
+
 #########################################
 # Run all runs
 
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ea4ca5c05c..8a1aa8ca88 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -45,6 +45,12 @@ static bool describeOneTSConfig(const char *oid, const char *nspname,
 								const char *pnspname, const char *prsname);
 static void printACLColumn(PQExpBuffer buf, const char *colname);
 static bool listOneExtensionContents(const char *extname, const char *oid);
+static bool validateSQLNamePattern(PQExpBuffer buf, const char *pattern,
+								   bool have_where, bool force_escape,
+								   const char *schemavar, const char *namevar,
+								   const char *altnamevar,
+								   const char *visibilityrule,
+								   bool *added_clause, int maxparts);
 
 
 /*----------------
@@ -121,9 +127,11 @@ describeAggregates(const char *pattern, bool verbose, bool showSystem)
 		appendPQExpBufferStr(&buf, "      AND n.nspname <> 'pg_catalog'\n"
 							 "      AND n.nspname <> 'information_schema'\n");
 
-	processSQLNamePattern(pset.db, &buf, pattern, true, false,
-						  "n.nspname", "p.proname", NULL,
-						  "pg_catalog.pg_function_is_visible(p.oid)");
+	if (!validateSQLNamePattern(&buf, pattern, true, false,
+							  "n.nspname", "p.proname", NULL,
+							  "pg_catalog.pg_function_is_visible(p.oid)",
+							  NULL, 3))
+		return true;
 
 	appendPQExpBufferStr(&buf, "ORDER BY 1, 2, 4;");
 
@@ -189,9 +197,11 @@ describeAccessMethods(const char *pattern, bool verbose)
 	appendPQExpBufferStr(&buf,
 						 "\nFROM pg_catalog.pg_am\n");
 
-	processSQLNamePattern(pset.db, &buf, pattern, false, false,
-						  NULL, "amname", NULL,
-						  NULL);
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								NULL, "amname", NULL,
+								NULL,
+								NULL, 1))
+		return true;
 
 	appendPQExpBufferStr(&buf, "ORDER BY 1;");
 
@@ -276,9 +286,11 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBufferStr(&buf,
 						 "\nFROM pg_catalog.pg_tablespace\n");
 
-	processSQLNamePattern(pset.db, &buf, pattern, false, false,
-						  NULL, "spcname", NULL,
-						  NULL);
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								NULL, "spcname", NULL,
+								NULL,
+								NULL, 1))
+		return true;
 
 	appendPQExpBufferStr(&buf, "ORDER BY 1;");
 
@@ -639,9 +651,11 @@ describeFunctions(const char *functypes, const char *func_pattern,
 		appendPQExpBufferStr(&buf, "      )\n");
 	}
 
-	processSQLNamePattern(pset.db, &buf, func_pattern, have_where, false,
-						  "n.nspname", "p.proname", NULL,
-						  "pg_catalog.pg_function_is_visible(p.oid)");
+	if (!validateSQLNamePattern(&buf, func_pattern, have_where, false,
+								"n.nspname", "p.proname", NULL,
+								"pg_catalog.pg_function_is_visible(p.oid)",
+								NULL, 3))
+		return true;
 
 	for (int i = 0; i < num_arg_patterns; i++)
 	{
@@ -663,10 +677,12 @@ describeFunctions(const char *functypes, const char *func_pattern,
 					 "pg_catalog.format_type(t%d.oid, NULL)", i);
 			snprintf(tiv, sizeof(tiv),
 					 "pg_catalog.pg_type_is_visible(t%d.oid)", i);
-			processSQLNamePattern(pset.db, &buf,
-								  map_typename_pattern(arg_patterns[i]),
-								  true, false,
-								  nspname, typname, ft, tiv);
+			if (!validateSQLNamePattern(&buf,
+										map_typename_pattern(arg_patterns[i]),
+										true, false,
+										nspname, typname, ft, tiv,
+										NULL, 3))
+				return true;
 		}
 		else
 		{
@@ -804,11 +820,13 @@ describeTypes(const char *pattern, bool verbose, bool showSystem)
 							 "      AND n.nspname <> 'information_schema'\n");
 
 	/* Match name pattern against either internal or external name */
-	processSQLNamePattern(pset.db, &buf, map_typename_pattern(pattern),
-						  true, false,
-						  "n.nspname", "t.typname",
-						  "pg_catalog.format_type(t.oid, NULL)",
-						  "pg_catalog.pg_type_is_visible(t.oid)");
+	if (!validateSQLNamePattern(&buf, map_typename_pattern(pattern),
+								true, false,
+								"n.nspname", "t.typname",
+								"pg_catalog.format_type(t.oid, NULL)",
+								"pg_catalog.pg_type_is_visible(t.oid)",
+								NULL, 3))
+		return true;
 
 	appendPQExpBufferStr(&buf, "ORDER BY 1, 2;");
 
@@ -958,10 +976,12 @@ describeOperators(const char *oper_pattern,
 		appendPQExpBufferStr(&buf, "WHERE n.nspname <> 'pg_catalog'\n"
 							 "      AND n.nspname <> 'information_schema'\n");
 
-	processSQLNamePattern(pset.db, &buf, oper_pattern,
-						  !showSystem && !oper_pattern, true,
-						  "n.nspname", "o.oprname", NULL,
-						  "pg_catalog.pg_operator_is_visible(o.oid)");
+	if (!validateSQLNamePattern(&buf, oper_pattern,
+								!showSystem && !oper_pattern, true,
+								"n.nspname", "o.oprname", NULL,
+								"pg_catalog.pg_operator_is_visible(o.oid)",
+								NULL, 3))
+		return true;
 
 	if (num_arg_patterns == 1)
 		appendPQExpBufferStr(&buf, "  AND o.oprleft = 0\n");
@@ -986,10 +1006,12 @@ describeOperators(const char *oper_pattern,
 					 "pg_catalog.format_type(t%d.oid, NULL)", i);
 			snprintf(tiv, sizeof(tiv),
 					 "pg_catalog.pg_type_is_visible(t%d.oid)", i);
-			processSQLNamePattern(pset.db, &buf,
-								  map_typename_pattern(arg_patterns[i]),
-								  true, false,
-								  nspname, typname, ft, tiv);
+			if (!validateSQLNamePattern(&buf,
+										map_typename_pattern(arg_patterns[i]),
+										true, false,
+										nspname, typname, ft, tiv,
+										NULL, 3))
+				return true;
 		}
 		else
 		{
@@ -1067,8 +1089,10 @@ listAllDbs(const char *pattern, bool verbose)
 							 "  JOIN pg_catalog.pg_tablespace t on d.dattablespace = t.oid\n");
 
 	if (pattern)
-		processSQLNamePattern(pset.db, &buf, pattern, false, false,
-							  NULL, "d.datname", NULL, NULL);
+		if (!validateSQLNamePattern(&buf, pattern, false, false,
+									NULL, "d.datname", NULL, NULL,
+									NULL, 1))
+			return true;
 
 	appendPQExpBufferStr(&buf, "ORDER BY 1;");
 	res = PSQLexec(buf.data);
@@ -1218,9 +1242,11 @@ permissionsList(const char *pattern)
 	 * point of view.  You can see 'em by explicit request though, eg with \z
 	 * pg_catalog.*
 	 */
-	processSQLNamePattern(pset.db, &buf, pattern, true, false,
-						  "n.nspname", "c.relname", NULL,
-						  "n.nspname !~ '^pg_' AND pg_catalog.pg_table_is_visible(c.oid)");
+	if (!validateSQLNamePattern(&buf, pattern, true, false,
+								"n.nspname", "c.relname", NULL,
+								"n.nspname !~ '^pg_' AND pg_catalog.pg_table_is_visible(c.oid)",
+								NULL, 3))
+		return true;
 
 	appendPQExpBufferStr(&buf, "ORDER BY 1, 2;");
 
@@ -1295,11 +1321,13 @@ listDefaultACLs(const char *pattern)
 	appendPQExpBufferStr(&buf, "\nFROM pg_catalog.pg_default_acl d\n"
 						 "     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = d.defaclnamespace\n");
 
-	processSQLNamePattern(pset.db, &buf, pattern, false, false,
-						  NULL,
-						  "n.nspname",
-						  "pg_catalog.pg_get_userbyid(d.defaclrole)",
-						  NULL);
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								NULL,
+								"n.nspname",
+								"pg_catalog.pg_get_userbyid(d.defaclrole)",
+								NULL,
+								NULL, 3))
+		return true;
 
 	appendPQExpBufferStr(&buf, "ORDER BY 1, 2, 3;");
 
@@ -1371,9 +1399,11 @@ objectDescription(const char *pattern, bool showSystem)
 		appendPQExpBufferStr(&buf, "WHERE n.nspname <> 'pg_catalog'\n"
 							 "      AND n.nspname <> 'information_schema'\n");
 
-	processSQLNamePattern(pset.db, &buf, pattern, !showSystem && !pattern,
-						  false, "n.nspname", "pgc.conname", NULL,
-						  "pg_catalog.pg_table_is_visible(c.oid)");
+	if (!validateSQLNamePattern(&buf, pattern, !showSystem && !pattern,
+								false, "n.nspname", "pgc.conname", NULL,
+								"pg_catalog.pg_table_is_visible(c.oid)",
+								NULL, 3))
+		return true;
 
 	/* Domain constraint descriptions */
 	appendPQExpBuffer(&buf,
@@ -1393,9 +1423,11 @@ objectDescription(const char *pattern, bool showSystem)
 		appendPQExpBufferStr(&buf, "WHERE n.nspname <> 'pg_catalog'\n"
 							 "      AND n.nspname <> 'information_schema'\n");
 
-	processSQLNamePattern(pset.db, &buf, pattern, !showSystem && !pattern,
-						  false, "n.nspname", "pgc.conname", NULL,
-						  "pg_catalog.pg_type_is_visible(t.oid)");
+	if (!validateSQLNamePattern(&buf, pattern, !showSystem && !pattern,
+								false, "n.nspname", "pgc.conname", NULL,
+								"pg_catalog.pg_type_is_visible(t.oid)",
+								NULL, 3))
+		return true;
 
 
 	/*
@@ -1421,9 +1453,11 @@ objectDescription(const char *pattern, bool showSystem)
 			appendPQExpBufferStr(&buf, "      AND n.nspname <> 'pg_catalog'\n"
 								 "      AND n.nspname <> 'information_schema'\n");
 
-		processSQLNamePattern(pset.db, &buf, pattern, true, false,
-							  "n.nspname", "o.opcname", NULL,
-							  "pg_catalog.pg_opclass_is_visible(o.oid)");
+		if (!validateSQLNamePattern(&buf, pattern, true, false,
+									"n.nspname", "o.opcname", NULL,
+									"pg_catalog.pg_opclass_is_visible(o.oid)",
+									NULL, 3))
+			return true;
 	}
 
 	/*
@@ -1450,9 +1484,11 @@ objectDescription(const char *pattern, bool showSystem)
 			appendPQExpBufferStr(&buf, "      AND n.nspname <> 'pg_catalog'\n"
 								 "      AND n.nspname <> 'information_schema'\n");
 
-		processSQLNamePattern(pset.db, &buf, pattern, true, false,
-							  "n.nspname", "opf.opfname", NULL,
-							  "pg_catalog.pg_opfamily_is_visible(opf.oid)");
+		if (!validateSQLNamePattern(&buf, pattern, true, false,
+									"n.nspname", "opf.opfname", NULL,
+									"pg_catalog.pg_opfamily_is_visible(opf.oid)",
+									NULL, 3))
+			return true;
 	}
 
 	/* Rule descriptions (ignore rules for views) */
@@ -1472,9 +1508,11 @@ objectDescription(const char *pattern, bool showSystem)
 		appendPQExpBufferStr(&buf, "      AND n.nspname <> 'pg_catalog'\n"
 							 "      AND n.nspname <> 'information_schema'\n");
 
-	processSQLNamePattern(pset.db, &buf, pattern, true, false,
-						  "n.nspname", "r.rulename", NULL,
-						  "pg_catalog.pg_table_is_visible(c.oid)");
+	if (!validateSQLNamePattern(&buf, pattern, true, false,
+								"n.nspname", "r.rulename", NULL,
+								"pg_catalog.pg_table_is_visible(c.oid)",
+								NULL, 3))
+		return true;
 
 	/* Trigger descriptions */
 	appendPQExpBuffer(&buf,
@@ -1492,9 +1530,11 @@ objectDescription(const char *pattern, bool showSystem)
 		appendPQExpBufferStr(&buf, "WHERE n.nspname <> 'pg_catalog'\n"
 							 "      AND n.nspname <> 'information_schema'\n");
 
-	processSQLNamePattern(pset.db, &buf, pattern, !showSystem && !pattern, false,
-						  "n.nspname", "t.tgname", NULL,
-						  "pg_catalog.pg_table_is_visible(c.oid)");
+	if (!validateSQLNamePattern(&buf, pattern, !showSystem && !pattern, false,
+								"n.nspname", "t.tgname", NULL,
+								"pg_catalog.pg_table_is_visible(c.oid)",
+								NULL, 3))
+		return true;
 
 	appendPQExpBufferStr(&buf,
 						 ") AS tt\n"
@@ -1548,9 +1588,11 @@ describeTableDetails(const char *pattern, bool verbose, bool showSystem)
 		appendPQExpBufferStr(&buf, "WHERE n.nspname <> 'pg_catalog'\n"
 							 "      AND n.nspname <> 'information_schema'\n");
 
-	processSQLNamePattern(pset.db, &buf, pattern, !showSystem && !pattern, false,
-						  "n.nspname", "c.relname", NULL,
-						  "pg_catalog.pg_table_is_visible(c.oid)");
+	if (!validateSQLNamePattern(&buf, pattern, !showSystem && !pattern, false,
+								"n.nspname", "c.relname", NULL,
+								"pg_catalog.pg_table_is_visible(c.oid)",
+								NULL, 3))
+		return true;
 
 	appendPQExpBufferStr(&buf, "ORDER BY 2, 3;");
 
@@ -3783,8 +3825,10 @@ describeRoles(const char *pattern, bool verbose, bool showSystem)
 		if (!showSystem && !pattern)
 			appendPQExpBufferStr(&buf, "WHERE r.rolname !~ '^pg_'\n");
 
-		processSQLNamePattern(pset.db, &buf, pattern, false, false,
-							  NULL, "r.rolname", NULL, NULL);
+		if (!validateSQLNamePattern(&buf, pattern, false, false,
+									NULL, "r.rolname", NULL, NULL,
+									NULL, 1))
+			return true;
 	}
 	else
 	{
@@ -3798,8 +3842,10 @@ describeRoles(const char *pattern, bool verbose, bool showSystem)
 						  "  ARRAY(SELECT g.groname FROM pg_catalog.pg_group g WHERE u.usesysid = ANY(g.grolist)) as memberof"
 						  "\nFROM pg_catalog.pg_user u\n");
 
-		processSQLNamePattern(pset.db, &buf, pattern, false, false,
-							  NULL, "u.usename", NULL, NULL);
+		if (!validateSQLNamePattern(&buf, pattern, false, false,
+									NULL, "u.usename", NULL, NULL,
+									NULL, 1))
+			return true;
 	}
 
 	appendPQExpBufferStr(&buf, "ORDER BY 1;");
@@ -3934,10 +3980,13 @@ listDbRoleSettings(const char *pattern, const char *pattern2)
 					  gettext_noop("Role"),
 					  gettext_noop("Database"),
 					  gettext_noop("Settings"));
-	havewhere = processSQLNamePattern(pset.db, &buf, pattern, false, false,
-									  NULL, "r.rolname", NULL, NULL);
-	processSQLNamePattern(pset.db, &buf, pattern2, havewhere, false,
-						  NULL, "d.datname", NULL, NULL);
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								NULL, "r.rolname", NULL, NULL, &havewhere, 1))
+		return true;
+	if (!validateSQLNamePattern(&buf, pattern2, havewhere, false,
+								NULL, "d.datname", NULL, NULL,
+								NULL, 1))
+		return true;
 	appendPQExpBufferStr(&buf, "ORDER BY 1, 2;");
 
 	res = PSQLexec(buf.data);
@@ -4152,9 +4201,11 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys
 							 "      AND n.nspname !~ '^pg_toast'\n"
 							 "      AND n.nspname <> 'information_schema'\n");
 
-	processSQLNamePattern(pset.db, &buf, pattern, true, false,
-						  "n.nspname", "c.relname", NULL,
-						  "pg_catalog.pg_table_is_visible(c.oid)");
+	if (!validateSQLNamePattern(&buf, pattern, true, false,
+								"n.nspname", "c.relname", NULL,
+								"pg_catalog.pg_table_is_visible(c.oid)",
+								NULL, 3))
+		return true;
 
 	appendPQExpBufferStr(&buf, "ORDER BY 1,2;");
 
@@ -4367,9 +4418,11 @@ listPartitionedTables(const char *reltypes, const char *pattern, bool verbose)
 							 "      AND n.nspname !~ '^pg_toast'\n"
 							 "      AND n.nspname <> 'information_schema'\n");
 
-	processSQLNamePattern(pset.db, &buf, pattern, true, false,
-						  "n.nspname", "c.relname", NULL,
-						  "pg_catalog.pg_table_is_visible(c.oid)");
+	if (!validateSQLNamePattern(&buf, pattern, true, false,
+								"n.nspname", "c.relname", NULL,
+								"pg_catalog.pg_table_is_visible(c.oid)",
+								NULL, 3))
+		return true;
 
 	appendPQExpBuffer(&buf, "ORDER BY \"Schema\", %s%s\"Name\";",
 					  mixed_output ? "\"Type\" DESC, " : "",
@@ -4447,8 +4500,10 @@ listLanguages(const char *pattern, bool verbose, bool showSystem)
 					  gettext_noop("Description"));
 
 	if (pattern)
-		processSQLNamePattern(pset.db, &buf, pattern, false, false,
-							  NULL, "l.lanname", NULL, NULL);
+		if (!validateSQLNamePattern(&buf, pattern, false, false,
+									NULL, "l.lanname", NULL, NULL,
+									NULL, 2))
+			return true;
 
 	if (!showSystem && !pattern)
 		appendPQExpBufferStr(&buf, "WHERE l.lanplcallfoid != 0\n");
@@ -4537,9 +4592,11 @@ listDomains(const char *pattern, bool verbose, bool showSystem)
 		appendPQExpBufferStr(&buf, "      AND n.nspname <> 'pg_catalog'\n"
 							 "      AND n.nspname <> 'information_schema'\n");
 
-	processSQLNamePattern(pset.db, &buf, pattern, true, false,
-						  "n.nspname", "t.typname", NULL,
-						  "pg_catalog.pg_type_is_visible(t.oid)");
+	if (!validateSQLNamePattern(&buf, pattern, true, false,
+								"n.nspname", "t.typname", NULL,
+								"pg_catalog.pg_type_is_visible(t.oid)",
+								NULL, 3))
+		return true;
 
 	appendPQExpBufferStr(&buf, "ORDER BY 1, 2;");
 
@@ -4611,9 +4668,11 @@ listConversions(const char *pattern, bool verbose, bool showSystem)
 		appendPQExpBufferStr(&buf, "  AND n.nspname <> 'pg_catalog'\n"
 							 "  AND n.nspname <> 'information_schema'\n");
 
-	processSQLNamePattern(pset.db, &buf, pattern, true, false,
-						  "n.nspname", "c.conname", NULL,
-						  "pg_catalog.pg_conversion_is_visible(c.oid)");
+	if (!validateSQLNamePattern(&buf, pattern, true, false,
+								"n.nspname", "c.conname", NULL,
+								"pg_catalog.pg_conversion_is_visible(c.oid)",
+								NULL, 3))
+		return true;
 
 	appendPQExpBufferStr(&buf, "ORDER BY 1, 2;");
 
@@ -4678,8 +4737,10 @@ listEventTriggers(const char *pattern, bool verbose)
 	appendPQExpBufferStr(&buf,
 						 "\nFROM pg_catalog.pg_event_trigger e ");
 
-	processSQLNamePattern(pset.db, &buf, pattern, false, false,
-						  NULL, "evtname", NULL, NULL);
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								NULL, "evtname", NULL, NULL,
+								NULL, 1))
+		return true;
 
 	appendPQExpBufferStr(&buf, "ORDER BY 1");
 
@@ -4770,10 +4831,12 @@ listExtendedStats(const char *pattern)
 	appendPQExpBufferStr(&buf,
 						 " \nFROM pg_catalog.pg_statistic_ext es \n");
 
-	processSQLNamePattern(pset.db, &buf, pattern,
-						  false, false,
-						  "es.stxnamespace::pg_catalog.regnamespace::text", "es.stxname",
-						  NULL, "pg_catalog.pg_statistics_obj_is_visible(es.oid)");
+	if (!validateSQLNamePattern(&buf, pattern,
+								false, false,
+								"es.stxnamespace::pg_catalog.regnamespace::text", "es.stxname",
+								NULL, "pg_catalog.pg_statistics_obj_is_visible(es.oid)",
+								NULL, 3))
+		return true;
 
 	appendPQExpBufferStr(&buf, "ORDER BY 1, 2;");
 
@@ -4879,17 +4942,21 @@ listCasts(const char *pattern, bool verbose)
 	 * Match name pattern against either internal or external name of either
 	 * castsource or casttarget
 	 */
-	processSQLNamePattern(pset.db, &buf, pattern, true, false,
-						  "ns.nspname", "ts.typname",
-						  "pg_catalog.format_type(ts.oid, NULL)",
-						  "pg_catalog.pg_type_is_visible(ts.oid)");
+	if (!validateSQLNamePattern(&buf, pattern, true, false,
+								"ns.nspname", "ts.typname",
+								"pg_catalog.format_type(ts.oid, NULL)",
+								"pg_catalog.pg_type_is_visible(ts.oid)",
+								NULL, 3))
+		return true;
 
 	appendPQExpBufferStr(&buf, ") OR (true");
 
-	processSQLNamePattern(pset.db, &buf, pattern, true, false,
-						  "nt.nspname", "tt.typname",
-						  "pg_catalog.format_type(tt.oid, NULL)",
-						  "pg_catalog.pg_type_is_visible(tt.oid)");
+	if (!validateSQLNamePattern(&buf, pattern, true, false,
+								"nt.nspname", "tt.typname",
+								"pg_catalog.format_type(tt.oid, NULL)",
+								"pg_catalog.pg_type_is_visible(tt.oid)",
+								NULL, 3))
+		return true;
 
 	appendPQExpBufferStr(&buf, ") )\nORDER BY 1, 2;");
 
@@ -4986,9 +5053,11 @@ listCollations(const char *pattern, bool verbose, bool showSystem)
 	 */
 	appendPQExpBufferStr(&buf, "      AND c.collencoding IN (-1, pg_catalog.pg_char_to_encoding(pg_catalog.getdatabaseencoding()))\n");
 
-	processSQLNamePattern(pset.db, &buf, pattern, true, false,
-						  "n.nspname", "c.collname", NULL,
-						  "pg_catalog.pg_collation_is_visible(c.oid)");
+	if (!validateSQLNamePattern(&buf, pattern, true, false,
+								"n.nspname", "c.collname", NULL,
+								"pg_catalog.pg_collation_is_visible(c.oid)",
+								NULL, 3))
+		return true;
 
 	appendPQExpBufferStr(&buf, "ORDER BY 1, 2;");
 
@@ -5044,10 +5113,12 @@ listSchemas(const char *pattern, bool verbose, bool showSystem)
 		appendPQExpBufferStr(&buf,
 							 "WHERE n.nspname !~ '^pg_' AND n.nspname <> 'information_schema'\n");
 
-	processSQLNamePattern(pset.db, &buf, pattern,
-						  !showSystem && !pattern, false,
-						  NULL, "n.nspname", NULL,
-						  NULL);
+	if (!validateSQLNamePattern(&buf, pattern,
+								!showSystem && !pattern, false,
+								NULL, "n.nspname", NULL,
+								NULL,
+								NULL, 2))
+		return true;
 
 	appendPQExpBufferStr(&buf, "ORDER BY 1;");
 
@@ -5105,9 +5176,11 @@ listTSParsers(const char *pattern, bool verbose)
 					  gettext_noop("Description")
 		);
 
-	processSQLNamePattern(pset.db, &buf, pattern, false, false,
-						  "n.nspname", "p.prsname", NULL,
-						  "pg_catalog.pg_ts_parser_is_visible(p.oid)");
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								"n.nspname", "p.prsname", NULL,
+								"pg_catalog.pg_ts_parser_is_visible(p.oid)",
+								NULL, 3))
+		return true;
 
 	appendPQExpBufferStr(&buf, "ORDER BY 1, 2;");
 
@@ -5146,9 +5219,11 @@ listTSParsersVerbose(const char *pattern)
 					  "LEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.prsnamespace\n"
 		);
 
-	processSQLNamePattern(pset.db, &buf, pattern, false, false,
-						  "n.nspname", "p.prsname", NULL,
-						  "pg_catalog.pg_ts_parser_is_visible(p.oid)");
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								"n.nspname", "p.prsname", NULL,
+								"pg_catalog.pg_ts_parser_is_visible(p.oid)",
+								NULL, 3))
+		return true;
 
 	appendPQExpBufferStr(&buf, "ORDER BY 1, 2;");
 
@@ -5363,9 +5438,11 @@ listTSDictionaries(const char *pattern, bool verbose)
 	appendPQExpBufferStr(&buf, "FROM pg_catalog.pg_ts_dict d\n"
 						 "LEFT JOIN pg_catalog.pg_namespace n ON n.oid = d.dictnamespace\n");
 
-	processSQLNamePattern(pset.db, &buf, pattern, false, false,
-						  "n.nspname", "d.dictname", NULL,
-						  "pg_catalog.pg_ts_dict_is_visible(d.oid)");
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								"n.nspname", "d.dictname", NULL,
+								"pg_catalog.pg_ts_dict_is_visible(d.oid)",
+								NULL, 3))
+		return true;
 
 	appendPQExpBufferStr(&buf, "ORDER BY 1, 2;");
 
@@ -5434,9 +5511,11 @@ listTSTemplates(const char *pattern, bool verbose)
 	appendPQExpBufferStr(&buf, "FROM pg_catalog.pg_ts_template t\n"
 						 "LEFT JOIN pg_catalog.pg_namespace n ON n.oid = t.tmplnamespace\n");
 
-	processSQLNamePattern(pset.db, &buf, pattern, false, false,
-						  "n.nspname", "t.tmplname", NULL,
-						  "pg_catalog.pg_ts_template_is_visible(t.oid)");
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								"n.nspname", "t.tmplname", NULL,
+								"pg_catalog.pg_ts_template_is_visible(t.oid)",
+								NULL, 3))
+		return true;
 
 	appendPQExpBufferStr(&buf, "ORDER BY 1, 2;");
 
@@ -5494,9 +5573,11 @@ listTSConfigs(const char *pattern, bool verbose)
 					  gettext_noop("Description")
 		);
 
-	processSQLNamePattern(pset.db, &buf, pattern, false, false,
-						  "n.nspname", "c.cfgname", NULL,
-						  "pg_catalog.pg_ts_config_is_visible(c.oid)");
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								"n.nspname", "c.cfgname", NULL,
+								"pg_catalog.pg_ts_config_is_visible(c.oid)",
+								NULL, 3))
+		return true;
 
 	appendPQExpBufferStr(&buf, "ORDER BY 1, 2;");
 
@@ -5536,9 +5617,11 @@ listTSConfigsVerbose(const char *pattern)
 					  "WHERE  p.oid = c.cfgparser\n"
 		);
 
-	processSQLNamePattern(pset.db, &buf, pattern, true, false,
-						  "n.nspname", "c.cfgname", NULL,
-						  "pg_catalog.pg_ts_config_is_visible(c.oid)");
+	if (!validateSQLNamePattern(&buf, pattern, true, false,
+								"n.nspname", "c.cfgname", NULL,
+								"pg_catalog.pg_ts_config_is_visible(c.oid)",
+								NULL, 3))
+		return true;
 
 	appendPQExpBufferStr(&buf, "ORDER BY 3, 2;");
 
@@ -5724,8 +5807,10 @@ listForeignDataWrappers(const char *pattern, bool verbose)
 							 "       ON d.classoid = fdw.tableoid "
 							 "AND d.objoid = fdw.oid AND d.objsubid = 0\n");
 
-	processSQLNamePattern(pset.db, &buf, pattern, false, false,
-						  NULL, "fdwname", NULL, NULL);
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								NULL, "fdwname", NULL, NULL,
+								NULL, 1))
+		return true;
 
 	appendPQExpBufferStr(&buf, "ORDER BY 1;");
 
@@ -5806,8 +5891,10 @@ listForeignServers(const char *pattern, bool verbose)
 							 "ON d.classoid = s.tableoid AND d.objoid = s.oid "
 							 "AND d.objsubid = 0\n");
 
-	processSQLNamePattern(pset.db, &buf, pattern, false, false,
-						  NULL, "s.srvname", NULL, NULL);
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								NULL, "s.srvname", NULL, NULL,
+								NULL, 1))
+		return true;
 
 	appendPQExpBufferStr(&buf, "ORDER BY 1;");
 
@@ -5867,8 +5954,10 @@ listUserMappings(const char *pattern, bool verbose)
 
 	appendPQExpBufferStr(&buf, "\nFROM pg_catalog.pg_user_mappings um\n");
 
-	processSQLNamePattern(pset.db, &buf, pattern, false, false,
-						  NULL, "um.srvname", "um.usename", NULL);
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								NULL, "um.srvname", "um.usename", NULL,
+								NULL, 1))
+		return true;
 
 	appendPQExpBufferStr(&buf, "ORDER BY 1, 2;");
 
@@ -5944,9 +6033,11 @@ listForeignTables(const char *pattern, bool verbose)
 							 "          ON d.classoid = c.tableoid AND "
 							 "d.objoid = c.oid AND d.objsubid = 0\n");
 
-	processSQLNamePattern(pset.db, &buf, pattern, false, false,
-						  "n.nspname", "c.relname", NULL,
-						  "pg_catalog.pg_table_is_visible(c.oid)");
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								"n.nspname", "c.relname", NULL,
+								"pg_catalog.pg_table_is_visible(c.oid)",
+								NULL, 3))
+		return true;
 
 	appendPQExpBufferStr(&buf, "ORDER BY 1, 2;");
 
@@ -6000,10 +6091,12 @@ listExtensions(const char *pattern)
 					  gettext_noop("Schema"),
 					  gettext_noop("Description"));
 
-	processSQLNamePattern(pset.db, &buf, pattern,
-						  false, false,
-						  NULL, "e.extname", NULL,
-						  NULL);
+	if (!validateSQLNamePattern(&buf, pattern,
+								false, false,
+								NULL, "e.extname", NULL,
+								NULL,
+								NULL, 1))
+		return true;
 
 	appendPQExpBufferStr(&buf, "ORDER BY 1;");
 
@@ -6049,10 +6142,12 @@ listExtensionContents(const char *pattern)
 					  "SELECT e.extname, e.oid\n"
 					  "FROM pg_catalog.pg_extension e\n");
 
-	processSQLNamePattern(pset.db, &buf, pattern,
-						  false, false,
-						  NULL, "e.extname", NULL,
-						  NULL);
+	if (!validateSQLNamePattern(&buf, pattern,
+								false, false,
+								NULL, "e.extname", NULL,
+								NULL,
+								NULL, 1))
+		return true;
 
 	appendPQExpBufferStr(&buf, "ORDER BY 1;");
 
@@ -6134,6 +6229,61 @@ listOneExtensionContents(const char *extname, const char *oid)
 	return true;
 }
 
+/*
+ * validateSQLNamePattern
+ *
+ * Wrapper around string_utils's processSQLNamePattern which also checks the
+ * pattern's validity.  In addition to that function's parameters, takes a
+ * 'maxparts' parameter specifying the maximum number of dotted names the
+ * pattern is allowed to have, and a 'added_clause' parameter that returns by
+ * reference whether a clause was added to 'buf'.  Returns whether the pattern
+ * passed validation, after logging any errors.
+ */
+static bool
+validateSQLNamePattern(PQExpBuffer buf, const char *pattern, bool have_where,
+					   bool force_escape, const char *schemavar,
+					   const char *namevar, const char *altnamevar,
+					   const char *visibilityrule, bool *added_clause,
+					   int maxparts)
+{
+	PQExpBufferData	dbbuf;
+	int			dotcnt;
+	bool		dbname_is_literal;
+	bool		added;
+
+	initPQExpBuffer(&dbbuf);
+	added = processSQLNamePattern(pset.db, buf, pattern, have_where, force_escape,
+								  schemavar, namevar, altnamevar,
+								  visibilityrule, &dbbuf, &dotcnt,
+								  &dbname_is_literal);
+	if (added_clause != NULL)
+		*added_clause = added;
+
+	if (dotcnt >= maxparts)
+	{
+		pg_log_error("improper qualified name (too many dotted names): %s",
+					 pattern);
+		termPQExpBuffer(&dbbuf);
+		return false;
+	}
+
+	if (maxparts > 1 && dotcnt == maxparts-1)
+	{
+		if (!dbname_is_literal)
+		{
+			pg_log_error("database name must be literal: %s", pattern);
+			return false;
+		}
+		else if (PQdb(pset.db) == NULL || strcmp(PQdb(pset.db), dbbuf.data) != 0)
+		{
+			pg_log_error("cross-database references are not implemented: %s",
+						 pattern);
+			return false;
+		}
+	}
+	return true;
+}
+
 /*
  * \dRp
  * Lists publications.
@@ -6185,9 +6335,11 @@ listPublications(const char *pattern)
 	appendPQExpBufferStr(&buf,
 						 "\nFROM pg_catalog.pg_publication\n");
 
-	processSQLNamePattern(pset.db, &buf, pattern, false, false,
-						  NULL, "pubname", NULL,
-						  NULL);
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								NULL, "pubname", NULL,
+								NULL,
+								NULL, 1))
+		return true;
 
 	appendPQExpBufferStr(&buf, "ORDER BY 1;");
 
@@ -6252,9 +6404,11 @@ describePublications(const char *pattern)
 	appendPQExpBufferStr(&buf,
 						 "\nFROM pg_catalog.pg_publication\n");
 
-	processSQLNamePattern(pset.db, &buf, pattern, false, false,
-						  NULL, "pubname", NULL,
-						  NULL);
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								NULL, "pubname", NULL,
+								NULL,
+								NULL, 1))
+		return true;
 
 	appendPQExpBufferStr(&buf, "ORDER BY 2;");
 
@@ -6442,9 +6596,11 @@ describeSubscriptions(const char *pattern, bool verbose)
 						 "                 FROM pg_catalog.pg_database\n"
 						 "                 WHERE datname = pg_catalog.current_database())");
 
-	processSQLNamePattern(pset.db, &buf, pattern, true, false,
-						  NULL, "subname", NULL,
-						  NULL);
+	if (!validateSQLNamePattern(&buf, pattern, true, false,
+								NULL, "subname", NULL,
+								NULL,
+								NULL, 1))
+		return true;
 
 	appendPQExpBufferStr(&buf, "ORDER BY 1;");
 
@@ -6550,15 +6706,19 @@ listOperatorClasses(const char *access_method_pattern,
 							 "  LEFT JOIN pg_catalog.pg_namespace ofn ON ofn.oid = of.opfnamespace\n");
 
 	if (access_method_pattern)
-		have_where = processSQLNamePattern(pset.db, &buf, access_method_pattern,
-										   false, false, NULL, "am.amname", NULL, NULL);
+		if (!validateSQLNamePattern(&buf, access_method_pattern,
+									false, false, NULL, "am.amname", NULL, NULL,
+									&have_where, 1))
+			return true;
 	if (type_pattern)
 	{
 		/* Match type name pattern against either internal or external name */
-		processSQLNamePattern(pset.db, &buf, type_pattern, have_where, false,
-							  "tn.nspname", "t.typname",
-							  "pg_catalog.format_type(t.oid, NULL)",
-							  "pg_catalog.pg_type_is_visible(t.oid)");
+		if (!validateSQLNamePattern(&buf, type_pattern, have_where, false,
+									"tn.nspname", "t.typname",
+									"pg_catalog.format_type(t.oid, NULL)",
+									"pg_catalog.pg_type_is_visible(t.oid)",
+									NULL, 3))
+			return true;
 	}
 
 	appendPQExpBufferStr(&buf, "ORDER BY 1, 2, 4;");
@@ -6622,8 +6782,10 @@ listOperatorFamilies(const char *access_method_pattern,
 						 "  LEFT JOIN pg_catalog.pg_namespace n ON n.oid = f.opfnamespace\n");
 
 	if (access_method_pattern)
-		have_where = processSQLNamePattern(pset.db, &buf, access_method_pattern,
-										   false, false, NULL, "am.amname", NULL, NULL);
+		if (!validateSQLNamePattern(&buf, access_method_pattern,
+									false, false, NULL, "am.amname", NULL, NULL,
+									&have_where, 1))
+			return true;
 	if (type_pattern)
 	{
 		appendPQExpBuffer(&buf,
@@ -6635,10 +6797,12 @@ listOperatorFamilies(const char *access_method_pattern,
 						  "    WHERE oc.opcfamily = f.oid\n",
 						  have_where ? "AND" : "WHERE");
 		/* Match type name pattern against either internal or external name */
-		processSQLNamePattern(pset.db, &buf, type_pattern, true, false,
-							  "tn.nspname", "t.typname",
-							  "pg_catalog.format_type(t.oid, NULL)",
-							  "pg_catalog.pg_type_is_visible(t.oid)");
+		if (!validateSQLNamePattern(&buf, type_pattern, true, false,
+									"tn.nspname", "t.typname",
+									"pg_catalog.format_type(t.oid, NULL)",
+									"pg_catalog.pg_type_is_visible(t.oid)",
+									NULL, 3))
+			return true;
 		appendPQExpBufferStr(&buf, "  )\n");
 	}
 
@@ -6716,13 +6880,17 @@ listOpFamilyOperators(const char *access_method_pattern,
 							 "  LEFT JOIN pg_catalog.pg_opfamily ofs ON ofs.oid = o.amopsortfamily\n");
 
 	if (access_method_pattern)
-		have_where = processSQLNamePattern(pset.db, &buf, access_method_pattern,
-										   false, false, NULL, "am.amname",
-										   NULL, NULL);
+		if (!validateSQLNamePattern(&buf, access_method_pattern,
+									false, false, NULL, "am.amname",
+									NULL, NULL,
+									&have_where, 1))
+			return true;
 
 	if (family_pattern)
-		processSQLNamePattern(pset.db, &buf, family_pattern, have_where, false,
-							  "nsf.nspname", "of.opfname", NULL, NULL);
+		if (!validateSQLNamePattern(&buf, family_pattern, have_where, false,
+									"nsf.nspname", "of.opfname", NULL, NULL,
+									NULL, 3))
+			return true;
 
 	appendPQExpBufferStr(&buf, "ORDER BY 1, 2,\n"
 						 "  o.amoplefttype = o.amoprighttype DESC,\n"
@@ -6800,12 +6968,16 @@ listOpFamilyFunctions(const char *access_method_pattern,
 						 "  LEFT JOIN pg_catalog.pg_proc p ON ap.amproc = p.oid\n");
 
 	if (access_method_pattern)
-		have_where = processSQLNamePattern(pset.db, &buf, access_method_pattern,
-										   false, false, NULL, "am.amname",
-										   NULL, NULL);
+		if (!validateSQLNamePattern(&buf, access_method_pattern,
+									false, false, NULL, "am.amname",
+									NULL, NULL,
+									&have_where, 1))
+			return true;
 	if (family_pattern)
-		processSQLNamePattern(pset.db, &buf, family_pattern, have_where, false,
-							  "ns.nspname", "of.opfname", NULL, NULL);
+		if (!validateSQLNamePattern(&buf, family_pattern, have_where, false,
+									"ns.nspname", "of.opfname", NULL, NULL,
+									NULL, 3))
+			return true;
 
 	appendPQExpBufferStr(&buf, "ORDER BY 1, 2,\n"
 						 "  ap.amproclefttype = ap.amprocrighttype DESC,\n"
diff --git a/src/fe_utils/string_utils.c b/src/fe_utils/string_utils.c
index 3efee4e7ee..c3ee191369 100644
--- a/src/fe_utils/string_utils.c
+++ b/src/fe_utils/string_utils.c
@@ -819,6 +819,8 @@ appendReloptionsArray(PQExpBuffer buffer, const char *reloptions,
  * altnamevar: NULL, or name of an alternative variable to match against name.
  * visibilityrule: clause to use if we want to restrict to visible objects
  * (for example, "pg_catalog.pg_table_is_visible(p.oid)").  Can be NULL.
+ * dotcnt: how many separators were parsed from the pattern, by reference.
+ * Can be NULL.
  *
  * Formatting note: the text already present in buf should end with a newline.
  * The appended text, if any, will end with one too.
@@ -827,16 +829,21 @@ bool
 processSQLNamePattern(PGconn *conn, PQExpBuffer buf, const char *pattern,
 					  bool have_where, bool force_escape,
 					  const char *schemavar, const char *namevar,
-					  const char *altnamevar, const char *visibilityrule)
+					  const char *altnamevar, const char *visibilityrule,
+					  PQExpBuffer db, int *dotcnt, bool *dbname_is_literal)
 {
 	PQExpBufferData schemabuf;
 	PQExpBufferData namebuf;
+	PQExpBuffer		schema = NULL;
+	PQExpBuffer		name = NULL;
 	bool		added_clause = false;
 
 #define WHEREAND() \
 	(appendPQExpBufferStr(buf, have_where ? "  AND " : "WHERE "), \
 	 have_where = true, added_clause = true)
 
+	Assert(dotcnt != NULL);
+	*dotcnt = 0;
 	if (pattern == NULL)
 	{
 		/* Default: select all visible objects */
@@ -848,16 +855,24 @@ processSQLNamePattern(PGconn *conn, PQExpBuffer buf, const char *pattern,
 		return added_clause;
 	}
 
-	initPQExpBuffer(&schemabuf);
-	initPQExpBuffer(&namebuf);
+	if (schemavar)
+	{
+		schema = &schemabuf;
+		initPQExpBuffer(schema);
+	}
+	if (namevar || altnamevar)
+	{
+		name = &namebuf;
+		initPQExpBuffer(name);
+	}
 
 	/*
 	 * Convert shell-style 'pattern' into the regular expression(s) we want to
 	 * execute.  Quoting/escaping into SQL literal format will be done below
 	 * using appendStringLiteralConn().
 	 */
-	patternToSQLRegex(PQclientEncoding(conn), NULL, &schemabuf, &namebuf,
-					  pattern, force_escape);
+	patternToSQLRegex(PQclientEncoding(conn), db, schema, name, pattern,
+					  force_escape, true, dotcnt, dbname_is_literal);
 
 	/*
 	 * Now decide what we need to emit.  We may run under a hostile
@@ -870,25 +885,25 @@ processSQLNamePattern(PGconn *conn, PQExpBuffer buf, const char *pattern,
 	 * is >= v12 then we need to force it through explicit COLLATE clauses,
 	 * otherwise the "C" collation attached to "name" catalog columns wins.
 	 */
-	if (namebuf.len > 2)
+	if (name && name->len > 2)
 	{
 		/* We have a name pattern, so constrain the namevar(s) */
 
 		/* Optimize away a "*" pattern */
-		if (strcmp(namebuf.data, "^(.*)$") != 0)
+		if (strcmp(name->data, "^(.*)$") != 0)
 		{
 			WHEREAND();
 			if (altnamevar)
 			{
 				appendPQExpBuffer(buf,
 								  "(%s OPERATOR(pg_catalog.~) ", namevar);
-				appendStringLiteralConn(buf, namebuf.data, conn);
+				appendStringLiteralConn(buf, name->data, conn);
 				if (PQserverVersion(conn) >= 120000)
 					appendPQExpBufferStr(buf, " COLLATE pg_catalog.default");
 				appendPQExpBuffer(buf,
 								  "\n        OR %s OPERATOR(pg_catalog.~) ",
 								  altnamevar);
-				appendStringLiteralConn(buf, namebuf.data, conn);
+				appendStringLiteralConn(buf, name->data, conn);
 				if (PQserverVersion(conn) >= 120000)
 					appendPQExpBufferStr(buf, " COLLATE pg_catalog.default");
 				appendPQExpBufferStr(buf, ")\n");
@@ -896,7 +911,7 @@ processSQLNamePattern(PGconn *conn, PQExpBuffer buf, const char *pattern,
 			else
 			{
 				appendPQExpBuffer(buf, "%s OPERATOR(pg_catalog.~) ", namevar);
-				appendStringLiteralConn(buf, namebuf.data, conn);
+				appendStringLiteralConn(buf, name->data, conn);
 				if (PQserverVersion(conn) >= 120000)
 					appendPQExpBufferStr(buf, " COLLATE pg_catalog.default");
 				appendPQExpBufferChar(buf, '\n');
@@ -904,16 +919,16 @@ processSQLNamePattern(PGconn *conn, PQExpBuffer buf, const char *pattern,
 		}
 	}
 
-	if (schemabuf.len > 2)
+	if (schema && schema->len > 2)
 	{
 		/* We have a schema pattern, so constrain the schemavar */
 
 		/* Optimize away a "*" pattern */
-		if (strcmp(schemabuf.data, "^(.*)$") != 0 && schemavar)
+		if (strcmp(schema->data, "^(.*)$") != 0 && schemavar)
 		{
 			WHEREAND();
 			appendPQExpBuffer(buf, "%s OPERATOR(pg_catalog.~) ", schemavar);
-			appendStringLiteralConn(buf, schemabuf.data, conn);
+			appendStringLiteralConn(buf, schema->data, conn);
 			if (PQserverVersion(conn) >= 120000)
 				appendPQExpBufferStr(buf, " COLLATE pg_catalog.default");
 			appendPQExpBufferChar(buf, '\n');
@@ -929,8 +944,10 @@ processSQLNamePattern(PGconn *conn, PQExpBuffer buf, const char *pattern,
 		}
 	}
 
-	termPQExpBuffer(&schemabuf);
-	termPQExpBuffer(&namebuf);
+	if (schema)
+		termPQExpBuffer(schema);
+	if (name)
+		termPQExpBuffer(name);
 
 	return added_clause;
 #undef WHEREAND
@@ -965,32 +982,40 @@ processSQLNamePattern(PGconn *conn, PQExpBuffer buf, const char *pattern,
  */
 void
 patternToSQLRegex(int encoding, PQExpBuffer dbnamebuf, PQExpBuffer schemabuf,
-				  PQExpBuffer namebuf, const char *pattern, bool force_escape)
+				  PQExpBuffer namebuf, const char *pattern, bool force_escape,
+				  bool want_literal_dbname, int *dotcnt,
+				  bool *dbname_is_literal)
 {
 	PQExpBufferData buf[3];
+	PQExpBufferData left_literal;
 	PQExpBuffer curbuf;
 	PQExpBuffer maxbuf;
 	int			i;
 	bool		inquotes;
+	bool		left,
+				left_is_literal;
 	const char *cp;
 
 	Assert(pattern != NULL);
-	Assert(namebuf != NULL);
-
-	/* callers should never expect "dbname.relname" format */
-	Assert(dbnamebuf == NULL || schemabuf != NULL);
+	Assert(dotcnt != NULL);
 
+	*dotcnt = 0;
 	inquotes = false;
 	cp = pattern;
 
+	maxbuf = &buf[0];
 	if (dbnamebuf != NULL)
-		maxbuf = &buf[2];
-	else if (schemabuf != NULL)
-		maxbuf = &buf[1];
-	else
-		maxbuf = &buf[0];
+		maxbuf++;
+	if (schemabuf != NULL)
+		maxbuf++;
+	if (namebuf != NULL)
+		maxbuf++;
 
 	curbuf = &buf[0];
+	left = true;
+	if (want_literal_dbname)
+		initPQExpBuffer(&left_literal);
+	left_is_literal = true;
 	initPQExpBuffer(curbuf);
 	appendPQExpBufferStr(curbuf, "^(");
 	while (*cp)
@@ -1003,6 +1028,8 @@ patternToSQLRegex(int encoding, PQExpBuffer dbnamebuf, PQExpBuffer schemabuf,
 			{
 				/* emit one quote, stay in inquotes mode */
 				appendPQExpBufferChar(curbuf, '"');
+				if (left && want_literal_dbname)
+					appendPQExpBufferChar(&left_literal, '"');
 				cp++;
 			}
 			else
@@ -1013,32 +1040,48 @@ patternToSQLRegex(int encoding, PQExpBuffer dbnamebuf, PQExpBuffer schemabuf,
 		{
 			appendPQExpBufferChar(curbuf,
 								  pg_tolower((unsigned char) ch));
+			if (left && want_literal_dbname)
+				appendPQExpBufferChar(&left_literal,
+									  pg_tolower((unsigned char) ch));
 			cp++;
 		}
 		else if (!inquotes && ch == '*')
 		{
 			appendPQExpBufferStr(curbuf, ".*");
+			if (left)
+			{
+				if (want_literal_dbname)
+					appendPQExpBufferChar(&left_literal, '*');
+				left_is_literal = false;
+			}
 			cp++;
 		}
 		else if (!inquotes && ch == '?')
 		{
 			appendPQExpBufferChar(curbuf, '.');
+			if (left)
+			{
+				if (want_literal_dbname)
+					appendPQExpBufferChar(&left_literal, '?');
+				left_is_literal = false;
+			}
 			cp++;
 		}
-
-		/*
-		 * When we find a dbname/schema/name separator, we treat it specially
-		 * only if the caller requested more patterns to be parsed than we
-		 * have already parsed from the pattern.  Otherwise, dot characters
-		 * are not special.
-		 */
-		else if (!inquotes && ch == '.' && curbuf < maxbuf)
+		else if (!inquotes && ch == '.')
 		{
-			appendPQExpBufferStr(curbuf, ")$");
-			curbuf++;
-			initPQExpBuffer(curbuf);
-			appendPQExpBufferStr(curbuf, "^(");
-			cp++;
+			left = false;
+			if (dotcnt)
+				(*dotcnt)++;
+			if (curbuf < maxbuf-1)
+			{
+				appendPQExpBufferStr(curbuf, ")$");
+				curbuf++;
+				initPQExpBuffer(curbuf);
+				appendPQExpBufferStr(curbuf, "^(");
+				cp++;
+			}
+			else
+				appendPQExpBufferChar(curbuf, *cp++);
 		}
 		else if (ch == '$')
 		{
@@ -1050,6 +1093,8 @@ patternToSQLRegex(int encoding, PQExpBuffer dbnamebuf, PQExpBuffer schemabuf,
 			 * having it possess its regexp meaning.
 			 */
 			appendPQExpBufferStr(curbuf, "\\$");
+			if (left && want_literal_dbname)
+				appendPQExpBufferChar(&left_literal, '$');
 			cp++;
 		}
 		else
@@ -1074,25 +1119,44 @@ patternToSQLRegex(int encoding, PQExpBuffer dbnamebuf, PQExpBuffer schemabuf,
 				appendPQExpBufferChar(curbuf, '\\');
 			i = PQmblenBounded(cp, encoding);
 			while (i--)
+			{
+				if (left)
+				{
+					if (want_literal_dbname)
+						appendPQExpBufferChar(&left_literal, *cp);
+					if (!inquotes && strchr("|+()[]{}.^\\", *cp))
+						left_is_literal = false;
+				}
 				appendPQExpBufferChar(curbuf, *cp++);
+			}
 		}
 	}
 	appendPQExpBufferStr(curbuf, ")$");
 
-	appendPQExpBufferStr(namebuf, curbuf->data);
-	termPQExpBuffer(curbuf);
-
-	if (curbuf > buf)
+	if (namebuf)
 	{
+		appendPQExpBufferStr(namebuf, curbuf->data);
+		termPQExpBuffer(curbuf);
 		curbuf--;
+	}
+
+	if (schemabuf && curbuf >= buf)
+	{
 		appendPQExpBufferStr(schemabuf, curbuf->data);
 		termPQExpBuffer(curbuf);
+		curbuf--;
+	}
 
-		if (curbuf > buf)
-		{
-			curbuf--;
+	if (dbnamebuf && curbuf >= buf)
+	{
+		if (want_literal_dbname)
+			appendPQExpBufferStr(dbnamebuf, left_literal.data);
+		else
 			appendPQExpBufferStr(dbnamebuf, curbuf->data);
-			termPQExpBuffer(curbuf);
-		}
+		termPQExpBuffer(curbuf);
+		if (dbname_is_literal)
+			*dbname_is_literal = left_is_literal;
 	}
+	else if (dbname_is_literal)
+		*dbname_is_literal = true;		/* treat empty dbname as literal */
 }
diff --git a/src/include/fe_utils/string_utils.h b/src/include/fe_utils/string_utils.h
index caafb97d29..d271cbc4bf 100644
--- a/src/include/fe_utils/string_utils.h
+++ b/src/include/fe_utils/string_utils.h
@@ -54,10 +54,14 @@ extern bool processSQLNamePattern(PGconn *conn, PQExpBuffer buf,
 								  const char *pattern,
 								  bool have_where, bool force_escape,
 								  const char *schemavar, const char *namevar,
-								  const char *altnamevar, const char *visibilityrule);
+								  const char *altnamevar, const char *visibilityrule,
+								  PQExpBuffer db, int *dotcnt,
+								  bool *dbname_is_literal);
 
 extern void patternToSQLRegex(int encoding, PQExpBuffer dbnamebuf,
 							  PQExpBuffer schemabuf, PQExpBuffer namebuf,
-							  const char *pattern, bool force_escape);
+							  const char *pattern, bool force_escape,
+							  bool want_literal_dbname, int *dotcnt,
+							  bool *dbname_is_literal);
 
 #endif							/* STRING_UTILS_H */
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 930ce8597a..d770f980b3 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5278,3 +5278,224 @@ ERROR:  relation "notexists" does not exist
 LINE 1: SELECT * FROM notexists;
                       ^
 STATEMENT:  SELECT * FROM notexists;
+-- check describing invalid multipart names
+\dA regression.heap
+improper qualified name (too many dotted names): regression.heap
+\dA nonesuch.heap
+improper qualified name (too many dotted names): nonesuch.heap
+\dt host.regression.pg_catalog.pg_class
+improper qualified name (too many dotted names): host.regression.pg_catalog.pg_class
+\dt |.pg_catalog.pg_class
+database name must be literal: |.pg_catalog.pg_class
+\dt nonesuch.pg_catalog.pg_class
+cross-database references are not implemented: nonesuch.pg_catalog.pg_class
+\da host.regression.pg_catalog.sum
+improper qualified name (too many dotted names): host.regression.pg_catalog.sum
+\da +.pg_catalog.sum
+database name must be literal: +.pg_catalog.sum
+\da nonesuch.pg_catalog.sum
+cross-database references are not implemented: nonesuch.pg_catalog.sum
+\dAc nonesuch.brin
+improper qualified name (too many dotted names): nonesuch.brin
+\dAc regression.brin
+improper qualified name (too many dotted names): regression.brin
+\dAf nonesuch.brin
+improper qualified name (too many dotted names): nonesuch.brin
+\dAf regression.brin
+improper qualified name (too many dotted names): regression.brin
+\dAo nonesuch.brin
+improper qualified name (too many dotted names): nonesuch.brin
+\dAo regression.brin
+improper qualified name (too many dotted names): regression.brin
+\dAp nonesuch.brin
+improper qualified name (too many dotted names): nonesuch.brin
+\dAp regression.brin
+improper qualified name (too many dotted names): regression.brin
+\db nonesuch.pg_default
+improper qualified name (too many dotted names): nonesuch.pg_default
+\db regression.pg_default
+improper qualified name (too many dotted names): regression.pg_default
+\dc host.regression.public.conversion
+improper qualified name (too many dotted names): host.regression.public.conversion
+\dc (.public.conversion
+database name must be literal: (.public.conversion
+\dc nonesuch.public.conversion
+cross-database references are not implemented: nonesuch.public.conversion
+\dC host.regression.pg_catalog.int8
+improper qualified name (too many dotted names): host.regression.pg_catalog.int8
+\dC ).pg_catalog.int8
+database name must be literal: ).pg_catalog.int8
+\dC nonesuch.pg_catalog.int8
+cross-database references are not implemented: nonesuch.pg_catalog.int8
+\dd host.regression.pg_catalog.pg_class
+improper qualified name (too many dotted names): host.regression.pg_catalog.pg_class
+\dd [.pg_catalog.pg_class
+database name must be literal: [.pg_catalog.pg_class
+\dd nonesuch.pg_catalog.pg_class
+cross-database references are not implemented: nonesuch.pg_catalog.pg_class
+\dD host.regression.public.gtestdomain1
+improper qualified name (too many dotted names): host.regression.public.gtestdomain1
+\dD ].public.gtestdomain1
+database name must be literal: ].public.gtestdomain1
+\dD nonesuch.public.gtestdomain1
+cross-database references are not implemented: nonesuch.public.gtestdomain1
+\ddp host.regression.pg_catalog.pg_class
+improper qualified name (too many dotted names): host.regression.pg_catalog.pg_class
+\ddp {.pg_catalog.pg_class
+database name must be literal: {.pg_catalog.pg_class
+\ddp nonesuch.pg_catalog.pg_class
+cross-database references are not implemented: nonesuch.pg_catalog.pg_class
+\dE host.regression.public.ft
+improper qualified name (too many dotted names): host.regression.public.ft
+\dE }.public.ft
+database name must be literal: }.public.ft
+\dE nonesuch.public.ft
+cross-database references are not implemented: nonesuch.public.ft
+\di host.regression.public.tenk1_hundred
+improper qualified name (too many dotted names): host.regression.public.tenk1_hundred
+\di ..public.tenk1_hundred
+improper qualified name (too many dotted names): ..public.tenk1_hundred
+\di nonesuch.public.tenk1_hundred
+cross-database references are not implemented: nonesuch.public.tenk1_hundred
+\dm host.regression.public.mvtest_bb
+improper qualified name (too many dotted names): host.regression.public.mvtest_bb
+\dm ^.public.mvtest_bb
+database name must be literal: ^.public.mvtest_bb
+\dm nonesuch.public.mvtest_bb
+cross-database references are not implemented: nonesuch.public.mvtest_bb
+\ds host.regression.public.check_seq
+improper qualified name (too many dotted names): host.regression.public.check_seq
+\ds regression|mydb.public.check_seq
+database name must be literal: regression|mydb.public.check_seq
+\ds nonesuch.public.check_seq
+cross-database references are not implemented: nonesuch.public.check_seq
+\dt host.regression.public.b_star
+improper qualified name (too many dotted names): host.regression.public.b_star
+\dt regres+ion.public.b_star
+database name must be literal: regres+ion.public.b_star
+\dt nonesuch.public.b_star
+cross-database references are not implemented: nonesuch.public.b_star
+\dv host.regression.public.shoe
+improper qualified name (too many dotted names): host.regression.public.shoe
+\dv regress(ion).public.shoe
+database name must be literal: regress(ion).public.shoe
+\dv nonesuch.public.shoe
+cross-database references are not implemented: nonesuch.public.shoe
+\des nonesuch.server
+improper qualified name (too many dotted names): nonesuch.server
+\des regression.server
+improper qualified name (too many dotted names): regression.server
+\des nonesuch.server
+improper qualified name (too many dotted names): nonesuch.server
+\des regression.server
+improper qualified name (too many dotted names): regression.server
+\des nonesuch.username
+improper qualified name (too many dotted names): nonesuch.username
+\des regression.username
+improper qualified name (too many dotted names): regression.username
+\dew nonesuch.fdw
+improper qualified name (too many dotted names): nonesuch.fdw
+\dew regression.fdw
+improper qualified name (too many dotted names): regression.fdw
+\df host.regression.public.namelen
+improper qualified name (too many dotted names): host.regression.public.namelen
+\df regres[qrstuv]ion.public.namelen
+database name must be literal: regres[qrstuv]ion.public.namelen
+\df nonesuch.public.namelen
+cross-database references are not implemented: nonesuch.public.namelen
+\dF host.regression.pg_catalog.arabic
+improper qualified name (too many dotted names): host.regression.pg_catalog.arabic
+\dF regres{1,2}ion.pg_catalog.arabic
+database name must be literal: regres{1,2}ion.pg_catalog.arabic
+\dF nonesuch.pg_catalog.arabic
+cross-database references are not implemented: nonesuch.pg_catalog.arabic
+\dFd host.regression.pg_catalog.arabic_stem
+improper qualified name (too many dotted names): host.regression.pg_catalog.arabic_stem
+\dFd regres?ion.pg_catalog.arabic_stem
+database name must be literal: regres?ion.pg_catalog.arabic_stem
+\dFd nonesuch.pg_catalog.arabic_stem
+cross-database references are not implemented: nonesuch.pg_catalog.arabic_stem
+\dFp host.regression.pg_catalog.default
+improper qualified name (too many dotted names): host.regression.pg_catalog.default
+\dFp ^regression.pg_catalog.default
+database name must be literal: ^regression.pg_catalog.default
+\dFp nonesuch.pg_catalog.default
+cross-database references are not implemented: nonesuch.pg_catalog.default
+\dFt host.regression.pg_catalog.ispell
+improper qualified name (too many dotted names): host.regression.pg_catalog.ispell
+\dFt regression$.pg_catalog.ispell
+cross-database references are not implemented: regression$.pg_catalog.ispell
+\dFt nonesuch.pg_catalog.ispell
+cross-database references are not implemented: nonesuch.pg_catalog.ispell
+\dg nonesuch.pg_database_owner
+improper qualified name (too many dotted names): nonesuch.pg_database_owner
+\dg regression.pg_database_owner
+improper qualified name (too many dotted names): regression.pg_database_owner
+\dL host.regression.plpgsql
+improper qualified name (too many dotted names): host.regression.plpgsql
+\dL *.plpgsql
+database name must be literal: *.plpgsql
+\dL nonesuch.plpgsql
+cross-database references are not implemented: nonesuch.plpgsql
+\dn host.regression.public
+improper qualified name (too many dotted names): host.regression.public
+\dn """".public
+cross-database references are not implemented: """".public
+\dn nonesuch.public
+cross-database references are not implemented: nonesuch.public
+\do host.regression.public.!=-
+improper qualified name (too many dotted names): host.regression.public.!=-
+\do "regression|mydb".public.!=-
+cross-database references are not implemented: "regression|mydb".public.!=-
+\do nonesuch.public.!=-
+cross-database references are not implemented: nonesuch.public.!=-
+\dO host.regression.pg_catalog.POSIX
+improper qualified name (too many dotted names): host.regression.pg_catalog.POSIX
+\dO .pg_catalog.POSIX
+cross-database references are not implemented: .pg_catalog.POSIX
+\dO nonesuch.pg_catalog.POSIX
+cross-database references are not implemented: nonesuch.pg_catalog.POSIX
+\dp host.regression.public.a_star
+improper qualified name (too many dotted names): host.regression.public.a_star
+\dp "regres+ion".public.a_star
+cross-database references are not implemented: "regres+ion".public.a_star
+\dp nonesuch.public.a_star
+cross-database references are not implemented: nonesuch.public.a_star
+\dP host.regression.public.mlparted
+improper qualified name (too many dotted names): host.regression.public.mlparted
+\dP "regres(sion)".public.mlparted
+cross-database references are not implemented: "regres(sion)".public.mlparted
+\dP nonesuch.public.mlparted
+cross-database references are not implemented: nonesuch.public.mlparted
+\drds nonesuch.lc_messages
+improper qualified name (too many dotted names): nonesuch.lc_messages
+\drds regression.lc_messages
+improper qualified name (too many dotted names): regression.lc_messages
+\dRp public.mypub
+improper qualified name (too many dotted names): public.mypub
+\dRp regression.mypub
+improper qualified name (too many dotted names): regression.mypub
+\dRs public.mysub
+improper qualified name (too many dotted names): public.mysub
+\dRs regression.mysub
+improper qualified name (too many dotted names): regression.mysub
+\dT host.regression.public.widget
+improper qualified name (too many dotted names): host.regression.public.widget
+\dT "regression{1,2}".public.widget
+cross-database references are not implemented: "regression{1,2}".public.widget
+\dT nonesuch.public.widget
+cross-database references are not implemented: nonesuch.public.widget
+\dx regression.plpgsql
+improper qualified name (too many dotted names): regression.plpgsql
+\dx nonesuch.plpgsql
+improper qualified name (too many dotted names): nonesuch.plpgsql
+\dX host.regression.public.func_deps_stat
+improper qualified name (too many dotted names): host.regression.public.func_deps_stat
+\dX "^regression$".public.func_deps_stat
+cross-database references are not implemented: "^regression$".public.func_deps_stat
+\dX nonesuch.public.func_deps_stat
+cross-database references are not implemented: nonesuch.public.func_deps_stat
+\dy regression.myevt
+improper qualified name (too many dotted names): regression.myevt
+\dy nonesuch.myevt
+improper qualified name (too many dotted names): nonesuch.myevt
diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql
index e9d504baf2..042237f3ee 100644
--- a/src/test/regress/sql/psql.sql
+++ b/src/test/regress/sql/psql.sql
@@ -1304,3 +1304,116 @@ DROP TABLE oer_test;
 \set ECHO errors
 SELECT * FROM notexists;
 \set ECHO none
+\set ECHO all
+
+-- check describing invalid multipart names
+\dA regression.heap
+\dA nonesuch.heap
+\dt host.regression.pg_catalog.pg_class
+\dt |.pg_catalog.pg_class
+\dt nonesuch.pg_catalog.pg_class
+\da host.regression.pg_catalog.sum
+\da +.pg_catalog.sum
+\da nonesuch.pg_catalog.sum
+\dAc nonesuch.brin
+\dAc regression.brin
+\dAf nonesuch.brin
+\dAf regression.brin
+\dAo nonesuch.brin
+\dAo regression.brin
+\dAp nonesuch.brin
+\dAp regression.brin
+\db nonesuch.pg_default
+\db regression.pg_default
+\dc host.regression.public.conversion
+\dc (.public.conversion
+\dc nonesuch.public.conversion
+\dC host.regression.pg_catalog.int8
+\dC ).pg_catalog.int8
+\dC nonesuch.pg_catalog.int8
+\dd host.regression.pg_catalog.pg_class
+\dd [.pg_catalog.pg_class
+\dd nonesuch.pg_catalog.pg_class
+\dD host.regression.public.gtestdomain1
+\dD ].public.gtestdomain1
+\dD nonesuch.public.gtestdomain1
+\ddp host.regression.pg_catalog.pg_class
+\ddp {.pg_catalog.pg_class
+\ddp nonesuch.pg_catalog.pg_class
+\dE host.regression.public.ft
+\dE }.public.ft
+\dE nonesuch.public.ft
+\di host.regression.public.tenk1_hundred
+\di ..public.tenk1_hundred
+\di nonesuch.public.tenk1_hundred
+\dm host.regression.public.mvtest_bb
+\dm ^.public.mvtest_bb
+\dm nonesuch.public.mvtest_bb
+\ds host.regression.public.check_seq
+\ds regression|mydb.public.check_seq
+\ds nonesuch.public.check_seq
+\dt host.regression.public.b_star
+\dt regres+ion.public.b_star
+\dt nonesuch.public.b_star
+\dv host.regression.public.shoe
+\dv regress(ion).public.shoe
+\dv nonesuch.public.shoe
+\des nonesuch.server
+\des regression.server
+\des nonesuch.server
+\des regression.server
+\des nonesuch.username
+\des regression.username
+\dew nonesuch.fdw
+\dew regression.fdw
+\df host.regression.public.namelen
+\df regres[qrstuv]ion.public.namelen
+\df nonesuch.public.namelen
+\dF host.regression.pg_catalog.arabic
+\dF regres{1,2}ion.pg_catalog.arabic
+\dF nonesuch.pg_catalog.arabic
+\dFd host.regression.pg_catalog.arabic_stem
+\dFd regres?ion.pg_catalog.arabic_stem
+\dFd nonesuch.pg_catalog.arabic_stem
+\dFp host.regression.pg_catalog.default
+\dFp ^regression.pg_catalog.default
+\dFp nonesuch.pg_catalog.default
+\dFt host.regression.pg_catalog.ispell
+\dFt regression$.pg_catalog.ispell
+\dFt nonesuch.pg_catalog.ispell
+\dg nonesuch.pg_database_owner
+\dg regression.pg_database_owner
+\dL host.regression.plpgsql
+\dL *.plpgsql
+\dL nonesuch.plpgsql
+\dn host.regression.public
+\dn """".public
+\dn nonesuch.public
+\do host.regression.public.!=-
+\do "regression|mydb".public.!=-
+\do nonesuch.public.!=-
+\dO host.regression.pg_catalog.POSIX
+\dO .pg_catalog.POSIX
+\dO nonesuch.pg_catalog.POSIX
+\dp host.regression.public.a_star
+\dp "regres+ion".public.a_star
+\dp nonesuch.public.a_star
+\dP host.regression.public.mlparted
+\dP "regres(sion)".public.mlparted
+\dP nonesuch.public.mlparted
+\drds nonesuch.lc_messages
+\drds regression.lc_messages
+\dRp public.mypub
+\dRp regression.mypub
+\dRs public.mysub
+\dRs regression.mysub
+\dT host.regression.public.widget
+\dT "regression{1,2}".public.widget
+\dT nonesuch.public.widget
+\dx regression.plpgsql
+\dx nonesuch.plpgsql
+\dX host.regression.public.func_deps_stat
+\dX "^regression$".public.func_deps_stat
+\dX nonesuch.public.func_deps_stat
+\dy regression.myevt
+\dy nonesuch.myevt
-- 
2.21.1 (Apple Git-122.3)

