From 40af702b80ad312f11d53b292ddcea3048884e20 Mon Sep 17 00:00:00 2001 From: shihao zhong Date: Wed, 28 Oct 2026 21:21:58 -0400 Subject: [PATCH v4] pg_upgrade: Check for settings and casts that 19 cannot restore Upgrades from 18 or older failed on standard_conforming_strings = off, escape_string_warning and uuid bytea casts. Check for them. Backpatch-through: 19 Discussion: https://postgr.es/m/CAGRkXqT+5+mQhoKZFTiM4yOZtVV=NW506vDwFtVeG+VmL_W5wQ@mail.gmail.com --- src/bin/pg_upgrade/check.c | 222 +++++++++++++++++++++++++ src/bin/pg_upgrade/t/002_pg_upgrade.pl | 77 +++++++++ 2 files changed, 299 insertions(+) diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c index 150c4fc34b9..351ca5c5583 100644 --- a/src/bin/pg_upgrade/check.c +++ b/src/bin/pg_upgrade/check.c @@ -14,6 +14,7 @@ #include "catalog/pg_am_d.h" #include "catalog/pg_authid_d.h" #include "catalog/pg_class_d.h" +#include "catalog/pg_type_d.h" #include "fe_utils/string_utils.h" #include "mb/pg_wchar.h" #include "pg_upgrade.h" @@ -30,6 +31,8 @@ static void check_for_incompatible_polymorphics(ClusterInfo *cluster); static void check_for_tables_with_oids(ClusterInfo *cluster); static void check_for_not_null_inheritance(ClusterInfo *cluster); static void check_for_gist_inet_ops(ClusterInfo *cluster); +static void check_for_nonstandard_string_settings(ClusterInfo *cluster); +static void check_for_uuid_bytea_casts(ClusterInfo *cluster); static void check_for_new_tablespace_dir(void); static void check_for_user_defined_encoding_conversions(ClusterInfo *cluster); static void check_for_unicode_update(ClusterInfo *cluster); @@ -688,6 +691,23 @@ check_and_dump_old_cluster(void) if (GET_MAJOR_VERSION(old_cluster.major_version) <= 1800) check_for_gist_inet_ops(&old_cluster); + /* + * PG 19 removed escape_string_warning and no longer accepts + * standard_conforming_strings = off, so restoring a role, database or + * function that sets either of them fails. Verify there are none, iff + * applicable. + */ + if (GET_MAJOR_VERSION(old_cluster.major_version) <= 1800) + check_for_nonstandard_string_settings(&old_cluster); + + /* + * PG 19 added built-in casts between uuid and bytea, so restoring a + * user-defined cast between them fails. Verify there are none, iff + * applicable. + */ + if (GET_MAJOR_VERSION(old_cluster.major_version) <= 1800) + check_for_uuid_bytea_casts(&old_cluster); + /* * While not a check option, we do this now because this is the only time * the old server is running. @@ -1812,6 +1832,208 @@ check_for_gist_inet_ops(ClusterInfo *cluster) check_ok(); } +/* + * SQL condition matching a setconfig or proconfig entry that sets + * standard_conforming_strings to any spelling of off, or sets + * escape_string_warning to anything. + */ +#define NONSTANDARD_STRING_SETTING(c) \ + "(pg_catalog.lower(" c ") ~ " \ + "'^standard_conforming_strings=(0|f|fa|fal|fals|false|n|no|of|off)$' " \ + " OR pg_catalog.split_part(" c ", '=', 1) = 'escape_string_warning')" + +/* + * Callback function for processing results of query for + * check_for_nonstandard_string_settings()'s UpgradeTask. If the query + * returned any rows (i.e., the check failed), write the details to the report + * file. + */ +static void +process_nonstandard_string_settings(DbInfo *dbinfo, PGresult *res, void *arg) +{ + UpgradeTaskReport *report = (UpgradeTaskReport *) arg; + int ntups = PQntuples(res); + int i_proc = PQfnumber(res, "proc"); + int i_setting = PQfnumber(res, "setting"); + + if (ntups == 0) + return; + + if (report->file == NULL && + (report->file = fopen_priv(report->path, "w")) == NULL) + pg_fatal("could not open file \"%s\": %m", report->path); + + fprintf(report->file, "In database: %s\n", dbinfo->db_name); + + for (int rowno = 0; rowno < ntups; rowno++) + fprintf(report->file, " function %s: %s\n", + PQgetvalue(res, rowno, i_proc), + PQgetvalue(res, rowno, i_setting)); +} + +/* + * check_for_nonstandard_string_settings() + * + * Verify that no role, database or function sets standard_conforming_strings + * to off or sets escape_string_warning. Since 19, the former is rejected and + * the latter no longer exists, so restoring them would fail. + */ +static void +check_for_nonstandard_string_settings(ClusterInfo *cluster) +{ + UpgradeTaskReport report; + UpgradeTask *task; + PGconn *conn; + PGresult *res; + const char *query; + + prep_status("Checking for non-standard string settings"); + + report.file = NULL; + snprintf(report.path, sizeof(report.path), "%s/%s", + log_opts.basedir, + "nonstandard_string_settings.txt"); + + /* + * Role and database settings are cluster-wide, so look them up once. + * Settings for all roles in all databases are not dumped, so skip them. + */ + conn = connectToServer(cluster, "template1"); + res = executeQueryOrDie(conn, + "SELECT d.datname, r.rolname, s.setting " + "FROM (SELECT setdatabase, setrole, " + " pg_catalog.unnest(setconfig) AS setting " + " FROM pg_catalog.pg_db_role_setting " + " WHERE setdatabase <> 0 OR setrole <> 0) s " + " LEFT JOIN pg_catalog.pg_database d " + " ON d.oid = s.setdatabase " + " LEFT JOIN pg_catalog.pg_roles r " + " ON r.oid = s.setrole " + "WHERE " NONSTANDARD_STRING_SETTING("s.setting")); + + if (PQntuples(res) > 0 && + (report.file = fopen_priv(report.path, "w")) == NULL) + pg_fatal("could not open file \"%s\": %m", report.path); + + for (int rowno = 0; rowno < PQntuples(res); rowno++) + { + const char *datname = PQgetvalue(res, rowno, 0); + const char *rolname = PQgetvalue(res, rowno, 1); + const char *setting = PQgetvalue(res, rowno, 2); + + if (PQgetisnull(res, rowno, 1)) + fprintf(report.file, "database %s: %s\n", datname, setting); + else if (PQgetisnull(res, rowno, 0)) + fprintf(report.file, "role %s: %s\n", rolname, setting); + else + fprintf(report.file, "role %s in database %s: %s\n", + rolname, datname, setting); + } + + PQclear(res); + PQfinish(conn); + + /* Function settings are per-database. */ + query = "SELECT p.oid::pg_catalog.regprocedure AS proc, p.setting " + "FROM (SELECT oid, pg_catalog.unnest(proconfig) AS setting " + " FROM pg_catalog.pg_proc " + " WHERE proconfig IS NOT NULL) p " + "WHERE " NONSTANDARD_STRING_SETTING("p.setting"); + + task = upgrade_task_create(); + upgrade_task_add_step(task, query, process_nonstandard_string_settings, + true, &report); + upgrade_task_run(task, cluster); + upgrade_task_free(task); + + if (report.file) + { + fclose(report.file); + pg_log(PG_REPORT, "fatal"); + pg_fatal("Your installation contains roles, databases, or functions that set\n" + "standard_conforming_strings to off or set escape_string_warning.\n" + "These settings are no longer supported, so the upgrade would fail.\n" + "Remove them with ALTER ... RESET and restart the upgrade.\n" + "A list of the settings is in the file:\n" + " %s", report.path); + } + else + check_ok(); +} + +/* + * Callback function for processing results of query for + * check_for_uuid_bytea_casts()'s UpgradeTask. If the query returned any rows + * (i.e., the check failed), write the details to the report file. + */ +static void +process_uuid_bytea_casts(DbInfo *dbinfo, PGresult *res, void *arg) +{ + UpgradeTaskReport *report = (UpgradeTaskReport *) arg; + int ntups = PQntuples(res); + int i_source = PQfnumber(res, "source"); + int i_target = PQfnumber(res, "target"); + + if (ntups == 0) + return; + + if (report->file == NULL && + (report->file = fopen_priv(report->path, "w")) == NULL) + pg_fatal("could not open file \"%s\": %m", report->path); + + fprintf(report->file, "In database: %s\n", dbinfo->db_name); + + for (int rowno = 0; rowno < ntups; rowno++) + fprintf(report->file, " cast from %s to %s\n", + PQgetvalue(res, rowno, i_source), + PQgetvalue(res, rowno, i_target)); +} + +/* + * check_for_uuid_bytea_casts() + * + * Verify that there are no user-defined casts between uuid and bytea. Since + * 19 these casts are built in, so restoring them would fail. + */ +static void +check_for_uuid_bytea_casts(ClusterInfo *cluster) +{ + UpgradeTaskReport report; + UpgradeTask *task = upgrade_task_create(); + const char *query = "SELECT c.castsource::pg_catalog.regtype AS source, " + " c.casttarget::pg_catalog.regtype AS target " + "FROM pg_catalog.pg_cast c " + "WHERE (c.castsource = " CppAsString2(UUIDOID) + " AND c.casttarget = " CppAsString2(BYTEAOID) ")" + " OR (c.castsource = " CppAsString2(BYTEAOID) + " AND c.casttarget = " CppAsString2(UUIDOID) ")"; + + prep_status("Checking for user-defined casts between uuid and bytea"); + + report.file = NULL; + snprintf(report.path, sizeof(report.path), "%s/%s", + log_opts.basedir, + "uuid_bytea_casts.txt"); + + upgrade_task_add_step(task, query, process_uuid_bytea_casts, + true, &report); + upgrade_task_run(task, cluster); + upgrade_task_free(task); + + if (report.file) + { + fclose(report.file); + pg_log(PG_REPORT, "fatal"); + pg_fatal("Your installation contains user-defined casts between uuid and bytea.\n" + "The new cluster has built-in casts between these types, so the upgrade\n" + "would fail. Drop these casts and restart the upgrade.\n" + "A list of the casts is in the file:\n" + " %s", report.path); + } + else + check_ok(); +} + /* * Callback function for processing results of query for * check_for_user_defined_encoding_conversions()'s UpgradeTask. If the query diff --git a/src/bin/pg_upgrade/t/002_pg_upgrade.pl b/src/bin/pg_upgrade/t/002_pg_upgrade.pl index be57b2d838c..2dc44b7fe00 100644 --- a/src/bin/pg_upgrade/t/002_pg_upgrade.pl +++ b/src/bin/pg_upgrade/t/002_pg_upgrade.pl @@ -599,6 +599,83 @@ $oldnode->start; $oldnode->safe_psql('postgres', 'DROP DATABASE regression_invalid'); $oldnode->stop; +# Check that pg_upgrade aborts on settings and casts that 19 cannot restore. +# These checks only run for old clusters of version 18 or older. +SKIP: +{ + skip "only applies to upgrades from 18 or older", 9 + if $oldnode->pg_version >= '19devel'; + + my @check_command = ( + 'pg_upgrade', '--no-sync', + '--old-datadir' => $oldnode->data_dir, + '--new-datadir' => $newnode->data_dir, + '--old-bindir' => $oldbindir, + '--new-bindir' => $newbindir, + '--socketdir' => $newnode->host, + '--old-port' => $oldnode->port, + '--new-port' => $newnode->port, + $mode, '--check',); + + # standard_conforming_strings = on is still fine, so only the role + # regress_scs_off and the function should be reported. + $oldnode->start; + $oldnode->safe_psql( + 'postgres', qq( + CREATE ROLE regress_scs_off; + ALTER ROLE regress_scs_off SET standard_conforming_strings = off; + CREATE ROLE regress_scs_on; + ALTER ROLE regress_scs_on SET standard_conforming_strings = on; + CREATE FUNCTION regress_esw() RETURNS int LANGUAGE sql + SET escape_string_warning = off AS 'SELECT 1'; +)); + $oldnode->stop; + + command_checks_all( + \@check_command, + 1, + [qr/standard_conforming_strings to off or set escape_string_warning/], + [qr/^$/], + 'non-standard string settings cause failure'); + + my ($report) = glob($newnode->data_dir + . "/pg_upgrade_output.d/*/nonstandard_string_settings.txt"); + my $contents = slurp_file($report); + like( + $contents, + qr/^role regress_scs_off: standard_conforming_strings=off$/m, + 'role setting is reported'); + like( + $contents, + qr/^ function .*regress_esw\(\): escape_string_warning=off$/m, + 'function setting is reported'); + unlike($contents, qr/regress_scs_on/, + 'standard_conforming_strings = on is not reported'); + rmtree($newnode->data_dir . "/pg_upgrade_output.d"); + + $oldnode->start; + $oldnode->safe_psql( + 'postgres', qq( + DROP ROLE regress_scs_off; + DROP ROLE regress_scs_on; + DROP FUNCTION regress_esw(); + CREATE CAST (uuid AS bytea) WITH FUNCTION uuid_send(uuid); +)); + $oldnode->stop; + + command_checks_all( + \@check_command, + 1, + [qr/user-defined casts between uuid and bytea/], + [qr/^$/], + 'cast from uuid to bytea causes failure'); + rmtree($newnode->data_dir . "/pg_upgrade_output.d"); + + $oldnode->start; + $oldnode->safe_psql('postgres', 'DROP CAST (uuid AS bytea)'); + $oldnode->stop; +} + # --check command works here, cleans up pg_upgrade_output.d. command_ok( [ -- 2.37.1 (Apple Git-137.1)