From ba34fcfe69c79688df5597ccf0a5be28eec8f946 Mon Sep 17 00:00:00 2001
From: Bohyun Lee <bohyun.lee@databricks.com>
Date: Wed, 28 Oct 2026 14:05:27 +0100
Subject: [PATCH] pg_upgrade: preserve template0 ICU rules

Copy template0's daticurules alongside its other locale attributes.
Escape the rules with PQescapeLiteral() and include them in the existing
UPDATE so executeQueryOrDie() retains the full query in error reports.

Grow the query and logging buffers with StringInfo to avoid truncating
long rules or their diagnostics. Preserve errno when formatting retries.

Remove target-version branches from set_locale_and_encoding(), since
pg_upgrade only supports the current version as its target. Keep the
source-version checks for catalog differences.
---
 src/bin/pg_upgrade/info.c       | 18 ++++++--
 src/bin/pg_upgrade/pg_upgrade.c | 74 +++++++++++++++------------------
 src/bin/pg_upgrade/pg_upgrade.h |  1 +
 src/bin/pg_upgrade/server.c     | 32 +++++++++-----
 src/bin/pg_upgrade/util.c       | 23 +++++++++-
 5 files changed, 91 insertions(+), 57 deletions(-)

diff --git a/src/bin/pg_upgrade/info.c b/src/bin/pg_upgrade/info.c
index 5c59cfb32e8..0a88764b078 100644
--- a/src/bin/pg_upgrade/info.c
+++ b/src/bin/pg_upgrade/info.c
@@ -339,23 +339,28 @@ get_template0_info(ClusterInfo *cluster)
 	int			i_datcollate;
 	int			i_datctype;
 	int			i_datlocale;
+	int			i_daticurules;
 
 	if (GET_MAJOR_VERSION(cluster->major_version) >= 1700)
 		dbres = executeQueryOrDie(conn,
 								  "SELECT encoding, datlocprovider, "
-								  "       datcollate, datctype, datlocale "
+								  "       datcollate, datctype, datlocale, daticurules "
 								  "FROM	pg_catalog.pg_database "
 								  "WHERE datname='template0'");
 	else if (GET_MAJOR_VERSION(cluster->major_version) >= 1500)
 		dbres = executeQueryOrDie(conn,
 								  "SELECT encoding, datlocprovider, "
-								  "       datcollate, datctype, daticulocale AS datlocale "
+								  "       datcollate, datctype, daticulocale AS datlocale, "
+								  "       %s AS daticurules "
 								  "FROM	pg_catalog.pg_database "
-								  "WHERE datname='template0'");
+								  "WHERE datname='template0'",
+								  GET_MAJOR_VERSION(cluster->major_version) >= 1600 ?
+								  "daticurules" : "NULL");
 	else
 		dbres = executeQueryOrDie(conn,
 								  "SELECT encoding, 'c' AS datlocprovider, "
-								  "       datcollate, datctype, NULL AS datlocale "
+								  "       datcollate, datctype, NULL AS datlocale, "
+								  "       NULL AS daticurules "
 								  "FROM	pg_catalog.pg_database "
 								  "WHERE datname='template0'");
 
@@ -370,6 +375,7 @@ get_template0_info(ClusterInfo *cluster)
 	i_datcollate = PQfnumber(dbres, "datcollate");
 	i_datctype = PQfnumber(dbres, "datctype");
 	i_datlocale = PQfnumber(dbres, "datlocale");
+	i_daticurules = PQfnumber(dbres, "daticurules");
 
 	locale->db_encoding = atoi(PQgetvalue(dbres, 0, i_datencoding));
 	locale->db_collprovider = PQgetvalue(dbres, 0, i_datlocprovider)[0];
@@ -379,6 +385,10 @@ get_template0_info(ClusterInfo *cluster)
 		locale->db_locale = NULL;
 	else
 		locale->db_locale = pg_strdup(PQgetvalue(dbres, 0, i_datlocale));
+	if (PQgetisnull(dbres, 0, i_daticurules))
+		locale->db_icu_rules = NULL;
+	else
+		locale->db_icu_rules = pg_strdup(PQgetvalue(dbres, 0, i_daticurules));
 
 	cluster->template0 = locale;
 
