On Fri, Aug 21, 2026 at 11:59:53AM +0200, Alexander Kukushkin wrote:
> here is v3 version of the patch addressing all nit-picks

Thanks.

> 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.

Since this is already the fourth follow-up to my original change, I had Opus 5
look for more ways to reach the assertion.  It found one more:

D1  DO_POLICY: the "RLS enabled" pseudo-object borrows its table's relname, so
    it ties with a policy named after that same table.  An assert-enabled
    pg_dump aborts; a production build orders the two by comparing a pg_class
    OID against a pg_policy OID, which pg_upgrade inverts.

Let's fix that at the same time.  Would you like to add that, or would you
like me to add it?

I'm attaching the larger Opus 5 report as an FYI.  It found many other pg_dump
ordering problems distinct from the DOTypeNameCompare() assertion, and D3 is a
notable functional bug.  They're off-topic for $SUBJECT, though.
>From 99d7f92890217317005c6e5d6b0d41bf616e3fc5 Mon Sep 17 00:00:00 2001
From: Noah Misch <[email protected]>
Date: Thu, 3 Sep 2026 18:06:51 +0000
Subject: [PATCH 1/2] Audit report and regression tests for pg_dump dump-order
 instability

Audit of what the DO_CAST/DO_TRANSFORM tiebreakers in the preceding commit do
not cover.  Twelve confirmed defects; only one of them is another tie in
DOTypeNameCompare().

D1  DO_POLICY: the "RLS enabled" pseudo-object borrows its table's relname, so
    it ties with a policy named after that same table.  An assert-enabled
    pg_dump aborts; a production build orders the two by comparing a pg_class
    OID against a pg_policy OID, which pg_upgrade inverts.
D2  Dependency-loop repair picks its start point in dumpId order, so which
    object is broken out of a cycle follows OID assignment.  Reported only:
    fixing it changes the dump of databases that dump fine today.
D3  getInherits() has no ORDER BY and pg_dump never reads inhseqno, so the
    INHERITS list follows pg_inherits heap order.  This is not only an
    ordering defect: a plain dump/restore can permute the child's columns.
D4  dumpOpfamily()/dumpOpclass() order members by strategy number alone.
D5  getPolicies() builds the policy's TO role list with an unordered
    sub-select over pg_roles.
D6  getPublications() does not order the FOR ALL TABLES EXCEPT list.
D7  dumpDatabaseConfig() does not order per-role database settings.
D8  append_depends_on_extension() does not order its rows.
D9  collectSecLabels() omits provider from its ORDER BY, as does the
    shared-object path in dumputils.c.
D10 pg_dumpall's dumpTablespaces() says ORDER BY 1 on a select list whose
    first column is oid; the sibling dumpRoles() says ORDER BY 2.
D11 dumpExtension() emits an extension's requires array under
    --binary-upgrade in dependency-array order, which is OID-derived.
D12 getDefaultACLs() emits defaclacl in the backend's canonical order, which
    aclitemsort() makes grantee-OID order.

Tests cover all of these but D2.  Each fails, or aborts pg_dump, on the tree
without the sample fixes in the following commit.  Two needed test
infrastructure rather than a test entry: D8 lives in test_pg_dump because
showing it needs one object with two extension dependencies and a bare initdb
has only plpgsql, and D9 adds a second label provider to dummy_seclabel.

This work is model-generated and unreviewed by a human; see PROVENANCE.md.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Newm1jZHVy54kfX1eRPDwB
---
 DUMP_SORT_STABILITY_REPORT.md                 | 878 ++++++++++++++++++
 PROVENANCE.md                                 | 126 +++
 src/bin/pg_dump/t/002_pg_dump.pl              | 238 ++++-
 src/bin/pg_dump/t/003_pg_dump_with_server.pl  |  91 ++
 .../modules/dummy_seclabel/dummy_seclabel.c   |  26 +
 src/test/modules/test_pg_dump/t/001_base.pl   |  45 +
 6 files changed, 1399 insertions(+), 5 deletions(-)
 create mode 100644 DUMP_SORT_STABILITY_REPORT.md
 create mode 100644 PROVENANCE.md

