From 28acfb6db870802903fdb0fd1c41f24ea2872fd1 Mon Sep 17 00:00:00 2001 From: Bharath Rupireddy Date: Mon, 14 Sep 2026 18:43:33 +0000 Subject: [PATCH v1 3/3] pg_dump: Preserve the chunk_id type of a TOAST table. Previously, pg_dump built the WITH clause of a relation from its reloptions alone. As toast_value_type is read only when a TOAST table is created, a reset of it removes the entry from the reloptions but leaves the TOAST table alone, which keeps the OID8 chunk_id it was created with. pg_attribute is then the only place recording the type in use, so the dump of such a relation carried no type and the restore created its TOAST table with an OID instead. Nothing reported an error, as the chunk identifiers are assigned afresh by the restore, but the relation was limited to 2^32 out-of-line values again. Fix this by reporting the type of chunk_id in use, read from pg_attribute, when the reloptions do not mention it. A value present in the reloptions is a request to change the type, and takes precedence, so that setting it and then dumping and restoring changes the type. Note that nothing is reported in binary upgrade mode, where the type is carried by binary_upgrade_set_next_toast_chunk_id_typoid without reading the reloption, so pg_upgrade preserved the type all along. Add a test for this in the pg_upgrade TAP test. Author: Bharath Rupireddy Discussion: https://postgr.es/m/aqd_gYs35v--sVlR%40paquier.xyz --- doc/src/sgml/ref/create_table.sgml | 4 +- src/bin/pg_dump/pg_dump.c | 58 +++++++++++++++++++++++++- src/bin/pg_dump/pg_dump.h | 2 + src/bin/pg_upgrade/t/002_pg_upgrade.pl | 16 +++++++ 4 files changed, 78 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml index fef24d8f3a2..c7456f0f788 100644 --- a/doc/src/sgml/ref/create_table.sgml +++ b/doc/src/sgml/ref/create_table.sgml @@ -1678,7 +1678,9 @@ WITH ( MODULUS numeric_literal, REM VACUUM FULL preserves the type already in use. A dump and restore, on the other hand, recreates the TOAST relation and therefore - applies the current value of this parameter. + applies this parameter when it is set. When it is not set, + pg_dump reports the type used by the existing + TOAST table, so that a restore preserves it. diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index 388c3b9c346..f54cfcbba10 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -7154,6 +7154,7 @@ getTables(Archive *fout, int *numTables) int i; PQExpBuffer query = createPQExpBuffer(); TableInfo *tblinfo; + bool dump_toast_value_type; int i_reltableoid; int i_reloid; int i_relname; @@ -7187,6 +7188,7 @@ getTables(Archive *fout, int *numTables) int i_reloptions; int i_checkoption; int i_toastreloptions; + int i_toastvaluetype; int i_reloftype; int i_foreignserver; int i_amname; @@ -7212,6 +7214,9 @@ getTables(Archive *fout, int *numTables) * wrong answers if any concurrent DDL is happening. */ + dump_toast_value_type = (fout->remoteVersion >= 200000 && + !dopt->binary_upgrade); + appendPQExpBufferStr(query, "SELECT c.tableoid, c.oid, c.relname, " "c.relnamespace, c.relkind, c.reltype, " @@ -7266,6 +7271,34 @@ getTables(Archive *fout, int *numTables) "CASE WHEN 'check_option=local' = ANY (c.reloptions) THEN 'LOCAL'::text " "WHEN 'check_option=cascaded' = ANY (c.reloptions) THEN 'CASCADED'::text ELSE NULL END AS checkoption, "); + /* + * A reset of toast_value_type removes the entry from the reloptions of a + * relation but leaves its TOAST table alone, which keeps the chunk_id + * type it was created with. The reloptions then say nothing about the + * type in use, and a WITH clause built from the reloptions alone would + * have the restore create the TOAST table with the default type instead. + * Report the type of chunk_id for such a relation. Only OID8 needs to be + * reported, as a relation using OID matches the default. A value present + * in the reloptions is a request to change the type, and takes + * precedence. + * + * Nothing is reported in binary upgrade mode. There the type is carried + * by binary_upgrade_set_next_toast_chunk_id_typoid and the reloption is + * not read at all, so a WITH clause would only add an entry the + * reloptions of the old cluster do not have. + */ + if (dump_toast_value_type) + appendPQExpBufferStr(query, + "CASE WHEN ta.atttypid = " + CppAsString2(OID8OID) " AND " + "NOT EXISTS (SELECT 1 FROM " + "unnest(coalesce(c.reloptions, '{}')) AS o " + "WHERE split_part(o, '=', 1) = 'toast_value_type') " + "THEN 'oid8' ELSE NULL END AS toast_value_type, "); + else + appendPQExpBufferStr(query, + "NULL AS toast_value_type, "); + appendPQExpBufferStr(query, "am.amname, "); @@ -7306,6 +7339,15 @@ getTables(Archive *fout, int *numTables) " AND tc.relkind = " CppAsString2(RELKIND_TOASTVALUE) " AND c.relkind <> " CppAsString2(RELKIND_PARTITIONED_TABLE) ")\n"); + /* + * Left join to pg_attribute to pick up the type of chunk_id in use by the + * TOAST table. + */ + if (dump_toast_value_type) + appendPQExpBufferStr(query, + "LEFT JOIN pg_attribute ta ON (ta.attrelid = tc.oid" + " AND ta.attname = 'chunk_id')\n"); + /* * Restrict to interesting relkinds (in particular, not indexes). Not all * relkinds are possible in older servers, but it's not worth the trouble @@ -7377,6 +7419,7 @@ getTables(Archive *fout, int *numTables) i_reloptions = PQfnumber(res, "reloptions"); i_checkoption = PQfnumber(res, "checkoption"); i_toastreloptions = PQfnumber(res, "toast_reloptions"); + i_toastvaluetype = PQfnumber(res, "toast_value_type"); i_reloftype = PQfnumber(res, "reloftype"); i_foreignserver = PQfnumber(res, "foreignserver"); i_amname = PQfnumber(res, "amname"); @@ -7458,6 +7501,10 @@ getTables(Archive *fout, int *numTables) else tblinfo[i].checkoption = pg_strdup(PQgetvalue(res, i, i_checkoption)); tblinfo[i].toast_reloptions = pg_strdup(PQgetvalue(res, i, i_toastreloptions)); + if (PQgetisnull(res, i, i_toastvaluetype)) + tblinfo[i].toast_value_type = NULL; + else + tblinfo[i].toast_value_type = pg_strdup(PQgetvalue(res, i, i_toastvaluetype)); tblinfo[i].reloftype = atooid(PQgetvalue(res, i, i_reloftype)); tblinfo[i].foreign_server = atooid(PQgetvalue(res, i, i_foreignserver)); if (PQgetisnull(res, i, i_amname)) @@ -17390,7 +17437,8 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo) } if (nonemptyReloptions(tbinfo->reloptions) || - nonemptyReloptions(tbinfo->toast_reloptions)) + nonemptyReloptions(tbinfo->toast_reloptions) || + tbinfo->toast_value_type != NULL) { bool addcomma = false; @@ -17400,6 +17448,14 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo) addcomma = true; appendReloptionsArrayAH(q, tbinfo->reloptions, "", fout); } + if (tbinfo->toast_value_type != NULL) + { + if (addcomma) + appendPQExpBufferStr(q, ", "); + addcomma = true; + appendPQExpBuffer(q, "toast_value_type=%s", + tbinfo->toast_value_type); + } if (nonemptyReloptions(tbinfo->toast_reloptions)) { if (addcomma) diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index 2bbb5d5773b..0a9089a815a 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -315,6 +315,8 @@ typedef struct _tableInfo char *reloptions; /* options specified by WITH (...) */ char *checkoption; /* WITH CHECK OPTION, if any */ char *toast_reloptions; /* WITH options for the TOAST table */ + char *toast_value_type; /* type of chunk_id in the TOAST table, + * when not implied by reloptions */ bool hasindex; /* does it have any indexes? */ bool hasrules; /* does it have any rules? */ bool hastriggers; /* does it have any triggers? */ diff --git a/src/bin/pg_upgrade/t/002_pg_upgrade.pl b/src/bin/pg_upgrade/t/002_pg_upgrade.pl index 9590625fc32..493a3dc4d8b 100644 --- a/src/bin/pg_upgrade/t/002_pg_upgrade.pl +++ b/src/bin/pg_upgrade/t/002_pg_upgrade.pl @@ -456,6 +456,22 @@ push(@dump_command, '--extra-float-digits', '0') if ($oldnode->pg_version < 12); $newnode->command_ok(\@dump_command, 'dump before running pg_upgrade'); +# A dump reports the type of chunk_id when the reloptions say nothing about +# it, so that a restore recreates the TOAST table with the same type. +# toasttest_oid8 is defined in strings.sql, which resets the reloption. +if (!defined($ENV{olddump})) +{ + like( + slurp_file($dump1_file), + qr/^ + \QCREATE TABLE public.toasttest_oid8 (\E\n + \s+\Qf1 bytea\E\n + \Q)\E\n + \QWITH (toast_value_type=oid8);\E + /xm, + 'dump reports the chunk_id type after a reset of toast_value_type'); +} + # After dumping, update references to the old source tree's regress.so # to point to the new tree. if (defined($ENV{oldinstall})) -- 2.47.3