diff --git a/src/bin/pg_upgrade/pg_upgrade.c b/src/bin/pg_upgrade/pg_upgrade.c
index 6d01084e4b0..bc605e0ade8 100644
--- a/src/bin/pg_upgrade/pg_upgrade.c
+++ b/src/bin/pg_upgrade/pg_upgrade.c
@@ -473,9 +473,9 @@ set_new_cluster_char_signedness(void)
 /*
  * Copy locale and encoding information into the new cluster's template0.
  *
- * We need to copy the encoding, datlocprovider, datcollate, datctype, and
- * datlocale. We don't need datcollversion because that's never set for
- * template0.
+ * We need to copy the encoding, datlocprovider, datcollate, datctype,
+ * datlocale, and daticurules. We don't need datcollversion because that's
+ * never set for template0.
  */
 static void
 set_locale_and_encoding(void)
@@ -484,6 +484,7 @@ set_locale_and_encoding(void)
 	char	   *datcollate_literal;
 	char	   *datctype_literal;
 	char	   *datlocale_literal = NULL;
+	char	   *daticurules_literal = NULL;
 	DbLocaleInfo *locale = old_cluster.template0;
 
 	prep_status("Setting locale and encoding for new cluster");
@@ -505,50 +506,41 @@ set_locale_and_encoding(void)
 	else
 		datlocale_literal = "NULL";
 
-	/* update template0 in new cluster */
-	if (GET_MAJOR_VERSION(new_cluster.major_version) >= 1700)
-		PQclear(executeQueryOrDie(conn_new_template1,
-								  "UPDATE pg_catalog.pg_database "
-								  "  SET encoding = %d, "
-								  "      datlocprovider = '%c', "
-								  "      datcollate = %s, "
-								  "      datctype = %s, "
-								  "      datlocale = %s "
-								  "  WHERE datname = 'template0' ",
-								  locale->db_encoding,
-								  locale->db_collprovider,
-								  datcollate_literal,
-								  datctype_literal,
-								  datlocale_literal));
-	else if (GET_MAJOR_VERSION(new_cluster.major_version) >= 1500)
-		PQclear(executeQueryOrDie(conn_new_template1,
-								  "UPDATE pg_catalog.pg_database "
-								  "  SET encoding = %d, "
-								  "      datlocprovider = '%c', "
-								  "      datcollate = %s, "
-								  "      datctype = %s, "
-								  "      daticulocale = %s "
-								  "  WHERE datname = 'template0' ",
-								  locale->db_encoding,
-								  locale->db_collprovider,
-								  datcollate_literal,
-								  datctype_literal,
-								  datlocale_literal));
+	if (locale->db_icu_rules)
+	{
+		daticurules_literal = PQescapeLiteral(conn_new_template1,
+											  locale->db_icu_rules,
+											  strlen(locale->db_icu_rules));
+		if (daticurules_literal == NULL)
+			pg_fatal("could not escape ICU rules for new cluster's template0: %s",
+					 PQerrorMessage(conn_new_template1));
+	}
 	else
-		PQclear(executeQueryOrDie(conn_new_template1,
-								  "UPDATE pg_catalog.pg_database "
-								  "  SET encoding = %d, "
-								  "      datcollate = %s, "
-								  "      datctype = %s "
-								  "  WHERE datname = 'template0' ",
-								  locale->db_encoding,
-								  datcollate_literal,
-								  datctype_literal));
+		daticurules_literal = "NULL";
+
+	/* update template0 in new cluster */
+	PQclear(executeQueryOrDie(conn_new_template1,
+							  "UPDATE pg_catalog.pg_database "
+							  "  SET encoding = %d, "
+							  "      datlocprovider = '%c', "
+							  "      datcollate = %s, "
+							  "      datctype = %s, "
+							  "      datlocale = %s, "
+							  "      daticurules = %s "
+							  "  WHERE datname = 'template0' ",
+							  locale->db_encoding,
+							  locale->db_collprovider,
+							  datcollate_literal,
+							  datctype_literal,
+							  datlocale_literal,
+							  daticurules_literal));
 
 	PQfreemem(datcollate_literal);
 	PQfreemem(datctype_literal);
 	if (locale->db_locale)
 		PQfreemem(datlocale_literal);
+	if (locale->db_icu_rules)
+		PQfreemem(daticurules_literal);
 
 	PQfinish(conn_new_template1);
 
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index 30a2e263d2d..427fb302381 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -195,6 +195,7 @@ typedef struct
 	char	   *db_ctype;
 	char		db_collprovider;
 	char	   *db_locale;
