From 0f08a5a1bb0ffa493e18a7cf461e2be172e9768d Mon Sep 17 00:00:00 2001 From: reshke Date: Sat, 3 Oct 2026 14:36:26 +0300 Subject: [PATCH v1] Check for foreign key with non-deterministic collations in pg_upgrade Since PG 18/9321d2fdf808, a foreign key with different collations on the key columns is rejected if either collation is nondeterministic. Upgrading such cluster would fail. Add a per-database pre-check of the old cluster reporting exactly the shapes the new cluster would reject --- src/bin/pg_upgrade/check.c | 132 ++++++++++++++++++ src/bin/pg_upgrade/meson.build | 1 + .../pg_upgrade/t/009_fk_collation_mismatch.pl | 94 +++++++++++++ 3 files changed, 227 insertions(+) create mode 100644 src/bin/pg_upgrade/t/009_fk_collation_mismatch.pl diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c index 150c4fc34b9..b5526d9d1cf 100644 --- a/src/bin/pg_upgrade/check.c +++ b/src/bin/pg_upgrade/check.c @@ -29,6 +29,7 @@ static void check_for_user_defined_postfix_ops(ClusterInfo *cluster); 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_fk_collation_mismatch(ClusterInfo *cluster); static void check_for_gist_inet_ops(ClusterInfo *cluster); static void check_for_new_tablespace_dir(void); static void check_for_user_defined_encoding_conversions(ClusterInfo *cluster); @@ -673,6 +674,19 @@ check_and_dump_old_cluster(void) if (GET_MAJOR_VERSION(old_cluster.major_version) <= 1800) check_for_not_null_inheritance(&old_cluster); + /* + * PG 18 rejects foreign keys where the referencing and referenced + * columns have different collations and either of them is + * nondeterministic, but older clusters allow creating them, so the + * schema restore of the new cluster would fail. Verify there are + * none, iff the new cluster enforces the rule. Versions before 12 + * have no nondeterministic collations, so they cannot contain the + * problem. + */ + if (GET_MAJOR_VERSION(new_cluster.major_version) >= 1800 && + GET_MAJOR_VERSION(old_cluster.major_version) >= 1200) + check_for_fk_collation_mismatch(&old_cluster); + /* * The btree_gist extension contains gist_inet_ops and gist_cidr_ops * opclasses that do not reliably give correct answers. We want to @@ -1738,6 +1752,124 @@ check_for_not_null_inheritance(ClusterInfo *cluster) check_ok(); } +/* + * Callback function for processing results of query for + * check_for_fk_collation_mismatch()'s UpgradeTask. If the query returned + * any rows (i.e., the check failed), write the details to the report file. + */ +static void +process_fk_collation_mismatch(DbInfo *dbinfo, PGresult *res, void *arg) +{ + UpgradeTaskReport *report = (UpgradeTaskReport *) arg; + int ntups = PQntuples(res); + int i_fknspname = PQfnumber(res, "fknspname"); + int i_fkrelname = PQfnumber(res, "fkrelname"); + int i_conname = PQfnumber(res, "conname"); + int i_fkattname = PQfnumber(res, "fkattname"); + int i_fkcname = PQfnumber(res, "fkcname"); + int i_pknspname = PQfnumber(res, "pknspname"); + int i_pkrelname = PQfnumber(res, "pkrelname"); + int i_pkattname = PQfnumber(res, "pkattname"); + int i_pkcname = PQfnumber(res, "pkcname"); + + 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, + " constraint %s on column %s.%s.%s (collation \"%s\")\n" + " references column %s.%s.%s (collation \"%s\")\n", + PQgetvalue(res, rowno, i_conname), + PQgetvalue(res, rowno, i_fknspname), + PQgetvalue(res, rowno, i_fkrelname), + PQgetvalue(res, rowno, i_fkattname), + PQgetvalue(res, rowno, i_fkcname), + PQgetvalue(res, rowno, i_pknspname), + PQgetvalue(res, rowno, i_pkrelname), + PQgetvalue(res, rowno, i_pkattname), + PQgetvalue(res, rowno, i_pkcname)); +} + +/* + * check_for_fk_collation_mismatch() + * + * PG 18 made it an error to declare a foreign key whose referencing and + * referenced columns have different collations with either of them being + * nondeterministic (prior versions allowed it). Such a constraint cannot + * be created on the new cluster, so its schema restore would fail mid-way + * through the upgrade. Check that the cluster to be upgraded doesn't + * contain any of those. + */ +static void +check_for_fk_collation_mismatch(ClusterInfo *cluster) +{ + UpgradeTaskReport report; + UpgradeTask *task; + const char *query; + + prep_status("Checking for foreign keys with incompatible collations"); + + report.file = NULL; + snprintf(report.path, sizeof(report.path), "%s/%s", + log_opts.basedir, + "fk_collation_mismatch.txt"); + + query = "SELECT fkn.nspname AS fknspname, fkc.relname AS fkrelname, " + " con.conname, fka.attname AS fkattname, " + " fkc2.collname AS fkcname, " + " pkn.nspname AS pknspname, pkc.relname AS pkrelname, " + " pka.attname AS pkattname, pkc2.collname AS pkcname " + "FROM pg_catalog.pg_constraint con, " + " unnest(con.conkey, con.confkey) AS keys(fkattnum, pkattnum), " + " pg_catalog.pg_attribute fka, pg_catalog.pg_collation fkc2, " + " pg_catalog.pg_attribute pka, pg_catalog.pg_collation pkc2, " + " pg_catalog.pg_class fkc, pg_catalog.pg_namespace fkn, " + " pg_catalog.pg_class pkc, pg_catalog.pg_namespace pkn " + "WHERE con.contype = 'f' " + " AND fka.attrelid = con.conrelid AND fka.attnum = keys.fkattnum " + " AND pka.attrelid = con.confrelid AND pka.attnum = keys.pkattnum " + " AND fkc2.oid = fka.attcollation " + " AND pkc2.oid = pka.attcollation " + " AND fka.attcollation <> pka.attcollation " + " AND (NOT fkc2.collisdeterministic OR NOT pkc2.collisdeterministic) " + " AND fkc.oid = con.conrelid AND fkn.oid = fkc.relnamespace " + " AND pkc.oid = con.confrelid AND pkn.oid = pkc.relnamespace"; + + task = upgrade_task_create(); + upgrade_task_add_step(task, query, + process_fk_collation_mismatch, + 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 foreign key constraints with\n" + "different collations on the referencing and referenced\n" + "columns, at least one of them nondeterministic. Such\n" + "constraints are rejected by the new PostgreSQL version, so\n" + "this cluster cannot currently be upgraded. You can\n" + "fix this by making the referencing and referenced columns\n" + "use the same collation, e.g.:\n" + " ALTER TABLE fk_table ALTER COLUMN fk_col TYPE text\n" + " COLLATE collation;\n" + "or by removing the constraint and recreating it after the\n" + "upgrade.\n" + "A list of the problem constraints is in the file:\n" + " %s", report.path); + } + else + check_ok(); +} + /* * Callback function for processing results of query for * check_for_gist_inet_ops()'s UpgradeTask. If the query returned any rows diff --git a/src/bin/pg_upgrade/meson.build b/src/bin/pg_upgrade/meson.build index ffbf6ae8d75..5aeaa41be87 100644 --- a/src/bin/pg_upgrade/meson.build +++ b/src/bin/pg_upgrade/meson.build @@ -69,6 +69,7 @@ tests += { 't/006_transfer_modes.pl', 't/007_multixact_conversion.pl', 't/008_extension_control_path.pl', + 't/009_fk_collation_mismatch.pl', ], 'deps': [test_ext], 'test_kwargs': {'priority': 40}, # pg_upgrade tests are slow diff --git a/src/bin/pg_upgrade/t/009_fk_collation_mismatch.pl b/src/bin/pg_upgrade/t/009_fk_collation_mismatch.pl new file mode 100644 index 00000000000..796b1478c2b --- /dev/null +++ b/src/bin/pg_upgrade/t/009_fk_collation_mismatch.pl @@ -0,0 +1,94 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test that pg_upgrade detects foreign keys with incompatible collations on +# the key columns, where at least one of the collations is nondeterministic. +# Those were rejected starting with PG 18, but clusters upgraded from older +# versions could still contain them, and without the check the failure would +# happen only in the middle of the schema restore. + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +# Can be changed to test the other modes. +my $mode = $ENV{PG_TEST_PG_UPGRADE_MODE} || '--copy'; + +# Nondeterministic collations are only supported by builds with ICU. +plan skip_all => 'ICU not supported by this build' + unless defined($ENV{with_icu}) && $ENV{with_icu} eq 'yes'; + +# Initialize old and new clusters. +my $old = PostgreSQL::Test::Cluster->new('old'); +my $new = PostgreSQL::Test::Cluster->new('new'); +$old->init(); +$new->init(); + +$old->start; + +$old->safe_psql('postgres', qq{ +CREATE COLLATION ci_nd (provider = icu, locale = 'und-u-ks-level2', + deterministic = false); +CREATE TABLE fk_p (k text COLLATE ci_nd PRIMARY KEY); +CREATE TABLE fk_c (k text COLLATE ci_nd REFERENCES fk_p (k)); +}); + +# An FK with mismatched nondeterministic collations cannot be created with +# regular commands anymore, so mimic a pre-18 cluster by directly pointing +# the referencing column at a different (deterministic) collation in the +# catalogs, in the same shape pg_upgrade would see after an upgrade from +# an old cluster. +$old->safe_psql('postgres', qq{ +UPDATE pg_attribute a +SET attcollation = 'pg_catalog."default"'::regcollation +WHERE a.attrelid = 'public.fk_c'::regclass AND a.attname = 'k'; +}); + +$old->stop; + +# In a VPATH build, we'll be started in the source directory, but we want +# to run pg_upgrade in the build directory so that any files generated +# finish in it, like delete_old_cluster.{sh,bat}. +chdir ${PostgreSQL::Test::Utils::tmp_check}; + +# The check should detect the mismatch and fail. +command_checks_all( + [ + 'pg_upgrade', '--no-sync', '--check', + '--old-datadir' => $old->data_dir, + '--new-datadir' => $new->data_dir, + '--old-bindir' => $old->config_data('--bindir'), + '--new-bindir' => $new->config_data('--bindir'), + '--socketdir' => $new->host, + '--old-port' => $old->port, + '--new-port' => $new->port, + $mode + ], + 1, + [qr/Checking for foreign keys with incompatible collations/, + qr/foreign key constraints with.*different collations on the referencing/s], + [], + 'run of pg_upgrade --check with FK collation mismatch'); + +# After removing the offending constraint, the upgrade should succeed. +$old->start; +$old->safe_psql('postgres', 'ALTER TABLE fk_c DROP CONSTRAINT fk_c_k_fkey'); +$old->stop; + +command_ok( + [ + 'pg_upgrade', '--no-sync', + '--old-datadir' => $old->data_dir, + '--new-datadir' => $new->data_dir, + '--old-bindir' => $old->config_data('--bindir'), + '--new-bindir' => $new->config_data('--bindir'), + '--socketdir' => $new->host, + '--old-port' => $old->port, + '--new-port' => $new->port, + $mode + ], + 'run of pg_upgrade after dropping the offending FK'); + +done_testing(); -- 2.43.0