Hi hackers,

pg_dump's DOTypeNameCompare() can reach its Assert(false) fall-through
(pg_dump_sort.c) when a database contains two casts, or two transforms,
whose types share a typname across different schemas. On an
assertion-enabled build this aborts the dump:
pg_dump: pg_dump_sort.c:479: DOTypeNameCompare: Assertion `0' failed.

I hit this in the field with an extension that defines its own json type
alongside pg_catalog.json and casts to both, but it reproduces trivially
without any extension:
CREATE SCHEMA s;
CREATE TYPE public.tgt AS ENUM ('a');
CREATE TYPE s.tgt      AS ENUM ('a');
CREATE TYPE public.src AS ENUM ('a');
CREATE CAST (public.src AS public.tgt) WITH INOUT;
CREATE CAST (public.src AS s.tgt)      WITH INOUT;

$ pg_dump --schema-only ...        # aborts on an --enable-cassert build

The cause: casts and transforms have no namespace of their own, and
getCasts() / getTransforms() build their sort name from the unqualified
type (and language) names. Two casts therefore tie on the full sort key
whenever their source/target type names match but the types live in
different schemas ("sourcetype tgt" in the example); transforms tie the same
way as "typname langname". Since DOTypeNameCompare() has no DO_CAST or
DO_TRANSFORM tiebreaker, such pairs fall through to the assert on master and
 RL_19_STABLE branches.

The attached patch adds the missing tiebreakers, comparing the referenced
types by their full natural key via the existing pgTypeNameCompare()
(nspname, then typname) — the same helper already used for function
arguments and operator operands. For transforms, comparing trftype alone
is sufficient, since a name tie already implies the same unqualified typname
and language name. It also adds regression coverage to 002_pg_dump.pl (two
casts and two transforms sharing a typname across schemas), which aborts an
unpatched assert-enabled run and passes with the fix.

Regards,
--
Alexander Kukushkin
From 42ca91a5b69381860678a9bda66782fad30ae184 Mon Sep 17 00:00:00 2001
From: Alexander Kukushkin <[email protected]>
Date: Thu, 20 Aug 2026 12:06:17 +0200
Subject: [PATCH] pg_dump: sort casts and transforms independent of OIDs

DOTypeNameCompare() sorts dumpable objects by (priority, namespace, name,
objType) and then an object-type-specific natural-key tiebreaker.  Casts
and transforms have no namespace of their own, and getCasts() /
getTransforms() build their sort "name" from the *unqualified* type (and
language) names.  Two casts therefore tie whenever their source and target
type names match while the types live in different schemas -- for example a
cast to pg_catalog.json and a cast to someext.json from the same source
type both get the sort name "sourcetype json".  Transforms tie the same way
("typname langname").

With no DO_CAST or DO_TRANSFORM tiebreaker, such pairs reach the
Assert(false) fall-through added in commit 0decd5e89db (aborting
assert-enabled pg_dump), or on non-assert builds sort by OID, reintroducing
exactly the schema-diff instability that commit and its follow-ups
(b61a5c4bed7, 4921a5972a3, d49936f3028) have been eliminating.

Break the tie using the referenced types' full natural keys via the
existing pgTypeNameCompare() (nspname, then typname), the same helper
already used for function arguments and operator operands.  For transforms,
comparing trftype alone suffices: a name tie already implies the same
unqualified typname and the same language name, so only the type's schema
can differ.

Add regression coverage to 002_pg_dump.pl: two casts and two transforms
whose types share a typname across schemas, which abort an unpatched
assert-enabled run and pass with the fix.
---
 src/bin/pg_dump/pg_dump_sort.c   | 28 +++++++++++++++++++++++
 src/bin/pg_dump/t/002_pg_dump.pl | 39 ++++++++++++++++++++++++++++++++
 2 files changed, 67 insertions(+)

diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 03e5c1c1116..6ff106d8ac3 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -342,6 +342,34 @@ DOTypeNameCompare(const void *p1, const void *p2)
 		if (cmpval != 0)
 			return cmpval;
 	}
+	else if (obj1->objType == DO_CAST)
+	{
+		CastInfo   *cobj1 = *(CastInfo *const *) p1;
+		CastInfo   *cobj2 = *(CastInfo *const *) p2;
+
+		/*
+		 * The "name" is only the source and target type names, unqualified,
+		 * so two casts tie whenever their types share typnames across
+		 * different schemas.  Break the tie by the source then target types'
+		 * full natural keys.
+		 */
+		cmpval = pgTypeNameCompare(cobj1->castsource, cobj2->castsource);
+		if (cmpval != 0)
+			return cmpval;
+		cmpval = pgTypeNameCompare(cobj1->casttarget, cobj2->casttarget);
+		if (cmpval != 0)
+			return cmpval;
+	}
+	else if (obj1->objType == DO_TRANSFORM)
+	{
+		TransformInfo *tobj1 = *(TransformInfo *const *) p1;
+		TransformInfo *tobj2 = *(TransformInfo *const *) p2;
+
+		/* Same unqualified-typname ambiguity as casts; break by type. */
+		cmpval = pgTypeNameCompare(tobj1->trftype, tobj2->trftype);
+		if (cmpval != 0)
+			return cmpval;
+	}
 	else if (obj1->objType == DO_ATTRDEF)
 	{
 		AttrDefInfo *adobj1 = *(AttrDefInfo *const *) p1;
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 9258948b583..4307cca8525 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -2204,6 +2204,26 @@ my %tests = (
 		like => { %full_runs, section_pre_data => 1, },
 	},
 
+	'CREATE CAST with typname shared across schemas' => {
+		create_order => 51,
+		create_sql => '
+			CREATE SCHEMA dump_cast_schema;
+			CREATE TYPE public.dump_cast_src AS ENUM (\'a\');
+			CREATE TYPE public.dump_cast_tgt AS ENUM (\'a\');
+			CREATE TYPE dump_cast_schema.dump_cast_tgt AS ENUM (\'a\');
+			CREATE CAST (public.dump_cast_src AS public.dump_cast_tgt) WITH INOUT;
+			CREATE CAST (public.dump_cast_src AS dump_cast_schema.dump_cast_tgt) WITH INOUT;',
+		regexp =>
+		  qr/CREATE CAST \(public\.dump_cast_src AS public\.dump_cast_tgt\) WITH INOUT;/m,
+		like => { %full_runs, section_pre_data => 1, },
+	},
+
+	'CREATE CAST to schema-qualified type sharing a typname' => {
+		regexp =>
+		  qr/CREATE CAST \(public\.dump_cast_src AS dump_cast_schema\.dump_cast_tgt\) WITH INOUT;/m,
+		like => { %full_runs, section_pre_data => 1, },
+	},
+
 	'CREATE DATABASE postgres' => {
 		regexp => qr/^
 			\QCREATE DATABASE postgres WITH TEMPLATE = template0 \E
@@ -2927,6 +2947,25 @@ my %tests = (
 		like => { %full_runs, section_pre_data => 1, },
 	},
 
+	'CREATE TRANSFORM with typname shared across schemas' => {
+		create_order => 34,
+		create_sql => '
+			CREATE SCHEMA dump_trf_schema;
+			CREATE TYPE public.dump_trf_type AS ENUM (\'a\');
+			CREATE TYPE dump_trf_schema.dump_trf_type AS ENUM (\'a\');
+			CREATE TRANSFORM FOR public.dump_trf_type LANGUAGE sql (FROM SQL WITH FUNCTION prsd_lextype(internal));
+			CREATE TRANSFORM FOR dump_trf_schema.dump_trf_type LANGUAGE sql (FROM SQL WITH FUNCTION prsd_lextype(internal));',
+		regexp =>
+		  qr/CREATE TRANSFORM FOR public\.dump_trf_type LANGUAGE sql \(FROM SQL WITH FUNCTION pg_catalog\.prsd_lextype\(internal\)\);/m,
+		like => { %full_runs, section_pre_data => 1, },
+	},
+
+	'CREATE TRANSFORM for schema-qualified type sharing a typname' => {
+		regexp =>
+		  qr/CREATE TRANSFORM FOR dump_trf_schema\.dump_trf_type LANGUAGE sql \(FROM SQL WITH FUNCTION pg_catalog\.prsd_lextype\(internal\)\);/m,
+		like => { %full_runs, section_pre_data => 1, },
+	},
+
 	'CREATE LANGUAGE pltestlang' => {
 		create_order => 18,
 		create_sql => 'CREATE LANGUAGE pltestlang
-- 
2.34.1

Reply via email to