diff --git a/DUMP_SORT_STABILITY_REPORT.md b/DUMP_SORT_STABILITY_REPORT.md
new file mode 100644
index 0000000..6380580
--- /dev/null
+++ b/DUMP_SORT_STABILITY_REPORT.md
@@ -0,0 +1,878 @@
+# pg_dump dump-order stability: audit of what `sort_cast.patch` does not cover
+
+**Question asked:** after the DO_CAST / DO_TRANSFORM tiebreakers, are more 
sources of
+dump-order instability still lurking?
+
+**Answer:** yes -- **twelve**, but only **one** of them is another tie in
+`DOTypeNameCompare()`.  The other eleven are outside the object sort: one in 
dependency-loop
+repair, ten in dump-time queries and emitters whose result order reaches the 
output text
+directly.  One of those is not merely an ordering nuisance: it makes a plain 
`pg_dump`
+&rarr; restore **silently permute an inherited table's column order**.
+
+| # | Where | What | Severity |
+|---|---|---|---|
+| D1 | `DOTypeNameCompare()` | the RLS-enabled pseudo-object ties with a 
policy named after its own table -- `pg_dump` **aborts** on an assert build | 
high |
+| D2 | `pg_dump_sort.c` loop repair | `TopoSort()` builds its failure list in 
dumpId order, so which object gets broken out of a dependency cycle follows OID 
assignment | medium |
+| D3 | `getInherits()` | no `ORDER BY`; `inhseqno` is never read at all, so 
the `INHERITS` list follows heap order -- **restores with the child's columns 
permuted** | high |
+| D4 | `dumpOpfamily()`, `dumpOpclass()` | member lists ordered only by 
`amopstrategy` / `amprocnum`, which is not a key | medium |
+| D5 | `getPolicies()` | the policy `TO` role list is built by an unordered 
sub-select | medium |
+| D6 | `getPublications()` | `FOR ALL TABLES EXCEPT (...)` list has no `ORDER 
BY` | medium |
+| D7 | `dumpDatabaseConfig()` | `ALTER ROLE ... IN DATABASE ... SET` lines 
have no `ORDER BY` | medium |
+| D8 | `append_depends_on_extension()` | `DEPENDS ON EXTENSION` lines have no 
`ORDER BY` | low |
+| D9 | `collectSecLabels()`, `dumputils.c` | `ORDER BY` omits `provider`, 
which is part of the key | low |
+| D10 | `pg_dumpall.c` `dumpTablespaces()` | `ORDER BY 1` on `SELECT oid, 
spcname, ...` sorts by **OID** | low |
+| D11 | `dumpExtension()` `--binary-upgrade` | the `requires` array is emitted 
in dependency-array order, which is OID-derived | low |
+| D12 | `getDefaultACLs()` | `defaclacl` is emitted in the backend's canonical 
**grantee-OID** order | medium |
+
+Every one of the twelve was reproduced twice: once by an agent that found it, 
once by an
+independent agent whose brief was to refute it.  D1, D3, D4, D2 and D12 were 
additionally
+reproduced by hand, outside the agent framework; the commands are in this 
report.
+
+A twelfth candidate class -- `pg_dump` reproducing the element order of 
array-valued
+catalog columns (`aclitem[]`, `setconfig`, `reloptions`) -- was **rejected**, 
and the
+argument that killed it is worth reading: see [Considered and 
rejected](#considered-and-rejected).
+
+---
+
+## Method
+
+Three oracles, in increasing order of reach.
+
+**1. The stock assert-enabled build.** `DOTypeNameCompare()`'s fall-through is
+`Assert(false)`, so on an assert build a tie makes `pg_dump` abort.  This is 
the oracle
+that matters most, because it is exactly what a developer or a buildfarm 
animal sees, and
+it needs no instrumentation.
+
+**2. A tie reporter.** An audit-only build (`ss-shuf-inst`) whose
+`sortDumpableObjectsByTypeName()` walks the sorted array afterwards and 
reports every
+adjacent pair for which the comparator reached the fall-through, with
+`describeDumpableObject()` output for both.  Ties are necessarily adjacent 
after a sort, so
+one run enumerates *all* of them rather than aborting at the first.  The 
`Assert` is
+disarmed in that build, which also makes it a faithful stand-in for a 
production build:
+with no environment variables set it falls through to `oidcmp()` exactly as a 
non-assert
+`pg_dump` does.
+
+**3. A pre-sort shuffle.** The same build permutes the object array before 
sorting when
+`PGDUMP_SHUFFLE_SEED` is set, and makes the fall-through return 0 rather than 
comparing
+OIDs, so a tie leaves the order genuinely up to `qsort`.  Eight seeds, then 
diff.  Dump
+output must not depend on the input permutation; if it does, the order is 
unstable --
+whatever the cause, including causes that never reach the comparator.
+
+Calibration: all three fire on the DO_POLICY case (D1) and all three are 
silent on the
+core regression database (2291 relations) and on control schemas.  The shuffle 
oracle is
+blind to the D3..D11 class by construction -- those orders come from the 
*server's* result
+order, which is identical under every seed -- so that class had to be found by 
reading the
+queries and confirmed by diffing two independently built databases.  Where a 
finding is of
+that kind, the report says so explicitly and gives the two-database pair.
+
+**Fan-out.** A first workflow ran 17 discovery agents -- 8 sweeping the 48
+`DumpableObjectType` values against their catalogs' natural keys, 4 code 
lenses (the
+topological sort; every dump-time query in `pg_dump.c`; `pg_dumpall.c` and the 
archive TOC;
+the history of the five commits that already fixed this class), and 5 
empirical lanes
+(cross-schema name collisions for every schema-qualified object type; pg_dump's
+manufactured pseudo-objects; in-tree extensions; the regression corpus; a
+differential two-database generator).  Their 54 raw candidates deduplicated to 
49, each of
+which got an independent verification agent and, unless refuted, an 
independent adversarial
+judge told to refute it.  40 survived; those 40 describe **11 distinct 
mechanisms** --
+the same defect was found by up to 13 agents through different object types.  
A second
+workflow put one agent on each surviving mechanism to re-reproduce it from 
scratch, write
+its regression test and write a sample fix, plus two adversarial critics.
+
+**What this method cannot see.** Dump-order instability that requires a server 
version
+older than this tree (`pg_dump` supports back to 9.2 and builds several 
queries in
+version-dependent branches; only the modern branch was exercised), instability 
visible only
+under `pg_restore -j` scheduling, and anything needing a platform this box is 
not.
+
+---
+
+## Part 1 -- the object sort
+
+### D1. `DO_POLICY`: the RLS-enabled pseudo-object collides with a policy 
named after its table
+
+`getPolicies()` represents "row level security is enabled on this table" as a 
`PolicyInfo`
+with `polname == NULL`, and gives it the **table's** name:
+
+```c
+/* src/bin/pg_dump/pg_dump.c:4249 */
+polinfo->dobj.objType = DO_POLICY;
+polinfo->dobj.catId.tableoid = 0;
+polinfo->dobj.catId.oid = tbinfo->dobj.catId.oid;
+AssignDumpId(&polinfo->dobj);
+polinfo->dobj.namespace = tbinfo->dobj.namespace;
+polinfo->dobj.name = pg_strdup(tbinfo->dobj.name);   /* <-- borrowed */
+polinfo->poltable = tbinfo;
+polinfo->polname = NULL;
+```
+
+A real policy gets `dobj.name = polname` and the same namespace, and the 
`DO_POLICY`
+tiebreaker compares only the table name:
+
+```c
+/* src/bin/pg_dump/pg_dump_sort.c */
+else if (obj1->objType == DO_POLICY)
+{
+       /* Sort by table name (table namespace was considered already) */
+       cmpval = strcmp(pobj1->poltable->dobj.name, pobj2->poltable->dobj.name);
+       if (cmpval != 0)
+               return cmpval;
+}
+```
+
+So for a policy whose `polname` equals its own table's `relname`, every step 
returns 0:
+same priority, same namespace, same name, same objType, same table.  This is 
the same shape
+as the cast/transform defect -- a name that is not the object's own -- but 
arrived at from
+the other direction: instead of building a name out of other objects' 
unqualified names, it
+*borrows* one wholesale.
+
+Three statements reproduce it:
+
+```sql
+CREATE TABLE pol_t (i int);
+ALTER TABLE pol_t ENABLE ROW LEVEL SECURITY;
+CREATE POLICY pol_t ON pol_t USING (true);
+```
+
+```
+$ /home/nm/src/pg/ssrun/pgrun.sh plain t_policy.sql >/dev/null
+pg_dump: pg_dump_sort.c:511: DOTypeNameCompare: Assertion `0' failed.
+PG_DUMP FAILED (exit 134)
+ASSERTION FAILURE
+
+$ /home/nm/src/pg/ssrun/pgrun.sh tie t_policy.sql >/dev/null
+SORT TIE: objType 41 name "pol_t" nsp "public" | POLICY (ID 3523 OID 16384) | 
POLICY (ID 3524 OID 16387)
+TIES DETECTED
+```
+
+**On an assert-enabled build the database is simply not dumpable.**  On a 
production build
+the tie falls through to `oidcmp()`, comparing the *table's* OID (the 
pseudo-object carries
+`catId.oid = table oid`, `tableoid = 0`) against the *pg_policy* OID.  In a
+normally-built database the policy always postdates its table, so the order is 
stable by
+luck.  It stops being stable exactly where `pg_upgrade` operates: relation 
OIDs are
+preserved across an upgrade, `pg_policy` OIDs are not.  Give the table a high 
OID in the
+old cluster and the restored policy gets a low one, and the two logically 
identical
+databases dump in opposite orders:
+
+```
+== old cluster OIDs ==            == new cluster OIDs ==
+ table  | 18784                    table  | 18784   (preserved)
+ policy | 18787                    policy | 16384   (reassigned)
+
+$ diff -u old.dump new.dump
+--- Name: rls_demo; Type: ROW SECURITY; Schema: public; Owner: postgres
++-- Name: rls_demo rls_demo; Type: POLICY; Schema: public; Owner: postgres
+-ALTER TABLE public.rls_demo ENABLE ROW LEVEL SECURITY;
++CREATE POLICY rls_demo ON public.rls_demo USING (true);
+```
+
+(`/home/nm/src/pg/ssrun/oidflip.sh` -- it pads the OID counter, takes a
+`pg_dump --binary-upgrade --schema-only`, restores it into a second cluster 
started with
+`-b`, and dumps both with the assert-disarmed build.  That is the flake the 
comment above
+`Assert(false)` predicts, reproduced deliberately.)
+
+The fix is the missing natural-key column.  `pg_policy`'s key is `(polrelid, 
polname)`; the
+pseudo-object is the one row where `polname` is absent, so comparing "is 
`polname` NULL"
+after the table name completes the key:
+
+```c
+       /*
+        * The RLS-enabled pseudo-object (polname == NULL) borrows its name from
+        * its table, so it ties with a policy whose polname equals that table
+        * name.  Sort the pseudo-object first, consistent with ENABLE ROW LEVEL
+        * SECURITY logically preceding the policies on the table.
+        */
+       if (pobj1->polname == NULL)
+       {
+               if (pobj2->polname != NULL)
+                       return -1;
+       }
+       else if (pobj2->polname == NULL)
+               return 1;
+```
+
+Two non-NULL `polname`s on the same table cannot both survive to this point: 
`polname`
+*is* `dobj.name`, already compared at step 3.
+
+### Why nothing else in the comparator ties
+
+D1 is the only tie the audit found, and that claim was put to a dedicated 
adversarial
+critic whose brief was to falsify it.  The reason the rest of the comparator 
is sound comes
+down to two observations that are worth recording, because they are what a 
future reviewer
+needs in order to check a new object type:
+
+1. **Constructed names are now closed.**  Only three construction sites build a
+   `dobj.name` out of other names rather than copying a catalog column: 
`getCasts()` and
+   `getTransforms()` (fixed by `885a841`) and `getLOs()`, whose name is a 
large-object OID
+   range -- and a large object's OID *is* its identity, so that one is not a 
defect.
+2. **Borrowed names are safe wherever the borrower has its priority to 
itself.**  Twelve
+   object types take their name from another object -- `DO_TABLE_ATTACH`,
+   `DO_INDEX_ATTACH`, `DO_ATTRDEF`, `DO_TABLE_DATA`, `DO_SEQUENCE_SET`,
+   `DO_REFRESH_MATVIEW`, `DO_REL_STATS`, `DO_SHELL_TYPE`, `DO_DUMMY_TYPE`,
+   `DO_PUBLICATION_REL`, `DO_PUBLICATION_TABLE_IN_SCHEMA`, 
`DO_SUBSCRIPTION_REL` -- and
+   every one of them is either alone at its priority level or separated from 
its
+   priority-mate by the `objType` comparison, and each has at most one 
instance per
+   borrowed-from object.  `DO_POLICY` is the single case where a borrowed-name
+   pseudo-object shares both a priority *and* an `objType` with a genuinely 
named object.
+
+Two near misses are worth a note rather than a change, and both were refuted 
with
+structural arguments rather than merely not reproduced:
+
+* `DO_INDEX` takes its namespace from its table rather than from 
`pg_class.relnamespace`,
+  and has no tiebreaker.  Today an index's `relnamespace` is pinned to its 
table's, so
+  `(namespace, name)` still reduces to `pg_class_relname_nsp_index`; the sort 
key is one
+  line narrower than the natural key, but nothing can exploit it.
+* the pseudo-objects built with `catId.tableoid = 0, catId.oid = 0`
+  (`DO_TABLE_ATTACH`, `DO_INDEX_ATTACH`, `DO_REL_STATS`) have no OID for the
+  `oidcmp()` safety net to fall back on, so if a future change did introduce a 
tie among
+  them, the fall-through would return 0 and the order would be pure `qsort` 
luck rather
+  than merely OID-dependent.
+
+---
+
+## Part 2 -- dependency-loop repair
+
+### D2. `TopoSort()` reports its failures in dumpId order, so loop repair 
follows OID assignment
+
+When the dependency graph has a cycle, `TopoSort()` fails and hands the 
objects it could
+not place to `findDependencyLoops()`, which finds a cycle and calls
+`repairDependencyLoop()` to break it -- by marking one object `separate`, so 
that (for
+example) a `CHECK` constraint moves out of `CREATE TABLE` into a post-data
+`ALTER TABLE ... ADD CONSTRAINT`, or one view of a mutually-recursive pair is 
emitted as a
+dummy `SELECT NULL::...` placeholder and rebuilt later with `CREATE OR REPLACE 
VIEW`.
+
+Which object gets chosen is decided by OID assignment order, not by name.  
Three links:
+
+```c
+/* pg_dump_sort.c:757 -- the failure list is rebuilt in dumpId order, 
discarding
+ * the name-sorted order the caller passed in */
+k = 0;
+for (j = 1; j <= maxDumpId; j++)
+{
+       if (beforeConstraints[j] != 0)
+               ordering[k++] = objs[idMap[j]];
+}
+```
+
+`findDependencyLoops()` then walks that array front to back, so `loop[0]` is 
the
+lowest-dumpId cycle member; and `repairDependencyLoop()`'s multi-object 
branches scan
+`loop[]` front to back and repair the *first* member of the type they are 
looking for.
+dumpIds are handed out by `AssignDumpId()` in catalog-scan order, and the 
scans are
+OID-ordered (`getTables()` ends `ORDER BY c.oid`; `getTypes()` and 
`getFuncs()` have no
+`ORDER BY` at all, so heap order).  The whole repair decision therefore rides 
on which
+object was created first.
+
+Four statements, differing only in which of two domains is created first:
+
+```sql
+-- A                                    -- B
+CREATE DOMAIN d1 AS int;                CREATE DOMAIN d2 AS int;
+CREATE DOMAIN d2 AS int;                CREATE DOMAIN d1 AS int;
+ALTER DOMAIN d1 ADD CONSTRAINT c1 CHECK ((CAST(VALUE AS int)::d2) IS NOT NULL);
+ALTER DOMAIN d2 ADD CONSTRAINT c2 CHECK ((CAST(VALUE AS int)::d1) IS NOT NULL);
+```
+
+```
+$ diff -u a.dump b.dump
+-CREATE DOMAIN public.d2 AS integer
+-      CONSTRAINT c2 CHECK (((VALUE)::public.d1 IS NOT NULL));
++CREATE DOMAIN public.d1 AS integer
++      CONSTRAINT c1 CHECK (((VALUE)::public.d2 IS NOT NULL));
+-ALTER DOMAIN public.d1
+-    ADD CONSTRAINT c1 CHECK (((VALUE)::public.d2 IS NOT NULL));
++ALTER DOMAIN public.d2
++    ADD CONSTRAINT c2 CHECK (((VALUE)::public.d1 IS NOT NULL));
+```
+
+A puts `c1` in a separate `ALTER DOMAIN` and inlines `c2`; B does the 
opposite.  No
+assertion fires; the divergence is silent.  The verification agent checked 
that the two
+databases are logically identical by projecting the whole catalog -- including 
the entire
+`pg_depend` graph with every OID rendered as 
`regclass`/`regprocedure`/`regtype` -- and
+diffing: no output.  The same instability was demonstrated through four 
different repair
+paths (a table `CHECK` constraint via `BEGIN ATOMIC` functions, a domain 
`CHECK`
+constraint, the dummy-view choice in a view/rule cycle, and which column 
`DEFAULT` is split
+into a separate `ALTER TABLE ... SET DEFAULT`), and in one variant the dump 
flipped with
+*every OID identical* -- so dumpId order, not OID order as such, is the real 
input.
+
+**No sample fix is proposed for D2 and no test is committed for it.**  The 
natural fix has
+two parts -- make `TopoSort()`'s failure list inherit the caller's name-sorted 
order, and
+make `repairDependencyLoop()` pick the minimum by natural key rather than the 
first in
+`loop[]` order -- and both change which object gets broken out in existing 
cases, i.e. they
+change dump output for databases that dump fine today.  That is a judgement 
call about
+`pg_dump`'s output, not a mechanical key completion, so it is written up here 
and left to
+you.  A test pinned to today's choice would only entrench the OID dependence; 
a test
+pinned to the fixed choice presumes the fix.
+
+---
+
+## Part 3 -- dump-time queries whose result order reaches the output
+
+Nine of the eleven findings are of one shape: a query whose rows are pasted 
into the dump
+in result order, ordered by less than a key -- or not ordered at all.  None of 
them reaches
+`DOTypeNameCompare()`, so the tie and shuffle oracles are silent on all nine; 
each was
+established by reading the query, checking the plan, and diffing two 
independently built
+databases.  They are listed worst first.
+
+### D3. `getInherits()` never reads `inhseqno`, and this permutes columns on 
restore
+
+```c
+/* src/bin/pg_dump/pg_dump.c:7696 */
+appendPQExpBufferStr(query, "SELECT inhrelid, inhparent FROM pg_inherits");
+```
+
+No `ORDER BY`, and `pg_dump` reads `inhseqno` **nowhere** -- `grep -rn inhseqno
+src/bin/pg_dump/` returns nothing.  `flagInhTables()` appends parents in 
`PGresult` order
+(`common.c:323`) and nothing re-sorts, so the `INHERITS (...)` list at 
`pg_dump.c:17454`
+and the `--binary-upgrade` `ALTER TABLE ONLY ... INHERIT` at `pg_dump.c:17764` 
both follow
+`pg_inherits` **heap** order.  `pg_inherits`'s natural key is `(inhrelid, 
inhseqno)`.
+
+Heap order diverges from `inhseqno` order as soon as a line pointer is reused, 
and it also
+just differs with creation order when other children's rows are interleaved.  
Seven
+statements, all ordinary DDL:
+
+```sql
+CREATE TABLE p1 (a int);
+CREATE TABLE p2 (b int);
+CREATE TABLE decoy () INHERITS (p1);
+CREATE TABLE ch (b int) INHERITS (p1);
+DROP TABLE decoy;
+VACUUM pg_inherits;
+ALTER TABLE ch INHERIT p2;
+```
+
+The catalog now says the parent order is `p1` then `p2`, and `ch`'s columns 
are `(a, b)`
+accordingly, but the two rows sit in the heap the other way round:
+
+```
+ ctid  | inhparent | inhseqno            attnum | attname
+-------+-----------+----------          --------+---------
+ (0,1) | p2        |        2                 1 | a
+ (0,2) | p1        |        1                 2 | b
+```
+
+and `pg_dump` emits the heap order:
+
+```sql
+CREATE TABLE public.ch (
+    b integer
+)
+INHERITS (public.p2, public.p1);
+```
+
+Restoring that gives `ch` the columns of `p2` first.  **The column order 
changes:**
+
+```
+== ORIGINAL ch columns:        == RESTORED ch columns:
+ 1 | a                          1 | b
+ 2 | b                          2 | a
+```
+
+This is not a spurious-schema-diff problem.  A restored database in which a 
table's columns
+have swapped positions breaks `SELECT *`, `INSERT` without a column list, and 
every client
+that binds by position -- silently, with no error anywhere in the dump or the 
restore.
+(`pg_dump`'s own `COPY` statements carry explicit column lists, so the *data* 
lands in the
+right columns; it is the schema that moves.)  Ordering the query by 
`(inhrelid, inhseqno)`
+fixes both the instability and the wrong restore, and needs no other change 
because
+`flagInhTables()` preserves `PGresult` order.
+
+### D4. `dumpOpfamily()` and `dumpOpclass()` order members by strategy alone
+
+```c
+/* pg_dump.c, dumpOpfamily(): both member queries */
+... "ORDER BY amopstrategy",     /* pg_amop  */
+... "ORDER BY amprocnum",        /* pg_amproc */
+```
+
+`pg_amop`'s key is `(amopfamily, amoplefttype, amoprighttype, amopstrategy)` 
and
+`pg_amproc`'s is `(amprocfamily, amproclefttype, amprocrighttype, amprocnum)`. 
 Within one
+family, every cross-type member pair shares a strategy number, so the sort key 
is not a
+key at all and the remaining order is the executor's -- which the judge traced 
to an index
+scan on `pg_depend`, i.e. ascending member OID.  Adding the same two support 
functions in
+the opposite order permanently changes the dump:
+
+```sql
+CREATE OPERATOR FAMILY myfam USING btree;
+ALTER OPERATOR FAMILY myfam USING btree ADD FUNCTION 1 btint4cmp(int4, int4);
+ALTER OPERATOR FAMILY myfam USING btree ADD FUNCTION 1 btint8cmp(int8, int8);
+-- versus the same two ADDs in the opposite order
+```
+
+```
+$ diff -u a.dump b.dump
+ ALTER OPERATOR FAMILY public.myfam USING btree ADD
+-    FUNCTION 1 (integer, integer) btint4cmp(integer,integer) ,
+-    FUNCTION 1 (bigint, bigint) btint8cmp(bigint,bigint);
++    FUNCTION 1 (bigint, bigint) btint8cmp(bigint,bigint) ,
++    FUNCTION 1 (integer, integer) btint4cmp(integer,integer);
+```
+
+Deterministic, and it reproduces on every run.  The `pg_amop` half of the same 
query pair
+has the identical missing key columns; the audit could not make the operator 
list flip
+(the plan it gets happens to be insensitive to insertion order), so that half 
is reported
+as latent rather than demonstrated.  `dumpOpclass()`'s two queries are also 
latent for a
+different reason: only members whose left and right types both equal 
`opcintype` depend on
+the opclass rather than the family, so today at most one member per strategy 
reaches them
+-- access methods without an `amadjustmembers` hook are where that could stop 
holding.
+
+### D5. `getPolicies()` builds the `TO` role list with an unordered sub-select
+
+```c
+/* pg_dump.c:4278 */
+"CASE WHEN pol.polroles = '{0}' THEN NULL ELSE "
+"  pg_catalog.array_to_string(ARRAY(SELECT pg_catalog.quote_ident(rolname) "
+"                                   from pg_catalog.pg_roles "
+"                                   WHERE oid = ANY(pol.polroles)), ', ') END 
AS polroles, "
+```
+
+The `ARRAY()` sub-select has no `ORDER BY` and plans as a seq scan on 
`pg_authid`, so the
+list follows role **creation** order -- neither the stored `polroles` array 
order (which
+`policy_role_list_to_array()` preserves from the `CREATE POLICY` text) nor 
`rolname` order.
+Two databases whose roles were created in the opposite order dump
+`CREATE POLICY p ON t TO alice, bob` versus `... TO bob, alice`.  Ordering by 
each OID's
+position within `pol.polroles` (`unnest ... WITH ORDINALITY`) both stabilises 
it and makes
+the clause a faithful round trip of what the user wrote.
+
+### D6. `getPublications()` does not order the `FOR ALL TABLES EXCEPT` list
+
+```c
+/* pg_dump.c:4598, per publication, remoteVersion >= 190000 */
+"SELECT prrelid\n"
+"FROM pg_catalog.pg_publication_rel\n"
+"WHERE prpubid = %u AND prexcept"
+```
+
+The rows go into a `SimplePtrList` in arrival order and `dumpPublication()` 
walks it
+verbatim, so `CREATE PUBLICATION p FOR ALL TABLES EXCEPT (TABLE ONLY a, TABLE 
ONLY b)`
+follows `pg_publication_rel` heap order -- i.e. the order the tables were 
listed when the
+publication was created.  Three statements per database reproduce it.  This 
one is new
+code (v19), which makes it the cheapest of the nine to fix before it ships in 
a release.
+
+### D7. `dumpDatabaseConfig()` does not order per-role database settings
+
+```c
+/* pg_dump.c:3764 */
+"SELECT rolname, unnest(setconfig) FROM pg_db_role_setting s, pg_roles r "
+"WHERE setrole = r.oid AND setdatabase = '%u'::oid"
+```
+
+One row per `(role, database)`, no `ORDER BY`, and the plan seq-scans 
`pg_authid` on the
+probe side, so the `ALTER ROLE ... IN DATABASE ... SET` lines in a `--create` 
preamble come
+out in role-OID order.  `ORDER BY 1` (`rolname`) is a complete key here, and 
the
+verification agent checked the one thing that could have gone wrong -- that 
sorting above
+the set-returning `unnest` does not permute the settings *within* a role -- by 
confirming
+the planner puts the sort below the `ProjectSet`.
+
+### D8. `append_depends_on_extension()` does not order its rows
+
+The query behind `ALTER ... DEPENDS ON EXTENSION` (`pg_dump.c:5702`) has no 
`ORDER BY`, so
+an object with two extension dependencies emits them in `pg_depend` row order 
-- the order
+the `ALTER ... DEPENDS ON EXTENSION` statements happened to run, and it 
changes if one is
+dropped and re-added.  Affects every caller (`dumpFunc()`, `dumpTrigger()`, 
index and
+materialized-view paths).  `ORDER BY 1` on the extension name is a complete 
key.
+
+### D9. `collectSecLabels()` omits `provider` from its `ORDER BY`
+
+```c
+/* pg_dump.c:16755 */
+"SELECT label, provider, classoid, objoid, objsubid "
+"FROM pg_catalog.pg_seclabels ORDER BY classoid, objoid, objsubid"
+```
+
+`pg_seclabel`'s key is `(objoid, classoid, objsubid, provider)`.  Two 
providers labelling
+one object produce two rows that tie under that `ORDER BY`, and the server's 
sort is not
+stable, so the two `SECURITY LABEL FOR ...` statements come out in an order 
the catalog
+does not determine.  `collectSecLabels()` is one of two sites; the 
shared-object path in `dumputils.c` has the
+same gap.  Reaching it needs two registered label providers, which no in-tree 
module
+supplied, so the committed test adds a second provider to
+`src/test/modules/dummy_seclabel`, whose whole purpose is exercising this 
machinery.
+
+### D10. `pg_dumpall`'s `dumpTablespaces()` orders by OID
+
+```c
+/* pg_dumpall.c:1368 */
+"SELECT oid, spcname, ... FROM pg_catalog.pg_tablespace "
+"WHERE spcname !~ '^pg_' "
+"ORDER BY 1"          /* select-list column 1 is oid */
+```
+
+Select-list column 1 is `oid`, so the whole per-tablespace block -- `CREATE 
TABLESPACE`,
+`ALTER TABLESPACE ... SET`, the ACL commands, `COMMENT`, `SECURITY LABEL` -- 
is emitted in
+OID order.  That this is an off-by-one rather than intent is clear from the 
sibling
+`dumpRoles()` at `pg_dumpall.c:855`, which has the identical select-list shape
+(`SELECT oid, rolname, ...`) and says `ORDER BY 2`.  `spcname` alone is a 
complete key
+(`pg_tablespace_spcname_index` is unique and `pg_tablespace` has no namespace).
+
+### D11. `dumpExtension()` emits the `requires` array in dependency-array order
+
+Under `--binary-upgrade`, `dumpExtension()` builds the seventh argument of
+`binary_upgrade_create_empty_extension()` by walking 
`extinfo->dobj.dependencies[]` and
+printing each `DO_EXTENSION` it finds (`pg_dump.c:11992`).  Nothing ever sorts 
a
+`dependencies[]` array: `getDependencies()` ends `ORDER BY 1,2` -- `(classid, 
objid)`, with
+`refobjid` absent -- so all of one extension's requires-rows tie and arrive in 
scan order,
+which the backend wrote in *descending referenced-OID* order
+(`eliminate_duplicate_dependencies()` &rarr; `object_address_comparator()`, 
"Primary sort
+key is OID descending").  Two statements per database:
+
+```sql
+CREATE EXTENSION plperl;   CREATE EXTENSION hstore_plperl CASCADE;   -- 
ARRAY['hstore','plperl']
+CREATE EXTENSION hstore;   CREATE EXTENSION hstore_plperl CASCADE;   -- 
ARRAY['plperl','hstore']
+```
+
+Only four in-tree control files list more than one `requires` entry
+(`hstore_plperl`, `hstore_plperlu`, `hstore_plpython3u`, `ltree_plpython3u`), 
so the
+reachable surface is narrow, but this is on the `pg_upgrade` path, which is 
where the
+dump-comparison test lives.
+
+---
+
+## Considered and rejected
+
+### Array-valued catalog columns reproduced verbatim -- *mostly* not a defect
+
+This class was put to a dedicated adjudicator after the first workflow's 
judges split
+three-to-one on it.  Its verdict: the two demonstrated cases are **not** 
defects, but the
+sweep had stopped one array short, and that one **is** -- see D12 below.
+
+#### The two demonstrated cases
+
+Several catalog columns are arrays whose element order is an artifact of the 
order the DDL
+was issued, and `pg_dump` reproduces that order.  The audit demonstrated it 
twice:
+
+```sql
+GRANT SELECT ON acl_t TO r_aaa;   GRANT SELECT ON acl_t TO r_bbb;
+-- versus the same two GRANTs in the opposite order
+```
+
+```
+=== A ===                                        === B ===
+GRANT SELECT ON TABLE public.acl_t TO r_aaa;     GRANT SELECT ON TABLE 
public.acl_t TO r_bbb;
+GRANT SELECT ON TABLE public.acl_t TO r_bbb;     GRANT SELECT ON TABLE 
public.acl_t TO r_aaa;
+```
+
+and the same for `pg_db_role_setting.setconfig` under `ALTER DATABASE ... 
SET`.  Four
+agents split three-to-one on whether this belongs in the findings list.  It 
does not, for
+three reasons, the third of which is decisive:
+
+1. **It inverts the defect definition.**  The other findings are: identical 
catalog
+   content, different OIDs, different output.  This is: *different* catalog 
content
+   (`relacl` genuinely holds a different array value), identical OIDs, 
different output.
+   `pg_dump` is reporting the catalog, not choosing an order.
+2. **It is a fixed point.**  Dump, restore, dump again: the second dump equals 
the first.
+   None of the harms that motivate this class occur -- no `Assert`, no tie, no
+   `002_pg_upgrade.pl` mismatch.
+3. **`buildACLCommands()`'s order is load-bearing.**  With a `WITH GRANT 
OPTION` chain, a
+   grant must be replayed after the grant that authorised it.  A naive sort of 
the aclitem
+   list would produce a dump that **fails to restore**.  Whatever is done here 
cannot be a
+   plain sort.
+
+The same argument covers every `*acl` column `pg_dump` feeds to 
`buildACLCommands()`
+(`relacl`, `typacl`, `proacl`, `nspacl`, `defaclacl`, `lanacl`, `fdwacl`, 
`srvacl`,
+`datacl`, `spcacl`, `lomacl`, parameter ACLs in `pg_dumpall`) and column-level 
ACLs, and it
+covers `reloptions`, `proconfig` and `attoptions` for reason 1 alone.  
Recorded here so it
+is not re-proposed.
+
+### Checked and found clean
+
+* **The core regression database** (2291 relations) -- no ties, and 
byte-identical dump
+  output across eight pre-sort shuffles.  Also clean under `--with-statistics`,
+  `--no-owner`, `--no-privileges`, `--section=*`, `--schema-only`, 
`--data-only` and
+  `--binary-upgrade`.
+* **`TopoSort()` itself** -- given a fixed input order and a fixed dependency 
graph, its
+  output is deterministic; the binary heap is keyed on the input index.  The 
instability in
+  D2 is in what feeds it on failure, not in the sort.
+* **The archive TOC** -- `-Fc` TOC order and single-threaded `pg_restore -f -` 
output
+  follow the same sorted list as the plain dump.  (`pg_restore -j` 
deliberately does not,
+  as the comment above `Assert(false)` already says.)
+* **The rest of `pg_dumpall.c`** -- roles, role memberships, role GUC 
settings, databases
+  and subscriptions are all ordered by name; `dumpTablespaces()` (D10) is the 
only one that
+  is not.
+* **`getDependencies()`'s `ORDER BY 1,2`** -- incomplete as a key, but the 
only place a
+  `dependencies[]` array's order reaches the output is D11.
+* **The comparator's helper functions** -- `pgTypeNameCompare()` compares
+  `(nspname, typname)`, `accessMethodNameCompare()` compares `amname`; both 
are complete
+  for their catalogs, and both handle the not-found case by returning "equal" 
so the caller
+  falls through to its next basis for comparison.
+* **Comments** -- `collectComments()` orders by `(classoid, objoid, 
objsubid)`, which is
+  `pg_description`'s whole key; only the security-label sibling (D9) has a 
fourth key
+  column.
+
+---
+
+## Tests and sample fixes on this branch
+
+Eleven of the twelve findings have both a regression test and a sample fix.  
D2 has
+neither, for the reason given in Part 2.
+
+| # | Test | Sample fix |
+|---|---|---|
+| D1 | `002_pg_dump.pl`, policy named after its own table | `pg_dump_sort.c`: 
compare `polname == NULL` after the table name |
+| D3 | `002_pg_dump.pl`, `inh_order_child` | `pg_dump.c`: `ORDER BY inhrelid, 
inhseqno` |
+| D4 | `002_pg_dump.pl`, `op_family` | `pg_dump.c`: add the member type names 
to all four member queries |
+| D5 | `002_pg_dump.pl`, policy `p7` with a multi-role `TO` list | 
`pg_dump.c`: `unnest(polroles) WITH ORDINALITY` |
+| D6 | `002_pg_dump.pl`, publications `pub9`/`pub10` | `pg_dump.c`: `ORDER BY 
n.nspname, c.relname` |
+| D7 | `002_pg_dump.pl`, `ALTER ROLE ... IN DATABASE` | `pg_dump.c`: `ORDER BY 
rolname` |
+| D8 | `test_pg_dump/t/001_base.pl` | `pg_dump.c`: `ORDER BY e.extname` |
+| D9 | `003_pg_dump_with_server.pl` (+ a second provider in `dummy_seclabel`) 
| `pg_dump.c`, `dumputils.c`: add `provider` to both `ORDER BY`s |
+| D10 | `002_pg_dump.pl`, `CREATE TABLESPACE in name order` | `pg_dumpall.c`: 
`ORDER BY 1` &rarr; `ORDER BY 2` |
+| D11 | `003_pg_dump_with_server.pl` | `pg_dump.c`: sort the requires names 
with `pg_qsort_strcmp` |
+| D12 | `002_pg_dump.pl`, `ALTER DEFAULT PRIVILEGES grantees ... in name 
order` | `pg_dump.c`: re-sort `defaclacl` by aclitem text under `COLLATE "C"` |
+
+**The sample fixes are not proposed patches.**  They exist so the branch is 
coherent -- the
+tests need something to pass against -- and so that "this test fails without 
the fix" is a
+statement someone can check.  They are in their own commit and can be dropped 
wholesale.
+Four of them involve a judgement a committer should make rather than accept:
+
+* **D5** could instead be `ORDER BY rolname`.  The committed fix preserves the 
order the
+  user wrote in `CREATE POLICY`, which round-trips; alphabetical order would 
be simpler but
+  would rewrite the clause.  Both remove the OID dependence.
+* **D4** orders by the members' type names.  Ordering by `regtype` output 
would have been
+  shorter, but that rendering depends on `search_path`, so the fix joins 
`pg_type` and
+  `pg_namespace` and orders by `(nspname, typname)` -- the same key
+  `pgTypeNameCompare()` uses.
+* **D1** sorts the RLS-enable pseudo-object *before* the policies on its 
table.  Either
+  order is stable; this one matches `ENABLE ROW LEVEL SECURITY` logically 
preceding them.
+* **D12** sorts an ACL array, which the sibling `relacl` case shows can be 
unsafe.  The
+  argument that it is safe *here* -- a default ACL's items all share one 
grantor, so there
+  is no grant chain to replay in order -- is the whole basis of the fix, and 
is the thing
+  to check before accepting it.
+
+Two findings needed test infrastructure rather than just a test entry.  D8 
lives in
+`src/test/modules/test_pg_dump` because showing it needs one object with 
**two** extension
+dependencies, and a bare `initdb` has exactly one extension (`plpgsql`); 
`src/bin/pg_dump`'s
+test install does not build contrib, so a test in `002_pg_dump.pl` would have 
to make the
+core pg_dump suite depend on contrib.  `test_pg_dump` already installs its own 
extension
+and already owns the only existing `DEPENDS ON EXTENSION` coverage.  D11 
sidesteps the same
+problem differently: its test writes three throwaway control files into the 
test's temp
+directory and points `extension_control_path` at them, so it needs no contrib 
at all.
+
+## Verification
+
+Three runs of `meson test --suite setup --suite pg_dump --suite test_pg_dump 
--suite
+dummy_seclabel`, on this branch, in this order.
+
+**1. Everything applied: 13/13 pass**, including `002_pg_dump` with 13697 
subtests.
+
+**2. All five product files reverted, tests kept: 3 suites fail.**  
`002_pg_dump` dies
+early:
+
+```
+# pg_dump: ../ss-audit/src/bin/pg_dump/pg_dump_sort.c:511: DOTypeNameCompare: 
Assertion `0' failed.
+#   Failed test 'binary_upgrade: pg_dump runs'
+```
+
+That is D1 doing what it should -- and it is also why this run alone is not 
enough: the
+abort kills the dump before the ordering tests can be evaluated.
+
+**3. Only D1's fix applied, the other ten reverted: 3 suites fail, each test 
by its own
+name.**  `002_pg_dump` now runs to completion and fails on exactly the new 
entries:
+
+```
+should dump CREATE TABLE inh_order_child                          (D3)
+should dump CREATE TABLE inh_order_child pg_upgrade               (D3)
+should dump ALTER OPERATOR FAMILY dump_test.op_family USING btree (D4)
+should dump CREATE POLICY p7 ON test_table with a multi-role TO list (D5)
+should dump CREATE PUBLICATION pub9 / pub10                       (D6)
+should dump ALTER ROLE ... IN DATABASE postgres SET, in role name order (D7)
+should dump CREATE TABLESPACE in name order                       (D10)
+should dump ALTER DEFAULT PRIVILEGES grantees are dumped in name order (D12)
+```
+
+`003_pg_dump_with_server` reports "failed 3 tests of 12" (D9 and D11), and
+`test_pg_dump/001_base` fails (D8).  Every committed test fails for its own 
reason on the
+unfixed tree.
+
+Separately, each finding was re-checked outside the TAP suite by building the 
two databases
+the report describes and diffing the dumps with the unfixed and the fixed 
binary.  All of
+D4, D5, D6, D7, D8, D11 and D12 go from UNSTABLE to STABLE; D1 stops aborting; 
D3 emits
+`INHERITS (public.p1, public.p2)` and restores the child with its columns in 
the original
+order; D10 emits the tablespaces in name order.
+
+**What is still unverified.**  The completeness critic that examined the D1 
claim -- 48
+object types, 55 construction sites, five SQL corpora each under 17 pg_dump 
option sets,
+all 59 contrib extensions, plus the regression database -- returned "claim 
holds", and
+named what it could not reach: cross-version dumps (pg_dump's older-server 
query branches),
+`DO_SUBSCRIPTION_REL`, multi-encoding collations, and catalog corruption.  D2 
is reported
+without a fix or a test by choice.  Nothing else on this branch is unverified.
+
+### D12. `getDefaultACLs()`: `defaclacl` is emitted in grantee-OID order
+
+This one came out of the adjudication above, not out of discovery: the agent 
sent to settle
+whether array order is ever a defect reproduced both demonstrated cases, 
agreed they are
+not, and then checked the arrays the sweep had not.  
`pg_default_acl.defaclacl` is a
+different animal:
+
+```sql
+CREATE ROLE r_aaa;  CREATE ROLE r_bbb;                     -- database A
+ALTER DEFAULT PRIVILEGES GRANT SELECT ON TABLES TO r_aaa;
+ALTER DEFAULT PRIVILEGES GRANT SELECT ON TABLES TO r_bbb;
+-- database B: identical, only the two CREATE ROLE lines swapped
+```
+
+```
+D12-defaclacl/OLD: UNSTABLE
+    -ALTER DEFAULT PRIVILEGES FOR ROLE postgres GRANT SELECT ON TABLES TO 
r_aaa;
+    +ALTER DEFAULT PRIVILEGES FOR ROLE postgres GRANT SELECT ON TABLES TO 
r_aaa;
+D12-defaclacl/NEW: STABLE (dumps identical)
+```
+
+The reason it is a defect where `relacl` is not: the backend **throws the DDL 
order away**.
+`ExecGrant_Default_Acl()` canonicalizes the array with `aclitemsort()`, which 
orders by
+grantee OID.  So the stored order is not "what the user wrote", it is a 
function of role
+OIDs -- and a restore into a cluster that assigns different role OIDs produces 
a different
+canonical order.  That also removes the objection that killed the `relacl` 
case: a default
+ACL cannot contain a chain of grants by different grantors (every item's 
grantor is
+`defaclrole`), so `buildACLCommands()`'s load-bearing replay order does not 
apply and
+sorting is safe.
+
+Sorting by the aclitem's text under `COLLATE "C"` in `getDefaultACLs()` fixes 
it.
+
+The same adjudicator's negative results are worth as much as the finding, and 
are why the
+`relacl` and `setconfig` cases stay rejected: it fuzzed 160 tables, 20 
functions, 10 schemas
+and 10 types with random `GRANT`/`REVOKE` histories, non-owner grantors, 
`PUBLIC`, column
+privileges and three grant-option holders, then dumped, restored and re-dumped 
-- byte
+identical.  It also ran a real `pg_upgrade` and confirmed that although the 
catalog arrays
+*are* rewritten, `pg_dump` already normalizes around it (it drops items 
matching
+`acldefault` and hoists owner self-grants into `firstsql`), so the dump 
comparison passes.
+And it confirmed that `relacl` order really is load-bearing, by replaying a 
grant chain in
+grantee-name order and getting `ERROR: permission denied for table t5`.
+
+---
+
+## Appendix A -- the instrumented build
+
+Applied to `sortDumpableObjectsByTypeName()` in `pg_dump_sort.c` for the audit 
build only;
+never committed.
+
+```c
+       /* PGDUMP_SHUFFLE_SEED=N: permute the array before sorting. */
+       {
+               const char *seedstr = getenv("PGDUMP_SHUFFLE_SEED");
+
+               instr_tie_zero = (getenv("PGDUMP_TIE_ZERO") != NULL);
+               if (seedstr != NULL && numObjs > 1)
+               {
+                       srand((unsigned int) atoi(seedstr));
+                       for (int i = numObjs - 1; i > 0; i--)
+                       {
+                               int                     j = rand() % (i + 1);
+                               DumpableObject *tmp = objs[i];
+
+                               objs[i] = objs[j];
+                               objs[j] = tmp;
+                       }
+               }
+       }
+
+       if (numObjs > 1)
+               qsort(objs, numObjs, sizeof(DumpableObject *), 
DOTypeNameCompare);
+
+       /* PGDUMP_TIE_REPORT=1: report every adjacent pair that reached the
+        * comparator's fall-through.  Ties are adjacent after a sort, so this
+        * enumerates all of them. */
+       if (getenv("PGDUMP_TIE_REPORT") != NULL)
+       {
+               for (int i = 1; i < numObjs; i++)
+               {
+                       instr_tie_fallthrough = false;
+                       DOTypeNameCompare(&objs[i - 1], &objs[i]);
+                       if (instr_tie_fallthrough)
+                       {
+                               char            buf1[512], buf2[512];
+
+                               describeDumpableObject(objs[i - 1], buf1, 
sizeof(buf1));
+                               describeDumpableObject(objs[i], buf2, 
sizeof(buf2));
+                               fprintf(stderr, "SORT TIE: objType %d name 
\"%s\" nsp \"%s\" | %s | %s\n",
+                                               (int) objs[i]->objType, 
objs[i]->name,
+                                               objs[i]->namespace ? 
objs[i]->namespace->dobj.name : "(none)",
+                                               buf1, buf2);
+                       }
+               }
+       }
+```
+
+and, in `DOTypeNameCompare()`, the fall-through becomes
+
+```c
+       instr_tie_fallthrough = true;
+       if (instr_tie_zero)
+               return 0;
+       return oidcmp(obj1->catId.oid, obj2->catId.oid);
+```
+
+**The first version of this was wrong and reported nothing**, because disarming
+`Assert(false)` left the fall-through returning `oidcmp()`, so the reporter's
+"did these two compare equal?" test never fired.  It was caught only by 
running the
+detector against a defect already known to be present.  A detector that 
silently finds
+nothing is the failure mode that would have turned this report into "no 
defects", so
+calibrate any replacement the same way.
+
+## Appendix B -- minimal reproducers
+
+Each is a complete `.sql` for a fresh database.  Where a finding is a 
two-database
+comparison, both variants are given; dump each with the stated options and 
diff, after
+normalizing pg_dump's random `\restrict` token
+(`sed -E 's/^(\\(un)?restrict) [A-Za-z0-9]+$/\1 XXX/'`).
+
+```sql
+-- D1: assert-enabled pg_dump aborts.
+CREATE TABLE pol_t (i int);
+ALTER TABLE pol_t ENABLE ROW LEVEL SECURITY;
+CREATE POLICY pol_t ON pol_t USING (true);
+
+-- D2: two databases, only the two CREATE DOMAIN lines swapped.
+CREATE DOMAIN d1 AS int;
+CREATE DOMAIN d2 AS int;
+ALTER DOMAIN d1 ADD CONSTRAINT c1 CHECK ((CAST(VALUE AS int)::d2) IS NOT NULL);
+ALTER DOMAIN d2 ADD CONSTRAINT c2 CHECK ((CAST(VALUE AS int)::d1) IS NOT NULL);
+
+-- D3: one database.  Dump, restore, and compare ch's column order.
+CREATE TABLE p1 (a int);
+CREATE TABLE p2 (b int);
+CREATE TABLE decoy () INHERITS (p1);
+CREATE TABLE ch (b int) INHERITS (p1);
+DROP TABLE decoy;
+VACUUM pg_inherits;
+ALTER TABLE ch INHERIT p2;
+
+-- D4: two databases, the two ADD FUNCTION lines swapped.
+CREATE OPERATOR FAMILY myfam USING btree;
+ALTER OPERATOR FAMILY myfam USING btree ADD FUNCTION 1 btint4cmp(int4, int4);
+ALTER OPERATOR FAMILY myfam USING btree ADD FUNCTION 1 btint8cmp(int8, int8);
+
+-- D5: two databases, the two CREATE ROLE lines swapped.
+CREATE ROLE alice NOLOGIN;  CREATE ROLE bob NOLOGIN;
+CREATE TABLE t (a int);
+CREATE POLICY p ON t TO alice, bob USING (true);
+
+-- D6: two databases, the EXCEPT list written in the opposite order.
+CREATE TABLE ta (x int);  CREATE TABLE tb (x int);
+CREATE PUBLICATION p FOR ALL TABLES EXCEPT (TABLE ta, TABLE tb);
+
+-- D7: two databases, the two CREATE ROLE lines swapped.  Dump with --create.
+CREATE ROLE ra NOLOGIN;  CREATE ROLE rb NOLOGIN;
+ALTER ROLE ra IN DATABASE postgres SET work_mem='5MB';
+ALTER ROLE rb IN DATABASE postgres SET work_mem='6MB';
+
+-- D8: two databases, the two ALTER TRIGGER lines swapped.
+CREATE EXTENSION cube;
+CREATE TABLE t (a int);
+CREATE TRIGGER tg BEFORE UPDATE ON t FOR EACH ROW
+    EXECUTE FUNCTION suppress_redundant_updates_trigger();
+ALTER TRIGGER tg ON t DEPENDS ON EXTENSION plpgsql;
+ALTER TRIGGER tg ON t DEPENDS ON EXTENSION cube;
+
+-- D9: needs two registered label providers; see the committed test, which 
adds a
+-- second provider to src/test/modules/dummy_seclabel.
+
+-- D10: two clusters, the two CREATE TABLESPACE lines swapped.  pg_dumpall 
--globals-only.
+SET allow_in_place_tablespaces = on;
+CREATE TABLESPACE ts_aaa LOCATION '';
+CREATE TABLESPACE ts_bbb LOCATION '';
+
+-- D11: two databases.  Dump with --binary-upgrade.
+CREATE EXTENSION plperl;   CREATE EXTENSION hstore_plperl CASCADE;   -- 
variant A
+CREATE EXTENSION hstore;   CREATE EXTENSION hstore_plperl CASCADE;   -- 
variant B
+
+-- D12: two databases, the two CREATE ROLE lines swapped.
+CREATE ROLE r_aaa NOLOGIN;  CREATE ROLE r_bbb NOLOGIN;
+ALTER DEFAULT PRIVILEGES GRANT SELECT ON TABLES TO r_aaa;
+ALTER DEFAULT PRIVILEGES GRANT SELECT ON TABLES TO r_bbb;
+```
diff --git a/PROVENANCE.md b/PROVENANCE.md
new file mode 100644
index 0000000..2b49ca3
--- /dev/null
+++ b/PROVENANCE.md
@@ -0,0 +1,126 @@
+# PROVENANCE
+
+Branch `dump-sort-stability-tests` is the output of an automated, model-driven 
audit that
+looked for sources of pg_dump dump-order instability **other than** the one 
fixed by the
+cast/transform patch the branch's first commit carries.  Everything on the 
branch after
+that first commit -- the report, the regression tests, and the SAMPLE fixes -- 
was written
+by a language model.  **No human wrote any of it, and no human has reviewed 
it.**  This
+file records how it was produced, including what went wrong, so a reviewer can 
judge the
+work and reproduce every claim in it.
+
+---
+
+## Tooling
+
+* **Tool:** Claude Code (Anthropic's agentic CLI), using its Workflow feature 
-- a
+  deterministic JavaScript script that drives many subagents.
+* **Model:** Opus 5 (`claude-opus-5`) for the orchestrator and for every 
subagent.
+* **Run by:** the repository owner (Noah Misch), interactively, from
+  `/home/nm/src/pg/postgresql`.
+* **Date:** 2026-09-03.
+* **Human contribution:** the prompt below, and one mid-run question about 
whether work was
+  blocked.  Nothing in the findings, the tests, the fixes or the report came 
from a human.
+
+## The prompt
+
+> ~/sort_cast.patch contains a reasonable-looking fix.  I'm concerned that yet
+> more sources of dump sort instability are still lurking.  Make a worklfow to
+> look for others and, if any found, write test cases covering them.  Use your
+> own worktree; disregard the present dir except as repository to which to
+> attach your worktree.
+>
+> Commit the following on a fresh branch:
+> - A report.  Prefix the report with [no defects] if that's so.
+> - Any tests written
+> - A PROVENANCE.md file containing model, prompt, etc.
+
+## Base
+
+* Upstream `master` at `6885b84` ("doc: Fix link on 
pg_dsm_registry_allocations page.").
+* Commit `885a841` is `~/sort_cast.patch` applied verbatim with `git am`.  It 
is Alexander
+  Kukushkin's patch, **not** model-written; it is on the branch because the 
audit's whole
+  purpose was to find what that patch does not cover.  Everything after it is 
the audit.
+
+## What was built to do the audit
+
+Three artifacts, all outside the branch, under `/home/nm/src/pg/`:
+
+* `ss-audit/` -- the branch worktree.  Agents were given it **read-only**.
+* `ss-inst/` -- a stock assert-enabled install of `885a841`.  Its `pg_dump` 
aborts when
+  `DOTypeNameCompare()` reaches `Assert(false)`, which is the audit's primary 
oracle
+  because it is exactly what a developer or buildfarm animal sees.
+* `ss-instr/` + `ss-shuf-inst/` -- the same commit with audit-only 
instrumentation in
+  `sortDumpableObjectsByTypeName()`: a pre-sort shuffle under 
`PGDUMP_SHUFFLE_SEED`, a
+  complete adjacent-pair tie report under `PGDUMP_TIE_REPORT`, and the 
comparator's
+  `Assert(false)` disarmed (which also makes it a faithful stand-in for a 
production
+  non-assert build).  The instrumentation is reproduced in full in the 
report's appendix.
+  It was **never** committed to the branch.
+* `ssrun/pgrun.sh` -- runs a `.sql` file in a throwaway cluster and dumps it 
in `plain`,
+  `tie` or `shuffle` mode.  `ssrun/regdata/` -- a prepared core-regression 
database
+  (243/243 tests passed, 2291 relations) used as the large realistic corpus.
+
+Calibration before any agent ran: all three oracles fire on a known defect and 
are silent
+on the regression database and on control schemas.
+
+## What the agents did
+
+**Workflow 1 -- discovery** (`wf_7ac43658-438`, 152 agents, 110 completed, 
10.0M subagent
+tokens, 8h04m wall clock).
+
+* 17 discovery agents in parallel: 8 sweeping the 48 `DumpableObjectType` 
values against
+  their catalogs' natural keys, 4 code lenses (the topological sort; every 
dump-time query
+  in `pg_dump.c`; `pg_dumpall.c` and the archive TOC; the history of the five 
commits that
+  already fixed this class), and 5 empirical lanes (cross-schema name 
collisions; pg_dump's
+  manufactured pseudo-objects; in-tree extensions; the regression corpus; a 
differential
+  two-database generator).
+* 54 raw candidates, deduplicated to 49 by key.
+* Each candidate then got an independent verification agent, and each survivor 
an
+  independent adversarial judge whose brief was to **refute** it.  40 survived.
+* Those 40 describe **11 distinct mechanisms**; one mechanism was found 
independently by 13
+  different agents through 13 different object types.
+
+**Workflow 2 -- tests and fixes** (`wf_db8fc3d2-7db`, 12 agents, 12 completed, 
1.8M
+subagent tokens): one agent per confirmed mechanism, each required to 
re-reproduce it from
+scratch before writing its regression test and sample fix, plus two 
adversarial critics --
+one told to falsify the claim that `DO_POLICY` is the only remaining 
comparator tie, one to
+settle a finding the first workflow's judges had split on.
+
+The first critic returned **"claim holds"** after enumerating all 48 
`DumpableObjectType`
+values and their 55 construction sites and attacking the claim with five SQL 
corpora, each
+under 17 pg_dump option sets, plus all 59 contrib extensions and the 
regression database.
+The second **found a twelfth defect** (D12) while refuting the finding it was 
sent to
+adjudicate: the two array-order cases it was given are not defects, but the 
sweep had
+stopped one array short of `pg_default_acl.defaclacl`, which is.  So the final 
count is
+**twelve**, not the eleven the discovery workflow produced.
+
+## What went wrong, and what was done about it
+
+* **The Anthropic API returned 529 Overloaded for about an hour.**  It killed 
workflow 1's
+  entire Tests phase and its completeness critic (42 of the 152 agents), then 
two full
+  launches of workflow 2 (12 agents each, all failing at zero tokens).  **No 
finding was
+  lost** -- discovery, verification and adjudication had all completed -- but 
no test was
+  written until the third launch of workflow 2 succeeded.
+* **The first tie-detector build was wrong and reported nothing.**  Disarming
+  `Assert(false)` left the fall-through returning `oidcmp()`, so the reporter's
+  "did these two compare equal?" test never fired.  Caught by running it 
against a defect
+  known to be present; fixed by having the fall-through set a flag the 
reporter reads.
+  Recorded here because a silent detector is the failure mode that would have 
made this
+  whole audit report "no defects".
+* **The shuffle oracle produced false positives** until `pg_dump`'s random 
`\restrict`
+  token was normalized away before diffing.
+* **The first dedup was too coarse**: 40 confirmed reports collapse to 11 
mechanisms, but
+  the agents chose 40 different key strings, so the Tests phase was sized at 
42 agents when
+  10 would do.  Consolidation was done by hand between the two workflows.
+* **`002_pg_dump.pl` cannot express every finding.**  Which findings got a TAP 
test, which
+  did not, and why, is stated explicitly in the report -- no finding is 
quietly dropped.
+
+## How to check the work
+
+Every finding in the report carries the minimal SQL that produces it.  For a 
comparator
+tie, run it under an assert-enabled `pg_dump` and watch the assertion fire.  
For an
+ordering finding, build the two databases the report gives and diff the dumps. 
 The
+tests on this branch are the same reproducers expressed in `002_pg_dump.pl`; 
each one
+fails on `885a841` and passes with that finding's sample fix applied.
+
+The audit's own verification of the committed artifacts is described at the 
end of the
+report, including the result of reverting the sample fixes and re-running the 
suite.
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 4b719e7..461463f 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -783,6 +783,33 @@ my %tests = (
                unlike => { no_privs => 1, },
        },
 
+       # The backend keeps a pg_default_acl entry's ACL array in grantee-OID 
order
+       # (ExecGrant_Default_Acl canonicalizes it with aclitemsort()), so 
emitting
+       # the GRANTs in array order would make the dump depend on the order the
+       # grantee roles happened to be created in.  Two databases with the same
+       # default privileges must dump alike, and a dump/restore round trip 
must be
+       # order-stable even though the restore assigns new role OIDs.  Create 
these
+       # roles in the reverse of their name order and require name order out.
+       'ALTER DEFAULT PRIVILEGES grantees are dumped in name order' => {
+               create_order => 57,
+               create_sql => 'CREATE ROLE regress_dump_defacl_zzz;
+                                          CREATE ROLE regress_dump_defacl_aaa;
+                                          ALTER DEFAULT PRIVILEGES
+                                          FOR ROLE regress_dump_test_role
+                                          GRANT SELECT ON SEQUENCES
+                                          TO regress_dump_defacl_zzz, 
regress_dump_defacl_aaa;',
+               regexp => qr/^
+                       \QALTER DEFAULT PRIVILEGES \E
+                       \QFOR ROLE regress_dump_test_role \E
+                       \QGRANT SELECT ON SEQUENCES TO 
regress_dump_defacl_aaa;\E\n
+                       \QALTER DEFAULT PRIVILEGES \E
+                       \QFOR ROLE regress_dump_test_role \E
+                       \QGRANT SELECT ON SEQUENCES TO 
regress_dump_defacl_zzz;\E
+                       /xm,
+               like => { %full_runs, section_post_data => 1, },
+               unlike => { no_privs => 1, },
+       },
+
        'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role REVOKE SELECT'
          => {
                create_order => 56,
@@ -815,6 +842,31 @@ my %tests = (
                },
        },
 
+       # dumpDatabaseConfig() must emit these in role name order.  The roles 
are
+       # created in the reverse of that order, and the ALTER ROLE statements 
are
+       # issued in the reverse of that order too, so neither pg_authid OID 
order
+       # nor pg_db_role_setting heap order can produce the expected output by
+       # accident; only an explicit sort on rolname can.
+       'ALTER ROLE ... IN DATABASE postgres SET, in role name order' => {
+               create_order => 28,
+               create_sql => '
+                       CREATE ROLE regress_dump_role_z;
+                       CREATE ROLE regress_dump_role_a;
+                       ALTER ROLE regress_dump_role_z IN DATABASE postgres
+                               SET work_mem = \'7MB\';
+                       ALTER ROLE regress_dump_role_a IN DATABASE postgres
+                               SET work_mem = \'6MB\';',
+               regexp => qr/^
+                       \QALTER ROLE regress_dump_role_a IN DATABASE postgres 
SET work_mem TO '6MB';\E\n
+                       \QALTER ROLE regress_dump_role_z IN DATABASE postgres 
SET work_mem TO '7MB';\E
+                       /xm,
+
+               # These commands live in the DATABASE PROPERTIES entry, which 
only
+               # --create emits.  pg_dumpall passes --create for other 
databases, but
+               # not for "postgres" unless --clean is given too.
+               like => { createdb => 1, },
+       },
+
        'ALTER COLLATION test0 OWNER TO' => {
                regexp => qr/^\QALTER COLLATION public.test0 OWNER TO \E.+;/m,
                collation => 1,
@@ -884,10 +936,10 @@ my %tests = (
                        \QOPERATOR 4 >=(bigint,integer) ,\E\n\s+
                        \QOPERATOR 5 >(bigint,integer) ,\E\n\s+
                        \QFUNCTION 1 (integer, integer) 
btint4cmp(integer,integer) ,\E\n\s+
-                       \QFUNCTION 2 (bigint, bigint) 
btint8sortsupport(internal) ,\E\n\s+
                        \QFUNCTION 2 (integer, integer) 
btint4sortsupport(internal) ,\E\n\s+
-                       \QFUNCTION 4 (bigint, bigint) btequalimage(oid) ,\E\n\s+
-                       \QFUNCTION 4 (integer, integer) btequalimage(oid);\E
+                       \QFUNCTION 2 (bigint, bigint) 
btint8sortsupport(internal) ,\E\n\s+
+                       \QFUNCTION 4 (integer, integer) btequalimage(oid) 
,\E\n\s+
+                       \QFUNCTION 4 (bigint, bigint) btequalimage(oid);\E
                        /xm,
                like =>
                  { %full_runs, %dump_test_schema_runs, section_pre_data => 1, 
},
@@ -2156,6 +2208,30 @@ my %tests = (
                },
        },
 
+       # pg_dumpall must emit tablespaces in name order, not in 
pg_tablespace.oid
+       # order.  These two are created in descending name order, so an 
OID-ordered
+       # dump emits _b before _a.
+       'CREATE TABLESPACE in name order' => {
+               create_order => 2,
+               create_sql => q(
+                   SET allow_in_place_tablespaces = on;
+                       CREATE TABLESPACE regress_dump_tablespace_b
+                       OWNER regress_dump_test_role LOCATION '';
+                       CREATE TABLESPACE regress_dump_tablespace_a
+                       OWNER regress_dump_test_role LOCATION ''),
+               regexp => qr/^
+                       \QCREATE TABLESPACE regress_dump_tablespace_a OWNER 
regress_dump_test_role LOCATION '';\E
+                       .*?
+                       ^\QCREATE TABLESPACE regress_dump_tablespace_b OWNER 
regress_dump_test_role LOCATION '';\E
+                       /xms,
+               like => {
+                       pg_dumpall_dbprivs => 1,
+                       pg_dumpall_exclude => 1,
+                       pg_dumpall_globals => 1,
+                       pg_dumpall_globals_clean => 1,
+               },
+       },
+
        'CREATE DATABASE regression_invalid...' => {
                create_order => 1,
                create_sql => q(
@@ -3227,6 +3303,79 @@ my %tests = (
                },
        },
 
+       # The "RLS is enabled" pseudo-object borrows its table's relname, so it
+       # ties in the sort with a policy of that same name on that same table.
+       # Check that the marker still dumps ahead of the policy.
+       'CREATE POLICY test_table ON test_table' => {
+               create_order => 27,
+               create_sql => 'CREATE POLICY test_table ON dump_test.test_table
+                                                  USING (true);',
+               regexp => qr/^
+                       \QALTER TABLE dump_test.test_table ENABLE ROW LEVEL 
SECURITY;\E\n.+
+                       \QCREATE POLICY test_table ON dump_test.test_table 
USING (true);\E
+                       /xms,
+               like => {
+                       %full_runs,
+                       %dump_test_schema_runs,
+                       only_dump_test_table => 1,
+                       section_post_data => 1,
+               },
+               unlike => {
+                       exclude_dump_test_schema => 1,
+                       exclude_test_table => 1,
+                       no_policies => 1,
+                       no_policies_restore => 1,
+                       only_dump_measurement => 1,
+               },
+       },
+
+       'CREATE POLICY p7 ON test_table with a multi-role TO list' => {
+               create_order => 28,
+               create_sql => 'CREATE ROLE regress_dump_policy_role_a;
+                                          CREATE ROLE 
regress_dump_policy_role_b;
+                                          CREATE POLICY p7 ON 
dump_test.test_table
+                                                  TO 
regress_dump_policy_role_b, regress_dump_policy_role_a
+                                                  USING (true);',
+               regexp => qr/^
+                       \QCREATE POLICY p7 ON dump_test.test_table \E
+                       \QTO regress_dump_policy_role_b, 
regress_dump_policy_role_a \E
+                       \QUSING (true);\E
+                       /xm,
+               like => {
+                       %full_runs,
+                       %dump_test_schema_runs,
+                       only_dump_test_table => 1,
+                       section_post_data => 1,
+               },
+               unlike => {
+                       exclude_dump_test_schema => 1,
+                       exclude_test_table => 1,
+                       no_policies => 1,
+                       no_policies_restore => 1,
+                       only_dump_measurement => 1,
+               },
+       },
+
+       'CREATE ROLE regress_dump_policy_role_a' => {
+               regexp => qr/^CREATE ROLE regress_dump_policy_role_a;/m,
+               like => {
+                       pg_dumpall_dbprivs => 1,
+                       pg_dumpall_exclude => 1,
+                       pg_dumpall_globals => 1,
+                       pg_dumpall_globals_clean => 1,
+               },
+       },
+
+       'CREATE ROLE regress_dump_policy_role_b' => {
+               regexp => qr/^CREATE ROLE regress_dump_policy_role_b;/m,
+               like => {
+                       pg_dumpall_dbprivs => 1,
+                       pg_dumpall_exclude => 1,
+                       pg_dumpall_globals => 1,
+                       pg_dumpall_globals_clean => 1,
+               },
+       },
+
        'CREATE PROPERTY GRAPH propgraph' => {
                create_order => 20,
                create_sql => 'CREATE PROPERTY GRAPH dump_test.propgraph;',
@@ -3323,7 +3472,7 @@ my %tests = (
                create_sql =>
                  'CREATE PUBLICATION pub9 FOR ALL TABLES EXCEPT (TABLE 
dump_test.test_table, dump_test.test_second_table);',
                regexp => qr/^
-                       \QCREATE PUBLICATION pub9 FOR ALL TABLES EXCEPT (TABLE 
ONLY dump_test.test_table, TABLE ONLY dump_test.test_second_table) WITH 
(publish = 'insert, update, delete, truncate');\E
+                       \QCREATE PUBLICATION pub9 FOR ALL TABLES EXCEPT (TABLE 
ONLY dump_test.test_second_table, TABLE ONLY dump_test.test_table) WITH 
(publish = 'insert, update, delete, truncate');\E
                        /xm,
                like => { %full_runs, section_post_data => 1, },
        },
@@ -3333,7 +3482,7 @@ my %tests = (
                create_sql =>
                  'CREATE PUBLICATION pub10 FOR ALL TABLES EXCEPT (TABLE 
dump_test.test_inheritance_parent);',
                regexp => qr/^
-                       \QCREATE PUBLICATION pub10 FOR ALL TABLES EXCEPT (TABLE 
ONLY dump_test.test_inheritance_parent, TABLE ONLY 
dump_test.test_inheritance_child) WITH (publish = 'insert, update, delete, 
truncate');\E
+                       \QCREATE PUBLICATION pub10 FOR ALL TABLES EXCEPT (TABLE 
ONLY dump_test.test_inheritance_child, TABLE ONLY 
dump_test.test_inheritance_parent) WITH (publish = 'insert, update, delete, 
truncate');\E
                        /xm,
                like => { %full_runs, section_post_data => 1, },
        },
@@ -4075,6 +4224,85 @@ my %tests = (
                },
        },
 
+       # The order of a table's parents is a logical property of the database:
+       # pg_inherits.inhseqno fixes it, and it determines the order of the
+       # child's inherited columns.  Here inh_order_parent1 is re-attached 
after
+       # a NO INHERIT, so it has the *higher* inhseqno; VACUUM frees the line
+       # pointer of the removed pg_inherits row and the re-added one reuses it,
+       # putting the higher-inhseqno parent physically first.  The INHERITS 
list
+       # must still come out in inhseqno order.
+       'CREATE TABLE inh_order_parent1' => {
+               create_order => 101,
+               create_sql => 'CREATE TABLE dump_test.inh_order_parent1 (
+                                                  col1 int
+                                                );',
+               regexp => qr/^
+               \QCREATE TABLE dump_test.inh_order_parent1 (\E\n
+               \s+\Qcol1 integer\E\n
+               \Q);\E\n
+               /xm,
+               like =>
+                 { %full_runs, %dump_test_schema_runs, section_pre_data => 1, 
},
+               unlike => {
+                       exclude_dump_test_schema => 1,
+                       only_dump_measurement => 1,
+               },
+       },
+
+       'CREATE TABLE inh_order_parent2' => {
+               create_order => 102,
+               create_sql => 'CREATE TABLE dump_test.inh_order_parent2 (
+                                                  col1 int
+                                                );',
+               regexp => qr/^
+               \QCREATE TABLE dump_test.inh_order_parent2 (\E\n
+               \s+\Qcol1 integer\E\n
+               \Q);\E\n
+               /xm,
+               like =>
+                 { %full_runs, %dump_test_schema_runs, section_pre_data => 1, 
},
+               unlike => {
+                       exclude_dump_test_schema => 1,
+                       only_dump_measurement => 1,
+               },
+       },
+
+       'CREATE TABLE inh_order_child' => {
+               create_order => 103,
+               create_sql => 'CREATE TABLE dump_test.inh_order_child (
+                                                  col2 int
+                                                ) INHERITS 
(dump_test.inh_order_parent1,
+                                                                        
dump_test.inh_order_parent2);
+                                          ALTER TABLE dump_test.inh_order_child
+                                                  NO INHERIT 
dump_test.inh_order_parent1;
+                                          VACUUM pg_catalog.pg_inherits;
+                                          ALTER TABLE dump_test.inh_order_child
+                                                  INHERIT 
dump_test.inh_order_parent1;',
+               regexp => qr/^
+               \QCREATE TABLE dump_test.inh_order_child (\E\n
+               \s+\Qcol2 integer\E\n
+               \)\n
+               \QINHERITS (dump_test.inh_order_parent2, 
dump_test.inh_order_parent1);\E\n
+               /xm,
+               like => {
+                       %full_runs, %dump_test_schema_runs, section_pre_data => 
1,
+               },
+               unlike => {
+                       binary_upgrade => 1,
+                       exclude_dump_test_schema => 1,
+                       only_dump_measurement => 1,
+               },
+       },
+
+       'CREATE TABLE inh_order_child pg_upgrade' => {
+               regexp => qr/^
+               \QALTER TABLE ONLY dump_test.inh_order_child INHERIT 
dump_test.inh_order_parent2;\E\n
+               \QALTER TABLE ONLY dump_test.inh_order_child INHERIT 
dump_test.inh_order_parent1;\E\n
+               /xm,
+               like => { binary_upgrade => 1, },
+       },
+
+
        'CREATE STATISTICS extended_stats_no_options' => {
                create_order => 97,
                create_sql => 'CREATE STATISTICS 
dump_test.test_ext_stats_no_options
diff --git a/src/bin/pg_dump/t/003_pg_dump_with_server.pl 
b/src/bin/pg_dump/t/003_pg_dump_with_server.pl
index 349add6..7c10e3a 100644
--- a/src/bin/pg_dump/t/003_pg_dump_with_server.pl
+++ b/src/bin/pg_dump/t/003_pg_dump_with_server.pl
@@ -47,4 +47,95 @@ command_ok(
        ],
        "dump foreign server with no tables");
 
+#########################################
+# Verify that --binary-upgrade lists an extension's required extensions in
+# name order.  pg_dump reads the requires list out of pg_depend, which
+# returns those rows in an order derived from the required extensions'
+# OIDs; without an explicit sort, two databases holding the same extensions
+# dump differently depending on the order the extensions were created in.
+
+mkdir "$tempdir/extension"
+  or die "could not create directory \"$tempdir/extension\": $!";
+foreach my $ext ('dump_test_ext_a', 'dump_test_ext_b', 'dump_test_ext_c')
+{
+       open my $cf, '>', "$tempdir/extension/$ext.control"
+         or die "could not create control file for $ext: $!";
+       print $cf "default_version = '1.0'\n";
+       print $cf "relocatable = true\n";
+       print $cf "requires = 'dump_test_ext_a,dump_test_ext_b'\n"
+         if $ext eq 'dump_test_ext_c';
+       close $cf;
+
+       # The extensions need no members, so an empty script will do.
+       open my $sf, '>', "$tempdir/extension/$ext--1.0.sql"
+         or die "could not create script file for $ext: $!";
+       close $sf;
+}
+
+my $sep = $windows_os ? ';' : ':';
+my $ext_path = $windows_os ? ($tempdir =~ s/\\/\\\\/gr) : $tempdir;
+
+# Create dump_test_ext_a before dump_test_ext_b, so that the requirement
+# that sorts first by name is the one with the smaller OID.  pg_depend
+# hands back these rows in descending OID order, that is, in the reverse of
+# the order the dump must use.
+$node->safe_psql(
+       'postgres', qq{
+       SET extension_control_path = '\$system$sep$ext_path';
+       CREATE EXTENSION dump_test_ext_a;
+       CREATE EXTENSION dump_test_ext_b;
+       CREATE EXTENSION dump_test_ext_c;});
+
+command_like(
+       [ 'pg_dump', '--port' => $port, '--binary-upgrade', 'postgres' ],
+       qr/\QSELECT 
pg_catalog.binary_upgrade_create_empty_extension('dump_test_ext_c', 'public', 
true, '1.0', NULL, NULL, 
ARRAY['dump_test_ext_a','dump_test_ext_b']::pg_catalog.text[]);\E/,
+       'binary upgrade dumps required extensions in name order');
+
+#########################################
+# Verify that an object carrying labels from more than one security label
+# provider gets its SECURITY LABEL commands emitted in provider name order,
+# not in pg_seclabel/pg_shseclabel physical order.  dummy_seclabel registers
+# a second provider, "dummy2", when dummy_seclabel.second_provider is turned
+# on before the module is loaded.
+
+SKIP:
+{
+       skip "dummy_seclabel module not installed", 6
+         unless $node->check_extension('dummy_seclabel');
+
+       # Label each object with "dummy2" before "dummy", that is, in the 
reverse
+       # of the order the dump has to use, so that emitting the labels in
+       # catalog order would produce the wrong output.
+       $node->safe_psql(
+               'postgres', q|
+               SET dummy_seclabel.second_provider = on;
+               LOAD 'dummy_seclabel';
+               CREATE TABLE seclabel_order_tbl (a int);
+               SECURITY LABEL FOR dummy2 ON TABLE seclabel_order_tbl IS 
'classified';
+               SECURITY LABEL FOR dummy ON TABLE seclabel_order_tbl IS 
'classified';
+               SECURITY LABEL FOR dummy2 ON COLUMN seclabel_order_tbl.a IS 
'classified';
+               SECURITY LABEL FOR dummy ON COLUMN seclabel_order_tbl.a IS 
'classified';
+               SECURITY LABEL FOR dummy2 ON DATABASE postgres IS 'classified';
+               SECURITY LABEL FOR dummy ON DATABASE postgres IS 'classified';
+       |);
+
+       $node->command_like(
+               [ 'pg_dump', '--schema-only', 'postgres' ],
+               qr/^
+                       \QSECURITY LABEL FOR dummy ON TABLE 
public.seclabel_order_tbl IS 'classified';\E\n
+                       \QSECURITY LABEL FOR dummy2 ON TABLE 
public.seclabel_order_tbl IS 'classified';\E\n
+                       \QSECURITY LABEL FOR dummy ON COLUMN 
public.seclabel_order_tbl.a IS 'classified';\E\n
+                       \QSECURITY LABEL FOR dummy2 ON COLUMN 
public.seclabel_order_tbl.a IS 'classified';\E$
+                       /xm,
+               'security labels are dumped in provider order');
+
+       $node->command_like(
+               [ 'pg_dump', '--schema-only', '--create', 'postgres' ],
+               qr/^
+                       \QSECURITY LABEL FOR dummy ON DATABASE postgres IS 
'classified';\E\n
+                       \QSECURITY LABEL FOR dummy2 ON DATABASE postgres IS 
'classified';\E$
+                       /xm,
+               'shared security labels are dumped in provider order');
+}
+
 done_testing();
diff --git a/src/test/modules/dummy_seclabel/dummy_seclabel.c 
b/src/test/modules/dummy_seclabel/dummy_seclabel.c
index 7277f61..909a1b7 100644
--- a/src/test/modules/dummy_seclabel/dummy_seclabel.c
+++ b/src/test/modules/dummy_seclabel/dummy_seclabel.c
@@ -15,12 +15,15 @@
 #include "commands/seclabel.h"
 #include "fmgr.h"
 #include "miscadmin.h"
+#include "utils/guc.h"
 #include "utils/rel.h"
 
 PG_MODULE_MAGIC;
 
 PG_FUNCTION_INFO_V1(dummy_seclabel_dummy);
 
+static bool dummy_seclabel_second_provider = false;
+
 static void
 dummy_object_relabel(const ObjectAddress *object, const char *seclabel)
 {
@@ -47,6 +50,29 @@ void
 _PG_init(void)
 {
        register_label_provider("dummy", dummy_object_relabel);
+
+       /*
+        * Optionally register a second provider.  Tests that need two providers
+        * registered at the same time turn this on before the module is loaded.
+        * It defaults to off, so that the provider-less "SECURITY LABEL ON ... 
IS
+        * ..." syntax, which requires exactly one registered provider, keeps
+        * working.
+        */
+       DefineCustomBoolVariable("dummy_seclabel.second_provider",
+                                                        "Also register a 
\"dummy2\" label provider.",
+                                                        NULL,
+                                                        
&dummy_seclabel_second_provider,
+                                                        false,
+                                                        PGC_SUSET,
+                                                        0,
+                                                        NULL,
+                                                        NULL,
+                                                        NULL);
+
+       MarkGUCPrefixReserved("dummy_seclabel");
+
+       if (dummy_seclabel_second_provider)
+               register_label_provider("dummy2", dummy_object_relabel);
 }
 
 /*
diff --git a/src/test/modules/test_pg_dump/t/001_base.pl 
b/src/test/modules/test_pg_dump/t/001_base.pl
index 3d65ce4..d9e1ea9 100644
--- a/src/test/modules/test_pg_dump/t/001_base.pl
+++ b/src/test/modules/test_pg_dump/t/001_base.pl
@@ -846,6 +846,51 @@ my %tests = (
                },
        },
 
+       'CREATE TRIGGER extdepend_trig' => {
+               create_order => 12,
+               create_sql =>
+                 'CREATE TRIGGER extdepend_trig BEFORE UPDATE ON 
regress_pg_dump_schema.extdependtab
+               FOR EACH ROW EXECUTE FUNCTION 
suppress_redundant_updates_trigger();
+               ALTER TRIGGER extdepend_trig ON 
regress_pg_dump_schema.extdependtab DEPENDS ON EXTENSION test_pg_dump;
+               ALTER TRIGGER extdepend_trig ON 
regress_pg_dump_schema.extdependtab DEPENDS ON EXTENSION plpgsql;',
+               regexp => qr/^
+               \QCREATE TRIGGER extdepend_trig BEFORE UPDATE ON 
regress_pg_dump_schema.extdependtab FOR EACH ROW EXECUTE FUNCTION 
suppress_redundant_updates_trigger();\E\n
+               /xms,
+               like => {%pgdump_runs},
+               unlike => {
+                       data_only => 1,
+                       extension_schema => 1,
+                       pg_dumpall_globals => 1,
+                       privileged_internals => 1,
+                       section_data => 1,
+                       section_pre_data => 1,
+                       # Excludes this schema as extension is not listed.
+                       without_extension_explicit_schema => 1,
+               },
+       },
+
+       # The two ALTER TRIGGER ... DEPENDS ON EXTENSION statements above are
+       # executed test_pg_dump first, plpgsql second, but pg_dump must emit 
them
+       # in extension name order, so that the archive entry's text does not
+       # depend on pg_depend's physical row order.
+       'ALTER TRIGGER DEPENDS ON extension in name order' => {
+               regexp => qr/^
+               \QALTER TRIGGER extdepend_trig ON 
regress_pg_dump_schema.extdependtab DEPENDS ON EXTENSION plpgsql;\E\n
+               \QALTER TRIGGER extdepend_trig ON 
regress_pg_dump_schema.extdependtab DEPENDS ON EXTENSION test_pg_dump;\E\n
+               /xms,
+               like => {%pgdump_runs},
+               unlike => {
+                       data_only => 1,
+                       extension_schema => 1,
+                       pg_dumpall_globals => 1,
+                       privileged_internals => 1,
+                       section_data => 1,
+                       section_pre_data => 1,
+                       # Excludes this schema as extension is not listed.
+                       without_extension_explicit_schema => 1,
+               },
+       },
+
        # Objects not included in extension, part of schema created by extension
        'CREATE TABLE regress_pg_dump_schema.external_tab' => {
                create_order => 4,
-- 
2.49.0

>From 2d0d0f3158afdc47e49afa68d87dd4823ad3d32c Mon Sep 17 00:00:00 2001
From: Noah Misch <[email protected]>
Date: Thu, 3 Sep 2026 18:07:02 +0000
Subject: [PATCH 2/2] SAMPLE fixes for the eleven testable dump-order defects

Not proposed patches.  These exist so that the branch is coherent -- the tests
in the preceding commit need something to pass against -- and so that "this
test fails without the fix" is checkable.  Drop this commit to see them fail.

Four involve a judgement rather than a mechanical key completion:

* D5 preserves the order the user wrote in CREATE POLICY, via unnest ... WITH
  ORDINALITY.  Plain ORDER BY rolname would be simpler but rewrites the
  clause.  Both remove the OID dependence.
* D4 joins pg_type and pg_namespace and orders by (nspname, typname) rather
  than by regtype output, whose rendering depends on search_path.
* D1 sorts the RLS-enable pseudo-object before its table's policies.  Either
  order is stable.
* D12 sorts an ACL array, which buildACLCommands() warns can be unsafe.  It is
  safe here only because a default ACL's items all share one grantor, so there
  is no grant chain to replay in order; that argument is the basis of the fix
  and is the thing to check before accepting it.

Verified: with these applied, meson test over the pg_dump, test_pg_dump and
dummy_seclabel suites is 13/13 (002_pg_dump alone is 13697 subtests).  With
all five product files reverted, pg_dump aborts on D1's assertion.  With only
D1's fix applied, each remaining test fails by its own name.

This work is model-generated and unreviewed by a human; see PROVENANCE.md.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Newm1jZHVy54kfX1eRPDwB
---
 src/bin/pg_dump/common.c       |   8 +-
 src/bin/pg_dump/dumputils.c    |   8 +-
 src/bin/pg_dump/pg_dump.c      | 155 +++++++++++++++++++++++++++------
 src/bin/pg_dump/pg_dump_sort.c |  12 +++
 src/bin/pg_dump/pg_dumpall.c   |   2 +-
 5 files changed, 153 insertions(+), 32 deletions(-)

diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c
index 047e1c6..38eb274 100644
--- a/src/bin/pg_dump/common.c
+++ b/src/bin/pg_dump/common.c
@@ -286,10 +286,10 @@ flagInhTables(Archive *fout, TableInfo *tblinfo, int 
numTables,
        for (i = 0; i < numInherits; i++)
        {
                /*
-                * Skip a hashtable lookup if it's same table as last time.  
This is
-                * unlikely for the child, but less so for the parent.  (Maybe 
we
-                * should ask the backend for a sorted array to make it more 
likely?
-                * Not clear the sorting effort would be repaid, though.)
+                * Skip a hashtable lookup if it's same table as last time.
+                * getInherits() sorts by inhrelid, so consecutive rows for the 
same
+                * child do come together; repeats of the same parent are less
+                * predictable.
                 */
                if (child == NULL ||
                        child->dobj.catId.oid != inhinfo[i].inhrelid)
diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c
index a3835cc..d7b000f 100644
--- a/src/bin/pg_dump/dumputils.c
+++ b/src/bin/pg_dump/dumputils.c
@@ -681,10 +681,16 @@ void
 buildShSecLabelQuery(const char *catalog_name, Oid objectId,
                                         PQExpBuffer sql)
 {
+       /*
+        * Sort by provider, the remaining column of pg_shseclabel's unique key
+        * (classoid and objoid are already fixed by the WHERE clause), so that
+        * the emitted commands do not depend on physical row order.
+        */
        appendPQExpBuffer(sql,
                                          "SELECT provider, label FROM 
pg_catalog.pg_shseclabel "
                                          "WHERE classoid = 
'pg_catalog.%s'::pg_catalog.regclass "
-                                         "AND objoid = '%u'", catalog_name, 
objectId);
+                                         "AND objoid = '%u' "
+                                         "ORDER BY provider", catalog_name, 
objectId);
 }
 
 /*
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834..436c17d 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -3760,10 +3760,16 @@ dumpDatabaseConfig(Archive *AH, PQExpBuffer outbuf,
 
        PQclear(res);
 
-       /* Now look for role-and-database-specific options */
+       /*
+        * Now look for role-and-database-specific options.  Order by role name,
+        * so that the emitted commands don't depend on the roles' OIDs; rolname
+        * is a complete sort key, since pg_db_role_setting has at most one row
+        * per (setdatabase, setrole).
+        */
        printfPQExpBuffer(buf, "SELECT rolname, unnest(setconfig) "
                                          "FROM pg_db_role_setting s, pg_roles 
r "
-                                         "WHERE setrole = r.oid AND 
setdatabase = '%u'::oid",
+                                         "WHERE setrole = r.oid AND 
setdatabase = '%u'::oid "
+                                         "ORDER BY 1",
                                          dboid);
 
        res = ExecuteSqlQuery(AH, buf->data, PGRES_TUPLES_OK);
@@ -4274,9 +4280,20 @@ getPolicies(Archive *fout, TableInfo tblinfo[], int 
numTables)
        printfPQExpBuffer(query,
                                          "SELECT pol.oid, pol.tableoid, 
pol.polrelid, pol.polname, pol.polcmd, ");
        appendPQExpBufferStr(query, "pol.polpermissive, ");
+       /*
+        * The role names in the policy's TO clause must come out in the order
+        * they appear in polroles, which is the order they were written in
+        * CREATE POLICY.  An unordered ARRAY() subquery would instead return 
them
+        * in pg_authid scan order, so two databases holding identical policies
+        * would dump differently whenever their roles occupy different physical
+        * positions or the planner picks a different scan for pg_authid.
+        */
        appendPQExpBuffer(query,
                                          "CASE WHEN pol.polroles = '{0}' THEN 
NULL ELSE "
-                                         "   
pg_catalog.array_to_string(ARRAY(SELECT pg_catalog.quote_ident(rolname) from 
pg_catalog.pg_roles WHERE oid = ANY(pol.polroles)), ', ') END AS polroles, "
+                                         "   
pg_catalog.array_to_string(ARRAY(SELECT pg_catalog.quote_ident(r.rolname) "
+                                         "FROM pg_catalog.unnest(pol.polroles) 
WITH ORDINALITY AS u(roleoid, ord) "
+                                         "JOIN pg_catalog.pg_roles r ON (r.oid 
= u.roleoid) "
+                                         "ORDER BY u.ord), ', ') END AS 
polroles, "
                                          "pg_catalog.pg_get_expr(pol.polqual, 
pol.polrelid) AS polqual, "
                                          
"pg_catalog.pg_get_expr(pol.polwithcheck, pol.polrelid) AS polwithcheck "
                                          "FROM unnest('%s'::pg_catalog.oid[]) 
AS src(tbloid)\n"
@@ -4595,10 +4612,22 @@ getPublications(Archive *fout)
                        PGresult   *res_tbls;
 
                        resetPQExpBuffer(query);
+                       /*
+                        * Sort the EXCEPT list by the excluded relations' 
names.  The list
+                        * is a set, and pg_publication_rel has no ordering 
column, so an
+                        * unordered query would emit it in heap order and make 
two
+                        * logically-identical databases dump differently.  
Sorting by
+                        * prrelid would just trade heap order for OID order; 
use the
+                        * referenced relation's natural key (nspname, 
relname), matching
+                        * DOTypeNameCompare().
+                        */
                        appendPQExpBuffer(query,
-                                                         "SELECT prrelid\n"
-                                                         "FROM 
pg_catalog.pg_publication_rel\n"
-                                                         "WHERE prpubid = %u 
AND prexcept",
+                                                         "SELECT pr.prrelid\n"
+                                                         "FROM 
pg_catalog.pg_publication_rel pr\n"
+                                                         "     JOIN 
pg_catalog.pg_class c ON c.oid = pr.prrelid\n"
+                                                         "     JOIN 
pg_catalog.pg_namespace n ON n.oid = c.relnamespace\n"
+                                                         "WHERE pr.prpubid = 
%u AND pr.prexcept\n"
+                                                         "ORDER BY n.nspname, 
c.relname",
                                                          
pubinfo[i].dobj.catId.oid);
 
                        res_tbls = ExecuteSqlQuery(fout, query->data, 
PGRES_TUPLES_OK);
@@ -5677,6 +5706,10 @@ dumpSubscription(Archive *fout, const SubscriptionInfo 
*subinfo)
 /*
  * Given a "create query", append as many ALTER ... DEPENDS ON EXTENSION as
  * the object needs.
+ *
+ * The statements are emitted in extension name order, so that the text of the
+ * object's archive entry is a function of the object's dependencies and not of
+ * the order in which those dependencies happen to appear in pg_depend.
  */
 static void
 append_depends_on_extension(Archive *fout,
@@ -5704,7 +5737,8 @@ append_depends_on_extension(Archive *fout,
                                                  "FROM pg_catalog.pg_depend d, 
pg_catalog.pg_extension e "
                                                  "WHERE d.refobjid = e.oid AND 
classid = '%s'::pg_catalog.regclass "
                                                  "AND objid = 
'%u'::pg_catalog.oid AND deptype = 'x' "
-                                                 "AND refclassid = 
'pg_catalog.pg_extension'::pg_catalog.regclass",
+                                                 "AND refclassid = 
'pg_catalog.pg_extension'::pg_catalog.regclass "
+                                                 "ORDER BY e.extname",
                                                  catalog,
                                                  dobj->catId.oid);
                res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
@@ -7692,8 +7726,18 @@ getInherits(Archive *fout, int *numInherits)
        int                     i_inhrelid;
        int                     i_inhparent;
 
-       /* find all the inheritance information */
-       appendPQExpBufferStr(query, "SELECT inhrelid, inhparent FROM 
pg_inherits");
+       /*
+        * Find all the inheritance information.  ORDER BY inhseqno is 
essential:
+        * the order of a table's parents is a logical property of the database
+        * (inhseqno fixes the order of the child's inherited columns), while 
the
+        * physical order of pg_inherits rows is not, since a line pointer freed
+        * by NO INHERIT or DROP TABLE and then reclaimed by VACUUM gets reused 
by
+        * a later entry with a higher inhseqno.  Sorting by inhrelid as well
+        * makes the "same table as last time" caching in flagInhTables() work.
+        */
+       appendPQExpBufferStr(query,
+                                                "SELECT inhrelid, inhparent 
FROM pg_inherits "
+                                                "ORDER BY inhrelid, inhseqno");
 
        res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
 
@@ -10570,12 +10614,26 @@ getDefaultACLs(Archive *fout)
         * for the case of 'S' (DEFACLOBJ_SEQUENCE) which must be converted to
         * 's'.
         */
+       /*
+        * The stored element order of defaclacl carries no information: the
+        * backend canonicalizes these arrays with aclitemsort(), which orders 
them
+        * by grantee OID.  Dumping in that order would make our output depend 
on
+        * OID assignment, so re-sort by the aclitem's textual form, i.e. by
+        * grantee name.  Unlike an object's own ACL, a default ACL cannot 
contain
+        * a chain of grants by different grantors -- every item's grantor is
+        * defaclrole -- so reordering is safe here.
+        */
        appendPQExpBufferStr(query,
                                                 "SELECT oid, tableoid, "
                                                 "defaclrole, "
                                                 "defaclnamespace, "
                                                 "defaclobjtype, "
-                                                "defaclacl, "
+                                                "CASE WHEN 
pg_catalog.array_length(defaclacl, 1) IS NULL "
+                                                "THEN defaclacl ELSE "
+                                                "(SELECT 
pg_catalog.array_agg(a ORDER BY "
+                                                "a::pg_catalog.text COLLATE 
pg_catalog.\"C\") "
+                                                "FROM 
pg_catalog.unnest(defaclacl) AS a) "
+                                                "END AS defaclacl, "
                                                 "CASE WHEN defaclnamespace = 0 
THEN "
                                                 "acldefault(CASE WHEN 
defaclobjtype = 'S' "
                                                 "THEN 's'::\"char\" ELSE 
defaclobjtype END, "
@@ -11954,6 +12012,7 @@ dumpExtension(Archive *fout, const ExtensionInfo 
*extinfo)
                 */
                int                     i;
                int                     n;
+               char      **reqexts;
 
                appendPQExpBufferStr(q, "-- For binary upgrade, create an empty 
extension and insert objects into it\n");
 
@@ -11989,7 +12048,14 @@ dumpExtension(Archive *fout, const ExtensionInfo 
*extinfo)
                else
                        appendPQExpBufferStr(q, "NULL");
                appendPQExpBufferStr(q, ", ");
-               appendPQExpBufferStr(q, "ARRAY[");
+               /*
+                * Collect the names of the extensions this one requires.  The
+                * dependency array is in the order getDependencies() read the
+                * pg_depend rows, which is a function of the required 
extensions'
+                * OIDs; sort the names so that the output depends only on the
+                * database's logical content.
+                */
+               reqexts = (char **) pg_malloc(extinfo->dobj.nDeps * sizeof(char 
*));
                n = 0;
                for (i = 0; i < extinfo->dobj.nDeps; i++)
                {
@@ -11997,14 +12063,20 @@ dumpExtension(Archive *fout, const ExtensionInfo 
*extinfo)
 
                        extobj = 
findObjectByDumpId(extinfo->dobj.dependencies[i]);
                        if (extobj && extobj->objType == DO_EXTENSION)
-                       {
-                               if (n++ > 0)
-                                       appendPQExpBufferChar(q, ',');
-                               appendStringLiteralAH(q, extobj->name, fout);
-                       }
+                               reqexts[n++] = extobj->name;
+               }
+               qsort(reqexts, n, sizeof(char *), pg_qsort_strcmp);
+
+               appendPQExpBufferStr(q, "ARRAY[");
+               for (i = 0; i < n; i++)
+               {
+                       if (i > 0)
+                               appendPQExpBufferChar(q, ',');
+                       appendStringLiteralAH(q, reqexts[i], fout);
                }
                appendPQExpBufferStr(q, "]::pg_catalog.text[]");
                appendPQExpBufferStr(q, ");\n");
+               pg_free(reqexts);
        }
 
        if (extinfo->dobj.dump & DUMP_COMPONENT_DEFINITION)
@@ -14577,15 +14649,20 @@ dumpOpclass(Archive *fout, const OpclassInfo *opcinfo)
        appendPQExpBuffer(query, "SELECT amopstrategy, "
                                          "amopopr::pg_catalog.regoperator, "
                                          "opfname AS sortfamily, "
-                                         "nspname AS sortfamilynsp "
+                                         "n.nspname AS sortfamilynsp "
                                          "FROM pg_catalog.pg_amop ao JOIN 
pg_catalog.pg_depend ON "
                                          "(classid = 
'pg_catalog.pg_amop'::pg_catalog.regclass AND objid = ao.oid) "
                                          "LEFT JOIN pg_catalog.pg_opfamily f 
ON f.oid = amopsortfamily "
                                          "LEFT JOIN pg_catalog.pg_namespace n 
ON n.oid = opfnamespace "
+                                         "JOIN pg_catalog.pg_type lt ON lt.oid 
= ao.amoplefttype "
+                                         "JOIN pg_catalog.pg_namespace ln ON 
ln.oid = lt.typnamespace "
+                                         "JOIN pg_catalog.pg_type rt ON rt.oid 
= ao.amoprighttype "
+                                         "JOIN pg_catalog.pg_namespace rn ON 
rn.oid = rt.typnamespace "
                                          "WHERE refclassid = 
'pg_catalog.pg_opclass'::pg_catalog.regclass "
                                          "AND refobjid = '%u'::pg_catalog.oid "
                                          "AND amopfamily = 
'%s'::pg_catalog.oid "
-                                         "ORDER BY amopstrategy",
+                                         "ORDER BY amopstrategy, ln.nspname, 
lt.typname, "
+                                         "rn.nspname, rt.typname",
                                          opcinfo->dobj.catId.oid,
                                          opcfamily);
 
@@ -14639,12 +14716,19 @@ dumpOpclass(Archive *fout, const OpclassInfo *opcinfo)
                                          "amproc::pg_catalog.regprocedure, "
                                          "amproclefttype::pg_catalog.regtype, "
                                          "amprocrighttype::pg_catalog.regtype "
-                                         "FROM pg_catalog.pg_amproc ap, 
pg_catalog.pg_depend "
+                                         "FROM pg_catalog.pg_amproc ap, 
pg_catalog.pg_depend, "
+                                         "pg_catalog.pg_type lt, 
pg_catalog.pg_namespace ln, "
+                                         "pg_catalog.pg_type rt, 
pg_catalog.pg_namespace rn "
                                          "WHERE refclassid = 
'pg_catalog.pg_opclass'::pg_catalog.regclass "
                                          "AND refobjid = '%u'::pg_catalog.oid "
                                          "AND classid = 
'pg_catalog.pg_amproc'::pg_catalog.regclass "
                                          "AND objid = ap.oid "
-                                         "ORDER BY amprocnum",
+                                         "AND lt.oid = ap.amproclefttype "
+                                         "AND ln.oid = lt.typnamespace "
+                                         "AND rt.oid = ap.amprocrighttype "
+                                         "AND rn.oid = rt.typnamespace "
+                                         "ORDER BY amprocnum, ln.nspname, 
lt.typname, "
+                                         "rn.nspname, rt.typname",
                                          opcinfo->dobj.catId.oid);
 
        res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
@@ -14779,15 +14863,20 @@ dumpOpfamily(Archive *fout, const OpfamilyInfo 
*opfinfo)
        appendPQExpBuffer(query, "SELECT amopstrategy, "
                                          "amopopr::pg_catalog.regoperator, "
                                          "opfname AS sortfamily, "
-                                         "nspname AS sortfamilynsp "
+                                         "n.nspname AS sortfamilynsp "
                                          "FROM pg_catalog.pg_amop ao JOIN 
pg_catalog.pg_depend ON "
                                          "(classid = 
'pg_catalog.pg_amop'::pg_catalog.regclass AND objid = ao.oid) "
                                          "LEFT JOIN pg_catalog.pg_opfamily f 
ON f.oid = amopsortfamily "
                                          "LEFT JOIN pg_catalog.pg_namespace n 
ON n.oid = opfnamespace "
+                                         "JOIN pg_catalog.pg_type lt ON lt.oid 
= ao.amoplefttype "
+                                         "JOIN pg_catalog.pg_namespace ln ON 
ln.oid = lt.typnamespace "
+                                         "JOIN pg_catalog.pg_type rt ON rt.oid 
= ao.amoprighttype "
+                                         "JOIN pg_catalog.pg_namespace rn ON 
rn.oid = rt.typnamespace "
                                          "WHERE refclassid = 
'pg_catalog.pg_opfamily'::pg_catalog.regclass "
                                          "AND refobjid = '%u'::pg_catalog.oid "
                                          "AND amopfamily = 
'%u'::pg_catalog.oid "
-                                         "ORDER BY amopstrategy",
+                                         "ORDER BY amopstrategy, ln.nspname, 
lt.typname, "
+                                         "rn.nspname, rt.typname",
                                          opfinfo->dobj.catId.oid,
                                          opfinfo->dobj.catId.oid);
 
@@ -14799,12 +14888,19 @@ dumpOpfamily(Archive *fout, const OpfamilyInfo 
*opfinfo)
                                          "amproc::pg_catalog.regprocedure, "
                                          "amproclefttype::pg_catalog.regtype, "
                                          "amprocrighttype::pg_catalog.regtype "
-                                         "FROM pg_catalog.pg_amproc ap, 
pg_catalog.pg_depend "
+                                         "FROM pg_catalog.pg_amproc ap, 
pg_catalog.pg_depend, "
+                                         "pg_catalog.pg_type lt, 
pg_catalog.pg_namespace ln, "
+                                         "pg_catalog.pg_type rt, 
pg_catalog.pg_namespace rn "
                                          "WHERE refclassid = 
'pg_catalog.pg_opfamily'::pg_catalog.regclass "
                                          "AND refobjid = '%u'::pg_catalog.oid "
                                          "AND classid = 
'pg_catalog.pg_amproc'::pg_catalog.regclass "
                                          "AND objid = ap.oid "
-                                         "ORDER BY amprocnum",
+                                         "AND lt.oid = ap.amproclefttype "
+                                         "AND ln.oid = lt.typnamespace "
+                                         "AND rt.oid = ap.amprocrighttype "
+                                         "AND rn.oid = rt.typnamespace "
+                                         "ORDER BY amprocnum, ln.nspname, 
lt.typname, "
+                                         "rn.nspname, rt.typname",
                                          opfinfo->dobj.catId.oid);
 
        res_procs = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
@@ -16731,7 +16827,8 @@ findSecLabels(Oid classoid, Oid objoid, SecLabelItem 
**items)
  * Construct a table of all security labels available for database objects;
  * also set the has-seclabel component flag for each relevant object.
  *
- * The table is sorted by classoid/objid/objsubid for speed in lookup.
+ * The table is sorted by classoid/objid/objsubid/provider for speed in
+ * lookup.
  */
 static void
 collectSecLabels(Archive *fout)
@@ -16749,10 +16846,16 @@ collectSecLabels(Archive *fout)
 
        query = createPQExpBuffer();
 
+       /*
+        * Sort by provider as well.  It is the remaining column of 
pg_seclabel's
+        * unique key, so adding it makes the ordering total; without it, the
+        * order of the labels an object has from different providers would come
+        * from physical row order, making the dump unstable.
+        */
        appendPQExpBufferStr(query,
                                                 "SELECT label, provider, 
classoid, objoid, objsubid "
                                                 "FROM pg_catalog.pg_seclabels "
-                                                "ORDER BY classoid, objoid, 
objsubid");
+                                                "ORDER BY classoid, objoid, 
objsubid, provider");
 
        res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
 
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 8ca0332..38c3a1c 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -394,6 +394,18 @@ DOTypeNameCompare(const void *p1, const void *p2)
                                                pobj2->poltable->dobj.name);
                if (cmpval != 0)
                        return cmpval;
+
+               /*
+                * getPolicies() represents "RLS is enabled on this table" as a
+                * PolicyInfo with null polname whose dobj.name is the table's 
relname.
+                * Policy names live in a per-table namespace disjoint from 
relation
+                * names, so such a marker ties with a real policy of that same 
name on
+                * that same table; whether polname is null is then the only 
remaining
+                * natural-key field.  Sort the marker first.
+                */
+               cmpval = (pobj1->polname != NULL) - (pobj2->polname != NULL);
+               if (cmpval != 0)
+                       return cmpval;
        }
        else if (obj1->objType == DO_RULE)
        {
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index c53e77c..d867e9e 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -1373,7 +1373,7 @@ dumpTablespaces(PGconn *conn)
                                           "pg_catalog.shobj_description(oid, 
'pg_tablespace') "
                                           "FROM pg_catalog.pg_tablespace "
                                           "WHERE spcname !~ '^pg_' "
-                                          "ORDER BY 1");
+                                          "ORDER BY 2");
 
        if (PQntuples(res) > 0)
                fprintf(OPF, "--\n-- Tablespaces\n--\n\n");
-- 
2.49.0

Reply via email to