+	char	   *db_icu_rules;
 	int			db_encoding;
 } DbLocaleInfo;
 
diff --git a/src/bin/pg_upgrade/server.c b/src/bin/pg_upgrade/server.c
index 7da9dffe585..a75daf09137 100644
--- a/src/bin/pg_upgrade/server.c
+++ b/src/bin/pg_upgrade/server.c
@@ -11,6 +11,7 @@
 
 #include "common/connect.h"
 #include "fe_utils/string_utils.h"
+#include "lib/stringinfo.h"
 #include "libpq/pqcomm.h"
 #include "pg_upgrade.h"
 
@@ -123,30 +124,41 @@ cluster_conn_opts(ClusterInfo *cluster)
 PGresult *
 executeQueryOrDie(PGconn *conn, const char *fmt, ...)
 {
-	static char query[QUERY_ALLOC];
-	va_list		args;
+	int			save_errno = errno;
+	StringInfoData query;
 	PGresult   *result;
 	ExecStatusType status;
 
-	va_start(args, fmt);
-	vsnprintf(query, sizeof(query), fmt, args);
-	va_end(args);
+	initStringInfo(&query);
+	for (;;)
+	{
+		va_list		args;
+		int			needed;
+
+		errno = save_errno;
+		va_start(args, fmt);
+		needed = appendStringInfoVA(&query, fmt, args);
+		va_end(args);
+		if (needed == 0)
+			break;
+		enlargeStringInfo(&query, needed);
+	}
 
-	pg_log(PG_VERBOSE, "executing: %s", query);
-	result = PQexec(conn, query);
+	pg_log(PG_VERBOSE, "executing: %s", query.data);
+	result = PQexec(conn, query.data);
 	status = PQresultStatus(result);
 
 	if ((status != PGRES_TUPLES_OK) && (status != PGRES_COMMAND_OK))
 	{
-		pg_log(PG_REPORT, "SQL command failed\n%s\n%s", query,
+		pg_log(PG_REPORT, "SQL command failed\n%s\n%s", query.data,
 			   PQerrorMessage(conn));
 		PQclear(result);
 		PQfinish(conn);
 		printf(_("Failure, exiting\n"));
 		exit(1);
 	}
-	else
-		return result;
+	pg_free(query.data);
+	return result;
 }
 
 
diff --git a/src/bin/pg_upgrade/util.c b/src/bin/pg_upgrade/util.c
index e7d7ab56445..abdb35764de 100644
--- a/src/bin/pg_upgrade/util.c
+++ b/src/bin/pg_upgrade/util.c
@@ -12,6 +12,7 @@
 #include <signal.h>
 
 #include "common/username.h"
+#include "lib/stringinfo.h"
 #include "pg_upgrade.h"
 
 LogOpts		log_opts;
@@ -175,13 +176,30 @@ prep_status_progress(const char *fmt, ...)
 static void
 pg_log_v(eLogType type, const char *fmt, va_list ap)
 {
-	char		message[QUERY_ALLOC];
+	int			save_errno = errno;
+	StringInfoData buf;
+	char	   *message;
 
 	/* No incoming message should end in newline; we add that here. */
 	Assert(fmt);
 	Assert(fmt[0] == '\0' || fmt[strlen(fmt) - 1] != '\n');
 
-	vsnprintf(message, sizeof(message), _(fmt), ap);
+	fmt = _(fmt);
+	initStringInfo(&buf);
+	for (;;)
+	{
+		va_list		ap2;
+		int			needed;
+
+		errno = save_errno;
+		va_copy(ap2, ap);
+		needed = appendStringInfoVA(&buf, fmt, ap2);
+		va_end(ap2);
+		if (needed == 0)
+			break;
+		enlargeStringInfo(&buf, needed);
+	}
+	message = buf.data;
 
 	/* PG_VERBOSE and PG_STATUS are only output in verbose mode */
 	/* fopen() on log_opts.internal might have failed, so check it */
@@ -251,6 +269,7 @@ pg_log_v(eLogType type, const char *fmt, va_list ap)
 
 			/* No default:, we want a warning for omitted cases */
 	}
+	pg_free(message);
 	fflush(stdout);
 }
 

base-commit: 76a55702d89ba9f5d0198c2d5d3d2d62357a8880
-- 
2.50.1 (Apple Git-155)

