From c29e9e6322368ece386b4e61ad8609fccf0255f7 Mon Sep 17 00:00:00 2001
From: Stepan Neretin <slpmcf@gmail.com>
Date: Mon, 19 May 2025 14:21:39 +0700
Subject: [PATCH v2] Fix quoting of complex schema and table names in
 fetchAttributeStats()
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The function fetchAttributeStats() incorrectly assumed that array string
literals for EXECUTE getAttributeStats(...) could be safely constructed
using fmtId(), which doesn't escape single quotes. This led to a syntax
error when dumping tables with names containing single quotes, double quotes,
or spaces — as reported in bug #18923.

This patch replaces direct fmtId() appends with appendPGArray(), which correctly
formats array elements, and then quotes the full array string using
appendStringLiteralAH().

Also adds a regression test with a schema and table using complex identifiers,
including embedded single and double quotes, to ensure this scenario is covered.

Patch based on suggestions from Tom Lane, Jian He, and Nathan Bossart.

Author: Nathan Bossart, Stepan Neretin
---
 src/bin/pg_dump/pg_dump.c        | 21 +++++++++++++--------
 src/bin/pg_dump/t/002_pg_dump.pl | 23 +++++++++++++++++++++++
 2 files changed, 36 insertions(+), 8 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index e2e7975b34e..06a7684341f 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -10771,28 +10771,33 @@ fetchAttributeStats(Archive *fout)
 	 * This is perhaps not the sturdiest assumption, so we verify it matches
 	 * reality in dumpRelationStats_dumper().
 	 */
+
+	appendPQExpBufferChar(nspnames, '{');
+	appendPQExpBufferChar(relnames, '{');
 	for (; te != AH->toc && count < max_rels; te = te->next)
 	{
 		if ((te->reqs & REQ_STATS) != 0 &&
 			strcmp(te->desc, "STATISTICS DATA") == 0)
 		{
-			appendPQExpBuffer(nspnames, "%s%s", count ? "," : "",
-							  fmtId(te->namespace));
-			appendPQExpBuffer(relnames, "%s%s", count ? "," : "",
-							  fmtId(te->tag));
+			appendPGArray(nspnames, te->namespace);
+			appendPGArray(relnames, te->tag);
 			count++;
 		}
 	}
+	appendPQExpBufferChar(nspnames, '}');
+	appendPQExpBufferChar(relnames, '}');
 
 	/* Execute the query for the next batch of relations. */
 	if (count > 0)
 	{
 		PQExpBuffer query = createPQExpBuffer();
 
-		appendPQExpBuffer(query, "EXECUTE getAttributeStats("
-						  "'{%s}'::pg_catalog.name[],"
-						  "'{%s}'::pg_catalog.name[])",
-						  nspnames->data, relnames->data);
+		appendPQExpBufferStr(query, "EXECUTE getAttributeStats(");
+		appendStringLiteralAH(query, nspnames->data, fout);
+		appendPQExpBufferStr(query, "::pg_catalog.name[],");
+		appendStringLiteralAH(query, relnames->data, fout);
+		appendPQExpBufferStr(query, "::pg_catalog.name[])");
+
 		res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
 		destroyPQExpBuffer(query);
 	}
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 55d892d9c16..29a6d1952bc 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -4825,6 +4825,29 @@ my %tests = (
 		},
 	},
 
+
+	'CREATE SCHEMA "phil\'s schema\"3"' => {
+		create_sql => q{
+			CREATE SCHEMA "phil's schema""3";
+			SET search_path = "phil's schema""3";
+			DROP TABLE IF EXISTS "phil's tbl1";
+			CREATE TABLE "phil's tbl1" (
+				"phil's col11" serial PRIMARY KEY,
+				"phil's col12" text
+			);
+		},
+		regexp => qr/^
+		CREATE\ SCHEMA\ "phil's\ schema""3";\n
+		SET\ search_path\ =\ "phil's\ schema""3";\n
+		DROP\ TABLE\ IF\ EXISTS\ "phil's\ tbl1";\n
+		CREATE\ TABLE\ "phil's\ tbl1"\ \(\n
+		\s+"phil's\ col11"\ integer\ NOT\ NULL\ DEFAULT\ nextval\('[^']+'\)::regclass,\n
+		\s+"phil's\ col12"\ text\n
+		\s+CONSTRAINT\ "[^"]+"\ PRIMARY\ KEY\ \("phil's\ col11"\)\n
+		\);/xm,
+		like => {},
+	},
+
 	#
 	# TABLE and MATVIEW stats will end up in SECTION_DATA.
 	# INDEX stats (expression columns only) will end up in SECTION_POST_DATA.
-- 
2.43.0

