From: Noah Misch <noah@leadboat.com>
To: Alexander Kukushkin <cyberdemn@gmail.com>
Cc: Nitin Jadhav <nitinjadhavpostgres@gmail.com>
Cc: Pg Hackers <pgsql-hackers@lists.postgresql.org>
Subject: Re: pg_dump: assert failure sorting casts/transforms
Date: Tue, 8 Sep 2026 17:03:57 -0700
Message-ID: <20260909000357.b2.noahmisch@microsoft.com> (raw)
In-Reply-To: <CAFh8B=n-Dgffwdr68Or8x-psfGB1G0bMVR4n-NTiwJb8ro=oaA@mail.gmail.com>
References: <CAFh8B=nb2KLugfF5pFgOLY2Q0db8KOqp=46kxvWNzgvTQeP8oQ@mail.gmail.com>
<CAMm1aWYWzP4RpqmgOshyv3UhKDNRLFYFq0EXxQrqJGyrB-7FxA@mail.gmail.com>
<CAFh8B=nowWf0sDk01G7B=sa-TpGTObFEK=NoQMJQLzU_H6af=Q@mail.gmail.com>
<CAMm1aWb9N+t25eVH++SDzxK-usiS2tNA12X7_bYENqkU8P74fw@mail.gmail.com>
<CAFh8B=n-Dgffwdr68Or8x-psfGB1G0bMVR4n-NTiwJb8ro=oaA@mail.gmail.com>
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 <noah@leadboat.com>
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 <noreply@anthropic.com>
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.mdnew file mode 100644index 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`+→ 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()` → `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` → `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.mdnew file mode 100644index 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.plindex 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.plindex 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.cindex 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.plindex 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 <noah@leadboat.com>
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 <noreply@anthropic.com>
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.cindex 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.cindex 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.cindex 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.cindex 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.cindex 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
Attachments:
[text/plain] 0001-Audit-report-and-regression-tests-for-pg_dump-dump-o.patch (73.3K, ../20260909000357.b2.noahmisch@microsoft.com/2-0001-Audit-report-and-regression-tests-for-pg_dump-dump-o.patch)
download | inline diff:
From 99d7f92890217317005c6e5d6b0d41bf616e3fc5 Mon Sep 17 00:00:00 2001
From: Noah Misch <noah@leadboat.com>
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 <noreply@anthropic.com>
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.mdnew file mode 100644index 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`+→ 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()` → `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` → `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.mdnew file mode 100644index 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.plindex 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.plindex 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.cindex 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.plindex 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
[text/plain] 0002-SAMPLE-fixes-for-the-eleven-testable-dump-order-defe.patch (18.8K, ../20260909000357.b2.noahmisch@microsoft.com/3-0002-SAMPLE-fixes-for-the-eleven-testable-dump-order-defe.patch)
download | inline diff:
From 2d0d0f3158afdc47e49afa68d87dd4823ad3d32c Mon Sep 17 00:00:00 2001
From: Noah Misch <noah@leadboat.com>
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 <noreply@anthropic.com>
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.cindex 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.cindex 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.cindex 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.cindex 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.cindex 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 instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Reply to all the recipients using the --to and --cc options:
reply via email
To: pgsql-hackers@postgresql.org
Cc: noah@leadboat.com, cyberdemn@gmail.com, nitinjadhavpostgres@gmail.com, pgsql-hackers@lists.postgresql.org
Subject: Re: pg_dump: assert failure sorting casts/transforms
In-Reply-To: <20260909000357.b2.noahmisch@microsoft.com>
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
This inbox is served by DDX for PostgreSQL; see mirroring instructions
for how to clone and mirror all data and code used for this inbox