From 6752c30dfe834e10c1d0cdb99740235ffa64b1e7 Mon Sep 17 00:00:00 2001
From: Peter Smith <peter.b.smith@fujitsu.com>
Date: Thu, 30 Jul 2026 11:54:23 +1000
Subject: [PATCH v3] Fix escapes for psql describe schema patterns

psql \dn was building the query with a raw '%s' substitution of the user-supplied pattern, so a pattern containing a single quote (\dn "it's a bug") broke out of the string literal and produced a syntax error on the server.

Fixed by using a pattern escape function instead of raw-substitution.

Author: Peter Smith <smithpb2250@gmail.com>
Reviewed-by: Steven Niu <niushiji@gmail.com>
Reviewed by: Jim Jones <jim.jones@uni-muenster.de>
Reviewed-by: Nishant Sharma <nishant.sharma@enterprisedb.com>

Discussion: https://www.postgresql.org/message-id/flat/SA5PPF230C70D4BFBD6006437E357AE7CF9A7CB2%40SA5PPF230C70D4B.namprd14.prod.outlook.com#c4b1b145852d2076db6590e0aae5c7da
---
 src/bin/psql/describe.c                   | 12 ++--
 src/test/regress/expected/publication.out | 73 +++++++++++++++++++++++
 src/test/regress/sql/publication.sql      | 16 +++++
 3 files changed, 97 insertions(+), 4 deletions(-)

diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..635c77f993d 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -5327,10 +5327,14 @@ listSchemas(const char *pattern, bool verbose, bool showSystem)
 						  "SELECT pubname \n"
 						  "FROM pg_catalog.pg_publication p\n"
 						  "     JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n"
-						  "     JOIN pg_catalog.pg_namespace n ON n.oid = pn.pnnspid \n"
-						  "WHERE n.nspname = '%s'\n"
-						  "ORDER BY 1",
-						  pattern);
+						  "     JOIN pg_catalog.pg_namespace n ON n.oid = pn.pnnspid \n");
+
+		processSQLNamePattern(pset.db, &buf, pattern, false, false,
+							  NULL, "n.nspname", NULL,
+							  NULL, NULL, NULL);
+
+		appendPQExpBufferStr(&buf, "ORDER BY 1;");
+
 		result = PSQLexec(buf.data);
 		if (!result)
 			goto error_return;
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 9c6386d518a..9b2f8d04c88 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -171,6 +171,79 @@ CREATE PUBLICATION testpub_parsertst FOR TABLES IN SCHEMA foo, test.foo;
 ERROR:  invalid schema name
 LINE 1: ...CATION testpub_parsertst FOR TABLES IN SCHEMA foo, test.foo;
                                                               ^
+-- should be able to publish and describe schemas and tables with embedded quotes
+CREATE SCHEMA "it's my schema";
+CREATE TABLE "it's my schema"."it's my schema table"("it's my col1" int, "it's my col2" int);
+CREATE TABLE "it's my public table"("it's my col1" int, "it's my col2" int);
+CREATE PUBLICATION regress_pub_embedded_quotes1 FOR TABLES IN SCHEMA "it's my schema";
+CREATE PUBLICATION regress_pub_embedded_quotes2 FOR TABLE "it's my schema"."it's my schema table";
+CREATE PUBLICATION regress_pub_embedded_quotes3 FOR TABLE "it's my public table"("it's my col1");
+CREATE PUBLICATION regress_pub_embedded_quotes4 FOR ALL TABLES EXCEPT (TABLE "it's my public table");
+\dn "it's my schema"
+              List of schemas
+      Name      |          Owner           
+----------------+--------------------------
+ it's my schema | regress_publication_user
+Included in publications:
+    "regress_pub_embedded_quotes1"
+
+\d "it's my schema"."it's my schema table"
+       Table "it's my schema.it's my schema table"
+    Column    |  Type   | Collation | Nullable | Default 
+--------------+---------+-----------+----------+---------
+ it's my col1 | integer |           |          | 
+ it's my col2 | integer |           |          | 
+Included in publications:
+    "regress_pub_embedded_quotes1"
+    "regress_pub_embedded_quotes2"
+    "regress_pub_embedded_quotes4"
+    "testpub_foralltables"
+
+\d "it's my public table";
+           Table "public.it's my public table"
+    Column    |  Type   | Collation | Nullable | Default 
+--------------+---------+-----------+----------+---------
+ it's my col1 | integer |           |          | 
+ it's my col2 | integer |           |          | 
+Included in publications:
+    "regress_pub_embedded_quotes3" (it's my col1)
+    "testpub_foralltables"
+Excluded from publications:
+    "regress_pub_embedded_quotes4"
+
+\dRp+ regress_pub_embedded_quotes*
+                                                   Publication regress_pub_embedded_quotes1
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+-------------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f        | 
+Tables from schemas:
+    "it's my schema"
+
+                                                   Publication regress_pub_embedded_quotes2
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+-------------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f        | 
+Tables:
+    "it's my schema.it's my schema table"
+
+                                                   Publication regress_pub_embedded_quotes3
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+-------------
+ regress_publication_user | f          | f             | t       | t       | t       | t         | none              | f        | 
+Tables:
+    "public.it's my public table" (it's my col1)
+
+                                                   Publication regress_pub_embedded_quotes4
+          Owner           | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description 
+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+-------------
+ regress_publication_user | t          | f             | t       | t       | t       | t         | none              | f        | 
+Except tables:
+    "public.it's my public table"
+
+DROP PUBLICATION regress_pub_embedded_quotes1, regress_pub_embedded_quotes2, regress_pub_embedded_quotes3, regress_pub_embedded_quotes4;
+DROP SCHEMA "it's my schema" CASCADE;
+NOTICE:  drop cascades to table "it's my schema"."it's my schema table"
+DROP TABLE "it's my public table";
 -- should be able to add a table of the same schema to the schema publication
 ALTER PUBLICATION testpub_forschema ADD TABLE pub_test.testpub_nopk;
 \dRp+ testpub_forschema
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index fac54b02e27..0834b11ebad 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -90,6 +90,22 @@ RESET client_min_messages;
 CREATE PUBLICATION testpub_parsertst FOR TABLE pub_test.testpub_nopk, CURRENT_SCHEMA;
 CREATE PUBLICATION testpub_parsertst FOR TABLES IN SCHEMA foo, test.foo;
 
+-- should be able to publish and describe schemas and tables with embedded quotes
+CREATE SCHEMA "it's my schema";
+CREATE TABLE "it's my schema"."it's my schema table"("it's my col1" int, "it's my col2" int);
+CREATE TABLE "it's my public table"("it's my col1" int, "it's my col2" int);
+CREATE PUBLICATION regress_pub_embedded_quotes1 FOR TABLES IN SCHEMA "it's my schema";
+CREATE PUBLICATION regress_pub_embedded_quotes2 FOR TABLE "it's my schema"."it's my schema table";
+CREATE PUBLICATION regress_pub_embedded_quotes3 FOR TABLE "it's my public table"("it's my col1");
+CREATE PUBLICATION regress_pub_embedded_quotes4 FOR ALL TABLES EXCEPT (TABLE "it's my public table");
+\dn "it's my schema"
+\d "it's my schema"."it's my schema table"
+\d "it's my public table";
+\dRp+ regress_pub_embedded_quotes*
+DROP PUBLICATION regress_pub_embedded_quotes1, regress_pub_embedded_quotes2, regress_pub_embedded_quotes3, regress_pub_embedded_quotes4;
+DROP SCHEMA "it's my schema" CASCADE;
+DROP TABLE "it's my public table";
+
 -- should be able to add a table of the same schema to the schema publication
 ALTER PUBLICATION testpub_forschema ADD TABLE pub_test.testpub_nopk;
 \dRp+ testpub_forschema
-- 
2.47.3

