Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

Started by Michael Paquier7 months ago60 messages
#1Michael Paquier
michael@paquier.xyz
12 attachment(s)

Hi all,

I have been looking at $subject and the many past reviews recently,
also related to some of the work related to the potential support for
zstandard compression in TOAST values, and found myself pondering
about the following message from Tom, to be reminded that nothing has
been done regarding the fact that the backend may finish in an
infinite loop once a TOAST table reaches 4 billion values:
/messages/by-id/764273.1669674269@sss.pgh.pa.us

Spoiler: I have heard of users that are in this case, and the best
thing we can do currently except raising shoulders is to use
workarounds with data externalization AFAIK, which is not nice,
usually, and users notice the problem once they see some backends
stuck in the infinite loop. I have spent some time looking at the
problem, and looked at all the proposals in this area like these ones
(I hope so at least):
https://commitfest.postgresql.org/patch/4296/
/messages/by-id/224711f9-83b7-a307-b17f-4457ab73aa0a@sigaev.ru

Anyway, it seems like nothing goes in a direction that I think would
be suited to fix the two following problems (some of the proposed
patches broke backward-compatibility, as well, and that's critical):
- The limit of TOAST values to 4 billions, because external TOAST
pointers want OIDs.
- New compression methods, see the recent proposal about zstandard at
[1]: /messages/by-id/CAFAfj_HX84EK4hyRYw50AOHOcdVi-+FFwAAPo7JHx4aShCvunQ@mail.gmail.com -- Michael
extinfo field of varatt_external has only this much data remaining.
Spoiler: I want to propose a new varatt_external dedicated to
zstandard-compressed external pointers, but that's not for this
thread.

Please find attached a patch set I have finished with while poking at
the problem, to address points 1) and 2) in the first email mentioned
at the top of this message. It is not yet ready for prime day yet
(there are a couple of things that would need adjustments), but I have
reached the point where I am going to need a consensus about what
people would be OK to have in terms of design to be able to support
multiple types of varatt_external to address these issues. And I'm OK
to consume time on that for the v19 cycle.

While hacking at (playing with?) the whole toasting and detoasting
code to understand the blast radius that this would involve, I have
quickly found that it is very annoying to have to touch at many places
of varatt.h to make variants of the existing varatt_external structure
(what we store on-disk as varlena Datum for external TOAST pointers).
Spoiler: it's my first time touching the internals of this code area
so deeply. Most of the code churns happen because we need to make the
[de]toast code aware of what to do depending on the vartags of the
external varlenas. It would be simple to hardcode a bunch of new
VARATT_IS_EXTERNAL_ONDISK() variants to plug in the new structures.
While it is efficient, this has a cost for out-of-core code and in
core because all the places that touch external TOAST pointers need to
be adjusted. Point is: it can be done. But if we introduce more
types of external TOAST pointers we need to always patch all these
areas, and there's a cost in that each time one or more new vartags
are added.

So, I have invented a new interface aimed at manipulating on-disk
external TOAST pointers, called toast_external, that is an on-memory
structure that services as an interface between on-disk external TOAST
pointers and what the backend wants to look at when retrieving chunks
of data from the TOAST relations. That's the main proposal of this
patch set, with a structure looking like that:
typedef struct toast_external_data
{
/* Original data size (includes header) */
int32 rawsize;
/* External saved size (without header) */
uint32 extsize;
/* compression method */
ToastCompressionId compression_method;
/* Relation OID of TOAST table containing the value */
Oid toastrelid;
/*
* Unique ID of value within TOAST table. This could be an OID or an
* int8 value. This field is large enough to be able to store any of
* them.
*/
uint64 value;
} toast_external_data;

This is a bit similar to what the code does for R/W and R/O vartags,
only applying to the on-disk external pointers. Then, the [de]toast
code and extension code is updated so as varlenas are changed into
this structure if we need to retrieve some of its data, and these
areas of the code do not need to know about the details of the
external TOAST pointers. When saving an external set of chunks, this
structure is filled with information depending on what
toast_save_datum() deals with, be it a short varlena, a non-compressed
external value, or a compressed external value, then builds a varlena
with the vartag we want.

External TOAST pointers have three properties that are hardcoded in
the tree, bringing some challenges of their own:
- The maximum size of a chunk, TOAST_MAX_CHUNK_SIZE, tweaked at close
to 2k to make 4 chunks fit on a page. This depends on the size of the
external pointer. This one was actually easy to refactor.
- The varlena header size, based on VARTAG_SIZE(), which is kind of
tricky to refactor out in the new toast_external.c, but that seems OK
even if this knowledge stays in varatt.h.
- The toast pointer size, aka TOAST_POINTER_SIZE. This one is
actually very interesting (tricky): we use it in one place,
toast_tuple_find_biggest_attribute(), as a lower bound to decide if an
attribute should be toastable or not. I've refactored the code to use
a "best" guess depending on the value type in the TOAST relation, but
that's not 100% waterproof. That needs more thoughts.

Anyway, the patch set is able to demonstrate how much needs to be done
in the tree to support multiple chunk_id types, and the CI is happy
with the attached. Some of the things done:
- Introduction of a user-settable GUC called default_toast_type, that
can be switched between two modes "oid" and "int8", to force the
creation of a TOAST relation using one type or the other.
- Dump, restore and upgrade support are integrated, relying on a GUC
makes the logic a breeze.
- 64b values are retrieved from a single counter in the control file,
named a "TOAST counter", which has the same reliability and properties
as an OID, with checkpoint support, WAL records, etc.
- Rewrites are soft, so I have kicked the can down the toast on this
point to not make the proposal more complicated than it should be: a
VACUUM FULL retains the same TOAST value type as the original. We
could extend rewrites so as the type of TOAST value is changed. It is
possible to setup a new cluster with default_toast_type = int8 set
after an upgrade, with the existing tables still using the OID mode.
This relates to the recent proposal with a concurrent VACUUM FULL
(REPACK discussion).

The patch set keeps the existing vartag_external with OID values for
backward-compatibility, and adds a second vartag_external that can
store 8-byte values. This model is the simplest one, and
optimizations are possible, where the Datum TOAST pointer could be
shorter depending on the ID type (OID or int8), the compression method
and the actual value to divide in chunks. For example, if you know
that a chunk of data to save has a value less than UINT32_MAX, we
could store 4 bytes worth of data instead of 8. This design has the
advantage to allow plugging in new TOAST external structures easily.
Now I've not spent extra time in this tuning, because there's no point
in spending more time without an actual agreement about three things,
and *that's what I'm looking for* as feedback for this upcoming CF:
- The structures of the external TOAST pointers. Variable-sized
pointers could be one possibility, across multiple vartags. Ideas are
welcome.
- How many vartag_external types we want.
- If people are actually OK with this translation layer or not, and I
don't disagree that there may be some paths hot enough where the
translation between the on-disk varlenas and this on-memory
toast_external_data hurts. Again, it is possible to hardcode more
types of vartags in the tree, or just bypass the translation in the
paths that are too hot. That's doable still brutal, but if that's the
final consensus reached I'm OK with that as well. (See for example
the changes in amcheck to see how simpler things get.)

The patch set has been divided into multiple pieces to ease its
review. Again, I'm not completely happy with everything in it, but
it's a start. Each patch has its own commit message, so feel free to
refer to them for more details:
- 0001 introduces the GUC default_toast_type. It is just defined, not
used in the tree at this stage.
- 0002 adds support for catcache lookups for int8 values, required to
allow TOAST values with int8 and its indexes. Potentially useful for
extensions.
- 0003 introduces the "TOAST counter", 8 bytes in the control file to
allocate values for the int8 chunk_id. That's cheap, reliable.
- 0004 is a mechanical change, that enlarges a couple of TOAST
interfaces to use values of uint64 instead of OID.
- 0005, again a mechanical change, reducing a bit the footprint of
TOAST_MAX_CHUNK_SIZE because OID and int8 values need different
values.
- 0006 tweaks pg_column_toast_chunk_id() to use int8 as return type.

Then comes the "main" patches:
- 0007 adds support for int8 chunk_id in TOAST tables. This is mostly
a mechanical change. If applying the patches up to this point,
external Datums are applied to both OID and int8 values. Note that
there is one tweak I'm unhappy with: the toast counter generation
would need to be smarter to avoid concurrent values because we don't
cross-check the TOAST index for existing values. (Sorry, got slightly
lazy here).
- 0008 adds tests for external compressed and uncompressed TOAST
values for int8 TOAST types.
- 0009 adds support for dump, restore, upgrades of the TOAST table
types.
- 0010 is the main dish: refactoring of the TOAST code to use
toast_external_data, with OID vartags as the only type defined.
- 0011 adds a second vartag_external: the one with int8 values stored
in the external TOAST pointer.
- 0012 is a bonus for amcheck: what needs to be done in its TAP tests
to allow the corruption cases to work when supporting a new vartag.

That was a long message. Thank you for reading if you have reached
this point.

Regards,

[1]: /messages/by-id/CAFAfj_HX84EK4hyRYw50AOHOcdVi-+FFwAAPo7JHx4aShCvunQ@mail.gmail.com -- Michael
--
Michael

Attachments:

v1-0001-Add-GUC-default_toast_type.patchtext/x-diff; charset=us-asciiDownload
From b79fbad6c089e2ea050fcac532db12a53abf8f1a Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 18 Jun 2025 16:15:19 +0900
Subject: [PATCH v1 01/12] Add GUC default_toast_type

This GUC controls the data type used for newly-created TOAST values,
with two modes supported: "oid" and "int8".  This will be used by an
upcoming patch.
---
 src/include/access/toast_type.h               | 30 +++++++++++++++++++
 src/backend/catalog/toasting.c                |  4 +++
 src/backend/utils/misc/guc_tables.c           | 19 ++++++++++++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 doc/src/sgml/config.sgml                      | 17 +++++++++++
 5 files changed, 71 insertions(+)
 create mode 100644 src/include/access/toast_type.h

diff --git a/src/include/access/toast_type.h b/src/include/access/toast_type.h
new file mode 100644
index 000000000000..494c2a3e852e
--- /dev/null
+++ b/src/include/access/toast_type.h
@@ -0,0 +1,30 @@
+/*-------------------------------------------------------------------------
+ *
+ * toast_type.h
+ *	  Internal definitions for the types supported by values in TOAST
+ *	  relations.
+ *
+ * Copyright (c) 2000-2025, PostgreSQL Global Development Group
+ *
+ * src/include/access/toast_type.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef TOAST_TYPE_H
+#define TOAST_TYPE_H
+
+/*
+ * GUC support
+ *
+ * Detault value type in toast table.
+ */
+extern PGDLLIMPORT int default_toast_type;
+
+typedef enum ToastTypeId
+{
+	TOAST_TYPE_INVALID = 0,
+	TOAST_TYPE_OID = 1,
+	TOAST_TYPE_INT8 = 2,
+} ToastTypeId;
+
+#endif							/* TOAST_TYPE_H */
diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c
index 874a8fc89adb..e595cb61b375 100644
--- a/src/backend/catalog/toasting.c
+++ b/src/backend/catalog/toasting.c
@@ -16,6 +16,7 @@
 
 #include "access/heapam.h"
 #include "access/toast_compression.h"
+#include "access/toast_type.h"
 #include "access/xact.h"
 #include "catalog/binary_upgrade.h"
 #include "catalog/catalog.h"
@@ -33,6 +34,9 @@
 #include "utils/rel.h"
 #include "utils/syscache.h"
 
+/* GUC support */
+int			default_toast_type = TOAST_TYPE_OID;
+
 static void CheckAndCreateToastTable(Oid relOid, Datum reloptions,
 									 LOCKMODE lockmode, bool check,
 									 Oid OIDOldToast);
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index f04bfedb2fd1..146e3bc0fa93 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -33,6 +33,7 @@
 #include "access/gin.h"
 #include "access/slru.h"
 #include "access/toast_compression.h"
+#include "access/toast_type.h"
 #include "access/twophase.h"
 #include "access/xlog_internal.h"
 #include "access/xlogprefetcher.h"
@@ -464,6 +465,13 @@ static const struct config_enum_entry default_toast_compression_options[] = {
 	{NULL, 0, false}
 };
 
+
+static const struct config_enum_entry default_toast_type_options[] = {
+	{"oid", TOAST_TYPE_OID, false},
+	{"int8", TOAST_TYPE_INT8, false},
+	{NULL, 0, false}
+};
+
 static const struct config_enum_entry wal_compression_options[] = {
 	{"pglz", WAL_COMPRESSION_PGLZ, false},
 #ifdef USE_LZ4
@@ -5058,6 +5066,17 @@ struct config_enum ConfigureNamesEnum[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"default_toast_type", PGC_USERSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Sets the default type used for TOAST values."),
+			NULL
+		},
+		&default_toast_type,
+		TOAST_TYPE_OID,
+		default_toast_type_options,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"default_transaction_isolation", PGC_USERSET, CLIENT_CONN_STATEMENT,
 			gettext_noop("Sets the transaction isolation level of each new transaction."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 341f88adc87b..4fbf76e48ec4 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -753,6 +753,7 @@ autovacuum_worker_slots = 16	# autovacuum worker slots to allocate
 #default_table_access_method = 'heap'
 #default_tablespace = ''		# a tablespace name, '' uses the default
 #default_toast_compression = 'pglz'	# 'pglz' or 'lz4'
+#default_toast_type = 'oid'		# 'oid' or 'int8'
 #temp_tablespaces = ''			# a list of tablespace names, '' uses
 					# only default tablespace
 #check_function_bodies = on
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index b265cc89c9d4..dd0519774a09 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9827,6 +9827,23 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-default-toast-type" xreflabel="default_toast_type">
+      <term><varname>default_toast_type</varname> (<type>enum</type>)
+      <indexterm>
+       <primary><varname>default_toast_type</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        This variable sets the default type for
+        <link linkend="storage-toast">TOAST</link> values.
+        The value types supported are <literal>oid</literal> and
+        <literal>int8</literal>.
+        The default is <literal>oid</literal>.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-temp-tablespaces" xreflabel="temp_tablespaces">
       <term><varname>temp_tablespaces</varname> (<type>string</type>)
       <indexterm>
-- 
2.49.0

v1-0002-Add-catcache-support-for-INT8OID.patchtext/x-diff; charset=us-asciiDownload
From c68f293df7ddaa9010b87e64f92e4a62e267d101 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 18 Jun 2025 16:12:11 +0900
Subject: [PATCH v1 02/12] Add catcache support for INT8OID

This is required to be able to do catalog cache lookups of int8 fields
for toast values of the same type.
---
 src/backend/utils/cache/catcache.c | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index 657648996c23..caab559e1a8d 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -240,6 +240,18 @@ int4hashfast(Datum datum)
 	return murmurhash32((int32) DatumGetInt32(datum));
 }
 
+static bool
+int8eqfast(Datum a, Datum b)
+{
+	return DatumGetInt64(a) == DatumGetInt64(b);
+}
+
+static uint32
+int8hashfast(Datum datum)
+{
+	return murmurhash64((int64) DatumGetInt64(datum));
+}
+
 static bool
 texteqfast(Datum a, Datum b)
 {
@@ -300,6 +312,11 @@ GetCCHashEqFuncs(Oid keytype, CCHashFN *hashfunc, RegProcedure *eqfunc, CCFastEq
 			*fasteqfunc = int4eqfast;
 			*eqfunc = F_INT4EQ;
 			break;
+		case INT8OID:
+			*hashfunc = int8hashfast;
+			*fasteqfunc = int8eqfast;
+			*eqfunc = F_INT8EQ;
+			break;
 		case TEXTOID:
 			*hashfunc = texthashfast;
 			*fasteqfunc = texteqfast;
-- 
2.49.0

v1-0003-Introduce-global-64-bit-TOAST-ID-counter-in-contr.patchtext/x-diff; charset=us-asciiDownload
From 76f663bcfbb65315af093e0c596393ca3b6b4490 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 18 Jun 2025 16:23:16 +0900
Subject: [PATCH v1 03/12] Introduce global 64-bit TOAST ID counter in control
 file

An 8 byte counter is added to the control file, providing a unique
64-bit-wide source for toast value IDs, with the same guarantees as OIDs
in terms of durability.  SQL functions and tools looking at the control
file are updated.  A WAL record is generated every 8k values generated,
that can be adjusted if required.

Requires a bump of WAL format.
Requires a bump of control file version.
Requires a catalog version bump.
---
 src/include/access/toast_counter.h            | 35 +++++++
 src/include/access/xlog.h                     |  1 +
 src/include/catalog/pg_control.h              |  4 +-
 src/include/catalog/pg_proc.dat               |  6 +-
 src/include/storage/lwlocklist.h              |  1 +
 src/backend/access/common/Makefile            |  1 +
 src/backend/access/common/meson.build         |  1 +
 src/backend/access/common/toast_counter.c     | 98 +++++++++++++++++++
 src/backend/access/rmgrdesc/xlogdesc.c        | 10 ++
 src/backend/access/transam/xlog.c             | 44 +++++++++
 src/backend/replication/logical/decode.c      |  1 +
 src/backend/storage/ipc/ipci.c                |  5 +-
 .../utils/activity/wait_event_names.txt       |  1 +
 src/backend/utils/misc/pg_controldata.c       | 23 +++--
 src/bin/pg_controldata/pg_controldata.c       |  2 +
 src/bin/pg_resetwal/pg_resetwal.c             |  2 +
 doc/src/sgml/func.sgml                        |  5 +
 src/tools/pgindent/typedefs.list              |  1 +
 18 files changed, 226 insertions(+), 15 deletions(-)
 create mode 100644 src/include/access/toast_counter.h
 create mode 100644 src/backend/access/common/toast_counter.c

diff --git a/src/include/access/toast_counter.h b/src/include/access/toast_counter.h
new file mode 100644
index 000000000000..80749cba0f87
--- /dev/null
+++ b/src/include/access/toast_counter.h
@@ -0,0 +1,35 @@
+/*-------------------------------------------------------------------------
+ *
+ * toast_counter.h
+ *	  Machinery for TOAST value counter.
+ *
+ * Copyright (c) 2000-2025, PostgreSQL Global Development Group
+ *
+ * src/include/access/toast_counter.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef TOAST_COUNTER_H
+#define TOAST_COUNTER_H
+
+#define InvalidToastId	0		/* Invalid TOAST value ID */
+#define FirstToastId	1		/* First TOAST value ID assigned */
+
+/*
+ * Structure in shared memory to track TOAST value counter activity.
+ * These are protected by ToastIdGenLock.
+ */
+typedef struct ToastCounterData
+{
+	uint64		nextId;			/* next TOAST value ID to assign */
+	uint32		idCount;		/* IDs available before WAL work */
+} ToastCounterData;
+
+extern PGDLLIMPORT ToastCounterData *ToastCounter;
+
+/* external declarations */
+extern Size ToastCounterShmemSize(void);
+extern void ToastCounterShmemInit(void);
+extern uint64 GetNewToastId(void);
+
+#endif							/* TOAST_TYPE_H */
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index d313099c027f..a50296736242 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -245,6 +245,7 @@ extern bool CreateCheckPoint(int flags);
 extern bool CreateRestartPoint(int flags);
 extern WALAvailability GetWALAvailability(XLogRecPtr targetLSN);
 extern void XLogPutNextOid(Oid nextOid);
+extern void XLogPutNextToastId(uint64 nextId);
 extern XLogRecPtr XLogRestorePoint(const char *rpName);
 extern void UpdateFullPageWrites(void);
 extern void GetFullPageWriteInfo(XLogRecPtr *RedoRecPtr_p, bool *doPageWrites_p);
diff --git a/src/include/catalog/pg_control.h b/src/include/catalog/pg_control.h
index 63e834a6ce47..1194b4928155 100644
--- a/src/include/catalog/pg_control.h
+++ b/src/include/catalog/pg_control.h
@@ -22,7 +22,7 @@
 
 
 /* Version identifier for this pg_control format */
-#define PG_CONTROL_VERSION	1800
+#define PG_CONTROL_VERSION	1900
 
 /* Nonce key length, see below */
 #define MOCK_AUTH_NONCE_LEN		32
@@ -45,6 +45,7 @@ typedef struct CheckPoint
 	Oid			nextOid;		/* next free OID */
 	MultiXactId nextMulti;		/* next free MultiXactId */
 	MultiXactOffset nextMultiOffset;	/* next free MultiXact offset */
+	uint64		nextToastId;	/* next free TOAST ID */
 	TransactionId oldestXid;	/* cluster-wide minimum datfrozenxid */
 	Oid			oldestXidDB;	/* database with minimum datfrozenxid */
 	MultiXactId oldestMulti;	/* cluster-wide minimum datminmxid */
@@ -80,6 +81,7 @@ typedef struct CheckPoint
 /* 0xC0 is used in Postgres 9.5-11 */
 #define XLOG_OVERWRITE_CONTRECORD		0xD0
 #define XLOG_CHECKPOINT_REDO			0xE0
+#define XLOG_NEXT_TOAST_ID				0xF0
 
 
 /*
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index d3d28a263fa9..bdfdddd76fc7 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12286,9 +12286,9 @@
   descr => 'pg_controldata checkpoint state information as a function',
   proname => 'pg_control_checkpoint', provolatile => 'v',
   prorettype => 'record', proargtypes => '',
-  proallargtypes => '{pg_lsn,pg_lsn,text,int4,int4,bool,text,oid,xid,xid,xid,oid,xid,xid,oid,xid,xid,timestamptz}',
-  proargmodes => '{o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
-  proargnames => '{checkpoint_lsn,redo_lsn,redo_wal_file,timeline_id,prev_timeline_id,full_page_writes,next_xid,next_oid,next_multixact_id,next_multi_offset,oldest_xid,oldest_xid_dbid,oldest_active_xid,oldest_multi_xid,oldest_multi_dbid,oldest_commit_ts_xid,newest_commit_ts_xid,checkpoint_time}',
+  proallargtypes => '{pg_lsn,pg_lsn,text,int4,int4,bool,text,oid,xid,xid,int8,xid,oid,xid,xid,oid,xid,xid,timestamptz}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{checkpoint_lsn,redo_lsn,redo_wal_file,timeline_id,prev_timeline_id,full_page_writes,next_xid,next_oid,next_multixact_id,next_multi_offset,next_toast_id,oldest_xid,oldest_xid_dbid,oldest_active_xid,oldest_multi_xid,oldest_multi_dbid,oldest_commit_ts_xid,newest_commit_ts_xid,checkpoint_time}',
   prosrc => 'pg_control_checkpoint' },
 
 { oid => '3443',
diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h
index a9681738146e..7f7ca92382b5 100644
--- a/src/include/storage/lwlocklist.h
+++ b/src/include/storage/lwlocklist.h
@@ -84,3 +84,4 @@ PG_LWLOCK(50, DSMRegistry)
 PG_LWLOCK(51, InjectionPoint)
 PG_LWLOCK(52, SerialControl)
 PG_LWLOCK(53, AioWorkerSubmissionQueue)
+PG_LWLOCK(54, ToastIdGen)
diff --git a/src/backend/access/common/Makefile b/src/backend/access/common/Makefile
index e78de312659e..2fd4f4460b65 100644
--- a/src/backend/access/common/Makefile
+++ b/src/backend/access/common/Makefile
@@ -27,6 +27,7 @@ OBJS = \
 	syncscan.o \
 	tidstore.o \
 	toast_compression.o \
+	toast_counter.o \
 	toast_internals.o \
 	tupconvert.o \
 	tupdesc.o
diff --git a/src/backend/access/common/meson.build b/src/backend/access/common/meson.build
index e3cdbe7a22e1..e6143d9ffb83 100644
--- a/src/backend/access/common/meson.build
+++ b/src/backend/access/common/meson.build
@@ -15,6 +15,7 @@ backend_sources += files(
   'syncscan.c',
   'tidstore.c',
   'toast_compression.c',
+  'toast_counter.c',
   'toast_internals.c',
   'tupconvert.c',
   'tupdesc.c',
diff --git a/src/backend/access/common/toast_counter.c b/src/backend/access/common/toast_counter.c
new file mode 100644
index 000000000000..94d361d0d5c4
--- /dev/null
+++ b/src/backend/access/common/toast_counter.c
@@ -0,0 +1,98 @@
+/*-------------------------------------------------------------------------
+ *
+ * toast_counter.c
+ *	  Functions for TOAST value counter.
+ *
+ * Copyright (c) 2025, PostgreSQL Global Development Group
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/common/toast_counter.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/toast_counter.h"
+#include "access/xlog.h"
+#include "miscadmin.h"
+#include "storage/lwlock.h"
+#include "storage/shmem.h"
+
+/* Number of TOAST values to preallocate before WAL work */
+#define TOAST_ID_PREFETCH		8192
+
+/* pointer to variables struct in shared memory */
+ToastCounterData *ToastCounter = NULL;
+
+/*
+ * Initialization of shared memory for ToastCounter.
+ */
+Size
+ToastCounterShmemSize(void)
+{
+	return sizeof(ToastCounterData);
+}
+
+void
+ToastCounterShmemInit(void)
+{
+	bool		found;
+
+	/* Initialize shared state struct */
+	ToastCounter = ShmemInitStruct("ToastCounter",
+								   sizeof(ToastCounterData),
+								   &found);
+	if (!IsUnderPostmaster)
+	{
+		Assert(!found);
+		memset(ToastCounter, 0, sizeof(ToastCounterData));
+	}
+	else
+		Assert(found);
+}
+
+/*
+ * GetNewToastId
+ *
+ * Toast IDs are generated as a cluster-wide counter.  They are 64 bits
+ * wide, hence wraparound will unlikely happen.
+ */
+uint64
+GetNewToastId(void)
+{
+	uint64		result;
+
+	if (RecoveryInProgress())
+		elog(ERROR, "cannot assign TOAST IDs during recovery");
+
+	LWLockAcquire(ToastIdGenLock, LW_EXCLUSIVE);
+
+	/*
+	 * Check for initialization or wraparound of the toast counter ID.
+	 * InvalidToastId (0) should never be returned.  We are 64 bit-wide, hence
+	 * wraparound is unlikely going to happen, but this check is cheap so
+	 * let's play it safe.
+	 */
+	if (ToastCounter->nextId < ((uint64) FirstToastId))
+	{
+		/* Most-likely first bootstrap or initdb assignment */
+		ToastCounter->nextId = FirstToastId;
+		ToastCounter->idCount = 0;
+	}
+
+	/* If running out of logged for TOAST IDs, log more */
+	if (ToastCounter->idCount == 0)
+	{
+		XLogPutNextToastId(ToastCounter->nextId + TOAST_ID_PREFETCH);
+		ToastCounter->idCount = TOAST_ID_PREFETCH;
+	}
+
+	result = ToastCounter->nextId;
+	(ToastCounter->nextId)++;
+	(ToastCounter->idCount)--;
+
+	LWLockRelease(ToastIdGenLock);
+
+	return result;
+}
diff --git a/src/backend/access/rmgrdesc/xlogdesc.c b/src/backend/access/rmgrdesc/xlogdesc.c
index 58040f28656f..0940040b33ab 100644
--- a/src/backend/access/rmgrdesc/xlogdesc.c
+++ b/src/backend/access/rmgrdesc/xlogdesc.c
@@ -96,6 +96,13 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
 		memcpy(&nextOid, rec, sizeof(Oid));
 		appendStringInfo(buf, "%u", nextOid);
 	}
+	else if (info == XLOG_NEXT_TOAST_ID)
+	{
+		uint64		nextId;
+
+		memcpy(&nextId, rec, sizeof(uint64));
+		appendStringInfo(buf, "%" PRIu64, nextId);
+	}
 	else if (info == XLOG_RESTORE_POINT)
 	{
 		xl_restore_point *xlrec = (xl_restore_point *) rec;
@@ -218,6 +225,9 @@ xlog_identify(uint8 info)
 		case XLOG_CHECKPOINT_REDO:
 			id = "CHECKPOINT_REDO";
 			break;
+		case XLOG_NEXT_TOAST_ID:
+			id = "NEXT_TOAST_ID";
+			break;
 	}
 
 	return id;
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 47ffc0a23077..4ae1ef8bb2c2 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -53,6 +53,7 @@
 #include "access/rewriteheap.h"
 #include "access/subtrans.h"
 #include "access/timeline.h"
+#include "access/toast_counter.h"
 #include "access/transam.h"
 #include "access/twophase.h"
 #include "access/xact.h"
@@ -5269,6 +5270,7 @@ BootStrapXLOG(uint32 data_checksum_version)
 	checkPoint.nextOid = FirstGenbkiObjectId;
 	checkPoint.nextMulti = FirstMultiXactId;
 	checkPoint.nextMultiOffset = 0;
+	checkPoint.nextToastId = FirstToastId;
 	checkPoint.oldestXid = FirstNormalTransactionId;
 	checkPoint.oldestXidDB = Template1DbOid;
 	checkPoint.oldestMulti = FirstMultiXactId;
@@ -5281,6 +5283,10 @@ BootStrapXLOG(uint32 data_checksum_version)
 	TransamVariables->nextXid = checkPoint.nextXid;
 	TransamVariables->nextOid = checkPoint.nextOid;
 	TransamVariables->oidCount = 0;
+
+	ToastCounter->nextId = checkPoint.nextToastId;
+	ToastCounter->idCount = 0;
+
 	MultiXactSetNextMXact(checkPoint.nextMulti, checkPoint.nextMultiOffset);
 	AdvanceOldestClogXid(checkPoint.oldestXid);
 	SetTransactionIdLimit(checkPoint.oldestXid, checkPoint.oldestXidDB);
@@ -5757,6 +5763,8 @@ StartupXLOG(void)
 	TransamVariables->nextXid = checkPoint.nextXid;
 	TransamVariables->nextOid = checkPoint.nextOid;
 	TransamVariables->oidCount = 0;
+	ToastCounter->nextId = checkPoint.nextToastId;
+	ToastCounter->idCount = 0;
 	MultiXactSetNextMXact(checkPoint.nextMulti, checkPoint.nextMultiOffset);
 	AdvanceOldestClogXid(checkPoint.oldestXid);
 	SetTransactionIdLimit(checkPoint.oldestXid, checkPoint.oldestXidDB);
@@ -7299,6 +7307,12 @@ CreateCheckPoint(int flags)
 		checkPoint.nextOid += TransamVariables->oidCount;
 	LWLockRelease(OidGenLock);
 
+	LWLockAcquire(ToastIdGenLock, LW_SHARED);
+	checkPoint.nextToastId = ToastCounter->nextId;
+	if (!shutdown)
+		checkPoint.nextToastId += ToastCounter->idCount;
+	LWLockRelease(ToastIdGenLock);
+
 	MultiXactGetCheckptMulti(shutdown,
 							 &checkPoint.nextMulti,
 							 &checkPoint.nextMultiOffset,
@@ -8238,6 +8252,22 @@ XLogPutNextOid(Oid nextOid)
 	 */
 }
 
+/*
+ * Write a NEXT_TOAST_ID log record.
+ */
+void
+XLogPutNextToastId(uint64 nextId)
+{
+	XLogBeginInsert();
+	XLogRegisterData(&nextId, sizeof(uint64));
+	(void) XLogInsert(RM_XLOG_ID, XLOG_NEXT_TOAST_ID);
+
+	/*
+	 * The next TOAST value ID is not flushed immediately, for the same reason
+	 * as above for the OIDs in XLogPutNextOid().
+	 */
+}
+
 /*
  * Write an XLOG SWITCH record.
  *
@@ -8453,6 +8483,16 @@ xlog_redo(XLogReaderState *record)
 		TransamVariables->oidCount = 0;
 		LWLockRelease(OidGenLock);
 	}
+	else if (info == XLOG_NEXT_TOAST_ID)
+	{
+		uint64		nextToastId;
+
+		memcpy(&nextToastId, XLogRecGetData(record), sizeof(uint64));
+		LWLockAcquire(ToastIdGenLock, LW_EXCLUSIVE);
+		ToastCounter->nextId = nextToastId;
+		ToastCounter->idCount = 0;
+		LWLockRelease(ToastIdGenLock);
+	}
 	else if (info == XLOG_CHECKPOINT_SHUTDOWN)
 	{
 		CheckPoint	checkPoint;
@@ -8467,6 +8507,10 @@ xlog_redo(XLogReaderState *record)
 		TransamVariables->nextOid = checkPoint.nextOid;
 		TransamVariables->oidCount = 0;
 		LWLockRelease(OidGenLock);
+		LWLockAcquire(ToastIdGenLock, LW_EXCLUSIVE);
+		ToastCounter->nextId = checkPoint.nextToastId;
+		ToastCounter->idCount = 0;
+		LWLockRelease(ToastIdGenLock);
 		MultiXactSetNextMXact(checkPoint.nextMulti,
 							  checkPoint.nextMultiOffset);
 
diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.c
index cc03f0706e9c..bb0337d37201 100644
--- a/src/backend/replication/logical/decode.c
+++ b/src/backend/replication/logical/decode.c
@@ -188,6 +188,7 @@ xlog_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
 		case XLOG_FPI:
 		case XLOG_OVERWRITE_CONTRECORD:
 		case XLOG_CHECKPOINT_REDO:
+		case XLOG_NEXT_TOAST_ID:
 			break;
 		default:
 			elog(ERROR, "unexpected RM_XLOG_ID record type: %u", info);
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index 2fa045e6b0f6..9102c267d7b0 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -20,6 +20,7 @@
 #include "access/nbtree.h"
 #include "access/subtrans.h"
 #include "access/syncscan.h"
+#include "access/toast_counter.h"
 #include "access/transam.h"
 #include "access/twophase.h"
 #include "access/xlogprefetcher.h"
@@ -119,6 +120,7 @@ CalculateShmemSize(int *num_semaphores)
 	size = add_size(size, ProcGlobalShmemSize());
 	size = add_size(size, XLogPrefetchShmemSize());
 	size = add_size(size, VarsupShmemSize());
+	size = add_size(size, ToastCounterShmemSize());
 	size = add_size(size, XLOGShmemSize());
 	size = add_size(size, XLogRecoveryShmemSize());
 	size = add_size(size, CLOGShmemSize());
@@ -280,8 +282,9 @@ CreateOrAttachShmemStructs(void)
 	DSMRegistryShmemInit();
 
 	/*
-	 * Set up xlog, clog, and buffers
+	 * Set up TOAST counter, xlog, clog, and buffers
 	 */
+	ToastCounterShmemInit();
 	VarsupShmemInit();
 	XLOGShmemInit();
 	XLogPrefetchShmemInit();
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 4da68312b5f9..9aa44de58770 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -352,6 +352,7 @@ DSMRegistry	"Waiting to read or update the dynamic shared memory registry."
 InjectionPoint	"Waiting to read or update information related to injection points."
 SerialControl	"Waiting to read or update shared <filename>pg_serial</filename> state."
 AioWorkerSubmissionQueue	"Waiting to access AIO worker submission queue."
+ToastIdGen	"Waiting to allocate a new TOAST value ID."
 
 #
 # END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE)
diff --git a/src/backend/utils/misc/pg_controldata.c b/src/backend/utils/misc/pg_controldata.c
index 6d036e3bf328..e4abf8593b8d 100644
--- a/src/backend/utils/misc/pg_controldata.c
+++ b/src/backend/utils/misc/pg_controldata.c
@@ -69,8 +69,8 @@ pg_control_system(PG_FUNCTION_ARGS)
 Datum
 pg_control_checkpoint(PG_FUNCTION_ARGS)
 {
-	Datum		values[18];
-	bool		nulls[18];
+	Datum		values[19];
+	bool		nulls[19];
 	TupleDesc	tupdesc;
 	HeapTuple	htup;
 	ControlFileData *ControlFile;
@@ -130,30 +130,33 @@ pg_control_checkpoint(PG_FUNCTION_ARGS)
 	values[9] = TransactionIdGetDatum(ControlFile->checkPointCopy.nextMultiOffset);
 	nulls[9] = false;
 
-	values[10] = TransactionIdGetDatum(ControlFile->checkPointCopy.oldestXid);
+	values[10] = UInt64GetDatum(ControlFile->checkPointCopy.nextToastId);
 	nulls[10] = false;
 
-	values[11] = ObjectIdGetDatum(ControlFile->checkPointCopy.oldestXidDB);
+	values[11] = TransactionIdGetDatum(ControlFile->checkPointCopy.oldestXid);
 	nulls[11] = false;
 
-	values[12] = TransactionIdGetDatum(ControlFile->checkPointCopy.oldestActiveXid);
+	values[12] = ObjectIdGetDatum(ControlFile->checkPointCopy.oldestXidDB);
 	nulls[12] = false;
 
-	values[13] = TransactionIdGetDatum(ControlFile->checkPointCopy.oldestMulti);
+	values[13] = TransactionIdGetDatum(ControlFile->checkPointCopy.oldestActiveXid);
 	nulls[13] = false;
 
-	values[14] = ObjectIdGetDatum(ControlFile->checkPointCopy.oldestMultiDB);
+	values[14] = TransactionIdGetDatum(ControlFile->checkPointCopy.oldestMulti);
 	nulls[14] = false;
 
-	values[15] = TransactionIdGetDatum(ControlFile->checkPointCopy.oldestCommitTsXid);
+	values[15] = ObjectIdGetDatum(ControlFile->checkPointCopy.oldestMultiDB);
 	nulls[15] = false;
 
-	values[16] = TransactionIdGetDatum(ControlFile->checkPointCopy.newestCommitTsXid);
+	values[16] = TransactionIdGetDatum(ControlFile->checkPointCopy.oldestCommitTsXid);
 	nulls[16] = false;
 
-	values[17] = TimestampTzGetDatum(time_t_to_timestamptz(ControlFile->checkPointCopy.time));
+	values[17] = TransactionIdGetDatum(ControlFile->checkPointCopy.newestCommitTsXid);
 	nulls[17] = false;
 
+	values[18] = TimestampTzGetDatum(time_t_to_timestamptz(ControlFile->checkPointCopy.time));
+	nulls[18] = false;
+
 	htup = heap_form_tuple(tupdesc, values, nulls);
 
 	PG_RETURN_DATUM(HeapTupleGetDatum(htup));
diff --git a/src/bin/pg_controldata/pg_controldata.c b/src/bin/pg_controldata/pg_controldata.c
index 7bb801bb8861..d83368ba4910 100644
--- a/src/bin/pg_controldata/pg_controldata.c
+++ b/src/bin/pg_controldata/pg_controldata.c
@@ -266,6 +266,8 @@ main(int argc, char *argv[])
 		   ControlFile->checkPointCopy.nextMulti);
 	printf(_("Latest checkpoint's NextMultiOffset:  %u\n"),
 		   ControlFile->checkPointCopy.nextMultiOffset);
+	printf(_("Latest checkpoint's NextToastID:      %" PRIu64 "\n"),
+		   ControlFile->checkPointCopy.nextToastId);
 	printf(_("Latest checkpoint's oldestXID:        %u\n"),
 		   ControlFile->checkPointCopy.oldestXid);
 	printf(_("Latest checkpoint's oldestXID's DB:   %u\n"),
diff --git a/src/bin/pg_resetwal/pg_resetwal.c b/src/bin/pg_resetwal/pg_resetwal.c
index e876f35f38ed..bb324c710911 100644
--- a/src/bin/pg_resetwal/pg_resetwal.c
+++ b/src/bin/pg_resetwal/pg_resetwal.c
@@ -45,6 +45,7 @@
 
 #include "access/heaptoast.h"
 #include "access/multixact.h"
+#include "access/toast_counter.h"
 #include "access/transam.h"
 #include "access/xlog.h"
 #include "access/xlog_internal.h"
@@ -686,6 +687,7 @@ GuessControlValues(void)
 	ControlFile.checkPointCopy.nextOid = FirstGenbkiObjectId;
 	ControlFile.checkPointCopy.nextMulti = FirstMultiXactId;
 	ControlFile.checkPointCopy.nextMultiOffset = 0;
+	ControlFile.checkPointCopy.nextToastId = FirstToastId;
 	ControlFile.checkPointCopy.oldestXid = FirstNormalTransactionId;
 	ControlFile.checkPointCopy.oldestXidDB = InvalidOid;
 	ControlFile.checkPointCopy.oldestMulti = FirstMultiXactId;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index c67688cbf5f9..ce982c210371 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -28104,6 +28104,11 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
        <entry><type>xid</type></entry>
       </row>
 
+      <row>
+       <entry><structfield>next_toast_id</structfield></entry>
+       <entry><type>bigint</type></entry>
+      </row>
+
       <row>
        <entry><structfield>oldest_xid</structfield></entry>
        <entry><type>xid</type></entry>
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 32d6e718adca..4e1d553643bf 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3044,6 +3044,7 @@ TmFromChar
 TmToChar
 ToastAttrInfo
 ToastCompressionId
+ToastCounterData
 ToastTupleContext
 ToastedAttribute
 TocEntry
-- 
2.49.0

v1-0004-Refactor-some-TOAST-value-ID-code-to-use-uint64-i.patchtext/x-diff; charset=us-asciiDownload
From 3b17dcc81355ed55aeb56896e97563787f5849c6 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 19 Jun 2025 09:57:19 +0900
Subject: [PATCH v1 04/12] Refactor some TOAST value ID code to use uint64
 instead of Oid

This change is a mechanical switch to change most of the code paths that
assume TOAST value IDs to be Oids to become uint64, easing an upcoming
change to allow 8-byte TOAST values.

The areas touched are related to table AM, amcheck and logical
decoding's reorder buffer.  A good chunk of the changes involve
switching printf() markers from %u to PRIu64.
---
 src/include/access/heaptoast.h                |  2 +-
 src/include/access/tableam.h                  |  4 +-
 src/backend/access/common/toast_internals.c   |  8 +--
 src/backend/access/heap/heaptoast.c           | 12 ++--
 .../replication/logical/reorderbuffer.c       | 14 ++--
 contrib/amcheck/verify_heapam.c               | 69 +++++++++++--------
 6 files changed, 62 insertions(+), 47 deletions(-)

diff --git a/src/include/access/heaptoast.h b/src/include/access/heaptoast.h
index 6385a27caf83..6e3558cbd6d2 100644
--- a/src/include/access/heaptoast.h
+++ b/src/include/access/heaptoast.h
@@ -142,7 +142,7 @@ extern HeapTuple toast_build_flattened_tuple(TupleDesc tupleDesc,
  *	Fetch a slice from a toast value stored in a heap table.
  * ----------
  */
-extern void heap_fetch_toast_slice(Relation toastrel, Oid valueid,
+extern void heap_fetch_toast_slice(Relation toastrel, uint64 valueid,
 								   int32 attrsize, int32 sliceoffset,
 								   int32 slicelength, struct varlena *result);
 
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index 8713e12cbfb9..3135b9d55cf0 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -740,7 +740,7 @@ typedef struct TableAmRoutine
 	 * table implemented by this AM.  See table_relation_fetch_toast_slice()
 	 * for more details.
 	 */
-	void		(*relation_fetch_toast_slice) (Relation toastrel, Oid valueid,
+	void		(*relation_fetch_toast_slice) (Relation toastrel, uint64 valueid,
 											   int32 attrsize,
 											   int32 sliceoffset,
 											   int32 slicelength,
@@ -1873,7 +1873,7 @@ table_relation_toast_am(Relation rel)
  * stored.
  */
 static inline void
-table_relation_fetch_toast_slice(Relation toastrel, Oid valueid,
+table_relation_fetch_toast_slice(Relation toastrel, uint64 valueid,
 								 int32 attrsize, int32 sliceoffset,
 								 int32 slicelength, struct varlena *result)
 {
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 7d8be8346ce5..4a1342da6e1b 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -26,8 +26,8 @@
 #include "utils/rel.h"
 #include "utils/snapmgr.h"
 
-static bool toastrel_valueid_exists(Relation toastrel, Oid valueid);
-static bool toastid_valueid_exists(Oid toastrelid, Oid valueid);
+static bool toastrel_valueid_exists(Relation toastrel, uint64 valueid);
+static bool toastid_valueid_exists(Oid toastrelid, uint64 valueid);
 
 /* ----------
  * toast_compress_datum -
@@ -456,7 +456,7 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
  * ----------
  */
 static bool
-toastrel_valueid_exists(Relation toastrel, Oid valueid)
+toastrel_valueid_exists(Relation toastrel, uint64 valueid)
 {
 	bool		result = false;
 	ScanKeyData toastkey;
@@ -504,7 +504,7 @@ toastrel_valueid_exists(Relation toastrel, Oid valueid)
  * ----------
  */
 static bool
-toastid_valueid_exists(Oid toastrelid, Oid valueid)
+toastid_valueid_exists(Oid toastrelid, uint64 valueid)
 {
 	bool		result;
 	Relation	toastrel;
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index cb1e57030f64..76936b2f4944 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -623,7 +623,7 @@ toast_build_flattened_tuple(TupleDesc tupleDesc,
  * result is the varlena into which the results should be written.
  */
 void
-heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
+heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 					   int32 sliceoffset, int32 slicelength,
 					   struct varlena *result)
 {
@@ -725,7 +725,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
 		else
 		{
 			/* should never happen */
-			elog(ERROR, "found toasted toast chunk for toast value %u in %s",
+			elog(ERROR, "found toasted toast chunk for toast value %" PRIu64 " in %s",
 				 valueid, RelationGetRelationName(toastrel));
 			chunksize = 0;		/* keep compiler quiet */
 			chunkdata = NULL;
@@ -737,13 +737,13 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
 		if (curchunk != expectedchunk)
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
-					 errmsg_internal("unexpected chunk number %d (expected %d) for toast value %u in %s",
+					 errmsg_internal("unexpected chunk number %d (expected %d) for toast value %" PRIu64 " in %s",
 									 curchunk, expectedchunk, valueid,
 									 RelationGetRelationName(toastrel))));
 		if (curchunk > endchunk)
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
-					 errmsg_internal("unexpected chunk number %d (out of range %d..%d) for toast value %u in %s",
+					 errmsg_internal("unexpected chunk number %d (out of range %d..%d) for toast value %" PRIu64 " in %s",
 									 curchunk,
 									 startchunk, endchunk, valueid,
 									 RelationGetRelationName(toastrel))));
@@ -752,7 +752,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
 		if (chunksize != expected_size)
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
-					 errmsg_internal("unexpected chunk size %d (expected %d) in chunk %d of %d for toast value %u in %s",
+					 errmsg_internal("unexpected chunk size %d (expected %d) in chunk %d of %d for toast value %" PRIu64 " in %s",
 									 chunksize, expected_size,
 									 curchunk, totalchunks, valueid,
 									 RelationGetRelationName(toastrel))));
@@ -781,7 +781,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
 	if (expectedchunk != (endchunk + 1))
 		ereport(ERROR,
 				(errcode(ERRCODE_DATA_CORRUPTED),
-				 errmsg_internal("missing chunk number %d for toast value %u in %s",
+				 errmsg_internal("missing chunk number %d for toast value %" PRIu64 " in %s",
 								 expectedchunk, valueid,
 								 RelationGetRelationName(toastrel))));
 
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index c4299c76fb16..5ba1ca78ff67 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -176,7 +176,7 @@ typedef struct ReorderBufferIterTXNState
 /* toast datastructures */
 typedef struct ReorderBufferToastEnt
 {
-	Oid			chunk_id;		/* toast_table.chunk_id */
+	uint64		chunk_id;		/* toast_table.chunk_id */
 	int32		last_chunk_seq; /* toast_table.chunk_seq of the last chunk we
 								 * have seen */
 	Size		num_chunks;		/* number of chunks we've already seen */
@@ -4944,7 +4944,7 @@ ReorderBufferToastInitHash(ReorderBuffer *rb, ReorderBufferTXN *txn)
 
 	Assert(txn->toast_hash == NULL);
 
-	hash_ctl.keysize = sizeof(Oid);
+	hash_ctl.keysize = sizeof(uint64);
 	hash_ctl.entrysize = sizeof(ReorderBufferToastEnt);
 	hash_ctl.hcxt = rb->context;
 	txn->toast_hash = hash_create("ReorderBufferToastHash", 5, &hash_ctl,
@@ -4968,7 +4968,7 @@ ReorderBufferToastAppendChunk(ReorderBuffer *rb, ReorderBufferTXN *txn,
 	bool		isnull;
 	Pointer		chunk;
 	TupleDesc	desc = RelationGetDescr(relation);
-	Oid			chunk_id;
+	uint64		chunk_id;
 	int32		chunk_seq;
 
 	if (txn->toast_hash == NULL)
@@ -4995,11 +4995,11 @@ ReorderBufferToastAppendChunk(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		dlist_init(&ent->chunks);
 
 		if (chunk_seq != 0)
-			elog(ERROR, "got sequence entry %d for toast chunk %u instead of seq 0",
+			elog(ERROR, "got sequence entry %d for toast chunk %" PRIu64 " instead of seq 0",
 				 chunk_seq, chunk_id);
 	}
 	else if (found && chunk_seq != ent->last_chunk_seq + 1)
-		elog(ERROR, "got sequence entry %d for toast chunk %u instead of seq %d",
+		elog(ERROR, "got sequence entry %d for toast chunk %" PRIu64 " instead of seq %d",
 			 chunk_seq, chunk_id, ent->last_chunk_seq + 1);
 
 	chunk = DatumGetPointer(fastgetattr(newtup, 3, desc, &isnull));
@@ -5108,6 +5108,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		struct varlena *reconstructed;
 		dlist_iter	it;
 		Size		data_done = 0;
+		uint64		toast_valueid;
 
 		/* system columns aren't toasted */
 		if (attr->attnum < 0)
@@ -5132,13 +5133,14 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 			continue;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, varlena);
+		toast_valueid = toast_pointer.va_valueid;
 
 		/*
 		 * Check whether the toast tuple changed, replace if so.
 		 */
 		ent = (ReorderBufferToastEnt *)
 			hash_search(txn->toast_hash,
-						&toast_pointer.va_valueid,
+						&toast_valueid,
 						HASH_FIND,
 						NULL);
 		if (ent == NULL)
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index aa9cccd1da4f..51ac416f7c6e 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1556,11 +1556,18 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 				  uint32 extsize)
 {
 	int32		chunk_seq;
-	int32		last_chunk_seq = (extsize - 1) / TOAST_MAX_CHUNK_SIZE;
+	int32		last_chunk_seq;
 	Pointer		chunk;
 	bool		isnull;
 	int32		chunksize;
 	int32		expected_size;
+	uint64		toast_valueid;
+	int32		max_chunk_size;
+
+	toast_valueid = ta->toast_pointer.va_valueid;
+
+	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	last_chunk_seq = (extsize - 1) / max_chunk_size;
 
 	/* Sanity-check the sequence number. */
 	chunk_seq = DatumGetInt32(fastgetattr(toasttup, 2,
@@ -1568,16 +1575,16 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	if (isnull)
 	{
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u has toast chunk with null sequence number",
-										 ta->toast_pointer.va_valueid));
+								psprintf("toast value %" PRIu64 " has toast chunk with null sequence number",
+										 toast_valueid));
 		return;
 	}
 	if (chunk_seq != *expected_chunk_seq)
 	{
 		/* Either the TOAST index is corrupt, or we don't have all chunks. */
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u index scan returned chunk %d when expecting chunk %d",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value %" PRIu64 " index scan returned chunk %d when expecting chunk %d",
+										 toast_valueid,
 										 chunk_seq, *expected_chunk_seq));
 	}
 	*expected_chunk_seq = chunk_seq + 1;
@@ -1588,8 +1595,8 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	if (isnull)
 	{
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u chunk %d has null data",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value %" PRIu64 " chunk %d has null data",
+										 toast_valueid,
 										 chunk_seq));
 		return;
 	}
@@ -1608,8 +1615,8 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 		uint32		header = ((varattrib_4b *) chunk)->va_4byte.va_header;
 
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u chunk %d has invalid varlena header %0x",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value %" PRIu64 " chunk %d has invalid varlena header %0x",
+										 toast_valueid,
 										 chunk_seq, header));
 		return;
 	}
@@ -1620,19 +1627,19 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	if (chunk_seq > last_chunk_seq)
 	{
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u chunk %d follows last expected chunk %d",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value %" PRIu64 " chunk %d follows last expected chunk %d",
+										 toast_valueid,
 										 chunk_seq, last_chunk_seq));
 		return;
 	}
 
-	expected_size = chunk_seq < last_chunk_seq ? TOAST_MAX_CHUNK_SIZE
-		: extsize - (last_chunk_seq * TOAST_MAX_CHUNK_SIZE);
+	expected_size = chunk_seq < last_chunk_seq ? max_chunk_size
+		: extsize - (last_chunk_seq * max_chunk_size);
 
 	if (chunksize != expected_size)
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u chunk %d has size %u, but expected size %u",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value %" PRIu64 " chunk %d has size %u, but expected size %u",
+										 toast_valueid,
 										 chunk_seq, chunksize, expected_size));
 }
 
@@ -1663,6 +1670,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	struct varlena *attr;
 	char	   *tp;				/* pointer to the tuple data */
 	uint16		infomask;
+	uint64		toast_pointer_valueid;
 	CompactAttribute *thisatt;
 	struct varatt_external toast_pointer;
 
@@ -1766,6 +1774,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 		return true;
 
 	/* It is external, and we're looking at a page on disk */
+	toast_pointer_valueid = toast_pointer.va_valueid;
 
 	/*
 	 * Must copy attr into toast_pointer for alignment considerations
@@ -1775,8 +1784,8 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	/* Toasted attributes too large to be untoasted should never be stored */
 	if (toast_pointer.va_rawsize > VARLENA_SIZE_LIMIT)
 		report_corruption(ctx,
-						  psprintf("toast value %u rawsize %d exceeds limit %d",
-								   toast_pointer.va_valueid,
+						  psprintf("toast value %" PRIu64 " rawsize %d exceeds limit %d",
+								   toast_pointer_valueid,
 								   toast_pointer.va_rawsize,
 								   VARLENA_SIZE_LIMIT));
 
@@ -1803,16 +1812,16 @@ check_tuple_attribute(HeapCheckContext *ctx)
 		}
 		if (!valid)
 			report_corruption(ctx,
-							  psprintf("toast value %u has invalid compression method id %d",
-									   toast_pointer.va_valueid, cmid));
+							  psprintf("toast value %" PRIu64 " has invalid compression method id %d",
+									   toast_pointer_valueid, cmid));
 	}
 
 	/* The tuple header better claim to contain toasted values */
 	if (!(infomask & HEAP_HASEXTERNAL))
 	{
 		report_corruption(ctx,
-						  psprintf("toast value %u is external but tuple header flag HEAP_HASEXTERNAL not set",
-								   toast_pointer.va_valueid));
+						  psprintf("toast value %" PRIu64 " is external but tuple header flag HEAP_HASEXTERNAL not set",
+								   toast_pointer_valueid));
 		return true;
 	}
 
@@ -1820,8 +1829,8 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	if (!ctx->rel->rd_rel->reltoastrelid)
 	{
 		report_corruption(ctx,
-						  psprintf("toast value %u is external but relation has no toast relation",
-								   toast_pointer.va_valueid));
+						  psprintf("toast value %" PRIu64 " is external but relation has no toast relation",
+								   toast_pointer_valueid));
 		return true;
 	}
 
@@ -1866,9 +1875,11 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	uint32		extsize;
 	int32		expected_chunk_seq = 0;
 	int32		last_chunk_seq;
+	uint64		toast_valueid;
+	int32		max_chunk_size = TOAST_MAX_CHUNK_SIZE;
 
 	extsize = VARATT_EXTERNAL_GET_EXTSIZE(ta->toast_pointer);
-	last_chunk_seq = (extsize - 1) / TOAST_MAX_CHUNK_SIZE;
+	last_chunk_seq = (extsize - 1) / max_chunk_size;
 
 	/*
 	 * Setup a scan key to find chunks in toast table with matching va_valueid
@@ -1896,14 +1907,16 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	}
 	systable_endscan_ordered(toastscan);
 
+	toast_valueid = ta->toast_pointer.va_valueid;
+
 	if (!found_toasttup)
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u not found in toast table",
-										 ta->toast_pointer.va_valueid));
+								psprintf("toast value %" PRIu64 " not found in toast table",
+										 toast_valueid));
 	else if (expected_chunk_seq <= last_chunk_seq)
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u was expected to end at chunk %d, but ended while expecting chunk %d",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value %" PRIu64 " was expected to end at chunk %d, but ended while expecting chunk %d",
+										 toast_valueid,
 										 last_chunk_seq, expected_chunk_seq));
 }
 
-- 
2.49.0

v1-0005-Minimize-footprint-of-TOAST_MAX_CHUNK_SIZE-in-hea.patchtext/x-diff; charset=us-asciiDownload
From a5c2a8e1835e879a9398ec9d13cc8a95bd675bec Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 19 Jun 2025 10:03:46 +0900
Subject: [PATCH v1 05/12] Minimize footprint of TOAST_MAX_CHUNK_SIZE in heap
 TOAST code

This eases a follow-up change to support 8-byte TOAST value IDs, as the
maximum chunk size allowed for a single chunk of TOASTed data depends on
the size of the value ID.
---
 src/backend/access/heap/heaptoast.c | 20 ++++++++++++--------
 1 file changed, 12 insertions(+), 8 deletions(-)

diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index 76936b2f4944..ae8d502ddcd3 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -634,11 +634,12 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 	SysScanDesc toastscan;
 	HeapTuple	ttup;
 	int32		expectedchunk;
-	int32		totalchunks = ((attrsize - 1) / TOAST_MAX_CHUNK_SIZE) + 1;
+	int32		totalchunks;
 	int			startchunk;
 	int			endchunk;
 	int			num_indexes;
 	int			validIndex;
+	int32		max_chunk_size;
 
 	/* Look for the valid index of toast relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -646,8 +647,11 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 									&toastidxs,
 									&num_indexes);
 
-	startchunk = sliceoffset / TOAST_MAX_CHUNK_SIZE;
-	endchunk = (sliceoffset + slicelength - 1) / TOAST_MAX_CHUNK_SIZE;
+	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+
+	totalchunks = ((attrsize - 1) / max_chunk_size) + 1;
+	startchunk = sliceoffset / max_chunk_size;
+	endchunk = (sliceoffset + slicelength - 1) / max_chunk_size;
 	Assert(endchunk <= totalchunks);
 
 	/* Set up a scan key to fetch from the index. */
@@ -747,8 +751,8 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 									 curchunk,
 									 startchunk, endchunk, valueid,
 									 RelationGetRelationName(toastrel))));
-		expected_size = curchunk < totalchunks - 1 ? TOAST_MAX_CHUNK_SIZE
-			: attrsize - ((totalchunks - 1) * TOAST_MAX_CHUNK_SIZE);
+		expected_size = curchunk < totalchunks - 1 ? max_chunk_size
+			: attrsize - ((totalchunks - 1) * max_chunk_size);
 		if (chunksize != expected_size)
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
@@ -763,12 +767,12 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 		chcpystrt = 0;
 		chcpyend = chunksize - 1;
 		if (curchunk == startchunk)
-			chcpystrt = sliceoffset % TOAST_MAX_CHUNK_SIZE;
+			chcpystrt = sliceoffset % max_chunk_size;
 		if (curchunk == endchunk)
-			chcpyend = (sliceoffset + slicelength - 1) % TOAST_MAX_CHUNK_SIZE;
+			chcpyend = (sliceoffset + slicelength - 1) % max_chunk_size;
 
 		memcpy(VARDATA(result) +
-			   (curchunk * TOAST_MAX_CHUNK_SIZE - sliceoffset) + chcpystrt,
+			   (curchunk * max_chunk_size - sliceoffset) + chcpystrt,
 			   chunkdata + chcpystrt,
 			   (chcpyend - chcpystrt) + 1);
 
-- 
2.49.0

v1-0006-Switch-pg_column_toast_chunk_id-return-value-from.patchtext/x-diff; charset=us-asciiDownload
From 86627050a0aed7490b3d39c7c7082290a404455d Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 19 Jun 2025 10:11:40 +0900
Subject: [PATCH v1 06/12] Switch pg_column_toast_chunk_id() return value from
 oid to bigint

This is required for a follow-up patch that will add support for 8-byte
TOAST values, with this function being changed so as it is able to
support the largest TOAST value type available.
---
 src/include/catalog/pg_proc.dat | 2 +-
 src/backend/utils/adt/varlena.c | 4 +++-
 doc/src/sgml/func.sgml          | 2 +-
 3 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index bdfdddd76fc7..5ff493420658 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7723,7 +7723,7 @@
   proargtypes => 'any', prosrc => 'pg_column_compression' },
 { oid => '6316', descr => 'chunk ID of on-disk TOASTed value',
   proname => 'pg_column_toast_chunk_id', provolatile => 's',
-  prorettype => 'oid', proargtypes => 'any',
+  prorettype => 'int8', proargtypes => 'any',
   prosrc => 'pg_column_toast_chunk_id' },
 { oid => '2322',
   descr => 'total disk space usage for the specified tablespace',
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 3e4d5568bde8..093994b1070f 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -5318,6 +5318,7 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 	int			typlen;
 	struct varlena *attr;
 	struct varatt_external toast_pointer;
+	uint64		toast_valueid;
 
 	/* On first call, get the input type's typlen, and save at *fn_extra */
 	if (fcinfo->flinfo->fn_extra == NULL)
@@ -5345,8 +5346,9 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 		PG_RETURN_NULL();
 
 	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_valueid = toast_pointer.va_valueid;
 
-	PG_RETURN_OID(toast_pointer.va_valueid);
+	PG_RETURN_UINT64(toast_valueid);
 }
 
 /*
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index ce982c210371..11736a181918 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30066,7 +30066,7 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
          <primary>pg_column_toast_chunk_id</primary>
         </indexterm>
         <function>pg_column_toast_chunk_id</function> ( <type>"any"</type> )
-        <returnvalue>oid</returnvalue>
+        <returnvalue>bigint</returnvalue>
        </para>
        <para>
         Shows the <structfield>chunk_id</structfield> of an on-disk
-- 
2.49.0

v1-0007-Add-support-for-bigint-TOAST-values.patchtext/x-diff; charset=us-asciiDownload
From b2a9bb0be2a32e679089d343cf31d0c61f4162ec Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 19 Jun 2025 12:08:27 +0900
Subject: [PATCH v1 07/12] Add support for bigint TOAST values

This commit adds the possibility to define TOAST tables with bigint as
value ID, baesd on the GUC default_toast_type.  All the external TOAST
pointers still rely on varatt_external and a single vartag, with all the
values inserted in the bigint TOAST tables fed from the existing OID
value generator.  This will be changed in an upcoming patch that adds
more vartag_external types and its associated structures, with the code
being able to use a different external TOAST pointer depending on the
attribute type of chunk_id in TOAST relations.

All the changes done here are mechanical, with all the TOAST code able
to do chunk ID lookups based on the two types supported.

XXX: Catalog version bump required.
---
 src/backend/access/common/toast_internals.c | 127 +++++++++++++++-----
 src/backend/access/heap/heaptoast.c         |  20 ++-
 src/backend/catalog/toasting.c              |  37 +++++-
 doc/src/sgml/storage.sgml                   |   7 +-
 contrib/amcheck/verify_heapam.c             |  19 ++-
 5 files changed, 166 insertions(+), 44 deletions(-)

diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 4a1342da6e1b..5e28a33557dc 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -18,6 +18,7 @@
 #include "access/heapam.h"
 #include "access/heaptoast.h"
 #include "access/table.h"
+#include "access/toast_counter.h"
 #include "access/toast_internals.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -143,6 +144,8 @@ toast_save_datum(Relation rel, Datum value,
 	Pointer		dval = DatumGetPointer(value);
 	int			num_indexes;
 	int			validIndex;
+	Oid			toast_typid;
+	uint64		new_valueid = 0;
 
 	Assert(!VARATT_IS_EXTERNAL(value));
 
@@ -154,6 +157,13 @@ toast_save_datum(Relation rel, Datum value,
 	toastrel = table_open(rel->rd_rel->reltoastrelid, RowExclusiveLock);
 	toasttupDesc = toastrel->rd_att;
 
+	/*
+	 * This varies depending on the attribute of the toast relation used for
+	 * its values.
+	 */
+	toast_typid = TupleDescAttr(toasttupDesc, 0)->atttypid;
+	Assert(toast_typid == OIDOID || toast_typid == INT8OID);
+
 	/* Open all the toast indexes and look for the valid one */
 	validIndex = toast_open_indexes(toastrel,
 									RowExclusiveLock,
@@ -212,29 +222,41 @@ toast_save_datum(Relation rel, Datum value,
 		toast_pointer.va_toastrelid = RelationGetRelid(toastrel);
 
 	/*
-	 * Choose an OID to use as the value ID for this toast value.
+	 * Choose a new value to use as the value ID for this toast value, be it
+	 * for OID or int8-based TOAST relations.
 	 *
-	 * Normally we just choose an unused OID within the toast table.  But
+	 * Normally we just choose an unused value within the toast table.  But
 	 * during table-rewriting operations where we are preserving an existing
-	 * toast table OID, we want to preserve toast value OIDs too.  So, if
+	 * toast table OID, we want to preserve toast value IDs too.  So, if
 	 * rd_toastoid is set and we had a prior external value from that same
 	 * toast table, re-use its value ID.  If we didn't have a prior external
 	 * value (which is a corner case, but possible if the table's attstorage
 	 * options have been changed), we have to pick a value ID that doesn't
-	 * conflict with either new or existing toast value OIDs.
+	 * conflict with either new or existing toast value IDs.  If the TOAST
+	 * table uses 8-byte value IDs, we should not really care much about
+	 * that.
 	 */
 	if (!OidIsValid(rel->rd_toastoid))
 	{
-		/* normal case: just choose an unused OID */
-		toast_pointer.va_valueid =
-			GetNewOidWithIndex(toastrel,
-							   RelationGetRelid(toastidxs[validIndex]),
-							   (AttrNumber) 1);
+		/* normal case: just choose an unused ID */
+		if (toast_typid == OIDOID)
+			new_valueid = GetNewOidWithIndex(toastrel,
+											 RelationGetRelid(toastidxs[validIndex]),
+											 (AttrNumber) 1);
+		else if (toast_typid == INT8OID)
+			new_valueid = GetNewToastId();
+		else
+			Assert(false);
 	}
 	else
 	{
 		/* rewrite case: check to see if value was in old toast table */
-		toast_pointer.va_valueid = InvalidOid;
+		if (toast_typid == OIDOID)
+			new_valueid = InvalidOid;
+		else if (toast_typid == INT8OID)
+			new_valueid = InvalidToastId;
+		else
+			Assert(false);
 		if (oldexternal != NULL)
 		{
 			struct varatt_external old_toast_pointer;
@@ -242,10 +264,16 @@ toast_save_datum(Relation rel, Datum value,
 			Assert(VARATT_IS_EXTERNAL_ONDISK(oldexternal));
 			/* Must copy to access aligned fields */
 			VARATT_EXTERNAL_GET_POINTER(old_toast_pointer, oldexternal);
+
 			if (old_toast_pointer.va_toastrelid == rel->rd_toastoid)
 			{
-				/* This value came from the old toast table; reuse its OID */
-				toast_pointer.va_valueid = old_toast_pointer.va_valueid;
+				uint64		old_valueid;
+
+				/*
+				 * The old and new toast relations match, hence their type
+				 * of value match as well.
+				 */
+				old_valueid = old_toast_pointer.va_valueid;
 
 				/*
 				 * There is a corner case here: the table rewrite might have
@@ -265,34 +293,48 @@ toast_save_datum(Relation rel, Datum value,
 				 * be reclaimed by VACUUM.
 				 */
 				if (toastrel_valueid_exists(toastrel,
-											toast_pointer.va_valueid))
+											old_valueid))
 				{
 					/* Match, so short-circuit the data storage loop below */
 					data_todo = 0;
 				}
+
+				/*
+				 * The old and new toast relations match, hence their type
+				 * of value match as well
+				 */
+				new_valueid = old_valueid;
 			}
 		}
-		if (toast_pointer.va_valueid == InvalidOid)
+		if ((new_valueid == InvalidToastId && toast_typid == INT8OID) ||
+			(new_valueid == InvalidOid && toast_typid == OIDOID))
 		{
 			/*
-			 * new value; must choose an OID that doesn't conflict in either
-			 * old or new toast table
+			 * new value; must choose a value that doesn't conflict in either
+			 * old or new toast table.
 			 */
 			do
 			{
-				toast_pointer.va_valueid =
-					GetNewOidWithIndex(toastrel,
-									   RelationGetRelid(toastidxs[validIndex]),
-									   (AttrNumber) 1);
-			} while (toastid_valueid_exists(rel->rd_toastoid,
-											toast_pointer.va_valueid));
+				if (toast_typid == OIDOID)
+					new_valueid = GetNewOidWithIndex(toastrel,
+													 RelationGetRelid(toastidxs[validIndex]),
+													 (AttrNumber) 1);
+				else if (toast_typid == INT8OID)
+					new_valueid = GetNewToastId();
+			} while (toastid_valueid_exists(rel->rd_toastoid, new_valueid));
 		}
 	}
 
+	/* Now set the new value */
+	toast_pointer.va_valueid = new_valueid;
+
 	/*
 	 * Initialize constant parts of the tuple data
 	 */
-	t_values[0] = ObjectIdGetDatum(toast_pointer.va_valueid);
+	if (toast_typid == OIDOID)
+		t_values[0] = ObjectIdGetDatum(toast_pointer.va_valueid);
+	else if (toast_typid == INT8OID)
+		t_values[0] = Int64GetDatum(toast_pointer.va_valueid);
 	t_values[2] = PointerGetDatum(&chunk_data);
 	t_isnull[0] = false;
 	t_isnull[1] = false;
@@ -393,6 +435,7 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	HeapTuple	toasttup;
 	int			num_indexes;
 	int			validIndex;
+	Oid			toast_typid;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		return;
@@ -404,6 +447,8 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	 * Open the toast relation and its indexes
 	 */
 	toastrel = table_open(toast_pointer.va_toastrelid, RowExclusiveLock);
+	toast_typid = TupleDescAttr(toastrel->rd_att, 0)->atttypid;
+	Assert(toast_typid == OIDOID || toast_typid == INT8OID);
 
 	/* Fetch valid relation used for process */
 	validIndex = toast_open_indexes(toastrel,
@@ -414,10 +459,18 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	/*
 	 * Setup a scan key to find chunks with matching va_valueid
 	 */
-	ScanKeyInit(&toastkey,
-				(AttrNumber) 1,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(toast_pointer.va_valueid));
+	if (toast_typid == OIDOID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(toast_pointer.va_valueid));
+	else if (toast_typid == INT8OID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_INT8EQ,
+					Int64GetDatum(toast_pointer.va_valueid));
+	else
+		Assert(false);
 
 	/*
 	 * Find all the chunks.  (We don't actually care whether we see them in
@@ -464,6 +517,7 @@ toastrel_valueid_exists(Relation toastrel, uint64 valueid)
 	int			num_indexes;
 	int			validIndex;
 	Relation   *toastidxs;
+	Oid			toast_typid;
 
 	/* Fetch a valid index relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -471,13 +525,24 @@ toastrel_valueid_exists(Relation toastrel, uint64 valueid)
 									&toastidxs,
 									&num_indexes);
 
+	toast_typid = TupleDescAttr(toastrel->rd_att, 0)->atttypid;
+	Assert(toast_typid == OIDOID || toast_typid == INT8OID);
+
 	/*
 	 * Setup a scan key to find chunks with matching va_valueid
 	 */
-	ScanKeyInit(&toastkey,
-				(AttrNumber) 1,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(valueid));
+	if (toast_typid == OIDOID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(valueid));
+	else if (toast_typid == INT8OID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_INT8EQ,
+					Int64GetDatum(valueid));
+	else
+		Assert(false);
 
 	/*
 	 * Is there any such chunk?
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index ae8d502ddcd3..f45be1d0d401 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -640,6 +640,7 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 	int			num_indexes;
 	int			validIndex;
 	int32		max_chunk_size;
+	Oid			toast_typid;
 
 	/* Look for the valid index of toast relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -647,6 +648,9 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 									&toastidxs,
 									&num_indexes);
 
+	toast_typid = TupleDescAttr(toastrel->rd_att, 0)->atttypid;
+	Assert(toast_typid == OIDOID || toast_typid == INT8OID);
+
 	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
 
 	totalchunks = ((attrsize - 1) / max_chunk_size) + 1;
@@ -655,10 +659,18 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 	Assert(endchunk <= totalchunks);
 
 	/* Set up a scan key to fetch from the index. */
-	ScanKeyInit(&toastkey[0],
-				(AttrNumber) 1,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(valueid));
+	if (toast_typid == OIDOID)
+		ScanKeyInit(&toastkey[0],
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(valueid));
+	else if (toast_typid == INT8OID)
+		ScanKeyInit(&toastkey[0],
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_INT8EQ,
+					Int64GetDatum(valueid));
+	else
+		Assert(false);
 
 	/*
 	 * No additional condition if fetching all chunks. Otherwise, use an
diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c
index e595cb61b375..3df83c9835d4 100644
--- a/src/backend/catalog/toasting.c
+++ b/src/backend/catalog/toasting.c
@@ -149,6 +149,7 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	int16		coloptions[2];
 	ObjectAddress baseobject,
 				toastobject;
+	Oid			toast_typid = InvalidOid;
 
 	/*
 	 * Is it already toasted?
@@ -204,11 +205,40 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	snprintf(toast_idxname, sizeof(toast_idxname),
 			 "pg_toast_%u_index", relOid);
 
+	/*
+	 * Determine the type OID to use for the value.  If OIDOldToast is
+	 * defined, we need to rely on the existing table for the job because
+	 * we do not want to create an inconsistent relation that would conflict
+	 * with the parent and break the world.
+	 */
+	if (!OidIsValid(OIDOldToast))
+	{
+		if (default_toast_type == TOAST_TYPE_OID)
+			toast_typid = OIDOID;
+		else if (default_toast_type == TOAST_TYPE_INT8)
+			toast_typid = INT8OID;
+		else
+			Assert(false);
+	}
+	else
+	{
+		HeapTuple	tuple;
+		Form_pg_attribute atttoast;
+
+		/* For the chunk_id type. */
+		tuple = SearchSysCacheAttNum(OIDOldToast, 1);
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for relation %u", OIDOldToast);
+		atttoast = (Form_pg_attribute) GETSTRUCT(tuple);
+		toast_typid = atttoast->atttypid;
+		ReleaseSysCache(tuple);
+	}
+
 	/* this is pretty painful...  need a tuple descriptor */
 	tupdesc = CreateTemplateTupleDesc(3);
 	TupleDescInitEntry(tupdesc, (AttrNumber) 1,
 					   "chunk_id",
-					   OIDOID,
+					   toast_typid,
 					   -1, 0);
 	TupleDescInitEntry(tupdesc, (AttrNumber) 2,
 					   "chunk_seq",
@@ -316,7 +346,10 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	collationIds[0] = InvalidOid;
 	collationIds[1] = InvalidOid;
 
-	opclassIds[0] = OID_BTREE_OPS_OID;
+	if (toast_typid == OIDOID)
+		opclassIds[0] = OID_BTREE_OPS_OID;
+	else if (toast_typid == INT8OID)
+		opclassIds[0] = INT8_BTREE_OPS_OID;
 	opclassIds[1] = INT4_BTREE_OPS_OID;
 
 	coloptions[0] = 0;
diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml
index 61250799ec07..1b2592645ab4 100644
--- a/doc/src/sgml/storage.sgml
+++ b/doc/src/sgml/storage.sgml
@@ -419,14 +419,15 @@ most <symbol>TOAST_MAX_CHUNK_SIZE</symbol> bytes (by default this value is chose
 so that four chunk rows will fit on a page, making it about 2000 bytes).
 Each chunk is stored as a separate row in the <acronym>TOAST</acronym> table
 belonging to the owning table.  Every
-<acronym>TOAST</acronym> table has the columns <structfield>chunk_id</structfield> (an OID
-identifying the particular <acronym>TOAST</acronym>ed value),
+<acronym>TOAST</acronym> table has the columns
+<structfield>chunk_id</structfield> (an OID or an 8-byte integer identifying
+the particular <acronym>TOAST</acronym>ed value),
 <structfield>chunk_seq</structfield> (a sequence number for the chunk within its value),
 and <structfield>chunk_data</structfield> (the actual data of the chunk).  A unique index
 on <structfield>chunk_id</structfield> and <structfield>chunk_seq</structfield> provides fast
 retrieval of the values.  A pointer datum representing an out-of-line on-disk
 <acronym>TOAST</acronym>ed value therefore needs to store the OID of the
-<acronym>TOAST</acronym> table in which to look and the OID of the specific value
+<acronym>TOAST</acronym> table in which to look and the specific value
 (its <structfield>chunk_id</structfield>).  For convenience, pointer datums also store the
 logical datum size (original uncompressed data length), physical stored size
 (different if compression was applied), and the compression method used, if
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 51ac416f7c6e..885400fa7058 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1877,6 +1877,9 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	int32		last_chunk_seq;
 	uint64		toast_valueid;
 	int32		max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	Oid			toast_typid;
+
+	toast_typid = TupleDescAttr(ctx->toast_rel->rd_att, 0)->atttypid;
 
 	extsize = VARATT_EXTERNAL_GET_EXTSIZE(ta->toast_pointer);
 	last_chunk_seq = (extsize - 1) / max_chunk_size;
@@ -1884,10 +1887,18 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	/*
 	 * Setup a scan key to find chunks in toast table with matching va_valueid
 	 */
-	ScanKeyInit(&toastkey,
-				(AttrNumber) 1,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(ta->toast_pointer.va_valueid));
+	if (toast_typid == OIDOID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(ta->toast_pointer.va_valueid));
+	else if (toast_typid == INT8OID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_INT8EQ,
+					Int64GetDatum(ta->toast_pointer.va_valueid));
+	else
+		Assert(false);
 
 	/*
 	 * Check if any chunks for this toasted object exist in the toast table,
-- 
2.49.0

v1-0008-Add-tests-for-TOAST-relations-with-bigint-as-valu.patchtext/x-diff; charset=us-asciiDownload
From 9b393f5c1379e38a951cb30681d9e03e44be849f Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 19 Jun 2025 11:15:48 +0900
Subject: [PATCH v1 08/12] Add tests for TOAST relations with bigint as value
 type

This adds coverage for relations created with default_toast_type =
'int8', for external TOAST pointers both compressed and uncompressed.
---
 src/test/regress/expected/strings.out | 238 ++++++++++++++++++++++----
 src/test/regress/sql/strings.sql      | 142 +++++++++++----
 2 files changed, 305 insertions(+), 75 deletions(-)

diff --git a/src/test/regress/expected/strings.out b/src/test/regress/expected/strings.out
index 788844abd20e..0dd34808a673 100644
--- a/src/test/regress/expected/strings.out
+++ b/src/test/regress/expected/strings.out
@@ -1933,21 +1933,40 @@ SELECT text 'text' || varchar ' and varchar' AS "Concat text to varchar";
 (1 row)
 
 --
--- test substr with toasted text values
+-- test substr with toasted text values, for all types of TOAST relations
+-- supported.
 --
-CREATE TABLE toasttest(f1 text);
-insert into toasttest values(repeat('1234567890',10000));
-insert into toasttest values(repeat('1234567890',10000));
+SET default_toast_type = 'oid';
+CREATE TABLE toasttest_oid(f1 text);
+SET default_toast_type = 'int8';
+CREATE TABLE toasttest_int8(f1 text);
+RESET default_toast_type;
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_int8 values(repeat('1234567890',10000));
+insert into toasttest_int8 values(repeat('1234567890',10000));
 --
 -- Ensure that some values are uncompressed, to test the faster substring
 -- operation used in that case
 --
-alter table toasttest alter column f1 set storage external;
-insert into toasttest values(repeat('1234567890',10000));
-insert into toasttest values(repeat('1234567890',10000));
+alter table toasttest_oid alter column f1 set storage external;
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+alter table toasttest_int8 alter column f1 set storage external;
+insert into toasttest_int8 values(repeat('1234567890',10000));
+insert into toasttest_int8 values(repeat('1234567890',10000));
 -- If the starting position is zero or less, then return from the start of the string
 -- adjusting the length to be consistent with the "negative start" per SQL.
-SELECT substr(f1, -1, 5) from toasttest;
+SELECT substr(f1, -1, 5) from toasttest_oid;
+ substr 
+--------
+ 123
+ 123
+ 123
+ 123
+(4 rows)
+
+SELECT substr(f1, -1, 5) from toasttest_int8;
  substr 
 --------
  123
@@ -1957,11 +1976,22 @@ SELECT substr(f1, -1, 5) from toasttest;
 (4 rows)
 
 -- If the length is less than zero, an ERROR is thrown.
-SELECT substr(f1, 5, -1) from toasttest;
+SELECT substr(f1, 5, -1) from toasttest_oid;
+ERROR:  negative substring length not allowed
+SELECT substr(f1, 5, -1) from toasttest_int8;
 ERROR:  negative substring length not allowed
 -- If no third argument (length) is provided, the length to the end of the
 -- string is assumed.
-SELECT substr(f1, 99995) from toasttest;
+SELECT substr(f1, 99995) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995) from toasttest_int8;
  substr 
 --------
  567890
@@ -1972,7 +2002,7 @@ SELECT substr(f1, 99995) from toasttest;
 
 -- If start plus length is > string length, the result is truncated to
 -- string length
-SELECT substr(f1, 99995, 10) from toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
  substr 
 --------
  567890
@@ -1981,50 +2011,108 @@ SELECT substr(f1, 99995, 10) from toasttest;
  567890
 (4 rows)
 
-TRUNCATE TABLE toasttest;
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
+SELECT substr(f1, 99995, 10) from toasttest_int8;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+-- TRUNCATE cases for TOAST relations with OID values.
+TRUNCATE TABLE toasttest_oid;
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
 -- expect >0 blocks
 SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
-  FROM pg_class where relname = 'toasttest';
+  FROM pg_class where relname = 'toasttest_oid';
  is_empty 
 ----------
  f
 (1 row)
 
-TRUNCATE TABLE toasttest;
-ALTER TABLE toasttest set (toast_tuple_target = 4080);
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
+TRUNCATE TABLE toasttest_oid;
+ALTER TABLE toasttest_oid set (toast_tuple_target = 4080);
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
 -- expect 0 blocks
 SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
-  FROM pg_class where relname = 'toasttest';
+  FROM pg_class where relname = 'toasttest_oid';
  is_empty 
 ----------
  t
 (1 row)
 
-DROP TABLE toasttest;
+DROP TABLE toasttest_oid;
+-- TRUNCATE cases for TOAST relation with int8 values.
+TRUNCATE TABLE toasttest_int8;
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+-- expect >0 blocks
+SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
+  FROM pg_class where relname = 'toasttest_int8';
+ is_empty 
+----------
+ f
+(1 row)
+
+TRUNCATE TABLE toasttest_int8;
+ALTER TABLE toasttest_int8 set (toast_tuple_target = 4080);
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+-- expect 0 blocks
+SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
+  FROM pg_class where relname = 'toasttest_int8';
+ is_empty 
+----------
+ t
+(1 row)
+
+DROP TABLE toasttest_int8;
 --
--- test substr with toasted bytea values
+-- test substr with toasted bytea values, for all types of TOAST relations
+-- supported.
 --
-CREATE TABLE toasttest(f1 bytea);
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
+SET default_toast_type = 'oid';
+CREATE TABLE toasttest_oid(f1 bytea);
+SET default_toast_type = 'int8';
+CREATE TABLE toasttest_int8(f1 bytea);
+RESET default_toast_type;
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_int8 values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_int8 values(decode(repeat('1234567890',10000),'escape'));
 --
 -- Ensure that some values are uncompressed, to test the faster substring
 -- operation used in that case
 --
-alter table toasttest alter column f1 set storage external;
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
+alter table toasttest_oid alter column f1 set storage external;
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+alter table toasttest_int8 alter column f1 set storage external;
+insert into toasttest_int8 values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_int8 values(decode(repeat('1234567890',10000),'escape'));
 -- If the starting position is zero or less, then return from the start of the string
 -- adjusting the length to be consistent with the "negative start" per SQL.
-SELECT substr(f1, -1, 5) from toasttest;
+SELECT substr(f1, -1, 5) from toasttest_oid;
+ substr 
+--------
+ 123
+ 123
+ 123
+ 123
+(4 rows)
+
+SELECT substr(f1, -1, 5) from toasttest_int8;
  substr 
 --------
  123
@@ -2034,11 +2122,22 @@ SELECT substr(f1, -1, 5) from toasttest;
 (4 rows)
 
 -- If the length is less than zero, an ERROR is thrown.
-SELECT substr(f1, 5, -1) from toasttest;
+SELECT substr(f1, 5, -1) from toasttest_oid;
+ERROR:  negative substring length not allowed
+SELECT substr(f1, 5, -1) from toasttest_int8;
 ERROR:  negative substring length not allowed
 -- If no third argument (length) is provided, the length to the end of the
 -- string is assumed.
-SELECT substr(f1, 99995) from toasttest;
+SELECT substr(f1, 99995) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995) from toasttest_int8;
  substr 
 --------
  567890
@@ -2049,7 +2148,7 @@ SELECT substr(f1, 99995) from toasttest;
 
 -- If start plus length is > string length, the result is truncated to
 -- string length
-SELECT substr(f1, 99995, 10) from toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
  substr 
 --------
  567890
@@ -2058,7 +2157,72 @@ SELECT substr(f1, 99995, 10) from toasttest;
  567890
 (4 rows)
 
-DROP TABLE toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_int8;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+-- A relation rewrite leaves the TOAST value attributes unchanged.
+VACUUM FULL toasttest_oid;
+VACUUM FULL toasttest_int8;
+SELECT c1.relname, a.atttypid::regtype
+  FROM pg_attribute AS a,
+       pg_class AS c1,
+       pg_class AS c2
+  WHERE
+       c1.relname IN ('toasttest_oid', 'toasttest_int8') AND
+       c1.reltoastrelid = c2.oid AND
+       a.attrelid = c2.oid AND
+       a.attname = 'chunk_id'
+  ORDER BY c1.relname COLLATE "C";
+    relname     | atttypid 
+----------------+----------
+ toasttest_int8 | bigint
+ toasttest_oid  | oid
+(2 rows)
+
+-- Check that data slices are still accessible.
+SELECT substr(f1, 99995) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995) from toasttest_int8;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995, 10) from toasttest_int8;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+DROP TABLE toasttest_oid, toasttest_int8;
 -- test internally compressing datums
 -- this tests compressing a datum to a very small size which exercises a
 -- corner case in packed-varlena handling: even though small, the compressed
diff --git a/src/test/regress/sql/strings.sql b/src/test/regress/sql/strings.sql
index 2577a42987de..49b4163493c8 100644
--- a/src/test/regress/sql/strings.sql
+++ b/src/test/regress/sql/strings.sql
@@ -551,89 +551,155 @@ SELECT text 'text' || char(20) ' and characters' AS "Concat text to char";
 SELECT text 'text' || varchar ' and varchar' AS "Concat text to varchar";
 
 --
--- test substr with toasted text values
+-- test substr with toasted text values, for all types of TOAST relations
+-- supported.
 --
-CREATE TABLE toasttest(f1 text);
+SET default_toast_type = 'oid';
+CREATE TABLE toasttest_oid(f1 text);
+SET default_toast_type = 'int8';
+CREATE TABLE toasttest_int8(f1 text);
+RESET default_toast_type;
 
-insert into toasttest values(repeat('1234567890',10000));
-insert into toasttest values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_int8 values(repeat('1234567890',10000));
+insert into toasttest_int8 values(repeat('1234567890',10000));
 
 --
 -- Ensure that some values are uncompressed, to test the faster substring
 -- operation used in that case
 --
-alter table toasttest alter column f1 set storage external;
-insert into toasttest values(repeat('1234567890',10000));
-insert into toasttest values(repeat('1234567890',10000));
+alter table toasttest_oid alter column f1 set storage external;
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+alter table toasttest_int8 alter column f1 set storage external;
+insert into toasttest_int8 values(repeat('1234567890',10000));
+insert into toasttest_int8 values(repeat('1234567890',10000));
 
 -- If the starting position is zero or less, then return from the start of the string
 -- adjusting the length to be consistent with the "negative start" per SQL.
-SELECT substr(f1, -1, 5) from toasttest;
+SELECT substr(f1, -1, 5) from toasttest_oid;
+SELECT substr(f1, -1, 5) from toasttest_int8;
 
 -- If the length is less than zero, an ERROR is thrown.
-SELECT substr(f1, 5, -1) from toasttest;
+SELECT substr(f1, 5, -1) from toasttest_oid;
+SELECT substr(f1, 5, -1) from toasttest_int8;
 
 -- If no third argument (length) is provided, the length to the end of the
 -- string is assumed.
-SELECT substr(f1, 99995) from toasttest;
+SELECT substr(f1, 99995) from toasttest_oid;
+SELECT substr(f1, 99995) from toasttest_int8;
 
 -- If start plus length is > string length, the result is truncated to
 -- string length
-SELECT substr(f1, 99995, 10) from toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+SELECT substr(f1, 99995, 10) from toasttest_int8;
 
-TRUNCATE TABLE toasttest;
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
+-- TRUNCATE cases for TOAST relations with OID values.
+TRUNCATE TABLE toasttest_oid;
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
 -- expect >0 blocks
 SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
-  FROM pg_class where relname = 'toasttest';
-
-TRUNCATE TABLE toasttest;
-ALTER TABLE toasttest set (toast_tuple_target = 4080);
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
+  FROM pg_class where relname = 'toasttest_oid';
+TRUNCATE TABLE toasttest_oid;
+ALTER TABLE toasttest_oid set (toast_tuple_target = 4080);
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
 -- expect 0 blocks
 SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
-  FROM pg_class where relname = 'toasttest';
+  FROM pg_class where relname = 'toasttest_oid';
+DROP TABLE toasttest_oid;
 
-DROP TABLE toasttest;
+-- TRUNCATE cases for TOAST relation with int8 values.
+TRUNCATE TABLE toasttest_int8;
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+-- expect >0 blocks
+SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
+  FROM pg_class where relname = 'toasttest_int8';
+TRUNCATE TABLE toasttest_int8;
+ALTER TABLE toasttest_int8 set (toast_tuple_target = 4080);
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+-- expect 0 blocks
+SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
+  FROM pg_class where relname = 'toasttest_int8';
+DROP TABLE toasttest_int8;
 
 --
--- test substr with toasted bytea values
+-- test substr with toasted bytea values, for all types of TOAST relations
+-- supported.
 --
-CREATE TABLE toasttest(f1 bytea);
+SET default_toast_type = 'oid';
+CREATE TABLE toasttest_oid(f1 bytea);
+SET default_toast_type = 'int8';
+CREATE TABLE toasttest_int8(f1 bytea);
+RESET default_toast_type;
 
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_int8 values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_int8 values(decode(repeat('1234567890',10000),'escape'));
 
 --
 -- Ensure that some values are uncompressed, to test the faster substring
 -- operation used in that case
 --
-alter table toasttest alter column f1 set storage external;
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
+alter table toasttest_oid alter column f1 set storage external;
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+alter table toasttest_int8 alter column f1 set storage external;
+insert into toasttest_int8 values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_int8 values(decode(repeat('1234567890',10000),'escape'));
 
 -- If the starting position is zero or less, then return from the start of the string
 -- adjusting the length to be consistent with the "negative start" per SQL.
-SELECT substr(f1, -1, 5) from toasttest;
+SELECT substr(f1, -1, 5) from toasttest_oid;
+SELECT substr(f1, -1, 5) from toasttest_int8;
 
 -- If the length is less than zero, an ERROR is thrown.
-SELECT substr(f1, 5, -1) from toasttest;
+SELECT substr(f1, 5, -1) from toasttest_oid;
+SELECT substr(f1, 5, -1) from toasttest_int8;
 
 -- If no third argument (length) is provided, the length to the end of the
 -- string is assumed.
-SELECT substr(f1, 99995) from toasttest;
+SELECT substr(f1, 99995) from toasttest_oid;
+SELECT substr(f1, 99995) from toasttest_int8;
 
 -- If start plus length is > string length, the result is truncated to
 -- string length
-SELECT substr(f1, 99995, 10) from toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+SELECT substr(f1, 99995, 10) from toasttest_int8;
 
-DROP TABLE toasttest;
+-- A relation rewrite leaves the TOAST value attributes unchanged.
+VACUUM FULL toasttest_oid;
+VACUUM FULL toasttest_int8;
+SELECT c1.relname, a.atttypid::regtype
+  FROM pg_attribute AS a,
+       pg_class AS c1,
+       pg_class AS c2
+  WHERE
+       c1.relname IN ('toasttest_oid', 'toasttest_int8') AND
+       c1.reltoastrelid = c2.oid AND
+       a.attrelid = c2.oid AND
+       a.attname = 'chunk_id'
+  ORDER BY c1.relname COLLATE "C";
+-- Check that data slices are still accessible.
+SELECT substr(f1, 99995) from toasttest_oid;
+SELECT substr(f1, 99995) from toasttest_int8;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+SELECT substr(f1, 99995, 10) from toasttest_int8;
+
+DROP TABLE toasttest_oid, toasttest_int8;
 
 -- test internally compressing datums
 
-- 
2.49.0

v1-0009-Add-support-for-TOAST-table-types-in-pg_dump-and-.patchtext/x-diff; charset=us-asciiDownload
From b6da6d3f01d06571a4784acb5dfe55ab7cef21e6 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 19 Jun 2025 11:51:52 +0900
Subject: [PATCH v1 09/12] Add support for TOAST table types in pg_dump and
 pg_restore

This includes the possibility to perform binary upgrades with TOAST
table types applied to a new cluster, relying on SET commands based on
default_toast_type to apply one type of TOAST table or the other.

Some tests are included, this is a pretty mechanical change.

Dump format is bumped to 1.17 due to the addition of the TOAST table
type in the custom format.
---
 src/bin/pg_dump/pg_backup.h          |  2 +
 src/bin/pg_dump/pg_backup_archiver.c | 69 +++++++++++++++++++++++++++-
 src/bin/pg_dump/pg_backup_archiver.h |  6 ++-
 src/bin/pg_dump/pg_dump.c            | 21 +++++++++
 src/bin/pg_dump/pg_dump.h            |  1 +
 src/bin/pg_dump/pg_dumpall.c         |  5 ++
 src/bin/pg_dump/pg_restore.c         |  4 ++
 src/bin/pg_dump/t/002_pg_dump.pl     | 35 ++++++++++++++
 doc/src/sgml/ref/pg_dump.sgml        | 12 +++++
 doc/src/sgml/ref/pg_dumpall.sgml     | 12 +++++
 doc/src/sgml/ref/pg_restore.sgml     | 12 +++++
 11 files changed, 177 insertions(+), 2 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index af0007fb6d2f..84dccdb0eed7 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -99,6 +99,7 @@ typedef struct _restoreOptions
 	int			noOwner;		/* Don't try to match original object owner */
 	int			noTableAm;		/* Don't issue table-AM-related commands */
 	int			noTablespace;	/* Don't issue tablespace-related commands */
+	int			noToastType;	/* Don't issue TOAST-type-related commands */
 	int			disable_triggers;	/* disable triggers during data-only
 									 * restore */
 	int			use_setsessauth;	/* Use SET SESSION AUTHORIZATION commands
@@ -192,6 +193,7 @@ typedef struct _dumpOptions
 	int			disable_triggers;
 	int			outputNoTableAm;
 	int			outputNoTablespaces;
+	int			outputNoToastType;
 	int			use_setsessauth;
 	int			enable_row_security;
 	int			load_via_partition_root;
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 197c1295d93f..574e62d53a3e 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -183,6 +183,7 @@ dumpOptionsFromRestoreOptions(RestoreOptions *ropt)
 	dopt->outputNoOwner = ropt->noOwner;
 	dopt->outputNoTableAm = ropt->noTableAm;
 	dopt->outputNoTablespaces = ropt->noTablespace;
+	dopt->outputNoToastType = ropt->noToastType;
 	dopt->disable_triggers = ropt->disable_triggers;
 	dopt->use_setsessauth = ropt->use_setsessauth;
 	dopt->disable_dollar_quoting = ropt->disable_dollar_quoting;
@@ -1247,6 +1248,7 @@ ArchiveEntry(Archive *AHX, CatalogId catalogId, DumpId dumpId,
 	newToc->namespace = opts->namespace ? pg_strdup(opts->namespace) : NULL;
 	newToc->tablespace = opts->tablespace ? pg_strdup(opts->tablespace) : NULL;
 	newToc->tableam = opts->tableam ? pg_strdup(opts->tableam) : NULL;
+	newToc->toasttype = opts->toasttype ? pg_strdup(opts->toasttype) : NULL;
 	newToc->relkind = opts->relkind;
 	newToc->owner = opts->owner ? pg_strdup(opts->owner) : NULL;
 	newToc->desc = pg_strdup(opts->description);
@@ -2407,6 +2409,7 @@ _allocAH(const char *FileSpec, const ArchiveFormat fmt,
 	AH->currSchema = NULL;		/* ditto */
 	AH->currTablespace = NULL;	/* ditto */
 	AH->currTableAm = NULL;		/* ditto */
+	AH->currToastType = NULL;		/* ditto */
 
 	AH->toc = (TocEntry *) pg_malloc0(sizeof(TocEntry));
 
@@ -2674,6 +2677,7 @@ WriteToc(ArchiveHandle *AH)
 		WriteStr(AH, te->tablespace);
 		WriteStr(AH, te->tableam);
 		WriteInt(AH, te->relkind);
+		WriteStr(AH, te->toasttype);
 		WriteStr(AH, te->owner);
 		WriteStr(AH, "false");
 
@@ -2782,6 +2786,9 @@ ReadToc(ArchiveHandle *AH)
 		if (AH->version >= K_VERS_1_16)
 			te->relkind = ReadInt(AH);
 
+		if (AH->version >= K_VERS_1_17)
+			te->toasttype = ReadStr(AH);
+
 		te->owner = ReadStr(AH);
 		is_supported = true;
 		if (AH->version < K_VERS_1_9)
@@ -3468,6 +3475,9 @@ _reconnectToDB(ArchiveHandle *AH, const char *dbname)
 	free(AH->currTablespace);
 	AH->currTablespace = NULL;
 
+	free(AH->currToastType);
+	AH->currToastType = NULL;
+
 	/* re-establish fixed state */
 	_doSetFixedOutputState(AH);
 }
@@ -3673,6 +3683,56 @@ _selectTableAccessMethod(ArchiveHandle *AH, const char *tableam)
 	AH->currTableAm = pg_strdup(want);
 }
 
+
+/*
+ * Set the proper default_toast_type value for the table.
+ */
+static void
+_selectToastType(ArchiveHandle *AH, const char *toasttype)
+{
+	RestoreOptions *ropt = AH->public.ropt;
+	PQExpBuffer cmd;
+	const char *want,
+			   *have;
+
+	/* do nothing in --no-toast-type mode */
+	if (ropt->noToastType)
+		return;
+
+	have = AH->currToastType;
+	want = toasttype;
+
+	if (!want)
+		return;
+
+	if (have && strcmp(want, have) == 0)
+		return;
+
+	cmd = createPQExpBuffer();
+
+	appendPQExpBuffer(cmd, "SET default_toast_type = %s;", fmtId(toasttype));
+
+	if (RestoringToDB(AH))
+	{
+		PGresult   *res;
+
+		res = PQexec(AH->connection, cmd->data);
+
+		if (!res || PQresultStatus(res) != PGRES_COMMAND_OK)
+			warn_or_exit_horribly(AH,
+								  "could not set \"default_toast_type\": %s",
+								  PQerrorMessage(AH->connection));
+		PQclear(res);
+	}
+	else
+		ahprintf(AH, "%s\n\n", cmd->data);
+
+	destroyPQExpBuffer(cmd);
+
+	free(AH->currToastType);
+	AH->currToastType = pg_strdup(want);
+}
+
 /*
  * Set the proper default table access method for a table without storage.
  * Currently, this is required only for partitioned tables with a table AM.
@@ -3828,13 +3888,16 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, const char *pfx)
 	 * Select owner, schema, tablespace and default AM as necessary. The
 	 * default access method for partitioned tables is handled after
 	 * generating the object definition, as it requires an ALTER command
-	 * rather than SET.
+	 * rather than SET.  Partitioned tables do not have TOAST tables.
 	 */
 	_becomeOwner(AH, te);
 	_selectOutputSchema(AH, te->namespace);
 	_selectTablespace(AH, te->tablespace);
 	if (te->relkind != RELKIND_PARTITIONED_TABLE)
+	{
 		_selectTableAccessMethod(AH, te->tableam);
+		_selectToastType(AH, te->toasttype);
+	}
 
 	/* Emit header comment for item */
 	if (!AH->noTocComments)
@@ -4393,6 +4456,8 @@ restore_toc_entries_prefork(ArchiveHandle *AH, TocEntry *pending_list)
 	AH->currTablespace = NULL;
 	free(AH->currTableAm);
 	AH->currTableAm = NULL;
+	free(AH->currToastType);
+	AH->currToastType = NULL;
 }
 
 /*
@@ -5130,6 +5195,7 @@ CloneArchive(ArchiveHandle *AH)
 	clone->currSchema = NULL;
 	clone->currTableAm = NULL;
 	clone->currTablespace = NULL;
+	clone->currToastType = NULL;
 
 	/* savedPassword must be local in case we change it while connecting */
 	if (clone->savedPassword)
@@ -5189,6 +5255,7 @@ DeCloneArchive(ArchiveHandle *AH)
 	free(AH->currSchema);
 	free(AH->currTablespace);
 	free(AH->currTableAm);
+	free(AH->currToastType);
 	free(AH->savedPassword);
 
 	free(AH);
diff --git a/src/bin/pg_dump/pg_backup_archiver.h b/src/bin/pg_dump/pg_backup_archiver.h
index 365073b3eae4..cc7aa46b483a 100644
--- a/src/bin/pg_dump/pg_backup_archiver.h
+++ b/src/bin/pg_dump/pg_backup_archiver.h
@@ -71,10 +71,11 @@
 #define K_VERS_1_16 MAKE_ARCHIVE_VERSION(1, 16, 0)	/* BLOB METADATA entries
 													 * and multiple BLOBS,
 													 * relkind */
+#define K_VERS_1_17 MAKE_ARCHIVE_VERSION(1, 17, 0)	/* TOAST type */
 
 /* Current archive version number (the format we can output) */
 #define K_VERS_MAJOR 1
-#define K_VERS_MINOR 16
+#define K_VERS_MINOR 17
 #define K_VERS_REV 0
 #define K_VERS_SELF MAKE_ARCHIVE_VERSION(K_VERS_MAJOR, K_VERS_MINOR, K_VERS_REV)
 
@@ -325,6 +326,7 @@ struct _archiveHandle
 	char	   *currSchema;		/* current schema, or NULL */
 	char	   *currTablespace; /* current tablespace, or NULL */
 	char	   *currTableAm;	/* current table access method, or NULL */
+	char	   *currToastType;	/* current TOAST type, or NULL */
 
 	/* in --transaction-size mode, this counts objects emitted in cur xact */
 	int			txnCount;
@@ -359,6 +361,7 @@ struct _tocEntry
 	char	   *tablespace;		/* null if not in a tablespace; empty string
 								 * means use database default */
 	char	   *tableam;		/* table access method, only for TABLE tags */
+	char	   *toasttype;		/* TOAST table type, only for TABLE tags */
 	char		relkind;		/* relation kind, only for TABLE tags */
 	char	   *owner;
 	char	   *desc;
@@ -405,6 +408,7 @@ typedef struct _archiveOpts
 	const char *namespace;
 	const char *tablespace;
 	const char *tableam;
+	const char *toasttype;
 	char		relkind;
 	const char *owner;
 	const char *description;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index a8f0309e8fc1..8ada7e480e07 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -497,6 +497,7 @@ main(int argc, char **argv)
 		{"lock-wait-timeout", required_argument, NULL, 2},
 		{"no-table-access-method", no_argument, &dopt.outputNoTableAm, 1},
 		{"no-tablespaces", no_argument, &dopt.outputNoTablespaces, 1},
+		{"no-toast-type", no_argument, &dopt.outputNoToastType, 1},
 		{"quote-all-identifiers", no_argument, &quote_all_identifiers, 1},
 		{"load-via-partition-root", no_argument, &dopt.load_via_partition_root, 1},
 		{"role", required_argument, NULL, 3},
@@ -1184,6 +1185,7 @@ main(int argc, char **argv)
 	ropt->noOwner = dopt.outputNoOwner;
 	ropt->noTableAm = dopt.outputNoTableAm;
 	ropt->noTablespace = dopt.outputNoTablespaces;
+	ropt->noToastType = dopt.outputNoToastType;
 	ropt->disable_triggers = dopt.disable_triggers;
 	ropt->use_setsessauth = dopt.use_setsessauth;
 	ropt->disable_dollar_quoting = dopt.disable_dollar_quoting;
@@ -1306,6 +1308,7 @@ help(const char *progname)
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
 	printf(_("  --no-toast-compression       do not dump TOAST compression methods\n"));
+	printf(_("  --no-toast-type              do not dump TOAST table type\n"));
 	printf(_("  --no-unlogged-table-data     do not dump unlogged table data\n"));
 	printf(_("  --on-conflict-do-nothing     add ON CONFLICT DO NOTHING to INSERT commands\n"));
 	printf(_("  --quote-all-identifiers      quote all identifiers, even if not key words\n"));
@@ -6991,6 +6994,7 @@ getTables(Archive *fout, int *numTables)
 	int			i_relfrozenxid;
 	int			i_toastfrozenxid;
 	int			i_toastoid;
+	int			i_toasttype;
 	int			i_relminmxid;
 	int			i_toastminmxid;
 	int			i_reloptions;
@@ -7045,6 +7049,14 @@ getTables(Archive *fout, int *numTables)
 						 "ELSE 0 END AS foreignserver, "
 						 "c.relfrozenxid, tc.relfrozenxid AS tfrozenxid, "
 						 "tc.oid AS toid, "
+						 "CASE WHEN c.reltoastrelid <> 0 THEN "
+						 " (SELECT CASE "
+						 "   WHEN a.atttypid::regtype = 'oid'::regtype THEN 'oid'::text "
+						 "   WHEN a.atttypid::regtype = 'bigint'::regtype THEN 'int8'::text "
+						 "   ELSE NULL END"
+						 "  FROM pg_attribute AS a "
+						 "  WHERE a.attrelid = tc.oid AND a.attname = 'chunk_id') "
+						 " ELSE NULL END AS toasttype, "
 						 "tc.relpages AS toastpages, "
 						 "tc.reloptions AS toast_reloptions, "
 						 "d.refobjid AS owning_tab, "
@@ -7215,6 +7227,7 @@ getTables(Archive *fout, int *numTables)
 	i_relfrozenxid = PQfnumber(res, "relfrozenxid");
 	i_toastfrozenxid = PQfnumber(res, "tfrozenxid");
 	i_toastoid = PQfnumber(res, "toid");
+	i_toasttype = PQfnumber(res, "toasttype");
 	i_relminmxid = PQfnumber(res, "relminmxid");
 	i_toastminmxid = PQfnumber(res, "tminmxid");
 	i_reloptions = PQfnumber(res, "reloptions");
@@ -7293,6 +7306,10 @@ getTables(Archive *fout, int *numTables)
 		tblinfo[i].frozenxid = atooid(PQgetvalue(res, i, i_relfrozenxid));
 		tblinfo[i].toast_frozenxid = atooid(PQgetvalue(res, i, i_toastfrozenxid));
 		tblinfo[i].toast_oid = atooid(PQgetvalue(res, i, i_toastoid));
+		if (PQgetisnull(res, i, i_toasttype))
+			tblinfo[i].toast_type = NULL;
+		else
+			tblinfo[i].toast_type = pg_strdup(PQgetvalue(res, i, i_toasttype));
 		tblinfo[i].minmxid = atooid(PQgetvalue(res, i, i_relminmxid));
 		tblinfo[i].toast_minmxid = atooid(PQgetvalue(res, i, i_toastminmxid));
 		tblinfo[i].reloptions = pg_strdup(PQgetvalue(res, i, i_reloptions));
@@ -17649,6 +17666,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 	{
 		char	   *tablespace = NULL;
 		char	   *tableam = NULL;
+		char	   *toasttype = NULL;
 
 		/*
 		 * _selectTablespace() relies on tablespace-enabled objects in the
@@ -17663,12 +17681,15 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 		if (RELKIND_HAS_TABLE_AM(tbinfo->relkind) ||
 			tbinfo->relkind == RELKIND_PARTITIONED_TABLE)
 			tableam = tbinfo->amname;
+		if (OidIsValid(tbinfo->toast_oid))
+			toasttype = tbinfo->toast_type;
 
 		ArchiveEntry(fout, tbinfo->dobj.catId, tbinfo->dobj.dumpId,
 					 ARCHIVE_OPTS(.tag = tbinfo->dobj.name,
 								  .namespace = tbinfo->dobj.namespace->dobj.name,
 								  .tablespace = tablespace,
 								  .tableam = tableam,
+								  .toasttype = toasttype,
 								  .relkind = tbinfo->relkind,
 								  .owner = tbinfo->rolname,
 								  .description = reltypename,
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 7417eab6aefa..24f313b68438 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -318,6 +318,7 @@ typedef struct _tableInfo
 	uint32		frozenxid;		/* table's relfrozenxid */
 	uint32		minmxid;		/* table's relminmxid */
 	Oid			toast_oid;		/* toast table's OID, or 0 if none */
+	char	   *toast_type;		/* toast table type, or NULL if none */
 	uint32		toast_frozenxid;	/* toast table's relfrozenxid, if any */
 	uint32		toast_minmxid;	/* toast table's relminmxid */
 	int			ncheck;			/* # of CHECK expressions */
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index b1f388cb3916..5266140939e5 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -95,6 +95,7 @@ static int	if_exists = 0;
 static int	inserts = 0;
 static int	no_table_access_method = 0;
 static int	no_tablespaces = 0;
+static int	no_toast_type = 0;
 static int	use_setsessauth = 0;
 static int	no_comments = 0;
 static int	no_policies = 0;
@@ -167,6 +168,7 @@ main(int argc, char *argv[])
 		{"lock-wait-timeout", required_argument, NULL, 2},
 		{"no-table-access-method", no_argument, &no_table_access_method, 1},
 		{"no-tablespaces", no_argument, &no_tablespaces, 1},
+		{"no-toast-type", no_argument, &no_tablespaces, 1},
 		{"quote-all-identifiers", no_argument, &quote_all_identifiers, 1},
 		{"load-via-partition-root", no_argument, &load_via_partition_root, 1},
 		{"role", required_argument, NULL, 3},
@@ -471,6 +473,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --no-table-access-method");
 	if (no_tablespaces)
 		appendPQExpBufferStr(pgdumpopts, " --no-tablespaces");
+	if (no_toast_type)
+		appendPQExpBufferStr(pgdumpopts, " --no-toast-type");
 	if (quote_all_identifiers)
 		appendPQExpBufferStr(pgdumpopts, " --quote-all-identifiers");
 	if (load_via_partition_root)
@@ -745,6 +749,7 @@ help(void)
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
 	printf(_("  --no-toast-compression       do not dump TOAST compression methods\n"));
+	printf(_("  --no-toast-type              do not dump TOAST table types\n"));
 	printf(_("  --no-unlogged-table-data     do not dump unlogged table data\n"));
 	printf(_("  --on-conflict-do-nothing     add ON CONFLICT DO NOTHING to INSERT commands\n"));
 	printf(_("  --quote-all-identifiers      quote all identifiers, even if not key words\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index 6ef789cb06d6..610103ae4b99 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -100,6 +100,7 @@ main(int argc, char **argv)
 	static int	no_data_for_failed_tables = 0;
 	static int	outputNoTableAm = 0;
 	static int	outputNoTablespaces = 0;
+	static int	outputNoToastType = 0;
 	static int	use_setsessauth = 0;
 	static int	no_comments = 0;
 	static int	no_data = 0;
@@ -156,6 +157,7 @@ main(int argc, char **argv)
 		{"no-data-for-failed-tables", no_argument, &no_data_for_failed_tables, 1},
 		{"no-table-access-method", no_argument, &outputNoTableAm, 1},
 		{"no-tablespaces", no_argument, &outputNoTablespaces, 1},
+		{"no-toast-type", no_argument, &outputNoToastType, 1},
 		{"role", required_argument, NULL, 2},
 		{"section", required_argument, NULL, 3},
 		{"strict-names", no_argument, &strict_names, 1},
@@ -461,6 +463,7 @@ main(int argc, char **argv)
 	opts->noDataForFailedTables = no_data_for_failed_tables;
 	opts->noTableAm = outputNoTableAm;
 	opts->noTablespace = outputNoTablespaces;
+	opts->noToastType = outputNoToastType;
 	opts->use_setsessauth = use_setsessauth;
 	opts->no_comments = no_comments;
 	opts->no_policies = no_policies;
@@ -704,6 +707,7 @@ usage(const char *progname)
 	printf(_("  --no-subscriptions           do not restore subscriptions\n"));
 	printf(_("  --no-table-access-method     do not restore table access methods\n"));
 	printf(_("  --no-tablespaces             do not restore tablespace assignments\n"));
+	printf(_("  --no-toast-type              do not restore TOAST table types\n"));
 	printf(_("  --section=SECTION            restore named section (pre-data, data, or post-data)\n"));
 	printf(_("  --statistics-only            restore only the statistics, not schema or data\n"));
 	printf(_("  --strict-names               require table and/or schema include patterns to\n"
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 386e21e0c596..3fa1ee69542f 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -659,6 +659,15 @@ my %pgdump_runs = (
 			'postgres',
 		],
 	},
+	no_toast_type => {
+		dump_cmd => [
+			'pg_dump', '--no-sync',
+			'--file' => "$tempdir/no_toast_type.sql",
+			'--no-toast-type',
+			'--with-statistics',
+			'postgres',
+		],
+	},
 	only_dump_test_schema => {
 		dump_cmd => [
 			'pg_dump', '--no-sync',
@@ -882,6 +891,7 @@ my %full_runs = (
 	no_privs => 1,
 	no_statistics => 1,
 	no_table_access_method => 1,
+	no_toast_type => 1,
 	pg_dumpall_dbprivs => 1,
 	pg_dumpall_exclude => 1,
 	schema_only => 1,
@@ -4884,6 +4894,31 @@ my %tests = (
 		},
 	},
 
+	# Test the case of multiple TOAST table types.
+	'CREATE TABLE regress_toast_type' => {
+		create_order => 13,
+		create_sql => '
+			SET default_toast_type = int8;
+			CREATE TABLE dump_test.regress_toast_type_int8 (col1 text);
+			SET default_toast_type = oid;
+			CREATE TABLE dump_test.regress_toast_type_oid (col1 text);
+			RESET default_toast_type;',
+		regexp => qr/^
+			\QSET default_toast_type = int8;\E
+			(\n(?!SET[^;]+;)[^\n]*)*
+			\n\QCREATE TABLE dump_test.regress_toast_type_int8 (\E
+			\n\s+\Qcol1 text\E
+			\n\);/xm,
+		like => {
+			%full_runs, %dump_test_schema_runs, section_pre_data => 1,
+		},
+		unlike => {
+			exclude_dump_test_schema => 1,
+			no_toast_type => 1,
+			only_dump_measurement => 1,
+		},
+	},
+
 	#
 	# TABLE and MATVIEW stats will end up in SECTION_DATA.
 	# INDEX stats (expression columns only) will end up in SECTION_POST_DATA.
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index 1e06bd33bdcd..4cf9fea479d6 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -1208,6 +1208,18 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-toast-type</option></term>
+      <listitem>
+       <para>
+        Do not output commands to set <acronym>TOAST</acronym> table
+        types.
+        With this option, all <acronym>TOAST</acronym> tables will be
+        restored with the default type.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-unlogged-table-data</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_dumpall.sgml b/doc/src/sgml/ref/pg_dumpall.sgml
index 43f384ed16a9..7e7f33404aa7 100644
--- a/doc/src/sgml/ref/pg_dumpall.sgml
+++ b/doc/src/sgml/ref/pg_dumpall.sgml
@@ -630,6 +630,18 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-toast-type</option></term>
+      <listitem>
+       <para>
+        Do not output commands to set <acronym>TOAST</acronym> table
+        types.
+        With this option, all <acronym>TOAST</acronym> tables will be
+        restored with the default type.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-unlogged-table-data</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml
index 8c88b07dcc86..251b30bc20dd 100644
--- a/doc/src/sgml/ref/pg_restore.sgml
+++ b/doc/src/sgml/ref/pg_restore.sgml
@@ -842,6 +842,18 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-toast-type</option></term>
+      <listitem>
+       <para>
+        Do not output commands to select <acronym>TOAST</acronym> table
+        types.
+        With this option, all <acronym>TOAST</acronym> tables will be
+        created with whichever type is the default during restore.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
        <term><option>--section=<replaceable class="parameter">sectionname</replaceable></option></term>
        <listitem>
-- 
2.49.0

v1-0010-Refactor-external-TOAST-pointer-code-for-better-p.patchtext/x-diff; charset=us-asciiDownload
From aab2ca3d076e4c2b05d16204a116d504d7d2c06b Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 19 Jun 2025 12:57:14 +0900
Subject: [PATCH v1 10/12] Refactor external TOAST pointer code for better
 pluggability

This commit introduces a new interface for external TOAST pointers,
which is able to make a translation of the varlena pointers stored on
disk to/from an new in-memory structure called toast_external.  The
types of varatt_external supported on disk need to be registered into a
new subsystem in a new file, called toast_external.[c|h], then define a
set of callbacks to allow the toasting and detoasting code to use it.

The existing varatt_external is renamed to varatt_external_oid, to map
with the fact that this structure is used to store external TOAST
pointers based on OID values.

A follow-up change will rely on this refactoring to introduce a new type
of vartag_external with an associated varatt_external that is able to
store 8-byte value IDs on disk.
---
 src/include/access/detoast.h                  |  12 +-
 src/include/access/heaptoast.h                |   5 +-
 src/include/access/toast_external.h           | 163 ++++++++++++++++++
 src/include/access/toast_helper.h             |   1 +
 src/include/varatt.h                          |  34 ++--
 src/backend/access/common/Makefile            |   1 +
 src/backend/access/common/detoast.c           |  59 +++----
 src/backend/access/common/meson.build         |   1 +
 src/backend/access/common/toast_compression.c |  10 +-
 src/backend/access/common/toast_external.c    | 138 +++++++++++++++
 src/backend/access/common/toast_internals.c   |  83 ++++++---
 src/backend/access/heap/heaptoast.c           |  64 ++++++-
 src/backend/access/table/toast_helper.c       |  20 ++-
 .../replication/logical/reorderbuffer.c       |  25 ++-
 src/backend/utils/adt/varlena.c               |   5 +-
 src/backend/utils/cache/relcache.c            |   1 +
 doc/src/sgml/storage.sgml                     |   2 +-
 contrib/amcheck/verify_heapam.c               |  39 +++--
 src/tools/pgindent/typedefs.list              |   3 +
 19 files changed, 550 insertions(+), 116 deletions(-)
 create mode 100644 src/include/access/toast_external.h
 create mode 100644 src/backend/access/common/toast_external.c

diff --git a/src/include/access/detoast.h b/src/include/access/detoast.h
index e603a2276c38..4195f7b5bdfd 100644
--- a/src/include/access/detoast.h
+++ b/src/include/access/detoast.h
@@ -14,10 +14,11 @@
 
 /*
  * Macro to fetch the possibly-unaligned contents of an EXTERNAL datum
- * into a local "struct varatt_external" toast pointer.  This should be
- * just a memcpy, but some versions of gcc seem to produce broken code
- * that assumes the datum contents are aligned.  Introducing an explicit
- * intermediate "varattrib_1b_e *" variable seems to fix it.
+ * into a local "struct varatt_external_*" toast pointer, as supported
+ * in toast_external.c and varatt.h.  This should be just a memcpy, but
+ * some versions of gcc seem to produce broken code that assumes the datum
+ * contents are aligned.  Introducing an explicit intermediate
+ * "varattrib_1b_e *" variable seems to fix it.
  */
 #define VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr) \
 do { \
@@ -27,9 +28,6 @@ do { \
 	memcpy(&(toast_pointer), VARDATA_EXTERNAL(attre), sizeof(toast_pointer)); \
 } while (0)
 
-/* Size of an EXTERNAL datum that contains a standard TOAST pointer */
-#define TOAST_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external))
-
 /* Size of an EXTERNAL datum that contains an indirection pointer */
 #define INDIRECT_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_indirect))
 
diff --git a/src/include/access/heaptoast.h b/src/include/access/heaptoast.h
index 6e3558cbd6d2..49c31b77e493 100644
--- a/src/include/access/heaptoast.h
+++ b/src/include/access/heaptoast.h
@@ -81,13 +81,16 @@
 
 #define EXTERN_TUPLE_MAX_SIZE	MaximumBytesPerTuple(EXTERN_TUPLES_PER_PAGE)
 
-#define TOAST_MAX_CHUNK_SIZE	\
+#define TOAST_MAX_CHUNK_SIZE_OID	\
 	(EXTERN_TUPLE_MAX_SIZE -							\
 	 MAXALIGN(SizeofHeapTupleHeader) -					\
 	 sizeof(Oid) -										\
 	 sizeof(int32) -									\
 	 VARHDRSZ)
 
+/* Maximum size of chunk possible for both types */
+#define TOAST_MAX_CHUNK_SIZE	TOAST_MAX_CHUNK_SIZE_OID
+
 /* ----------
  * heap_toast_insert_or_update -
  *
diff --git a/src/include/access/toast_external.h b/src/include/access/toast_external.h
new file mode 100644
index 000000000000..c52fd495cecf
--- /dev/null
+++ b/src/include/access/toast_external.h
@@ -0,0 +1,163 @@
+/*-------------------------------------------------------------------------
+ *
+ * toast_external.h
+ *	  Support for on-disk external TOAST pointers
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1995, Regents of the University of California
+ *
+ * src/include/access/toast_external.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef TOAST_EXTERNAL_H
+#define TOAST_EXTERNAL_H
+
+#include "access/toast_compression.h"
+#include "varatt.h"
+
+/*
+ * Intermediate in-memory structure used when creating on-disk
+ * varatt_external_* or when deserializing varlena contents.
+ */
+typedef struct toast_external_data
+{
+	/* Original data size (includes header) */
+	int32       rawsize;
+	/* External saved size (without header) */
+	uint32      extsize;
+	/* compression method */
+	ToastCompressionId compression_method;
+	/* Relation OID of TOAST table containing the value */
+	Oid			toastrelid;
+	/*
+	 * Unique ID of value within TOAST table.  This could be an OID or an
+	 * int8 value.  This field is large enough to be able to store any of
+	 * them.
+	 */
+	uint64		value;
+} toast_external_data;
+
+/*
+ * Metadata for external TOAST pointer kinds, separated based on their
+ * vartag_external.
+ */
+typedef struct toast_external_info
+{
+	/*
+	 * Maximum chunk of data authorized for this type of external TOAST
+	 * pointer, when dividing an entry by chunks.  Sized depending on
+	 * the size of its varatt_external_* structure.
+	 */
+	int32		maximum_chunk_size;
+
+	/*
+	 * Size of an external TOAST pointer of this type, typically
+	 * (VARHDRSZ_EXTERNAL + sizeof(varatt_external_struct)).
+	 */
+	int32		toast_pointer_size;
+
+	/*
+	 * Map an input varlena to a toast_external_data, for consumption
+	 * in the backend code.  "data" is an input/output result.
+	 */
+	void		(*to_external_data) (struct varlena *attr,
+									 toast_external_data *data);
+
+	/*
+	 * Create a varlena that will be used on-disk for the given TOAST
+	 * type, based on the given input data.
+	 *
+	 * The result is the varlena created, for on-disk insertion.
+	 */
+	struct varlena  *(*create_external_data) (toast_external_data data);
+
+} toast_external_info;
+
+/* Retrieve a toast_external_info from a vartag */
+extern const toast_external_info *toast_external_get_info(uint8 tag);
+
+/* Retrieve toast_pointer_size using a TOAST attribute type */
+extern int32 toast_external_info_get_pointer_size(Oid toast_typid);
+
+/*
+ * Testing whether an externally-stored value is compressed now requires
+ * comparing size stored in extsize (the actual length of the external data)
+ * to rawsize (the original uncompressed datum's size).  The latter includes
+ * VARHDRSZ overhead, the former doesn't.  We never use compression unless it
+ * actually saves space, so we expect either equality or less-than.
+ */
+#define TOAST_EXTERNAL_IS_COMPRESSED(data) \
+	((data).extsize < (data).rawsize - VARHDRSZ)
+
+/* Full data structure */
+static inline void
+toast_external_info_get_data(struct varlena *attr, toast_external_data *data)
+{
+	uint8 tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+
+	info->to_external_data(attr, data);
+}
+
+/*
+ * Helper routines to recover specific fields in toast_external_data.  Most
+ * code paths doing work with on-disk external TOAST pointers care about
+ * these.
+ */
+
+/* Detoasted "raw" size */
+static inline Size
+toast_external_info_get_rawsize(struct varlena *attr)
+{
+	uint8 tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+	toast_external_data data;
+
+	info->to_external_data(attr, &data);
+
+	return data.rawsize;
+}
+
+/* External saved size */
+static inline Size
+toast_external_info_get_extsize(struct varlena *attr)
+{
+	uint8 tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+	toast_external_data data;
+
+	info->to_external_data(attr, &data);
+
+	return data.extsize;
+}
+
+/* Compression method ID */
+static inline ToastCompressionId
+toast_external_info_get_compression_method(struct varlena *attr)
+{
+	uint8 tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+	toast_external_data data;
+
+	info->to_external_data(attr, &data);
+
+	return data.compression_method;
+}
+
+/* Value ID */
+static inline Size
+toast_external_info_get_value(struct varlena *attr)
+{
+	uint8 tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+	toast_external_data data;
+
+	info->to_external_data(attr, &data);
+
+	return data.value;
+}
+
+#endif			/* TOAST_EXTERNAL_H */
diff --git a/src/include/access/toast_helper.h b/src/include/access/toast_helper.h
index e6ab8afffb67..729c593afebd 100644
--- a/src/include/access/toast_helper.h
+++ b/src/include/access/toast_helper.h
@@ -47,6 +47,7 @@ typedef struct
 	 * should be NULL in the case of an insert.
 	 */
 	Relation	ttc_rel;		/* the relation that contains the tuple */
+	int32		ttc_toast_pointer_size;	/* size of external TOAST pointer */
 	Datum	   *ttc_values;		/* values from the tuple columns */
 	bool	   *ttc_isnull;		/* null flags for the tuple columns */
 	Datum	   *ttc_oldvalues;	/* values from previous tuple */
diff --git a/src/include/varatt.h b/src/include/varatt.h
index 2e8564d49980..793030dae932 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -16,11 +16,14 @@
 #define VARATT_H
 
 /*
- * struct varatt_external is a traditional "TOAST pointer", that is, the
+ * struct varatt_external_oid is a traditional "TOAST pointer", that is, the
  * information needed to fetch a Datum stored out-of-line in a TOAST table.
  * The data is compressed if and only if the external size stored in
  * va_extinfo is less than va_rawsize - VARHDRSZ.
  *
+ * The value ID used is an OID, used for TOAST relations with OID as
+ * attribute for chunk_id.
+ *
  * This struct must not contain any padding, because we sometimes compare
  * these pointers using memcmp.
  *
@@ -29,14 +32,15 @@
  * you can look at these fields!  (The reason we use memcmp is to avoid
  * having to do that just to detect equality of two TOAST pointers...)
  */
-typedef struct varatt_external
+typedef struct varatt_external_oid
 {
 	int32		va_rawsize;		/* Original data size (includes header) */
 	uint32		va_extinfo;		/* External saved size (without header) and
 								 * compression method */
 	Oid			va_valueid;		/* Unique ID of value within TOAST table */
 	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
-}			varatt_external;
+}			varatt_external_oid;
+
 
 /*
  * These macros define the "saved size" portion of va_extinfo.  Its remaining
@@ -78,15 +82,16 @@ typedef struct varatt_expanded
 
 /*
  * Type tag for the various sorts of "TOAST pointer" datums.  The peculiar
- * value for VARTAG_ONDISK comes from a requirement for on-disk compatibility
- * with a previous notion that the tag field was the pointer datum's length.
+ * value for VARTAG_ONDISK_OID comes from a requirement for on-disk
+ * compatibility with a previous notion that the tag field was the pointer
+ * datum's length.
  */
 typedef enum vartag_external
 {
 	VARTAG_INDIRECT = 1,
 	VARTAG_EXPANDED_RO = 2,
 	VARTAG_EXPANDED_RW = 3,
-	VARTAG_ONDISK = 18
+	VARTAG_ONDISK_OID = 18
 } vartag_external;
 
 /* this test relies on the specific tag values above */
@@ -96,7 +101,7 @@ typedef enum vartag_external
 #define VARTAG_SIZE(tag) \
 	((tag) == VARTAG_INDIRECT ? sizeof(varatt_indirect) : \
 	 VARTAG_IS_EXPANDED(tag) ? sizeof(varatt_expanded) : \
-	 (tag) == VARTAG_ONDISK ? sizeof(varatt_external) : \
+	 (tag) == VARTAG_ONDISK_OID ? sizeof(varatt_external_oid) : \
 	 (AssertMacro(false), 0))
 
 /*
@@ -287,8 +292,10 @@ typedef struct
 
 #define VARATT_IS_COMPRESSED(PTR)			VARATT_IS_4B_C(PTR)
 #define VARATT_IS_EXTERNAL(PTR)				VARATT_IS_1B_E(PTR)
+#define VARATT_IS_EXTERNAL_ONDISK_OID(PTR) \
+	(VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID)
 #define VARATT_IS_EXTERNAL_ONDISK(PTR) \
-	(VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK)
+	(VARATT_IS_EXTERNAL_ONDISK_OID(PTR))
 #define VARATT_IS_EXTERNAL_INDIRECT(PTR) \
 	(VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_INDIRECT)
 #define VARATT_IS_EXTERNAL_EXPANDED_RO(PTR) \
@@ -330,7 +337,10 @@ typedef struct
 #define VARDATA_COMPRESSED_GET_COMPRESS_METHOD(PTR) \
 	(((varattrib_4b *) (PTR))->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS)
 
-/* Same for external Datums; but note argument is a struct varatt_external */
+/*
+ * Same for external Datums; but note argument is a struct
+ * varatt_external_oid.
+ */
 #define VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) \
 	((toast_pointer).va_extinfo & VARLENA_EXTSIZE_MASK)
 #define VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) \
@@ -351,8 +361,10 @@ typedef struct
  * VARHDRSZ overhead, the former doesn't.  We never use compression unless it
  * actually saves space, so we expect either equality or less-than.
  */
+
 #define VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer) \
-	(VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) < \
-	 (toast_pointer).va_rawsize - VARHDRSZ)
+ (VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) < \
+  (toast_pointer).va_rawsize - VARHDRSZ)
+
 
 #endif
diff --git a/src/backend/access/common/Makefile b/src/backend/access/common/Makefile
index 2fd4f4460b65..6e9a3a430c19 100644
--- a/src/backend/access/common/Makefile
+++ b/src/backend/access/common/Makefile
@@ -28,6 +28,7 @@ OBJS = \
 	tidstore.o \
 	toast_compression.o \
 	toast_counter.o \
+	toast_external.o \
 	toast_internals.o \
 	tupconvert.o \
 	tupdesc.o
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 626517877422..684e1b0b7d3b 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -16,6 +16,7 @@
 #include "access/detoast.h"
 #include "access/table.h"
 #include "access/tableam.h"
+#include "access/toast_external.h"
 #include "access/toast_internals.h"
 #include "common/int.h"
 #include "common/pg_lzcompress.h"
@@ -225,12 +226,12 @@ detoast_attr_slice(struct varlena *attr,
 
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
-		struct varatt_external toast_pointer;
+		struct toast_external_data toast_pointer;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		toast_external_info_get_data(attr, &toast_pointer);
 
 		/* fast path for non-compressed external datums */
-		if (!VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+		if (!TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 			return toast_fetch_datum_slice(attr, sliceoffset, slicelength);
 
 		/*
@@ -240,7 +241,7 @@ detoast_attr_slice(struct varlena *attr,
 		 */
 		if (slicelimit >= 0)
 		{
-			int32		max_size = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+			int32		max_size = toast_pointer.extsize;
 
 			/*
 			 * Determine maximum amount of compressed data needed for a prefix
@@ -251,8 +252,7 @@ detoast_attr_slice(struct varlena *attr,
 			 * determine how much compressed data we need to be sure of being
 			 * able to decompress the required slice.
 			 */
-			if (VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) ==
-				TOAST_PGLZ_COMPRESSION_ID)
+			if (toast_pointer.compression_method == TOAST_PGLZ_COMPRESSION_ID)
 				max_size = pglz_maximum_compressed_size(slicelimit, max_size);
 
 			/*
@@ -344,20 +344,21 @@ toast_fetch_datum(struct varlena *attr)
 {
 	Relation	toastrel;
 	struct varlena *result;
-	struct varatt_external toast_pointer;
+	struct toast_external_data toast_pointer;
 	int32		attrsize;
+	uint64		valueid;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		elog(ERROR, "toast_fetch_datum shouldn't be called for non-ondisk datums");
 
 	/* Must copy to access aligned fields */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_external_info_get_data(attr, &toast_pointer);
 
-	attrsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+	attrsize = toast_pointer.extsize;
 
 	result = (struct varlena *) palloc(attrsize + VARHDRSZ);
 
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+	if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 		SET_VARSIZE_COMPRESSED(result, attrsize + VARHDRSZ);
 	else
 		SET_VARSIZE(result, attrsize + VARHDRSZ);
@@ -365,14 +366,15 @@ toast_fetch_datum(struct varlena *attr)
 	if (attrsize == 0)
 		return result;			/* Probably shouldn't happen, but just in
 								 * case. */
+	valueid = toast_pointer.value;
 
 	/*
 	 * Open the toast relation and its indexes
 	 */
-	toastrel = table_open(toast_pointer.va_toastrelid, AccessShareLock);
+	toastrel = table_open(toast_pointer.toastrelid, AccessShareLock);
 
 	/* Fetch all chunks */
-	table_relation_fetch_toast_slice(toastrel, toast_pointer.va_valueid,
+	table_relation_fetch_toast_slice(toastrel, valueid,
 									 attrsize, 0, attrsize, result);
 
 	/* Close toast table */
@@ -398,23 +400,26 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 {
 	Relation	toastrel;
 	struct varlena *result;
-	struct varatt_external toast_pointer;
+	struct toast_external_data toast_pointer;
 	int32		attrsize;
+	uint64		valueid;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		elog(ERROR, "toast_fetch_datum_slice shouldn't be called for non-ondisk datums");
 
 	/* Must copy to access aligned fields */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_external_info_get_data(attr, &toast_pointer);
+
+	valueid = toast_pointer.value;
 
 	/*
 	 * It's nonsense to fetch slices of a compressed datum unless when it's a
 	 * prefix -- this isn't lo_* we can't return a compressed datum which is
 	 * meaningful to toast later.
 	 */
-	Assert(!VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer) || 0 == sliceoffset);
+	Assert(!TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer) || 0 == sliceoffset);
 
-	attrsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+	attrsize = toast_pointer.extsize;
 
 	if (sliceoffset >= attrsize)
 	{
@@ -427,7 +432,7 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 	 * space required by va_tcinfo, which is stored at the beginning as an
 	 * int32 value.
 	 */
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer) && slicelength > 0)
+	if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer) && slicelength > 0)
 		slicelength = slicelength + sizeof(int32);
 
 	/*
@@ -440,7 +445,7 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 
 	result = (struct varlena *) palloc(slicelength + VARHDRSZ);
 
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+	if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 		SET_VARSIZE_COMPRESSED(result, slicelength + VARHDRSZ);
 	else
 		SET_VARSIZE(result, slicelength + VARHDRSZ);
@@ -449,10 +454,11 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 		return result;			/* Can save a lot of work at this point! */
 
 	/* Open the toast relation */
-	toastrel = table_open(toast_pointer.va_toastrelid, AccessShareLock);
+	toastrel = table_open(toast_pointer.toastrelid, AccessShareLock);
 
 	/* Fetch all chunks */
-	table_relation_fetch_toast_slice(toastrel, toast_pointer.va_valueid,
+	table_relation_fetch_toast_slice(toastrel,
+									 valueid,
 									 attrsize, sliceoffset, slicelength,
 									 result);
 
@@ -549,11 +555,7 @@ toast_raw_datum_size(Datum value)
 
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
-		/* va_rawsize is the size of the original datum -- including header */
-		struct varatt_external toast_pointer;
-
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-		result = toast_pointer.va_rawsize;
+		result = toast_external_info_get_rawsize(attr);
 	}
 	else if (VARATT_IS_EXTERNAL_INDIRECT(attr))
 	{
@@ -609,11 +611,10 @@ toast_datum_size(Datum value)
 		 * Attribute is stored externally - return the extsize whether
 		 * compressed or not.  We do not count the size of the toast pointer
 		 * ... should we?
+		 *
+		 * XXX: this comment should be documented elsewhere.
 		 */
-		struct varatt_external toast_pointer;
-
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-		result = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+		result = toast_external_info_get_extsize(attr);
 	}
 	else if (VARATT_IS_EXTERNAL_INDIRECT(attr))
 	{
diff --git a/src/backend/access/common/meson.build b/src/backend/access/common/meson.build
index e6143d9ffb83..4254132c8dfd 100644
--- a/src/backend/access/common/meson.build
+++ b/src/backend/access/common/meson.build
@@ -16,6 +16,7 @@ backend_sources += files(
   'tidstore.c',
   'toast_compression.c',
   'toast_counter.c',
+  'toast_external.c',
   'toast_internals.c',
   'tupconvert.c',
   'tupdesc.c',
diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c
index 21f2f4af97e3..e1c76dea4905 100644
--- a/src/backend/access/common/toast_compression.c
+++ b/src/backend/access/common/toast_compression.c
@@ -19,6 +19,7 @@
 
 #include "access/detoast.h"
 #include "access/toast_compression.h"
+#include "access/toast_external.h"
 #include "common/pg_lzcompress.h"
 #include "varatt.h"
 
@@ -261,14 +262,7 @@ toast_get_compression_id(struct varlena *attr)
 	 * toast compression header.
 	 */
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
-	{
-		struct varatt_external toast_pointer;
-
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-
-		if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
-			cmid = VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer);
-	}
+		cmid = toast_external_info_get_compression_method(attr);
 	else if (VARATT_IS_COMPRESSED(attr))
 		cmid = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(attr);
 
diff --git a/src/backend/access/common/toast_external.c b/src/backend/access/common/toast_external.c
new file mode 100644
index 000000000000..b6e8ff4facde
--- /dev/null
+++ b/src/backend/access/common/toast_external.c
@@ -0,0 +1,138 @@
+/*-------------------------------------------------------------------------
+ *
+ * toast_external.c
+ *	  Functions for the support external on-disk TOAST pointers.
+ *
+ * Copyright (c) 2025, PostgreSQL Global Development Group
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/common/toast_external.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/detoast.h"
+#include "access/heaptoast.h"
+#include "access/toast_external.h"
+
+/* Callbacks for VARTAG_ONDISK_OID */
+static void ondisk_oid_to_external_data(struct varlena *attr,
+										toast_external_data *data);
+static struct varlena *ondisk_oid_create_external_data(toast_external_data data);
+
+
+/*
+ * Size of an EXTERNAL datum that contains a standard TOAST pointer (OID
+ * value).
+ */
+#define TOAST_POINTER_OID_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_oid))
+
+/*
+ * For now there are only two types, all defined in this file.  For now this
+ * is the maximum value of vartag_external, which is a historical choice.
+ */
+#define TOAST_EXTERNAL_INFO_SIZE	(VARTAG_ONDISK_OID + 1)
+
+/*
+ * The different kinds of on-disk external TOAST pointers. divided by
+ * vartag_external.
+ *
+ * See comments for struct toast_external_info about the details of the
+ * individual fields.
+ */
+static const toast_external_info toast_external_infos[TOAST_EXTERNAL_INFO_SIZE] = {
+	[VARTAG_ONDISK_OID] = {
+		.toast_pointer_size = TOAST_POINTER_OID_SIZE,
+		.maximum_chunk_size = TOAST_MAX_CHUNK_SIZE_OID,
+		.to_external_data = ondisk_oid_to_external_data,
+		.create_external_data = ondisk_oid_create_external_data,
+	},
+};
+
+
+/* Get toast_external_info of the defined vartag_external */
+const toast_external_info *
+toast_external_get_info(uint8 tag)
+{
+	return &toast_external_infos[tag];
+}
+
+/*
+ * Get external TOAST pointer size based on the attribute type of a TOAST
+ * value.
+ */
+int32
+toast_external_info_get_pointer_size(Oid toast_typid)
+{
+	/* This could be a loop, but let's take a shortcut for now */
+	if (toast_typid == OIDOID)
+		return toast_external_infos[VARTAG_ONDISK_OID].toast_pointer_size;
+	else if (toast_typid == INT8OID)
+		return toast_external_infos[VARTAG_ONDISK_OID].toast_pointer_size;
+
+	Assert(false);
+	return 0;	/* keep compiler quiet */
+}
+
+/*
+ * Helper routines able to translate the various varatt_external_* from/to
+ * the in-memory representation toast_external_data used in the backend.
+ */
+
+/* Callbacks for VARTAG_ONDISK_OID */
+static void
+ondisk_oid_to_external_data(struct varlena *attr, toast_external_data *data)
+{
+	varatt_external_oid		external;
+
+	VARATT_EXTERNAL_GET_POINTER(external, attr);
+	data->rawsize = external.va_rawsize;
+
+	/*
+	 * External size and compression methods are stored in the same field,
+	 * extract.
+	 */
+	if (VARATT_EXTERNAL_IS_COMPRESSED(external))
+	{
+		data->extsize = VARATT_EXTERNAL_GET_EXTSIZE(external);
+		data->compression_method = VARATT_EXTERNAL_GET_COMPRESS_METHOD(external);
+	}
+	else
+	{
+		data->extsize = external.va_extinfo;
+		data->compression_method = TOAST_INVALID_COMPRESSION_ID;
+	}
+
+	data->value = (uint64) external.va_valueid;
+	data->toastrelid = external.va_toastrelid;
+}
+
+static struct varlena *
+ondisk_oid_create_external_data(toast_external_data data)
+{
+	struct varlena *result = NULL;
+	varatt_external_oid external;
+
+	external.va_rawsize = data.rawsize;
+
+	if (data.compression_method != TOAST_INVALID_COMPRESSION_ID)
+	{
+		/* Set size and compression method, in a single field. */
+		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(external,
+													 data.extsize,
+													 data.compression_method);
+	}
+	else
+		external.va_extinfo = data.extsize;
+
+	external.va_toastrelid = data.toastrelid;
+	external.va_valueid = (Oid) data.value;
+
+	result = (struct varlena *) palloc(TOAST_POINTER_OID_SIZE);
+	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_OID);
+	memcpy(VARDATA_EXTERNAL(result), &external, sizeof(external));
+
+	return result;
+}
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 5e28a33557dc..a7f29398ca9e 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -19,6 +19,7 @@
 #include "access/heaptoast.h"
 #include "access/table.h"
 #include "access/toast_counter.h"
+#include "access/toast_external.h"
 #include "access/toast_internals.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -128,7 +129,7 @@ toast_save_datum(Relation rel, Datum value,
 	bool		t_isnull[3];
 	CommandId	mycid = GetCurrentCommandId(true);
 	struct varlena *result;
-	struct varatt_external toast_pointer;
+	struct toast_external_data toast_pointer;
 	union
 	{
 		struct varlena hdr;
@@ -146,6 +147,8 @@ toast_save_datum(Relation rel, Datum value,
 	int			validIndex;
 	Oid			toast_typid;
 	uint64		new_valueid = 0;
+	const toast_external_info *info;
+	uint8		tag = VARTAG_INDIRECT;	/* init value does not matter */
 
 	Assert(!VARATT_IS_EXTERNAL(value));
 
@@ -164,6 +167,19 @@ toast_save_datum(Relation rel, Datum value,
 	toast_typid = TupleDescAttr(toasttupDesc, 0)->atttypid;
 	Assert(toast_typid == OIDOID || toast_typid == INT8OID);
 
+	/*
+	 * Grab the information for toast_external_data.
+	 *
+	 * Note: this logic assumes that the vartag used is linked to the value
+	 * attribute.  If we support multiple external vartags for a single value
+	 * type, we would need to be smarter in the vartag selection.
+	 */
+	if (toast_typid == OIDOID)
+		tag = VARTAG_ONDISK_OID;
+	else if (toast_typid == INT8OID)
+		tag = VARTAG_ONDISK_OID;
+	info = toast_external_get_info(tag);
+
 	/* Open all the toast indexes and look for the valid one */
 	validIndex = toast_open_indexes(toastrel,
 									RowExclusiveLock,
@@ -184,28 +200,41 @@ toast_save_datum(Relation rel, Datum value,
 	{
 		data_p = VARDATA_SHORT(dval);
 		data_todo = VARSIZE_SHORT(dval) - VARHDRSZ_SHORT;
-		toast_pointer.va_rawsize = data_todo + VARHDRSZ;	/* as if not short */
-		toast_pointer.va_extinfo = data_todo;
+		toast_pointer.rawsize = data_todo + VARHDRSZ;	/* as if not short */
+		toast_pointer.extsize = data_todo;
+
+		/*
+		 * Note: we set compression_method to be able to build a correct
+		 * on-disk TOAST pointer.
+		 */
+		toast_pointer.compression_method = TOAST_INVALID_COMPRESSION_ID;
 	}
 	else if (VARATT_IS_COMPRESSED(dval))
 	{
 		data_p = VARDATA(dval);
 		data_todo = VARSIZE(dval) - VARHDRSZ;
 		/* rawsize in a compressed datum is just the size of the payload */
-		toast_pointer.va_rawsize = VARDATA_COMPRESSED_GET_EXTSIZE(dval) + VARHDRSZ;
+		toast_pointer.rawsize = VARDATA_COMPRESSED_GET_EXTSIZE(dval) + VARHDRSZ;
 
 		/* set external size and compression method */
-		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, data_todo,
-													 VARDATA_COMPRESSED_GET_COMPRESS_METHOD(dval));
+		toast_pointer.extsize = data_todo;
+		toast_pointer.compression_method = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(dval);
+
 		/* Assert that the numbers look like it's compressed */
-		Assert(VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer));
+		Assert(TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer));
 	}
 	else
 	{
 		data_p = VARDATA(dval);
 		data_todo = VARSIZE(dval) - VARHDRSZ;
-		toast_pointer.va_rawsize = VARSIZE(dval);
-		toast_pointer.va_extinfo = data_todo;
+		toast_pointer.rawsize = VARSIZE(dval);
+		toast_pointer.extsize = data_todo;
+
+		/*
+		 * Note: we set compression_method to be able to build a correct
+		 * on-disk TOAST pointer.
+		 */
+		toast_pointer.compression_method = TOAST_INVALID_COMPRESSION_ID;
 	}
 
 	/*
@@ -217,9 +246,9 @@ toast_save_datum(Relation rel, Datum value,
 	 * if we have to substitute such an OID.
 	 */
 	if (OidIsValid(rel->rd_toastoid))
-		toast_pointer.va_toastrelid = rel->rd_toastoid;
+		toast_pointer.toastrelid = rel->rd_toastoid;
 	else
-		toast_pointer.va_toastrelid = RelationGetRelid(toastrel);
+		toast_pointer.toastrelid = RelationGetRelid(toastrel);
 
 	/*
 	 * Choose a new value to use as the value ID for this toast value, be it
@@ -259,13 +288,13 @@ toast_save_datum(Relation rel, Datum value,
 			Assert(false);
 		if (oldexternal != NULL)
 		{
-			struct varatt_external old_toast_pointer;
+			struct toast_external_data old_toast_pointer;
 
 			Assert(VARATT_IS_EXTERNAL_ONDISK(oldexternal));
-			/* Must copy to access aligned fields */
-			VARATT_EXTERNAL_GET_POINTER(old_toast_pointer, oldexternal);
 
-			if (old_toast_pointer.va_toastrelid == rel->rd_toastoid)
+			toast_external_info_get_data(oldexternal, &old_toast_pointer);
+
+			if (old_toast_pointer.toastrelid == rel->rd_toastoid)
 			{
 				uint64		old_valueid;
 
@@ -273,7 +302,7 @@ toast_save_datum(Relation rel, Datum value,
 				 * The old and new toast relations match, hence their type
 				 * of value match as well.
 				 */
-				old_valueid = old_toast_pointer.va_valueid;
+				old_valueid = old_toast_pointer.value;
 
 				/*
 				 * There is a corner case here: the table rewrite might have
@@ -326,15 +355,15 @@ toast_save_datum(Relation rel, Datum value,
 	}
 
 	/* Now set the new value */
-	toast_pointer.va_valueid = new_valueid;
+	toast_pointer.value = new_valueid;
 
 	/*
 	 * Initialize constant parts of the tuple data
 	 */
 	if (toast_typid == OIDOID)
-		t_values[0] = ObjectIdGetDatum(toast_pointer.va_valueid);
+		t_values[0] = ObjectIdGetDatum(toast_pointer.value);
 	else if (toast_typid == INT8OID)
-		t_values[0] = Int64GetDatum(toast_pointer.va_valueid);
+		t_values[0] = Int64GetDatum(toast_pointer.value);
 	t_values[2] = PointerGetDatum(&chunk_data);
 	t_isnull[0] = false;
 	t_isnull[1] = false;
@@ -352,7 +381,7 @@ toast_save_datum(Relation rel, Datum value,
 		/*
 		 * Calculate the size of this chunk
 		 */
-		chunk_size = Min(TOAST_MAX_CHUNK_SIZE, data_todo);
+		chunk_size = Min(info->maximum_chunk_size, data_todo);
 
 		/*
 		 * Build a tuple and store it
@@ -410,9 +439,7 @@ toast_save_datum(Relation rel, Datum value,
 	/*
 	 * Create the TOAST pointer value that we'll return
 	 */
-	result = (struct varlena *) palloc(TOAST_POINTER_SIZE);
-	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK);
-	memcpy(VARDATA_EXTERNAL(result), &toast_pointer, sizeof(toast_pointer));
+	result = info->create_external_data(toast_pointer);
 
 	return PointerGetDatum(result);
 }
@@ -427,7 +454,7 @@ void
 toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 {
 	struct varlena *attr = (struct varlena *) DatumGetPointer(value);
-	struct varatt_external toast_pointer;
+	struct toast_external_data toast_pointer;
 	Relation	toastrel;
 	Relation   *toastidxs;
 	ScanKeyData toastkey;
@@ -441,12 +468,12 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 		return;
 
 	/* Must copy to access aligned fields */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_external_info_get_data(attr, &toast_pointer);
 
 	/*
 	 * Open the toast relation and its indexes
 	 */
-	toastrel = table_open(toast_pointer.va_toastrelid, RowExclusiveLock);
+	toastrel = table_open(toast_pointer.toastrelid, RowExclusiveLock);
 	toast_typid = TupleDescAttr(toastrel->rd_att, 0)->atttypid;
 	Assert(toast_typid == OIDOID || toast_typid == INT8OID);
 
@@ -463,12 +490,12 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 		ScanKeyInit(&toastkey,
 					(AttrNumber) 1,
 					BTEqualStrategyNumber, F_OIDEQ,
-					ObjectIdGetDatum(toast_pointer.va_valueid));
+					ObjectIdGetDatum(toast_pointer.value));
 	else if (toast_typid == INT8OID)
 		ScanKeyInit(&toastkey,
 					(AttrNumber) 1,
 					BTEqualStrategyNumber, F_INT8EQ,
-					Int64GetDatum(toast_pointer.va_valueid));
+					Int64GetDatum(toast_pointer.value));
 	else
 		Assert(false);
 
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index f45be1d0d401..6993aef2c2c3 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -28,9 +28,12 @@
 #include "access/genam.h"
 #include "access/heapam.h"
 #include "access/heaptoast.h"
+#include "access/toast_external.h"
 #include "access/toast_helper.h"
 #include "access/toast_internals.h"
+#include "access/toast_type.h"
 #include "utils/fmgroids.h"
+#include "utils/syscache.h"
 
 
 /* ----------
@@ -140,6 +143,46 @@ heap_toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup,
 	 * Prepare for toasting
 	 * ----------
 	 */
+
+	/* Retrieve the toast pointer size based on the TOAST value type. */
+	if (OidIsValid(rel->rd_rel->reltoastrelid))
+	{
+		HeapTuple	atttuple;
+		Form_pg_attribute atttoast;
+
+		/*
+		 * XXX: This is very unlikely efficient, but it is not possible to
+		 * rely on the relation cache to retrieve this information as syscache
+		 * lookups should not happen when loading critical entries.
+		 */
+		atttuple = SearchSysCacheAttNum(rel->rd_rel->reltoastrelid, 1);
+		if (!HeapTupleIsValid(atttuple))
+			elog(ERROR, "cache lookup failed for relation %u",
+				 rel->rd_rel->reltoastrelid);
+		atttoast = (Form_pg_attribute) GETSTRUCT(atttuple);
+		ttc.ttc_toast_pointer_size =
+			toast_external_info_get_pointer_size(atttoast->atttypid);
+		ReleaseSysCache(atttuple);
+	}
+	else
+	{
+		/*
+		 * No TOAST relation to rely on, which is a case possible when
+		 * dealing with partitioned tables, for example.  Hence, perform
+		 * a best guess based on the GUC default_toast_type.
+		 */
+		Oid		toast_typeid = InvalidOid;
+
+		if (default_toast_type == TOAST_TYPE_INT8)
+			toast_typeid = INT8OID;
+		else if (default_toast_type == TOAST_TYPE_OID)
+			toast_typeid = OIDOID;
+		else
+			Assert(false);
+		ttc.ttc_toast_pointer_size =
+			toast_external_info_get_pointer_size(toast_typeid);
+	}
+
 	ttc.ttc_rel = rel;
 	ttc.ttc_values = toast_values;
 	ttc.ttc_isnull = toast_isnull;
@@ -641,6 +684,8 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 	int			validIndex;
 	int32		max_chunk_size;
 	Oid			toast_typid;
+	const toast_external_info *info;
+	uint8		tag = VARTAG_INDIRECT;  /* init value does not matter */
 
 	/* Look for the valid index of toast relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -651,7 +696,24 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 	toast_typid = TupleDescAttr(toastrel->rd_att, 0)->atttypid;
 	Assert(toast_typid == OIDOID || toast_typid == INT8OID);
 
-	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	/*
+	 * Grab the information for toast_external_data.
+	 *
+	 * Note: there is no access to the vartag of the original varlena from
+	 * which we are trying to retrieve the chunks from the TOAST relation,
+	 * so guess the external TOAST pointer information to use depending
+	 * on the attribute of the TOAST value.  If we begin to support multiple
+	 * external TOAST pointers for a single attribute type, we would need
+	 * to pass down this information from the upper callers.  This is
+	 * currently on required for the maximum chunk_size.
+	 */
+	if (toast_typid == OIDOID)
+		tag = VARTAG_ONDISK_OID;
+	else if (toast_typid == INT8OID)
+		tag = VARTAG_ONDISK_OID;
+	info = toast_external_get_info(tag);
+
+	max_chunk_size = info->maximum_chunk_size;
 
 	totalchunks = ((attrsize - 1) / max_chunk_size) + 1;
 	startchunk = sliceoffset / max_chunk_size;
diff --git a/src/backend/access/table/toast_helper.c b/src/backend/access/table/toast_helper.c
index b60fab0a4d29..7fa22e2403eb 100644
--- a/src/backend/access/table/toast_helper.c
+++ b/src/backend/access/table/toast_helper.c
@@ -171,8 +171,10 @@ toast_tuple_init(ToastTupleContext *ttc)
  * The column must have attstorage EXTERNAL or EXTENDED if check_main is
  * false, and must have attstorage MAIN if check_main is true.
  *
- * The column must have a minimum size of MAXALIGN(TOAST_POINTER_SIZE);
- * if not, no benefit is to be expected by compressing it.
+ * The column must have a minimum size of MAXALIGN(tcc_toast_pointer_size);
+ * if not, no benefit is to be expected by compressing it.  The TOAST
+ * pointer size is given by the caller, depending on the type of TOAST
+ * table we are dealing with.
  *
  * The return value is the index of the biggest suitable column, or
  * -1 if there is none.
@@ -184,10 +186,22 @@ toast_tuple_find_biggest_attribute(ToastTupleContext *ttc,
 	TupleDesc	tupleDesc = ttc->ttc_rel->rd_att;
 	int			numAttrs = tupleDesc->natts;
 	int			biggest_attno = -1;
-	int32		biggest_size = MAXALIGN(TOAST_POINTER_SIZE);
+	int32		biggest_size = 0;
 	int32		skip_colflags = TOASTCOL_IGNORE;
 	int			i;
 
+	/*
+	 * Define the lower-bound, depending on the TOAST table type used
+	 * for values from the relation cache.
+	 *
+	 * Note: if we begin supporting multiple external TOAST pointer types
+	 * in a single attribute value type, we should reconsider how this
+	 * lower-bound is defined.  Should it be a minimum or a maximum value
+	 * of all the TOAST pointer sizes supported for this relation?
+	 */
+	biggest_size = MAXALIGN(ttc->ttc_toast_pointer_size);
+	Assert(biggest_size != 0);
+
 	if (for_compression)
 		skip_colflags |= TOASTCOL_INCOMPRESSIBLE;
 
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 5ba1ca78ff67..0436d0e0f6b1 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -92,6 +92,7 @@
 #include "access/detoast.h"
 #include "access/heapam.h"
 #include "access/rewriteheap.h"
+#include "access/toast_external.h"
 #include "access/transam.h"
 #include "access/xact.h"
 #include "access/xlog_internal.h"
@@ -4970,14 +4971,24 @@ ReorderBufferToastAppendChunk(ReorderBuffer *rb, ReorderBufferTXN *txn,
 	TupleDesc	desc = RelationGetDescr(relation);
 	uint64		chunk_id;
 	int32		chunk_seq;
+	Oid			toast_typid;
 
 	if (txn->toast_hash == NULL)
 		ReorderBufferToastInitHash(rb, txn);
+	toast_typid = TupleDescAttr(desc, 0)->atttypid;
 
 	Assert(IsToastRelation(relation));
 
 	newtup = change->data.tp.newtuple;
-	chunk_id = DatumGetObjectId(fastgetattr(newtup, 1, desc, &isnull));
+
+	/* This depends on the type of TOAST value dealt with. */
+	if (toast_typid == OIDOID)
+		chunk_id = DatumGetObjectId(fastgetattr(newtup, 1, desc, &isnull));
+	else if (toast_typid == INT8OID)
+		chunk_id = DatumGetUInt64(fastgetattr(newtup, 1, desc, &isnull));
+	else
+		Assert(false);
+
 	Assert(!isnull);
 	chunk_seq = DatumGetInt32(fastgetattr(newtup, 2, desc, &isnull));
 	Assert(!isnull);
@@ -5102,7 +5113,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		struct varlena *varlena;
 
 		/* va_rawsize is the size of the original datum -- including header */
-		struct varatt_external toast_pointer;
+		struct toast_external_data toast_pointer;
 		struct varatt_indirect redirect_pointer;
 		struct varlena *new_datum = NULL;
 		struct varlena *reconstructed;
@@ -5132,8 +5143,8 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		if (!VARATT_IS_EXTERNAL(varlena))
 			continue;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, varlena);
-		toast_valueid = toast_pointer.va_valueid;
+		toast_external_info_get_data(varlena, &toast_pointer);
+		toast_valueid = toast_pointer.value;
 
 		/*
 		 * Check whether the toast tuple changed, replace if so.
@@ -5151,7 +5162,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 
 		free[natt] = true;
 
-		reconstructed = palloc0(toast_pointer.va_rawsize);
+		reconstructed = palloc0(toast_pointer.rawsize);
 
 		ent->reconstructed = reconstructed;
 
@@ -5176,10 +5187,10 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 				   VARSIZE(chunk) - VARHDRSZ);
 			data_done += VARSIZE(chunk) - VARHDRSZ;
 		}
-		Assert(data_done == VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer));
+		Assert(data_done == toast_pointer.extsize);
 
 		/* make sure its marked as compressed or not */
-		if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+		if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 			SET_VARSIZE_COMPRESSED(reconstructed, data_done + VARHDRSZ);
 		else
 			SET_VARSIZE(reconstructed, data_done + VARHDRSZ);
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 093994b1070f..fc872a1ccfad 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -19,6 +19,7 @@
 
 #include "access/detoast.h"
 #include "access/toast_compression.h"
+#include "access/toast_external.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_type.h"
 #include "common/hashfn.h"
@@ -5317,7 +5318,6 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 {
 	int			typlen;
 	struct varlena *attr;
-	struct varatt_external toast_pointer;
 	uint64		toast_valueid;
 
 	/* On first call, get the input type's typlen, and save at *fn_extra */
@@ -5345,8 +5345,7 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		PG_RETURN_NULL();
 
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-	toast_valueid = toast_pointer.va_valueid;
+	toast_valueid = toast_external_info_get_value(attr);
 
 	PG_RETURN_UINT64(toast_valueid);
 }
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 559ba9cdb2cd..8761762a6f3a 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -37,6 +37,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "access/tableam.h"
+#include "access/toast_external.h"
 #include "access/tupdesc_details.h"
 #include "access/xact.h"
 #include "catalog/binary_upgrade.h"
diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml
index 1b2592645ab4..564783a1c559 100644
--- a/doc/src/sgml/storage.sgml
+++ b/doc/src/sgml/storage.sgml
@@ -415,7 +415,7 @@ described in more detail below.
 
 <para>
 Out-of-line values are divided (after compression if used) into chunks of at
-most <symbol>TOAST_MAX_CHUNK_SIZE</symbol> bytes (by default this value is chosen
+most <symbol>TOAST_MAX_CHUNK_SIZE_OID</symbol> bytes (by default this value is chosen
 so that four chunk rows will fit on a page, making it about 2000 bytes).
 Each chunk is stored as a separate row in the <acronym>TOAST</acronym> table
 belonging to the owning table.  Every
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 885400fa7058..0898b6eea074 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -16,6 +16,7 @@
 #include "access/multixact.h"
 #include "access/relation.h"
 #include "access/table.h"
+#include "access/toast_external.h"
 #include "access/toast_internals.h"
 #include "access/visibilitymap.h"
 #include "access/xact.h"
@@ -73,7 +74,8 @@ typedef enum SkipPages
  */
 typedef struct ToastedAttribute
 {
-	struct varatt_external toast_pointer;
+	struct toast_external_data toast_pointer;
+	const toast_external_info *info;
 	BlockNumber blkno;			/* block in main table */
 	OffsetNumber offnum;		/* offset in main table */
 	AttrNumber	attnum;			/* attribute in main table */
@@ -1564,9 +1566,9 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	uint64		toast_valueid;
 	int32		max_chunk_size;
 
-	toast_valueid = ta->toast_pointer.va_valueid;
+	toast_valueid = ta->toast_pointer.value;
 
-	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	max_chunk_size = ta->info->maximum_chunk_size;
 	last_chunk_seq = (extsize - 1) / max_chunk_size;
 
 	/* Sanity-check the sequence number. */
@@ -1672,7 +1674,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	uint16		infomask;
 	uint64		toast_pointer_valueid;
 	CompactAttribute *thisatt;
-	struct varatt_external toast_pointer;
+	struct toast_external_data toast_pointer;
 
 	infomask = ctx->tuphdr->t_infomask;
 	thisatt = TupleDescCompactAttr(RelationGetDescr(ctx->rel), ctx->attnum);
@@ -1731,7 +1733,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	{
 		uint8		va_tag = VARTAG_EXTERNAL(tp + ctx->offset);
 
-		if (va_tag != VARTAG_ONDISK)
+		if (va_tag != VARTAG_ONDISK_OID)
 		{
 			report_corruption(ctx,
 							  psprintf("toasted attribute has unexpected TOAST tag %u",
@@ -1774,28 +1776,28 @@ check_tuple_attribute(HeapCheckContext *ctx)
 		return true;
 
 	/* It is external, and we're looking at a page on disk */
-	toast_pointer_valueid = toast_pointer.va_valueid;
 
 	/*
 	 * Must copy attr into toast_pointer for alignment considerations
 	 */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_external_info_get_data(attr, &toast_pointer);
+	toast_pointer_valueid = toast_pointer.value;
 
 	/* Toasted attributes too large to be untoasted should never be stored */
-	if (toast_pointer.va_rawsize > VARLENA_SIZE_LIMIT)
+	if (toast_pointer.rawsize > VARLENA_SIZE_LIMIT)
 		report_corruption(ctx,
 						  psprintf("toast value %" PRIu64 " rawsize %d exceeds limit %d",
 								   toast_pointer_valueid,
-								   toast_pointer.va_rawsize,
+								   toast_pointer.rawsize,
 								   VARLENA_SIZE_LIMIT));
 
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+	if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 	{
 		ToastCompressionId cmid;
 		bool		valid = false;
 
 		/* Compressed attributes should have a valid compression method */
-		cmid = TOAST_COMPRESS_METHOD(&toast_pointer);
+		cmid = toast_pointer.compression_method;
 		switch (cmid)
 		{
 				/* List of all valid compression method IDs */
@@ -1849,7 +1851,8 @@ check_tuple_attribute(HeapCheckContext *ctx)
 
 		ta = (ToastedAttribute *) palloc0(sizeof(ToastedAttribute));
 
-		VARATT_EXTERNAL_GET_POINTER(ta->toast_pointer, attr);
+		toast_external_info_get_data(attr, &ta->toast_pointer);
+		ta->info = toast_external_get_info(VARTAG_EXTERNAL(attr));
 		ta->blkno = ctx->blkno;
 		ta->offnum = ctx->offnum;
 		ta->attnum = ctx->attnum;
@@ -1876,12 +1879,14 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	int32		expected_chunk_seq = 0;
 	int32		last_chunk_seq;
 	uint64		toast_valueid;
-	int32		max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	int32		max_chunk_size;
 	Oid			toast_typid;
 
+	extsize = ta->toast_pointer.extsize;
+
 	toast_typid = TupleDescAttr(ctx->toast_rel->rd_att, 0)->atttypid;
 
-	extsize = VARATT_EXTERNAL_GET_EXTSIZE(ta->toast_pointer);
+	max_chunk_size = ta->info->maximum_chunk_size;
 	last_chunk_seq = (extsize - 1) / max_chunk_size;
 
 	/*
@@ -1891,12 +1896,12 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 		ScanKeyInit(&toastkey,
 					(AttrNumber) 1,
 					BTEqualStrategyNumber, F_OIDEQ,
-					ObjectIdGetDatum(ta->toast_pointer.va_valueid));
+					ObjectIdGetDatum(ta->toast_pointer.value));
 	else if (toast_typid == INT8OID)
 		ScanKeyInit(&toastkey,
 					(AttrNumber) 1,
 					BTEqualStrategyNumber, F_INT8EQ,
-					Int64GetDatum(ta->toast_pointer.va_valueid));
+					Int64GetDatum(ta->toast_pointer.value));
 	else
 		Assert(false);
 
@@ -1918,7 +1923,7 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	}
 	systable_endscan_ordered(toastscan);
 
-	toast_valueid = ta->toast_pointer.va_valueid;
+	toast_valueid = ta->toast_pointer.value;
 
 	if (!found_toasttup)
 		report_toast_corruption(ctx, ta,
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 4e1d553643bf..344dc8f1e422 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3046,6 +3046,7 @@ ToastAttrInfo
 ToastCompressionId
 ToastCounterData
 ToastTupleContext
+ToastTypeId
 ToastedAttribute
 TocEntry
 TokenAuxData
@@ -4130,6 +4131,8 @@ timeout_params
 timerCA
 tlist_vinfo
 toast_compress_header
+toast_external_data
+toast_external_info
 tokenize_error_callback_arg
 transferMode
 transfer_thread_arg
-- 
2.49.0

v1-0011-Add-new-vartag_external-for-8-byte-TOAST-values.patchtext/x-diff; charset=us-asciiDownload
From 2134a99b286c37fbdd6eee847b94abceeb28f7a2 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 19 Jun 2025 13:09:09 +0900
Subject: [PATCH v1 11/12] Add new vartag_external for 8-byte TOAST values

This is a new type of external TOAST pointer, able to be fed 8-byte
TOAST values.  It uses a dedicated vartag_external, which is used when
a TOAST table uses bigint for its chunk_id.

The relevant callbacks are added to toast_external.c.
---
 src/include/access/heaptoast.h              |  8 ++-
 src/include/varatt.h                        | 31 ++++++++-
 src/backend/access/common/toast_external.c  | 77 ++++++++++++++++++++-
 src/backend/access/common/toast_internals.c |  2 +-
 src/backend/access/heap/heaptoast.c         |  2 +-
 doc/src/sgml/storage.sgml                   |  6 +-
 contrib/amcheck/verify_heapam.c             |  2 +-
 7 files changed, 119 insertions(+), 9 deletions(-)

diff --git a/src/include/access/heaptoast.h b/src/include/access/heaptoast.h
index 49c31b77e493..f72755a64dfe 100644
--- a/src/include/access/heaptoast.h
+++ b/src/include/access/heaptoast.h
@@ -81,6 +81,12 @@
 
 #define EXTERN_TUPLE_MAX_SIZE	MaximumBytesPerTuple(EXTERN_TUPLES_PER_PAGE)
 
+#define TOAST_MAX_CHUNK_SIZE_INT8	\
+	(EXTERN_TUPLE_MAX_SIZE -							\
+	 MAXALIGN(SizeofHeapTupleHeader) -					\
+	 (sizeof(uint32) * 2) -								\
+	 sizeof(int32) -									\
+	 VARHDRSZ)
 #define TOAST_MAX_CHUNK_SIZE_OID	\
 	(EXTERN_TUPLE_MAX_SIZE -							\
 	 MAXALIGN(SizeofHeapTupleHeader) -					\
@@ -89,7 +95,7 @@
 	 VARHDRSZ)
 
 /* Maximum size of chunk possible for both types */
-#define TOAST_MAX_CHUNK_SIZE	TOAST_MAX_CHUNK_SIZE_OID
+#define TOAST_MAX_CHUNK_SIZE	Max(TOAST_MAX_CHUNK_SIZE_INT8, TOAST_MAX_CHUNK_SIZE_OID)
 
 /* ----------
  * heap_toast_insert_or_update -
diff --git a/src/include/varatt.h b/src/include/varatt.h
index 793030dae932..aa36e8e1f561 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -41,6 +41,29 @@ typedef struct varatt_external_oid
 	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
 }			varatt_external_oid;
 
+/*
+ * struct varatt_external_int8 is a "larger" version of "TOAST pointer",
+ * that uses an 8-byte integer as value.
+ *
+ * This follows the same properties as varatt_external_oid, except that
+ * this is used in TOAST relations with int8 as attribute for chunk_id.
+ */
+typedef struct varatt_external_int8
+{
+	int32		va_rawsize;		/* Original data size (includes header) */
+	uint32		va_extinfo;		/* External saved size (without header) and
+								 * compression method */
+	/*
+	 * Unique ID of value within TOAST table, as two uint32 for alignment
+	 * and padding.
+	 * XXX: think for example about the addition of an extra field for
+	 * meta-data and/or more compression data, even if it's OK here).
+	 */
+	uint32		va_valueid_lo;
+	uint32		va_valueid_hi;
+	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
+}			varatt_external_int8;
+
 
 /*
  * These macros define the "saved size" portion of va_extinfo.  Its remaining
@@ -91,6 +114,7 @@ typedef enum vartag_external
 	VARTAG_INDIRECT = 1,
 	VARTAG_EXPANDED_RO = 2,
 	VARTAG_EXPANDED_RW = 3,
+	VARTAG_ONDISK_INT8 = 4,
 	VARTAG_ONDISK_OID = 18
 } vartag_external;
 
@@ -102,6 +126,7 @@ typedef enum vartag_external
 	((tag) == VARTAG_INDIRECT ? sizeof(varatt_indirect) : \
 	 VARTAG_IS_EXPANDED(tag) ? sizeof(varatt_expanded) : \
 	 (tag) == VARTAG_ONDISK_OID ? sizeof(varatt_external_oid) : \
+	 (tag) == VARTAG_ONDISK_INT8 ? sizeof(varatt_external_int8) : \
 	 (AssertMacro(false), 0))
 
 /*
@@ -294,8 +319,10 @@ typedef struct
 #define VARATT_IS_EXTERNAL(PTR)				VARATT_IS_1B_E(PTR)
 #define VARATT_IS_EXTERNAL_ONDISK_OID(PTR) \
 	(VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID)
+#define VARATT_IS_EXTERNAL_ONDISK_INT8(PTR) \
+	(VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_INT8)
 #define VARATT_IS_EXTERNAL_ONDISK(PTR) \
-	(VARATT_IS_EXTERNAL_ONDISK_OID(PTR))
+	(VARATT_IS_EXTERNAL_ONDISK_OID(PTR) || VARATT_IS_EXTERNAL_ONDISK_INT8(PTR))
 #define VARATT_IS_EXTERNAL_INDIRECT(PTR) \
 	(VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_INDIRECT)
 #define VARATT_IS_EXTERNAL_EXPANDED_RO(PTR) \
@@ -339,7 +366,7 @@ typedef struct
 
 /*
  * Same for external Datums; but note argument is a struct
- * varatt_external_oid.
+ * varatt_external_oid or varatt_external_int8.
  */
 #define VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) \
 	((toast_pointer).va_extinfo & VARLENA_EXTSIZE_MASK)
diff --git a/src/backend/access/common/toast_external.c b/src/backend/access/common/toast_external.c
index b6e8ff4facde..1f21dc699799 100644
--- a/src/backend/access/common/toast_external.c
+++ b/src/backend/access/common/toast_external.c
@@ -17,12 +17,23 @@
 #include "access/heaptoast.h"
 #include "access/toast_external.h"
 
+/* Callbacks for VARTAG_ONDISK_INT8 */
+static void ondisk_int8_to_external_data(struct varlena *attr,
+										 toast_external_data *data);
+static struct varlena *ondisk_int8_create_external_data(toast_external_data data);
+
 /* Callbacks for VARTAG_ONDISK_OID */
 static void ondisk_oid_to_external_data(struct varlena *attr,
 										toast_external_data *data);
 static struct varlena *ondisk_oid_create_external_data(toast_external_data data);
 
 
+/*
+ * Size of an EXTERNAL datum that contains a standard TOAST pointer
+ * (int8 value).
+ */
+#define TOAST_POINTER_INT8_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_int8))
+
 /*
  * Size of an EXTERNAL datum that contains a standard TOAST pointer (OID
  * value).
@@ -43,8 +54,14 @@ static struct varlena *ondisk_oid_create_external_data(toast_external_data data)
  * individual fields.
  */
 static const toast_external_info toast_external_infos[TOAST_EXTERNAL_INFO_SIZE] = {
+	[VARTAG_ONDISK_INT8] = {
+		.toast_pointer_size = TOAST_POINTER_INT8_SIZE,
+		.maximum_chunk_size = TOAST_MAX_CHUNK_SIZE_INT8,
+		.to_external_data = ondisk_int8_to_external_data,
+		.create_external_data = ondisk_int8_create_external_data,
+	},
 	[VARTAG_ONDISK_OID] = {
-		.toast_pointer_size = TOAST_POINTER_OID_SIZE,
+		.toast_pointer_size = TOAST_POINTER_INT8_SIZE,
 		.maximum_chunk_size = TOAST_MAX_CHUNK_SIZE_OID,
 		.to_external_data = ondisk_oid_to_external_data,
 		.create_external_data = ondisk_oid_create_external_data,
@@ -70,7 +87,7 @@ toast_external_info_get_pointer_size(Oid toast_typid)
 	if (toast_typid == OIDOID)
 		return toast_external_infos[VARTAG_ONDISK_OID].toast_pointer_size;
 	else if (toast_typid == INT8OID)
-		return toast_external_infos[VARTAG_ONDISK_OID].toast_pointer_size;
+		return toast_external_infos[VARTAG_ONDISK_INT8].toast_pointer_size;
 
 	Assert(false);
 	return 0;	/* keep compiler quiet */
@@ -81,6 +98,62 @@ toast_external_info_get_pointer_size(Oid toast_typid)
  * the in-memory representation toast_external_data used in the backend.
  */
 
+/* Callbacks for VARTAG_ONDISK_INT8 */
+static void
+ondisk_int8_to_external_data(struct varlena *attr, toast_external_data *data)
+{
+	varatt_external_int8	external;
+
+	VARATT_EXTERNAL_GET_POINTER(external, attr);
+	data->rawsize = external.va_rawsize;
+
+	/* External size and compression methods are stored in the same field */
+	if (VARATT_EXTERNAL_IS_COMPRESSED(external))
+	{
+		data->extsize = VARATT_EXTERNAL_GET_EXTSIZE(external);
+		data->compression_method = VARATT_EXTERNAL_GET_COMPRESS_METHOD(external);
+	}
+	else
+	{
+		data->extsize = external.va_extinfo;
+		data->compression_method = TOAST_INVALID_COMPRESSION_ID;
+	}
+
+	data->value = (((uint64) external.va_valueid_hi) << 32) |
+		external.va_valueid_lo;
+	data->toastrelid = external.va_toastrelid;
+
+}
+
+static struct varlena *
+ondisk_int8_create_external_data(toast_external_data data)
+{
+	struct varlena *result = NULL;
+	varatt_external_int8 external;
+
+	external.va_rawsize = data.rawsize;
+
+	if (data.compression_method != TOAST_INVALID_COMPRESSION_ID)
+	{
+		/* Set size and compression method, in a single field. */
+		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(external,
+													 data.extsize,
+													 data.compression_method);
+	}
+	else
+		external.va_extinfo = data.extsize;
+
+	external.va_toastrelid = data.toastrelid;
+	external.va_valueid_hi = (((uint64) data.value) >> 32);
+	external.va_valueid_lo = (uint32) data.value;
+
+	result = (struct varlena *) palloc(TOAST_POINTER_INT8_SIZE);
+	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_INT8);
+	memcpy(VARDATA_EXTERNAL(result), &external, sizeof(external));
+
+	return result;
+}
+
 /* Callbacks for VARTAG_ONDISK_OID */
 static void
 ondisk_oid_to_external_data(struct varlena *attr, toast_external_data *data)
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index a7f29398ca9e..2e155f7f6b79 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -177,7 +177,7 @@ toast_save_datum(Relation rel, Datum value,
 	if (toast_typid == OIDOID)
 		tag = VARTAG_ONDISK_OID;
 	else if (toast_typid == INT8OID)
-		tag = VARTAG_ONDISK_OID;
+		tag = VARTAG_ONDISK_INT8;
 	info = toast_external_get_info(tag);
 
 	/* Open all the toast indexes and look for the valid one */
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index 6993aef2c2c3..fa132841c945 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -710,7 +710,7 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 	if (toast_typid == OIDOID)
 		tag = VARTAG_ONDISK_OID;
 	else if (toast_typid == INT8OID)
-		tag = VARTAG_ONDISK_OID;
+		tag = VARTAG_ONDISK_INT8;
 	info = toast_external_get_info(tag);
 
 	max_chunk_size = info->maximum_chunk_size;
diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml
index 564783a1c559..29ba80e8423c 100644
--- a/doc/src/sgml/storage.sgml
+++ b/doc/src/sgml/storage.sgml
@@ -415,7 +415,11 @@ described in more detail below.
 
 <para>
 Out-of-line values are divided (after compression if used) into chunks of at
-most <symbol>TOAST_MAX_CHUNK_SIZE_OID</symbol> bytes (by default this value is chosen
+most <symbol>TOAST_MAX_CHUNK_SIZE_OID</symbol> bytes if the
+<acronym>TOAST</acronym> relation uses the <literal>oid</literal> type for
+<literal>chunk_id</literal>, or <symbol>TOAST_MAX_CHUNK_SIZE_INT8</symbol>
+bytes if the <acronym>TOAST</acronym> relation uses the <literal>int8</literal>
+type for <literal>chunk_id</literal> (by default these values are chosen
 so that four chunk rows will fit on a page, making it about 2000 bytes).
 Each chunk is stored as a separate row in the <acronym>TOAST</acronym> table
 belonging to the owning table.  Every
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 0898b6eea074..12e9ceb4d7ae 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1733,7 +1733,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	{
 		uint8		va_tag = VARTAG_EXTERNAL(tp + ctx->offset);
 
-		if (va_tag != VARTAG_ONDISK_OID)
+		if (va_tag != VARTAG_ONDISK_OID && va_tag != VARTAG_ONDISK_INT8)
 		{
 			report_corruption(ctx,
 							  psprintf("toasted attribute has unexpected TOAST tag %u",
-- 
2.49.0

v1-0012-amcheck-Add-test-cases-for-8-byte-TOAST-values.patchtext/x-diff; charset=us-asciiDownload
From 814ccdaad4484a7ecb2fd5f38b83bb165ecc13c8 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 19 Jun 2025 13:09:11 +0900
Subject: [PATCH v1 12/12] amcheck: Add test cases for 8-byte TOAST values

This patch is a proof of concept to show what is required to change in
the tests of pg_amcheck to be able to work with the new type of external
TOAST pointer.
---
 src/bin/pg_amcheck/t/004_verify_heapam.pl | 15 +++++++++------
 1 file changed, 9 insertions(+), 6 deletions(-)

diff --git a/src/bin/pg_amcheck/t/004_verify_heapam.pl b/src/bin/pg_amcheck/t/004_verify_heapam.pl
index 2a3af2666f52..50217372c128 100644
--- a/src/bin/pg_amcheck/t/004_verify_heapam.pl
+++ b/src/bin/pg_amcheck/t/004_verify_heapam.pl
@@ -83,8 +83,8 @@ use Test::More;
 # it is convenient enough to do it this way.  We define packing code
 # constants here, where they can be compared easily against the layout.
 
-use constant HEAPTUPLE_PACK_CODE => 'LLLSSSSSCCLLCCCCCCCCCCllLL';
-use constant HEAPTUPLE_PACK_LENGTH => 58;    # Total size
+use constant HEAPTUPLE_PACK_CODE => 'LLLSSSSSCCLLCCCCCCCCCCllLLL';
+use constant HEAPTUPLE_PACK_LENGTH => 62;    # Total size
 
 # Read a tuple of our table from a heap page.
 #
@@ -129,7 +129,8 @@ sub read_tuple
 		c_va_vartag => shift,
 		c_va_rawsize => shift,
 		c_va_extinfo => shift,
-		c_va_valueid => shift,
+		c_va_valueid_lo => shift,
+		c_va_valueid_hi => shift,
 		c_va_toastrelid => shift);
 	# Stitch together the text for column 'b'
 	$tup{b} = join('', map { chr($tup{"b_body$_"}) } (1 .. 7));
@@ -163,7 +164,8 @@ sub write_tuple
 		$tup->{b_body6}, $tup->{b_body7},
 		$tup->{c_va_header}, $tup->{c_va_vartag},
 		$tup->{c_va_rawsize}, $tup->{c_va_extinfo},
-		$tup->{c_va_valueid}, $tup->{c_va_toastrelid});
+		$tup->{c_va_valueid_lo}, $tup->{c_va_valueid_hi},
+		$tup->{c_va_toastrelid});
 	sysseek($fh, $offset, 0)
 	  or BAIL_OUT("sysseek failed: $!");
 	defined(syswrite($fh, $buffer, HEAPTUPLE_PACK_LENGTH))
@@ -184,6 +186,7 @@ my $node = PostgreSQL::Test::Cluster->new('test');
 $node->init(no_data_checksums => 1);
 $node->append_conf('postgresql.conf', 'autovacuum=off');
 $node->append_conf('postgresql.conf', 'max_prepared_transactions=10');
+$node->append_conf('postgresql.conf', 'default_toast_type=int8');
 
 # Start the node and load the extensions.  We depend on both
 # amcheck and pageinspect for this test.
@@ -496,7 +499,7 @@ for (my $tupidx = 0; $tupidx < $ROWCOUNT; $tupidx++)
 		$tup->{t_hoff} += 128;
 
 		push @expected,
-		  qr/${$header}data begins at offset 152 beyond the tuple length 58/,
+		  qr/${$header}data begins at offset 152 beyond the tuple length 62/,
 		  qr/${$header}tuple data should begin at byte 24, but actually begins at byte 152 \(3 attributes, no nulls\)/;
 	}
 	elsif ($offnum == 6)
@@ -581,7 +584,7 @@ for (my $tupidx = 0; $tupidx < $ROWCOUNT; $tupidx++)
 	elsif ($offnum == 13)
 	{
 		# Corrupt the bits in column 'c' toast pointer
-		$tup->{c_va_valueid} = 0xFFFFFFFF;
+		$tup->{c_va_valueid_lo} = 0xFFFFFFFF;
 
 		$header = header(0, $offnum, 2);
 		push @expected, qr/${header}toast value \d+ not found in toast table/;
-- 
2.49.0

#2Hannu Krosing
hannuk@google.com
In reply to: Michael Paquier (#1)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

Hi Michael

I'll take a look at the patch set.

While digging around in the TOAST code did you have any ideas on how
one could extract the TOAST APIs in a way that they can be added in
Table Access Method definition ?

Not all TAMs need TOAST, but the ones that do could also be the ones
that still like to do something different when materializing toasted
values.

And TOAST is actually a nice abstraction which could be used as basis
for both offloading more columns into separate forks and files as well
as implementing some kinds of vectored, columnar and compressed
storages.

----
Hannu

Show quoted text

On Thu, Jun 19, 2025 at 7:59 AM Michael Paquier <michael@paquier.xyz> wrote:

Hi all,

I have been looking at $subject and the many past reviews recently,
also related to some of the work related to the potential support for
zstandard compression in TOAST values, and found myself pondering
about the following message from Tom, to be reminded that nothing has
been done regarding the fact that the backend may finish in an
infinite loop once a TOAST table reaches 4 billion values:
/messages/by-id/764273.1669674269@sss.pgh.pa.us

Spoiler: I have heard of users that are in this case, and the best
thing we can do currently except raising shoulders is to use
workarounds with data externalization AFAIK, which is not nice,
usually, and users notice the problem once they see some backends
stuck in the infinite loop. I have spent some time looking at the
problem, and looked at all the proposals in this area like these ones
(I hope so at least):
https://commitfest.postgresql.org/patch/4296/
/messages/by-id/224711f9-83b7-a307-b17f-4457ab73aa0a@sigaev.ru

Anyway, it seems like nothing goes in a direction that I think would
be suited to fix the two following problems (some of the proposed
patches broke backward-compatibility, as well, and that's critical):
- The limit of TOAST values to 4 billions, because external TOAST
pointers want OIDs.
- New compression methods, see the recent proposal about zstandard at
[1]. ToastCompressionId is currently limited to 4 values because the
extinfo field of varatt_external has only this much data remaining.
Spoiler: I want to propose a new varatt_external dedicated to
zstandard-compressed external pointers, but that's not for this
thread.

Please find attached a patch set I have finished with while poking at
the problem, to address points 1) and 2) in the first email mentioned
at the top of this message. It is not yet ready for prime day yet
(there are a couple of things that would need adjustments), but I have
reached the point where I am going to need a consensus about what
people would be OK to have in terms of design to be able to support
multiple types of varatt_external to address these issues. And I'm OK
to consume time on that for the v19 cycle.

While hacking at (playing with?) the whole toasting and detoasting
code to understand the blast radius that this would involve, I have
quickly found that it is very annoying to have to touch at many places
of varatt.h to make variants of the existing varatt_external structure
(what we store on-disk as varlena Datum for external TOAST pointers).
Spoiler: it's my first time touching the internals of this code area
so deeply. Most of the code churns happen because we need to make the
[de]toast code aware of what to do depending on the vartags of the
external varlenas. It would be simple to hardcode a bunch of new
VARATT_IS_EXTERNAL_ONDISK() variants to plug in the new structures.
While it is efficient, this has a cost for out-of-core code and in
core because all the places that touch external TOAST pointers need to
be adjusted. Point is: it can be done. But if we introduce more
types of external TOAST pointers we need to always patch all these
areas, and there's a cost in that each time one or more new vartags
are added.

So, I have invented a new interface aimed at manipulating on-disk
external TOAST pointers, called toast_external, that is an on-memory
structure that services as an interface between on-disk external TOAST
pointers and what the backend wants to look at when retrieving chunks
of data from the TOAST relations. That's the main proposal of this
patch set, with a structure looking like that:
typedef struct toast_external_data
{
/* Original data size (includes header) */
int32 rawsize;
/* External saved size (without header) */
uint32 extsize;
/* compression method */
ToastCompressionId compression_method;
/* Relation OID of TOAST table containing the value */
Oid toastrelid;
/*
* Unique ID of value within TOAST table. This could be an OID or an
* int8 value. This field is large enough to be able to store any of
* them.
*/
uint64 value;
} toast_external_data;

This is a bit similar to what the code does for R/W and R/O vartags,
only applying to the on-disk external pointers. Then, the [de]toast
code and extension code is updated so as varlenas are changed into
this structure if we need to retrieve some of its data, and these
areas of the code do not need to know about the details of the
external TOAST pointers. When saving an external set of chunks, this
structure is filled with information depending on what
toast_save_datum() deals with, be it a short varlena, a non-compressed
external value, or a compressed external value, then builds a varlena
with the vartag we want.

External TOAST pointers have three properties that are hardcoded in
the tree, bringing some challenges of their own:
- The maximum size of a chunk, TOAST_MAX_CHUNK_SIZE, tweaked at close
to 2k to make 4 chunks fit on a page. This depends on the size of the
external pointer. This one was actually easy to refactor.
- The varlena header size, based on VARTAG_SIZE(), which is kind of
tricky to refactor out in the new toast_external.c, but that seems OK
even if this knowledge stays in varatt.h.
- The toast pointer size, aka TOAST_POINTER_SIZE. This one is
actually very interesting (tricky): we use it in one place,
toast_tuple_find_biggest_attribute(), as a lower bound to decide if an
attribute should be toastable or not. I've refactored the code to use
a "best" guess depending on the value type in the TOAST relation, but
that's not 100% waterproof. That needs more thoughts.

Anyway, the patch set is able to demonstrate how much needs to be done
in the tree to support multiple chunk_id types, and the CI is happy
with the attached. Some of the things done:
- Introduction of a user-settable GUC called default_toast_type, that
can be switched between two modes "oid" and "int8", to force the
creation of a TOAST relation using one type or the other.
- Dump, restore and upgrade support are integrated, relying on a GUC
makes the logic a breeze.
- 64b values are retrieved from a single counter in the control file,
named a "TOAST counter", which has the same reliability and properties
as an OID, with checkpoint support, WAL records, etc.
- Rewrites are soft, so I have kicked the can down the toast on this
point to not make the proposal more complicated than it should be: a
VACUUM FULL retains the same TOAST value type as the original. We
could extend rewrites so as the type of TOAST value is changed. It is
possible to setup a new cluster with default_toast_type = int8 set
after an upgrade, with the existing tables still using the OID mode.
This relates to the recent proposal with a concurrent VACUUM FULL
(REPACK discussion).

The patch set keeps the existing vartag_external with OID values for
backward-compatibility, and adds a second vartag_external that can
store 8-byte values. This model is the simplest one, and
optimizations are possible, where the Datum TOAST pointer could be
shorter depending on the ID type (OID or int8), the compression method
and the actual value to divide in chunks. For example, if you know
that a chunk of data to save has a value less than UINT32_MAX, we
could store 4 bytes worth of data instead of 8. This design has the
advantage to allow plugging in new TOAST external structures easily.
Now I've not spent extra time in this tuning, because there's no point
in spending more time without an actual agreement about three things,
and *that's what I'm looking for* as feedback for this upcoming CF:
- The structures of the external TOAST pointers. Variable-sized
pointers could be one possibility, across multiple vartags. Ideas are
welcome.
- How many vartag_external types we want.
- If people are actually OK with this translation layer or not, and I
don't disagree that there may be some paths hot enough where the
translation between the on-disk varlenas and this on-memory
toast_external_data hurts. Again, it is possible to hardcode more
types of vartags in the tree, or just bypass the translation in the
paths that are too hot. That's doable still brutal, but if that's the
final consensus reached I'm OK with that as well. (See for example
the changes in amcheck to see how simpler things get.)

The patch set has been divided into multiple pieces to ease its
review. Again, I'm not completely happy with everything in it, but
it's a start. Each patch has its own commit message, so feel free to
refer to them for more details:
- 0001 introduces the GUC default_toast_type. It is just defined, not
used in the tree at this stage.
- 0002 adds support for catcache lookups for int8 values, required to
allow TOAST values with int8 and its indexes. Potentially useful for
extensions.
- 0003 introduces the "TOAST counter", 8 bytes in the control file to
allocate values for the int8 chunk_id. That's cheap, reliable.
- 0004 is a mechanical change, that enlarges a couple of TOAST
interfaces to use values of uint64 instead of OID.
- 0005, again a mechanical change, reducing a bit the footprint of
TOAST_MAX_CHUNK_SIZE because OID and int8 values need different
values.
- 0006 tweaks pg_column_toast_chunk_id() to use int8 as return type.

Then comes the "main" patches:
- 0007 adds support for int8 chunk_id in TOAST tables. This is mostly
a mechanical change. If applying the patches up to this point,
external Datums are applied to both OID and int8 values. Note that
there is one tweak I'm unhappy with: the toast counter generation
would need to be smarter to avoid concurrent values because we don't
cross-check the TOAST index for existing values. (Sorry, got slightly
lazy here).
- 0008 adds tests for external compressed and uncompressed TOAST
values for int8 TOAST types.
- 0009 adds support for dump, restore, upgrades of the TOAST table
types.
- 0010 is the main dish: refactoring of the TOAST code to use
toast_external_data, with OID vartags as the only type defined.
- 0011 adds a second vartag_external: the one with int8 values stored
in the external TOAST pointer.
- 0012 is a bonus for amcheck: what needs to be done in its TAP tests
to allow the corruption cases to work when supporting a new vartag.

That was a long message. Thank you for reading if you have reached
this point.

Regards,

[1]: /messages/by-id/CAFAfj_HX84EK4hyRYw50AOHOcdVi-+FFwAAPo7JHx4aShCvunQ@mail.gmail.com
--
Michael

#3Nikita Malakhov
hukutoc@gmail.com
In reply to: Hannu Krosing (#2)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

Hi!

Hannu, we'd already made an attempt to extract the TOAST functionality as
API
and make it extensible and usable by other AMs in [1]Pluggable TOAST </messages/by-id/224711f9-83b7-a307-b17f-4457ab73aa0a@sigaev.ru&gt;, the patch set was
met calmly
but we still have some hopes on it.

Michael, glad you continue this work! Took patch set for review.

[1]: Pluggable TOAST </messages/by-id/224711f9-83b7-a307-b17f-4457ab73aa0a@sigaev.ru&gt;
</messages/by-id/224711f9-83b7-a307-b17f-4457ab73aa0a@sigaev.ru&gt;

--
Regards,
Nikita Malakhov
Postgres Professional
The Russian Postgres Company
https://postgrespro.ru/

On Fri, Jul 4, 2025 at 2:03 PM Hannu Krosing <hannuk@google.com> wrote:

Show quoted text

Hi Michael

I'll take a look at the patch set.

While digging around in the TOAST code did you have any ideas on how
one could extract the TOAST APIs in a way that they can be added in
Table Access Method definition ?

Not all TAMs need TOAST, but the ones that do could also be the ones
that still like to do something different when materializing toasted
values.

And TOAST is actually a nice abstraction which could be used as basis
for both offloading more columns into separate forks and files as well
as implementing some kinds of vectored, columnar and compressed
storages.

----
Hannu

#4Michael Paquier
michael@paquier.xyz
In reply to: Nikita Malakhov (#3)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

On Fri, Jul 04, 2025 at 02:38:34PM +0300, Nikita Malakhov wrote:

Hannu, we'd already made an attempt to extract the TOAST functionality as
API and make it extensible and usable by other AMs in [1], the patch
set was met calmly but we still have some hopes on it.

Yeah, it's one of these I have studied, and just found that
overcomplicated, preventing us from moving on with a simpler proposal,
because I care about two things first:
- More compression methods, with more meta-data, but let's just add
more vartag_external for that once/if they're really required.
- Enlarge optionally to 8-byte values.
So I really want to stress about these two points, nothing else for
now, echoing from the feedback from 2022 and the fact that all
proposals done after that lacked a simple approach.

IMO, we would live fine enough, *if* being able to plug in a pluggable
TOAST engine makes sense, if we just limit ourselves with an external
interface. We could allow backends to load their own vartag_external
with their own set of callbacks like the ones I am proposing here, so
as we can translate from/to a Datum in heap (or a different table AM)
to an external source, with the backend able to understand what this
external source should be. The key is to define a structure good
enough for the backend (toast_external_data in the patch). So to
answer your and Hannu's question: I had the case of different table
AMs in mind with an interface able to plug into it, yes. And yes, I
have designed the patch set with this in scope. Now there's also a
data type component to that, so that's assuming that a table AM would
want to rely on a varlena to store this data externally, somewhere
else that may not be exactly TOAST, still we want an OID and a value
to be able to retrieve this external value, and we want to store this
external OID and this value (+extra like a compression method and
sizes) in a Datum of the main relation file.

FYI, the patch set posted on this thread is not the latest one. I
have a v2, posted on this branch, where I have reordered things:
https://github.com/michaelpq/postgres/tree/toast_64bit_v2

The refactoring to the new toast_external_data with its callbacks is
done first, and the new vartag_external with 8-byte value support is
added on top of that. There were still two things I wanted to do, and
could not get down to it because I've spent my last week or so
working on other's stuff so I lacked room:
- Evaluate the cost of the transfer layer to toast_external_data. The
worst case I was planning to work with is a non-compressed data stored
in TOAST, then check profiles with the the detoasting path by grabbing
slices of the data with pgbench and a read-only query. The
write/insert path is not going to matter, the detoast is. The
reordering is actually for this reason: I want to see the effect of
the new interface first, and this needs to happen before we even
consider the option of adding 8-byte values.
- Add a callback for the value ID assignment. I was hesitating to add
that when I first backed on the patch but I think that's the correct
design moving forward, with an extra logic to be able to check if an
8-byte value is already in use in a relation, as we do for OID
assignment, but applied to the Toast generator added to the patch.
The backend should decide if a new value is required, we should not
decide the rewrite cases in the callback.

There is a second branch that I use for development, force-pushing to
it periodically, as well:
https://github.com/michaelpq/postgres/tree/toast_64bit
That's much dirtier, always WIP, just one of my playgrounds.
--
Michael

#5Nikita Malakhov
hukutoc@gmail.com
In reply to: Michael Paquier (#4)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

Hi!

I'm reviewing code at toast_64bit_v2.
Michael, isn't there a typo?
static const toast_external_info
toast_external_infos[TOAST_EXTERNAL_INFO_SIZE] = {
[VARTAG_ONDISK_INT8] = {
.toast_pointer_size = TOAST_POINTER_INT8_SIZE,
.maximum_chunk_size = TOAST_MAX_CHUNK_SIZE_INT8,
.to_external_data = ondisk_int8_to_external_data,
.create_external_data = ondisk_int8_create_external_data,
},
[VARTAG_ONDISK_OID] = {
.toast_pointer_size = TOAST_POINTER_INT8_SIZE, <--- here
.maximum_chunk_size = TOAST_MAX_CHUNK_SIZE_OID,
.to_external_data = ondisk_oid_to_external_data,
.create_external_data = ondisk_oid_create_external_data,
},
};

Shouldn't TOAST_POINTER_INT8_SIZE be replaced with TOAST_POINTER_OID_SIZE?

Regards,

--
Nikita Malakhov
Postgres Professional
The Russian Postgres Company
https://postgrespro.ru/

#6Michael Paquier
michael@paquier.xyz
In reply to: Nikita Malakhov (#5)
13 attachment(s)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

On Mon, Jul 07, 2025 at 05:33:11PM +0300, Nikita Malakhov wrote:

[VARTAG_ONDISK_OID] = {
.toast_pointer_size = TOAST_POINTER_INT8_SIZE, <--- here
.maximum_chunk_size = TOAST_MAX_CHUNK_SIZE_OID,
.to_external_data = ondisk_oid_to_external_data,
.create_external_data = ondisk_oid_create_external_data,
},
};

Shouldn't TOAST_POINTER_INT8_SIZE be replaced with TOAST_POINTER_OID_SIZE?

Yes, thanks for pointing this out. This one has lurked in one of the
rebases (not sure how) and it was impacting the threshold calculation
where we consider if an attribute should be compressed or not. I have
taken this occasion to work a bit more on the patch set. The patch
structure is mostly the same, with two tweaks because I was unhappy
with these in the initial patch set:
- The addition of a new callback able to retrieve a new TOAST value,
to ease the diffs in toast_save_datum().
- Reordering of the patch set, with the TOAST external refactoring
done much earlier in the series, now placed in 0003.

The most interesting piece of the patch is still 0003 "Refactor
external TOAST pointer code for better pluggability". On top of that
stands a 0004 patch named "Introduce new callback to get fresh TOAST
values" where I have added value conflict handling for int8. The
split makes reviews easier, hopefully.

Please note that I still need to look at perf profiles and some flame
graphs with the refactoring done in 0003 with the worst case I've
mentioned upthread with detoasting and values stored uncompressed in
the TOAST relation.

I have also pushed this v2 on this branch, so feel free to grab it if
that makes your life easier:
https://github.com/michaelpq/postgres/tree/toast_64bit_v2
--
Michael

Attachments:

v2-0001-Refactor-some-TOAST-value-ID-code-to-use-uint64-i.patchtext/x-diff; charset=us-asciiDownload
From aea486a6b3aa8dee840fe7be83421963f83a6f67 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 19 Jun 2025 09:57:19 +0900
Subject: [PATCH v2 01/13] Refactor some TOAST value ID code to use uint64
 instead of Oid

This change is a mechanical switch to change most of the code paths that
assume TOAST value IDs to be Oids to become uint64, easing an upcoming
change to allow 8-byte TOAST values.

The areas touched are related to table AM, amcheck and logical
decoding's reorder buffer.  A good chunk of the changes involve
switching printf() markers from %u to PRIu64.
---
 src/include/access/heaptoast.h                |  2 +-
 src/include/access/tableam.h                  |  4 +-
 src/backend/access/common/toast_internals.c   |  8 +--
 src/backend/access/heap/heaptoast.c           | 12 ++--
 .../replication/logical/reorderbuffer.c       | 14 ++--
 contrib/amcheck/verify_heapam.c               | 69 +++++++++++--------
 6 files changed, 62 insertions(+), 47 deletions(-)

diff --git a/src/include/access/heaptoast.h b/src/include/access/heaptoast.h
index 6385a27caf83..6e3558cbd6d2 100644
--- a/src/include/access/heaptoast.h
+++ b/src/include/access/heaptoast.h
@@ -142,7 +142,7 @@ extern HeapTuple toast_build_flattened_tuple(TupleDesc tupleDesc,
  *	Fetch a slice from a toast value stored in a heap table.
  * ----------
  */
-extern void heap_fetch_toast_slice(Relation toastrel, Oid valueid,
+extern void heap_fetch_toast_slice(Relation toastrel, uint64 valueid,
 								   int32 attrsize, int32 sliceoffset,
 								   int32 slicelength, struct varlena *result);
 
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index 1c9e802a6b12..b640047d2fdc 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -740,7 +740,7 @@ typedef struct TableAmRoutine
 	 * table implemented by this AM.  See table_relation_fetch_toast_slice()
 	 * for more details.
 	 */
-	void		(*relation_fetch_toast_slice) (Relation toastrel, Oid valueid,
+	void		(*relation_fetch_toast_slice) (Relation toastrel, uint64 valueid,
 											   int32 attrsize,
 											   int32 sliceoffset,
 											   int32 slicelength,
@@ -1873,7 +1873,7 @@ table_relation_toast_am(Relation rel)
  * stored.
  */
 static inline void
-table_relation_fetch_toast_slice(Relation toastrel, Oid valueid,
+table_relation_fetch_toast_slice(Relation toastrel, uint64 valueid,
 								 int32 attrsize, int32 sliceoffset,
 								 int32 slicelength, struct varlena *result)
 {
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 7d8be8346ce5..4a1342da6e1b 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -26,8 +26,8 @@
 #include "utils/rel.h"
 #include "utils/snapmgr.h"
 
-static bool toastrel_valueid_exists(Relation toastrel, Oid valueid);
-static bool toastid_valueid_exists(Oid toastrelid, Oid valueid);
+static bool toastrel_valueid_exists(Relation toastrel, uint64 valueid);
+static bool toastid_valueid_exists(Oid toastrelid, uint64 valueid);
 
 /* ----------
  * toast_compress_datum -
@@ -456,7 +456,7 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
  * ----------
  */
 static bool
-toastrel_valueid_exists(Relation toastrel, Oid valueid)
+toastrel_valueid_exists(Relation toastrel, uint64 valueid)
 {
 	bool		result = false;
 	ScanKeyData toastkey;
@@ -504,7 +504,7 @@ toastrel_valueid_exists(Relation toastrel, Oid valueid)
  * ----------
  */
 static bool
-toastid_valueid_exists(Oid toastrelid, Oid valueid)
+toastid_valueid_exists(Oid toastrelid, uint64 valueid)
 {
 	bool		result;
 	Relation	toastrel;
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index cb1e57030f64..76936b2f4944 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -623,7 +623,7 @@ toast_build_flattened_tuple(TupleDesc tupleDesc,
  * result is the varlena into which the results should be written.
  */
 void
-heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
+heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 					   int32 sliceoffset, int32 slicelength,
 					   struct varlena *result)
 {
@@ -725,7 +725,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
 		else
 		{
 			/* should never happen */
-			elog(ERROR, "found toasted toast chunk for toast value %u in %s",
+			elog(ERROR, "found toasted toast chunk for toast value %" PRIu64 " in %s",
 				 valueid, RelationGetRelationName(toastrel));
 			chunksize = 0;		/* keep compiler quiet */
 			chunkdata = NULL;
@@ -737,13 +737,13 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
 		if (curchunk != expectedchunk)
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
-					 errmsg_internal("unexpected chunk number %d (expected %d) for toast value %u in %s",
+					 errmsg_internal("unexpected chunk number %d (expected %d) for toast value %" PRIu64 " in %s",
 									 curchunk, expectedchunk, valueid,
 									 RelationGetRelationName(toastrel))));
 		if (curchunk > endchunk)
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
-					 errmsg_internal("unexpected chunk number %d (out of range %d..%d) for toast value %u in %s",
+					 errmsg_internal("unexpected chunk number %d (out of range %d..%d) for toast value %" PRIu64 " in %s",
 									 curchunk,
 									 startchunk, endchunk, valueid,
 									 RelationGetRelationName(toastrel))));
@@ -752,7 +752,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
 		if (chunksize != expected_size)
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
-					 errmsg_internal("unexpected chunk size %d (expected %d) in chunk %d of %d for toast value %u in %s",
+					 errmsg_internal("unexpected chunk size %d (expected %d) in chunk %d of %d for toast value %" PRIu64 " in %s",
 									 chunksize, expected_size,
 									 curchunk, totalchunks, valueid,
 									 RelationGetRelationName(toastrel))));
@@ -781,7 +781,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
 	if (expectedchunk != (endchunk + 1))
 		ereport(ERROR,
 				(errcode(ERRCODE_DATA_CORRUPTED),
-				 errmsg_internal("missing chunk number %d for toast value %u in %s",
+				 errmsg_internal("missing chunk number %d for toast value %" PRIu64 " in %s",
 								 expectedchunk, valueid,
 								 RelationGetRelationName(toastrel))));
 
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 7b4e8629553b..c871708b5932 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -176,7 +176,7 @@ typedef struct ReorderBufferIterTXNState
 /* toast datastructures */
 typedef struct ReorderBufferToastEnt
 {
-	Oid			chunk_id;		/* toast_table.chunk_id */
+	uint64		chunk_id;		/* toast_table.chunk_id */
 	int32		last_chunk_seq; /* toast_table.chunk_seq of the last chunk we
 								 * have seen */
 	Size		num_chunks;		/* number of chunks we've already seen */
@@ -4944,7 +4944,7 @@ ReorderBufferToastInitHash(ReorderBuffer *rb, ReorderBufferTXN *txn)
 
 	Assert(txn->toast_hash == NULL);
 
-	hash_ctl.keysize = sizeof(Oid);
+	hash_ctl.keysize = sizeof(uint64);
 	hash_ctl.entrysize = sizeof(ReorderBufferToastEnt);
 	hash_ctl.hcxt = rb->context;
 	txn->toast_hash = hash_create("ReorderBufferToastHash", 5, &hash_ctl,
@@ -4968,7 +4968,7 @@ ReorderBufferToastAppendChunk(ReorderBuffer *rb, ReorderBufferTXN *txn,
 	bool		isnull;
 	Pointer		chunk;
 	TupleDesc	desc = RelationGetDescr(relation);
-	Oid			chunk_id;
+	uint64		chunk_id;
 	int32		chunk_seq;
 
 	if (txn->toast_hash == NULL)
@@ -4995,11 +4995,11 @@ ReorderBufferToastAppendChunk(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		dlist_init(&ent->chunks);
 
 		if (chunk_seq != 0)
-			elog(ERROR, "got sequence entry %d for toast chunk %u instead of seq 0",
+			elog(ERROR, "got sequence entry %d for toast chunk %" PRIu64 " instead of seq 0",
 				 chunk_seq, chunk_id);
 	}
 	else if (found && chunk_seq != ent->last_chunk_seq + 1)
-		elog(ERROR, "got sequence entry %d for toast chunk %u instead of seq %d",
+		elog(ERROR, "got sequence entry %d for toast chunk %" PRIu64 " instead of seq %d",
 			 chunk_seq, chunk_id, ent->last_chunk_seq + 1);
 
 	chunk = DatumGetPointer(fastgetattr(newtup, 3, desc, &isnull));
@@ -5108,6 +5108,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		struct varlena *reconstructed;
 		dlist_iter	it;
 		Size		data_done = 0;
+		uint64		toast_valueid;
 
 		/* system columns aren't toasted */
 		if (attr->attnum < 0)
@@ -5132,13 +5133,14 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 			continue;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, varlena);
+		toast_valueid = toast_pointer.va_valueid;
 
 		/*
 		 * Check whether the toast tuple changed, replace if so.
 		 */
 		ent = (ReorderBufferToastEnt *)
 			hash_search(txn->toast_hash,
-						&toast_pointer.va_valueid,
+						&toast_valueid,
 						HASH_FIND,
 						NULL);
 		if (ent == NULL)
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 4963e9245cb5..3b2bdced4cdc 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1556,11 +1556,18 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 				  uint32 extsize)
 {
 	int32		chunk_seq;
-	int32		last_chunk_seq = (extsize - 1) / TOAST_MAX_CHUNK_SIZE;
+	int32		last_chunk_seq;
 	Pointer		chunk;
 	bool		isnull;
 	int32		chunksize;
 	int32		expected_size;
+	uint64		toast_valueid;
+	int32		max_chunk_size;
+
+	toast_valueid = ta->toast_pointer.va_valueid;
+
+	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	last_chunk_seq = (extsize - 1) / max_chunk_size;
 
 	/* Sanity-check the sequence number. */
 	chunk_seq = DatumGetInt32(fastgetattr(toasttup, 2,
@@ -1568,16 +1575,16 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	if (isnull)
 	{
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u has toast chunk with null sequence number",
-										 ta->toast_pointer.va_valueid));
+								psprintf("toast value %" PRIu64 " has toast chunk with null sequence number",
+										 toast_valueid));
 		return;
 	}
 	if (chunk_seq != *expected_chunk_seq)
 	{
 		/* Either the TOAST index is corrupt, or we don't have all chunks. */
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u index scan returned chunk %d when expecting chunk %d",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value %" PRIu64 " index scan returned chunk %d when expecting chunk %d",
+										 toast_valueid,
 										 chunk_seq, *expected_chunk_seq));
 	}
 	*expected_chunk_seq = chunk_seq + 1;
@@ -1588,8 +1595,8 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	if (isnull)
 	{
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u chunk %d has null data",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value %" PRIu64 " chunk %d has null data",
+										 toast_valueid,
 										 chunk_seq));
 		return;
 	}
@@ -1608,8 +1615,8 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 		uint32		header = ((varattrib_4b *) chunk)->va_4byte.va_header;
 
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u chunk %d has invalid varlena header %0x",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value %" PRIu64 " chunk %d has invalid varlena header %0x",
+										 toast_valueid,
 										 chunk_seq, header));
 		return;
 	}
@@ -1620,19 +1627,19 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	if (chunk_seq > last_chunk_seq)
 	{
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u chunk %d follows last expected chunk %d",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value %" PRIu64 " chunk %d follows last expected chunk %d",
+										 toast_valueid,
 										 chunk_seq, last_chunk_seq));
 		return;
 	}
 
-	expected_size = chunk_seq < last_chunk_seq ? TOAST_MAX_CHUNK_SIZE
-		: extsize - (last_chunk_seq * TOAST_MAX_CHUNK_SIZE);
+	expected_size = chunk_seq < last_chunk_seq ? max_chunk_size
+		: extsize - (last_chunk_seq * max_chunk_size);
 
 	if (chunksize != expected_size)
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u chunk %d has size %u, but expected size %u",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value %" PRIu64 " chunk %d has size %u, but expected size %u",
+										 toast_valueid,
 										 chunk_seq, chunksize, expected_size));
 }
 
@@ -1663,6 +1670,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	struct varlena *attr;
 	char	   *tp;				/* pointer to the tuple data */
 	uint16		infomask;
+	uint64		toast_pointer_valueid;
 	CompactAttribute *thisatt;
 	struct varatt_external toast_pointer;
 
@@ -1766,6 +1774,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 		return true;
 
 	/* It is external, and we're looking at a page on disk */
+	toast_pointer_valueid = toast_pointer.va_valueid;
 
 	/*
 	 * Must copy attr into toast_pointer for alignment considerations
@@ -1775,8 +1784,8 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	/* Toasted attributes too large to be untoasted should never be stored */
 	if (toast_pointer.va_rawsize > VARLENA_SIZE_LIMIT)
 		report_corruption(ctx,
-						  psprintf("toast value %u rawsize %d exceeds limit %d",
-								   toast_pointer.va_valueid,
+						  psprintf("toast value %" PRIu64 " rawsize %d exceeds limit %d",
+								   toast_pointer_valueid,
 								   toast_pointer.va_rawsize,
 								   VARLENA_SIZE_LIMIT));
 
@@ -1803,16 +1812,16 @@ check_tuple_attribute(HeapCheckContext *ctx)
 		}
 		if (!valid)
 			report_corruption(ctx,
-							  psprintf("toast value %u has invalid compression method id %d",
-									   toast_pointer.va_valueid, cmid));
+							  psprintf("toast value %" PRIu64 " has invalid compression method id %d",
+									   toast_pointer_valueid, cmid));
 	}
 
 	/* The tuple header better claim to contain toasted values */
 	if (!(infomask & HEAP_HASEXTERNAL))
 	{
 		report_corruption(ctx,
-						  psprintf("toast value %u is external but tuple header flag HEAP_HASEXTERNAL not set",
-								   toast_pointer.va_valueid));
+						  psprintf("toast value %" PRIu64 " is external but tuple header flag HEAP_HASEXTERNAL not set",
+								   toast_pointer_valueid));
 		return true;
 	}
 
@@ -1820,8 +1829,8 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	if (!ctx->rel->rd_rel->reltoastrelid)
 	{
 		report_corruption(ctx,
-						  psprintf("toast value %u is external but relation has no toast relation",
-								   toast_pointer.va_valueid));
+						  psprintf("toast value %" PRIu64 " is external but relation has no toast relation",
+								   toast_pointer_valueid));
 		return true;
 	}
 
@@ -1866,9 +1875,11 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	uint32		extsize;
 	int32		expected_chunk_seq = 0;
 	int32		last_chunk_seq;
+	uint64		toast_valueid;
+	int32		max_chunk_size = TOAST_MAX_CHUNK_SIZE;
 
 	extsize = VARATT_EXTERNAL_GET_EXTSIZE(ta->toast_pointer);
-	last_chunk_seq = (extsize - 1) / TOAST_MAX_CHUNK_SIZE;
+	last_chunk_seq = (extsize - 1) / max_chunk_size;
 
 	/*
 	 * Setup a scan key to find chunks in toast table with matching va_valueid
@@ -1896,14 +1907,16 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	}
 	systable_endscan_ordered(toastscan);
 
+	toast_valueid = ta->toast_pointer.va_valueid;
+
 	if (!found_toasttup)
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u not found in toast table",
-										 ta->toast_pointer.va_valueid));
+								psprintf("toast value %" PRIu64 " not found in toast table",
+										 toast_valueid));
 	else if (expected_chunk_seq <= last_chunk_seq)
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u was expected to end at chunk %d, but ended while expecting chunk %d",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value %" PRIu64 " was expected to end at chunk %d, but ended while expecting chunk %d",
+										 toast_valueid,
 										 last_chunk_seq, expected_chunk_seq));
 }
 
-- 
2.50.0

v2-0002-Minimize-footprint-of-TOAST_MAX_CHUNK_SIZE-in-hea.patchtext/x-diff; charset=us-asciiDownload
From eeddfe4b7e953dcd5a053587bf189083e01dda00 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 19 Jun 2025 10:03:46 +0900
Subject: [PATCH v2 02/13] Minimize footprint of TOAST_MAX_CHUNK_SIZE in heap
 TOAST code

This eases a follow-up change to support 8-byte TOAST value IDs, as the
maximum chunk size allowed for a single chunk of TOASTed data depends on
the size of the value ID.
---
 src/backend/access/heap/heaptoast.c | 20 ++++++++++++--------
 1 file changed, 12 insertions(+), 8 deletions(-)

diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index 76936b2f4944..ae8d502ddcd3 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -634,11 +634,12 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 	SysScanDesc toastscan;
 	HeapTuple	ttup;
 	int32		expectedchunk;
-	int32		totalchunks = ((attrsize - 1) / TOAST_MAX_CHUNK_SIZE) + 1;
+	int32		totalchunks;
 	int			startchunk;
 	int			endchunk;
 	int			num_indexes;
 	int			validIndex;
+	int32		max_chunk_size;
 
 	/* Look for the valid index of toast relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -646,8 +647,11 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 									&toastidxs,
 									&num_indexes);
 
-	startchunk = sliceoffset / TOAST_MAX_CHUNK_SIZE;
-	endchunk = (sliceoffset + slicelength - 1) / TOAST_MAX_CHUNK_SIZE;
+	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+
+	totalchunks = ((attrsize - 1) / max_chunk_size) + 1;
+	startchunk = sliceoffset / max_chunk_size;
+	endchunk = (sliceoffset + slicelength - 1) / max_chunk_size;
 	Assert(endchunk <= totalchunks);
 
 	/* Set up a scan key to fetch from the index. */
@@ -747,8 +751,8 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 									 curchunk,
 									 startchunk, endchunk, valueid,
 									 RelationGetRelationName(toastrel))));
-		expected_size = curchunk < totalchunks - 1 ? TOAST_MAX_CHUNK_SIZE
-			: attrsize - ((totalchunks - 1) * TOAST_MAX_CHUNK_SIZE);
+		expected_size = curchunk < totalchunks - 1 ? max_chunk_size
+			: attrsize - ((totalchunks - 1) * max_chunk_size);
 		if (chunksize != expected_size)
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
@@ -763,12 +767,12 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 		chcpystrt = 0;
 		chcpyend = chunksize - 1;
 		if (curchunk == startchunk)
-			chcpystrt = sliceoffset % TOAST_MAX_CHUNK_SIZE;
+			chcpystrt = sliceoffset % max_chunk_size;
 		if (curchunk == endchunk)
-			chcpyend = (sliceoffset + slicelength - 1) % TOAST_MAX_CHUNK_SIZE;
+			chcpyend = (sliceoffset + slicelength - 1) % max_chunk_size;
 
 		memcpy(VARDATA(result) +
-			   (curchunk * TOAST_MAX_CHUNK_SIZE - sliceoffset) + chcpystrt,
+			   (curchunk * max_chunk_size - sliceoffset) + chcpystrt,
 			   chunkdata + chcpystrt,
 			   (chcpyend - chcpystrt) + 1);
 
-- 
2.50.0

v2-0003-Refactor-external-TOAST-pointer-code-for-better-p.patchtext/x-diff; charset=us-asciiDownload
From 8e6f816cd51acffa681a4ac561af510a4e019cef Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Mon, 23 Jun 2025 12:43:14 +0900
Subject: [PATCH v2 03/13] Refactor external TOAST pointer code for better
 pluggability

This commit introduces a new interface for external TOAST pointers,
which is able to make a translation of the varlena pointers stored on
disk to/from an new in-memory structure called toast_external.  The
types of varatt_external supported on disk need to be registered into a
new subsystem in a new file, called toast_external.[c|h], then define a
set of callbacks to allow the toasting and detoasting code to use it.

The existing varatt_external is renamed to varatt_external_oid, to map
with the fact that this structure is used to store external TOAST
pointers based on OID values.

A follow-up change will rely on this refactoring to introduce a new type
of vartag_external with an associated varatt_external that is able to
store 8-byte value IDs on disk.
---
 src/include/access/detoast.h                  |  12 +-
 src/include/access/heaptoast.h                |   5 +-
 src/include/access/toast_external.h           | 163 ++++++++++++++++++
 src/include/access/toast_helper.h             |   1 +
 src/include/varatt.h                          |  34 ++--
 src/backend/access/common/Makefile            |   1 +
 src/backend/access/common/detoast.c           |  59 +++----
 src/backend/access/common/meson.build         |   1 +
 src/backend/access/common/toast_compression.c |  10 +-
 src/backend/access/common/toast_external.c    | 131 ++++++++++++++
 src/backend/access/common/toast_internals.c   |  85 +++++----
 src/backend/access/heap/heaptoast.c           |  30 +++-
 src/backend/access/table/toast_helper.c       |  12 +-
 .../replication/logical/reorderbuffer.c       |  13 +-
 src/backend/utils/adt/varlena.c               |   7 +-
 src/backend/utils/cache/relcache.c            |   1 +
 doc/src/sgml/storage.sgml                     |   2 +-
 contrib/amcheck/verify_heapam.c               |  37 ++--
 src/tools/pgindent/typedefs.list              |   2 +
 19 files changed, 489 insertions(+), 117 deletions(-)
 create mode 100644 src/include/access/toast_external.h
 create mode 100644 src/backend/access/common/toast_external.c

diff --git a/src/include/access/detoast.h b/src/include/access/detoast.h
index e603a2276c38..4195f7b5bdfd 100644
--- a/src/include/access/detoast.h
+++ b/src/include/access/detoast.h
@@ -14,10 +14,11 @@
 
 /*
  * Macro to fetch the possibly-unaligned contents of an EXTERNAL datum
- * into a local "struct varatt_external" toast pointer.  This should be
- * just a memcpy, but some versions of gcc seem to produce broken code
- * that assumes the datum contents are aligned.  Introducing an explicit
- * intermediate "varattrib_1b_e *" variable seems to fix it.
+ * into a local "struct varatt_external_*" toast pointer, as supported
+ * in toast_external.c and varatt.h.  This should be just a memcpy, but
+ * some versions of gcc seem to produce broken code that assumes the datum
+ * contents are aligned.  Introducing an explicit intermediate
+ * "varattrib_1b_e *" variable seems to fix it.
  */
 #define VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr) \
 do { \
@@ -27,9 +28,6 @@ do { \
 	memcpy(&(toast_pointer), VARDATA_EXTERNAL(attre), sizeof(toast_pointer)); \
 } while (0)
 
-/* Size of an EXTERNAL datum that contains a standard TOAST pointer */
-#define TOAST_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external))
-
 /* Size of an EXTERNAL datum that contains an indirection pointer */
 #define INDIRECT_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_indirect))
 
diff --git a/src/include/access/heaptoast.h b/src/include/access/heaptoast.h
index 6e3558cbd6d2..673e96f5488c 100644
--- a/src/include/access/heaptoast.h
+++ b/src/include/access/heaptoast.h
@@ -81,13 +81,16 @@
 
 #define EXTERN_TUPLE_MAX_SIZE	MaximumBytesPerTuple(EXTERN_TUPLES_PER_PAGE)
 
-#define TOAST_MAX_CHUNK_SIZE	\
+#define TOAST_MAX_CHUNK_SIZE_OID	\
 	(EXTERN_TUPLE_MAX_SIZE -							\
 	 MAXALIGN(SizeofHeapTupleHeader) -					\
 	 sizeof(Oid) -										\
 	 sizeof(int32) -									\
 	 VARHDRSZ)
 
+/* Maximum size of chunk possible */
+#define TOAST_MAX_CHUNK_SIZE	TOAST_MAX_CHUNK_SIZE_OID
+
 /* ----------
  * heap_toast_insert_or_update -
  *
diff --git a/src/include/access/toast_external.h b/src/include/access/toast_external.h
new file mode 100644
index 000000000000..1e3ba8062286
--- /dev/null
+++ b/src/include/access/toast_external.h
@@ -0,0 +1,163 @@
+/*-------------------------------------------------------------------------
+ *
+ * toast_external.h
+ *	  Support for on-disk external TOAST pointers
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1995, Regents of the University of California
+ *
+ * src/include/access/toast_external.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef TOAST_EXTERNAL_H
+#define TOAST_EXTERNAL_H
+
+#include "access/toast_compression.h"
+#include "varatt.h"
+
+/*
+ * Intermediate in-memory structure used when creating on-disk
+ * varatt_external_* or when deserializing varlena contents.
+ */
+typedef struct toast_external_data
+{
+	/* Original data size (includes header) */
+	int32       rawsize;
+	/* External saved size (without header) */
+	uint32      extsize;
+	/* compression method */
+	ToastCompressionId compression_method;
+	/* Relation OID of TOAST table containing the value */
+	Oid			toastrelid;
+	/*
+	 * Unique ID of value within TOAST table.  This could be an OID or an
+	 * int8 value.  This field is large enough to be able to store any of
+	 * them.
+	 */
+	uint64		value;
+} toast_external_data;
+
+/*
+ * Metadata for external TOAST pointer kinds, separated based on their
+ * vartag_external.
+ */
+typedef struct toast_external_info
+{
+	/*
+	 * Maximum chunk of data authorized for this type of external TOAST
+	 * pointer, when dividing an entry by chunks.  Sized depending on
+	 * the size of its varatt_external_* structure.
+	 */
+	int32		maximum_chunk_size;
+
+	/*
+	 * Size of an external TOAST pointer of this type, typically
+	 * (VARHDRSZ_EXTERNAL + sizeof(varatt_external_struct)).
+	 */
+	int32		toast_pointer_size;
+
+	/*
+	 * Map an input varlena to a toast_external_data, for consumption
+	 * in the backend code.  "data" is an input/output result.
+	 */
+	void		(*to_external_data) (struct varlena *attr,
+									 toast_external_data *data);
+
+	/*
+	 * Create a varlena that will be used on-disk for the given TOAST
+	 * type, based on the given input data.
+	 *
+	 * The result is the varlena created, for on-disk insertion.
+	 */
+	struct varlena  *(*create_external_data) (toast_external_data data);
+
+} toast_external_info;
+
+/* Retrieve a toast_external_info from a vartag */
+extern const toast_external_info *toast_external_get_info(uint8 tag);
+
+/* Retrieve toast_pointer_size using a TOAST attribute type */
+extern int32 toast_external_info_get_pointer_size(uint8 tag);
+
+/*
+ * Testing whether an externally-stored value is compressed now requires
+ * comparing size stored in extsize (the actual length of the external data)
+ * to rawsize (the original uncompressed datum's size).  The latter includes
+ * VARHDRSZ overhead, the former doesn't.  We never use compression unless it
+ * actually saves space, so we expect either equality or less-than.
+ */
+#define TOAST_EXTERNAL_IS_COMPRESSED(data) \
+	((data).extsize < (data).rawsize - VARHDRSZ)
+
+/* Full data structure */
+static inline void
+toast_external_info_get_data(struct varlena *attr, toast_external_data *data)
+{
+	uint8 tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+
+	info->to_external_data(attr, data);
+}
+
+/*
+ * Helper routines to recover specific fields in toast_external_data.  Most
+ * code paths doing work with on-disk external TOAST pointers care about
+ * these.
+ */
+
+/* Detoasted "raw" size */
+static inline Size
+toast_external_info_get_rawsize(struct varlena *attr)
+{
+	uint8 tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+	toast_external_data data;
+
+	info->to_external_data(attr, &data);
+
+	return data.rawsize;
+}
+
+/* External saved size */
+static inline Size
+toast_external_info_get_extsize(struct varlena *attr)
+{
+	uint8 tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+	toast_external_data data;
+
+	info->to_external_data(attr, &data);
+
+	return data.extsize;
+}
+
+/* Compression method ID */
+static inline ToastCompressionId
+toast_external_info_get_compression_method(struct varlena *attr)
+{
+	uint8 tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+	toast_external_data data;
+
+	info->to_external_data(attr, &data);
+
+	return data.compression_method;
+}
+
+/* Value ID */
+static inline Size
+toast_external_info_get_value(struct varlena *attr)
+{
+	uint8 tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+	toast_external_data data;
+
+	info->to_external_data(attr, &data);
+
+	return data.value;
+}
+
+#endif			/* TOAST_EXTERNAL_H */
diff --git a/src/include/access/toast_helper.h b/src/include/access/toast_helper.h
index e6ab8afffb67..729c593afebd 100644
--- a/src/include/access/toast_helper.h
+++ b/src/include/access/toast_helper.h
@@ -47,6 +47,7 @@ typedef struct
 	 * should be NULL in the case of an insert.
 	 */
 	Relation	ttc_rel;		/* the relation that contains the tuple */
+	int32		ttc_toast_pointer_size;	/* size of external TOAST pointer */
 	Datum	   *ttc_values;		/* values from the tuple columns */
 	bool	   *ttc_isnull;		/* null flags for the tuple columns */
 	Datum	   *ttc_oldvalues;	/* values from previous tuple */
diff --git a/src/include/varatt.h b/src/include/varatt.h
index 2e8564d49980..793030dae932 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -16,11 +16,14 @@
 #define VARATT_H
 
 /*
- * struct varatt_external is a traditional "TOAST pointer", that is, the
+ * struct varatt_external_oid is a traditional "TOAST pointer", that is, the
  * information needed to fetch a Datum stored out-of-line in a TOAST table.
  * The data is compressed if and only if the external size stored in
  * va_extinfo is less than va_rawsize - VARHDRSZ.
  *
+ * The value ID used is an OID, used for TOAST relations with OID as
+ * attribute for chunk_id.
+ *
  * This struct must not contain any padding, because we sometimes compare
  * these pointers using memcmp.
  *
@@ -29,14 +32,15 @@
  * you can look at these fields!  (The reason we use memcmp is to avoid
  * having to do that just to detect equality of two TOAST pointers...)
  */
-typedef struct varatt_external
+typedef struct varatt_external_oid
 {
 	int32		va_rawsize;		/* Original data size (includes header) */
 	uint32		va_extinfo;		/* External saved size (without header) and
 								 * compression method */
 	Oid			va_valueid;		/* Unique ID of value within TOAST table */
 	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
-}			varatt_external;
+}			varatt_external_oid;
+
 
 /*
  * These macros define the "saved size" portion of va_extinfo.  Its remaining
@@ -78,15 +82,16 @@ typedef struct varatt_expanded
 
 /*
  * Type tag for the various sorts of "TOAST pointer" datums.  The peculiar
- * value for VARTAG_ONDISK comes from a requirement for on-disk compatibility
- * with a previous notion that the tag field was the pointer datum's length.
+ * value for VARTAG_ONDISK_OID comes from a requirement for on-disk
+ * compatibility with a previous notion that the tag field was the pointer
+ * datum's length.
  */
 typedef enum vartag_external
 {
 	VARTAG_INDIRECT = 1,
 	VARTAG_EXPANDED_RO = 2,
 	VARTAG_EXPANDED_RW = 3,
-	VARTAG_ONDISK = 18
+	VARTAG_ONDISK_OID = 18
 } vartag_external;
 
 /* this test relies on the specific tag values above */
@@ -96,7 +101,7 @@ typedef enum vartag_external
 #define VARTAG_SIZE(tag) \
 	((tag) == VARTAG_INDIRECT ? sizeof(varatt_indirect) : \
 	 VARTAG_IS_EXPANDED(tag) ? sizeof(varatt_expanded) : \
-	 (tag) == VARTAG_ONDISK ? sizeof(varatt_external) : \
+	 (tag) == VARTAG_ONDISK_OID ? sizeof(varatt_external_oid) : \
 	 (AssertMacro(false), 0))
 
 /*
@@ -287,8 +292,10 @@ typedef struct
 
 #define VARATT_IS_COMPRESSED(PTR)			VARATT_IS_4B_C(PTR)
 #define VARATT_IS_EXTERNAL(PTR)				VARATT_IS_1B_E(PTR)
+#define VARATT_IS_EXTERNAL_ONDISK_OID(PTR) \
+	(VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID)
 #define VARATT_IS_EXTERNAL_ONDISK(PTR) \
-	(VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK)
+	(VARATT_IS_EXTERNAL_ONDISK_OID(PTR))
 #define VARATT_IS_EXTERNAL_INDIRECT(PTR) \
 	(VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_INDIRECT)
 #define VARATT_IS_EXTERNAL_EXPANDED_RO(PTR) \
@@ -330,7 +337,10 @@ typedef struct
 #define VARDATA_COMPRESSED_GET_COMPRESS_METHOD(PTR) \
 	(((varattrib_4b *) (PTR))->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS)
 
-/* Same for external Datums; but note argument is a struct varatt_external */
+/*
+ * Same for external Datums; but note argument is a struct
+ * varatt_external_oid.
+ */
 #define VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) \
 	((toast_pointer).va_extinfo & VARLENA_EXTSIZE_MASK)
 #define VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) \
@@ -351,8 +361,10 @@ typedef struct
  * VARHDRSZ overhead, the former doesn't.  We never use compression unless it
  * actually saves space, so we expect either equality or less-than.
  */
+
 #define VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer) \
-	(VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) < \
-	 (toast_pointer).va_rawsize - VARHDRSZ)
+ (VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) < \
+  (toast_pointer).va_rawsize - VARHDRSZ)
+
 
 #endif
diff --git a/src/backend/access/common/Makefile b/src/backend/access/common/Makefile
index e78de312659e..1ef86a245886 100644
--- a/src/backend/access/common/Makefile
+++ b/src/backend/access/common/Makefile
@@ -27,6 +27,7 @@ OBJS = \
 	syncscan.o \
 	tidstore.o \
 	toast_compression.o \
+	toast_external.o \
 	toast_internals.o \
 	tupconvert.o \
 	tupdesc.o
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 626517877422..684e1b0b7d3b 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -16,6 +16,7 @@
 #include "access/detoast.h"
 #include "access/table.h"
 #include "access/tableam.h"
+#include "access/toast_external.h"
 #include "access/toast_internals.h"
 #include "common/int.h"
 #include "common/pg_lzcompress.h"
@@ -225,12 +226,12 @@ detoast_attr_slice(struct varlena *attr,
 
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
-		struct varatt_external toast_pointer;
+		struct toast_external_data toast_pointer;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		toast_external_info_get_data(attr, &toast_pointer);
 
 		/* fast path for non-compressed external datums */
-		if (!VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+		if (!TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 			return toast_fetch_datum_slice(attr, sliceoffset, slicelength);
 
 		/*
@@ -240,7 +241,7 @@ detoast_attr_slice(struct varlena *attr,
 		 */
 		if (slicelimit >= 0)
 		{
-			int32		max_size = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+			int32		max_size = toast_pointer.extsize;
 
 			/*
 			 * Determine maximum amount of compressed data needed for a prefix
@@ -251,8 +252,7 @@ detoast_attr_slice(struct varlena *attr,
 			 * determine how much compressed data we need to be sure of being
 			 * able to decompress the required slice.
 			 */
-			if (VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) ==
-				TOAST_PGLZ_COMPRESSION_ID)
+			if (toast_pointer.compression_method == TOAST_PGLZ_COMPRESSION_ID)
 				max_size = pglz_maximum_compressed_size(slicelimit, max_size);
 
 			/*
@@ -344,20 +344,21 @@ toast_fetch_datum(struct varlena *attr)
 {
 	Relation	toastrel;
 	struct varlena *result;
-	struct varatt_external toast_pointer;
+	struct toast_external_data toast_pointer;
 	int32		attrsize;
+	uint64		valueid;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		elog(ERROR, "toast_fetch_datum shouldn't be called for non-ondisk datums");
 
 	/* Must copy to access aligned fields */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_external_info_get_data(attr, &toast_pointer);
 
-	attrsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+	attrsize = toast_pointer.extsize;
 
 	result = (struct varlena *) palloc(attrsize + VARHDRSZ);
 
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+	if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 		SET_VARSIZE_COMPRESSED(result, attrsize + VARHDRSZ);
 	else
 		SET_VARSIZE(result, attrsize + VARHDRSZ);
@@ -365,14 +366,15 @@ toast_fetch_datum(struct varlena *attr)
 	if (attrsize == 0)
 		return result;			/* Probably shouldn't happen, but just in
 								 * case. */
+	valueid = toast_pointer.value;
 
 	/*
 	 * Open the toast relation and its indexes
 	 */
-	toastrel = table_open(toast_pointer.va_toastrelid, AccessShareLock);
+	toastrel = table_open(toast_pointer.toastrelid, AccessShareLock);
 
 	/* Fetch all chunks */
-	table_relation_fetch_toast_slice(toastrel, toast_pointer.va_valueid,
+	table_relation_fetch_toast_slice(toastrel, valueid,
 									 attrsize, 0, attrsize, result);
 
 	/* Close toast table */
@@ -398,23 +400,26 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 {
 	Relation	toastrel;
 	struct varlena *result;
-	struct varatt_external toast_pointer;
+	struct toast_external_data toast_pointer;
 	int32		attrsize;
+	uint64		valueid;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		elog(ERROR, "toast_fetch_datum_slice shouldn't be called for non-ondisk datums");
 
 	/* Must copy to access aligned fields */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_external_info_get_data(attr, &toast_pointer);
+
+	valueid = toast_pointer.value;
 
 	/*
 	 * It's nonsense to fetch slices of a compressed datum unless when it's a
 	 * prefix -- this isn't lo_* we can't return a compressed datum which is
 	 * meaningful to toast later.
 	 */
-	Assert(!VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer) || 0 == sliceoffset);
+	Assert(!TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer) || 0 == sliceoffset);
 
-	attrsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+	attrsize = toast_pointer.extsize;
 
 	if (sliceoffset >= attrsize)
 	{
@@ -427,7 +432,7 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 	 * space required by va_tcinfo, which is stored at the beginning as an
 	 * int32 value.
 	 */
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer) && slicelength > 0)
+	if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer) && slicelength > 0)
 		slicelength = slicelength + sizeof(int32);
 
 	/*
@@ -440,7 +445,7 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 
 	result = (struct varlena *) palloc(slicelength + VARHDRSZ);
 
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+	if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 		SET_VARSIZE_COMPRESSED(result, slicelength + VARHDRSZ);
 	else
 		SET_VARSIZE(result, slicelength + VARHDRSZ);
@@ -449,10 +454,11 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 		return result;			/* Can save a lot of work at this point! */
 
 	/* Open the toast relation */
-	toastrel = table_open(toast_pointer.va_toastrelid, AccessShareLock);
+	toastrel = table_open(toast_pointer.toastrelid, AccessShareLock);
 
 	/* Fetch all chunks */
-	table_relation_fetch_toast_slice(toastrel, toast_pointer.va_valueid,
+	table_relation_fetch_toast_slice(toastrel,
+									 valueid,
 									 attrsize, sliceoffset, slicelength,
 									 result);
 
@@ -549,11 +555,7 @@ toast_raw_datum_size(Datum value)
 
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
-		/* va_rawsize is the size of the original datum -- including header */
-		struct varatt_external toast_pointer;
-
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-		result = toast_pointer.va_rawsize;
+		result = toast_external_info_get_rawsize(attr);
 	}
 	else if (VARATT_IS_EXTERNAL_INDIRECT(attr))
 	{
@@ -609,11 +611,10 @@ toast_datum_size(Datum value)
 		 * Attribute is stored externally - return the extsize whether
 		 * compressed or not.  We do not count the size of the toast pointer
 		 * ... should we?
+		 *
+		 * XXX: this comment should be documented elsewhere.
 		 */
-		struct varatt_external toast_pointer;
-
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-		result = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+		result = toast_external_info_get_extsize(attr);
 	}
 	else if (VARATT_IS_EXTERNAL_INDIRECT(attr))
 	{
diff --git a/src/backend/access/common/meson.build b/src/backend/access/common/meson.build
index e3cdbe7a22e1..c20f2e88921e 100644
--- a/src/backend/access/common/meson.build
+++ b/src/backend/access/common/meson.build
@@ -15,6 +15,7 @@ backend_sources += files(
   'syncscan.c',
   'tidstore.c',
   'toast_compression.c',
+  'toast_external.c',
   'toast_internals.c',
   'tupconvert.c',
   'tupdesc.c',
diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c
index 21f2f4af97e3..e1c76dea4905 100644
--- a/src/backend/access/common/toast_compression.c
+++ b/src/backend/access/common/toast_compression.c
@@ -19,6 +19,7 @@
 
 #include "access/detoast.h"
 #include "access/toast_compression.h"
+#include "access/toast_external.h"
 #include "common/pg_lzcompress.h"
 #include "varatt.h"
 
@@ -261,14 +262,7 @@ toast_get_compression_id(struct varlena *attr)
 	 * toast compression header.
 	 */
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
-	{
-		struct varatt_external toast_pointer;
-
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-
-		if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
-			cmid = VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer);
-	}
+		cmid = toast_external_info_get_compression_method(attr);
 	else if (VARATT_IS_COMPRESSED(attr))
 		cmid = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(attr);
 
diff --git a/src/backend/access/common/toast_external.c b/src/backend/access/common/toast_external.c
new file mode 100644
index 000000000000..a851a8e4184b
--- /dev/null
+++ b/src/backend/access/common/toast_external.c
@@ -0,0 +1,131 @@
+/*-------------------------------------------------------------------------
+ *
+ * toast_external.c
+ *	  Functions for the support of external on-disk TOAST pointers.
+ *
+ * Copyright (c) 2025, PostgreSQL Global Development Group
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/common/toast_external.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/detoast.h"
+#include "access/heaptoast.h"
+#include "access/toast_external.h"
+
+/* Callbacks for VARTAG_ONDISK_OID */
+static void ondisk_oid_to_external_data(struct varlena *attr,
+										toast_external_data *data);
+static struct varlena *ondisk_oid_create_external_data(toast_external_data data);
+
+
+/*
+ * Size of an EXTERNAL datum that contains a standard TOAST pointer (OID
+ * value).
+ */
+#define TOAST_POINTER_OID_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_oid))
+
+/*
+ * For now there are only two types, all defined in this file.  For now this
+ * is the maximum value of vartag_external, which is a historical choice.
+ */
+#define TOAST_EXTERNAL_INFO_SIZE	(VARTAG_ONDISK_OID + 1)
+
+/*
+ * The different kinds of on-disk external TOAST pointers. divided by
+ * vartag_external.
+ *
+ * See comments for struct toast_external_info about the details of the
+ * individual fields.
+ */
+static const toast_external_info toast_external_infos[TOAST_EXTERNAL_INFO_SIZE] = {
+	[VARTAG_ONDISK_OID] = {
+		.toast_pointer_size = TOAST_POINTER_OID_SIZE,
+		.maximum_chunk_size = TOAST_MAX_CHUNK_SIZE_OID,
+		.to_external_data = ondisk_oid_to_external_data,
+		.create_external_data = ondisk_oid_create_external_data,
+	},
+};
+
+
+/* Get toast_external_info of the defined vartag_external */
+const toast_external_info *
+toast_external_get_info(uint8 tag)
+{
+	return &toast_external_infos[tag];
+}
+
+/*
+ * Get external TOAST pointer size based on the attribute type of a TOAST
+ * value.
+ */
+int32
+toast_external_info_get_pointer_size(uint8 tag)
+{
+	return toast_external_infos[tag].toast_pointer_size;
+}
+
+/*
+ * Helper routines able to translate the various varatt_external_* from/to
+ * the in-memory representation toast_external_data used in the backend.
+ */
+
+/* Callbacks for VARTAG_ONDISK_OID */
+static void
+ondisk_oid_to_external_data(struct varlena *attr, toast_external_data *data)
+{
+	varatt_external_oid		external;
+
+	VARATT_EXTERNAL_GET_POINTER(external, attr);
+	data->rawsize = external.va_rawsize;
+
+	/*
+	 * External size and compression methods are stored in the same field,
+	 * extract.
+	 */
+	if (VARATT_EXTERNAL_IS_COMPRESSED(external))
+	{
+		data->extsize = VARATT_EXTERNAL_GET_EXTSIZE(external);
+		data->compression_method = VARATT_EXTERNAL_GET_COMPRESS_METHOD(external);
+	}
+	else
+	{
+		data->extsize = external.va_extinfo;
+		data->compression_method = TOAST_INVALID_COMPRESSION_ID;
+	}
+
+	data->value = (uint64) external.va_valueid;
+	data->toastrelid = external.va_toastrelid;
+}
+
+static struct varlena *
+ondisk_oid_create_external_data(toast_external_data data)
+{
+	struct varlena *result = NULL;
+	varatt_external_oid external;
+
+	external.va_rawsize = data.rawsize;
+
+	if (data.compression_method != TOAST_INVALID_COMPRESSION_ID)
+	{
+		/* Set size and compression method, in a single field. */
+		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(external,
+													 data.extsize,
+													 data.compression_method);
+	}
+	else
+		external.va_extinfo = data.extsize;
+
+	external.va_toastrelid = data.toastrelid;
+	external.va_valueid = (Oid) data.value;
+
+	result = (struct varlena *) palloc(TOAST_POINTER_OID_SIZE);
+	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_OID);
+	memcpy(VARDATA_EXTERNAL(result), &external, sizeof(external));
+
+	return result;
+}
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 4a1342da6e1b..5e6e19fbc363 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -18,6 +18,7 @@
 #include "access/heapam.h"
 #include "access/heaptoast.h"
 #include "access/table.h"
+#include "access/toast_external.h"
 #include "access/toast_internals.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -127,7 +128,7 @@ toast_save_datum(Relation rel, Datum value,
 	bool		t_isnull[3];
 	CommandId	mycid = GetCurrentCommandId(true);
 	struct varlena *result;
-	struct varatt_external toast_pointer;
+	struct toast_external_data toast_pointer;
 	union
 	{
 		struct varlena hdr;
@@ -143,6 +144,8 @@ toast_save_datum(Relation rel, Datum value,
 	Pointer		dval = DatumGetPointer(value);
 	int			num_indexes;
 	int			validIndex;
+	const toast_external_info *info;
+	uint8		tag = VARTAG_INDIRECT;	/* init value does not matter */
 
 	Assert(!VARATT_IS_EXTERNAL(value));
 
@@ -154,6 +157,15 @@ toast_save_datum(Relation rel, Datum value,
 	toastrel = table_open(rel->rd_rel->reltoastrelid, RowExclusiveLock);
 	toasttupDesc = toastrel->rd_att;
 
+	/*
+	 * Grab the information for toast_external_data.
+	 *
+	 * Note: if we support multiple external vartags for a single value
+	 * type, we would need to be smarter in the vartag selection.
+	 */
+	tag = VARTAG_ONDISK_OID;
+	info = toast_external_get_info(tag);
+
 	/* Open all the toast indexes and look for the valid one */
 	validIndex = toast_open_indexes(toastrel,
 									RowExclusiveLock,
@@ -174,28 +186,41 @@ toast_save_datum(Relation rel, Datum value,
 	{
 		data_p = VARDATA_SHORT(dval);
 		data_todo = VARSIZE_SHORT(dval) - VARHDRSZ_SHORT;
-		toast_pointer.va_rawsize = data_todo + VARHDRSZ;	/* as if not short */
-		toast_pointer.va_extinfo = data_todo;
+		toast_pointer.rawsize = data_todo + VARHDRSZ;	/* as if not short */
+		toast_pointer.extsize = data_todo;
+
+		/*
+		 * Note: we set compression_method to be able to build a correct
+		 * on-disk TOAST pointer.
+		 */
+		toast_pointer.compression_method = TOAST_INVALID_COMPRESSION_ID;
 	}
 	else if (VARATT_IS_COMPRESSED(dval))
 	{
 		data_p = VARDATA(dval);
 		data_todo = VARSIZE(dval) - VARHDRSZ;
 		/* rawsize in a compressed datum is just the size of the payload */
-		toast_pointer.va_rawsize = VARDATA_COMPRESSED_GET_EXTSIZE(dval) + VARHDRSZ;
+		toast_pointer.rawsize = VARDATA_COMPRESSED_GET_EXTSIZE(dval) + VARHDRSZ;
 
 		/* set external size and compression method */
-		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, data_todo,
-													 VARDATA_COMPRESSED_GET_COMPRESS_METHOD(dval));
+		toast_pointer.extsize = data_todo;
+		toast_pointer.compression_method = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(dval);
+
 		/* Assert that the numbers look like it's compressed */
-		Assert(VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer));
+		Assert(TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer));
 	}
 	else
 	{
 		data_p = VARDATA(dval);
 		data_todo = VARSIZE(dval) - VARHDRSZ;
-		toast_pointer.va_rawsize = VARSIZE(dval);
-		toast_pointer.va_extinfo = data_todo;
+		toast_pointer.rawsize = VARSIZE(dval);
+		toast_pointer.extsize = data_todo;
+
+		/*
+		 * Note: we set compression_method to be able to build a correct
+		 * on-disk TOAST pointer.
+		 */
+		toast_pointer.compression_method = TOAST_INVALID_COMPRESSION_ID;
 	}
 
 	/*
@@ -207,9 +232,9 @@ toast_save_datum(Relation rel, Datum value,
 	 * if we have to substitute such an OID.
 	 */
 	if (OidIsValid(rel->rd_toastoid))
-		toast_pointer.va_toastrelid = rel->rd_toastoid;
+		toast_pointer.toastrelid = rel->rd_toastoid;
 	else
-		toast_pointer.va_toastrelid = RelationGetRelid(toastrel);
+		toast_pointer.toastrelid = RelationGetRelid(toastrel);
 
 	/*
 	 * Choose an OID to use as the value ID for this toast value.
@@ -226,7 +251,7 @@ toast_save_datum(Relation rel, Datum value,
 	if (!OidIsValid(rel->rd_toastoid))
 	{
 		/* normal case: just choose an unused OID */
-		toast_pointer.va_valueid =
+		toast_pointer.value =
 			GetNewOidWithIndex(toastrel,
 							   RelationGetRelid(toastidxs[validIndex]),
 							   (AttrNumber) 1);
@@ -234,18 +259,18 @@ toast_save_datum(Relation rel, Datum value,
 	else
 	{
 		/* rewrite case: check to see if value was in old toast table */
-		toast_pointer.va_valueid = InvalidOid;
+		toast_pointer.value = InvalidOid;
 		if (oldexternal != NULL)
 		{
-			struct varatt_external old_toast_pointer;
+			struct toast_external_data old_toast_pointer;
 
 			Assert(VARATT_IS_EXTERNAL_ONDISK(oldexternal));
-			/* Must copy to access aligned fields */
-			VARATT_EXTERNAL_GET_POINTER(old_toast_pointer, oldexternal);
-			if (old_toast_pointer.va_toastrelid == rel->rd_toastoid)
+			toast_external_info_get_data(oldexternal, &old_toast_pointer);
+
+			if (old_toast_pointer.toastrelid == rel->rd_toastoid)
 			{
 				/* This value came from the old toast table; reuse its OID */
-				toast_pointer.va_valueid = old_toast_pointer.va_valueid;
+				toast_pointer.value = old_toast_pointer.value;
 
 				/*
 				 * There is a corner case here: the table rewrite might have
@@ -265,14 +290,14 @@ toast_save_datum(Relation rel, Datum value,
 				 * be reclaimed by VACUUM.
 				 */
 				if (toastrel_valueid_exists(toastrel,
-											toast_pointer.va_valueid))
+											toast_pointer.value))
 				{
 					/* Match, so short-circuit the data storage loop below */
 					data_todo = 0;
 				}
 			}
 		}
-		if (toast_pointer.va_valueid == InvalidOid)
+		if (toast_pointer.value == InvalidOid)
 		{
 			/*
 			 * new value; must choose an OID that doesn't conflict in either
@@ -280,19 +305,19 @@ toast_save_datum(Relation rel, Datum value,
 			 */
 			do
 			{
-				toast_pointer.va_valueid =
+				toast_pointer.value =
 					GetNewOidWithIndex(toastrel,
 									   RelationGetRelid(toastidxs[validIndex]),
 									   (AttrNumber) 1);
 			} while (toastid_valueid_exists(rel->rd_toastoid,
-											toast_pointer.va_valueid));
+											toast_pointer.value));
 		}
 	}
 
 	/*
 	 * Initialize constant parts of the tuple data
 	 */
-	t_values[0] = ObjectIdGetDatum(toast_pointer.va_valueid);
+	t_values[0] = ObjectIdGetDatum(toast_pointer.value);
 	t_values[2] = PointerGetDatum(&chunk_data);
 	t_isnull[0] = false;
 	t_isnull[1] = false;
@@ -310,7 +335,7 @@ toast_save_datum(Relation rel, Datum value,
 		/*
 		 * Calculate the size of this chunk
 		 */
-		chunk_size = Min(TOAST_MAX_CHUNK_SIZE, data_todo);
+		chunk_size = Min(info->maximum_chunk_size, data_todo);
 
 		/*
 		 * Build a tuple and store it
@@ -368,9 +393,7 @@ toast_save_datum(Relation rel, Datum value,
 	/*
 	 * Create the TOAST pointer value that we'll return
 	 */
-	result = (struct varlena *) palloc(TOAST_POINTER_SIZE);
-	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK);
-	memcpy(VARDATA_EXTERNAL(result), &toast_pointer, sizeof(toast_pointer));
+	result = info->create_external_data(toast_pointer);
 
 	return PointerGetDatum(result);
 }
@@ -385,7 +408,7 @@ void
 toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 {
 	struct varlena *attr = (struct varlena *) DatumGetPointer(value);
-	struct varatt_external toast_pointer;
+	struct toast_external_data toast_pointer;
 	Relation	toastrel;
 	Relation   *toastidxs;
 	ScanKeyData toastkey;
@@ -398,12 +421,12 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 		return;
 
 	/* Must copy to access aligned fields */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_external_info_get_data(attr, &toast_pointer);
 
 	/*
 	 * Open the toast relation and its indexes
 	 */
-	toastrel = table_open(toast_pointer.va_toastrelid, RowExclusiveLock);
+	toastrel = table_open(toast_pointer.toastrelid, RowExclusiveLock);
 
 	/* Fetch valid relation used for process */
 	validIndex = toast_open_indexes(toastrel,
@@ -417,7 +440,7 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	ScanKeyInit(&toastkey,
 				(AttrNumber) 1,
 				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(toast_pointer.va_valueid));
+				ObjectIdGetDatum(toast_pointer.value));
 
 	/*
 	 * Find all the chunks.  (We don't actually care whether we see them in
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index ae8d502ddcd3..b3a63c10aef3 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -28,6 +28,7 @@
 #include "access/genam.h"
 #include "access/heapam.h"
 #include "access/heaptoast.h"
+#include "access/toast_external.h"
 #include "access/toast_helper.h"
 #include "access/toast_internals.h"
 #include "utils/fmgroids.h"
@@ -140,6 +141,17 @@ heap_toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup,
 	 * Prepare for toasting
 	 * ----------
 	 */
+
+	/*
+	 * Retrieve the toast pointer size based on the type of external TOAST
+	 * pointer assumed to be used.
+	 *
+	 * Only one format of external TOAST pointer is supported currently,
+	 * making this code simple, based on a single vartag.
+	 */
+	ttc.ttc_toast_pointer_size =
+		toast_external_info_get_pointer_size(VARTAG_ONDISK_OID);
+
 	ttc.ttc_rel = rel;
 	ttc.ttc_values = toast_values;
 	ttc.ttc_isnull = toast_isnull;
@@ -640,6 +652,8 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 	int			num_indexes;
 	int			validIndex;
 	int32		max_chunk_size;
+	const toast_external_info *info;
+	uint8		tag = VARTAG_INDIRECT;  /* init value does not matter */
 
 	/* Look for the valid index of toast relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -647,7 +661,21 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 									&toastidxs,
 									&num_indexes);
 
-	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	/*
+	 * Grab the information for toast_external_data.
+	 *
+	 * Note: there is no access to the vartag of the original varlena from
+	 * which we are trying to retrieve the chunks from the TOAST relation,
+	 * so guess the external TOAST pointer information to use depending
+	 * on the attribute of the TOAST value.  If we begin to support multiple
+	 * external TOAST pointers for a single attribute type, we would need
+	 * to pass down this information from the upper callers.  This is
+	 * currently on required for the maximum chunk_size.
+	 */
+	tag = VARTAG_ONDISK_OID;
+	info = toast_external_get_info(tag);
+
+	max_chunk_size = info->maximum_chunk_size;
 
 	totalchunks = ((attrsize - 1) / max_chunk_size) + 1;
 	startchunk = sliceoffset / max_chunk_size;
diff --git a/src/backend/access/table/toast_helper.c b/src/backend/access/table/toast_helper.c
index b60fab0a4d29..a2b44e093d79 100644
--- a/src/backend/access/table/toast_helper.c
+++ b/src/backend/access/table/toast_helper.c
@@ -171,8 +171,10 @@ toast_tuple_init(ToastTupleContext *ttc)
  * The column must have attstorage EXTERNAL or EXTENDED if check_main is
  * false, and must have attstorage MAIN if check_main is true.
  *
- * The column must have a minimum size of MAXALIGN(TOAST_POINTER_SIZE);
- * if not, no benefit is to be expected by compressing it.
+ * The column must have a minimum size of MAXALIGN(tcc_toast_pointer_size);
+ * if not, no benefit is to be expected by compressing it.  The TOAST
+ * pointer size is given by the caller, depending on the type of TOAST
+ * table we are dealing with.
  *
  * The return value is the index of the biggest suitable column, or
  * -1 if there is none.
@@ -184,10 +186,14 @@ toast_tuple_find_biggest_attribute(ToastTupleContext *ttc,
 	TupleDesc	tupleDesc = ttc->ttc_rel->rd_att;
 	int			numAttrs = tupleDesc->natts;
 	int			biggest_attno = -1;
-	int32		biggest_size = MAXALIGN(TOAST_POINTER_SIZE);
+	int32		biggest_size = 0;
 	int32		skip_colflags = TOASTCOL_IGNORE;
 	int			i;
 
+	/* Define the lower-bound */
+	biggest_size = MAXALIGN(ttc->ttc_toast_pointer_size);
+	Assert(biggest_size != 0);
+
 	if (for_compression)
 		skip_colflags |= TOASTCOL_INCOMPRESSIBLE;
 
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index c871708b5932..77939c7f849c 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -92,6 +92,7 @@
 #include "access/detoast.h"
 #include "access/heapam.h"
 #include "access/rewriteheap.h"
+#include "access/toast_external.h"
 #include "access/transam.h"
 #include "access/xact.h"
 #include "access/xlog_internal.h"
@@ -5102,7 +5103,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		struct varlena *varlena;
 
 		/* va_rawsize is the size of the original datum -- including header */
-		struct varatt_external toast_pointer;
+		struct toast_external_data toast_pointer;
 		struct varatt_indirect redirect_pointer;
 		struct varlena *new_datum = NULL;
 		struct varlena *reconstructed;
@@ -5132,8 +5133,8 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		if (!VARATT_IS_EXTERNAL(varlena))
 			continue;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, varlena);
-		toast_valueid = toast_pointer.va_valueid;
+		toast_external_info_get_data(varlena, &toast_pointer);
+		toast_valueid = toast_pointer.value;
 
 		/*
 		 * Check whether the toast tuple changed, replace if so.
@@ -5151,7 +5152,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 
 		free[natt] = true;
 
-		reconstructed = palloc0(toast_pointer.va_rawsize);
+		reconstructed = palloc0(toast_pointer.rawsize);
 
 		ent->reconstructed = reconstructed;
 
@@ -5176,10 +5177,10 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 				   VARSIZE(chunk) - VARHDRSZ);
 			data_done += VARSIZE(chunk) - VARHDRSZ;
 		}
-		Assert(data_done == VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer));
+		Assert(data_done == toast_pointer.extsize);
 
 		/* make sure its marked as compressed or not */
-		if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+		if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 			SET_VARSIZE_COMPRESSED(reconstructed, data_done + VARHDRSZ);
 		else
 			SET_VARSIZE(reconstructed, data_done + VARHDRSZ);
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index ffae8c23abfa..d76386407a08 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -19,6 +19,7 @@
 
 #include "access/detoast.h"
 #include "access/toast_compression.h"
+#include "access/toast_external.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_type.h"
 #include "common/hashfn.h"
@@ -4219,7 +4220,7 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 {
 	int			typlen;
 	struct varlena *attr;
-	struct varatt_external toast_pointer;
+	uint64		toast_valueid;
 
 	/* On first call, get the input type's typlen, and save at *fn_extra */
 	if (fcinfo->flinfo->fn_extra == NULL)
@@ -4246,9 +4247,9 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		PG_RETURN_NULL();
 
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_valueid = toast_external_info_get_value(attr);
 
-	PG_RETURN_OID(toast_pointer.va_valueid);
+	PG_RETURN_OID(toast_valueid);
 }
 
 /*
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 559ba9cdb2cd..8761762a6f3a 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -37,6 +37,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "access/tableam.h"
+#include "access/toast_external.h"
 #include "access/tupdesc_details.h"
 #include "access/xact.h"
 #include "catalog/binary_upgrade.h"
diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml
index 61250799ec07..f3c6cd8860b5 100644
--- a/doc/src/sgml/storage.sgml
+++ b/doc/src/sgml/storage.sgml
@@ -415,7 +415,7 @@ described in more detail below.
 
 <para>
 Out-of-line values are divided (after compression if used) into chunks of at
-most <symbol>TOAST_MAX_CHUNK_SIZE</symbol> bytes (by default this value is chosen
+most <symbol>TOAST_MAX_CHUNK_SIZE_OID</symbol> bytes (by default this value is chosen
 so that four chunk rows will fit on a page, making it about 2000 bytes).
 Each chunk is stored as a separate row in the <acronym>TOAST</acronym> table
 belonging to the owning table.  Every
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 3b2bdced4cdc..11c4507ae6e2 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -16,6 +16,7 @@
 #include "access/multixact.h"
 #include "access/relation.h"
 #include "access/table.h"
+#include "access/toast_external.h"
 #include "access/toast_internals.h"
 #include "access/visibilitymap.h"
 #include "access/xact.h"
@@ -73,7 +74,8 @@ typedef enum SkipPages
  */
 typedef struct ToastedAttribute
 {
-	struct varatt_external toast_pointer;
+	struct toast_external_data toast_pointer;
+	const toast_external_info *info;
 	BlockNumber blkno;			/* block in main table */
 	OffsetNumber offnum;		/* offset in main table */
 	AttrNumber	attnum;			/* attribute in main table */
@@ -1564,9 +1566,9 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	uint64		toast_valueid;
 	int32		max_chunk_size;
 
-	toast_valueid = ta->toast_pointer.va_valueid;
+	toast_valueid = ta->toast_pointer.value;
 
-	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	max_chunk_size = ta->info->maximum_chunk_size;
 	last_chunk_seq = (extsize - 1) / max_chunk_size;
 
 	/* Sanity-check the sequence number. */
@@ -1672,7 +1674,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	uint16		infomask;
 	uint64		toast_pointer_valueid;
 	CompactAttribute *thisatt;
-	struct varatt_external toast_pointer;
+	struct toast_external_data toast_pointer;
 
 	infomask = ctx->tuphdr->t_infomask;
 	thisatt = TupleDescCompactAttr(RelationGetDescr(ctx->rel), ctx->attnum);
@@ -1731,7 +1733,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	{
 		uint8		va_tag = VARTAG_EXTERNAL(tp + ctx->offset);
 
-		if (va_tag != VARTAG_ONDISK)
+		if (va_tag != VARTAG_ONDISK_OID)
 		{
 			report_corruption(ctx,
 							  psprintf("toasted attribute has unexpected TOAST tag %u",
@@ -1774,28 +1776,28 @@ check_tuple_attribute(HeapCheckContext *ctx)
 		return true;
 
 	/* It is external, and we're looking at a page on disk */
-	toast_pointer_valueid = toast_pointer.va_valueid;
 
 	/*
 	 * Must copy attr into toast_pointer for alignment considerations
 	 */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_external_info_get_data(attr, &toast_pointer);
+	toast_pointer_valueid = toast_pointer.value;
 
 	/* Toasted attributes too large to be untoasted should never be stored */
-	if (toast_pointer.va_rawsize > VARLENA_SIZE_LIMIT)
+	if (toast_pointer.rawsize > VARLENA_SIZE_LIMIT)
 		report_corruption(ctx,
 						  psprintf("toast value %" PRIu64 " rawsize %d exceeds limit %d",
 								   toast_pointer_valueid,
-								   toast_pointer.va_rawsize,
+								   toast_pointer.rawsize,
 								   VARLENA_SIZE_LIMIT));
 
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+	if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 	{
 		ToastCompressionId cmid;
 		bool		valid = false;
 
 		/* Compressed attributes should have a valid compression method */
-		cmid = TOAST_COMPRESS_METHOD(&toast_pointer);
+		cmid = toast_pointer.compression_method;
 		switch (cmid)
 		{
 				/* List of all valid compression method IDs */
@@ -1849,7 +1851,8 @@ check_tuple_attribute(HeapCheckContext *ctx)
 
 		ta = (ToastedAttribute *) palloc0(sizeof(ToastedAttribute));
 
-		VARATT_EXTERNAL_GET_POINTER(ta->toast_pointer, attr);
+		toast_external_info_get_data(attr, &ta->toast_pointer);
+		ta->info = toast_external_get_info(VARTAG_EXTERNAL(attr));
 		ta->blkno = ctx->blkno;
 		ta->offnum = ctx->offnum;
 		ta->attnum = ctx->attnum;
@@ -1876,9 +1879,11 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	int32		expected_chunk_seq = 0;
 	int32		last_chunk_seq;
 	uint64		toast_valueid;
-	int32		max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	int32		max_chunk_size;
 
-	extsize = VARATT_EXTERNAL_GET_EXTSIZE(ta->toast_pointer);
+	extsize = ta->toast_pointer.extsize;
+
+	max_chunk_size = ta->info->maximum_chunk_size;
 	last_chunk_seq = (extsize - 1) / max_chunk_size;
 
 	/*
@@ -1887,7 +1892,7 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	ScanKeyInit(&toastkey,
 				(AttrNumber) 1,
 				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(ta->toast_pointer.va_valueid));
+				ObjectIdGetDatum(ta->toast_pointer.value));
 
 	/*
 	 * Check if any chunks for this toasted object exist in the toast table,
@@ -1907,7 +1912,7 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	}
 	systable_endscan_ordered(toastscan);
 
-	toast_valueid = ta->toast_pointer.va_valueid;
+	toast_valueid = ta->toast_pointer.value;
 
 	if (!found_toasttup)
 		report_toast_corruption(ctx, ta,
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 114bdafafdfa..3b98606a1701 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -4137,6 +4137,8 @@ timeout_params
 timerCA
 tlist_vinfo
 toast_compress_header
+toast_external_data
+toast_external_info
 tokenize_error_callback_arg
 transferMode
 transfer_thread_arg
-- 
2.50.0

v2-0004-Introduce-new-callback-to-get-fresh-TOAST-values.patchtext/x-diff; charset=us-asciiDownload
From 05c96b293b7bf1563752e42a3b659b9d549a08c5 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Mon, 7 Jul 2025 16:05:13 +0900
Subject: [PATCH v2 04/13] Introduce new callback to get fresh TOAST values

This callback is called by toast_save_datum() to retrieve a new value
from a source related to the vartag_external we are dealing with.  As of
now, it is simply a wrapper around GetNewOidWithIndex() for the "OID"
on-disk TOAST external pointer.

This will be used later on by more external pointer types, like the int8
one.

InvalidToastId is introduced to track the concept of an "invalid" TOAST
value, required for toast_save_datum().
---
 src/include/access/toast_external.h         | 19 ++++++++++++++++++-
 src/backend/access/common/toast_external.c  | 11 +++++++++++
 src/backend/access/common/toast_internals.c | 16 ++++++++--------
 3 files changed, 37 insertions(+), 9 deletions(-)

diff --git a/src/include/access/toast_external.h b/src/include/access/toast_external.h
index 1e3ba8062286..1a7c61454f75 100644
--- a/src/include/access/toast_external.h
+++ b/src/include/access/toast_external.h
@@ -15,9 +15,14 @@
 #ifndef TOAST_EXTERNAL_H
 #define TOAST_EXTERNAL_H
 
+#include "access/attnum.h"
 #include "access/toast_compression.h"
+#include "utils/relcache.h"
 #include "varatt.h"
 
+/* Invalid TOAST value ID */
+#define InvalidToastId 0
+
 /*
  * Intermediate in-memory structure used when creating on-disk
  * varatt_external_* or when deserializing varlena contents.
@@ -35,7 +40,7 @@ typedef struct toast_external_data
 	/*
 	 * Unique ID of value within TOAST table.  This could be an OID or an
 	 * int8 value.  This field is large enough to be able to store any of
-	 * them.
+	 * them.  InvalidToastId if invalid.
 	 */
 	uint64		value;
 } toast_external_data;
@@ -74,6 +79,18 @@ typedef struct toast_external_info
 	 */
 	struct varlena  *(*create_external_data) (toast_external_data data);
 
+	/*
+	 * Retrieve a new value, to be assigned for a TOAST entry that will
+	 * be saved.  "toastrel" is the relation where the entry is added.
+	 * "indexid" and "attnum" can be used to check if a value is already
+	 * in use in the TOAST relation where the new entry is inserted.
+	 *
+	 * When "check" is set to true, the value generated should be rechecked
+	 * with the existing TOAST index.
+	 */
+	uint64		(*get_new_value) (Relation toastrel, Oid indexid,
+								  AttrNumber attnum);
+
 } toast_external_info;
 
 /* Retrieve a toast_external_info from a vartag */
diff --git a/src/backend/access/common/toast_external.c b/src/backend/access/common/toast_external.c
index a851a8e4184b..d179f0143803 100644
--- a/src/backend/access/common/toast_external.c
+++ b/src/backend/access/common/toast_external.c
@@ -16,11 +16,14 @@
 #include "access/detoast.h"
 #include "access/heaptoast.h"
 #include "access/toast_external.h"
+#include "catalog/catalog.h"
 
 /* Callbacks for VARTAG_ONDISK_OID */
 static void ondisk_oid_to_external_data(struct varlena *attr,
 										toast_external_data *data);
 static struct varlena *ondisk_oid_create_external_data(toast_external_data data);
+static uint64 ondisk_oid_get_new_value(Relation toastrel, Oid indexid,
+									   AttrNumber attnum);
 
 
 /*
@@ -48,6 +51,7 @@ static const toast_external_info toast_external_infos[TOAST_EXTERNAL_INFO_SIZE]
 		.maximum_chunk_size = TOAST_MAX_CHUNK_SIZE_OID,
 		.to_external_data = ondisk_oid_to_external_data,
 		.create_external_data = ondisk_oid_create_external_data,
+		.get_new_value = ondisk_oid_get_new_value,
 	},
 };
 
@@ -129,3 +133,10 @@ ondisk_oid_create_external_data(toast_external_data data)
 
 	return result;
 }
+
+static uint64
+ondisk_oid_get_new_value(Relation toastrel, Oid indexid,
+						 AttrNumber attnum)
+{
+	return GetNewOidWithIndex(toastrel, indexid, attnum);
+}
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 5e6e19fbc363..66dfedefc579 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -252,14 +252,14 @@ toast_save_datum(Relation rel, Datum value,
 	{
 		/* normal case: just choose an unused OID */
 		toast_pointer.value =
-			GetNewOidWithIndex(toastrel,
-							   RelationGetRelid(toastidxs[validIndex]),
-							   (AttrNumber) 1);
+			info->get_new_value(toastrel,
+								RelationGetRelid(toastidxs[validIndex]),
+								(AttrNumber) 1);
 	}
 	else
 	{
 		/* rewrite case: check to see if value was in old toast table */
-		toast_pointer.value = InvalidOid;
+		toast_pointer.value = InvalidToastId;
 		if (oldexternal != NULL)
 		{
 			struct toast_external_data old_toast_pointer;
@@ -297,7 +297,7 @@ toast_save_datum(Relation rel, Datum value,
 				}
 			}
 		}
-		if (toast_pointer.value == InvalidOid)
+		if (toast_pointer.value == InvalidToastId)
 		{
 			/*
 			 * new value; must choose an OID that doesn't conflict in either
@@ -306,9 +306,9 @@ toast_save_datum(Relation rel, Datum value,
 			do
 			{
 				toast_pointer.value =
-					GetNewOidWithIndex(toastrel,
-									   RelationGetRelid(toastidxs[validIndex]),
-									   (AttrNumber) 1);
+					info->get_new_value(toastrel,
+										RelationGetRelid(toastidxs[validIndex]),
+										(AttrNumber) 1);
 			} while (toastid_valueid_exists(rel->rd_toastoid,
 											toast_pointer.value));
 		}
-- 
2.50.0

v2-0005-Add-catcache-support-for-INT8OID.patchtext/x-diff; charset=us-asciiDownload
From 13bbb82a4765cbcfce8bbaed382507b127d20ca9 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 18 Jun 2025 16:12:11 +0900
Subject: [PATCH v2 05/13] Add catcache support for INT8OID

This is required to be able to do catalog cache lookups of int8 fields
for toast values of the same type.
---
 src/backend/utils/cache/catcache.c | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index d1b25214376e..c77f571014e5 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -240,6 +240,18 @@ int4hashfast(Datum datum)
 	return murmurhash32((int32) DatumGetInt32(datum));
 }
 
+static bool
+int8eqfast(Datum a, Datum b)
+{
+	return DatumGetInt64(a) == DatumGetInt64(b);
+}
+
+static uint32
+int8hashfast(Datum datum)
+{
+	return murmurhash64((int64) DatumGetInt64(datum));
+}
+
 static bool
 texteqfast(Datum a, Datum b)
 {
@@ -300,6 +312,11 @@ GetCCHashEqFuncs(Oid keytype, CCHashFN *hashfunc, RegProcedure *eqfunc, CCFastEq
 			*fasteqfunc = int4eqfast;
 			*eqfunc = F_INT4EQ;
 			break;
+		case INT8OID:
+			*hashfunc = int8hashfast;
+			*fasteqfunc = int8eqfast;
+			*eqfunc = F_INT8EQ;
+			break;
 		case TEXTOID:
 			*hashfunc = texthashfast;
 			*fasteqfunc = texteqfast;
-- 
2.50.0

v2-0006-Add-GUC-default_toast_type.patchtext/x-diff; charset=us-asciiDownload
From 102ce391913e0ae20ec7b82809a31a08926849b3 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 18 Jun 2025 16:15:19 +0900
Subject: [PATCH v2 06/13] Add GUC default_toast_type

This GUC controls the data type used for newly-created TOAST values,
with two modes supported: "oid" and "int8".  This will be used by an
upcoming patch.
---
 src/include/access/toast_type.h               | 30 +++++++++++++++++++
 src/backend/catalog/toasting.c                |  4 +++
 src/backend/utils/misc/guc_tables.c           | 19 ++++++++++++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 doc/src/sgml/config.sgml                      | 17 +++++++++++
 5 files changed, 71 insertions(+)
 create mode 100644 src/include/access/toast_type.h

diff --git a/src/include/access/toast_type.h b/src/include/access/toast_type.h
new file mode 100644
index 000000000000..494c2a3e852e
--- /dev/null
+++ b/src/include/access/toast_type.h
@@ -0,0 +1,30 @@
+/*-------------------------------------------------------------------------
+ *
+ * toast_type.h
+ *	  Internal definitions for the types supported by values in TOAST
+ *	  relations.
+ *
+ * Copyright (c) 2000-2025, PostgreSQL Global Development Group
+ *
+ * src/include/access/toast_type.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef TOAST_TYPE_H
+#define TOAST_TYPE_H
+
+/*
+ * GUC support
+ *
+ * Detault value type in toast table.
+ */
+extern PGDLLIMPORT int default_toast_type;
+
+typedef enum ToastTypeId
+{
+	TOAST_TYPE_INVALID = 0,
+	TOAST_TYPE_OID = 1,
+	TOAST_TYPE_INT8 = 2,
+} ToastTypeId;
+
+#endif							/* TOAST_TYPE_H */
diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c
index 874a8fc89adb..e595cb61b375 100644
--- a/src/backend/catalog/toasting.c
+++ b/src/backend/catalog/toasting.c
@@ -16,6 +16,7 @@
 
 #include "access/heapam.h"
 #include "access/toast_compression.h"
+#include "access/toast_type.h"
 #include "access/xact.h"
 #include "catalog/binary_upgrade.h"
 #include "catalog/catalog.h"
@@ -33,6 +34,9 @@
 #include "utils/rel.h"
 #include "utils/syscache.h"
 
+/* GUC support */
+int			default_toast_type = TOAST_TYPE_OID;
+
 static void CheckAndCreateToastTable(Oid relOid, Datum reloptions,
 									 LOCKMODE lockmode, bool check,
 									 Oid OIDOldToast);
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 511dc32d5192..0999a2b00b16 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -33,6 +33,7 @@
 #include "access/gin.h"
 #include "access/slru.h"
 #include "access/toast_compression.h"
+#include "access/toast_type.h"
 #include "access/twophase.h"
 #include "access/xlog_internal.h"
 #include "access/xlogprefetcher.h"
@@ -464,6 +465,13 @@ static const struct config_enum_entry default_toast_compression_options[] = {
 	{NULL, 0, false}
 };
 
+
+static const struct config_enum_entry default_toast_type_options[] = {
+	{"oid", TOAST_TYPE_OID, false},
+	{"int8", TOAST_TYPE_INT8, false},
+	{NULL, 0, false}
+};
+
 static const struct config_enum_entry wal_compression_options[] = {
 	{"pglz", WAL_COMPRESSION_PGLZ, false},
 #ifdef USE_LZ4
@@ -5058,6 +5066,17 @@ struct config_enum ConfigureNamesEnum[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"default_toast_type", PGC_USERSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Sets the default type used for TOAST values."),
+			NULL
+		},
+		&default_toast_type,
+		TOAST_TYPE_OID,
+		default_toast_type_options,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"default_transaction_isolation", PGC_USERSET, CLIENT_CONN_STATEMENT,
 			gettext_noop("Sets the transaction isolation level of each new transaction."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 341f88adc87b..4fbf76e48ec4 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -753,6 +753,7 @@ autovacuum_worker_slots = 16	# autovacuum worker slots to allocate
 #default_table_access_method = 'heap'
 #default_tablespace = ''		# a tablespace name, '' uses the default
 #default_toast_compression = 'pglz'	# 'pglz' or 'lz4'
+#default_toast_type = 'oid'		# 'oid' or 'int8'
 #temp_tablespaces = ''			# a list of tablespace names, '' uses
 					# only default tablespace
 #check_function_bodies = on
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 59a0874528a3..ad75c62d7fbc 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9830,6 +9830,23 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-default-toast-type" xreflabel="default_toast_type">
+      <term><varname>default_toast_type</varname> (<type>enum</type>)
+      <indexterm>
+       <primary><varname>default_toast_type</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        This variable sets the default type for
+        <link linkend="storage-toast">TOAST</link> values.
+        The value types supported are <literal>oid</literal> and
+        <literal>int8</literal>.
+        The default is <literal>oid</literal>.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-temp-tablespaces" xreflabel="temp_tablespaces">
       <term><varname>temp_tablespaces</varname> (<type>string</type>)
       <indexterm>
-- 
2.50.0

v2-0007-Introduce-global-64-bit-TOAST-ID-counter-in-contr.patchtext/x-diff; charset=us-asciiDownload
From ca099d7bc6b4598592048c3370b82b09d317d98b Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 18 Jun 2025 16:23:16 +0900
Subject: [PATCH v2 07/13] Introduce global 64-bit TOAST ID counter in control
 file

An 8 byte counter is added to the control file, providing a unique
64-bit-wide source for toast value IDs, with the same guarantees as OIDs
in terms of durability.  SQL functions and tools looking at the control
file are updated.  A WAL record is generated every 8k values generated,
that can be adjusted if required.

Requires a bump of WAL format.
Requires a bump of control file version.
Requires a catalog version bump.
---
 src/include/access/toast_counter.h            | 35 +++++++
 src/include/access/xlog.h                     |  1 +
 src/include/catalog/pg_control.h              |  4 +-
 src/include/catalog/pg_proc.dat               |  6 +-
 src/include/storage/lwlocklist.h              |  1 +
 src/backend/access/common/Makefile            |  1 +
 src/backend/access/common/meson.build         |  1 +
 src/backend/access/common/toast_counter.c     | 98 +++++++++++++++++++
 src/backend/access/rmgrdesc/xlogdesc.c        | 10 ++
 src/backend/access/transam/xlog.c             | 44 +++++++++
 src/backend/replication/logical/decode.c      |  1 +
 src/backend/storage/ipc/ipci.c                |  5 +-
 .../utils/activity/wait_event_names.txt       |  1 +
 src/backend/utils/misc/pg_controldata.c       | 23 +++--
 src/bin/pg_controldata/pg_controldata.c       |  2 +
 src/bin/pg_resetwal/pg_resetwal.c             |  2 +
 doc/src/sgml/func.sgml                        |  5 +
 src/tools/pgindent/typedefs.list              |  1 +
 18 files changed, 226 insertions(+), 15 deletions(-)
 create mode 100644 src/include/access/toast_counter.h
 create mode 100644 src/backend/access/common/toast_counter.c

diff --git a/src/include/access/toast_counter.h b/src/include/access/toast_counter.h
new file mode 100644
index 000000000000..80749cba0f87
--- /dev/null
+++ b/src/include/access/toast_counter.h
@@ -0,0 +1,35 @@
+/*-------------------------------------------------------------------------
+ *
+ * toast_counter.h
+ *	  Machinery for TOAST value counter.
+ *
+ * Copyright (c) 2000-2025, PostgreSQL Global Development Group
+ *
+ * src/include/access/toast_counter.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef TOAST_COUNTER_H
+#define TOAST_COUNTER_H
+
+#define InvalidToastId	0		/* Invalid TOAST value ID */
+#define FirstToastId	1		/* First TOAST value ID assigned */
+
+/*
+ * Structure in shared memory to track TOAST value counter activity.
+ * These are protected by ToastIdGenLock.
+ */
+typedef struct ToastCounterData
+{
+	uint64		nextId;			/* next TOAST value ID to assign */
+	uint32		idCount;		/* IDs available before WAL work */
+} ToastCounterData;
+
+extern PGDLLIMPORT ToastCounterData *ToastCounter;
+
+/* external declarations */
+extern Size ToastCounterShmemSize(void);
+extern void ToastCounterShmemInit(void);
+extern uint64 GetNewToastId(void);
+
+#endif							/* TOAST_TYPE_H */
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index d313099c027f..a50296736242 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -245,6 +245,7 @@ extern bool CreateCheckPoint(int flags);
 extern bool CreateRestartPoint(int flags);
 extern WALAvailability GetWALAvailability(XLogRecPtr targetLSN);
 extern void XLogPutNextOid(Oid nextOid);
+extern void XLogPutNextToastId(uint64 nextId);
 extern XLogRecPtr XLogRestorePoint(const char *rpName);
 extern void UpdateFullPageWrites(void);
 extern void GetFullPageWriteInfo(XLogRecPtr *RedoRecPtr_p, bool *doPageWrites_p);
diff --git a/src/include/catalog/pg_control.h b/src/include/catalog/pg_control.h
index 63e834a6ce47..1194b4928155 100644
--- a/src/include/catalog/pg_control.h
+++ b/src/include/catalog/pg_control.h
@@ -22,7 +22,7 @@
 
 
 /* Version identifier for this pg_control format */
-#define PG_CONTROL_VERSION	1800
+#define PG_CONTROL_VERSION	1900
 
 /* Nonce key length, see below */
 #define MOCK_AUTH_NONCE_LEN		32
@@ -45,6 +45,7 @@ typedef struct CheckPoint
 	Oid			nextOid;		/* next free OID */
 	MultiXactId nextMulti;		/* next free MultiXactId */
 	MultiXactOffset nextMultiOffset;	/* next free MultiXact offset */
+	uint64		nextToastId;	/* next free TOAST ID */
 	TransactionId oldestXid;	/* cluster-wide minimum datfrozenxid */
 	Oid			oldestXidDB;	/* database with minimum datfrozenxid */
 	MultiXactId oldestMulti;	/* cluster-wide minimum datminmxid */
@@ -80,6 +81,7 @@ typedef struct CheckPoint
 /* 0xC0 is used in Postgres 9.5-11 */
 #define XLOG_OVERWRITE_CONTRECORD		0xD0
 #define XLOG_CHECKPOINT_REDO			0xE0
+#define XLOG_NEXT_TOAST_ID				0xF0
 
 
 /*
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index d4650947c63a..ff0871037c53 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12306,9 +12306,9 @@
   descr => 'pg_controldata checkpoint state information as a function',
   proname => 'pg_control_checkpoint', provolatile => 'v',
   prorettype => 'record', proargtypes => '',
-  proallargtypes => '{pg_lsn,pg_lsn,text,int4,int4,bool,text,oid,xid,xid,xid,oid,xid,xid,oid,xid,xid,timestamptz}',
-  proargmodes => '{o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
-  proargnames => '{checkpoint_lsn,redo_lsn,redo_wal_file,timeline_id,prev_timeline_id,full_page_writes,next_xid,next_oid,next_multixact_id,next_multi_offset,oldest_xid,oldest_xid_dbid,oldest_active_xid,oldest_multi_xid,oldest_multi_dbid,oldest_commit_ts_xid,newest_commit_ts_xid,checkpoint_time}',
+  proallargtypes => '{pg_lsn,pg_lsn,text,int4,int4,bool,text,oid,xid,xid,int8,xid,oid,xid,xid,oid,xid,xid,timestamptz}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{checkpoint_lsn,redo_lsn,redo_wal_file,timeline_id,prev_timeline_id,full_page_writes,next_xid,next_oid,next_multixact_id,next_multi_offset,next_toast_id,oldest_xid,oldest_xid_dbid,oldest_active_xid,oldest_multi_xid,oldest_multi_dbid,oldest_commit_ts_xid,newest_commit_ts_xid,checkpoint_time}',
   prosrc => 'pg_control_checkpoint' },
 
 { oid => '3443',
diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h
index a9681738146e..7f7ca92382b5 100644
--- a/src/include/storage/lwlocklist.h
+++ b/src/include/storage/lwlocklist.h
@@ -84,3 +84,4 @@ PG_LWLOCK(50, DSMRegistry)
 PG_LWLOCK(51, InjectionPoint)
 PG_LWLOCK(52, SerialControl)
 PG_LWLOCK(53, AioWorkerSubmissionQueue)
+PG_LWLOCK(54, ToastIdGen)
diff --git a/src/backend/access/common/Makefile b/src/backend/access/common/Makefile
index 1ef86a245886..6e9a3a430c19 100644
--- a/src/backend/access/common/Makefile
+++ b/src/backend/access/common/Makefile
@@ -27,6 +27,7 @@ OBJS = \
 	syncscan.o \
 	tidstore.o \
 	toast_compression.o \
+	toast_counter.o \
 	toast_external.o \
 	toast_internals.o \
 	tupconvert.o \
diff --git a/src/backend/access/common/meson.build b/src/backend/access/common/meson.build
index c20f2e88921e..4254132c8dfd 100644
--- a/src/backend/access/common/meson.build
+++ b/src/backend/access/common/meson.build
@@ -15,6 +15,7 @@ backend_sources += files(
   'syncscan.c',
   'tidstore.c',
   'toast_compression.c',
+  'toast_counter.c',
   'toast_external.c',
   'toast_internals.c',
   'tupconvert.c',
diff --git a/src/backend/access/common/toast_counter.c b/src/backend/access/common/toast_counter.c
new file mode 100644
index 000000000000..94d361d0d5c4
--- /dev/null
+++ b/src/backend/access/common/toast_counter.c
@@ -0,0 +1,98 @@
+/*-------------------------------------------------------------------------
+ *
+ * toast_counter.c
+ *	  Functions for TOAST value counter.
+ *
+ * Copyright (c) 2025, PostgreSQL Global Development Group
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/common/toast_counter.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/toast_counter.h"
+#include "access/xlog.h"
+#include "miscadmin.h"
+#include "storage/lwlock.h"
+#include "storage/shmem.h"
+
+/* Number of TOAST values to preallocate before WAL work */
+#define TOAST_ID_PREFETCH		8192
+
+/* pointer to variables struct in shared memory */
+ToastCounterData *ToastCounter = NULL;
+
+/*
+ * Initialization of shared memory for ToastCounter.
+ */
+Size
+ToastCounterShmemSize(void)
+{
+	return sizeof(ToastCounterData);
+}
+
+void
+ToastCounterShmemInit(void)
+{
+	bool		found;
+
+	/* Initialize shared state struct */
+	ToastCounter = ShmemInitStruct("ToastCounter",
+								   sizeof(ToastCounterData),
+								   &found);
+	if (!IsUnderPostmaster)
+	{
+		Assert(!found);
+		memset(ToastCounter, 0, sizeof(ToastCounterData));
+	}
+	else
+		Assert(found);
+}
+
+/*
+ * GetNewToastId
+ *
+ * Toast IDs are generated as a cluster-wide counter.  They are 64 bits
+ * wide, hence wraparound will unlikely happen.
+ */
+uint64
+GetNewToastId(void)
+{
+	uint64		result;
+
+	if (RecoveryInProgress())
+		elog(ERROR, "cannot assign TOAST IDs during recovery");
+
+	LWLockAcquire(ToastIdGenLock, LW_EXCLUSIVE);
+
+	/*
+	 * Check for initialization or wraparound of the toast counter ID.
+	 * InvalidToastId (0) should never be returned.  We are 64 bit-wide, hence
+	 * wraparound is unlikely going to happen, but this check is cheap so
+	 * let's play it safe.
+	 */
+	if (ToastCounter->nextId < ((uint64) FirstToastId))
+	{
+		/* Most-likely first bootstrap or initdb assignment */
+		ToastCounter->nextId = FirstToastId;
+		ToastCounter->idCount = 0;
+	}
+
+	/* If running out of logged for TOAST IDs, log more */
+	if (ToastCounter->idCount == 0)
+	{
+		XLogPutNextToastId(ToastCounter->nextId + TOAST_ID_PREFETCH);
+		ToastCounter->idCount = TOAST_ID_PREFETCH;
+	}
+
+	result = ToastCounter->nextId;
+	(ToastCounter->nextId)++;
+	(ToastCounter->idCount)--;
+
+	LWLockRelease(ToastIdGenLock);
+
+	return result;
+}
diff --git a/src/backend/access/rmgrdesc/xlogdesc.c b/src/backend/access/rmgrdesc/xlogdesc.c
index 58040f28656f..0940040b33ab 100644
--- a/src/backend/access/rmgrdesc/xlogdesc.c
+++ b/src/backend/access/rmgrdesc/xlogdesc.c
@@ -96,6 +96,13 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
 		memcpy(&nextOid, rec, sizeof(Oid));
 		appendStringInfo(buf, "%u", nextOid);
 	}
+	else if (info == XLOG_NEXT_TOAST_ID)
+	{
+		uint64		nextId;
+
+		memcpy(&nextId, rec, sizeof(uint64));
+		appendStringInfo(buf, "%" PRIu64, nextId);
+	}
 	else if (info == XLOG_RESTORE_POINT)
 	{
 		xl_restore_point *xlrec = (xl_restore_point *) rec;
@@ -218,6 +225,9 @@ xlog_identify(uint8 info)
 		case XLOG_CHECKPOINT_REDO:
 			id = "CHECKPOINT_REDO";
 			break;
+		case XLOG_NEXT_TOAST_ID:
+			id = "NEXT_TOAST_ID";
+			break;
 	}
 
 	return id;
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 47ffc0a23077..4ae1ef8bb2c2 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -53,6 +53,7 @@
 #include "access/rewriteheap.h"
 #include "access/subtrans.h"
 #include "access/timeline.h"
+#include "access/toast_counter.h"
 #include "access/transam.h"
 #include "access/twophase.h"
 #include "access/xact.h"
@@ -5269,6 +5270,7 @@ BootStrapXLOG(uint32 data_checksum_version)
 	checkPoint.nextOid = FirstGenbkiObjectId;
 	checkPoint.nextMulti = FirstMultiXactId;
 	checkPoint.nextMultiOffset = 0;
+	checkPoint.nextToastId = FirstToastId;
 	checkPoint.oldestXid = FirstNormalTransactionId;
 	checkPoint.oldestXidDB = Template1DbOid;
 	checkPoint.oldestMulti = FirstMultiXactId;
@@ -5281,6 +5283,10 @@ BootStrapXLOG(uint32 data_checksum_version)
 	TransamVariables->nextXid = checkPoint.nextXid;
 	TransamVariables->nextOid = checkPoint.nextOid;
 	TransamVariables->oidCount = 0;
+
+	ToastCounter->nextId = checkPoint.nextToastId;
+	ToastCounter->idCount = 0;
+
 	MultiXactSetNextMXact(checkPoint.nextMulti, checkPoint.nextMultiOffset);
 	AdvanceOldestClogXid(checkPoint.oldestXid);
 	SetTransactionIdLimit(checkPoint.oldestXid, checkPoint.oldestXidDB);
@@ -5757,6 +5763,8 @@ StartupXLOG(void)
 	TransamVariables->nextXid = checkPoint.nextXid;
 	TransamVariables->nextOid = checkPoint.nextOid;
 	TransamVariables->oidCount = 0;
+	ToastCounter->nextId = checkPoint.nextToastId;
+	ToastCounter->idCount = 0;
 	MultiXactSetNextMXact(checkPoint.nextMulti, checkPoint.nextMultiOffset);
 	AdvanceOldestClogXid(checkPoint.oldestXid);
 	SetTransactionIdLimit(checkPoint.oldestXid, checkPoint.oldestXidDB);
@@ -7299,6 +7307,12 @@ CreateCheckPoint(int flags)
 		checkPoint.nextOid += TransamVariables->oidCount;
 	LWLockRelease(OidGenLock);
 
+	LWLockAcquire(ToastIdGenLock, LW_SHARED);
+	checkPoint.nextToastId = ToastCounter->nextId;
+	if (!shutdown)
+		checkPoint.nextToastId += ToastCounter->idCount;
+	LWLockRelease(ToastIdGenLock);
+
 	MultiXactGetCheckptMulti(shutdown,
 							 &checkPoint.nextMulti,
 							 &checkPoint.nextMultiOffset,
@@ -8238,6 +8252,22 @@ XLogPutNextOid(Oid nextOid)
 	 */
 }
 
+/*
+ * Write a NEXT_TOAST_ID log record.
+ */
+void
+XLogPutNextToastId(uint64 nextId)
+{
+	XLogBeginInsert();
+	XLogRegisterData(&nextId, sizeof(uint64));
+	(void) XLogInsert(RM_XLOG_ID, XLOG_NEXT_TOAST_ID);
+
+	/*
+	 * The next TOAST value ID is not flushed immediately, for the same reason
+	 * as above for the OIDs in XLogPutNextOid().
+	 */
+}
+
 /*
  * Write an XLOG SWITCH record.
  *
@@ -8453,6 +8483,16 @@ xlog_redo(XLogReaderState *record)
 		TransamVariables->oidCount = 0;
 		LWLockRelease(OidGenLock);
 	}
+	else if (info == XLOG_NEXT_TOAST_ID)
+	{
+		uint64		nextToastId;
+
+		memcpy(&nextToastId, XLogRecGetData(record), sizeof(uint64));
+		LWLockAcquire(ToastIdGenLock, LW_EXCLUSIVE);
+		ToastCounter->nextId = nextToastId;
+		ToastCounter->idCount = 0;
+		LWLockRelease(ToastIdGenLock);
+	}
 	else if (info == XLOG_CHECKPOINT_SHUTDOWN)
 	{
 		CheckPoint	checkPoint;
@@ -8467,6 +8507,10 @@ xlog_redo(XLogReaderState *record)
 		TransamVariables->nextOid = checkPoint.nextOid;
 		TransamVariables->oidCount = 0;
 		LWLockRelease(OidGenLock);
+		LWLockAcquire(ToastIdGenLock, LW_EXCLUSIVE);
+		ToastCounter->nextId = checkPoint.nextToastId;
+		ToastCounter->idCount = 0;
+		LWLockRelease(ToastIdGenLock);
 		MultiXactSetNextMXact(checkPoint.nextMulti,
 							  checkPoint.nextMultiOffset);
 
diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.c
index cc03f0706e9c..bb0337d37201 100644
--- a/src/backend/replication/logical/decode.c
+++ b/src/backend/replication/logical/decode.c
@@ -188,6 +188,7 @@ xlog_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
 		case XLOG_FPI:
 		case XLOG_OVERWRITE_CONTRECORD:
 		case XLOG_CHECKPOINT_REDO:
+		case XLOG_NEXT_TOAST_ID:
 			break;
 		default:
 			elog(ERROR, "unexpected RM_XLOG_ID record type: %u", info);
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index 2fa045e6b0f6..9102c267d7b0 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -20,6 +20,7 @@
 #include "access/nbtree.h"
 #include "access/subtrans.h"
 #include "access/syncscan.h"
+#include "access/toast_counter.h"
 #include "access/transam.h"
 #include "access/twophase.h"
 #include "access/xlogprefetcher.h"
@@ -119,6 +120,7 @@ CalculateShmemSize(int *num_semaphores)
 	size = add_size(size, ProcGlobalShmemSize());
 	size = add_size(size, XLogPrefetchShmemSize());
 	size = add_size(size, VarsupShmemSize());
+	size = add_size(size, ToastCounterShmemSize());
 	size = add_size(size, XLOGShmemSize());
 	size = add_size(size, XLogRecoveryShmemSize());
 	size = add_size(size, CLOGShmemSize());
@@ -280,8 +282,9 @@ CreateOrAttachShmemStructs(void)
 	DSMRegistryShmemInit();
 
 	/*
-	 * Set up xlog, clog, and buffers
+	 * Set up TOAST counter, xlog, clog, and buffers
 	 */
+	ToastCounterShmemInit();
 	VarsupShmemInit();
 	XLOGShmemInit();
 	XLogPrefetchShmemInit();
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 4da68312b5f9..9aa44de58770 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -352,6 +352,7 @@ DSMRegistry	"Waiting to read or update the dynamic shared memory registry."
 InjectionPoint	"Waiting to read or update information related to injection points."
 SerialControl	"Waiting to read or update shared <filename>pg_serial</filename> state."
 AioWorkerSubmissionQueue	"Waiting to access AIO worker submission queue."
+ToastIdGen	"Waiting to allocate a new TOAST value ID."
 
 #
 # END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE)
diff --git a/src/backend/utils/misc/pg_controldata.c b/src/backend/utils/misc/pg_controldata.c
index 6d036e3bf328..e4abf8593b8d 100644
--- a/src/backend/utils/misc/pg_controldata.c
+++ b/src/backend/utils/misc/pg_controldata.c
@@ -69,8 +69,8 @@ pg_control_system(PG_FUNCTION_ARGS)
 Datum
 pg_control_checkpoint(PG_FUNCTION_ARGS)
 {
-	Datum		values[18];
-	bool		nulls[18];
+	Datum		values[19];
+	bool		nulls[19];
 	TupleDesc	tupdesc;
 	HeapTuple	htup;
 	ControlFileData *ControlFile;
@@ -130,30 +130,33 @@ pg_control_checkpoint(PG_FUNCTION_ARGS)
 	values[9] = TransactionIdGetDatum(ControlFile->checkPointCopy.nextMultiOffset);
 	nulls[9] = false;
 
-	values[10] = TransactionIdGetDatum(ControlFile->checkPointCopy.oldestXid);
+	values[10] = UInt64GetDatum(ControlFile->checkPointCopy.nextToastId);
 	nulls[10] = false;
 
-	values[11] = ObjectIdGetDatum(ControlFile->checkPointCopy.oldestXidDB);
+	values[11] = TransactionIdGetDatum(ControlFile->checkPointCopy.oldestXid);
 	nulls[11] = false;
 
-	values[12] = TransactionIdGetDatum(ControlFile->checkPointCopy.oldestActiveXid);
+	values[12] = ObjectIdGetDatum(ControlFile->checkPointCopy.oldestXidDB);
 	nulls[12] = false;
 
-	values[13] = TransactionIdGetDatum(ControlFile->checkPointCopy.oldestMulti);
+	values[13] = TransactionIdGetDatum(ControlFile->checkPointCopy.oldestActiveXid);
 	nulls[13] = false;
 
-	values[14] = ObjectIdGetDatum(ControlFile->checkPointCopy.oldestMultiDB);
+	values[14] = TransactionIdGetDatum(ControlFile->checkPointCopy.oldestMulti);
 	nulls[14] = false;
 
-	values[15] = TransactionIdGetDatum(ControlFile->checkPointCopy.oldestCommitTsXid);
+	values[15] = ObjectIdGetDatum(ControlFile->checkPointCopy.oldestMultiDB);
 	nulls[15] = false;
 
-	values[16] = TransactionIdGetDatum(ControlFile->checkPointCopy.newestCommitTsXid);
+	values[16] = TransactionIdGetDatum(ControlFile->checkPointCopy.oldestCommitTsXid);
 	nulls[16] = false;
 
-	values[17] = TimestampTzGetDatum(time_t_to_timestamptz(ControlFile->checkPointCopy.time));
+	values[17] = TransactionIdGetDatum(ControlFile->checkPointCopy.newestCommitTsXid);
 	nulls[17] = false;
 
+	values[18] = TimestampTzGetDatum(time_t_to_timestamptz(ControlFile->checkPointCopy.time));
+	nulls[18] = false;
+
 	htup = heap_form_tuple(tupdesc, values, nulls);
 
 	PG_RETURN_DATUM(HeapTupleGetDatum(htup));
diff --git a/src/bin/pg_controldata/pg_controldata.c b/src/bin/pg_controldata/pg_controldata.c
index 7bb801bb8861..d83368ba4910 100644
--- a/src/bin/pg_controldata/pg_controldata.c
+++ b/src/bin/pg_controldata/pg_controldata.c
@@ -266,6 +266,8 @@ main(int argc, char *argv[])
 		   ControlFile->checkPointCopy.nextMulti);
 	printf(_("Latest checkpoint's NextMultiOffset:  %u\n"),
 		   ControlFile->checkPointCopy.nextMultiOffset);
+	printf(_("Latest checkpoint's NextToastID:      %" PRIu64 "\n"),
+		   ControlFile->checkPointCopy.nextToastId);
 	printf(_("Latest checkpoint's oldestXID:        %u\n"),
 		   ControlFile->checkPointCopy.oldestXid);
 	printf(_("Latest checkpoint's oldestXID's DB:   %u\n"),
diff --git a/src/bin/pg_resetwal/pg_resetwal.c b/src/bin/pg_resetwal/pg_resetwal.c
index e876f35f38ed..bb324c710911 100644
--- a/src/bin/pg_resetwal/pg_resetwal.c
+++ b/src/bin/pg_resetwal/pg_resetwal.c
@@ -45,6 +45,7 @@
 
 #include "access/heaptoast.h"
 #include "access/multixact.h"
+#include "access/toast_counter.h"
 #include "access/transam.h"
 #include "access/xlog.h"
 #include "access/xlog_internal.h"
@@ -686,6 +687,7 @@ GuessControlValues(void)
 	ControlFile.checkPointCopy.nextOid = FirstGenbkiObjectId;
 	ControlFile.checkPointCopy.nextMulti = FirstMultiXactId;
 	ControlFile.checkPointCopy.nextMultiOffset = 0;
+	ControlFile.checkPointCopy.nextToastId = FirstToastId;
 	ControlFile.checkPointCopy.oldestXid = FirstNormalTransactionId;
 	ControlFile.checkPointCopy.oldestXidDB = InvalidOid;
 	ControlFile.checkPointCopy.oldestMulti = FirstMultiXactId;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 810b2b50f0da..74bd691b0a8f 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -28157,6 +28157,11 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
        <entry><type>xid</type></entry>
       </row>
 
+      <row>
+       <entry><structfield>next_toast_id</structfield></entry>
+       <entry><type>bigint</type></entry>
+      </row>
+
       <row>
        <entry><structfield>oldest_xid</structfield></entry>
        <entry><type>xid</type></entry>
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 3b98606a1701..25ab35ea350f 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3050,6 +3050,7 @@ TmFromChar
 TmToChar
 ToastAttrInfo
 ToastCompressionId
+ToastCounterData
 ToastTupleContext
 ToastedAttribute
 TocEntry
-- 
2.50.0

v2-0008-Switch-pg_column_toast_chunk_id-return-value-from.patchtext/x-diff; charset=us-asciiDownload
From 32e2696d65fa244ed7ed007164144285cd9fe911 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 19 Jun 2025 10:11:40 +0900
Subject: [PATCH v2 08/13] Switch pg_column_toast_chunk_id() return value from
 oid to bigint

This is required for a follow-up patch that will add support for 8-byte
TOAST values, with this function being changed so as it is able to
support the largest TOAST value type available.
---
 src/include/catalog/pg_proc.dat | 2 +-
 src/backend/utils/adt/varlena.c | 2 +-
 doc/src/sgml/func.sgml          | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index ff0871037c53..822874a6a5e0 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7735,7 +7735,7 @@
   proargtypes => 'any', prosrc => 'pg_column_compression' },
 { oid => '6316', descr => 'chunk ID of on-disk TOASTed value',
   proname => 'pg_column_toast_chunk_id', provolatile => 's',
-  prorettype => 'oid', proargtypes => 'any',
+  prorettype => 'int8', proargtypes => 'any',
   prosrc => 'pg_column_toast_chunk_id' },
 { oid => '2322',
   descr => 'total disk space usage for the specified tablespace',
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index d76386407a08..26c720449f7b 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -4249,7 +4249,7 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 
 	toast_valueid = toast_external_info_get_value(attr);
 
-	PG_RETURN_OID(toast_valueid);
+	PG_RETURN_UINT64(toast_valueid);
 }
 
 /*
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 74bd691b0a8f..cab7ef816b6e 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30121,7 +30121,7 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
          <primary>pg_column_toast_chunk_id</primary>
         </indexterm>
         <function>pg_column_toast_chunk_id</function> ( <type>"any"</type> )
-        <returnvalue>oid</returnvalue>
+        <returnvalue>bigint</returnvalue>
        </para>
        <para>
         Shows the <structfield>chunk_id</structfield> of an on-disk
-- 
2.50.0

v2-0009-Add-support-for-bigint-TOAST-values.patchtext/x-diff; charset=us-asciiDownload
From e29166a2c02eac057743af07fa7cf095f44af608 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 8 Jul 2025 07:55:12 +0900
Subject: [PATCH v2 09/13] Add support for bigint TOAST values

This commit adds the possibility to define TOAST tables with bigint as
value ID, baesd on the GUC default_toast_type.  All the external TOAST
pointers still rely on varatt_external and a single vartag, with all the
values inserted in the bigint TOAST tables fed from the existing OID
value generator.  This will be changed in an upcoming patch that adds
more vartag_external types and its associated structures, with the code
being able to use a different external TOAST pointer depending on the
attribute type of chunk_id in TOAST relations.

All the changes done here are mechanical, with all the TOAST code able
to do chunk ID lookups based on the two types supported.

XXX: Catalog version bump required.
---
 src/backend/access/common/toast_internals.c | 68 +++++++++++++++------
 src/backend/access/heap/heaptoast.c         | 20 ++++--
 src/backend/catalog/toasting.c              | 37 ++++++++++-
 doc/src/sgml/storage.sgml                   |  7 ++-
 contrib/amcheck/verify_heapam.c             | 19 ++++--
 5 files changed, 121 insertions(+), 30 deletions(-)

diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 66dfedefc579..28e2867f8209 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -18,6 +18,7 @@
 #include "access/heapam.h"
 #include "access/heaptoast.h"
 #include "access/table.h"
+#include "access/toast_counter.h"
 #include "access/toast_external.h"
 #include "access/toast_internals.h"
 #include "access/xact.h"
@@ -146,6 +147,7 @@ toast_save_datum(Relation rel, Datum value,
 	int			validIndex;
 	const toast_external_info *info;
 	uint8		tag = VARTAG_INDIRECT;	/* init value does not matter */
+	Oid			toast_typid;
 
 	Assert(!VARATT_IS_EXTERNAL(value));
 
@@ -166,6 +168,9 @@ toast_save_datum(Relation rel, Datum value,
 	tag = VARTAG_ONDISK_OID;
 	info = toast_external_get_info(tag);
 
+	toast_typid = TupleDescAttr(toasttupDesc, 0)->atttypid;
+	Assert(toast_typid == OIDOID || toast_typid == INT8OID);
+
 	/* Open all the toast indexes and look for the valid one */
 	validIndex = toast_open_indexes(toastrel,
 									RowExclusiveLock,
@@ -237,20 +242,23 @@ toast_save_datum(Relation rel, Datum value,
 		toast_pointer.toastrelid = RelationGetRelid(toastrel);
 
 	/*
-	 * Choose an OID to use as the value ID for this toast value.
+	 * Choose a new value to use as the value ID for this toast value, be it
+	 * for OID or int8-based TOAST relations.
 	 *
-	 * Normally we just choose an unused OID within the toast table.  But
+	 * Normally we just choose an unused value within the toast table.  But
 	 * during table-rewriting operations where we are preserving an existing
-	 * toast table OID, we want to preserve toast value OIDs too.  So, if
+	 * toast table OID, we want to preserve toast value IDs too.  So, if
 	 * rd_toastoid is set and we had a prior external value from that same
 	 * toast table, re-use its value ID.  If we didn't have a prior external
 	 * value (which is a corner case, but possible if the table's attstorage
 	 * options have been changed), we have to pick a value ID that doesn't
-	 * conflict with either new or existing toast value OIDs.
+	 * conflict with either new or existing toast value IDs.  If the TOAST
+	 * table uses 8-byte value IDs, we should not really care much about
+	 * that.
 	 */
 	if (!OidIsValid(rel->rd_toastoid))
 	{
-		/* normal case: just choose an unused OID */
+		/* normal case: just choose an unused ID */
 		toast_pointer.value =
 			info->get_new_value(toastrel,
 								RelationGetRelid(toastidxs[validIndex]),
@@ -269,7 +277,7 @@ toast_save_datum(Relation rel, Datum value,
 
 			if (old_toast_pointer.toastrelid == rel->rd_toastoid)
 			{
-				/* This value came from the old toast table; reuse its OID */
+				/* This value came from the old toast table; reuse its ID */
 				toast_pointer.value = old_toast_pointer.value;
 
 				/*
@@ -300,8 +308,8 @@ toast_save_datum(Relation rel, Datum value,
 		if (toast_pointer.value == InvalidToastId)
 		{
 			/*
-			 * new value; must choose an OID that doesn't conflict in either
-			 * old or new toast table
+			 * new value; must choose a value that doesn't conflict in either
+			 * old or new toast table.
 			 */
 			do
 			{
@@ -317,7 +325,10 @@ toast_save_datum(Relation rel, Datum value,
 	/*
 	 * Initialize constant parts of the tuple data
 	 */
-	t_values[0] = ObjectIdGetDatum(toast_pointer.value);
+	if (toast_typid == OIDOID)
+		t_values[0] = ObjectIdGetDatum(toast_pointer.value);
+	else if (toast_typid == INT8OID)
+		t_values[0] = Int64GetDatum(toast_pointer.value);
 	t_values[2] = PointerGetDatum(&chunk_data);
 	t_isnull[0] = false;
 	t_isnull[1] = false;
@@ -416,6 +427,7 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	HeapTuple	toasttup;
 	int			num_indexes;
 	int			validIndex;
+	Oid			toast_typid;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		return;
@@ -427,6 +439,8 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	 * Open the toast relation and its indexes
 	 */
 	toastrel = table_open(toast_pointer.toastrelid, RowExclusiveLock);
+	toast_typid = TupleDescAttr(toastrel->rd_att, 0)->atttypid;
+	Assert(toast_typid == OIDOID || toast_typid == INT8OID);
 
 	/* Fetch valid relation used for process */
 	validIndex = toast_open_indexes(toastrel,
@@ -437,10 +451,18 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	/*
 	 * Setup a scan key to find chunks with matching va_valueid
 	 */
-	ScanKeyInit(&toastkey,
-				(AttrNumber) 1,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(toast_pointer.value));
+	if (toast_typid == OIDOID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(toast_pointer.value));
+	else if (toast_typid == INT8OID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_INT8EQ,
+					Int64GetDatum(toast_pointer.value));
+	else
+		Assert(false);
 
 	/*
 	 * Find all the chunks.  (We don't actually care whether we see them in
@@ -487,6 +509,7 @@ toastrel_valueid_exists(Relation toastrel, uint64 valueid)
 	int			num_indexes;
 	int			validIndex;
 	Relation   *toastidxs;
+	Oid			toast_typid;
 
 	/* Fetch a valid index relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -494,13 +517,24 @@ toastrel_valueid_exists(Relation toastrel, uint64 valueid)
 									&toastidxs,
 									&num_indexes);
 
+	toast_typid = TupleDescAttr(toastrel->rd_att, 0)->atttypid;
+	Assert(toast_typid == OIDOID || toast_typid == INT8OID);
+
 	/*
 	 * Setup a scan key to find chunks with matching va_valueid
 	 */
-	ScanKeyInit(&toastkey,
-				(AttrNumber) 1,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(valueid));
+	if (toast_typid == OIDOID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(valueid));
+	else if (toast_typid == INT8OID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_INT8EQ,
+					Int64GetDatum(valueid));
+	else
+		Assert(false);
 
 	/*
 	 * Is there any such chunk?
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index b3a63c10aef3..7a54cf7fae34 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -654,6 +654,7 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 	int32		max_chunk_size;
 	const toast_external_info *info;
 	uint8		tag = VARTAG_INDIRECT;  /* init value does not matter */
+	Oid			toast_typid;
 
 	/* Look for the valid index of toast relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -677,16 +678,27 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 
 	max_chunk_size = info->maximum_chunk_size;
 
+	toast_typid = TupleDescAttr(toastrel->rd_att, 0)->atttypid;
+	Assert(toast_typid == OIDOID || toast_typid == INT8OID);
+
 	totalchunks = ((attrsize - 1) / max_chunk_size) + 1;
 	startchunk = sliceoffset / max_chunk_size;
 	endchunk = (sliceoffset + slicelength - 1) / max_chunk_size;
 	Assert(endchunk <= totalchunks);
 
 	/* Set up a scan key to fetch from the index. */
-	ScanKeyInit(&toastkey[0],
-				(AttrNumber) 1,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(valueid));
+	if (toast_typid == OIDOID)
+		ScanKeyInit(&toastkey[0],
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(valueid));
+	else if (toast_typid == INT8OID)
+		ScanKeyInit(&toastkey[0],
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_INT8EQ,
+					Int64GetDatum(valueid));
+	else
+		Assert(false);
 
 	/*
 	 * No additional condition if fetching all chunks. Otherwise, use an
diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c
index e595cb61b375..3df83c9835d4 100644
--- a/src/backend/catalog/toasting.c
+++ b/src/backend/catalog/toasting.c
@@ -149,6 +149,7 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	int16		coloptions[2];
 	ObjectAddress baseobject,
 				toastobject;
+	Oid			toast_typid = InvalidOid;
 
 	/*
 	 * Is it already toasted?
@@ -204,11 +205,40 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	snprintf(toast_idxname, sizeof(toast_idxname),
 			 "pg_toast_%u_index", relOid);
 
+	/*
+	 * Determine the type OID to use for the value.  If OIDOldToast is
+	 * defined, we need to rely on the existing table for the job because
+	 * we do not want to create an inconsistent relation that would conflict
+	 * with the parent and break the world.
+	 */
+	if (!OidIsValid(OIDOldToast))
+	{
+		if (default_toast_type == TOAST_TYPE_OID)
+			toast_typid = OIDOID;
+		else if (default_toast_type == TOAST_TYPE_INT8)
+			toast_typid = INT8OID;
+		else
+			Assert(false);
+	}
+	else
+	{
+		HeapTuple	tuple;
+		Form_pg_attribute atttoast;
+
+		/* For the chunk_id type. */
+		tuple = SearchSysCacheAttNum(OIDOldToast, 1);
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for relation %u", OIDOldToast);
+		atttoast = (Form_pg_attribute) GETSTRUCT(tuple);
+		toast_typid = atttoast->atttypid;
+		ReleaseSysCache(tuple);
+	}
+
 	/* this is pretty painful...  need a tuple descriptor */
 	tupdesc = CreateTemplateTupleDesc(3);
 	TupleDescInitEntry(tupdesc, (AttrNumber) 1,
 					   "chunk_id",
-					   OIDOID,
+					   toast_typid,
 					   -1, 0);
 	TupleDescInitEntry(tupdesc, (AttrNumber) 2,
 					   "chunk_seq",
@@ -316,7 +346,10 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	collationIds[0] = InvalidOid;
 	collationIds[1] = InvalidOid;
 
-	opclassIds[0] = OID_BTREE_OPS_OID;
+	if (toast_typid == OIDOID)
+		opclassIds[0] = OID_BTREE_OPS_OID;
+	else if (toast_typid == INT8OID)
+		opclassIds[0] = INT8_BTREE_OPS_OID;
 	opclassIds[1] = INT4_BTREE_OPS_OID;
 
 	coloptions[0] = 0;
diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml
index f3c6cd8860b5..564783a1c559 100644
--- a/doc/src/sgml/storage.sgml
+++ b/doc/src/sgml/storage.sgml
@@ -419,14 +419,15 @@ most <symbol>TOAST_MAX_CHUNK_SIZE_OID</symbol> bytes (by default this value is c
 so that four chunk rows will fit on a page, making it about 2000 bytes).
 Each chunk is stored as a separate row in the <acronym>TOAST</acronym> table
 belonging to the owning table.  Every
-<acronym>TOAST</acronym> table has the columns <structfield>chunk_id</structfield> (an OID
-identifying the particular <acronym>TOAST</acronym>ed value),
+<acronym>TOAST</acronym> table has the columns
+<structfield>chunk_id</structfield> (an OID or an 8-byte integer identifying
+the particular <acronym>TOAST</acronym>ed value),
 <structfield>chunk_seq</structfield> (a sequence number for the chunk within its value),
 and <structfield>chunk_data</structfield> (the actual data of the chunk).  A unique index
 on <structfield>chunk_id</structfield> and <structfield>chunk_seq</structfield> provides fast
 retrieval of the values.  A pointer datum representing an out-of-line on-disk
 <acronym>TOAST</acronym>ed value therefore needs to store the OID of the
-<acronym>TOAST</acronym> table in which to look and the OID of the specific value
+<acronym>TOAST</acronym> table in which to look and the specific value
 (its <structfield>chunk_id</structfield>).  For convenience, pointer datums also store the
 logical datum size (original uncompressed data length), physical stored size
 (different if compression was applied), and the compression method used, if
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 11c4507ae6e2..833811c75437 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1880,6 +1880,9 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	int32		last_chunk_seq;
 	uint64		toast_valueid;
 	int32		max_chunk_size;
+	Oid			toast_typid;
+
+	toast_typid = TupleDescAttr(ctx->toast_rel->rd_att, 0)->atttypid;
 
 	extsize = ta->toast_pointer.extsize;
 
@@ -1889,10 +1892,18 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	/*
 	 * Setup a scan key to find chunks in toast table with matching va_valueid
 	 */
-	ScanKeyInit(&toastkey,
-				(AttrNumber) 1,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(ta->toast_pointer.value));
+	if (toast_typid == OIDOID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(ta->toast_pointer.value));
+	else if (toast_typid == INT8OID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_INT8EQ,
+					Int64GetDatum(ta->toast_pointer.value));
+	else
+		Assert(false);
 
 	/*
 	 * Check if any chunks for this toasted object exist in the toast table,
-- 
2.50.0

v2-0010-Add-tests-for-TOAST-relations-with-bigint-as-valu.patchtext/x-diff; charset=us-asciiDownload
From 140e50d769a0cb2183252d378c5836ef08c55f1f Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 19 Jun 2025 11:15:48 +0900
Subject: [PATCH v2 10/13] Add tests for TOAST relations with bigint as value
 type

This adds coverage for relations created with default_toast_type =
'int8', for external TOAST pointers both compressed and uncompressed.
---
 src/test/regress/expected/strings.out | 238 ++++++++++++++++++++++----
 src/test/regress/sql/strings.sql      | 142 +++++++++++----
 2 files changed, 305 insertions(+), 75 deletions(-)

diff --git a/src/test/regress/expected/strings.out b/src/test/regress/expected/strings.out
index 788844abd20e..0dd34808a673 100644
--- a/src/test/regress/expected/strings.out
+++ b/src/test/regress/expected/strings.out
@@ -1933,21 +1933,40 @@ SELECT text 'text' || varchar ' and varchar' AS "Concat text to varchar";
 (1 row)
 
 --
--- test substr with toasted text values
+-- test substr with toasted text values, for all types of TOAST relations
+-- supported.
 --
-CREATE TABLE toasttest(f1 text);
-insert into toasttest values(repeat('1234567890',10000));
-insert into toasttest values(repeat('1234567890',10000));
+SET default_toast_type = 'oid';
+CREATE TABLE toasttest_oid(f1 text);
+SET default_toast_type = 'int8';
+CREATE TABLE toasttest_int8(f1 text);
+RESET default_toast_type;
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_int8 values(repeat('1234567890',10000));
+insert into toasttest_int8 values(repeat('1234567890',10000));
 --
 -- Ensure that some values are uncompressed, to test the faster substring
 -- operation used in that case
 --
-alter table toasttest alter column f1 set storage external;
-insert into toasttest values(repeat('1234567890',10000));
-insert into toasttest values(repeat('1234567890',10000));
+alter table toasttest_oid alter column f1 set storage external;
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+alter table toasttest_int8 alter column f1 set storage external;
+insert into toasttest_int8 values(repeat('1234567890',10000));
+insert into toasttest_int8 values(repeat('1234567890',10000));
 -- If the starting position is zero or less, then return from the start of the string
 -- adjusting the length to be consistent with the "negative start" per SQL.
-SELECT substr(f1, -1, 5) from toasttest;
+SELECT substr(f1, -1, 5) from toasttest_oid;
+ substr 
+--------
+ 123
+ 123
+ 123
+ 123
+(4 rows)
+
+SELECT substr(f1, -1, 5) from toasttest_int8;
  substr 
 --------
  123
@@ -1957,11 +1976,22 @@ SELECT substr(f1, -1, 5) from toasttest;
 (4 rows)
 
 -- If the length is less than zero, an ERROR is thrown.
-SELECT substr(f1, 5, -1) from toasttest;
+SELECT substr(f1, 5, -1) from toasttest_oid;
+ERROR:  negative substring length not allowed
+SELECT substr(f1, 5, -1) from toasttest_int8;
 ERROR:  negative substring length not allowed
 -- If no third argument (length) is provided, the length to the end of the
 -- string is assumed.
-SELECT substr(f1, 99995) from toasttest;
+SELECT substr(f1, 99995) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995) from toasttest_int8;
  substr 
 --------
  567890
@@ -1972,7 +2002,7 @@ SELECT substr(f1, 99995) from toasttest;
 
 -- If start plus length is > string length, the result is truncated to
 -- string length
-SELECT substr(f1, 99995, 10) from toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
  substr 
 --------
  567890
@@ -1981,50 +2011,108 @@ SELECT substr(f1, 99995, 10) from toasttest;
  567890
 (4 rows)
 
-TRUNCATE TABLE toasttest;
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
+SELECT substr(f1, 99995, 10) from toasttest_int8;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+-- TRUNCATE cases for TOAST relations with OID values.
+TRUNCATE TABLE toasttest_oid;
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
 -- expect >0 blocks
 SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
-  FROM pg_class where relname = 'toasttest';
+  FROM pg_class where relname = 'toasttest_oid';
  is_empty 
 ----------
  f
 (1 row)
 
-TRUNCATE TABLE toasttest;
-ALTER TABLE toasttest set (toast_tuple_target = 4080);
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
+TRUNCATE TABLE toasttest_oid;
+ALTER TABLE toasttest_oid set (toast_tuple_target = 4080);
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
 -- expect 0 blocks
 SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
-  FROM pg_class where relname = 'toasttest';
+  FROM pg_class where relname = 'toasttest_oid';
  is_empty 
 ----------
  t
 (1 row)
 
-DROP TABLE toasttest;
+DROP TABLE toasttest_oid;
+-- TRUNCATE cases for TOAST relation with int8 values.
+TRUNCATE TABLE toasttest_int8;
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+-- expect >0 blocks
+SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
+  FROM pg_class where relname = 'toasttest_int8';
+ is_empty 
+----------
+ f
+(1 row)
+
+TRUNCATE TABLE toasttest_int8;
+ALTER TABLE toasttest_int8 set (toast_tuple_target = 4080);
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+-- expect 0 blocks
+SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
+  FROM pg_class where relname = 'toasttest_int8';
+ is_empty 
+----------
+ t
+(1 row)
+
+DROP TABLE toasttest_int8;
 --
--- test substr with toasted bytea values
+-- test substr with toasted bytea values, for all types of TOAST relations
+-- supported.
 --
-CREATE TABLE toasttest(f1 bytea);
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
+SET default_toast_type = 'oid';
+CREATE TABLE toasttest_oid(f1 bytea);
+SET default_toast_type = 'int8';
+CREATE TABLE toasttest_int8(f1 bytea);
+RESET default_toast_type;
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_int8 values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_int8 values(decode(repeat('1234567890',10000),'escape'));
 --
 -- Ensure that some values are uncompressed, to test the faster substring
 -- operation used in that case
 --
-alter table toasttest alter column f1 set storage external;
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
+alter table toasttest_oid alter column f1 set storage external;
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+alter table toasttest_int8 alter column f1 set storage external;
+insert into toasttest_int8 values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_int8 values(decode(repeat('1234567890',10000),'escape'));
 -- If the starting position is zero or less, then return from the start of the string
 -- adjusting the length to be consistent with the "negative start" per SQL.
-SELECT substr(f1, -1, 5) from toasttest;
+SELECT substr(f1, -1, 5) from toasttest_oid;
+ substr 
+--------
+ 123
+ 123
+ 123
+ 123
+(4 rows)
+
+SELECT substr(f1, -1, 5) from toasttest_int8;
  substr 
 --------
  123
@@ -2034,11 +2122,22 @@ SELECT substr(f1, -1, 5) from toasttest;
 (4 rows)
 
 -- If the length is less than zero, an ERROR is thrown.
-SELECT substr(f1, 5, -1) from toasttest;
+SELECT substr(f1, 5, -1) from toasttest_oid;
+ERROR:  negative substring length not allowed
+SELECT substr(f1, 5, -1) from toasttest_int8;
 ERROR:  negative substring length not allowed
 -- If no third argument (length) is provided, the length to the end of the
 -- string is assumed.
-SELECT substr(f1, 99995) from toasttest;
+SELECT substr(f1, 99995) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995) from toasttest_int8;
  substr 
 --------
  567890
@@ -2049,7 +2148,7 @@ SELECT substr(f1, 99995) from toasttest;
 
 -- If start plus length is > string length, the result is truncated to
 -- string length
-SELECT substr(f1, 99995, 10) from toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
  substr 
 --------
  567890
@@ -2058,7 +2157,72 @@ SELECT substr(f1, 99995, 10) from toasttest;
  567890
 (4 rows)
 
-DROP TABLE toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_int8;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+-- A relation rewrite leaves the TOAST value attributes unchanged.
+VACUUM FULL toasttest_oid;
+VACUUM FULL toasttest_int8;
+SELECT c1.relname, a.atttypid::regtype
+  FROM pg_attribute AS a,
+       pg_class AS c1,
+       pg_class AS c2
+  WHERE
+       c1.relname IN ('toasttest_oid', 'toasttest_int8') AND
+       c1.reltoastrelid = c2.oid AND
+       a.attrelid = c2.oid AND
+       a.attname = 'chunk_id'
+  ORDER BY c1.relname COLLATE "C";
+    relname     | atttypid 
+----------------+----------
+ toasttest_int8 | bigint
+ toasttest_oid  | oid
+(2 rows)
+
+-- Check that data slices are still accessible.
+SELECT substr(f1, 99995) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995) from toasttest_int8;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995, 10) from toasttest_int8;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+DROP TABLE toasttest_oid, toasttest_int8;
 -- test internally compressing datums
 -- this tests compressing a datum to a very small size which exercises a
 -- corner case in packed-varlena handling: even though small, the compressed
diff --git a/src/test/regress/sql/strings.sql b/src/test/regress/sql/strings.sql
index 2577a42987de..49b4163493c8 100644
--- a/src/test/regress/sql/strings.sql
+++ b/src/test/regress/sql/strings.sql
@@ -551,89 +551,155 @@ SELECT text 'text' || char(20) ' and characters' AS "Concat text to char";
 SELECT text 'text' || varchar ' and varchar' AS "Concat text to varchar";
 
 --
--- test substr with toasted text values
+-- test substr with toasted text values, for all types of TOAST relations
+-- supported.
 --
-CREATE TABLE toasttest(f1 text);
+SET default_toast_type = 'oid';
+CREATE TABLE toasttest_oid(f1 text);
+SET default_toast_type = 'int8';
+CREATE TABLE toasttest_int8(f1 text);
+RESET default_toast_type;
 
-insert into toasttest values(repeat('1234567890',10000));
-insert into toasttest values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_int8 values(repeat('1234567890',10000));
+insert into toasttest_int8 values(repeat('1234567890',10000));
 
 --
 -- Ensure that some values are uncompressed, to test the faster substring
 -- operation used in that case
 --
-alter table toasttest alter column f1 set storage external;
-insert into toasttest values(repeat('1234567890',10000));
-insert into toasttest values(repeat('1234567890',10000));
+alter table toasttest_oid alter column f1 set storage external;
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+alter table toasttest_int8 alter column f1 set storage external;
+insert into toasttest_int8 values(repeat('1234567890',10000));
+insert into toasttest_int8 values(repeat('1234567890',10000));
 
 -- If the starting position is zero or less, then return from the start of the string
 -- adjusting the length to be consistent with the "negative start" per SQL.
-SELECT substr(f1, -1, 5) from toasttest;
+SELECT substr(f1, -1, 5) from toasttest_oid;
+SELECT substr(f1, -1, 5) from toasttest_int8;
 
 -- If the length is less than zero, an ERROR is thrown.
-SELECT substr(f1, 5, -1) from toasttest;
+SELECT substr(f1, 5, -1) from toasttest_oid;
+SELECT substr(f1, 5, -1) from toasttest_int8;
 
 -- If no third argument (length) is provided, the length to the end of the
 -- string is assumed.
-SELECT substr(f1, 99995) from toasttest;
+SELECT substr(f1, 99995) from toasttest_oid;
+SELECT substr(f1, 99995) from toasttest_int8;
 
 -- If start plus length is > string length, the result is truncated to
 -- string length
-SELECT substr(f1, 99995, 10) from toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+SELECT substr(f1, 99995, 10) from toasttest_int8;
 
-TRUNCATE TABLE toasttest;
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
+-- TRUNCATE cases for TOAST relations with OID values.
+TRUNCATE TABLE toasttest_oid;
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
 -- expect >0 blocks
 SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
-  FROM pg_class where relname = 'toasttest';
-
-TRUNCATE TABLE toasttest;
-ALTER TABLE toasttest set (toast_tuple_target = 4080);
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
+  FROM pg_class where relname = 'toasttest_oid';
+TRUNCATE TABLE toasttest_oid;
+ALTER TABLE toasttest_oid set (toast_tuple_target = 4080);
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
 -- expect 0 blocks
 SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
-  FROM pg_class where relname = 'toasttest';
+  FROM pg_class where relname = 'toasttest_oid';
+DROP TABLE toasttest_oid;
 
-DROP TABLE toasttest;
+-- TRUNCATE cases for TOAST relation with int8 values.
+TRUNCATE TABLE toasttest_int8;
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+-- expect >0 blocks
+SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
+  FROM pg_class where relname = 'toasttest_int8';
+TRUNCATE TABLE toasttest_int8;
+ALTER TABLE toasttest_int8 set (toast_tuple_target = 4080);
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+-- expect 0 blocks
+SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
+  FROM pg_class where relname = 'toasttest_int8';
+DROP TABLE toasttest_int8;
 
 --
--- test substr with toasted bytea values
+-- test substr with toasted bytea values, for all types of TOAST relations
+-- supported.
 --
-CREATE TABLE toasttest(f1 bytea);
+SET default_toast_type = 'oid';
+CREATE TABLE toasttest_oid(f1 bytea);
+SET default_toast_type = 'int8';
+CREATE TABLE toasttest_int8(f1 bytea);
+RESET default_toast_type;
 
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_int8 values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_int8 values(decode(repeat('1234567890',10000),'escape'));
 
 --
 -- Ensure that some values are uncompressed, to test the faster substring
 -- operation used in that case
 --
-alter table toasttest alter column f1 set storage external;
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
+alter table toasttest_oid alter column f1 set storage external;
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+alter table toasttest_int8 alter column f1 set storage external;
+insert into toasttest_int8 values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_int8 values(decode(repeat('1234567890',10000),'escape'));
 
 -- If the starting position is zero or less, then return from the start of the string
 -- adjusting the length to be consistent with the "negative start" per SQL.
-SELECT substr(f1, -1, 5) from toasttest;
+SELECT substr(f1, -1, 5) from toasttest_oid;
+SELECT substr(f1, -1, 5) from toasttest_int8;
 
 -- If the length is less than zero, an ERROR is thrown.
-SELECT substr(f1, 5, -1) from toasttest;
+SELECT substr(f1, 5, -1) from toasttest_oid;
+SELECT substr(f1, 5, -1) from toasttest_int8;
 
 -- If no third argument (length) is provided, the length to the end of the
 -- string is assumed.
-SELECT substr(f1, 99995) from toasttest;
+SELECT substr(f1, 99995) from toasttest_oid;
+SELECT substr(f1, 99995) from toasttest_int8;
 
 -- If start plus length is > string length, the result is truncated to
 -- string length
-SELECT substr(f1, 99995, 10) from toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+SELECT substr(f1, 99995, 10) from toasttest_int8;
 
-DROP TABLE toasttest;
+-- A relation rewrite leaves the TOAST value attributes unchanged.
+VACUUM FULL toasttest_oid;
+VACUUM FULL toasttest_int8;
+SELECT c1.relname, a.atttypid::regtype
+  FROM pg_attribute AS a,
+       pg_class AS c1,
+       pg_class AS c2
+  WHERE
+       c1.relname IN ('toasttest_oid', 'toasttest_int8') AND
+       c1.reltoastrelid = c2.oid AND
+       a.attrelid = c2.oid AND
+       a.attname = 'chunk_id'
+  ORDER BY c1.relname COLLATE "C";
+-- Check that data slices are still accessible.
+SELECT substr(f1, 99995) from toasttest_oid;
+SELECT substr(f1, 99995) from toasttest_int8;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+SELECT substr(f1, 99995, 10) from toasttest_int8;
+
+DROP TABLE toasttest_oid, toasttest_int8;
 
 -- test internally compressing datums
 
-- 
2.50.0

v2-0011-Add-support-for-TOAST-table-types-in-pg_dump-and-.patchtext/x-diff; charset=us-asciiDownload
From d451e6192f26ddc14cfe58f1c6668bbbe93f1a25 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 19 Jun 2025 11:51:52 +0900
Subject: [PATCH v2 11/13] Add support for TOAST table types in pg_dump and
 pg_restore

This includes the possibility to perform binary upgrades with TOAST
table types applied to a new cluster, relying on SET commands based on
default_toast_type to apply one type of TOAST table or the other.

Some tests are included, this is a pretty mechanical change.

Dump format is bumped to 1.17 due to the addition of the TOAST table
type in the custom format.
---
 src/bin/pg_dump/pg_backup.h          |  2 +
 src/bin/pg_dump/pg_backup_archiver.c | 69 +++++++++++++++++++++++++++-
 src/bin/pg_dump/pg_backup_archiver.h |  6 ++-
 src/bin/pg_dump/pg_dump.c            | 21 +++++++++
 src/bin/pg_dump/pg_dump.h            |  1 +
 src/bin/pg_dump/pg_dumpall.c         |  5 ++
 src/bin/pg_dump/pg_restore.c         |  4 ++
 src/bin/pg_dump/t/002_pg_dump.pl     | 35 ++++++++++++++
 doc/src/sgml/ref/pg_dump.sgml        | 12 +++++
 doc/src/sgml/ref/pg_dumpall.sgml     | 12 +++++
 doc/src/sgml/ref/pg_restore.sgml     | 12 +++++
 11 files changed, 177 insertions(+), 2 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index af0007fb6d2f..84dccdb0eed7 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -99,6 +99,7 @@ typedef struct _restoreOptions
 	int			noOwner;		/* Don't try to match original object owner */
 	int			noTableAm;		/* Don't issue table-AM-related commands */
 	int			noTablespace;	/* Don't issue tablespace-related commands */
+	int			noToastType;	/* Don't issue TOAST-type-related commands */
 	int			disable_triggers;	/* disable triggers during data-only
 									 * restore */
 	int			use_setsessauth;	/* Use SET SESSION AUTHORIZATION commands
@@ -192,6 +193,7 @@ typedef struct _dumpOptions
 	int			disable_triggers;
 	int			outputNoTableAm;
 	int			outputNoTablespaces;
+	int			outputNoToastType;
 	int			use_setsessauth;
 	int			enable_row_security;
 	int			load_via_partition_root;
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 197c1295d93f..574e62d53a3e 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -183,6 +183,7 @@ dumpOptionsFromRestoreOptions(RestoreOptions *ropt)
 	dopt->outputNoOwner = ropt->noOwner;
 	dopt->outputNoTableAm = ropt->noTableAm;
 	dopt->outputNoTablespaces = ropt->noTablespace;
+	dopt->outputNoToastType = ropt->noToastType;
 	dopt->disable_triggers = ropt->disable_triggers;
 	dopt->use_setsessauth = ropt->use_setsessauth;
 	dopt->disable_dollar_quoting = ropt->disable_dollar_quoting;
@@ -1247,6 +1248,7 @@ ArchiveEntry(Archive *AHX, CatalogId catalogId, DumpId dumpId,
 	newToc->namespace = opts->namespace ? pg_strdup(opts->namespace) : NULL;
 	newToc->tablespace = opts->tablespace ? pg_strdup(opts->tablespace) : NULL;
 	newToc->tableam = opts->tableam ? pg_strdup(opts->tableam) : NULL;
+	newToc->toasttype = opts->toasttype ? pg_strdup(opts->toasttype) : NULL;
 	newToc->relkind = opts->relkind;
 	newToc->owner = opts->owner ? pg_strdup(opts->owner) : NULL;
 	newToc->desc = pg_strdup(opts->description);
@@ -2407,6 +2409,7 @@ _allocAH(const char *FileSpec, const ArchiveFormat fmt,
 	AH->currSchema = NULL;		/* ditto */
 	AH->currTablespace = NULL;	/* ditto */
 	AH->currTableAm = NULL;		/* ditto */
+	AH->currToastType = NULL;		/* ditto */
 
 	AH->toc = (TocEntry *) pg_malloc0(sizeof(TocEntry));
 
@@ -2674,6 +2677,7 @@ WriteToc(ArchiveHandle *AH)
 		WriteStr(AH, te->tablespace);
 		WriteStr(AH, te->tableam);
 		WriteInt(AH, te->relkind);
+		WriteStr(AH, te->toasttype);
 		WriteStr(AH, te->owner);
 		WriteStr(AH, "false");
 
@@ -2782,6 +2786,9 @@ ReadToc(ArchiveHandle *AH)
 		if (AH->version >= K_VERS_1_16)
 			te->relkind = ReadInt(AH);
 
+		if (AH->version >= K_VERS_1_17)
+			te->toasttype = ReadStr(AH);
+
 		te->owner = ReadStr(AH);
 		is_supported = true;
 		if (AH->version < K_VERS_1_9)
@@ -3468,6 +3475,9 @@ _reconnectToDB(ArchiveHandle *AH, const char *dbname)
 	free(AH->currTablespace);
 	AH->currTablespace = NULL;
 
+	free(AH->currToastType);
+	AH->currToastType = NULL;
+
 	/* re-establish fixed state */
 	_doSetFixedOutputState(AH);
 }
@@ -3673,6 +3683,56 @@ _selectTableAccessMethod(ArchiveHandle *AH, const char *tableam)
 	AH->currTableAm = pg_strdup(want);
 }
 
+
+/*
+ * Set the proper default_toast_type value for the table.
+ */
+static void
+_selectToastType(ArchiveHandle *AH, const char *toasttype)
+{
+	RestoreOptions *ropt = AH->public.ropt;
+	PQExpBuffer cmd;
+	const char *want,
+			   *have;
+
+	/* do nothing in --no-toast-type mode */
+	if (ropt->noToastType)
+		return;
+
+	have = AH->currToastType;
+	want = toasttype;
+
+	if (!want)
+		return;
+
+	if (have && strcmp(want, have) == 0)
+		return;
+
+	cmd = createPQExpBuffer();
+
+	appendPQExpBuffer(cmd, "SET default_toast_type = %s;", fmtId(toasttype));
+
+	if (RestoringToDB(AH))
+	{
+		PGresult   *res;
+
+		res = PQexec(AH->connection, cmd->data);
+
+		if (!res || PQresultStatus(res) != PGRES_COMMAND_OK)
+			warn_or_exit_horribly(AH,
+								  "could not set \"default_toast_type\": %s",
+								  PQerrorMessage(AH->connection));
+		PQclear(res);
+	}
+	else
+		ahprintf(AH, "%s\n\n", cmd->data);
+
+	destroyPQExpBuffer(cmd);
+
+	free(AH->currToastType);
+	AH->currToastType = pg_strdup(want);
+}
+
 /*
  * Set the proper default table access method for a table without storage.
  * Currently, this is required only for partitioned tables with a table AM.
@@ -3828,13 +3888,16 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, const char *pfx)
 	 * Select owner, schema, tablespace and default AM as necessary. The
 	 * default access method for partitioned tables is handled after
 	 * generating the object definition, as it requires an ALTER command
-	 * rather than SET.
+	 * rather than SET.  Partitioned tables do not have TOAST tables.
 	 */
 	_becomeOwner(AH, te);
 	_selectOutputSchema(AH, te->namespace);
 	_selectTablespace(AH, te->tablespace);
 	if (te->relkind != RELKIND_PARTITIONED_TABLE)
+	{
 		_selectTableAccessMethod(AH, te->tableam);
+		_selectToastType(AH, te->toasttype);
+	}
 
 	/* Emit header comment for item */
 	if (!AH->noTocComments)
@@ -4393,6 +4456,8 @@ restore_toc_entries_prefork(ArchiveHandle *AH, TocEntry *pending_list)
 	AH->currTablespace = NULL;
 	free(AH->currTableAm);
 	AH->currTableAm = NULL;
+	free(AH->currToastType);
+	AH->currToastType = NULL;
 }
 
 /*
@@ -5130,6 +5195,7 @@ CloneArchive(ArchiveHandle *AH)
 	clone->currSchema = NULL;
 	clone->currTableAm = NULL;
 	clone->currTablespace = NULL;
+	clone->currToastType = NULL;
 
 	/* savedPassword must be local in case we change it while connecting */
 	if (clone->savedPassword)
@@ -5189,6 +5255,7 @@ DeCloneArchive(ArchiveHandle *AH)
 	free(AH->currSchema);
 	free(AH->currTablespace);
 	free(AH->currTableAm);
+	free(AH->currToastType);
 	free(AH->savedPassword);
 
 	free(AH);
diff --git a/src/bin/pg_dump/pg_backup_archiver.h b/src/bin/pg_dump/pg_backup_archiver.h
index 365073b3eae4..cc7aa46b483a 100644
--- a/src/bin/pg_dump/pg_backup_archiver.h
+++ b/src/bin/pg_dump/pg_backup_archiver.h
@@ -71,10 +71,11 @@
 #define K_VERS_1_16 MAKE_ARCHIVE_VERSION(1, 16, 0)	/* BLOB METADATA entries
 													 * and multiple BLOBS,
 													 * relkind */
+#define K_VERS_1_17 MAKE_ARCHIVE_VERSION(1, 17, 0)	/* TOAST type */
 
 /* Current archive version number (the format we can output) */
 #define K_VERS_MAJOR 1
-#define K_VERS_MINOR 16
+#define K_VERS_MINOR 17
 #define K_VERS_REV 0
 #define K_VERS_SELF MAKE_ARCHIVE_VERSION(K_VERS_MAJOR, K_VERS_MINOR, K_VERS_REV)
 
@@ -325,6 +326,7 @@ struct _archiveHandle
 	char	   *currSchema;		/* current schema, or NULL */
 	char	   *currTablespace; /* current tablespace, or NULL */
 	char	   *currTableAm;	/* current table access method, or NULL */
+	char	   *currToastType;	/* current TOAST type, or NULL */
 
 	/* in --transaction-size mode, this counts objects emitted in cur xact */
 	int			txnCount;
@@ -359,6 +361,7 @@ struct _tocEntry
 	char	   *tablespace;		/* null if not in a tablespace; empty string
 								 * means use database default */
 	char	   *tableam;		/* table access method, only for TABLE tags */
+	char	   *toasttype;		/* TOAST table type, only for TABLE tags */
 	char		relkind;		/* relation kind, only for TABLE tags */
 	char	   *owner;
 	char	   *desc;
@@ -405,6 +408,7 @@ typedef struct _archiveOpts
 	const char *namespace;
 	const char *tablespace;
 	const char *tableam;
+	const char *toasttype;
 	char		relkind;
 	const char *owner;
 	const char *description;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 1937997ea674..c8542aa85abe 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -499,6 +499,7 @@ main(int argc, char **argv)
 		{"lock-wait-timeout", required_argument, NULL, 2},
 		{"no-table-access-method", no_argument, &dopt.outputNoTableAm, 1},
 		{"no-tablespaces", no_argument, &dopt.outputNoTablespaces, 1},
+		{"no-toast-type", no_argument, &dopt.outputNoToastType, 1},
 		{"quote-all-identifiers", no_argument, &quote_all_identifiers, 1},
 		{"load-via-partition-root", no_argument, &dopt.load_via_partition_root, 1},
 		{"role", required_argument, NULL, 3},
@@ -1186,6 +1187,7 @@ main(int argc, char **argv)
 	ropt->noOwner = dopt.outputNoOwner;
 	ropt->noTableAm = dopt.outputNoTableAm;
 	ropt->noTablespace = dopt.outputNoTablespaces;
+	ropt->noToastType = dopt.outputNoToastType;
 	ropt->disable_triggers = dopt.disable_triggers;
 	ropt->use_setsessauth = dopt.use_setsessauth;
 	ropt->disable_dollar_quoting = dopt.disable_dollar_quoting;
@@ -1308,6 +1310,7 @@ help(const char *progname)
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
 	printf(_("  --no-toast-compression       do not dump TOAST compression methods\n"));
+	printf(_("  --no-toast-type              do not dump TOAST table type\n"));
 	printf(_("  --no-unlogged-table-data     do not dump unlogged table data\n"));
 	printf(_("  --on-conflict-do-nothing     add ON CONFLICT DO NOTHING to INSERT commands\n"));
 	printf(_("  --quote-all-identifiers      quote all identifiers, even if not key words\n"));
@@ -6993,6 +6996,7 @@ getTables(Archive *fout, int *numTables)
 	int			i_relfrozenxid;
 	int			i_toastfrozenxid;
 	int			i_toastoid;
+	int			i_toasttype;
 	int			i_relminmxid;
 	int			i_toastminmxid;
 	int			i_reloptions;
@@ -7047,6 +7051,14 @@ getTables(Archive *fout, int *numTables)
 						 "ELSE 0 END AS foreignserver, "
 						 "c.relfrozenxid, tc.relfrozenxid AS tfrozenxid, "
 						 "tc.oid AS toid, "
+						 "CASE WHEN c.reltoastrelid <> 0 THEN "
+						 " (SELECT CASE "
+						 "   WHEN a.atttypid::regtype = 'oid'::regtype THEN 'oid'::text "
+						 "   WHEN a.atttypid::regtype = 'bigint'::regtype THEN 'int8'::text "
+						 "   ELSE NULL END"
+						 "  FROM pg_attribute AS a "
+						 "  WHERE a.attrelid = tc.oid AND a.attname = 'chunk_id') "
+						 " ELSE NULL END AS toasttype, "
 						 "tc.relpages AS toastpages, "
 						 "tc.reloptions AS toast_reloptions, "
 						 "d.refobjid AS owning_tab, "
@@ -7217,6 +7229,7 @@ getTables(Archive *fout, int *numTables)
 	i_relfrozenxid = PQfnumber(res, "relfrozenxid");
 	i_toastfrozenxid = PQfnumber(res, "tfrozenxid");
 	i_toastoid = PQfnumber(res, "toid");
+	i_toasttype = PQfnumber(res, "toasttype");
 	i_relminmxid = PQfnumber(res, "relminmxid");
 	i_toastminmxid = PQfnumber(res, "tminmxid");
 	i_reloptions = PQfnumber(res, "reloptions");
@@ -7295,6 +7308,10 @@ getTables(Archive *fout, int *numTables)
 		tblinfo[i].frozenxid = atooid(PQgetvalue(res, i, i_relfrozenxid));
 		tblinfo[i].toast_frozenxid = atooid(PQgetvalue(res, i, i_toastfrozenxid));
 		tblinfo[i].toast_oid = atooid(PQgetvalue(res, i, i_toastoid));
+		if (PQgetisnull(res, i, i_toasttype))
+			tblinfo[i].toast_type = NULL;
+		else
+			tblinfo[i].toast_type = pg_strdup(PQgetvalue(res, i, i_toasttype));
 		tblinfo[i].minmxid = atooid(PQgetvalue(res, i, i_relminmxid));
 		tblinfo[i].toast_minmxid = atooid(PQgetvalue(res, i, i_toastminmxid));
 		tblinfo[i].reloptions = pg_strdup(PQgetvalue(res, i, i_reloptions));
@@ -17667,6 +17684,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 	{
 		char	   *tablespace = NULL;
 		char	   *tableam = NULL;
+		char	   *toasttype = NULL;
 
 		/*
 		 * _selectTablespace() relies on tablespace-enabled objects in the
@@ -17681,12 +17699,15 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 		if (RELKIND_HAS_TABLE_AM(tbinfo->relkind) ||
 			tbinfo->relkind == RELKIND_PARTITIONED_TABLE)
 			tableam = tbinfo->amname;
+		if (OidIsValid(tbinfo->toast_oid))
+			toasttype = tbinfo->toast_type;
 
 		ArchiveEntry(fout, tbinfo->dobj.catId, tbinfo->dobj.dumpId,
 					 ARCHIVE_OPTS(.tag = tbinfo->dobj.name,
 								  .namespace = tbinfo->dobj.namespace->dobj.name,
 								  .tablespace = tablespace,
 								  .tableam = tableam,
+								  .toasttype = toasttype,
 								  .relkind = tbinfo->relkind,
 								  .owner = tbinfo->rolname,
 								  .description = reltypename,
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 39eef1d6617f..1e6627067fc7 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -318,6 +318,7 @@ typedef struct _tableInfo
 	uint32		frozenxid;		/* table's relfrozenxid */
 	uint32		minmxid;		/* table's relminmxid */
 	Oid			toast_oid;		/* toast table's OID, or 0 if none */
+	char	   *toast_type;		/* toast table type, or NULL if none */
 	uint32		toast_frozenxid;	/* toast table's relfrozenxid, if any */
 	uint32		toast_minmxid;	/* toast table's relminmxid */
 	int			ncheck;			/* # of CHECK expressions */
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 3cbcad65c5fb..1e52c79ce120 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -95,6 +95,7 @@ static int	if_exists = 0;
 static int	inserts = 0;
 static int	no_table_access_method = 0;
 static int	no_tablespaces = 0;
+static int	no_toast_type = 0;
 static int	use_setsessauth = 0;
 static int	no_comments = 0;
 static int	no_policies = 0;
@@ -167,6 +168,7 @@ main(int argc, char *argv[])
 		{"lock-wait-timeout", required_argument, NULL, 2},
 		{"no-table-access-method", no_argument, &no_table_access_method, 1},
 		{"no-tablespaces", no_argument, &no_tablespaces, 1},
+		{"no-toast-type", no_argument, &no_tablespaces, 1},
 		{"quote-all-identifiers", no_argument, &quote_all_identifiers, 1},
 		{"load-via-partition-root", no_argument, &load_via_partition_root, 1},
 		{"role", required_argument, NULL, 3},
@@ -471,6 +473,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --no-table-access-method");
 	if (no_tablespaces)
 		appendPQExpBufferStr(pgdumpopts, " --no-tablespaces");
+	if (no_toast_type)
+		appendPQExpBufferStr(pgdumpopts, " --no-toast-type");
 	if (quote_all_identifiers)
 		appendPQExpBufferStr(pgdumpopts, " --quote-all-identifiers");
 	if (load_via_partition_root)
@@ -745,6 +749,7 @@ help(void)
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
 	printf(_("  --no-toast-compression       do not dump TOAST compression methods\n"));
+	printf(_("  --no-toast-type              do not dump TOAST table types\n"));
 	printf(_("  --no-unlogged-table-data     do not dump unlogged table data\n"));
 	printf(_("  --on-conflict-do-nothing     add ON CONFLICT DO NOTHING to INSERT commands\n"));
 	printf(_("  --quote-all-identifiers      quote all identifiers, even if not key words\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index 6ef789cb06d6..610103ae4b99 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -100,6 +100,7 @@ main(int argc, char **argv)
 	static int	no_data_for_failed_tables = 0;
 	static int	outputNoTableAm = 0;
 	static int	outputNoTablespaces = 0;
+	static int	outputNoToastType = 0;
 	static int	use_setsessauth = 0;
 	static int	no_comments = 0;
 	static int	no_data = 0;
@@ -156,6 +157,7 @@ main(int argc, char **argv)
 		{"no-data-for-failed-tables", no_argument, &no_data_for_failed_tables, 1},
 		{"no-table-access-method", no_argument, &outputNoTableAm, 1},
 		{"no-tablespaces", no_argument, &outputNoTablespaces, 1},
+		{"no-toast-type", no_argument, &outputNoToastType, 1},
 		{"role", required_argument, NULL, 2},
 		{"section", required_argument, NULL, 3},
 		{"strict-names", no_argument, &strict_names, 1},
@@ -461,6 +463,7 @@ main(int argc, char **argv)
 	opts->noDataForFailedTables = no_data_for_failed_tables;
 	opts->noTableAm = outputNoTableAm;
 	opts->noTablespace = outputNoTablespaces;
+	opts->noToastType = outputNoToastType;
 	opts->use_setsessauth = use_setsessauth;
 	opts->no_comments = no_comments;
 	opts->no_policies = no_policies;
@@ -704,6 +707,7 @@ usage(const char *progname)
 	printf(_("  --no-subscriptions           do not restore subscriptions\n"));
 	printf(_("  --no-table-access-method     do not restore table access methods\n"));
 	printf(_("  --no-tablespaces             do not restore tablespace assignments\n"));
+	printf(_("  --no-toast-type              do not restore TOAST table types\n"));
 	printf(_("  --section=SECTION            restore named section (pre-data, data, or post-data)\n"));
 	printf(_("  --statistics-only            restore only the statistics, not schema or data\n"));
 	printf(_("  --strict-names               require table and/or schema include patterns to\n"
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 2485d8f360e5..b49e1324114c 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -659,6 +659,15 @@ my %pgdump_runs = (
 			'postgres',
 		],
 	},
+	no_toast_type => {
+		dump_cmd => [
+			'pg_dump', '--no-sync',
+			'--file' => "$tempdir/no_toast_type.sql",
+			'--no-toast-type',
+			'--with-statistics',
+			'postgres',
+		],
+	},
 	only_dump_test_schema => {
 		dump_cmd => [
 			'pg_dump', '--no-sync',
@@ -881,6 +890,7 @@ my %full_runs = (
 	no_privs => 1,
 	no_statistics => 1,
 	no_table_access_method => 1,
+	no_toast_type => 1,
 	pg_dumpall_dbprivs => 1,
 	pg_dumpall_exclude => 1,
 	schema_only => 1,
@@ -4913,6 +4923,31 @@ my %tests = (
 		},
 	},
 
+	# Test the case of multiple TOAST table types.
+	'CREATE TABLE regress_toast_type' => {
+		create_order => 13,
+		create_sql => '
+			SET default_toast_type = int8;
+			CREATE TABLE dump_test.regress_toast_type_int8 (col1 text);
+			SET default_toast_type = oid;
+			CREATE TABLE dump_test.regress_toast_type_oid (col1 text);
+			RESET default_toast_type;',
+		regexp => qr/^
+			\QSET default_toast_type = int8;\E
+			(\n(?!SET[^;]+;)[^\n]*)*
+			\n\QCREATE TABLE dump_test.regress_toast_type_int8 (\E
+			\n\s+\Qcol1 text\E
+			\n\);/xm,
+		like => {
+			%full_runs, %dump_test_schema_runs, section_pre_data => 1,
+		},
+		unlike => {
+			exclude_dump_test_schema => 1,
+			no_toast_type => 1,
+			only_dump_measurement => 1,
+		},
+	},
+
 	#
 	# TABLE and MATVIEW stats will end up in SECTION_DATA.
 	# INDEX stats (expression columns only) will end up in SECTION_POST_DATA.
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index 2ae084b5fa6f..d0efc5955505 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -1208,6 +1208,18 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-toast-type</option></term>
+      <listitem>
+       <para>
+        Do not output commands to set <acronym>TOAST</acronym> table
+        types.
+        With this option, all <acronym>TOAST</acronym> tables will be
+        restored with the default type.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-unlogged-table-data</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_dumpall.sgml b/doc/src/sgml/ref/pg_dumpall.sgml
index 8ca68da5a556..6e5a8beded5d 100644
--- a/doc/src/sgml/ref/pg_dumpall.sgml
+++ b/doc/src/sgml/ref/pg_dumpall.sgml
@@ -633,6 +633,18 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-toast-type</option></term>
+      <listitem>
+       <para>
+        Do not output commands to set <acronym>TOAST</acronym> table
+        types.
+        With this option, all <acronym>TOAST</acronym> tables will be
+        restored with the default type.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-unlogged-table-data</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml
index b649bd3a5ae0..fb64315d8aa6 100644
--- a/doc/src/sgml/ref/pg_restore.sgml
+++ b/doc/src/sgml/ref/pg_restore.sgml
@@ -842,6 +842,18 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-toast-type</option></term>
+      <listitem>
+       <para>
+        Do not output commands to select <acronym>TOAST</acronym> table
+        types.
+        With this option, all <acronym>TOAST</acronym> tables will be
+        created with whichever type is the default during restore.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
        <term><option>--section=<replaceable class="parameter">sectionname</replaceable></option></term>
        <listitem>
-- 
2.50.0

v2-0012-Add-new-vartag_external-for-8-byte-TOAST-values.patchtext/x-diff; charset=us-asciiDownload
From 392fe1b61eb7dbe84d4ad65f6749e74d075d370f Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 8 Jul 2025 08:20:31 +0900
Subject: [PATCH v2 12/13] Add new vartag_external for 8-byte TOAST values

This is a new type of external TOAST pointer, able to be fed 8-byte
TOAST values.  It uses a dedicated vartag_external, which is used when
a TOAST table uses bigint for its chunk_id.

The relevant callbacks are added to toast_external.c.
---
 src/include/access/heaptoast.h                |   8 +-
 src/include/varatt.h                          |  31 ++++-
 src/backend/access/common/toast_external.c    | 124 ++++++++++++++++++
 src/backend/access/common/toast_internals.c   |  11 +-
 src/backend/access/heap/heaptoast.c           |  69 ++++++++--
 .../replication/logical/reorderbuffer.c       |  10 +-
 doc/src/sgml/storage.sgml                     |   6 +-
 contrib/amcheck/verify_heapam.c               |   2 +-
 8 files changed, 239 insertions(+), 22 deletions(-)

diff --git a/src/include/access/heaptoast.h b/src/include/access/heaptoast.h
index 673e96f5488c..a39ad79a5ae9 100644
--- a/src/include/access/heaptoast.h
+++ b/src/include/access/heaptoast.h
@@ -81,6 +81,12 @@
 
 #define EXTERN_TUPLE_MAX_SIZE	MaximumBytesPerTuple(EXTERN_TUPLES_PER_PAGE)
 
+#define TOAST_MAX_CHUNK_SIZE_INT8	\
+	(EXTERN_TUPLE_MAX_SIZE -							\
+	 MAXALIGN(SizeofHeapTupleHeader) -					\
+	 (sizeof(uint32) * 2) -								\
+	 sizeof(int32) -									\
+	 VARHDRSZ)
 #define TOAST_MAX_CHUNK_SIZE_OID	\
 	(EXTERN_TUPLE_MAX_SIZE -							\
 	 MAXALIGN(SizeofHeapTupleHeader) -					\
@@ -89,7 +95,7 @@
 	 VARHDRSZ)
 
 /* Maximum size of chunk possible */
-#define TOAST_MAX_CHUNK_SIZE	TOAST_MAX_CHUNK_SIZE_OID
+#define TOAST_MAX_CHUNK_SIZE	Max(TOAST_MAX_CHUNK_SIZE_INT8, TOAST_MAX_CHUNK_SIZE_OID)
 
 /* ----------
  * heap_toast_insert_or_update -
diff --git a/src/include/varatt.h b/src/include/varatt.h
index 793030dae932..aa36e8e1f561 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -41,6 +41,29 @@ typedef struct varatt_external_oid
 	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
 }			varatt_external_oid;
 
+/*
+ * struct varatt_external_int8 is a "larger" version of "TOAST pointer",
+ * that uses an 8-byte integer as value.
+ *
+ * This follows the same properties as varatt_external_oid, except that
+ * this is used in TOAST relations with int8 as attribute for chunk_id.
+ */
+typedef struct varatt_external_int8
+{
+	int32		va_rawsize;		/* Original data size (includes header) */
+	uint32		va_extinfo;		/* External saved size (without header) and
+								 * compression method */
+	/*
+	 * Unique ID of value within TOAST table, as two uint32 for alignment
+	 * and padding.
+	 * XXX: think for example about the addition of an extra field for
+	 * meta-data and/or more compression data, even if it's OK here).
+	 */
+	uint32		va_valueid_lo;
+	uint32		va_valueid_hi;
+	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
+}			varatt_external_int8;
+
 
 /*
  * These macros define the "saved size" portion of va_extinfo.  Its remaining
@@ -91,6 +114,7 @@ typedef enum vartag_external
 	VARTAG_INDIRECT = 1,
 	VARTAG_EXPANDED_RO = 2,
 	VARTAG_EXPANDED_RW = 3,
+	VARTAG_ONDISK_INT8 = 4,
 	VARTAG_ONDISK_OID = 18
 } vartag_external;
 
@@ -102,6 +126,7 @@ typedef enum vartag_external
 	((tag) == VARTAG_INDIRECT ? sizeof(varatt_indirect) : \
 	 VARTAG_IS_EXPANDED(tag) ? sizeof(varatt_expanded) : \
 	 (tag) == VARTAG_ONDISK_OID ? sizeof(varatt_external_oid) : \
+	 (tag) == VARTAG_ONDISK_INT8 ? sizeof(varatt_external_int8) : \
 	 (AssertMacro(false), 0))
 
 /*
@@ -294,8 +319,10 @@ typedef struct
 #define VARATT_IS_EXTERNAL(PTR)				VARATT_IS_1B_E(PTR)
 #define VARATT_IS_EXTERNAL_ONDISK_OID(PTR) \
 	(VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID)
+#define VARATT_IS_EXTERNAL_ONDISK_INT8(PTR) \
+	(VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_INT8)
 #define VARATT_IS_EXTERNAL_ONDISK(PTR) \
-	(VARATT_IS_EXTERNAL_ONDISK_OID(PTR))
+	(VARATT_IS_EXTERNAL_ONDISK_OID(PTR) || VARATT_IS_EXTERNAL_ONDISK_INT8(PTR))
 #define VARATT_IS_EXTERNAL_INDIRECT(PTR) \
 	(VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_INDIRECT)
 #define VARATT_IS_EXTERNAL_EXPANDED_RO(PTR) \
@@ -339,7 +366,7 @@ typedef struct
 
 /*
  * Same for external Datums; but note argument is a struct
- * varatt_external_oid.
+ * varatt_external_oid or varatt_external_int8.
  */
 #define VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) \
 	((toast_pointer).va_extinfo & VARLENA_EXTSIZE_MASK)
diff --git a/src/backend/access/common/toast_external.c b/src/backend/access/common/toast_external.c
index d179f0143803..0e79ac8acae8 100644
--- a/src/backend/access/common/toast_external.c
+++ b/src/backend/access/common/toast_external.c
@@ -14,9 +14,22 @@
 #include "postgres.h"
 
 #include "access/detoast.h"
+#include "access/genam.h"
 #include "access/heaptoast.h"
+#include "access/toast_counter.h"
 #include "access/toast_external.h"
 #include "catalog/catalog.h"
+#include "miscadmin.h"
+#include "utils/fmgroids.h"
+#include "utils/snapmgr.h"
+
+
+/* Callbacks for VARTAG_ONDISK_INT8 */
+static void ondisk_int8_to_external_data(struct varlena *attr,
+										 toast_external_data *data);
+static struct varlena *ondisk_int8_create_external_data(toast_external_data data);
+static uint64 ondisk_int8_get_new_value(Relation toastrel, Oid indexid,
+										AttrNumber attnum);
 
 /* Callbacks for VARTAG_ONDISK_OID */
 static void ondisk_oid_to_external_data(struct varlena *attr,
@@ -26,6 +39,12 @@ static uint64 ondisk_oid_get_new_value(Relation toastrel, Oid indexid,
 									   AttrNumber attnum);
 
 
+/*
+ * Size of an EXTERNAL datum that contains a standard TOAST pointer
+ * (int8 value).
+ */
+#define TOAST_POINTER_INT8_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_int8))
+
 /*
  * Size of an EXTERNAL datum that contains a standard TOAST pointer (OID
  * value).
@@ -46,6 +65,13 @@ static uint64 ondisk_oid_get_new_value(Relation toastrel, Oid indexid,
  * individual fields.
  */
 static const toast_external_info toast_external_infos[TOAST_EXTERNAL_INFO_SIZE] = {
+	[VARTAG_ONDISK_INT8] = {
+		.toast_pointer_size = TOAST_POINTER_INT8_SIZE,
+		.maximum_chunk_size = TOAST_MAX_CHUNK_SIZE_INT8,
+		.to_external_data = ondisk_int8_to_external_data,
+		.create_external_data = ondisk_int8_create_external_data,
+		.get_new_value = ondisk_int8_get_new_value,
+	},
 	[VARTAG_ONDISK_OID] = {
 		.toast_pointer_size = TOAST_POINTER_OID_SIZE,
 		.maximum_chunk_size = TOAST_MAX_CHUNK_SIZE_OID,
@@ -78,6 +104,104 @@ toast_external_info_get_pointer_size(uint8 tag)
  * the in-memory representation toast_external_data used in the backend.
  */
 
+/* Callbacks for VARTAG_ONDISK_INT8 */
+static void
+ondisk_int8_to_external_data(struct varlena *attr, toast_external_data *data)
+{
+	varatt_external_int8	external;
+
+	VARATT_EXTERNAL_GET_POINTER(external, attr);
+	data->rawsize = external.va_rawsize;
+
+	/* External size and compression methods are stored in the same field */
+	if (VARATT_EXTERNAL_IS_COMPRESSED(external))
+	{
+		data->extsize = VARATT_EXTERNAL_GET_EXTSIZE(external);
+		data->compression_method = VARATT_EXTERNAL_GET_COMPRESS_METHOD(external);
+	}
+	else
+	{
+		data->extsize = external.va_extinfo;
+		data->compression_method = TOAST_INVALID_COMPRESSION_ID;
+	}
+
+	data->value = (((uint64) external.va_valueid_hi) << 32) |
+		external.va_valueid_lo;
+	data->toastrelid = external.va_toastrelid;
+
+}
+
+static struct varlena *
+ondisk_int8_create_external_data(toast_external_data data)
+{
+	struct varlena *result = NULL;
+	varatt_external_int8 external;
+
+	external.va_rawsize = data.rawsize;
+
+	if (data.compression_method != TOAST_INVALID_COMPRESSION_ID)
+	{
+		/* Set size and compression method, in a single field. */
+		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(external,
+													 data.extsize,
+													 data.compression_method);
+	}
+	else
+		external.va_extinfo = data.extsize;
+
+	external.va_toastrelid = data.toastrelid;
+	external.va_valueid_hi = (((uint64) data.value) >> 32);
+	external.va_valueid_lo = (uint32) data.value;
+
+	result = (struct varlena *) palloc(TOAST_POINTER_INT8_SIZE);
+	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_INT8);
+	memcpy(VARDATA_EXTERNAL(result), &external, sizeof(external));
+
+	return result;
+}
+
+static uint64
+ondisk_int8_get_new_value(Relation toastrel, Oid indexid,
+						  AttrNumber attnum)
+{
+	uint64		new_value;
+	SysScanDesc	scan;
+	ScanKeyData	key;
+	bool		collides = false;
+
+retry:
+	new_value = GetNewToastId();
+
+	/* No indexes in bootstrap mode, so leave */
+	if (IsBootstrapProcessingMode())
+		return new_value;
+
+	Assert(IsSystemRelation(toastrel));
+
+	CHECK_FOR_INTERRUPTS();
+
+	/*
+	 * Check if the new value picked already exists in the toast relation.
+	 * If there is a conflict, retry.
+	 */
+	ScanKeyInit(&key,
+				attnum,
+				BTEqualStrategyNumber, F_INT8EQ,
+				Int64GetDatum(new_value));
+
+	/* see notes in GetNewOidWithIndex() above about using SnapshotAny */
+	scan = systable_beginscan(toastrel, indexid, true,
+							  SnapshotAny, 1, &key);
+	collides = HeapTupleIsValid(systable_getnext(scan));
+	systable_endscan(scan);
+
+	if (collides)
+		goto retry;
+
+	return new_value;
+}
+
+
 /* Callbacks for VARTAG_ONDISK_OID */
 static void
 ondisk_oid_to_external_data(struct varlena *attr, toast_external_data *data)
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 28e2867f8209..c6b2d1522cef 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -18,7 +18,6 @@
 #include "access/heapam.h"
 #include "access/heaptoast.h"
 #include "access/table.h"
-#include "access/toast_counter.h"
 #include "access/toast_external.h"
 #include "access/toast_internals.h"
 #include "access/xact.h"
@@ -158,6 +157,8 @@ toast_save_datum(Relation rel, Datum value,
 	 */
 	toastrel = table_open(rel->rd_rel->reltoastrelid, RowExclusiveLock);
 	toasttupDesc = toastrel->rd_att;
+	toast_typid = TupleDescAttr(toasttupDesc, 0)->atttypid;
+	Assert(toast_typid == OIDOID || toast_typid == INT8OID);
 
 	/*
 	 * Grab the information for toast_external_data.
@@ -165,12 +166,12 @@ toast_save_datum(Relation rel, Datum value,
 	 * Note: if we support multiple external vartags for a single value
 	 * type, we would need to be smarter in the vartag selection.
 	 */
-	tag = VARTAG_ONDISK_OID;
+	if (toast_typid == OIDOID)
+		tag = VARTAG_ONDISK_OID;
+	else if (toast_typid == INT8OID)
+		tag = VARTAG_ONDISK_INT8;
 	info = toast_external_get_info(tag);
 
-	toast_typid = TupleDescAttr(toasttupDesc, 0)->atttypid;
-	Assert(toast_typid == OIDOID || toast_typid == INT8OID);
-
 	/* Open all the toast indexes and look for the valid one */
 	validIndex = toast_open_indexes(toastrel,
 									RowExclusiveLock,
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index 7a54cf7fae34..2121eb02848e 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -31,7 +31,9 @@
 #include "access/toast_external.h"
 #include "access/toast_helper.h"
 #include "access/toast_internals.h"
+#include "access/toast_type.h"
 #include "utils/fmgroids.h"
+#include "utils/syscache.h"
 
 
 /* ----------
@@ -145,12 +147,52 @@ heap_toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup,
 	/*
 	 * Retrieve the toast pointer size based on the type of external TOAST
 	 * pointer assumed to be used.
-	 *
-	 * Only one format of external TOAST pointer is supported currently,
-	 * making this code simple, based on a single vartag.
 	 */
-	ttc.ttc_toast_pointer_size =
-		toast_external_info_get_pointer_size(VARTAG_ONDISK_OID);
+	if (OidIsValid(rel->rd_rel->reltoastrelid))
+	{
+		HeapTuple	atttuple;
+		Form_pg_attribute atttoast;
+		uint8		vartag;
+
+		/*
+		 * XXX: This is very unlikely efficient, but it is not possible to
+		 * rely on the relation cache to retrieve this information as syscache
+		 * lookups should not happen when loading critical entries.
+		 */
+		atttuple = SearchSysCacheAttNum(rel->rd_rel->reltoastrelid, 1);
+		if (!HeapTupleIsValid(atttuple))
+			elog(ERROR, "cache lookup failed for relation %u",
+				 rel->rd_rel->reltoastrelid);
+		atttoast = (Form_pg_attribute) GETSTRUCT(atttuple);
+
+		if (atttoast->atttypid == OIDOID)
+			vartag = VARTAG_ONDISK_OID;
+		else if (atttoast->atttypid == INT8OID)
+			vartag = VARTAG_ONDISK_INT8;
+		else
+			Assert(false);
+		ttc.ttc_toast_pointer_size =
+			toast_external_info_get_pointer_size(vartag);
+		ReleaseSysCache(atttuple);
+	}
+	else
+	{
+		/*
+		 * No TOAST relation to rely on, which is a case possible when
+		 * dealing with partitioned tables, for example.  Hence, do a best
+		 * guess based on the GUC default_toast_type.
+		 */
+		uint8	vartag = VARTAG_ONDISK_OID;
+
+		if (default_toast_type == TOAST_TYPE_INT8)
+			vartag = VARTAG_ONDISK_INT8;
+		else if (default_toast_type == TOAST_TYPE_OID)
+			vartag = VARTAG_ONDISK_OID;
+		else
+			Assert(false);
+		ttc.ttc_toast_pointer_size =
+			toast_external_info_get_pointer_size(vartag);
+	}
 
 	ttc.ttc_rel = rel;
 	ttc.ttc_values = toast_values;
@@ -654,7 +696,7 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 	int32		max_chunk_size;
 	const toast_external_info *info;
 	uint8		tag = VARTAG_INDIRECT;  /* init value does not matter */
-	Oid			toast_typid;
+	Oid			toast_typid = InvalidOid;
 
 	/* Look for the valid index of toast relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -673,14 +715,19 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 	 * to pass down this information from the upper callers.  This is
 	 * currently on required for the maximum chunk_size.
 	 */
-	tag = VARTAG_ONDISK_OID;
-	info = toast_external_get_info(tag);
-
-	max_chunk_size = info->maximum_chunk_size;
-
 	toast_typid = TupleDescAttr(toastrel->rd_att, 0)->atttypid;
 	Assert(toast_typid == OIDOID || toast_typid == INT8OID);
 
+	if (toast_typid == OIDOID)
+		tag = VARTAG_ONDISK_OID;
+	else if (toast_typid == INT8OID)
+		tag = VARTAG_ONDISK_INT8;
+	else
+		Assert(false);
+
+	info = toast_external_get_info(tag);
+	max_chunk_size = info->maximum_chunk_size;
+
 	totalchunks = ((attrsize - 1) / max_chunk_size) + 1;
 	startchunk = sliceoffset / max_chunk_size;
 	endchunk = (sliceoffset + slicelength - 1) / max_chunk_size;
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 77939c7f849c..834040395e62 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -4971,14 +4971,22 @@ ReorderBufferToastAppendChunk(ReorderBuffer *rb, ReorderBufferTXN *txn,
 	TupleDesc	desc = RelationGetDescr(relation);
 	uint64		chunk_id;
 	int32		chunk_seq;
+	Oid			toast_typid;
 
 	if (txn->toast_hash == NULL)
 		ReorderBufferToastInitHash(rb, txn);
+	toast_typid = TupleDescAttr(desc, 0)->atttypid;
 
 	Assert(IsToastRelation(relation));
 
 	newtup = change->data.tp.newtuple;
-	chunk_id = DatumGetObjectId(fastgetattr(newtup, 1, desc, &isnull));
+	/* This depends on the type of TOAST value dealt with. */
+	if (toast_typid == OIDOID)
+		chunk_id = DatumGetObjectId(fastgetattr(newtup, 1, desc, &isnull));
+	else if (toast_typid == INT8OID)
+		chunk_id = DatumGetUInt64(fastgetattr(newtup, 1, desc, &isnull));
+	else
+		Assert(false);
 	Assert(!isnull);
 	chunk_seq = DatumGetInt32(fastgetattr(newtup, 2, desc, &isnull));
 	Assert(!isnull);
diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml
index 564783a1c559..29ba80e8423c 100644
--- a/doc/src/sgml/storage.sgml
+++ b/doc/src/sgml/storage.sgml
@@ -415,7 +415,11 @@ described in more detail below.
 
 <para>
 Out-of-line values are divided (after compression if used) into chunks of at
-most <symbol>TOAST_MAX_CHUNK_SIZE_OID</symbol> bytes (by default this value is chosen
+most <symbol>TOAST_MAX_CHUNK_SIZE_OID</symbol> bytes if the
+<acronym>TOAST</acronym> relation uses the <literal>oid</literal> type for
+<literal>chunk_id</literal>, or <symbol>TOAST_MAX_CHUNK_SIZE_INT8</symbol>
+bytes if the <acronym>TOAST</acronym> relation uses the <literal>int8</literal>
+type for <literal>chunk_id</literal> (by default these values are chosen
 so that four chunk rows will fit on a page, making it about 2000 bytes).
 Each chunk is stored as a separate row in the <acronym>TOAST</acronym> table
 belonging to the owning table.  Every
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 833811c75437..958b1451b4ff 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1733,7 +1733,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	{
 		uint8		va_tag = VARTAG_EXTERNAL(tp + ctx->offset);
 
-		if (va_tag != VARTAG_ONDISK_OID)
+		if (va_tag != VARTAG_ONDISK_OID && va_tag != VARTAG_ONDISK_INT8)
 		{
 			report_corruption(ctx,
 							  psprintf("toasted attribute has unexpected TOAST tag %u",
-- 
2.50.0

v2-0013-amcheck-Add-test-cases-for-8-byte-TOAST-values.patchtext/x-diff; charset=us-asciiDownload
From d639eb2b131c6f57c6b828bdc06cc6edbbedf6eb Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 19 Jun 2025 13:09:11 +0900
Subject: [PATCH v2 13/13] amcheck: Add test cases for 8-byte TOAST values

This patch is a proof of concept to show what is required to change in
the tests of pg_amcheck to be able to work with the new type of external
TOAST pointer.
---
 src/bin/pg_amcheck/t/004_verify_heapam.pl | 15 +++++++++------
 1 file changed, 9 insertions(+), 6 deletions(-)

diff --git a/src/bin/pg_amcheck/t/004_verify_heapam.pl b/src/bin/pg_amcheck/t/004_verify_heapam.pl
index 72693660fb64..5f82608b5c72 100644
--- a/src/bin/pg_amcheck/t/004_verify_heapam.pl
+++ b/src/bin/pg_amcheck/t/004_verify_heapam.pl
@@ -83,8 +83,8 @@ use Test::More;
 # it is convenient enough to do it this way.  We define packing code
 # constants here, where they can be compared easily against the layout.
 
-use constant HEAPTUPLE_PACK_CODE => 'LLLSSSSSCCLLCCCCCCCCCCllLL';
-use constant HEAPTUPLE_PACK_LENGTH => 58;    # Total size
+use constant HEAPTUPLE_PACK_CODE => 'LLLSSSSSCCLLCCCCCCCCCCllLLL';
+use constant HEAPTUPLE_PACK_LENGTH => 62;    # Total size
 
 # Read a tuple of our table from a heap page.
 #
@@ -129,7 +129,8 @@ sub read_tuple
 		c_va_vartag => shift,
 		c_va_rawsize => shift,
 		c_va_extinfo => shift,
-		c_va_valueid => shift,
+		c_va_valueid_lo => shift,
+		c_va_valueid_hi => shift,
 		c_va_toastrelid => shift);
 	# Stitch together the text for column 'b'
 	$tup{b} = join('', map { chr($tup{"b_body$_"}) } (1 .. 7));
@@ -163,7 +164,8 @@ sub write_tuple
 		$tup->{b_body6}, $tup->{b_body7},
 		$tup->{c_va_header}, $tup->{c_va_vartag},
 		$tup->{c_va_rawsize}, $tup->{c_va_extinfo},
-		$tup->{c_va_valueid}, $tup->{c_va_toastrelid});
+		$tup->{c_va_valueid_lo}, $tup->{c_va_valueid_hi},
+		$tup->{c_va_toastrelid});
 	sysseek($fh, $offset, 0)
 	  or BAIL_OUT("sysseek failed: $!");
 	defined(syswrite($fh, $buffer, HEAPTUPLE_PACK_LENGTH))
@@ -184,6 +186,7 @@ my $node = PostgreSQL::Test::Cluster->new('test');
 $node->init(no_data_checksums => 1);
 $node->append_conf('postgresql.conf', 'autovacuum=off');
 $node->append_conf('postgresql.conf', 'max_prepared_transactions=10');
+$node->append_conf('postgresql.conf', 'default_toast_type=int8');
 
 # Start the node and load the extensions.  We depend on both
 # amcheck and pageinspect for this test.
@@ -496,7 +499,7 @@ for (my $tupidx = 0; $tupidx < $ROWCOUNT; $tupidx++)
 		$tup->{t_hoff} += 128;
 
 		push @expected,
-		  qr/${$header}data begins at offset 152 beyond the tuple length 58/,
+		  qr/${$header}data begins at offset 152 beyond the tuple length 62/,
 		  qr/${$header}tuple data should begin at byte 24, but actually begins at byte 152 \(3 attributes, no nulls\)/;
 	}
 	elsif ($offnum == 6)
@@ -581,7 +584,7 @@ for (my $tupidx = 0; $tupidx < $ROWCOUNT; $tupidx++)
 	elsif ($offnum == 13)
 	{
 		# Corrupt the bits in column 'c' toast pointer
-		$tup->{c_va_valueid} = 0xFFFFFFFF;
+		$tup->{c_va_valueid_lo} = 0xFFFFFFFF;
 
 		$header = header(0, $offnum, 2);
 		push @expected, qr/${header}toast value \d+ not found in toast table/;
-- 
2.50.0

#7Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#6)
3 attachment(s)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

On Tue, Jul 08, 2025 at 08:38:41AM +0900, Michael Paquier wrote:

Please note that I still need to look at perf profiles and some flame
graphs with the refactoring done in 0003 with the worst case I've
mentioned upthread with detoasting and values stored uncompressed in
the TOAST relation.

So, the worst case I could think of for the slice detoast path is
something like that:
create table toasttest_bytea (f1 bytea);
alter table toasttest_bytea alter column f1 set storage external;
insert into toasttest_bytea values(decode(repeat('1234567890',10000),'escape'));

And then use something like the following query that retrieves a small
substring many times, to force a maximum of detoast_attr_slice() to
happen, checking the effect of toast_external_info_get_data():
select length(string_agg(substr(f1, 2, 3), '')) from
toasttest_bytea, lateral generate_series(1,1000000) as a (id);

I have taken this query, kept running that with a \watch, and took
samples of 10s perf records, finishing with the attached graphs
(runtime does not show any difference):
- detoast_master.svg, for the graph on HEAD.
- detoast_patch.svg with the patch set up to 0003 and the external
TOAST pointer refactoring, where detoast_attr_slice() shows up.
- master_patch_diff.svg as the difference between both, with
difffolded.pl from [1]https://github.com/brendangregg/FlameGraph -- Michael.

I don't see a difference in the times spent in these stacks, as we are
spending most of the run retrieving the slices from the TOAST relation
in fetch_datum_slice(). Opinions and/or comments are welcome.

[1]: https://github.com/brendangregg/FlameGraph -- Michael
--
Michael

Attachments:

detoast_master.svgimage/svg+xmlDownload
detoast_patch.svgimage/svg+xmlDownload
master_patch_diff.svgimage/svg+xmlDownload
#8Burd, Greg
greg@burd.me
In reply to: Michael Paquier (#6)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

On Jul 7, 2025, at 7:38 PM, Michael Paquier <michael@paquier.xyz> wrote:

I have also pushed this v2 on this branch, so feel free to grab it if
that makes your life easier:
https://github.com/michaelpq/postgres/tree/toast_64bit_v2
--
Michael

Thank you for spending time digging into this and for the well structured patch set (and GitHub branch which I personally find helpful). This $subject is important on its own, but even more so in the broader context of the zstd/dict work [1]https://commitfest.postgresql.org/patch/5702/ and also allowing for innovation when it comes to how externalized Datum are stored. The current model for toasting has served the community well for years, but I think that Hannu [2]"Yes, the idea is to put the tid pointer directly in the varlena external header and have a tid array in the toast table as an extra column. If all of the TOAST fits in the single record, this will be empty, else it will have an array of tids for all the pages for this toasted field." - Hannu Krosing in an email to me after PGConf.dev/2025 and Nikita and others have promising ideas that should be allowable without forcing core changes. I've worked a bit in this area too, I re-based the Pluggble TOAST work by Nikita [3]/messages/by-id/224711f9-83b7-a307-b17f-4457ab73aa0a@sigaev.ru onto 18devel earlier this year as I was looking for a way to implement a toaster for a custom type.

All that aside, I think you're right to tackle this one step at a time and try not to boil too much of the ocean at once (the patch set is already large enough). With that in mind I've read once or twice over your changes and have a few basic comments/questions.

v2-0001 Refactor some TOAST value ID code to use uint64 instead of Oid

This set of changes make sense and as you say are mechanical in nature, no real comments other than I think that using uint64 rather than Oid is the right call and addresses #2 on Tom's list.

v2-0002 Minimize footprint of TOAST_MAX_CHUNK_SIZE in heap TOAST code

I like this as well, clarifies the code and reduces repetition.

v2-0003 Refactor external TOAST pointer code for better pluggability

+ * For now there are only two types, all defined in this file. For now this
+ * is the maximum value of vartag_external, which is a historical choice.

This provides a bridge for compatibility, but doesn't open the door to a truly pluggable API. I'm guessing the goal is incremental change rather than wholesale rewrite.

+ * The different kinds of on-disk external TOAST pointers. divided by
+ * vartag_external.

Extra '.' in "TOAST pointers. divided" I'm guessing.

v2-0004 Introduce new callback to get fresh TOAST values
v2-0005 Add catcache support for INT8OID
v2-0006 Add GUC default_toast_type

Easily understood, good progression of supporting changes.

v2-0007 Introduce global 64-bit TOAST ID counter in control file

Do you have any concern that this might become a bottleneck when there are many relations and many backends all contending for a new id? I'd imagine that this would show up in a flame graph, but I think your test focused on the read side detoast_attr_slice() rather than insert/update and contention on the shared counter. Would this be even worse on NUMA systems?

v2-0008 Switch pg_column_toast_chunk_id() return value from oid to bigint
v2-0009 Add support for bigint TOAST values
v2-0010 Add tests for TOAST relations with bigint as value type
v2-0011 Add support for TOAST table types in pg_dump and pg_restore
v2-0012 Add new vartag_external for 8-byte TOAST values
V2-0013 amcheck: Add test cases for 8-byte TOAST values

I read through each of these patches, I like the break down and the attention to detail. The inclusion of good documentation at each step is helpful. Thank you.

Thanks for the flame graphs examining a heavy detoast_attr_slice() workload. I agree that there is little or no difference between them which is nice.

I think the only call out Tom made [4]/messages/by-id/764273.1669674269@sss.pgh.pa.us that isn't addressed was the ask for localized ID selection. That may make sense at some point, especially if there is contention on GetNewToastId(). I think that case is worth a separate performance test, something with a large number of relations and backends all performing a lot of updates generating a lot of new IDs. What do you think?

As for adding even more flexibility, I see the potential to move in that direction over time with this as a good focused incremental set of changes that address a few important issues now.

Really excited by this work, thank you.

-greg

[1]: https://commitfest.postgresql.org/patch/5702/
[2]: "Yes, the idea is to put the tid pointer directly in the varlena external header and have a tid array in the toast table as an extra column. If all of the TOAST fits in the single record, this will be empty, else it will have an array of tids for all the pages for this toasted field." - Hannu Krosing in an email to me after PGConf.dev/2025
external header and have a tid array in the toast table as an extra
column. If all of the TOAST fits in the single record, this will be
empty, else it will have an array of tids for all the pages for this
toasted field." - Hannu Krosing in an email to me after PGConf.dev/2025
[3]: /messages/by-id/224711f9-83b7-a307-b17f-4457ab73aa0a@sigaev.ru
[4]: /messages/by-id/764273.1669674269@sss.pgh.pa.us

#9Nikita Malakhov
hukutoc@gmail.com
In reply to: Burd, Greg (#8)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

Hi!

Greg, thanks for the interest in our work!

Michael, one more thing forgot to mention yesterday -
#define TOAST_EXTERNAL_INFO_SIZE (VARTAG_ONDISK_OID + 1)
static const toast_external_info
toast_external_infos[TOAST_EXTERNAL_INFO_SIZE]
VARTAG_ONDISK_OID historically has a value of 18
and here we got an array of 19 members with only 2 valid ones.

What do you think about having an individual
TOAST value id counter per relation instead of using
a common one? I think this is a very promising approach,
but a decision must be made where it should be stored.

--
Regards,
Nikita Malakhov
Postgres Professional
The Russian Postgres Company
https://postgrespro.ru/

#10Hannu Krosing
hannuk@google.com
In reply to: Nikita Malakhov (#9)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

I still think we should go with direct toast tid pointers in varlena
and not some kind of oid.

It will remove the need for any oid management and also will be
many-many orders of magnitude faster for large tables (just 2x faster
for in-memory small tables)

I plan to go over Michael's patch set here and see how much change is
needed to add the "direct toast"

My goals are:
1. fast lookup from skipping index lookup
2. making the toast pointer in main heap as small as possible -
hopefully just the 6 bytes of tid pointer - so that scans that do not
need toasted values get more tuples from each page
3. adding all (optional) the extra data into toast chunk record as
there we are free to add whatever is needed
Currently I plan to introduces something like this for toast chunk record

Column | Type | Storage
-------------+---------+----------
chunk_id | oid | plain | 0 when not using toast index, 0xfffe -
non-deletable, for example when used as dictionary for multiple
toasted values.
chunk_seq | integer | plain | if not 0 when referenced from toast
pointer then the toasted data starts at toast_pages[0] (or below it in
that tree), which *must* have chunk_id = 0
chunk_data | bytea | plain

-- added fields

toast_pages | tid[] | plain | can be chained or make up a tree
offsets | int[] | plain | -- starting offsets of the toast_pages
(octets or type-specific units), upper bit is used to indicate that a
new compressed span starts at that offset, 2nd highest bit indicates
that the page is another tree page
comp_method | int | plain | -- compression methos used maybe should be enum ?
dict_pages | tid[] | plain | -- pages to use as compression
dictionary, up to N pages, one level

This seems to be flexible enough to allow for both compressin and
efficient partial updates

---
Hannu

Show quoted text

On Tue, Jul 8, 2025 at 8:31 PM Nikita Malakhov <hukutoc@gmail.com> wrote:

Hi!

Greg, thanks for the interest in our work!

Michael, one more thing forgot to mention yesterday -
#define TOAST_EXTERNAL_INFO_SIZE (VARTAG_ONDISK_OID + 1)
static const toast_external_info toast_external_infos[TOAST_EXTERNAL_INFO_SIZE]
VARTAG_ONDISK_OID historically has a value of 18
and here we got an array of 19 members with only 2 valid ones.

What do you think about having an individual
TOAST value id counter per relation instead of using
a common one? I think this is a very promising approach,
but a decision must be made where it should be stored.

--
Regards,
Nikita Malakhov
Postgres Professional
The Russian Postgres Company
https://postgrespro.ru/

#11Álvaro Herrera
alvherre@kurilemu.de
In reply to: Hannu Krosing (#10)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

On 2025-Jul-08, Hannu Krosing wrote:

I still think we should go with direct toast tid pointers in varlena
and not some kind of oid.

I think this can be made to work, as long as we stop seeing the toast
table just like a normal heap table containing normal tuples. A lot to
reimplement though -- vacuum in particular. Maybe it can be thought of
as a new table AM. Not an easy project, I reckon.

--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/

#12Nikita Malakhov
hukutoc@gmail.com
In reply to: Hannu Krosing (#10)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

Hi,

Hannu, we have some thoughts on direct tids storage,
it was some time ago and done by another developer,
so I have to look. I'll share it as soon as I find it, if you
are interested.

--
Regards,
Nikita Malakhov
Postgres Professional
The Russian Postgres Company
https://postgrespro.ru/

#13Hannu Krosing
hannuk@google.com
In reply to: Álvaro Herrera (#11)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

chunk_

On Tue, Jul 8, 2025 at 9:37 PM Álvaro Herrera <alvherre@kurilemu.de> wrote:

On 2025-Jul-08, Hannu Krosing wrote:

I still think we should go with direct toast tid pointers in varlena
and not some kind of oid.

I think this can be made to work, as long as we stop seeing the toast
table just like a normal heap table containing normal tuples. A lot to
reimplement though -- vacuum in particular.

Non-FULL vacuum should already work. Only commands like VACUUM FULL
and CLUSTER which move tuples around should be disabled on TOAST
tables.

What other parts do you think need re-implementing in addition to
skipping the index lookup part and using the tid directly ?

The fact that per-page chunk_tid arrays allow also tree structures
should allow us much more flexibility in implementing
in-place-updatable structured storage in something otherways very
similar to toast, but this is not required for just moving from oid +
index ==> tid to using the tid directly.

I think that having a toast table as a normal table with full MVCC is
actually a good thing, as it can implement the "array element update"
as a real partial update of only the affected parts and not the
current 'copy everything' way of doing this. We already do collect the
array element update in the parse tree in a special way, now we just
need to have types that can do the partial update by changing a tid or
two in the chunk_tids array (and adjust the offsets array if needed)

This should make both
UPDATE t SET theintarray[3] = 5, theintarray[4] = 7 WHERE id = 1;

and even do partial up[dates for something like this

hannuk=# select * from jtab;
id | j
----+----------------------------
1 | {"a": 3, "b": 2}
2 | {"c": 1, "d": [10, 20, 3]}
(2 rows)
hannuk=# update jtab SET j['d'][3] = '7' WHERE id = 2;
UPDATE 1
hannuk=# select * from jtab;
id | j
----+-------------------------------
1 | {"a": 3, "b": 2}
2 | {"c": 1, "d": [10, 20, 3, 7]}
(2 rows)

when the JSON data is so large that changed part is in it's own chunk.

Maybe it can be thought of
as a new table AM. Not an easy project, I reckon.

I would prefer it to be an extension of current toast - just another
varatt_* type - as then you can upgrade to new storage CONCURRENTLY,
same way as you can currently switch compression methods.

Show quoted text

--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/

#14Michael Paquier
michael@paquier.xyz
In reply to: Hannu Krosing (#10)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

On Tue, Jul 08, 2025 at 08:54:33PM +0200, Hannu Krosing wrote:

I still think we should go with direct toast tid pointers in varlena
and not some kind of oid.

It will remove the need for any oid management and also will be
many-many orders of magnitude faster for large tables (just 2x faster
for in-memory small tables)

There is also the point of backward-compatibility. We cannot just
replace things, and we have to provide a way for users to be able to
rely on the system so as upgrades are painless. So we need to think
about the correct application layer to use to maintain the legacy code
behavior while considering improvements.

I plan to go over Michael's patch set here and see how much change is
needed to add the "direct toast"

If you do not have a lot of time looking at the full patch set, I'd
recommend looking at 0003, files toast_external.h and
toast_external.c which include the key idea. Adding a new external
TOAST pointer is then a two-step process:
- Add a new vartag_external.
- Add some callbacks to let the backend understand what it should do
with this new vartag_external.

My goals are:
1. fast lookup from skipping index lookup
2. making the toast pointer in main heap as small as possible -
hopefully just the 6 bytes of tid pointer - so that scans that do not
need toasted values get more tuples from each page
3. adding all (optional) the extra data into toast chunk record as
there we are free to add whatever is needed
Currently I plan to introduces something like this for toast chunk record

Points 2. and 3. are things that the refactoring should allow. About
1., I have no idea how much you want to store in the TOAST external
points and how it affects the backend, but you could surely implement
an option that lets the backend know that it should still index
lookups based on what the external TOAST pointer says, if this stuff
has benefits.

This seems to be flexible enough to allow for both compressin and
efficient partial updates

I don't really disagree with all that. Now the history of the TOAST
threads point out that we've good at proposing complex things, but
these had a high footprint. What I'm proposing is lighter than that,
I think, tackling my core issue with the infra supporting backward
compatibility and the addition of more modes on top of it.
--
Michael

#15Michael Paquier
michael@paquier.xyz
In reply to: Nikita Malakhov (#9)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

On Tue, Jul 08, 2025 at 09:31:29PM +0300, Nikita Malakhov wrote:

Michael, one more thing forgot to mention yesterday -
#define TOAST_EXTERNAL_INFO_SIZE (VARTAG_ONDISK_OID + 1)
static const toast_external_info
toast_external_infos[TOAST_EXTERNAL_INFO_SIZE]
VARTAG_ONDISK_OID historically has a value of 18
and here we got an array of 19 members with only 2 valid ones.

Yeah, I'm aware of that. The code is mostly to make it easier to read
while dealing with this historical behavior, even if it costs a bit
more in memory. I don't think that it's a big deal, and we could
always have one more level of redirection to reduce its size. Now
there's the extra complexity..

What do you think about having an individual
TOAST value id counter per relation instead of using
a common one? I think this is a very promising approach,
but a decision must be made where it should be stored.

I've thought about that, and decided to discard this idea for now to
keep the whole proposal simpler. This has benefits if you have many
relations with few OIDs consumed, but this has a cost in itself as you
need to maintain the data for each TOAST relation. When I looked at
the problems a couple of weeks ago, I came to the conclusion that all
the checkbox properties of "local" TOAST values are filled with a
sequence: WAL logging to ensure uniqueness, etc. So I was even
considering the addition of some code to create sequences on-the-fly,
but at the end that was just more complexity with how we define
sequences currently compared to a unique 8-byte counter in the
control file that's good enough for a veeery long time.

I've also noticed that this sort of links to a piece I've implemented
last year and is still sitting in the CF app without much interest
from others: sequence AMs. You could implement a "TOAST" sequence
method, for example, optimized for this purpose. As a whole, I
propose to limit the scope of the proposal to the pluggability of the
external TOAST pointers. The approach I've taken should allow such
improvements, these can be added later if really needed.
--
Michael

#16Michael Paquier
michael@paquier.xyz
In reply to: Burd, Greg (#8)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

On Tue, Jul 08, 2025 at 12:58:26PM -0400, Burd, Greg wrote:

All that aside, I think you're right to tackle this one step at a
time and try not to boil too much of the ocean at once (the patch
set is already large enough). With that in mind I've read once or
twice over your changes and have a few basic comments/questions.

v2-0001 Refactor some TOAST value ID code to use uint64 instead of Oid

This set of changes make sense and as you say are mechanical in
nature, no real comments other than I think that using uint64 rather
than Oid is the right call and addresses #2 on Tom's list.

v2-0002 Minimize footprint of TOAST_MAX_CHUNK_SIZE in heap TOAST code

I like this as well, clarifies the code and reduces repetition.

Thanks. These are independent pieces if you want to link the code
less to TOAST, assuming that an area of 8 bytes would be good enough
for any TOAST "value" concept. TIDs were mentioned as well on a
different part of the thread, ItemPointerData is 6 bytes.

v2-0003 Refactor external TOAST pointer code for better pluggability

+ * For now there are only two types, all defined in this file. For now this
+ * is the maximum value of vartag_external, which is a historical choice.

This provides a bridge for compatibility, but doesn't open the door
to a truly pluggable API. I'm guessing the goal is incremental
change rather than wholesale rewrite.

Nope, it does not introduce a pluggable thing, but it does untangle
the fact that one needs to change 15-ish code paths when they want to
add a new type of external TOAST pointer, without showing an actual
impact AFAIK when we insert a TOAST value or fetch it, as long as we
know that we're dealing with an on-disk thing that requires an
external lookup.

+ * The different kinds of on-disk external TOAST pointers. divided by
+ * vartag_external.

Extra '.' in "TOAST pointers. divided" I'm guessing.

Indeed, thanks.

v2-0007 Introduce global 64-bit TOAST ID counter in control file

Do you have any concern that this might become a bottleneck when
there are many relations and many backends all contending for a new
id? I'd imagine that this would show up in a flame graph, but I
think your test focused on the read side detoast_attr_slice() rather
than insert/update and contention on the shared counter. Would this
be even worse on NUMA systems?

That may be possible, see below.

Thanks for the flame graphs examining a heavy detoast_attr_slice()
workload. I agree that there is little or no difference between
them which is nice.

Cool. Yes. I was wondering why detoast_attr_slice() does not show up
in the profile on HEAD, perhaps it just got optimized away (I was
under -O2 for these profiles).

I think the only call out Tom made [4] that isn't addressed was the
ask for localized ID selection. That may make sense at some point,
especially if there is contention on GetNewToastId(). I think that
case is worth a separate performance test, something with a large
number of relations and backends all performing a lot of updates
generating a lot of new IDs. What do you think?

Yeah, I need to do more benchmark for the int8 part, I was holding on
such evaluations because this part of the patch does not fly if we
don't do the refactoring pieces first. Anyway, I cannot get excited
about the extra workload that this would require in the catalogs,
because we would need one TOAST sequence tracked in there, linked to
the TOAST relation so it would not be free. Or we invent a new
facility just for this purpose, meaning that we get far away even more
from being able to resolve the original problem with the values and
compression IDs. We're talking about two instructions. Well, I guess
that we could optimize it more some atomics or even cache a range of
values to save in ToastIdGenLock acquisitions in a single backend. I
suspect that the bottleneck is going to be the insertion of the TOAST
entries in toast_save_datum() anyway with the check for conflicting
values, even if your relation is unlogged or running-on-scissors in
memory.

[2] "Yes, the idea is to put the tid pointer directly in the varlena
external header and have a tid array in the toast table as an extra
column. If all of the TOAST fits in the single record, this will be
empty, else it will have an array of tids for all the pages for this
toasted field." - Hannu Krosing in an email to me after
PGConf.dev/2025

Sure, you could do that as well, but I suspect that we'll need the
steps of at least up to 0003 to be able to handle more easily multiple
external TOAST pointer types, or the code will be messier than it
currently is. :D
--
Michael

#17Nikita Malakhov
hukutoc@gmail.com
In reply to: Hannu Krosing (#13)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

Hi!

On this -

Non-FULL vacuum should already work. Only commands like VACUUM FULL
and CLUSTER which move tuples around should be disabled on TOAST
tables.

Cool, toast tables are subject to bloating in update-heavy scenarios
and it's a big problem in production systems, it seems there is a promising
way to solve it once and for all!

Have to mention though that we encountered issues in logical replication
when we made toast values updatable.

Also researching direct tids implementation.

Cheers!

--
Regards,
Nikita Malakhov
Postgres Professional
The Russian Postgres Company
https://postgrespro.ru/

#18Michael Paquier
michael@paquier.xyz
In reply to: Nikita Malakhov (#17)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

On Mon, Jul 14, 2025 at 09:01:28AM +0300, Nikita Malakhov wrote:

Cool, toast tables are subject to bloating in update-heavy scenarios
and it's a big problem in production systems, it seems there is a promising
way to solve it once and for all!

Have to mention though that we encountered issues in logical replication
when we made toast values updatable.

Also researching direct tids implementation.

I would be curious to see if the refactoring done on this thread would
be useful in the scope of what you are trying to do. I'd suggest
dropping that on a different thread, though, if you finish with a
patch or something worth looking at for others.
--
Michael

#19Nikita Malakhov
hukutoc@gmail.com
In reply to: Michael Paquier (#18)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

Hi Michael,

I'm currently debugging POC direct tids TOAST patch (on top of your branch),
will mail it in a day or two.

On Tue, Jul 15, 2025 at 3:56 AM Michael Paquier <michael@paquier.xyz> wrote:

On Mon, Jul 14, 2025 at 09:01:28AM +0300, Nikita Malakhov wrote:

Cool, toast tables are subject to bloating in update-heavy scenarios
and it's a big problem in production systems, it seems there is a

promising

way to solve it once and for all!

Have to mention though that we encountered issues in logical replication
when we made toast values updatable.

Also researching direct tids implementation.

I would be curious to see if the refactoring done on this thread would
be useful in the scope of what you are trying to do. I'd suggest
dropping that on a different thread, though, if you finish with a
patch or something worth looking at for others.
--
Michael

--
Regards,
Nikita Malakhov
Postgres Professional
The Russian Postgres Company
https://postgrespro.ru/

#20Hannu Krosing
hannuk@google.com
In reply to: Nikita Malakhov (#19)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

On Fri, Jul 18, 2025 at 9:24 PM Nikita Malakhov <hukutoc@gmail.com> wrote:

Hi Michael,

I'm currently debugging POC direct tids TOAST patch (on top of your branch),
will mail it in a day or two.

Great!

I also just started looking at it, starting from 0003 as recommended by Michael.

Will be interesting to see how similar / different our approaches will be :)

#21Hannu Krosing
hannuk@google.com
In reply to: Nikita Malakhov (#17)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

On Mon, Jul 14, 2025 at 8:15 AM Nikita Malakhov <hukutoc@gmail.com> wrote:

...

Have to mention though that we encountered issues in logical replication
when we made toast values updatable.

This seems to indicate that Logical Decoding does not honour
visibility checks in TOAST.
This works fine if the TOAST visibility never changes but will break
if it can change independently of heap-side tuple

Also researching direct tids implementation.

Cool!

#22Michael Paquier
michael@paquier.xyz
In reply to: Nikita Malakhov (#19)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

On Fri, Jul 18, 2025 at 10:24:12PM +0300, Nikita Malakhov wrote:

I'm currently debugging POC direct tids TOAST patch (on top of your branch),
will mail it in a day or two.

Interesting. Of course I may be wrong because I have no idea of how
you have shaped things, still I suspect that for the basics you should
just need 0003, 0004, the parts with the GUC to switch the TOAST table
type and the dump/restore/upgrade bits.
--
Michael

#23Nikhil Kumar Veldanda
veldanda.nikhilkumar17@gmail.com
In reply to: Michael Paquier (#22)
2 attachment(s)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

Hi Michael,

I'm reviewing your patches(toast_64bit_v2 branch) and have prepared
two incremental patches that add ZSTD compression support. While doing
this I made a small refactoring in heaptoast.c (around
ToastTupleContext) to make adding additional TOAST pointer formats
easier.

Added two new on-disk external vartags to support new compression
methods: one using an Oid value identifier and one using a 64‑bit
(int8) identifier for toast table types. This lets us support extended
compression methods for both existing Oid‑based TOAST tables and a
variant that needs a wider ID space.

typedef enum vartag_external
{
VARTAG_INDIRECT = 1,
VARTAG_EXPANDED_RO = 2,
VARTAG_EXPANDED_RW = 3,
VARTAG_ONDISK_INT8 = 4,
VARTAG_ONDISK_CE_OID = 5,
VARTAG_ONDISK_CE_INT8 = 6,
VARTAG_ONDISK_OID = 18
} vartag_external;

Two new ondisk TOAST pointer structs carrying an va_ecinfo field for
extended compression methods:

typedef struct varatt_external_ce_oid
{
int32 va_rawsize; /* Original data size (includes header) */
uint32 va_extinfo; /* External saved size (without header) and
* VARATT_CE_FLAG in top 2 bits */
uint32 va_ecinfo; /* Extended compression info */
Oid va_valueid; /* Unique ID of value within TOAST table */
Oid va_toastrelid; /* RelID of TOAST table containing it */
} varatt_external_ce_oid;

typedef struct varatt_external_ce_int8
{
int32 va_rawsize; /* Original data size (includes header) */
uint32 va_extinfo; /* External saved size (without header) and
* VARATT_CE_FLAG in top 2 bits */
uint32 va_ecinfo; /* Extended compression info */

/*
* Unique ID of value within TOAST table, as two uint32 for alignment and
* padding.
*/
uint32 va_valueid_lo;
uint32 va_valueid_hi;
Oid va_toastrelid; /* RelID of TOAST table containing it */
} varatt_external_ce_int8;

The inline (varattrib_4b) format extension (discussed in [1]/messages/by-id/CAFAfj_HX84EK4hyRYw50AOHOcdVi-+FFwAAPo7JHx4aShCvunQ@mail.gmail.com) is
included; I made one adjustment: the compression id field is now a
4‑byte va_ecinfo field (instead of 1 byte) for structural consistency
with the extended TOAST pointer formats.

typedef union
{
struct /* Normal varlena (4-byte length) */
{
uint32 va_header;
char va_data[FLEXIBLE_ARRAY_MEMBER];
} va_4byte;
struct /* Compressed-in-line format */
{
uint32 va_header;
uint32 va_tcinfo; /* Original data size (excludes header) and
* compression method; see va_extinfo */
char va_data[FLEXIBLE_ARRAY_MEMBER]; /* Compressed data */
} va_compressed;
struct
{
uint32 va_header;
uint32 va_tcinfo; /* Original data size (excludes header) and
* compression method or VARATT_CE_FLAG flag;
* see va_extinfo */
uint32 va_ecinfo; /* Extended compression info: 32-bit field
* where only the lower 8 bits are used for
* compression method. Upper 24 bits are
* reserved/unused. Lower 8 bits layout: Bits
* 7–1: encode (cmid − 2), so cmid is
* [2…129] Bit 0: flag for extra metadata
*/
char va_data[FLEXIBLE_ARRAY_MEMBER];
} va_compressed_ext;
} varattrib_4b;

[1]: /messages/by-id/CAFAfj_HX84EK4hyRYw50AOHOcdVi-+FFwAAPo7JHx4aShCvunQ@mail.gmail.com

Please review it and let me know if you have any questions or
feedback? Thank you!

v26-0014-Design-to-extend-the-varattrib_4b-and-toast-poin.patch:
Design proposal covering varattrib_4b, TOAST pointer layouts, and
related macro updates.
v26-0015-Implement-Zstd-compression-no-dictionary-support.patch: Plain
ZSTD (non dict) support and few basic tests.

--
Nikhil Veldanda

Attachments:

v26-0014-Design-to-extend-the-varattrib_4b-and-toast-poin.patchapplication/octet-stream; name=v26-0014-Design-to-extend-the-varattrib_4b-and-toast-poin.patchDownload
From 098dce72e7f73f91ee04447c589da1177ec49bd7 Mon Sep 17 00:00:00 2001
From: Nikhil Kumar Veldanda <veldanda.nikhilkumar17@gmail.com>
Date: Sat, 19 Jul 2025 23:07:41 +0000
Subject: [PATCH v26 14/15] Design to extend the varattrib_4b and toast pointer
 to support of multiple TOAST compression algorithms.

---
 contrib/amcheck/verify_heapam.c             |   5 +-
 src/backend/access/common/detoast.c         |   6 +-
 src/backend/access/common/toast_external.c  | 176 ++++++++++++++++++--
 src/backend/access/common/toast_internals.c |  27 ++-
 src/backend/access/heap/heaptoast.c         |  49 ++----
 src/backend/access/table/toast_helper.c     |  37 +++-
 src/include/access/toast_compression.h      |   6 +
 src/include/access/toast_helper.h           |   4 +-
 src/include/access/toast_internals.h        |  19 +--
 src/include/varatt.h                        | 115 +++++++++++--
 10 files changed, 335 insertions(+), 109 deletions(-)

diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 958b1451b4f..3a23dddcff4 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1733,7 +1733,10 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	{
 		uint8		va_tag = VARTAG_EXTERNAL(tp + ctx->offset);
 
-		if (va_tag != VARTAG_ONDISK_OID && va_tag != VARTAG_ONDISK_INT8)
+		if (va_tag != VARTAG_ONDISK_OID &&
+			va_tag != VARTAG_ONDISK_INT8 &&
+			va_tag != VARTAG_ONDISK_CE_INT8 &&
+			va_tag != VARTAG_ONDISK_CE_OID)
 		{
 			report_corruption(ctx,
 							  psprintf("toasted attribute has unexpected TOAST tag %u",
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 684e1b0b7d3..34a3f7c6694 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -484,7 +484,7 @@ toast_decompress_datum(struct varlena *attr)
 	 * Fetch the compression method id stored in the compression header and
 	 * decompress the data using the appropriate decompression routine.
 	 */
-	cmid = TOAST_COMPRESS_METHOD(attr);
+	cmid = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(attr);
 	switch (cmid)
 	{
 		case TOAST_PGLZ_COMPRESSION_ID:
@@ -520,14 +520,14 @@ toast_decompress_datum_slice(struct varlena *attr, int32 slicelength)
 	 * have been seen to give wrong results if passed an output size that is
 	 * more than the data's true decompressed size.
 	 */
-	if ((uint32) slicelength >= TOAST_COMPRESS_EXTSIZE(attr))
+	if ((uint32) slicelength >= VARDATA_COMPRESSED_GET_EXTSIZE(attr))
 		return toast_decompress_datum(attr);
 
 	/*
 	 * Fetch the compression method id stored in the compression header and
 	 * decompress the data slice using the appropriate decompression routine.
 	 */
-	cmid = TOAST_COMPRESS_METHOD(attr);
+	cmid = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(attr);
 	switch (cmid)
 	{
 		case TOAST_PGLZ_COMPRESSION_ID:
diff --git a/src/backend/access/common/toast_external.c b/src/backend/access/common/toast_external.c
index 0e79ac8acae..5bf17ed7182 100644
--- a/src/backend/access/common/toast_external.c
+++ b/src/backend/access/common/toast_external.c
@@ -38,6 +38,15 @@ static struct varlena *ondisk_oid_create_external_data(toast_external_data data)
 static uint64 ondisk_oid_get_new_value(Relation toastrel, Oid indexid,
 									   AttrNumber attnum);
 
+/* Callbacks for VARTAG_ONDISK_CE_OID */
+static void ondisk_ce_oid_to_external_data(struct varlena *attr,
+										   toast_external_data *data);
+static struct varlena *ondisk_ce_oid_create_external_data(toast_external_data data);
+
+/* Callbacks for VARTAG_ONDISK_CE_INT8 */
+static void ondisk_ce_int8_to_external_data(struct varlena *attr,
+											toast_external_data *data);
+static struct varlena *ondisk_ce_int8_create_external_data(toast_external_data data);
 
 /*
  * Size of an EXTERNAL datum that contains a standard TOAST pointer
@@ -51,6 +60,18 @@ static uint64 ondisk_oid_get_new_value(Relation toastrel, Oid indexid,
  */
 #define TOAST_POINTER_OID_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_oid))
 
+/*
+ * Size of an EXTERNAL datum that contains a TOAST pointer which supports extended compression methods
+ * (OID value).
+ */
+#define TOAST_POINTER_CE_OID_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_ce_oid))
+
+/*
+ * Size of an EXTERNAL datum that contains a TOAST pointer which supports extended compression methods
+ * (int8 value).
+ */
+#define TOAST_POINTER_CE_INT8_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_ce_int8))
+
 /*
  * For now there are only two types, all defined in this file.  For now this
  * is the maximum value of vartag_external, which is a historical choice.
@@ -72,6 +93,13 @@ static const toast_external_info toast_external_infos[TOAST_EXTERNAL_INFO_SIZE]
 		.create_external_data = ondisk_int8_create_external_data,
 		.get_new_value = ondisk_int8_get_new_value,
 	},
+	[VARTAG_ONDISK_CE_INT8] = {
+		.toast_pointer_size = TOAST_POINTER_CE_INT8_SIZE,
+		.maximum_chunk_size = TOAST_MAX_CHUNK_SIZE_INT8,
+		.to_external_data = ondisk_ce_int8_to_external_data,
+		.create_external_data = ondisk_ce_int8_create_external_data,
+		.get_new_value = ondisk_int8_get_new_value,
+	},
 	[VARTAG_ONDISK_OID] = {
 		.toast_pointer_size = TOAST_POINTER_OID_SIZE,
 		.maximum_chunk_size = TOAST_MAX_CHUNK_SIZE_OID,
@@ -79,6 +107,13 @@ static const toast_external_info toast_external_infos[TOAST_EXTERNAL_INFO_SIZE]
 		.create_external_data = ondisk_oid_create_external_data,
 		.get_new_value = ondisk_oid_get_new_value,
 	},
+	[VARTAG_ONDISK_CE_OID] = {
+		.toast_pointer_size = TOAST_POINTER_CE_OID_SIZE,
+		.maximum_chunk_size = TOAST_MAX_CHUNK_SIZE_OID,
+		.to_external_data = ondisk_ce_oid_to_external_data,
+		.create_external_data = ondisk_ce_oid_create_external_data,
+		.get_new_value = ondisk_oid_get_new_value,
+	},
 };
 
 
@@ -108,7 +143,7 @@ toast_external_info_get_pointer_size(uint8 tag)
 static void
 ondisk_int8_to_external_data(struct varlena *attr, toast_external_data *data)
 {
-	varatt_external_int8	external;
+	varatt_external_int8 external;
 
 	VARATT_EXTERNAL_GET_POINTER(external, attr);
 	data->rawsize = external.va_rawsize;
@@ -117,7 +152,7 @@ ondisk_int8_to_external_data(struct varlena *attr, toast_external_data *data)
 	if (VARATT_EXTERNAL_IS_COMPRESSED(external))
 	{
 		data->extsize = VARATT_EXTERNAL_GET_EXTSIZE(external);
-		data->compression_method = VARATT_EXTERNAL_GET_COMPRESS_METHOD(external);
+		data->compression_method = external.va_extinfo >> VARLENA_EXTSIZE_BITS;
 	}
 	else
 	{
@@ -141,10 +176,10 @@ ondisk_int8_create_external_data(toast_external_data data)
 
 	if (data.compression_method != TOAST_INVALID_COMPRESSION_ID)
 	{
+		/* Regular variants only support basic compression methods */
+		Assert(!CompressionMethodIdIsExtended(data.compression_method));
 		/* Set size and compression method, in a single field. */
-		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(external,
-													 data.extsize,
-													 data.compression_method);
+		external.va_extinfo = (uint32) data.extsize | ((uint32) data.compression_method << VARLENA_EXTSIZE_BITS);
 	}
 	else
 		external.va_extinfo = data.extsize;
@@ -165,8 +200,8 @@ ondisk_int8_get_new_value(Relation toastrel, Oid indexid,
 						  AttrNumber attnum)
 {
 	uint64		new_value;
-	SysScanDesc	scan;
-	ScanKeyData	key;
+	SysScanDesc scan;
+	ScanKeyData key;
 	bool		collides = false;
 
 retry:
@@ -181,8 +216,8 @@ retry:
 	CHECK_FOR_INTERRUPTS();
 
 	/*
-	 * Check if the new value picked already exists in the toast relation.
-	 * If there is a conflict, retry.
+	 * Check if the new value picked already exists in the toast relation. If
+	 * there is a conflict, retry.
 	 */
 	ScanKeyInit(&key,
 				attnum,
@@ -206,7 +241,7 @@ retry:
 static void
 ondisk_oid_to_external_data(struct varlena *attr, toast_external_data *data)
 {
-	varatt_external_oid		external;
+	varatt_external_oid external;
 
 	VARATT_EXTERNAL_GET_POINTER(external, attr);
 	data->rawsize = external.va_rawsize;
@@ -218,7 +253,7 @@ ondisk_oid_to_external_data(struct varlena *attr, toast_external_data *data)
 	if (VARATT_EXTERNAL_IS_COMPRESSED(external))
 	{
 		data->extsize = VARATT_EXTERNAL_GET_EXTSIZE(external);
-		data->compression_method = VARATT_EXTERNAL_GET_COMPRESS_METHOD(external);
+		data->compression_method = external.va_extinfo >> VARLENA_EXTSIZE_BITS;
 	}
 	else
 	{
@@ -240,10 +275,10 @@ ondisk_oid_create_external_data(toast_external_data data)
 
 	if (data.compression_method != TOAST_INVALID_COMPRESSION_ID)
 	{
+		/* Regular variants only support basic compression methods */
+		Assert(!CompressionMethodIdIsExtended(data.compression_method));
 		/* Set size and compression method, in a single field. */
-		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(external,
-													 data.extsize,
-													 data.compression_method);
+		external.va_extinfo = (uint32) data.extsize | ((uint32) data.compression_method << VARLENA_EXTSIZE_BITS);
 	}
 	else
 		external.va_extinfo = data.extsize;
@@ -264,3 +299,116 @@ ondisk_oid_get_new_value(Relation toastrel, Oid indexid,
 {
 	return GetNewOidWithIndex(toastrel, indexid, attnum);
 }
+
+/* Callbacks for VARTAG_ONDISK_CE_OID */
+static void
+ondisk_ce_oid_to_external_data(struct varlena *attr, toast_external_data *data)
+{
+	varatt_external_ce_oid external;
+
+	VARATT_EXTERNAL_GET_POINTER(external, attr);
+	data->rawsize = external.va_rawsize;
+
+	/*
+	 * External size and compression methods are stored in the different
+	 * fields, extract.
+	 */
+	if (VARATT_EXTERNAL_IS_COMPRESSED(external))
+	{
+		data->extsize = VARATT_EXTERNAL_GET_EXTSIZE(external);
+		data->compression_method = VARATT_CE_GET_COMPRESS_METHOD(external.va_ecinfo);
+	}
+	else
+	{
+		data->extsize = external.va_extinfo;
+		data->compression_method = TOAST_INVALID_COMPRESSION_ID;
+	}
+
+	data->value = (uint64) external.va_valueid;
+	data->toastrelid = external.va_toastrelid;
+}
+
+static struct varlena *
+ondisk_ce_oid_create_external_data(toast_external_data data)
+{
+	struct varlena *result = NULL;
+	varatt_external_ce_oid external;
+
+	external.va_rawsize = data.rawsize;
+
+	if (data.compression_method != TOAST_INVALID_COMPRESSION_ID)
+	{
+		Assert(CompressionMethodIdIsExtended(data.compression_method));
+		/* Set size and compression method. */
+		external.va_extinfo = (uint32) data.extsize | (VARATT_CE_FLAG << VARLENA_EXTSIZE_BITS);
+		VARATT_CE_SET_COMPRESS_METHOD(external.va_ecinfo, data.compression_method);
+	}
+	else
+		external.va_extinfo = data.extsize;
+
+	external.va_toastrelid = data.toastrelid;
+	external.va_valueid = (Oid) data.value;
+
+	result = (struct varlena *) palloc(TOAST_POINTER_CE_OID_SIZE);
+	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_CE_OID);
+	memcpy(VARDATA_EXTERNAL(result), &external, sizeof(external));
+
+	return result;
+}
+
+/* Callbacks for VARTAG_ONDISK_CE_INT8 */
+static void
+ondisk_ce_int8_to_external_data(struct varlena *attr, toast_external_data *data)
+{
+	varatt_external_ce_int8 external;
+
+	VARATT_EXTERNAL_GET_POINTER(external, attr);
+	data->rawsize = external.va_rawsize;
+
+	/*
+	 * External size and compression methods are stored in the different
+	 * fields
+	 */
+	if (VARATT_EXTERNAL_IS_COMPRESSED(external))
+	{
+		data->extsize = VARATT_EXTERNAL_GET_EXTSIZE(external);
+		data->compression_method = VARATT_CE_GET_COMPRESS_METHOD(external.va_ecinfo);
+	}
+	else
+	{
+		data->extsize = external.va_extinfo;
+		data->compression_method = TOAST_INVALID_COMPRESSION_ID;
+	}
+
+	data->value = (((uint64) external.va_valueid_hi) << 32) |
+		external.va_valueid_lo;
+	data->toastrelid = external.va_toastrelid;
+}
+
+static struct varlena *
+ondisk_ce_int8_create_external_data(toast_external_data data)
+{
+	struct varlena *result = NULL;
+	varatt_external_ce_int8 external;
+
+	external.va_rawsize = data.rawsize;
+
+	if (data.compression_method != TOAST_INVALID_COMPRESSION_ID)
+	{
+		/* Set size and compression method. */
+		external.va_extinfo = (uint32) data.extsize | (VARATT_CE_FLAG << VARLENA_EXTSIZE_BITS);
+		VARATT_CE_SET_COMPRESS_METHOD(external.va_ecinfo, data.compression_method);
+	}
+	else
+		external.va_extinfo = data.extsize;
+
+	external.va_toastrelid = data.toastrelid;
+	external.va_valueid_hi = (((uint64) data.value) >> 32);
+	external.va_valueid_lo = (uint32) data.value;
+
+	result = (struct varlena *) palloc(TOAST_POINTER_CE_INT8_SIZE);
+	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_CE_INT8);
+	memcpy(VARDATA_EXTERNAL(result), &external, sizeof(external));
+
+	return result;
+}
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index c6b2d1522ce..468aae64676 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -160,18 +160,6 @@ toast_save_datum(Relation rel, Datum value,
 	toast_typid = TupleDescAttr(toasttupDesc, 0)->atttypid;
 	Assert(toast_typid == OIDOID || toast_typid == INT8OID);
 
-	/*
-	 * Grab the information for toast_external_data.
-	 *
-	 * Note: if we support multiple external vartags for a single value
-	 * type, we would need to be smarter in the vartag selection.
-	 */
-	if (toast_typid == OIDOID)
-		tag = VARTAG_ONDISK_OID;
-	else if (toast_typid == INT8OID)
-		tag = VARTAG_ONDISK_INT8;
-	info = toast_external_get_info(tag);
-
 	/* Open all the toast indexes and look for the valid one */
 	validIndex = toast_open_indexes(toastrel,
 									RowExclusiveLock,
@@ -242,6 +230,18 @@ toast_save_datum(Relation rel, Datum value,
 	else
 		toast_pointer.toastrelid = RelationGetRelid(toastrel);
 
+	/*
+	 * Grab the information for toast_external_data.
+	 *
+	 * Note: if we support multiple external vartags for a single value type,
+	 * we would need to be smarter in the vartag selection.
+	 */
+	if (toast_typid == OIDOID)
+		tag = CompressionMethodIdIsExtended(toast_pointer.compression_method) ? VARTAG_ONDISK_CE_OID : VARTAG_ONDISK_OID;
+	else if (toast_typid == INT8OID)
+		tag = CompressionMethodIdIsExtended(toast_pointer.compression_method) ? VARTAG_ONDISK_CE_INT8 : VARTAG_ONDISK_INT8;
+	info = toast_external_get_info(tag);
+
 	/*
 	 * Choose a new value to use as the value ID for this toast value, be it
 	 * for OID or int8-based TOAST relations.
@@ -254,8 +254,7 @@ toast_save_datum(Relation rel, Datum value,
 	 * value (which is a corner case, but possible if the table's attstorage
 	 * options have been changed), we have to pick a value ID that doesn't
 	 * conflict with either new or existing toast value IDs.  If the TOAST
-	 * table uses 8-byte value IDs, we should not really care much about
-	 * that.
+	 * table uses 8-byte value IDs, we should not really care much about that.
 	 */
 	if (!OidIsValid(rel->rd_toastoid))
 	{
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index 87f1630d85f..7b03b27341f 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -152,7 +152,7 @@ heap_toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup,
 	{
 		HeapTuple	atttuple;
 		Form_pg_attribute atttoast;
-		uint8		vartag = VARTAG_ONDISK_OID;
+		ToastTypeId toast_type = TOAST_TYPE_INVALID;
 
 		/*
 		 * XXX: This is very unlikely efficient, but it is not possible to
@@ -166,32 +166,22 @@ heap_toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup,
 		atttoast = (Form_pg_attribute) GETSTRUCT(atttuple);
 
 		if (atttoast->atttypid == OIDOID)
-			vartag = VARTAG_ONDISK_OID;
+			toast_type = TOAST_TYPE_OID;
 		else if (atttoast->atttypid == INT8OID)
-			vartag = VARTAG_ONDISK_INT8;
+			toast_type = TOAST_TYPE_INT8;
 		else
 			Assert(false);
-		ttc.ttc_toast_pointer_size =
-			toast_external_info_get_pointer_size(vartag);
+		ttc.toast_type = toast_type;
 		ReleaseSysCache(atttuple);
 	}
 	else
 	{
 		/*
-		 * No TOAST relation to rely on, which is a case possible when
-		 * dealing with partitioned tables, for example.  Hence, do a best
-		 * guess based on the GUC default_toast_type.
+		 * No TOAST relation to rely on, which is a case possible when dealing
+		 * with partitioned tables, for example.  Hence, do a best guess based
+		 * on the GUC default_toast_type.
 		 */
-		uint8	vartag = VARTAG_ONDISK_OID;
-
-		if (default_toast_type == TOAST_TYPE_INT8)
-			vartag = VARTAG_ONDISK_INT8;
-		else if (default_toast_type == TOAST_TYPE_OID)
-			vartag = VARTAG_ONDISK_OID;
-		else
-			Assert(false);
-		ttc.ttc_toast_pointer_size =
-			toast_external_info_get_pointer_size(vartag);
+		ttc.toast_type = default_toast_type;
 	}
 
 	ttc.ttc_rel = rel;
@@ -693,9 +683,7 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 	int			endchunk;
 	int			num_indexes;
 	int			validIndex;
-	int32		max_chunk_size;
-	const toast_external_info *info;
-	uint8		tag = VARTAG_INDIRECT;  /* init value does not matter */
+	int32		max_chunk_size = TOAST_MAX_CHUNK_SIZE_OID;
 	Oid			toast_typid = InvalidOid;
 
 	/* Look for the valid index of toast relation */
@@ -708,26 +696,23 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 	 * Grab the information for toast_external_data.
 	 *
 	 * Note: there is no access to the vartag of the original varlena from
-	 * which we are trying to retrieve the chunks from the TOAST relation,
-	 * so guess the external TOAST pointer information to use depending
-	 * on the attribute of the TOAST value.  If we begin to support multiple
-	 * external TOAST pointers for a single attribute type, we would need
-	 * to pass down this information from the upper callers.  This is
-	 * currently on required for the maximum chunk_size.
+	 * which we are trying to retrieve the chunks from the TOAST relation, so
+	 * guess the external TOAST pointer information to use depending on the
+	 * attribute of the TOAST value.  If we begin to support multiple external
+	 * TOAST pointers for a single attribute type, we would need to pass down
+	 * this information from the upper callers.  This is currently on required
+	 * for the maximum chunk_size.
 	 */
 	toast_typid = TupleDescAttr(toastrel->rd_att, 0)->atttypid;
 	Assert(toast_typid == OIDOID || toast_typid == INT8OID);
 
 	if (toast_typid == OIDOID)
-		tag = VARTAG_ONDISK_OID;
+		max_chunk_size = TOAST_MAX_CHUNK_SIZE_OID;
 	else if (toast_typid == INT8OID)
-		tag = VARTAG_ONDISK_INT8;
+		max_chunk_size = TOAST_MAX_CHUNK_SIZE_INT8;
 	else
 		Assert(false);
 
-	info = toast_external_get_info(tag);
-	max_chunk_size = info->maximum_chunk_size;
-
 	totalchunks = ((attrsize - 1) / max_chunk_size) + 1;
 	startchunk = sliceoffset / max_chunk_size;
 	endchunk = (sliceoffset + slicelength - 1) / max_chunk_size;
diff --git a/src/backend/access/table/toast_helper.c b/src/backend/access/table/toast_helper.c
index a2b44e093d7..b87d3177bc3 100644
--- a/src/backend/access/table/toast_helper.c
+++ b/src/backend/access/table/toast_helper.c
@@ -15,6 +15,7 @@
 #include "postgres.h"
 
 #include "access/detoast.h"
+#include "access/toast_external.h"
 #include "access/toast_helper.h"
 #include "access/toast_internals.h"
 #include "catalog/pg_type_d.h"
@@ -51,10 +52,19 @@ toast_tuple_init(ToastTupleContext *ttc)
 		Form_pg_attribute att = TupleDescAttr(tupleDesc, i);
 		struct varlena *old_value;
 		struct varlena *new_value;
+		uint8		vartag = VARTAG_ONDISK_OID;
+
+		if (ttc->toast_type == TOAST_TYPE_OID)
+			vartag = CompressionMethodIsExtended(att->attcompression) ? VARTAG_ONDISK_CE_OID : VARTAG_ONDISK_OID;
+		else if (ttc->toast_type == TOAST_TYPE_INT8)
+			vartag = CompressionMethodIsExtended(att->attcompression) ? VARTAG_ONDISK_CE_INT8 : VARTAG_ONDISK_INT8;
+		else
+			Assert(false);
 
 		ttc->ttc_attr[i].tai_colflags = 0;
 		ttc->ttc_attr[i].tai_oldexternal = NULL;
 		ttc->ttc_attr[i].tai_compression = att->attcompression;
+		ttc->ttc_attr[i].toast_pointer_size = toast_external_info_get_pointer_size(vartag);
 
 		if (ttc->ttc_oldvalues != NULL)
 		{
@@ -171,10 +181,10 @@ toast_tuple_init(ToastTupleContext *ttc)
  * The column must have attstorage EXTERNAL or EXTENDED if check_main is
  * false, and must have attstorage MAIN if check_main is true.
  *
- * The column must have a minimum size of MAXALIGN(tcc_toast_pointer_size);
- * if not, no benefit is to be expected by compressing it.  The TOAST
- * pointer size is given by the caller, depending on the type of TOAST
- * table we are dealing with.
+ * Each column must have a minimum size of MAXALIGN(toast_pointer_size) for
+ * that specific column; if not, no benefit is to be expected by compressing it.
+ * The TOAST pointer size varies per column based on the TOAST table type
+ * (OID vs INT8) and different variants used for that specific attribute.
  *
  * The return value is the index of the biggest suitable column, or
  * -1 if there is none.
@@ -190,16 +200,13 @@ toast_tuple_find_biggest_attribute(ToastTupleContext *ttc,
 	int32		skip_colflags = TOASTCOL_IGNORE;
 	int			i;
 
-	/* Define the lower-bound */
-	biggest_size = MAXALIGN(ttc->ttc_toast_pointer_size);
-	Assert(biggest_size != 0);
-
 	if (for_compression)
 		skip_colflags |= TOASTCOL_INCOMPRESSIBLE;
 
 	for (i = 0; i < numAttrs; i++)
 	{
 		Form_pg_attribute att = TupleDescAttr(tupleDesc, i);
+		int32		min_size_for_column;
 
 		if ((ttc->ttc_attr[i].tai_colflags & skip_colflags) != 0)
 			continue;
@@ -214,7 +221,19 @@ toast_tuple_find_biggest_attribute(ToastTupleContext *ttc,
 			att->attstorage != TYPSTORAGE_EXTERNAL)
 			continue;
 
-		if (ttc->ttc_attr[i].tai_size > biggest_size)
+		/*
+		 * Each column has its own minimum size threshold based on its TOAST
+		 * pointer size
+		 */
+		min_size_for_column = MAXALIGN(ttc->ttc_attr[i].toast_pointer_size);
+		Assert(min_size_for_column > 0);
+
+		/*
+		 * Only consider this column if it's bigger than its specific
+		 * threshold AND bigger than current biggest
+		 */
+		if (ttc->ttc_attr[i].tai_size > min_size_for_column &&
+			ttc->ttc_attr[i].tai_size > biggest_size)
 		{
 			biggest_attno = i;
 			biggest_size = ttc->ttc_attr[i].tai_size;
diff --git a/src/include/access/toast_compression.h b/src/include/access/toast_compression.h
index 13c4612ceed..494e1b0dce6 100644
--- a/src/include/access/toast_compression.h
+++ b/src/include/access/toast_compression.h
@@ -51,6 +51,12 @@ typedef enum ToastCompressionId
 #define InvalidCompressionMethod		'\0'
 
 #define CompressionMethodIsValid(cm)  ((cm) != InvalidCompressionMethod)
+#define CompressionMethodIsExtended(cm)	(!(cm == TOAST_PGLZ_COMPRESSION ||		\
+										   cm == TOAST_LZ4_COMPRESSION ||		\
+										   cm == InvalidCompressionMethod))
+#define CompressionMethodIdIsExtended(cmpid)	(!(cmpid == TOAST_PGLZ_COMPRESSION_ID ||	\
+												   cmpid == TOAST_LZ4_COMPRESSION_ID ||		\
+												   cmpid == TOAST_INVALID_COMPRESSION_ID))
 
 
 /* pglz compression/decompression routines */
diff --git a/src/include/access/toast_helper.h b/src/include/access/toast_helper.h
index 729c593afeb..e0ae11a581c 100644
--- a/src/include/access/toast_helper.h
+++ b/src/include/access/toast_helper.h
@@ -14,6 +14,7 @@
 #ifndef TOAST_HELPER_H
 #define TOAST_HELPER_H
 
+#include "access/toast_type.h"
 #include "utils/rel.h"
 
 /*
@@ -33,6 +34,7 @@ typedef struct
 	int32		tai_size;
 	uint8		tai_colflags;
 	char		tai_compression;
+	int32		toast_pointer_size;
 } ToastAttrInfo;
 
 /*
@@ -47,11 +49,11 @@ typedef struct
 	 * should be NULL in the case of an insert.
 	 */
 	Relation	ttc_rel;		/* the relation that contains the tuple */
-	int32		ttc_toast_pointer_size;	/* size of external TOAST pointer */
 	Datum	   *ttc_values;		/* values from the tuple columns */
 	bool	   *ttc_isnull;		/* null flags for the tuple columns */
 	Datum	   *ttc_oldvalues;	/* values from previous tuple */
 	bool	   *ttc_oldisnull;	/* null flags from previous tuple */
+	ToastTypeId toast_type;		/* toast table type */
 
 	/*
 	 * Before calling toast_tuple_init, the caller should set ttc_attr to
diff --git a/src/include/access/toast_internals.h b/src/include/access/toast_internals.h
index 06ae8583c1e..27bc8a0b816 100644
--- a/src/include/access/toast_internals.h
+++ b/src/include/access/toast_internals.h
@@ -17,32 +17,17 @@
 #include "utils/relcache.h"
 #include "utils/snapshot.h"
 
-/*
- *	The information at the start of the compressed toast data.
- */
-typedef struct toast_compress_header
-{
-	int32		vl_len_;		/* varlena header (do not touch directly!) */
-	uint32		tcinfo;			/* 2 bits for compression method and 30 bits
-								 * external size; see va_extinfo */
-} toast_compress_header;
-
 /*
  * Utilities for manipulation of header information for compressed
  * toast entries.
  */
-#define TOAST_COMPRESS_EXTSIZE(ptr) \
-	(((toast_compress_header *) (ptr))->tcinfo & VARLENA_EXTSIZE_MASK)
-#define TOAST_COMPRESS_METHOD(ptr) \
-	(((toast_compress_header *) (ptr))->tcinfo >> VARLENA_EXTSIZE_BITS)
-
 #define TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(ptr, len, cm_method) \
 	do { \
 		Assert((len) > 0 && (len) <= VARLENA_EXTSIZE_MASK); \
 		Assert((cm_method) == TOAST_PGLZ_COMPRESSION_ID || \
 			   (cm_method) == TOAST_LZ4_COMPRESSION_ID); \
-		((toast_compress_header *) (ptr))->tcinfo = \
-			(len) | ((uint32) (cm_method) << VARLENA_EXTSIZE_BITS); \
+		((varattrib_4b *)(ptr))->va_compressed.va_tcinfo = \
+			((uint32)(len)) | ((uint32)(cm_method) << VARLENA_EXTSIZE_BITS); \
 	} while (0)
 
 extern Datum toast_compress_datum(Datum value, char cmethod);
diff --git a/src/include/varatt.h b/src/include/varatt.h
index aa36e8e1f56..52b17c349c3 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -53,17 +53,41 @@ typedef struct varatt_external_int8
 	int32		va_rawsize;		/* Original data size (includes header) */
 	uint32		va_extinfo;		/* External saved size (without header) and
 								 * compression method */
+
 	/*
-	 * Unique ID of value within TOAST table, as two uint32 for alignment
-	 * and padding.
-	 * XXX: think for example about the addition of an extra field for
-	 * meta-data and/or more compression data, even if it's OK here).
+	 * Unique ID of value within TOAST table, as two uint32 for alignment and
+	 * padding.
 	 */
 	uint32		va_valueid_lo;
 	uint32		va_valueid_hi;
 	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
 }			varatt_external_int8;
 
+typedef struct varatt_external_ce_oid
+{
+	int32		va_rawsize;		/* Original data size (includes header) */
+	uint32		va_extinfo;		/* External saved size (without header) and
+								 * VARATT_CE_FLAG in top 2 bits */
+	uint32		va_ecinfo;		/* Extended compression info */
+	Oid			va_valueid;		/* Unique ID of value within TOAST table */
+	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
+}			varatt_external_ce_oid;
+
+typedef struct varatt_external_ce_int8
+{
+	int32		va_rawsize;		/* Original data size (includes header) */
+	uint32		va_extinfo;		/* External saved size (without header) and
+								 * VARATT_CE_FLAG in top 2 bits */
+	uint32		va_ecinfo;		/* Extended compression info */
+
+	/*
+	 * Unique ID of value within TOAST table, as two uint32 for alignment and
+	 * padding.
+	 */
+	uint32		va_valueid_lo;
+	uint32		va_valueid_hi;
+	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
+}			varatt_external_ce_int8;
 
 /*
  * These macros define the "saved size" portion of va_extinfo.  Its remaining
@@ -115,6 +139,8 @@ typedef enum vartag_external
 	VARTAG_EXPANDED_RO = 2,
 	VARTAG_EXPANDED_RW = 3,
 	VARTAG_ONDISK_INT8 = 4,
+	VARTAG_ONDISK_CE_OID = 5,
+	VARTAG_ONDISK_CE_INT8 = 6,
 	VARTAG_ONDISK_OID = 18
 } vartag_external;
 
@@ -127,6 +153,8 @@ typedef enum vartag_external
 	 VARTAG_IS_EXPANDED(tag) ? sizeof(varatt_expanded) : \
 	 (tag) == VARTAG_ONDISK_OID ? sizeof(varatt_external_oid) : \
 	 (tag) == VARTAG_ONDISK_INT8 ? sizeof(varatt_external_int8) : \
+	 (tag) == VARTAG_ONDISK_CE_OID ? sizeof(varatt_external_ce_oid): \
+	 (tag) == VARTAG_ONDISK_CE_INT8 ? sizeof(varatt_external_ce_int8): \
 	 (AssertMacro(false), 0))
 
 /*
@@ -152,6 +180,21 @@ typedef union
 								 * compression method; see va_extinfo */
 		char		va_data[FLEXIBLE_ARRAY_MEMBER]; /* Compressed data */
 	}			va_compressed;
+	struct
+	{
+		uint32		va_header;
+		uint32		va_tcinfo;	/* Original data size (excludes header) and
+								 * compression method or VARATT_CE_FLAG flag;
+								 * see va_extinfo */
+		uint32		va_ecinfo;	/* Extended compression info: 32-bit field
+								 * where only the lower 8 bits are used for
+								 * compression method. Upper 24 bits are
+								 * reserved/unused. Lower 8 bits layout: Bits
+								 * 7–1: encode (cmid − 2), so cmid is
+								 * [2…129] Bit 0: flag for extra metadata
+								 */
+		char		va_data[FLEXIBLE_ARRAY_MEMBER];
+	}			va_compressed_ext;
 } varattrib_4b;
 
 typedef struct
@@ -321,8 +364,13 @@ typedef struct
 	(VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID)
 #define VARATT_IS_EXTERNAL_ONDISK_INT8(PTR) \
 	(VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_INT8)
+#define VARATT_IS_EXTERNAL_ONDISK_CE_OID(PTR) \
+	(VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_CE_OID)
+#define VARATT_IS_EXTERNAL_ONDISK_CE_INT8(PTR) \
+	(VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_CE_INT8)
 #define VARATT_IS_EXTERNAL_ONDISK(PTR) \
-	(VARATT_IS_EXTERNAL_ONDISK_OID(PTR) || VARATT_IS_EXTERNAL_ONDISK_INT8(PTR))
+	(VARATT_IS_EXTERNAL_ONDISK_OID(PTR) || VARATT_IS_EXTERNAL_ONDISK_INT8(PTR) \
+	 || VARATT_IS_EXTERNAL_ONDISK_CE_OID(PTR) || VARATT_IS_EXTERNAL_ONDISK_CE_INT8(PTR))
 #define VARATT_IS_EXTERNAL_INDIRECT(PTR) \
 	(VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_INDIRECT)
 #define VARATT_IS_EXTERNAL_EXPANDED_RO(PTR) \
@@ -359,10 +407,15 @@ typedef struct
 	 (VARATT_IS_1B(PTR) ? VARDATA_1B(PTR) : VARDATA_4B(PTR))
 
 /* Decompressed size and compression method of a compressed-in-line Datum */
-#define VARDATA_COMPRESSED_GET_EXTSIZE(PTR) \
-	(((varattrib_4b *) (PTR))->va_compressed.va_tcinfo & VARLENA_EXTSIZE_MASK)
+#define VARDATA_COMPRESSED_GET_EXTSIZE(PTR)														\
+	(																							\
+		(VARATT_IS_EXTENDED_COMPRESSED(PTR))													\
+			? ( ((varattrib_4b *)(PTR))->va_compressed_ext.va_tcinfo & VARLENA_EXTSIZE_MASK )	\
+			: ( ((varattrib_4b *)(PTR))->va_compressed.va_tcinfo & VARLENA_EXTSIZE_MASK )		\
+	)
 #define VARDATA_COMPRESSED_GET_COMPRESS_METHOD(PTR) \
-	(((varattrib_4b *) (PTR))->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS)
+	( (VARATT_IS_EXTENDED_COMPRESSED(PTR)) ? VARATT_CE_GET_COMPRESS_METHOD(((varattrib_4b *) (PTR))->va_compressed_ext.va_ecinfo) \
+	: (((varattrib_4b *) (PTR))->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS))
 
 /*
  * Same for external Datums; but note argument is a struct
@@ -370,16 +423,6 @@ typedef struct
  */
 #define VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) \
 	((toast_pointer).va_extinfo & VARLENA_EXTSIZE_MASK)
-#define VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) \
-	((toast_pointer).va_extinfo >> VARLENA_EXTSIZE_BITS)
-
-#define VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, len, cm) \
-	do { \
-		Assert((cm) == TOAST_PGLZ_COMPRESSION_ID || \
-			   (cm) == TOAST_LZ4_COMPRESSION_ID); \
-		((toast_pointer).va_extinfo = \
-			(len) | ((uint32) (cm) << VARLENA_EXTSIZE_BITS)); \
-	} while (0)
 
 /*
  * Testing whether an externally-stored value is compressed now requires
@@ -393,5 +436,41 @@ typedef struct
  (VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) < \
   (toast_pointer).va_rawsize - VARHDRSZ)
 
+/* Extended compression flag (0b11) marks use of extended compression methods */
+#define VARATT_CE_FLAG             0x3
+
+/*
+ * Extended compression info encoding (8-bit layout):
+ *
+ *   bit 7   6   5   4   3   2   1   0
+ *  +---+---+---+---+---+---+---+---+
+ *  |      compression_id       | M |
+ *  +---+---+---+---+---+---+---+---+
+ *
+ * • Bits 7–1: Compression method ID offset (cmid − 2)
+ *   Range: [0…127] maps to compression ID [2…129]
+ *
+ * • Bit 0 (M): Metadata flag (currently unused, always 0)
+ *   Reserved for future use to indicate extra compression metadata
+ */
+#define VARATT_CE_SET_COMPRESS_METHOD(va_ecinfo, cmid)		\
+	do {													\
+		uint8 _cmid = (uint8)(cmid);						\
+		Assert(_cmid >= 2 && _cmid <= 129);					\
+		(va_ecinfo) = (uint32)((_cmid - 2) << 1);			\
+	} while (0)
+
+#define VARATT_CE_GET_COMPRESS_METHOD(ecinfo)	((((uint8)(ecinfo) >> 1) & 0x7F) + 2)
+
+/* Test if varattrib_4b uses extended compression format */
+#define VARATT_IS_EXTENDED_COMPRESSED(ptr) \
+	((((varattrib_4b *)(ptr))->va_compressed_ext.va_tcinfo >> VARLENA_EXTSIZE_BITS) \
+		== VARATT_CE_FLAG)
+
+/* Access compressed data payload in extended format */
+#define VARDATA_EXTENDED_COMPRESSED(ptr) \
+	(((varattrib_4b *)(ptr))->va_compressed_ext.va_data)
+
+#define VARHDRSZ_EXTENDED_COMPRESSED	(offsetof(varattrib_4b, va_compressed_ext.va_data))
 
 #endif
-- 
2.47.1

v26-0015-Implement-Zstd-compression-no-dictionary-support.patchapplication/octet-stream; name=v26-0015-Implement-Zstd-compression-no-dictionary-support.patchDownload
From de11b29d6dc29acc4685c2dc5d90cfea876f4577 Mon Sep 17 00:00:00 2001
From: Nikhil Kumar Veldanda <veldanda.nikhilkumar17@gmail.com>
Date: Sat, 19 Jul 2025 23:31:23 +0000
Subject: [PATCH v26 15/15] Implement Zstd compression (no dictionary support)

---
 contrib/amcheck/verify_heapam.c               |   1 +
 doc/src/sgml/catalogs.sgml                    |   1 +
 doc/src/sgml/config.sgml                      |  12 +-
 doc/src/sgml/ref/alter_table.sgml             |   8 +-
 doc/src/sgml/ref/create_table.sgml            |   7 +-
 src/backend/access/common/detoast.c           |  12 +-
 src/backend/access/common/toast_compression.c | 139 ++++++++++
 src/backend/access/common/toast_internals.c   |   4 +
 src/backend/utils/adt/varlena.c               |   3 +
 src/backend/utils/misc/guc_tables.c           |   3 +
 src/backend/utils/misc/postgresql.conf.sample |   2 +-
 src/bin/pg_dump/pg_dump.c                     |   3 +
 src/bin/psql/describe.c                       |   5 +-
 src/bin/psql/tab-complete.in.c                |   2 +-
 src/include/access/toast_compression.h        |  33 ++-
 src/include/access/toast_internals.h          |  22 +-
 .../regress/expected/compression_zstd.out     | 249 ++++++++++++++++++
 .../regress/expected/compression_zstd_1.out   |   7 +
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/compression_zstd.sql     | 129 +++++++++
 20 files changed, 605 insertions(+), 39 deletions(-)
 create mode 100644 src/test/regress/expected/compression_zstd.out
 create mode 100644 src/test/regress/expected/compression_zstd_1.out
 create mode 100644 src/test/regress/sql/compression_zstd.sql

diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 3a23dddcff4..e482ddc106a 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1806,6 +1806,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 				/* List of all valid compression method IDs */
 			case TOAST_PGLZ_COMPRESSION_ID:
 			case TOAST_LZ4_COMPRESSION_ID:
+			case TOAST_ZSTD_COMPRESSION_ID:
 				valid = true;
 				break;
 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 0d23bc1b122..ee0c1a1f185 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -1249,6 +1249,7 @@
        (see <xref linkend="guc-default-toast-compression"/>).  Otherwise,
        <literal>'p'</literal> selects pglz compression, while
        <literal>'l'</literal> selects <productname>LZ4</productname>
+       compression, and <literal>'z'</literal> selects <productname>ZSTD</productname>
        compression.  However, this field is ignored
        whenever <structfield>attstorage</structfield> does not allow
        compression.
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 93d948e9161..7d784147032 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -3404,8 +3404,8 @@ include_dir 'conf.d'
         A compressed page image will be decompressed during WAL replay.
         The supported methods are <literal>pglz</literal>,
         <literal>lz4</literal> (if <productname>PostgreSQL</productname>
-        was compiled with <option>--with-lz4</option>) and
-        <literal>zstd</literal> (if <productname>PostgreSQL</productname>
+        was compiled with <option>--with-lz4</option>),
+        and <literal>zstd</literal> (if <productname>PostgreSQL</productname>
         was compiled with <option>--with-zstd</option>).
         The default value is <literal>off</literal>.
         Only superusers and users with the appropriate <literal>SET</literal>
@@ -9824,9 +9824,11 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
         the <literal>COMPRESSION</literal> column option in
         <command>CREATE TABLE</command> or
         <command>ALTER TABLE</command>.)
-        The supported compression methods are <literal>pglz</literal> and
-        (if <productname>PostgreSQL</productname> was compiled with
-        <option>--with-lz4</option>) <literal>lz4</literal>.
+        The supported compression methods are <literal>pglz</literal>,
+        <literal>lz4</literal> (if <productname>PostgreSQL</productname>
+        was compiled with <option>--with-lz4</option>),
+        and <literal>zstd</literal> (if <productname>PostgreSQL</productname>
+        was compiled with <option>--with-zstd</option>).
         The default is <literal>pglz</literal>.
        </para>
       </listitem>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 1e4f26c13f6..d4bd847d416 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -443,10 +443,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       its existing compression method, rather than being recompressed with the
       compression method of the target column.
       The supported compression
-      methods are <literal>pglz</literal> and <literal>lz4</literal>.
-      (<literal>lz4</literal> is available only if <option>--with-lz4</option>
-      was used when building <productname>PostgreSQL</productname>.)  In
-      addition, <replaceable class="parameter">compression_method</replaceable>
+      methods are <literal>pglz</literal>,
+      <literal>lz4</literal> (if <productname>PostgreSQL</productname> was compiled with <option>--with-lz4</option>),
+      and <literal>zstd</literal> (if <productname>PostgreSQL</productname> was compiled with <option>--with-zstd</option>).
+      In addition, <replaceable class="parameter">compression_method</replaceable>
       can be <literal>default</literal>, which selects the default behavior of
       consulting the <xref linkend="guc-default-toast-compression"/> setting
       at the time of data insertion to determine the method to use.
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index dc000e913c1..4fd68af2a09 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -343,10 +343,9 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       column storage modes.) Setting this property for a partitioned table
       has no direct effect, because such tables have no storage of their own,
       but the configured value will be inherited by newly-created partitions.
-      The supported compression methods are <literal>pglz</literal> and
-      <literal>lz4</literal>.  (<literal>lz4</literal> is available only if
-      <option>--with-lz4</option> was used when building
-      <productname>PostgreSQL</productname>.)  In addition,
+      The supported compression methods are <literal>pglz</literal>,
+      <literal>lz4</literal> (if <productname>PostgreSQL</productname> was compiled with <option>--with-lz4</option>),
+      and <literal>zstd</literal> (if <productname>PostgreSQL</productname> was compiled with <option>--with-zstd</option>). In addition,
       <replaceable class="parameter">compression_method</replaceable>
       can be <literal>default</literal> to explicitly specify the default
       behavior, which is to consult the
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 34a3f7c6694..0b28a5e7365 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -247,10 +247,10 @@ detoast_attr_slice(struct varlena *attr,
 			 * Determine maximum amount of compressed data needed for a prefix
 			 * of a given length (after decompression).
 			 *
-			 * At least for now, if it's LZ4 data, we'll have to fetch the
-			 * whole thing, because there doesn't seem to be an API call to
-			 * determine how much compressed data we need to be sure of being
-			 * able to decompress the required slice.
+			 * At least for now, if it's LZ4 or Zstandard data, we'll have to
+			 * fetch the whole thing, because there doesn't seem to be an API
+			 * call to determine how much compressed data we need to be sure
+			 * of being able to decompress the required slice.
 			 */
 			if (toast_pointer.compression_method == TOAST_PGLZ_COMPRESSION_ID)
 				max_size = pglz_maximum_compressed_size(slicelimit, max_size);
@@ -491,6 +491,8 @@ toast_decompress_datum(struct varlena *attr)
 			return pglz_decompress_datum(attr);
 		case TOAST_LZ4_COMPRESSION_ID:
 			return lz4_decompress_datum(attr);
+		case TOAST_ZSTD_COMPRESSION_ID:
+			return zstd_decompress_datum(attr);
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 			return NULL;		/* keep compiler quiet */
@@ -534,6 +536,8 @@ toast_decompress_datum_slice(struct varlena *attr, int32 slicelength)
 			return pglz_decompress_datum_slice(attr, slicelength);
 		case TOAST_LZ4_COMPRESSION_ID:
 			return lz4_decompress_datum_slice(attr, slicelength);
+		case TOAST_ZSTD_COMPRESSION_ID:
+			return zstd_decompress_datum_slice(attr, slicelength);
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 			return NULL;		/* keep compiler quiet */
diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c
index 94606a58c8f..56b4af251e1 100644
--- a/src/backend/access/common/toast_compression.c
+++ b/src/backend/access/common/toast_compression.c
@@ -17,6 +17,10 @@
 #include <lz4.h>
 #endif
 
+#ifdef USE_ZSTD
+#include <zstd.h>
+#endif
+
 #include "access/detoast.h"
 #include "access/toast_compression.h"
 #include "access/toast_external.h"
@@ -246,6 +250,132 @@ lz4_decompress_datum_slice(const struct varlena *value, int32 slicelength)
 #endif
 }
 
+/* Compress datum using ZSTD */
+struct varlena *
+zstd_compress_datum(const struct varlena *value)
+{
+#ifdef USE_ZSTD
+	uint32		valsize = VARSIZE_ANY_EXHDR(value);
+	size_t		max_size = ZSTD_compressBound(valsize);
+	struct varlena *compressed;
+	size_t		cmp_size;
+
+	/* Allocate space for the compressed varlena (header + data) */
+	compressed = (struct varlena *) palloc(max_size + VARHDRSZ_EXTENDED_COMPRESSED);
+
+	cmp_size = ZSTD_compress(VARDATA_EXTENDED_COMPRESSED(compressed),
+							 max_size,
+							 VARDATA_ANY(value),
+							 valsize,
+							 ZSTD_CLEVEL_DEFAULT);
+
+	if (ZSTD_isError(cmp_size))
+		elog(ERROR, "zstd compression failed");
+
+	/**
+	 *  If compression did not reduce size, return NULL so that the uncompressed data is stored
+	 */
+	if (cmp_size > valsize)
+	{
+		pfree(compressed);
+		return NULL;
+	}
+
+	/* Set the compressed size in the varlena header */
+	SET_VARSIZE_COMPRESSED(compressed, cmp_size + VARHDRSZ_EXTENDED_COMPRESSED);
+
+	return compressed;
+
+#else
+	NO_COMPRESSION_SUPPORT("zstd");
+	return NULL;
+#endif
+}
+
+/* Decompression routine */
+struct varlena *
+zstd_decompress_datum(const struct varlena *value)
+{
+#ifdef USE_ZSTD
+	uint32		actual_size_exhdr = VARDATA_COMPRESSED_GET_EXTSIZE(value);
+	uint32		cmplen;
+	struct varlena *result;
+	size_t		ucmplen;
+
+	cmplen = VARSIZE_ANY(value) - VARHDRSZ_EXTENDED_COMPRESSED;
+
+	/* Allocate space for the uncompressed data */
+	result = (struct varlena *) palloc(actual_size_exhdr + VARHDRSZ);
+
+	ucmplen = ZSTD_decompress(VARDATA(result),
+							  actual_size_exhdr,
+							  VARDATA_EXTENDED_COMPRESSED(value),
+							  cmplen);
+
+	if (ZSTD_isError(ucmplen))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATA_CORRUPTED),
+				 errmsg_internal("compressed zstd data is corrupt")));
+
+	/* Set final size in the varlena header */
+	SET_VARSIZE(result, ucmplen + VARHDRSZ);
+	return result;
+
+#else
+	NO_COMPRESSION_SUPPORT("zstd");
+	return NULL;
+#endif
+}
+
+/* Decompress a slice of the datum */
+struct varlena *
+zstd_decompress_datum_slice(const struct varlena *value, int32 slicelength)
+{
+#ifdef USE_ZSTD
+	/* ZSTD no dictionary compression */
+
+	struct varlena *result;
+	ZSTD_inBuffer inBuf;
+	ZSTD_outBuffer outBuf;
+	size_t		ret;
+	ZSTD_DCtx  *zstdDctx = ZSTD_createDCtx();
+
+	if (!zstdDctx)
+		elog(ERROR, "could not create zstd decompression context");
+
+	inBuf.src = VARDATA_EXTENDED_COMPRESSED(value);
+	inBuf.size = VARSIZE_ANY(value) - VARHDRSZ_EXTENDED_COMPRESSED;
+	inBuf.pos = 0;
+
+	result = (struct varlena *) palloc(slicelength + VARHDRSZ);
+	outBuf.dst = (char *) result + VARHDRSZ;
+	outBuf.size = slicelength;
+	outBuf.pos = 0;
+
+	/* Common decompression loop */
+	while (inBuf.pos < inBuf.size && outBuf.pos < outBuf.size)
+	{
+		ret = ZSTD_decompressStream(zstdDctx, &outBuf, &inBuf);
+		if (ZSTD_isError(ret))
+		{
+			ZSTD_freeDCtx(zstdDctx);
+			ereport(ERROR,
+					(errcode(ERRCODE_DATA_CORRUPTED),
+					 errmsg_internal("compressed zstd data is corrupt")));
+		}
+	}
+
+	ZSTD_freeDCtx(zstdDctx);
+	Assert(outBuf.size == slicelength && outBuf.pos == slicelength);
+	SET_VARSIZE(result, outBuf.pos + VARHDRSZ);
+
+	return result;
+#else
+	NO_COMPRESSION_SUPPORT("zstd");
+	return NULL;
+#endif
+}
+
 /*
  * Extract compression ID from a varlena.
  *
@@ -287,6 +417,13 @@ CompressionNameToMethod(const char *compression)
 #endif
 		return TOAST_LZ4_COMPRESSION;
 	}
+	else if (strcmp(compression, "zstd") == 0)
+	{
+#ifndef USE_ZSTD
+		NO_COMPRESSION_SUPPORT("zstd");
+#endif
+		return TOAST_ZSTD_COMPRESSION;
+	}
 
 	return InvalidCompressionMethod;
 }
@@ -303,6 +440,8 @@ GetCompressionMethodName(char method)
 			return "pglz";
 		case TOAST_LZ4_COMPRESSION:
 			return "lz4";
+		case TOAST_ZSTD_COMPRESSION:
+			return "zstd";
 		default:
 			elog(ERROR, "invalid compression method %c", method);
 			return NULL;		/* keep compiler quiet */
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 468aae64676..35be926b048 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -72,6 +72,10 @@ toast_compress_datum(Datum value, char cmethod)
 			tmp = lz4_compress_datum((const struct varlena *) value);
 			cmid = TOAST_LZ4_COMPRESSION_ID;
 			break;
+		case TOAST_ZSTD_COMPRESSION:
+			tmp = zstd_compress_datum((const struct varlena *) value);
+			cmid = TOAST_ZSTD_COMPRESSION_ID;
+			break;
 		default:
 			elog(ERROR, "invalid compression method %c", cmethod);
 	}
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 26c720449f7..3b4dd692d37 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -4204,6 +4204,9 @@ pg_column_compression(PG_FUNCTION_ARGS)
 		case TOAST_LZ4_COMPRESSION_ID:
 			result = "lz4";
 			break;
+		case TOAST_ZSTD_COMPRESSION_ID:
+			result = "zstd";
+			break;
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 	}
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index be523c9ac09..74c1fe424c4 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -461,6 +461,9 @@ static const struct config_enum_entry default_toast_compression_options[] = {
 	{"pglz", TOAST_PGLZ_COMPRESSION, false},
 #ifdef  USE_LZ4
 	{"lz4", TOAST_LZ4_COMPRESSION, false},
+#endif
+#ifdef  USE_ZSTD
+	{"zstd", TOAST_ZSTD_COMPRESSION, false},
 #endif
 	{NULL, 0, false}
 };
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 5f34b14ea39..0f1dc0dc05c 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -752,7 +752,7 @@ autovacuum_worker_slots = 16	# autovacuum worker slots to allocate
 #row_security = on
 #default_table_access_method = 'heap'
 #default_tablespace = ''		# a tablespace name, '' uses the default
-#default_toast_compression = 'pglz'	# 'pglz' or 'lz4'
+#default_toast_compression = 'pglz'	# 'pglz' or 'lz4' or 'zstd'
 #default_toast_type = 'oid'		# 'oid' or 'int8'
 #temp_tablespaces = ''			# a list of tablespace names, '' uses
 					# only default tablespace
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 7afb0d1a925..5a9353fe15d 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -17692,6 +17692,9 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 					case 'l':
 						cmname = "lz4";
 						break;
+					case 'z':
+						cmname = "zstd";
+						break;
 					default:
 						cmname = NULL;
 						break;
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index dd25d2fe7b8..e073f6766e8 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2172,8 +2172,9 @@ describeOneTableDetails(const char *schemaname,
 			/* these strings are literal in our syntax, so not translated. */
 			printTableAddCell(&cont, (compression[0] == 'p' ? "pglz" :
 									  (compression[0] == 'l' ? "lz4" :
-									   (compression[0] == '\0' ? "" :
-										"???"))),
+									   (compression[0] == 'z' ? "zstd" :
+										(compression[0] == '\0' ? "" :
+										 "???")))),
 							  false, false);
 		}
 
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 37524364290..9032902a5c6 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2913,7 +2913,7 @@ match_previous_words(int pattern_id,
 	/* ALTER TABLE ALTER [COLUMN] <foo> SET COMPRESSION */
 	else if (Matches("ALTER", "TABLE", MatchAny, "ALTER", "COLUMN", MatchAny, "SET", "COMPRESSION") ||
 			 Matches("ALTER", "TABLE", MatchAny, "ALTER", MatchAny, "SET", "COMPRESSION"))
-		COMPLETE_WITH("DEFAULT", "PGLZ", "LZ4");
+		COMPLETE_WITH("DEFAULT", "PGLZ", "LZ4", "ZSTD");
 	/* ALTER TABLE ALTER [COLUMN] <foo> SET EXPRESSION */
 	else if (Matches("ALTER", "TABLE", MatchAny, "ALTER", "COLUMN", MatchAny, "SET", "EXPRESSION") ||
 			 Matches("ALTER", "TABLE", MatchAny, "ALTER", MatchAny, "SET", "EXPRESSION"))
diff --git a/src/include/access/toast_compression.h b/src/include/access/toast_compression.h
index 494e1b0dce6..accc4746a56 100644
--- a/src/include/access/toast_compression.h
+++ b/src/include/access/toast_compression.h
@@ -13,6 +13,10 @@
 #ifndef TOAST_COMPRESSION_H
 #define TOAST_COMPRESSION_H
 
+#ifdef USE_ZSTD
+#include <zstd.h>
+#endif
+
 /*
  * GUC support.
  *
@@ -23,22 +27,25 @@
 extern PGDLLIMPORT int default_toast_compression;
 
 /*
- * Built-in compression method ID.  The toast compression header will store
- * this in the first 2 bits of the raw length.  These built-in compression
- * method IDs are directly mapped to the built-in compression methods.
+ * Built-in compression method ID.
+ *
+ * For TOAST-compressed values:
+ *   - If using a non-extended method, the first 2 bits of the raw length
+ *		field store this ID.
+ *   - If using an extended method, it is stored in the extended 4-byte header.
  *
- * Don't use these values for anything other than understanding the meaning
- * of the raw bits from a varlena; in particular, if the goal is to identify
- * a compression method, use the constants TOAST_PGLZ_COMPRESSION, etc.
- * below. We might someday support more than 4 compression methods, but
- * we can never have more than 4 values in this enum, because there are
- * only 2 bits available in the places where this is stored.
+ * These IDs map directly to the built-in compression methods.
+ *
+ * Note: Do not use these values for anything other than interpreting the
+ * raw bits from a varlena. To identify a compression method in code, use
+ * the named constants (e.g., TOAST_PGLZ_COMPRESSION) instead.
  */
 typedef enum ToastCompressionId
 {
 	TOAST_PGLZ_COMPRESSION_ID = 0,
 	TOAST_LZ4_COMPRESSION_ID = 1,
-	TOAST_INVALID_COMPRESSION_ID = 2,
+	TOAST_ZSTD_COMPRESSION_ID = 2,
+	TOAST_INVALID_COMPRESSION_ID = 3,
 } ToastCompressionId;
 
 /*
@@ -48,6 +55,7 @@ typedef enum ToastCompressionId
  */
 #define TOAST_PGLZ_COMPRESSION			'p'
 #define TOAST_LZ4_COMPRESSION			'l'
+#define TOAST_ZSTD_COMPRESSION			'z'
 #define InvalidCompressionMethod		'\0'
 
 #define CompressionMethodIsValid(cm)  ((cm) != InvalidCompressionMethod)
@@ -71,6 +79,11 @@ extern struct varlena *lz4_decompress_datum(const struct varlena *value);
 extern struct varlena *lz4_decompress_datum_slice(const struct varlena *value,
 												  int32 slicelength);
 
+/* zstd nodict compression/decompression routines */
+extern struct varlena *zstd_compress_datum(const struct varlena *value);
+extern struct varlena *zstd_decompress_datum(const struct varlena *value);
+extern struct varlena *zstd_decompress_datum_slice(const struct varlena *value, int32 slicelength);
+
 /* other stuff */
 extern ToastCompressionId toast_get_compression_id(struct varlena *attr);
 extern char CompressionNameToMethod(const char *compression);
diff --git a/src/include/access/toast_internals.h b/src/include/access/toast_internals.h
index 27bc8a0b816..5fb8ca93fdd 100644
--- a/src/include/access/toast_internals.h
+++ b/src/include/access/toast_internals.h
@@ -21,13 +21,21 @@
  * Utilities for manipulation of header information for compressed
  * toast entries.
  */
-#define TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(ptr, len, cm_method) \
-	do { \
-		Assert((len) > 0 && (len) <= VARLENA_EXTSIZE_MASK); \
-		Assert((cm_method) == TOAST_PGLZ_COMPRESSION_ID || \
-			   (cm_method) == TOAST_LZ4_COMPRESSION_ID); \
-		((varattrib_4b *)(ptr))->va_compressed.va_tcinfo = \
-			((uint32)(len)) | ((uint32)(cm_method) << VARLENA_EXTSIZE_BITS); \
+#define TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(ptr, len, cm_method)														\
+	do {																														\
+		Assert((len) > 0 && (len) <= VARLENA_EXTSIZE_MASK);																		\
+		Assert((cm_method) == TOAST_PGLZ_COMPRESSION_ID ||																		\
+				(cm_method) == TOAST_LZ4_COMPRESSION_ID ||																		\
+				(cm_method) == TOAST_ZSTD_COMPRESSION_ID);																		\
+		if (!CompressionMethodIdIsExtended((cm_method)))																		\
+			((varattrib_4b *)(ptr))->va_compressed.va_tcinfo = ((uint32)(len)) | ((uint32)(cm_method) << VARLENA_EXTSIZE_BITS);	\
+		else																													\
+		{																														\
+			/* extended path: mark EXT flag in tcinfo */																		\
+			((varattrib_4b *)(ptr))->va_compressed_ext.va_tcinfo =																\
+				((uint32)(len)) | ((uint32)(VARATT_CE_FLAG) << VARLENA_EXTSIZE_BITS);											\
+			VARATT_CE_SET_COMPRESS_METHOD(((varattrib_4b *)(ptr))->va_compressed_ext.va_ecinfo, (cm_method));					\
+		}																														\
 	} while (0)
 
 extern Datum toast_compress_datum(Datum value, char cmethod);
diff --git a/src/test/regress/expected/compression_zstd.out b/src/test/regress/expected/compression_zstd.out
new file mode 100644
index 00000000000..166ba022541
--- /dev/null
+++ b/src/test/regress/expected/compression_zstd.out
@@ -0,0 +1,249 @@
+-- Tests for TOAST compression with zstd
+SELECT NOT(enumvals @> '{zstd}') AS skip_test FROM pg_settings WHERE
+  name = 'default_toast_compression' \gset
+\if :skip_test
+   \echo '*** skipping TOAST tests with zstd (not supported) ***'
+   \quit
+\endif
+CREATE SCHEMA zstd;
+SET search_path TO zstd, public;
+\set HIDE_TOAST_COMPRESSION false
+-- Ensure we get stable results regardless of the installation's default.
+-- We rely on this GUC value for a few tests.
+SET default_toast_compression = 'pglz';
+-- test creating table with compression method
+CREATE TABLE cmdata_pglz(f1 text COMPRESSION pglz);
+CREATE INDEX idx ON cmdata_pglz(f1);
+INSERT INTO cmdata_pglz VALUES(repeat('1234567890', 1000));
+\d+ cmdata
+CREATE TABLE cmdata_zstd(f1 TEXT COMPRESSION zstd);
+INSERT INTO cmdata_zstd VALUES(repeat('1234567890', 1004));
+\d+ cmdata1
+-- verify stored compression method in the data
+SELECT pg_column_compression(f1) FROM cmdata_zstd;
+ pg_column_compression 
+-----------------------
+ zstd
+(1 row)
+
+-- decompress data slice
+SELECT SUBSTR(f1, 200, 5) FROM cmdata_pglz;
+ substr 
+--------
+ 01234
+(1 row)
+
+SELECT SUBSTR(f1, 2000, 50) FROM cmdata_zstd;
+                       substr                       
+----------------------------------------------------
+ 01234567890123456789012345678901234567890123456789
+(1 row)
+
+-- copy with table creation
+SELECT * INTO cmmove1 FROM cmdata_zstd;
+\d+ cmmove1
+                                         Table "zstd.cmmove1"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ f1     | text |           |          |         | extended |             |              | 
+
+SELECT pg_column_compression(f1) FROM cmmove1;
+ pg_column_compression 
+-----------------------
+ zstd
+(1 row)
+
+-- test LIKE INCLUDING COMPRESSION.  The GUC default_toast_compression
+-- has no effect, the compression method from the table being copied.
+CREATE TABLE cmdata2 (LIKE cmdata_zstd INCLUDING COMPRESSION);
+\d+ cmdata2
+                                         Table "zstd.cmdata2"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ f1     | text |           |          |         | extended | zstd        |              | 
+
+DROP TABLE cmdata2;
+-- copy to existing table
+CREATE TABLE cmmove3(f1 text COMPRESSION pglz);
+INSERT INTO cmmove3 SELECT * FROM cmdata_pglz;
+INSERT INTO cmmove3 SELECT * FROM cmdata_zstd;
+SELECT pg_column_compression(f1) FROM cmmove3;
+ pg_column_compression 
+-----------------------
+ pglz
+ zstd
+(2 rows)
+
+-- update using datum from different table with zstd data.
+CREATE TABLE cmmove2(f1 text COMPRESSION pglz);
+INSERT INTO cmmove2 VALUES (repeat('1234567890', 1004));
+SELECT pg_column_compression(f1) FROM cmmove2;
+ pg_column_compression 
+-----------------------
+ pglz
+(1 row)
+
+UPDATE cmmove2 SET f1 = cmdata_zstd.f1 FROM cmdata_zstd;
+SELECT pg_column_compression(f1) FROM cmmove2;
+ pg_column_compression 
+-----------------------
+ zstd
+(1 row)
+
+-- test externally stored compressed data
+CREATE OR REPLACE FUNCTION large_val_zstd() RETURNS TEXT LANGUAGE SQL AS
+'select array_agg(fipshash(g::text))::text from generate_series(1, 256) g';
+CREATE TABLE cmdata2 (f1 text COMPRESSION zstd);
+INSERT INTO cmdata2 SELECT large_val_zstd() || repeat('a', 4000);
+SELECT pg_column_compression(f1) FROM cmdata2;
+ pg_column_compression 
+-----------------------
+ zstd
+(1 row)
+
+SELECT SUBSTR(f1, 200, 5) FROM cmdata2;
+ substr 
+--------
+ 79026
+(1 row)
+
+DROP TABLE cmdata2;
+DROP FUNCTION large_val_zstd;
+-- test compression with materialized view
+CREATE MATERIALIZED VIEW compressmv(x) AS SELECT * FROM cmdata_zstd;
+\d+ compressmv
+                                 Materialized view "zstd.compressmv"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ x      | text |           |          |         | extended |             |              | 
+View definition:
+ SELECT f1 AS x
+   FROM cmdata_zstd;
+
+SELECT pg_column_compression(f1) FROM cmdata_zstd;
+ pg_column_compression 
+-----------------------
+ zstd
+(1 row)
+
+SELECT pg_column_compression(x) FROM compressmv;
+ pg_column_compression 
+-----------------------
+ zstd
+(1 row)
+
+-- test compression with partition
+CREATE TABLE cmpart(f1 text COMPRESSION zstd) PARTITION BY HASH(f1);
+CREATE TABLE cmpart1 PARTITION OF cmpart FOR VALUES WITH (MODULUS 2, REMAINDER 0);
+CREATE TABLE cmpart2(f1 text COMPRESSION pglz);
+ALTER TABLE cmpart ATTACH PARTITION cmpart2 FOR VALUES WITH (MODULUS 2, REMAINDER 1);
+INSERT INTO cmpart VALUES (repeat('123456789', 1004));
+INSERT INTO cmpart VALUES (repeat('123456789', 4004));
+SELECT pg_column_compression(f1) FROM cmpart1;
+ pg_column_compression 
+-----------------------
+ zstd
+(1 row)
+
+SELECT pg_column_compression(f1) FROM cmpart2;
+ pg_column_compression 
+-----------------------
+ pglz
+(1 row)
+
+-- test compression with inheritance
+CREATE TABLE cminh() INHERITS(cmdata_pglz, cmdata_zstd); -- error
+NOTICE:  merging multiple inherited definitions of column "f1"
+ERROR:  column "f1" has a compression method conflict
+DETAIL:  pglz versus zstd
+CREATE TABLE cminh(f1 TEXT COMPRESSION zstd) INHERITS(cmdata_pglz); -- error
+NOTICE:  merging column "f1" with inherited definition
+ERROR:  column "f1" has a compression method conflict
+DETAIL:  pglz versus zstd
+CREATE TABLE cmdata3(f1 text);
+CREATE TABLE cminh() INHERITS (cmdata_pglz, cmdata3);
+NOTICE:  merging multiple inherited definitions of column "f1"
+-- test default_toast_compression GUC
+SET default_toast_compression = 'zstd';
+-- test alter compression method
+ALTER TABLE cmdata_pglz ALTER COLUMN f1 SET COMPRESSION zstd;
+INSERT INTO cmdata_pglz VALUES (repeat('123456789', 4004));
+\d+ cmdata
+SELECT pg_column_compression(f1) FROM cmdata_pglz;
+ pg_column_compression 
+-----------------------
+ pglz
+ zstd
+(2 rows)
+
+ALTER TABLE cmdata_pglz ALTER COLUMN f1 SET COMPRESSION pglz;
+-- test alter compression method for materialized views
+ALTER MATERIALIZED VIEW compressmv ALTER COLUMN x SET COMPRESSION zstd;
+\d+ compressmv
+                                 Materialized view "zstd.compressmv"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ x      | text |           |          |         | extended | zstd        |              | 
+View definition:
+ SELECT f1 AS x
+   FROM cmdata_zstd;
+
+-- test alter compression method for partitioned tables
+ALTER TABLE cmpart1 ALTER COLUMN f1 SET COMPRESSION pglz;
+ALTER TABLE cmpart2 ALTER COLUMN f1 SET COMPRESSION zstd;
+-- new data should be compressed with the current compression method
+INSERT INTO cmpart VALUES (repeat('123456789', 1004));
+INSERT INTO cmpart VALUES (repeat('123456789', 4004));
+SELECT pg_column_compression(f1) FROM cmpart1;
+ pg_column_compression 
+-----------------------
+ zstd
+ pglz
+(2 rows)
+
+SELECT pg_column_compression(f1) FROM cmpart2;
+ pg_column_compression 
+-----------------------
+ pglz
+ zstd
+(2 rows)
+
+-- test expression index
+CREATE TABLE cmdata2 (f1 TEXT COMPRESSION pglz, f2 TEXT COMPRESSION zstd);
+CREATE UNIQUE INDEX idx1 ON cmdata2 ((f1 || f2));
+INSERT INTO cmdata2 VALUES((SELECT array_agg(fipshash(g::TEXT))::TEXT FROM
+generate_series(1, 50) g), VERSION());
+-- check data is ok
+SELECT length(f1) FROM cmdata_pglz;
+ length 
+--------
+  10000
+  36036
+(2 rows)
+
+SELECT length(f1) FROM cmdata_zstd;
+ length 
+--------
+  10040
+(1 row)
+
+SELECT length(f1) FROM cmmove1;
+ length 
+--------
+  10040
+(1 row)
+
+SELECT length(f1) FROM cmmove2;
+ length 
+--------
+  10040
+(1 row)
+
+SELECT length(f1) FROM cmmove3;
+ length 
+--------
+  10000
+  10040
+(2 rows)
+
+\set HIDE_TOAST_COMPRESSION true
diff --git a/src/test/regress/expected/compression_zstd_1.out b/src/test/regress/expected/compression_zstd_1.out
new file mode 100644
index 00000000000..5f07342fd51
--- /dev/null
+++ b/src/test/regress/expected/compression_zstd_1.out
@@ -0,0 +1,7 @@
+-- Tests for TOAST compression with zstd
+SELECT NOT(enumvals @> '{zstd}') AS skip_test FROM pg_settings WHERE
+  name = 'default_toast_compression' \gset
+\if :skip_test
+   \echo '*** skipping TOAST tests with zstd (not supported) ***'
+*** skipping TOAST tests with zstd (not supported) ***
+   \quit
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index fbffc67ae60..1ef4797cd10 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -123,7 +123,7 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion tr
 # The stats test resets stats, so nothing else needing stats access can be in
 # this group.
 # ----------
-test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression compression_lz4 memoize stats predicate numa
+test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression compression_lz4 compression_zstd memoize stats predicate numa
 
 # event_trigger depends on create_am and cannot run concurrently with
 # any test that runs DDL
diff --git a/src/test/regress/sql/compression_zstd.sql b/src/test/regress/sql/compression_zstd.sql
new file mode 100644
index 00000000000..134d1d44327
--- /dev/null
+++ b/src/test/regress/sql/compression_zstd.sql
@@ -0,0 +1,129 @@
+-- Tests for TOAST compression with zstd
+
+SELECT NOT(enumvals @> '{zstd}') AS skip_test FROM pg_settings WHERE
+  name = 'default_toast_compression' \gset
+\if :skip_test
+   \echo '*** skipping TOAST tests with zstd (not supported) ***'
+   \quit
+\endif
+
+CREATE SCHEMA zstd;
+SET search_path TO zstd, public;
+
+\set HIDE_TOAST_COMPRESSION false
+
+-- Ensure we get stable results regardless of the installation's default.
+-- We rely on this GUC value for a few tests.
+SET default_toast_compression = 'pglz';
+
+-- test creating table with compression method
+CREATE TABLE cmdata_pglz(f1 text COMPRESSION pglz);
+CREATE INDEX idx ON cmdata_pglz(f1);
+INSERT INTO cmdata_pglz VALUES(repeat('1234567890', 1000));
+\d+ cmdata
+CREATE TABLE cmdata_zstd(f1 TEXT COMPRESSION zstd);
+INSERT INTO cmdata_zstd VALUES(repeat('1234567890', 1004));
+\d+ cmdata1
+
+-- verify stored compression method in the data
+SELECT pg_column_compression(f1) FROM cmdata_zstd;
+
+-- decompress data slice
+SELECT SUBSTR(f1, 200, 5) FROM cmdata_pglz;
+SELECT SUBSTR(f1, 2000, 50) FROM cmdata_zstd;
+
+-- copy with table creation
+SELECT * INTO cmmove1 FROM cmdata_zstd;
+\d+ cmmove1
+SELECT pg_column_compression(f1) FROM cmmove1;
+
+-- test LIKE INCLUDING COMPRESSION.  The GUC default_toast_compression
+-- has no effect, the compression method from the table being copied.
+CREATE TABLE cmdata2 (LIKE cmdata_zstd INCLUDING COMPRESSION);
+\d+ cmdata2
+DROP TABLE cmdata2;
+
+-- copy to existing table
+CREATE TABLE cmmove3(f1 text COMPRESSION pglz);
+INSERT INTO cmmove3 SELECT * FROM cmdata_pglz;
+INSERT INTO cmmove3 SELECT * FROM cmdata_zstd;
+SELECT pg_column_compression(f1) FROM cmmove3;
+
+-- update using datum from different table with zstd data.
+CREATE TABLE cmmove2(f1 text COMPRESSION pglz);
+INSERT INTO cmmove2 VALUES (repeat('1234567890', 1004));
+SELECT pg_column_compression(f1) FROM cmmove2;
+UPDATE cmmove2 SET f1 = cmdata_zstd.f1 FROM cmdata_zstd;
+SELECT pg_column_compression(f1) FROM cmmove2;
+
+-- test externally stored compressed data
+CREATE OR REPLACE FUNCTION large_val_zstd() RETURNS TEXT LANGUAGE SQL AS
+'select array_agg(fipshash(g::text))::text from generate_series(1, 256) g';
+CREATE TABLE cmdata2 (f1 text COMPRESSION zstd);
+INSERT INTO cmdata2 SELECT large_val_zstd() || repeat('a', 4000);
+SELECT pg_column_compression(f1) FROM cmdata2;
+SELECT SUBSTR(f1, 200, 5) FROM cmdata2;
+DROP TABLE cmdata2;
+DROP FUNCTION large_val_zstd;
+
+-- test compression with materialized view
+CREATE MATERIALIZED VIEW compressmv(x) AS SELECT * FROM cmdata_zstd;
+\d+ compressmv
+SELECT pg_column_compression(f1) FROM cmdata_zstd;
+SELECT pg_column_compression(x) FROM compressmv;
+
+-- test compression with partition
+CREATE TABLE cmpart(f1 text COMPRESSION zstd) PARTITION BY HASH(f1);
+CREATE TABLE cmpart1 PARTITION OF cmpart FOR VALUES WITH (MODULUS 2, REMAINDER 0);
+CREATE TABLE cmpart2(f1 text COMPRESSION pglz);
+
+ALTER TABLE cmpart ATTACH PARTITION cmpart2 FOR VALUES WITH (MODULUS 2, REMAINDER 1);
+INSERT INTO cmpart VALUES (repeat('123456789', 1004));
+INSERT INTO cmpart VALUES (repeat('123456789', 4004));
+SELECT pg_column_compression(f1) FROM cmpart1;
+SELECT pg_column_compression(f1) FROM cmpart2;
+
+-- test compression with inheritance
+CREATE TABLE cminh() INHERITS(cmdata_pglz, cmdata_zstd); -- error
+CREATE TABLE cminh(f1 TEXT COMPRESSION zstd) INHERITS(cmdata_pglz); -- error
+CREATE TABLE cmdata3(f1 text);
+CREATE TABLE cminh() INHERITS (cmdata_pglz, cmdata3);
+
+-- test default_toast_compression GUC
+SET default_toast_compression = 'zstd';
+
+-- test alter compression method
+ALTER TABLE cmdata_pglz ALTER COLUMN f1 SET COMPRESSION zstd;
+INSERT INTO cmdata_pglz VALUES (repeat('123456789', 4004));
+\d+ cmdata
+SELECT pg_column_compression(f1) FROM cmdata_pglz;
+ALTER TABLE cmdata_pglz ALTER COLUMN f1 SET COMPRESSION pglz;
+
+-- test alter compression method for materialized views
+ALTER MATERIALIZED VIEW compressmv ALTER COLUMN x SET COMPRESSION zstd;
+\d+ compressmv
+
+-- test alter compression method for partitioned tables
+ALTER TABLE cmpart1 ALTER COLUMN f1 SET COMPRESSION pglz;
+ALTER TABLE cmpart2 ALTER COLUMN f1 SET COMPRESSION zstd;
+
+-- new data should be compressed with the current compression method
+INSERT INTO cmpart VALUES (repeat('123456789', 1004));
+INSERT INTO cmpart VALUES (repeat('123456789', 4004));
+SELECT pg_column_compression(f1) FROM cmpart1;
+SELECT pg_column_compression(f1) FROM cmpart2;
+
+-- test expression index
+CREATE TABLE cmdata2 (f1 TEXT COMPRESSION pglz, f2 TEXT COMPRESSION zstd);
+CREATE UNIQUE INDEX idx1 ON cmdata2 ((f1 || f2));
+INSERT INTO cmdata2 VALUES((SELECT array_agg(fipshash(g::TEXT))::TEXT FROM
+generate_series(1, 50) g), VERSION());
+
+-- check data is ok
+SELECT length(f1) FROM cmdata_pglz;
+SELECT length(f1) FROM cmdata_zstd;
+SELECT length(f1) FROM cmmove1;
+SELECT length(f1) FROM cmmove2;
+SELECT length(f1) FROM cmmove3;
+
+\set HIDE_TOAST_COMPRESSION true
-- 
2.47.1

#24Nikhil Kumar Veldanda
veldanda.nikhilkumar17@gmail.com
In reply to: Nikhil Kumar Veldanda (#23)
2 attachment(s)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

Hi,

v26-0014-Design-to-extend-the-varattrib_4b-and-toast-poin.patch:
Design proposal covering varattrib_4b, TOAST pointer layouts, and
related macro updates.
v26-0015-Implement-Zstd-compression-no-dictionary-support.patch: Plain
ZSTD (non dict) support and few basic tests.

Sending v27 patch with a small update over v26 patch.

v27-0014-Design-to-extend-the-varattrib_4b-and-toast-poin.patch:
Design proposal covering varattrib_4b, TOAST pointer layouts, and
related macro updates.
v27-0015-Implement-Zstd-compression-no-dictionary-support.patch: Plain
ZSTD (non dict) support and few basic tests.

--
Nikhil Veldanda

--
Nikhil Veldanda

Attachments:

v27-0015-Implement-Zstd-compression-no-dictionary-support.patchapplication/octet-stream; name=v27-0015-Implement-Zstd-compression-no-dictionary-support.patchDownload
From 02642175aa79f3d895744d17680004c85839a7d9 Mon Sep 17 00:00:00 2001
From: Nikhil Kumar Veldanda <veldanda.nikhilkumar17@gmail.com>
Date: Sat, 19 Jul 2025 23:31:23 +0000
Subject: [PATCH v27 15/15] Implement Zstd compression (no dictionary support)

---
 contrib/amcheck/verify_heapam.c               |   1 +
 doc/src/sgml/catalogs.sgml                    |   1 +
 doc/src/sgml/config.sgml                      |  12 +-
 doc/src/sgml/ref/alter_table.sgml             |   8 +-
 doc/src/sgml/ref/create_table.sgml            |   7 +-
 src/backend/access/common/detoast.c           |  12 +-
 src/backend/access/common/toast_compression.c | 139 ++++++++++
 src/backend/access/common/toast_internals.c   |   4 +
 src/backend/utils/adt/varlena.c               |   3 +
 src/backend/utils/misc/guc_tables.c           |   3 +
 src/backend/utils/misc/postgresql.conf.sample |   2 +-
 src/bin/pg_dump/pg_dump.c                     |   3 +
 src/bin/psql/describe.c                       |   5 +-
 src/bin/psql/tab-complete.in.c                |   2 +-
 src/include/access/toast_compression.h        |  33 ++-
 src/include/access/toast_internals.h          |  22 +-
 .../regress/expected/compression_zstd.out     | 249 ++++++++++++++++++
 .../regress/expected/compression_zstd_1.out   |   7 +
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/compression_zstd.sql     | 129 +++++++++
 20 files changed, 605 insertions(+), 39 deletions(-)
 create mode 100644 src/test/regress/expected/compression_zstd.out
 create mode 100644 src/test/regress/expected/compression_zstd_1.out
 create mode 100644 src/test/regress/sql/compression_zstd.sql

diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 3a23dddcff4..e482ddc106a 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1806,6 +1806,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 				/* List of all valid compression method IDs */
 			case TOAST_PGLZ_COMPRESSION_ID:
 			case TOAST_LZ4_COMPRESSION_ID:
+			case TOAST_ZSTD_COMPRESSION_ID:
 				valid = true;
 				break;
 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 0d23bc1b122..ee0c1a1f185 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -1249,6 +1249,7 @@
        (see <xref linkend="guc-default-toast-compression"/>).  Otherwise,
        <literal>'p'</literal> selects pglz compression, while
        <literal>'l'</literal> selects <productname>LZ4</productname>
+       compression, and <literal>'z'</literal> selects <productname>ZSTD</productname>
        compression.  However, this field is ignored
        whenever <structfield>attstorage</structfield> does not allow
        compression.
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 93d948e9161..7d784147032 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -3404,8 +3404,8 @@ include_dir 'conf.d'
         A compressed page image will be decompressed during WAL replay.
         The supported methods are <literal>pglz</literal>,
         <literal>lz4</literal> (if <productname>PostgreSQL</productname>
-        was compiled with <option>--with-lz4</option>) and
-        <literal>zstd</literal> (if <productname>PostgreSQL</productname>
+        was compiled with <option>--with-lz4</option>),
+        and <literal>zstd</literal> (if <productname>PostgreSQL</productname>
         was compiled with <option>--with-zstd</option>).
         The default value is <literal>off</literal>.
         Only superusers and users with the appropriate <literal>SET</literal>
@@ -9824,9 +9824,11 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
         the <literal>COMPRESSION</literal> column option in
         <command>CREATE TABLE</command> or
         <command>ALTER TABLE</command>.)
-        The supported compression methods are <literal>pglz</literal> and
-        (if <productname>PostgreSQL</productname> was compiled with
-        <option>--with-lz4</option>) <literal>lz4</literal>.
+        The supported compression methods are <literal>pglz</literal>,
+        <literal>lz4</literal> (if <productname>PostgreSQL</productname>
+        was compiled with <option>--with-lz4</option>),
+        and <literal>zstd</literal> (if <productname>PostgreSQL</productname>
+        was compiled with <option>--with-zstd</option>).
         The default is <literal>pglz</literal>.
        </para>
       </listitem>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 1e4f26c13f6..d4bd847d416 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -443,10 +443,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       its existing compression method, rather than being recompressed with the
       compression method of the target column.
       The supported compression
-      methods are <literal>pglz</literal> and <literal>lz4</literal>.
-      (<literal>lz4</literal> is available only if <option>--with-lz4</option>
-      was used when building <productname>PostgreSQL</productname>.)  In
-      addition, <replaceable class="parameter">compression_method</replaceable>
+      methods are <literal>pglz</literal>,
+      <literal>lz4</literal> (if <productname>PostgreSQL</productname> was compiled with <option>--with-lz4</option>),
+      and <literal>zstd</literal> (if <productname>PostgreSQL</productname> was compiled with <option>--with-zstd</option>).
+      In addition, <replaceable class="parameter">compression_method</replaceable>
       can be <literal>default</literal>, which selects the default behavior of
       consulting the <xref linkend="guc-default-toast-compression"/> setting
       at the time of data insertion to determine the method to use.
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index dc000e913c1..4fd68af2a09 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -343,10 +343,9 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       column storage modes.) Setting this property for a partitioned table
       has no direct effect, because such tables have no storage of their own,
       but the configured value will be inherited by newly-created partitions.
-      The supported compression methods are <literal>pglz</literal> and
-      <literal>lz4</literal>.  (<literal>lz4</literal> is available only if
-      <option>--with-lz4</option> was used when building
-      <productname>PostgreSQL</productname>.)  In addition,
+      The supported compression methods are <literal>pglz</literal>,
+      <literal>lz4</literal> (if <productname>PostgreSQL</productname> was compiled with <option>--with-lz4</option>),
+      and <literal>zstd</literal> (if <productname>PostgreSQL</productname> was compiled with <option>--with-zstd</option>). In addition,
       <replaceable class="parameter">compression_method</replaceable>
       can be <literal>default</literal> to explicitly specify the default
       behavior, which is to consult the
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 34a3f7c6694..0b28a5e7365 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -247,10 +247,10 @@ detoast_attr_slice(struct varlena *attr,
 			 * Determine maximum amount of compressed data needed for a prefix
 			 * of a given length (after decompression).
 			 *
-			 * At least for now, if it's LZ4 data, we'll have to fetch the
-			 * whole thing, because there doesn't seem to be an API call to
-			 * determine how much compressed data we need to be sure of being
-			 * able to decompress the required slice.
+			 * At least for now, if it's LZ4 or Zstandard data, we'll have to
+			 * fetch the whole thing, because there doesn't seem to be an API
+			 * call to determine how much compressed data we need to be sure
+			 * of being able to decompress the required slice.
 			 */
 			if (toast_pointer.compression_method == TOAST_PGLZ_COMPRESSION_ID)
 				max_size = pglz_maximum_compressed_size(slicelimit, max_size);
@@ -491,6 +491,8 @@ toast_decompress_datum(struct varlena *attr)
 			return pglz_decompress_datum(attr);
 		case TOAST_LZ4_COMPRESSION_ID:
 			return lz4_decompress_datum(attr);
+		case TOAST_ZSTD_COMPRESSION_ID:
+			return zstd_decompress_datum(attr);
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 			return NULL;		/* keep compiler quiet */
@@ -534,6 +536,8 @@ toast_decompress_datum_slice(struct varlena *attr, int32 slicelength)
 			return pglz_decompress_datum_slice(attr, slicelength);
 		case TOAST_LZ4_COMPRESSION_ID:
 			return lz4_decompress_datum_slice(attr, slicelength);
+		case TOAST_ZSTD_COMPRESSION_ID:
+			return zstd_decompress_datum_slice(attr, slicelength);
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 			return NULL;		/* keep compiler quiet */
diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c
index 94606a58c8f..56b4af251e1 100644
--- a/src/backend/access/common/toast_compression.c
+++ b/src/backend/access/common/toast_compression.c
@@ -17,6 +17,10 @@
 #include <lz4.h>
 #endif
 
+#ifdef USE_ZSTD
+#include <zstd.h>
+#endif
+
 #include "access/detoast.h"
 #include "access/toast_compression.h"
 #include "access/toast_external.h"
@@ -246,6 +250,132 @@ lz4_decompress_datum_slice(const struct varlena *value, int32 slicelength)
 #endif
 }
 
+/* Compress datum using ZSTD */
+struct varlena *
+zstd_compress_datum(const struct varlena *value)
+{
+#ifdef USE_ZSTD
+	uint32		valsize = VARSIZE_ANY_EXHDR(value);
+	size_t		max_size = ZSTD_compressBound(valsize);
+	struct varlena *compressed;
+	size_t		cmp_size;
+
+	/* Allocate space for the compressed varlena (header + data) */
+	compressed = (struct varlena *) palloc(max_size + VARHDRSZ_EXTENDED_COMPRESSED);
+
+	cmp_size = ZSTD_compress(VARDATA_EXTENDED_COMPRESSED(compressed),
+							 max_size,
+							 VARDATA_ANY(value),
+							 valsize,
+							 ZSTD_CLEVEL_DEFAULT);
+
+	if (ZSTD_isError(cmp_size))
+		elog(ERROR, "zstd compression failed");
+
+	/**
+	 *  If compression did not reduce size, return NULL so that the uncompressed data is stored
+	 */
+	if (cmp_size > valsize)
+	{
+		pfree(compressed);
+		return NULL;
+	}
+
+	/* Set the compressed size in the varlena header */
+	SET_VARSIZE_COMPRESSED(compressed, cmp_size + VARHDRSZ_EXTENDED_COMPRESSED);
+
+	return compressed;
+
+#else
+	NO_COMPRESSION_SUPPORT("zstd");
+	return NULL;
+#endif
+}
+
+/* Decompression routine */
+struct varlena *
+zstd_decompress_datum(const struct varlena *value)
+{
+#ifdef USE_ZSTD
+	uint32		actual_size_exhdr = VARDATA_COMPRESSED_GET_EXTSIZE(value);
+	uint32		cmplen;
+	struct varlena *result;
+	size_t		ucmplen;
+
+	cmplen = VARSIZE_ANY(value) - VARHDRSZ_EXTENDED_COMPRESSED;
+
+	/* Allocate space for the uncompressed data */
+	result = (struct varlena *) palloc(actual_size_exhdr + VARHDRSZ);
+
+	ucmplen = ZSTD_decompress(VARDATA(result),
+							  actual_size_exhdr,
+							  VARDATA_EXTENDED_COMPRESSED(value),
+							  cmplen);
+
+	if (ZSTD_isError(ucmplen))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATA_CORRUPTED),
+				 errmsg_internal("compressed zstd data is corrupt")));
+
+	/* Set final size in the varlena header */
+	SET_VARSIZE(result, ucmplen + VARHDRSZ);
+	return result;
+
+#else
+	NO_COMPRESSION_SUPPORT("zstd");
+	return NULL;
+#endif
+}
+
+/* Decompress a slice of the datum */
+struct varlena *
+zstd_decompress_datum_slice(const struct varlena *value, int32 slicelength)
+{
+#ifdef USE_ZSTD
+	/* ZSTD no dictionary compression */
+
+	struct varlena *result;
+	ZSTD_inBuffer inBuf;
+	ZSTD_outBuffer outBuf;
+	size_t		ret;
+	ZSTD_DCtx  *zstdDctx = ZSTD_createDCtx();
+
+	if (!zstdDctx)
+		elog(ERROR, "could not create zstd decompression context");
+
+	inBuf.src = VARDATA_EXTENDED_COMPRESSED(value);
+	inBuf.size = VARSIZE_ANY(value) - VARHDRSZ_EXTENDED_COMPRESSED;
+	inBuf.pos = 0;
+
+	result = (struct varlena *) palloc(slicelength + VARHDRSZ);
+	outBuf.dst = (char *) result + VARHDRSZ;
+	outBuf.size = slicelength;
+	outBuf.pos = 0;
+
+	/* Common decompression loop */
+	while (inBuf.pos < inBuf.size && outBuf.pos < outBuf.size)
+	{
+		ret = ZSTD_decompressStream(zstdDctx, &outBuf, &inBuf);
+		if (ZSTD_isError(ret))
+		{
+			ZSTD_freeDCtx(zstdDctx);
+			ereport(ERROR,
+					(errcode(ERRCODE_DATA_CORRUPTED),
+					 errmsg_internal("compressed zstd data is corrupt")));
+		}
+	}
+
+	ZSTD_freeDCtx(zstdDctx);
+	Assert(outBuf.size == slicelength && outBuf.pos == slicelength);
+	SET_VARSIZE(result, outBuf.pos + VARHDRSZ);
+
+	return result;
+#else
+	NO_COMPRESSION_SUPPORT("zstd");
+	return NULL;
+#endif
+}
+
 /*
  * Extract compression ID from a varlena.
  *
@@ -287,6 +417,13 @@ CompressionNameToMethod(const char *compression)
 #endif
 		return TOAST_LZ4_COMPRESSION;
 	}
+	else if (strcmp(compression, "zstd") == 0)
+	{
+#ifndef USE_ZSTD
+		NO_COMPRESSION_SUPPORT("zstd");
+#endif
+		return TOAST_ZSTD_COMPRESSION;
+	}
 
 	return InvalidCompressionMethod;
 }
@@ -303,6 +440,8 @@ GetCompressionMethodName(char method)
 			return "pglz";
 		case TOAST_LZ4_COMPRESSION:
 			return "lz4";
+		case TOAST_ZSTD_COMPRESSION:
+			return "zstd";
 		default:
 			elog(ERROR, "invalid compression method %c", method);
 			return NULL;		/* keep compiler quiet */
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 468aae64676..35be926b048 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -72,6 +72,10 @@ toast_compress_datum(Datum value, char cmethod)
 			tmp = lz4_compress_datum((const struct varlena *) value);
 			cmid = TOAST_LZ4_COMPRESSION_ID;
 			break;
+		case TOAST_ZSTD_COMPRESSION:
+			tmp = zstd_compress_datum((const struct varlena *) value);
+			cmid = TOAST_ZSTD_COMPRESSION_ID;
+			break;
 		default:
 			elog(ERROR, "invalid compression method %c", cmethod);
 	}
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 26c720449f7..3b4dd692d37 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -4204,6 +4204,9 @@ pg_column_compression(PG_FUNCTION_ARGS)
 		case TOAST_LZ4_COMPRESSION_ID:
 			result = "lz4";
 			break;
+		case TOAST_ZSTD_COMPRESSION_ID:
+			result = "zstd";
+			break;
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 	}
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index be523c9ac09..74c1fe424c4 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -461,6 +461,9 @@ static const struct config_enum_entry default_toast_compression_options[] = {
 	{"pglz", TOAST_PGLZ_COMPRESSION, false},
 #ifdef  USE_LZ4
 	{"lz4", TOAST_LZ4_COMPRESSION, false},
+#endif
+#ifdef  USE_ZSTD
+	{"zstd", TOAST_ZSTD_COMPRESSION, false},
 #endif
 	{NULL, 0, false}
 };
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 5f34b14ea39..0f1dc0dc05c 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -752,7 +752,7 @@ autovacuum_worker_slots = 16	# autovacuum worker slots to allocate
 #row_security = on
 #default_table_access_method = 'heap'
 #default_tablespace = ''		# a tablespace name, '' uses the default
-#default_toast_compression = 'pglz'	# 'pglz' or 'lz4'
+#default_toast_compression = 'pglz'	# 'pglz' or 'lz4' or 'zstd'
 #default_toast_type = 'oid'		# 'oid' or 'int8'
 #temp_tablespaces = ''			# a list of tablespace names, '' uses
 					# only default tablespace
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 7afb0d1a925..5a9353fe15d 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -17692,6 +17692,9 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 					case 'l':
 						cmname = "lz4";
 						break;
+					case 'z':
+						cmname = "zstd";
+						break;
 					default:
 						cmname = NULL;
 						break;
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index dd25d2fe7b8..e073f6766e8 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2172,8 +2172,9 @@ describeOneTableDetails(const char *schemaname,
 			/* these strings are literal in our syntax, so not translated. */
 			printTableAddCell(&cont, (compression[0] == 'p' ? "pglz" :
 									  (compression[0] == 'l' ? "lz4" :
-									   (compression[0] == '\0' ? "" :
-										"???"))),
+									   (compression[0] == 'z' ? "zstd" :
+										(compression[0] == '\0' ? "" :
+										 "???")))),
 							  false, false);
 		}
 
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 37524364290..9032902a5c6 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2913,7 +2913,7 @@ match_previous_words(int pattern_id,
 	/* ALTER TABLE ALTER [COLUMN] <foo> SET COMPRESSION */
 	else if (Matches("ALTER", "TABLE", MatchAny, "ALTER", "COLUMN", MatchAny, "SET", "COMPRESSION") ||
 			 Matches("ALTER", "TABLE", MatchAny, "ALTER", MatchAny, "SET", "COMPRESSION"))
-		COMPLETE_WITH("DEFAULT", "PGLZ", "LZ4");
+		COMPLETE_WITH("DEFAULT", "PGLZ", "LZ4", "ZSTD");
 	/* ALTER TABLE ALTER [COLUMN] <foo> SET EXPRESSION */
 	else if (Matches("ALTER", "TABLE", MatchAny, "ALTER", "COLUMN", MatchAny, "SET", "EXPRESSION") ||
 			 Matches("ALTER", "TABLE", MatchAny, "ALTER", MatchAny, "SET", "EXPRESSION"))
diff --git a/src/include/access/toast_compression.h b/src/include/access/toast_compression.h
index 494e1b0dce6..accc4746a56 100644
--- a/src/include/access/toast_compression.h
+++ b/src/include/access/toast_compression.h
@@ -13,6 +13,10 @@
 #ifndef TOAST_COMPRESSION_H
 #define TOAST_COMPRESSION_H
 
+#ifdef USE_ZSTD
+#include <zstd.h>
+#endif
+
 /*
  * GUC support.
  *
@@ -23,22 +27,25 @@
 extern PGDLLIMPORT int default_toast_compression;
 
 /*
- * Built-in compression method ID.  The toast compression header will store
- * this in the first 2 bits of the raw length.  These built-in compression
- * method IDs are directly mapped to the built-in compression methods.
+ * Built-in compression method ID.
+ *
+ * For TOAST-compressed values:
+ *   - If using a non-extended method, the first 2 bits of the raw length
+ *		field store this ID.
+ *   - If using an extended method, it is stored in the extended 4-byte header.
  *
- * Don't use these values for anything other than understanding the meaning
- * of the raw bits from a varlena; in particular, if the goal is to identify
- * a compression method, use the constants TOAST_PGLZ_COMPRESSION, etc.
- * below. We might someday support more than 4 compression methods, but
- * we can never have more than 4 values in this enum, because there are
- * only 2 bits available in the places where this is stored.
+ * These IDs map directly to the built-in compression methods.
+ *
+ * Note: Do not use these values for anything other than interpreting the
+ * raw bits from a varlena. To identify a compression method in code, use
+ * the named constants (e.g., TOAST_PGLZ_COMPRESSION) instead.
  */
 typedef enum ToastCompressionId
 {
 	TOAST_PGLZ_COMPRESSION_ID = 0,
 	TOAST_LZ4_COMPRESSION_ID = 1,
-	TOAST_INVALID_COMPRESSION_ID = 2,
+	TOAST_ZSTD_COMPRESSION_ID = 2,
+	TOAST_INVALID_COMPRESSION_ID = 3,
 } ToastCompressionId;
 
 /*
@@ -48,6 +55,7 @@ typedef enum ToastCompressionId
  */
 #define TOAST_PGLZ_COMPRESSION			'p'
 #define TOAST_LZ4_COMPRESSION			'l'
+#define TOAST_ZSTD_COMPRESSION			'z'
 #define InvalidCompressionMethod		'\0'
 
 #define CompressionMethodIsValid(cm)  ((cm) != InvalidCompressionMethod)
@@ -71,6 +79,11 @@ extern struct varlena *lz4_decompress_datum(const struct varlena *value);
 extern struct varlena *lz4_decompress_datum_slice(const struct varlena *value,
 												  int32 slicelength);
 
+/* zstd nodict compression/decompression routines */
+extern struct varlena *zstd_compress_datum(const struct varlena *value);
+extern struct varlena *zstd_decompress_datum(const struct varlena *value);
+extern struct varlena *zstd_decompress_datum_slice(const struct varlena *value, int32 slicelength);
+
 /* other stuff */
 extern ToastCompressionId toast_get_compression_id(struct varlena *attr);
 extern char CompressionNameToMethod(const char *compression);
diff --git a/src/include/access/toast_internals.h b/src/include/access/toast_internals.h
index 27bc8a0b816..5fb8ca93fdd 100644
--- a/src/include/access/toast_internals.h
+++ b/src/include/access/toast_internals.h
@@ -21,13 +21,21 @@
  * Utilities for manipulation of header information for compressed
  * toast entries.
  */
-#define TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(ptr, len, cm_method) \
-	do { \
-		Assert((len) > 0 && (len) <= VARLENA_EXTSIZE_MASK); \
-		Assert((cm_method) == TOAST_PGLZ_COMPRESSION_ID || \
-			   (cm_method) == TOAST_LZ4_COMPRESSION_ID); \
-		((varattrib_4b *)(ptr))->va_compressed.va_tcinfo = \
-			((uint32)(len)) | ((uint32)(cm_method) << VARLENA_EXTSIZE_BITS); \
+#define TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(ptr, len, cm_method)														\
+	do {																														\
+		Assert((len) > 0 && (len) <= VARLENA_EXTSIZE_MASK);																		\
+		Assert((cm_method) == TOAST_PGLZ_COMPRESSION_ID ||																		\
+				(cm_method) == TOAST_LZ4_COMPRESSION_ID ||																		\
+				(cm_method) == TOAST_ZSTD_COMPRESSION_ID);																		\
+		if (!CompressionMethodIdIsExtended((cm_method)))																		\
+			((varattrib_4b *)(ptr))->va_compressed.va_tcinfo = ((uint32)(len)) | ((uint32)(cm_method) << VARLENA_EXTSIZE_BITS);	\
+		else																													\
+		{																														\
+			/* extended path: mark EXT flag in tcinfo */																		\
+			((varattrib_4b *)(ptr))->va_compressed_ext.va_tcinfo =																\
+				((uint32)(len)) | ((uint32)(VARATT_CE_FLAG) << VARLENA_EXTSIZE_BITS);											\
+			VARATT_CE_SET_COMPRESS_METHOD(((varattrib_4b *)(ptr))->va_compressed_ext.va_ecinfo, (cm_method));					\
+		}																														\
 	} while (0)
 
 extern Datum toast_compress_datum(Datum value, char cmethod);
diff --git a/src/test/regress/expected/compression_zstd.out b/src/test/regress/expected/compression_zstd.out
new file mode 100644
index 00000000000..166ba022541
--- /dev/null
+++ b/src/test/regress/expected/compression_zstd.out
@@ -0,0 +1,249 @@
+-- Tests for TOAST compression with zstd
+SELECT NOT(enumvals @> '{zstd}') AS skip_test FROM pg_settings WHERE
+  name = 'default_toast_compression' \gset
+\if :skip_test
+   \echo '*** skipping TOAST tests with zstd (not supported) ***'
+   \quit
+\endif
+CREATE SCHEMA zstd;
+SET search_path TO zstd, public;
+\set HIDE_TOAST_COMPRESSION false
+-- Ensure we get stable results regardless of the installation's default.
+-- We rely on this GUC value for a few tests.
+SET default_toast_compression = 'pglz';
+-- test creating table with compression method
+CREATE TABLE cmdata_pglz(f1 text COMPRESSION pglz);
+CREATE INDEX idx ON cmdata_pglz(f1);
+INSERT INTO cmdata_pglz VALUES(repeat('1234567890', 1000));
+\d+ cmdata
+CREATE TABLE cmdata_zstd(f1 TEXT COMPRESSION zstd);
+INSERT INTO cmdata_zstd VALUES(repeat('1234567890', 1004));
+\d+ cmdata1
+-- verify stored compression method in the data
+SELECT pg_column_compression(f1) FROM cmdata_zstd;
+ pg_column_compression 
+-----------------------
+ zstd
+(1 row)
+
+-- decompress data slice
+SELECT SUBSTR(f1, 200, 5) FROM cmdata_pglz;
+ substr 
+--------
+ 01234
+(1 row)
+
+SELECT SUBSTR(f1, 2000, 50) FROM cmdata_zstd;
+                       substr                       
+----------------------------------------------------
+ 01234567890123456789012345678901234567890123456789
+(1 row)
+
+-- copy with table creation
+SELECT * INTO cmmove1 FROM cmdata_zstd;
+\d+ cmmove1
+                                         Table "zstd.cmmove1"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ f1     | text |           |          |         | extended |             |              | 
+
+SELECT pg_column_compression(f1) FROM cmmove1;
+ pg_column_compression 
+-----------------------
+ zstd
+(1 row)
+
+-- test LIKE INCLUDING COMPRESSION.  The GUC default_toast_compression
+-- has no effect, the compression method from the table being copied.
+CREATE TABLE cmdata2 (LIKE cmdata_zstd INCLUDING COMPRESSION);
+\d+ cmdata2
+                                         Table "zstd.cmdata2"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ f1     | text |           |          |         | extended | zstd        |              | 
+
+DROP TABLE cmdata2;
+-- copy to existing table
+CREATE TABLE cmmove3(f1 text COMPRESSION pglz);
+INSERT INTO cmmove3 SELECT * FROM cmdata_pglz;
+INSERT INTO cmmove3 SELECT * FROM cmdata_zstd;
+SELECT pg_column_compression(f1) FROM cmmove3;
+ pg_column_compression 
+-----------------------
+ pglz
+ zstd
+(2 rows)
+
+-- update using datum from different table with zstd data.
+CREATE TABLE cmmove2(f1 text COMPRESSION pglz);
+INSERT INTO cmmove2 VALUES (repeat('1234567890', 1004));
+SELECT pg_column_compression(f1) FROM cmmove2;
+ pg_column_compression 
+-----------------------
+ pglz
+(1 row)
+
+UPDATE cmmove2 SET f1 = cmdata_zstd.f1 FROM cmdata_zstd;
+SELECT pg_column_compression(f1) FROM cmmove2;
+ pg_column_compression 
+-----------------------
+ zstd
+(1 row)
+
+-- test externally stored compressed data
+CREATE OR REPLACE FUNCTION large_val_zstd() RETURNS TEXT LANGUAGE SQL AS
+'select array_agg(fipshash(g::text))::text from generate_series(1, 256) g';
+CREATE TABLE cmdata2 (f1 text COMPRESSION zstd);
+INSERT INTO cmdata2 SELECT large_val_zstd() || repeat('a', 4000);
+SELECT pg_column_compression(f1) FROM cmdata2;
+ pg_column_compression 
+-----------------------
+ zstd
+(1 row)
+
+SELECT SUBSTR(f1, 200, 5) FROM cmdata2;
+ substr 
+--------
+ 79026
+(1 row)
+
+DROP TABLE cmdata2;
+DROP FUNCTION large_val_zstd;
+-- test compression with materialized view
+CREATE MATERIALIZED VIEW compressmv(x) AS SELECT * FROM cmdata_zstd;
+\d+ compressmv
+                                 Materialized view "zstd.compressmv"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ x      | text |           |          |         | extended |             |              | 
+View definition:
+ SELECT f1 AS x
+   FROM cmdata_zstd;
+
+SELECT pg_column_compression(f1) FROM cmdata_zstd;
+ pg_column_compression 
+-----------------------
+ zstd
+(1 row)
+
+SELECT pg_column_compression(x) FROM compressmv;
+ pg_column_compression 
+-----------------------
+ zstd
+(1 row)
+
+-- test compression with partition
+CREATE TABLE cmpart(f1 text COMPRESSION zstd) PARTITION BY HASH(f1);
+CREATE TABLE cmpart1 PARTITION OF cmpart FOR VALUES WITH (MODULUS 2, REMAINDER 0);
+CREATE TABLE cmpart2(f1 text COMPRESSION pglz);
+ALTER TABLE cmpart ATTACH PARTITION cmpart2 FOR VALUES WITH (MODULUS 2, REMAINDER 1);
+INSERT INTO cmpart VALUES (repeat('123456789', 1004));
+INSERT INTO cmpart VALUES (repeat('123456789', 4004));
+SELECT pg_column_compression(f1) FROM cmpart1;
+ pg_column_compression 
+-----------------------
+ zstd
+(1 row)
+
+SELECT pg_column_compression(f1) FROM cmpart2;
+ pg_column_compression 
+-----------------------
+ pglz
+(1 row)
+
+-- test compression with inheritance
+CREATE TABLE cminh() INHERITS(cmdata_pglz, cmdata_zstd); -- error
+NOTICE:  merging multiple inherited definitions of column "f1"
+ERROR:  column "f1" has a compression method conflict
+DETAIL:  pglz versus zstd
+CREATE TABLE cminh(f1 TEXT COMPRESSION zstd) INHERITS(cmdata_pglz); -- error
+NOTICE:  merging column "f1" with inherited definition
+ERROR:  column "f1" has a compression method conflict
+DETAIL:  pglz versus zstd
+CREATE TABLE cmdata3(f1 text);
+CREATE TABLE cminh() INHERITS (cmdata_pglz, cmdata3);
+NOTICE:  merging multiple inherited definitions of column "f1"
+-- test default_toast_compression GUC
+SET default_toast_compression = 'zstd';
+-- test alter compression method
+ALTER TABLE cmdata_pglz ALTER COLUMN f1 SET COMPRESSION zstd;
+INSERT INTO cmdata_pglz VALUES (repeat('123456789', 4004));
+\d+ cmdata
+SELECT pg_column_compression(f1) FROM cmdata_pglz;
+ pg_column_compression 
+-----------------------
+ pglz
+ zstd
+(2 rows)
+
+ALTER TABLE cmdata_pglz ALTER COLUMN f1 SET COMPRESSION pglz;
+-- test alter compression method for materialized views
+ALTER MATERIALIZED VIEW compressmv ALTER COLUMN x SET COMPRESSION zstd;
+\d+ compressmv
+                                 Materialized view "zstd.compressmv"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ x      | text |           |          |         | extended | zstd        |              | 
+View definition:
+ SELECT f1 AS x
+   FROM cmdata_zstd;
+
+-- test alter compression method for partitioned tables
+ALTER TABLE cmpart1 ALTER COLUMN f1 SET COMPRESSION pglz;
+ALTER TABLE cmpart2 ALTER COLUMN f1 SET COMPRESSION zstd;
+-- new data should be compressed with the current compression method
+INSERT INTO cmpart VALUES (repeat('123456789', 1004));
+INSERT INTO cmpart VALUES (repeat('123456789', 4004));
+SELECT pg_column_compression(f1) FROM cmpart1;
+ pg_column_compression 
+-----------------------
+ zstd
+ pglz
+(2 rows)
+
+SELECT pg_column_compression(f1) FROM cmpart2;
+ pg_column_compression 
+-----------------------
+ pglz
+ zstd
+(2 rows)
+
+-- test expression index
+CREATE TABLE cmdata2 (f1 TEXT COMPRESSION pglz, f2 TEXT COMPRESSION zstd);
+CREATE UNIQUE INDEX idx1 ON cmdata2 ((f1 || f2));
+INSERT INTO cmdata2 VALUES((SELECT array_agg(fipshash(g::TEXT))::TEXT FROM
+generate_series(1, 50) g), VERSION());
+-- check data is ok
+SELECT length(f1) FROM cmdata_pglz;
+ length 
+--------
+  10000
+  36036
+(2 rows)
+
+SELECT length(f1) FROM cmdata_zstd;
+ length 
+--------
+  10040
+(1 row)
+
+SELECT length(f1) FROM cmmove1;
+ length 
+--------
+  10040
+(1 row)
+
+SELECT length(f1) FROM cmmove2;
+ length 
+--------
+  10040
+(1 row)
+
+SELECT length(f1) FROM cmmove3;
+ length 
+--------
+  10000
+  10040
+(2 rows)
+
+\set HIDE_TOAST_COMPRESSION true
diff --git a/src/test/regress/expected/compression_zstd_1.out b/src/test/regress/expected/compression_zstd_1.out
new file mode 100644
index 00000000000..5f07342fd51
--- /dev/null
+++ b/src/test/regress/expected/compression_zstd_1.out
@@ -0,0 +1,7 @@
+-- Tests for TOAST compression with zstd
+SELECT NOT(enumvals @> '{zstd}') AS skip_test FROM pg_settings WHERE
+  name = 'default_toast_compression' \gset
+\if :skip_test
+   \echo '*** skipping TOAST tests with zstd (not supported) ***'
+*** skipping TOAST tests with zstd (not supported) ***
+   \quit
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index fbffc67ae60..1ef4797cd10 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -123,7 +123,7 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion tr
 # The stats test resets stats, so nothing else needing stats access can be in
 # this group.
 # ----------
-test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression compression_lz4 memoize stats predicate numa
+test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression compression_lz4 compression_zstd memoize stats predicate numa
 
 # event_trigger depends on create_am and cannot run concurrently with
 # any test that runs DDL
diff --git a/src/test/regress/sql/compression_zstd.sql b/src/test/regress/sql/compression_zstd.sql
new file mode 100644
index 00000000000..134d1d44327
--- /dev/null
+++ b/src/test/regress/sql/compression_zstd.sql
@@ -0,0 +1,129 @@
+-- Tests for TOAST compression with zstd
+
+SELECT NOT(enumvals @> '{zstd}') AS skip_test FROM pg_settings WHERE
+  name = 'default_toast_compression' \gset
+\if :skip_test
+   \echo '*** skipping TOAST tests with zstd (not supported) ***'
+   \quit
+\endif
+
+CREATE SCHEMA zstd;
+SET search_path TO zstd, public;
+
+\set HIDE_TOAST_COMPRESSION false
+
+-- Ensure we get stable results regardless of the installation's default.
+-- We rely on this GUC value for a few tests.
+SET default_toast_compression = 'pglz';
+
+-- test creating table with compression method
+CREATE TABLE cmdata_pglz(f1 text COMPRESSION pglz);
+CREATE INDEX idx ON cmdata_pglz(f1);
+INSERT INTO cmdata_pglz VALUES(repeat('1234567890', 1000));
+\d+ cmdata
+CREATE TABLE cmdata_zstd(f1 TEXT COMPRESSION zstd);
+INSERT INTO cmdata_zstd VALUES(repeat('1234567890', 1004));
+\d+ cmdata1
+
+-- verify stored compression method in the data
+SELECT pg_column_compression(f1) FROM cmdata_zstd;
+
+-- decompress data slice
+SELECT SUBSTR(f1, 200, 5) FROM cmdata_pglz;
+SELECT SUBSTR(f1, 2000, 50) FROM cmdata_zstd;
+
+-- copy with table creation
+SELECT * INTO cmmove1 FROM cmdata_zstd;
+\d+ cmmove1
+SELECT pg_column_compression(f1) FROM cmmove1;
+
+-- test LIKE INCLUDING COMPRESSION.  The GUC default_toast_compression
+-- has no effect, the compression method from the table being copied.
+CREATE TABLE cmdata2 (LIKE cmdata_zstd INCLUDING COMPRESSION);
+\d+ cmdata2
+DROP TABLE cmdata2;
+
+-- copy to existing table
+CREATE TABLE cmmove3(f1 text COMPRESSION pglz);
+INSERT INTO cmmove3 SELECT * FROM cmdata_pglz;
+INSERT INTO cmmove3 SELECT * FROM cmdata_zstd;
+SELECT pg_column_compression(f1) FROM cmmove3;
+
+-- update using datum from different table with zstd data.
+CREATE TABLE cmmove2(f1 text COMPRESSION pglz);
+INSERT INTO cmmove2 VALUES (repeat('1234567890', 1004));
+SELECT pg_column_compression(f1) FROM cmmove2;
+UPDATE cmmove2 SET f1 = cmdata_zstd.f1 FROM cmdata_zstd;
+SELECT pg_column_compression(f1) FROM cmmove2;
+
+-- test externally stored compressed data
+CREATE OR REPLACE FUNCTION large_val_zstd() RETURNS TEXT LANGUAGE SQL AS
+'select array_agg(fipshash(g::text))::text from generate_series(1, 256) g';
+CREATE TABLE cmdata2 (f1 text COMPRESSION zstd);
+INSERT INTO cmdata2 SELECT large_val_zstd() || repeat('a', 4000);
+SELECT pg_column_compression(f1) FROM cmdata2;
+SELECT SUBSTR(f1, 200, 5) FROM cmdata2;
+DROP TABLE cmdata2;
+DROP FUNCTION large_val_zstd;
+
+-- test compression with materialized view
+CREATE MATERIALIZED VIEW compressmv(x) AS SELECT * FROM cmdata_zstd;
+\d+ compressmv
+SELECT pg_column_compression(f1) FROM cmdata_zstd;
+SELECT pg_column_compression(x) FROM compressmv;
+
+-- test compression with partition
+CREATE TABLE cmpart(f1 text COMPRESSION zstd) PARTITION BY HASH(f1);
+CREATE TABLE cmpart1 PARTITION OF cmpart FOR VALUES WITH (MODULUS 2, REMAINDER 0);
+CREATE TABLE cmpart2(f1 text COMPRESSION pglz);
+
+ALTER TABLE cmpart ATTACH PARTITION cmpart2 FOR VALUES WITH (MODULUS 2, REMAINDER 1);
+INSERT INTO cmpart VALUES (repeat('123456789', 1004));
+INSERT INTO cmpart VALUES (repeat('123456789', 4004));
+SELECT pg_column_compression(f1) FROM cmpart1;
+SELECT pg_column_compression(f1) FROM cmpart2;
+
+-- test compression with inheritance
+CREATE TABLE cminh() INHERITS(cmdata_pglz, cmdata_zstd); -- error
+CREATE TABLE cminh(f1 TEXT COMPRESSION zstd) INHERITS(cmdata_pglz); -- error
+CREATE TABLE cmdata3(f1 text);
+CREATE TABLE cminh() INHERITS (cmdata_pglz, cmdata3);
+
+-- test default_toast_compression GUC
+SET default_toast_compression = 'zstd';
+
+-- test alter compression method
+ALTER TABLE cmdata_pglz ALTER COLUMN f1 SET COMPRESSION zstd;
+INSERT INTO cmdata_pglz VALUES (repeat('123456789', 4004));
+\d+ cmdata
+SELECT pg_column_compression(f1) FROM cmdata_pglz;
+ALTER TABLE cmdata_pglz ALTER COLUMN f1 SET COMPRESSION pglz;
+
+-- test alter compression method for materialized views
+ALTER MATERIALIZED VIEW compressmv ALTER COLUMN x SET COMPRESSION zstd;
+\d+ compressmv
+
+-- test alter compression method for partitioned tables
+ALTER TABLE cmpart1 ALTER COLUMN f1 SET COMPRESSION pglz;
+ALTER TABLE cmpart2 ALTER COLUMN f1 SET COMPRESSION zstd;
+
+-- new data should be compressed with the current compression method
+INSERT INTO cmpart VALUES (repeat('123456789', 1004));
+INSERT INTO cmpart VALUES (repeat('123456789', 4004));
+SELECT pg_column_compression(f1) FROM cmpart1;
+SELECT pg_column_compression(f1) FROM cmpart2;
+
+-- test expression index
+CREATE TABLE cmdata2 (f1 TEXT COMPRESSION pglz, f2 TEXT COMPRESSION zstd);
+CREATE UNIQUE INDEX idx1 ON cmdata2 ((f1 || f2));
+INSERT INTO cmdata2 VALUES((SELECT array_agg(fipshash(g::TEXT))::TEXT FROM
+generate_series(1, 50) g), VERSION());
+
+-- check data is ok
+SELECT length(f1) FROM cmdata_pglz;
+SELECT length(f1) FROM cmdata_zstd;
+SELECT length(f1) FROM cmmove1;
+SELECT length(f1) FROM cmmove2;
+SELECT length(f1) FROM cmmove3;
+
+\set HIDE_TOAST_COMPRESSION true
-- 
2.47.1

v27-0014-Design-to-extend-the-varattrib_4b-and-toast-poin.patchapplication/octet-stream; name=v27-0014-Design-to-extend-the-varattrib_4b-and-toast-poin.patchDownload
From 43f9fc8c7b1ea13756d089bae8dadf0786ed5494 Mon Sep 17 00:00:00 2001
From: Nikhil Kumar Veldanda <veldanda.nikhilkumar17@gmail.com>
Date: Sat, 19 Jul 2025 23:07:41 +0000
Subject: [PATCH v27 14/15] Design to extend the varattrib_4b and toast pointer
 to support of multiple TOAST compression algorithms.

---
 contrib/amcheck/verify_heapam.c             |   5 +-
 src/backend/access/common/detoast.c         |   6 +-
 src/backend/access/common/toast_external.c  | 176 ++++++++++++++++++--
 src/backend/access/common/toast_internals.c |  27 ++-
 src/backend/access/heap/heaptoast.c         |  49 ++----
 src/backend/access/table/toast_helper.c     |  41 ++++-
 src/include/access/toast_compression.h      |   6 +
 src/include/access/toast_helper.h           |   4 +-
 src/include/access/toast_internals.h        |  19 +--
 src/include/varatt.h                        | 115 +++++++++++--
 10 files changed, 339 insertions(+), 109 deletions(-)

diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 958b1451b4f..3a23dddcff4 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1733,7 +1733,10 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	{
 		uint8		va_tag = VARTAG_EXTERNAL(tp + ctx->offset);
 
-		if (va_tag != VARTAG_ONDISK_OID && va_tag != VARTAG_ONDISK_INT8)
+		if (va_tag != VARTAG_ONDISK_OID &&
+			va_tag != VARTAG_ONDISK_INT8 &&
+			va_tag != VARTAG_ONDISK_CE_INT8 &&
+			va_tag != VARTAG_ONDISK_CE_OID)
 		{
 			report_corruption(ctx,
 							  psprintf("toasted attribute has unexpected TOAST tag %u",
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 684e1b0b7d3..34a3f7c6694 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -484,7 +484,7 @@ toast_decompress_datum(struct varlena *attr)
 	 * Fetch the compression method id stored in the compression header and
 	 * decompress the data using the appropriate decompression routine.
 	 */
-	cmid = TOAST_COMPRESS_METHOD(attr);
+	cmid = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(attr);
 	switch (cmid)
 	{
 		case TOAST_PGLZ_COMPRESSION_ID:
@@ -520,14 +520,14 @@ toast_decompress_datum_slice(struct varlena *attr, int32 slicelength)
 	 * have been seen to give wrong results if passed an output size that is
 	 * more than the data's true decompressed size.
 	 */
-	if ((uint32) slicelength >= TOAST_COMPRESS_EXTSIZE(attr))
+	if ((uint32) slicelength >= VARDATA_COMPRESSED_GET_EXTSIZE(attr))
 		return toast_decompress_datum(attr);
 
 	/*
 	 * Fetch the compression method id stored in the compression header and
 	 * decompress the data slice using the appropriate decompression routine.
 	 */
-	cmid = TOAST_COMPRESS_METHOD(attr);
+	cmid = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(attr);
 	switch (cmid)
 	{
 		case TOAST_PGLZ_COMPRESSION_ID:
diff --git a/src/backend/access/common/toast_external.c b/src/backend/access/common/toast_external.c
index 0e79ac8acae..5bf17ed7182 100644
--- a/src/backend/access/common/toast_external.c
+++ b/src/backend/access/common/toast_external.c
@@ -38,6 +38,15 @@ static struct varlena *ondisk_oid_create_external_data(toast_external_data data)
 static uint64 ondisk_oid_get_new_value(Relation toastrel, Oid indexid,
 									   AttrNumber attnum);
 
+/* Callbacks for VARTAG_ONDISK_CE_OID */
+static void ondisk_ce_oid_to_external_data(struct varlena *attr,
+										   toast_external_data *data);
+static struct varlena *ondisk_ce_oid_create_external_data(toast_external_data data);
+
+/* Callbacks for VARTAG_ONDISK_CE_INT8 */
+static void ondisk_ce_int8_to_external_data(struct varlena *attr,
+											toast_external_data *data);
+static struct varlena *ondisk_ce_int8_create_external_data(toast_external_data data);
 
 /*
  * Size of an EXTERNAL datum that contains a standard TOAST pointer
@@ -51,6 +60,18 @@ static uint64 ondisk_oid_get_new_value(Relation toastrel, Oid indexid,
  */
 #define TOAST_POINTER_OID_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_oid))
 
+/*
+ * Size of an EXTERNAL datum that contains a TOAST pointer which supports extended compression methods
+ * (OID value).
+ */
+#define TOAST_POINTER_CE_OID_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_ce_oid))
+
+/*
+ * Size of an EXTERNAL datum that contains a TOAST pointer which supports extended compression methods
+ * (int8 value).
+ */
+#define TOAST_POINTER_CE_INT8_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_ce_int8))
+
 /*
  * For now there are only two types, all defined in this file.  For now this
  * is the maximum value of vartag_external, which is a historical choice.
@@ -72,6 +93,13 @@ static const toast_external_info toast_external_infos[TOAST_EXTERNAL_INFO_SIZE]
 		.create_external_data = ondisk_int8_create_external_data,
 		.get_new_value = ondisk_int8_get_new_value,
 	},
+	[VARTAG_ONDISK_CE_INT8] = {
+		.toast_pointer_size = TOAST_POINTER_CE_INT8_SIZE,
+		.maximum_chunk_size = TOAST_MAX_CHUNK_SIZE_INT8,
+		.to_external_data = ondisk_ce_int8_to_external_data,
+		.create_external_data = ondisk_ce_int8_create_external_data,
+		.get_new_value = ondisk_int8_get_new_value,
+	},
 	[VARTAG_ONDISK_OID] = {
 		.toast_pointer_size = TOAST_POINTER_OID_SIZE,
 		.maximum_chunk_size = TOAST_MAX_CHUNK_SIZE_OID,
@@ -79,6 +107,13 @@ static const toast_external_info toast_external_infos[TOAST_EXTERNAL_INFO_SIZE]
 		.create_external_data = ondisk_oid_create_external_data,
 		.get_new_value = ondisk_oid_get_new_value,
 	},
+	[VARTAG_ONDISK_CE_OID] = {
+		.toast_pointer_size = TOAST_POINTER_CE_OID_SIZE,
+		.maximum_chunk_size = TOAST_MAX_CHUNK_SIZE_OID,
+		.to_external_data = ondisk_ce_oid_to_external_data,
+		.create_external_data = ondisk_ce_oid_create_external_data,
+		.get_new_value = ondisk_oid_get_new_value,
+	},
 };
 
 
@@ -108,7 +143,7 @@ toast_external_info_get_pointer_size(uint8 tag)
 static void
 ondisk_int8_to_external_data(struct varlena *attr, toast_external_data *data)
 {
-	varatt_external_int8	external;
+	varatt_external_int8 external;
 
 	VARATT_EXTERNAL_GET_POINTER(external, attr);
 	data->rawsize = external.va_rawsize;
@@ -117,7 +152,7 @@ ondisk_int8_to_external_data(struct varlena *attr, toast_external_data *data)
 	if (VARATT_EXTERNAL_IS_COMPRESSED(external))
 	{
 		data->extsize = VARATT_EXTERNAL_GET_EXTSIZE(external);
-		data->compression_method = VARATT_EXTERNAL_GET_COMPRESS_METHOD(external);
+		data->compression_method = external.va_extinfo >> VARLENA_EXTSIZE_BITS;
 	}
 	else
 	{
@@ -141,10 +176,10 @@ ondisk_int8_create_external_data(toast_external_data data)
 
 	if (data.compression_method != TOAST_INVALID_COMPRESSION_ID)
 	{
+		/* Regular variants only support basic compression methods */
+		Assert(!CompressionMethodIdIsExtended(data.compression_method));
 		/* Set size and compression method, in a single field. */
-		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(external,
-													 data.extsize,
-													 data.compression_method);
+		external.va_extinfo = (uint32) data.extsize | ((uint32) data.compression_method << VARLENA_EXTSIZE_BITS);
 	}
 	else
 		external.va_extinfo = data.extsize;
@@ -165,8 +200,8 @@ ondisk_int8_get_new_value(Relation toastrel, Oid indexid,
 						  AttrNumber attnum)
 {
 	uint64		new_value;
-	SysScanDesc	scan;
-	ScanKeyData	key;
+	SysScanDesc scan;
+	ScanKeyData key;
 	bool		collides = false;
 
 retry:
@@ -181,8 +216,8 @@ retry:
 	CHECK_FOR_INTERRUPTS();
 
 	/*
-	 * Check if the new value picked already exists in the toast relation.
-	 * If there is a conflict, retry.
+	 * Check if the new value picked already exists in the toast relation. If
+	 * there is a conflict, retry.
 	 */
 	ScanKeyInit(&key,
 				attnum,
@@ -206,7 +241,7 @@ retry:
 static void
 ondisk_oid_to_external_data(struct varlena *attr, toast_external_data *data)
 {
-	varatt_external_oid		external;
+	varatt_external_oid external;
 
 	VARATT_EXTERNAL_GET_POINTER(external, attr);
 	data->rawsize = external.va_rawsize;
@@ -218,7 +253,7 @@ ondisk_oid_to_external_data(struct varlena *attr, toast_external_data *data)
 	if (VARATT_EXTERNAL_IS_COMPRESSED(external))
 	{
 		data->extsize = VARATT_EXTERNAL_GET_EXTSIZE(external);
-		data->compression_method = VARATT_EXTERNAL_GET_COMPRESS_METHOD(external);
+		data->compression_method = external.va_extinfo >> VARLENA_EXTSIZE_BITS;
 	}
 	else
 	{
@@ -240,10 +275,10 @@ ondisk_oid_create_external_data(toast_external_data data)
 
 	if (data.compression_method != TOAST_INVALID_COMPRESSION_ID)
 	{
+		/* Regular variants only support basic compression methods */
+		Assert(!CompressionMethodIdIsExtended(data.compression_method));
 		/* Set size and compression method, in a single field. */
-		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(external,
-													 data.extsize,
-													 data.compression_method);
+		external.va_extinfo = (uint32) data.extsize | ((uint32) data.compression_method << VARLENA_EXTSIZE_BITS);
 	}
 	else
 		external.va_extinfo = data.extsize;
@@ -264,3 +299,116 @@ ondisk_oid_get_new_value(Relation toastrel, Oid indexid,
 {
 	return GetNewOidWithIndex(toastrel, indexid, attnum);
 }
+
+/* Callbacks for VARTAG_ONDISK_CE_OID */
+static void
+ondisk_ce_oid_to_external_data(struct varlena *attr, toast_external_data *data)
+{
+	varatt_external_ce_oid external;
+
+	VARATT_EXTERNAL_GET_POINTER(external, attr);
+	data->rawsize = external.va_rawsize;
+
+	/*
+	 * External size and compression methods are stored in the different
+	 * fields, extract.
+	 */
+	if (VARATT_EXTERNAL_IS_COMPRESSED(external))
+	{
+		data->extsize = VARATT_EXTERNAL_GET_EXTSIZE(external);
+		data->compression_method = VARATT_CE_GET_COMPRESS_METHOD(external.va_ecinfo);
+	}
+	else
+	{
+		data->extsize = external.va_extinfo;
+		data->compression_method = TOAST_INVALID_COMPRESSION_ID;
+	}
+
+	data->value = (uint64) external.va_valueid;
+	data->toastrelid = external.va_toastrelid;
+}
+
+static struct varlena *
+ondisk_ce_oid_create_external_data(toast_external_data data)
+{
+	struct varlena *result = NULL;
+	varatt_external_ce_oid external;
+
+	external.va_rawsize = data.rawsize;
+
+	if (data.compression_method != TOAST_INVALID_COMPRESSION_ID)
+	{
+		Assert(CompressionMethodIdIsExtended(data.compression_method));
+		/* Set size and compression method. */
+		external.va_extinfo = (uint32) data.extsize | (VARATT_CE_FLAG << VARLENA_EXTSIZE_BITS);
+		VARATT_CE_SET_COMPRESS_METHOD(external.va_ecinfo, data.compression_method);
+	}
+	else
+		external.va_extinfo = data.extsize;
+
+	external.va_toastrelid = data.toastrelid;
+	external.va_valueid = (Oid) data.value;
+
+	result = (struct varlena *) palloc(TOAST_POINTER_CE_OID_SIZE);
+	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_CE_OID);
+	memcpy(VARDATA_EXTERNAL(result), &external, sizeof(external));
+
+	return result;
+}
+
+/* Callbacks for VARTAG_ONDISK_CE_INT8 */
+static void
+ondisk_ce_int8_to_external_data(struct varlena *attr, toast_external_data *data)
+{
+	varatt_external_ce_int8 external;
+
+	VARATT_EXTERNAL_GET_POINTER(external, attr);
+	data->rawsize = external.va_rawsize;
+
+	/*
+	 * External size and compression methods are stored in the different
+	 * fields
+	 */
+	if (VARATT_EXTERNAL_IS_COMPRESSED(external))
+	{
+		data->extsize = VARATT_EXTERNAL_GET_EXTSIZE(external);
+		data->compression_method = VARATT_CE_GET_COMPRESS_METHOD(external.va_ecinfo);
+	}
+	else
+	{
+		data->extsize = external.va_extinfo;
+		data->compression_method = TOAST_INVALID_COMPRESSION_ID;
+	}
+
+	data->value = (((uint64) external.va_valueid_hi) << 32) |
+		external.va_valueid_lo;
+	data->toastrelid = external.va_toastrelid;
+}
+
+static struct varlena *
+ondisk_ce_int8_create_external_data(toast_external_data data)
+{
+	struct varlena *result = NULL;
+	varatt_external_ce_int8 external;
+
+	external.va_rawsize = data.rawsize;
+
+	if (data.compression_method != TOAST_INVALID_COMPRESSION_ID)
+	{
+		/* Set size and compression method. */
+		external.va_extinfo = (uint32) data.extsize | (VARATT_CE_FLAG << VARLENA_EXTSIZE_BITS);
+		VARATT_CE_SET_COMPRESS_METHOD(external.va_ecinfo, data.compression_method);
+	}
+	else
+		external.va_extinfo = data.extsize;
+
+	external.va_toastrelid = data.toastrelid;
+	external.va_valueid_hi = (((uint64) data.value) >> 32);
+	external.va_valueid_lo = (uint32) data.value;
+
+	result = (struct varlena *) palloc(TOAST_POINTER_CE_INT8_SIZE);
+	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_CE_INT8);
+	memcpy(VARDATA_EXTERNAL(result), &external, sizeof(external));
+
+	return result;
+}
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index c6b2d1522ce..468aae64676 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -160,18 +160,6 @@ toast_save_datum(Relation rel, Datum value,
 	toast_typid = TupleDescAttr(toasttupDesc, 0)->atttypid;
 	Assert(toast_typid == OIDOID || toast_typid == INT8OID);
 
-	/*
-	 * Grab the information for toast_external_data.
-	 *
-	 * Note: if we support multiple external vartags for a single value
-	 * type, we would need to be smarter in the vartag selection.
-	 */
-	if (toast_typid == OIDOID)
-		tag = VARTAG_ONDISK_OID;
-	else if (toast_typid == INT8OID)
-		tag = VARTAG_ONDISK_INT8;
-	info = toast_external_get_info(tag);
-
 	/* Open all the toast indexes and look for the valid one */
 	validIndex = toast_open_indexes(toastrel,
 									RowExclusiveLock,
@@ -242,6 +230,18 @@ toast_save_datum(Relation rel, Datum value,
 	else
 		toast_pointer.toastrelid = RelationGetRelid(toastrel);
 
+	/*
+	 * Grab the information for toast_external_data.
+	 *
+	 * Note: if we support multiple external vartags for a single value type,
+	 * we would need to be smarter in the vartag selection.
+	 */
+	if (toast_typid == OIDOID)
+		tag = CompressionMethodIdIsExtended(toast_pointer.compression_method) ? VARTAG_ONDISK_CE_OID : VARTAG_ONDISK_OID;
+	else if (toast_typid == INT8OID)
+		tag = CompressionMethodIdIsExtended(toast_pointer.compression_method) ? VARTAG_ONDISK_CE_INT8 : VARTAG_ONDISK_INT8;
+	info = toast_external_get_info(tag);
+
 	/*
 	 * Choose a new value to use as the value ID for this toast value, be it
 	 * for OID or int8-based TOAST relations.
@@ -254,8 +254,7 @@ toast_save_datum(Relation rel, Datum value,
 	 * value (which is a corner case, but possible if the table's attstorage
 	 * options have been changed), we have to pick a value ID that doesn't
 	 * conflict with either new or existing toast value IDs.  If the TOAST
-	 * table uses 8-byte value IDs, we should not really care much about
-	 * that.
+	 * table uses 8-byte value IDs, we should not really care much about that.
 	 */
 	if (!OidIsValid(rel->rd_toastoid))
 	{
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index 87f1630d85f..7b03b27341f 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -152,7 +152,7 @@ heap_toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup,
 	{
 		HeapTuple	atttuple;
 		Form_pg_attribute atttoast;
-		uint8		vartag = VARTAG_ONDISK_OID;
+		ToastTypeId toast_type = TOAST_TYPE_INVALID;
 
 		/*
 		 * XXX: This is very unlikely efficient, but it is not possible to
@@ -166,32 +166,22 @@ heap_toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup,
 		atttoast = (Form_pg_attribute) GETSTRUCT(atttuple);
 
 		if (atttoast->atttypid == OIDOID)
-			vartag = VARTAG_ONDISK_OID;
+			toast_type = TOAST_TYPE_OID;
 		else if (atttoast->atttypid == INT8OID)
-			vartag = VARTAG_ONDISK_INT8;
+			toast_type = TOAST_TYPE_INT8;
 		else
 			Assert(false);
-		ttc.ttc_toast_pointer_size =
-			toast_external_info_get_pointer_size(vartag);
+		ttc.toast_type = toast_type;
 		ReleaseSysCache(atttuple);
 	}
 	else
 	{
 		/*
-		 * No TOAST relation to rely on, which is a case possible when
-		 * dealing with partitioned tables, for example.  Hence, do a best
-		 * guess based on the GUC default_toast_type.
+		 * No TOAST relation to rely on, which is a case possible when dealing
+		 * with partitioned tables, for example.  Hence, do a best guess based
+		 * on the GUC default_toast_type.
 		 */
-		uint8	vartag = VARTAG_ONDISK_OID;
-
-		if (default_toast_type == TOAST_TYPE_INT8)
-			vartag = VARTAG_ONDISK_INT8;
-		else if (default_toast_type == TOAST_TYPE_OID)
-			vartag = VARTAG_ONDISK_OID;
-		else
-			Assert(false);
-		ttc.ttc_toast_pointer_size =
-			toast_external_info_get_pointer_size(vartag);
+		ttc.toast_type = default_toast_type;
 	}
 
 	ttc.ttc_rel = rel;
@@ -693,9 +683,7 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 	int			endchunk;
 	int			num_indexes;
 	int			validIndex;
-	int32		max_chunk_size;
-	const toast_external_info *info;
-	uint8		tag = VARTAG_INDIRECT;  /* init value does not matter */
+	int32		max_chunk_size = TOAST_MAX_CHUNK_SIZE_OID;
 	Oid			toast_typid = InvalidOid;
 
 	/* Look for the valid index of toast relation */
@@ -708,26 +696,23 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 	 * Grab the information for toast_external_data.
 	 *
 	 * Note: there is no access to the vartag of the original varlena from
-	 * which we are trying to retrieve the chunks from the TOAST relation,
-	 * so guess the external TOAST pointer information to use depending
-	 * on the attribute of the TOAST value.  If we begin to support multiple
-	 * external TOAST pointers for a single attribute type, we would need
-	 * to pass down this information from the upper callers.  This is
-	 * currently on required for the maximum chunk_size.
+	 * which we are trying to retrieve the chunks from the TOAST relation, so
+	 * guess the external TOAST pointer information to use depending on the
+	 * attribute of the TOAST value.  If we begin to support multiple external
+	 * TOAST pointers for a single attribute type, we would need to pass down
+	 * this information from the upper callers.  This is currently on required
+	 * for the maximum chunk_size.
 	 */
 	toast_typid = TupleDescAttr(toastrel->rd_att, 0)->atttypid;
 	Assert(toast_typid == OIDOID || toast_typid == INT8OID);
 
 	if (toast_typid == OIDOID)
-		tag = VARTAG_ONDISK_OID;
+		max_chunk_size = TOAST_MAX_CHUNK_SIZE_OID;
 	else if (toast_typid == INT8OID)
-		tag = VARTAG_ONDISK_INT8;
+		max_chunk_size = TOAST_MAX_CHUNK_SIZE_INT8;
 	else
 		Assert(false);
 
-	info = toast_external_get_info(tag);
-	max_chunk_size = info->maximum_chunk_size;
-
 	totalchunks = ((attrsize - 1) / max_chunk_size) + 1;
 	startchunk = sliceoffset / max_chunk_size;
 	endchunk = (sliceoffset + slicelength - 1) / max_chunk_size;
diff --git a/src/backend/access/table/toast_helper.c b/src/backend/access/table/toast_helper.c
index a2b44e093d7..d2a0517a36d 100644
--- a/src/backend/access/table/toast_helper.c
+++ b/src/backend/access/table/toast_helper.c
@@ -15,6 +15,7 @@
 #include "postgres.h"
 
 #include "access/detoast.h"
+#include "access/toast_external.h"
 #include "access/toast_helper.h"
 #include "access/toast_internals.h"
 #include "catalog/pg_type_d.h"
@@ -51,10 +52,23 @@ toast_tuple_init(ToastTupleContext *ttc)
 		Form_pg_attribute att = TupleDescAttr(tupleDesc, i);
 		struct varlena *old_value;
 		struct varlena *new_value;
+		uint8		vartag = VARTAG_ONDISK_OID;
+		char		cmethod = att->attcompression;
+
+		if (!CompressionMethodIsValid(cmethod))
+			cmethod = default_toast_compression;
+
+		if (ttc->toast_type == TOAST_TYPE_OID)
+			vartag = CompressionMethodIsExtended(cmethod) ? VARTAG_ONDISK_CE_OID : VARTAG_ONDISK_OID;
+		else if (ttc->toast_type == TOAST_TYPE_INT8)
+			vartag = CompressionMethodIsExtended(cmethod) ? VARTAG_ONDISK_CE_INT8 : VARTAG_ONDISK_INT8;
+		else
+			Assert(false);
 
 		ttc->ttc_attr[i].tai_colflags = 0;
 		ttc->ttc_attr[i].tai_oldexternal = NULL;
 		ttc->ttc_attr[i].tai_compression = att->attcompression;
+		ttc->ttc_attr[i].toast_pointer_size = toast_external_info_get_pointer_size(vartag);
 
 		if (ttc->ttc_oldvalues != NULL)
 		{
@@ -171,10 +185,10 @@ toast_tuple_init(ToastTupleContext *ttc)
  * The column must have attstorage EXTERNAL or EXTENDED if check_main is
  * false, and must have attstorage MAIN if check_main is true.
  *
- * The column must have a minimum size of MAXALIGN(tcc_toast_pointer_size);
- * if not, no benefit is to be expected by compressing it.  The TOAST
- * pointer size is given by the caller, depending on the type of TOAST
- * table we are dealing with.
+ * Each column must have a minimum size of MAXALIGN(toast_pointer_size) for
+ * that specific column; if not, no benefit is to be expected by compressing it.
+ * The TOAST pointer size varies per column based on the TOAST table type
+ * (OID vs INT8) and different variants used for that specific attribute.
  *
  * The return value is the index of the biggest suitable column, or
  * -1 if there is none.
@@ -190,16 +204,13 @@ toast_tuple_find_biggest_attribute(ToastTupleContext *ttc,
 	int32		skip_colflags = TOASTCOL_IGNORE;
 	int			i;
 
-	/* Define the lower-bound */
-	biggest_size = MAXALIGN(ttc->ttc_toast_pointer_size);
-	Assert(biggest_size != 0);
-
 	if (for_compression)
 		skip_colflags |= TOASTCOL_INCOMPRESSIBLE;
 
 	for (i = 0; i < numAttrs; i++)
 	{
 		Form_pg_attribute att = TupleDescAttr(tupleDesc, i);
+		int32		min_size_for_column;
 
 		if ((ttc->ttc_attr[i].tai_colflags & skip_colflags) != 0)
 			continue;
@@ -214,7 +225,19 @@ toast_tuple_find_biggest_attribute(ToastTupleContext *ttc,
 			att->attstorage != TYPSTORAGE_EXTERNAL)
 			continue;
 
-		if (ttc->ttc_attr[i].tai_size > biggest_size)
+		/*
+		 * Each column has its own minimum size threshold based on its TOAST
+		 * pointer size
+		 */
+		min_size_for_column = MAXALIGN(ttc->ttc_attr[i].toast_pointer_size);
+		Assert(min_size_for_column > 0);
+
+		/*
+		 * Only consider this column if it's bigger than its specific
+		 * threshold AND bigger than current biggest
+		 */
+		if (ttc->ttc_attr[i].tai_size > min_size_for_column &&
+			ttc->ttc_attr[i].tai_size > biggest_size)
 		{
 			biggest_attno = i;
 			biggest_size = ttc->ttc_attr[i].tai_size;
diff --git a/src/include/access/toast_compression.h b/src/include/access/toast_compression.h
index 13c4612ceed..494e1b0dce6 100644
--- a/src/include/access/toast_compression.h
+++ b/src/include/access/toast_compression.h
@@ -51,6 +51,12 @@ typedef enum ToastCompressionId
 #define InvalidCompressionMethod		'\0'
 
 #define CompressionMethodIsValid(cm)  ((cm) != InvalidCompressionMethod)
+#define CompressionMethodIsExtended(cm)	(!(cm == TOAST_PGLZ_COMPRESSION ||		\
+										   cm == TOAST_LZ4_COMPRESSION ||		\
+										   cm == InvalidCompressionMethod))
+#define CompressionMethodIdIsExtended(cmpid)	(!(cmpid == TOAST_PGLZ_COMPRESSION_ID ||	\
+												   cmpid == TOAST_LZ4_COMPRESSION_ID ||		\
+												   cmpid == TOAST_INVALID_COMPRESSION_ID))
 
 
 /* pglz compression/decompression routines */
diff --git a/src/include/access/toast_helper.h b/src/include/access/toast_helper.h
index 729c593afeb..e0ae11a581c 100644
--- a/src/include/access/toast_helper.h
+++ b/src/include/access/toast_helper.h
@@ -14,6 +14,7 @@
 #ifndef TOAST_HELPER_H
 #define TOAST_HELPER_H
 
+#include "access/toast_type.h"
 #include "utils/rel.h"
 
 /*
@@ -33,6 +34,7 @@ typedef struct
 	int32		tai_size;
 	uint8		tai_colflags;
 	char		tai_compression;
+	int32		toast_pointer_size;
 } ToastAttrInfo;
 
 /*
@@ -47,11 +49,11 @@ typedef struct
 	 * should be NULL in the case of an insert.
 	 */
 	Relation	ttc_rel;		/* the relation that contains the tuple */
-	int32		ttc_toast_pointer_size;	/* size of external TOAST pointer */
 	Datum	   *ttc_values;		/* values from the tuple columns */
 	bool	   *ttc_isnull;		/* null flags for the tuple columns */
 	Datum	   *ttc_oldvalues;	/* values from previous tuple */
 	bool	   *ttc_oldisnull;	/* null flags from previous tuple */
+	ToastTypeId toast_type;		/* toast table type */
 
 	/*
 	 * Before calling toast_tuple_init, the caller should set ttc_attr to
diff --git a/src/include/access/toast_internals.h b/src/include/access/toast_internals.h
index 06ae8583c1e..27bc8a0b816 100644
--- a/src/include/access/toast_internals.h
+++ b/src/include/access/toast_internals.h
@@ -17,32 +17,17 @@
 #include "utils/relcache.h"
 #include "utils/snapshot.h"
 
-/*
- *	The information at the start of the compressed toast data.
- */
-typedef struct toast_compress_header
-{
-	int32		vl_len_;		/* varlena header (do not touch directly!) */
-	uint32		tcinfo;			/* 2 bits for compression method and 30 bits
-								 * external size; see va_extinfo */
-} toast_compress_header;
-
 /*
  * Utilities for manipulation of header information for compressed
  * toast entries.
  */
-#define TOAST_COMPRESS_EXTSIZE(ptr) \
-	(((toast_compress_header *) (ptr))->tcinfo & VARLENA_EXTSIZE_MASK)
-#define TOAST_COMPRESS_METHOD(ptr) \
-	(((toast_compress_header *) (ptr))->tcinfo >> VARLENA_EXTSIZE_BITS)
-
 #define TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(ptr, len, cm_method) \
 	do { \
 		Assert((len) > 0 && (len) <= VARLENA_EXTSIZE_MASK); \
 		Assert((cm_method) == TOAST_PGLZ_COMPRESSION_ID || \
 			   (cm_method) == TOAST_LZ4_COMPRESSION_ID); \
-		((toast_compress_header *) (ptr))->tcinfo = \
-			(len) | ((uint32) (cm_method) << VARLENA_EXTSIZE_BITS); \
+		((varattrib_4b *)(ptr))->va_compressed.va_tcinfo = \
+			((uint32)(len)) | ((uint32)(cm_method) << VARLENA_EXTSIZE_BITS); \
 	} while (0)
 
 extern Datum toast_compress_datum(Datum value, char cmethod);
diff --git a/src/include/varatt.h b/src/include/varatt.h
index aa36e8e1f56..52b17c349c3 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -53,17 +53,41 @@ typedef struct varatt_external_int8
 	int32		va_rawsize;		/* Original data size (includes header) */
 	uint32		va_extinfo;		/* External saved size (without header) and
 								 * compression method */
+
 	/*
-	 * Unique ID of value within TOAST table, as two uint32 for alignment
-	 * and padding.
-	 * XXX: think for example about the addition of an extra field for
-	 * meta-data and/or more compression data, even if it's OK here).
+	 * Unique ID of value within TOAST table, as two uint32 for alignment and
+	 * padding.
 	 */
 	uint32		va_valueid_lo;
 	uint32		va_valueid_hi;
 	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
 }			varatt_external_int8;
 
+typedef struct varatt_external_ce_oid
+{
+	int32		va_rawsize;		/* Original data size (includes header) */
+	uint32		va_extinfo;		/* External saved size (without header) and
+								 * VARATT_CE_FLAG in top 2 bits */
+	uint32		va_ecinfo;		/* Extended compression info */
+	Oid			va_valueid;		/* Unique ID of value within TOAST table */
+	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
+}			varatt_external_ce_oid;
+
+typedef struct varatt_external_ce_int8
+{
+	int32		va_rawsize;		/* Original data size (includes header) */
+	uint32		va_extinfo;		/* External saved size (without header) and
+								 * VARATT_CE_FLAG in top 2 bits */
+	uint32		va_ecinfo;		/* Extended compression info */
+
+	/*
+	 * Unique ID of value within TOAST table, as two uint32 for alignment and
+	 * padding.
+	 */
+	uint32		va_valueid_lo;
+	uint32		va_valueid_hi;
+	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
+}			varatt_external_ce_int8;
 
 /*
  * These macros define the "saved size" portion of va_extinfo.  Its remaining
@@ -115,6 +139,8 @@ typedef enum vartag_external
 	VARTAG_EXPANDED_RO = 2,
 	VARTAG_EXPANDED_RW = 3,
 	VARTAG_ONDISK_INT8 = 4,
+	VARTAG_ONDISK_CE_OID = 5,
+	VARTAG_ONDISK_CE_INT8 = 6,
 	VARTAG_ONDISK_OID = 18
 } vartag_external;
 
@@ -127,6 +153,8 @@ typedef enum vartag_external
 	 VARTAG_IS_EXPANDED(tag) ? sizeof(varatt_expanded) : \
 	 (tag) == VARTAG_ONDISK_OID ? sizeof(varatt_external_oid) : \
 	 (tag) == VARTAG_ONDISK_INT8 ? sizeof(varatt_external_int8) : \
+	 (tag) == VARTAG_ONDISK_CE_OID ? sizeof(varatt_external_ce_oid): \
+	 (tag) == VARTAG_ONDISK_CE_INT8 ? sizeof(varatt_external_ce_int8): \
 	 (AssertMacro(false), 0))
 
 /*
@@ -152,6 +180,21 @@ typedef union
 								 * compression method; see va_extinfo */
 		char		va_data[FLEXIBLE_ARRAY_MEMBER]; /* Compressed data */
 	}			va_compressed;
+	struct
+	{
+		uint32		va_header;
+		uint32		va_tcinfo;	/* Original data size (excludes header) and
+								 * compression method or VARATT_CE_FLAG flag;
+								 * see va_extinfo */
+		uint32		va_ecinfo;	/* Extended compression info: 32-bit field
+								 * where only the lower 8 bits are used for
+								 * compression method. Upper 24 bits are
+								 * reserved/unused. Lower 8 bits layout: Bits
+								 * 7–1: encode (cmid − 2), so cmid is
+								 * [2…129] Bit 0: flag for extra metadata
+								 */
+		char		va_data[FLEXIBLE_ARRAY_MEMBER];
+	}			va_compressed_ext;
 } varattrib_4b;
 
 typedef struct
@@ -321,8 +364,13 @@ typedef struct
 	(VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID)
 #define VARATT_IS_EXTERNAL_ONDISK_INT8(PTR) \
 	(VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_INT8)
+#define VARATT_IS_EXTERNAL_ONDISK_CE_OID(PTR) \
+	(VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_CE_OID)
+#define VARATT_IS_EXTERNAL_ONDISK_CE_INT8(PTR) \
+	(VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_CE_INT8)
 #define VARATT_IS_EXTERNAL_ONDISK(PTR) \
-	(VARATT_IS_EXTERNAL_ONDISK_OID(PTR) || VARATT_IS_EXTERNAL_ONDISK_INT8(PTR))
+	(VARATT_IS_EXTERNAL_ONDISK_OID(PTR) || VARATT_IS_EXTERNAL_ONDISK_INT8(PTR) \
+	 || VARATT_IS_EXTERNAL_ONDISK_CE_OID(PTR) || VARATT_IS_EXTERNAL_ONDISK_CE_INT8(PTR))
 #define VARATT_IS_EXTERNAL_INDIRECT(PTR) \
 	(VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_INDIRECT)
 #define VARATT_IS_EXTERNAL_EXPANDED_RO(PTR) \
@@ -359,10 +407,15 @@ typedef struct
 	 (VARATT_IS_1B(PTR) ? VARDATA_1B(PTR) : VARDATA_4B(PTR))
 
 /* Decompressed size and compression method of a compressed-in-line Datum */
-#define VARDATA_COMPRESSED_GET_EXTSIZE(PTR) \
-	(((varattrib_4b *) (PTR))->va_compressed.va_tcinfo & VARLENA_EXTSIZE_MASK)
+#define VARDATA_COMPRESSED_GET_EXTSIZE(PTR)														\
+	(																							\
+		(VARATT_IS_EXTENDED_COMPRESSED(PTR))													\
+			? ( ((varattrib_4b *)(PTR))->va_compressed_ext.va_tcinfo & VARLENA_EXTSIZE_MASK )	\
+			: ( ((varattrib_4b *)(PTR))->va_compressed.va_tcinfo & VARLENA_EXTSIZE_MASK )		\
+	)
 #define VARDATA_COMPRESSED_GET_COMPRESS_METHOD(PTR) \
-	(((varattrib_4b *) (PTR))->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS)
+	( (VARATT_IS_EXTENDED_COMPRESSED(PTR)) ? VARATT_CE_GET_COMPRESS_METHOD(((varattrib_4b *) (PTR))->va_compressed_ext.va_ecinfo) \
+	: (((varattrib_4b *) (PTR))->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS))
 
 /*
  * Same for external Datums; but note argument is a struct
@@ -370,16 +423,6 @@ typedef struct
  */
 #define VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) \
 	((toast_pointer).va_extinfo & VARLENA_EXTSIZE_MASK)
-#define VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) \
-	((toast_pointer).va_extinfo >> VARLENA_EXTSIZE_BITS)
-
-#define VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, len, cm) \
-	do { \
-		Assert((cm) == TOAST_PGLZ_COMPRESSION_ID || \
-			   (cm) == TOAST_LZ4_COMPRESSION_ID); \
-		((toast_pointer).va_extinfo = \
-			(len) | ((uint32) (cm) << VARLENA_EXTSIZE_BITS)); \
-	} while (0)
 
 /*
  * Testing whether an externally-stored value is compressed now requires
@@ -393,5 +436,41 @@ typedef struct
  (VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) < \
   (toast_pointer).va_rawsize - VARHDRSZ)
 
+/* Extended compression flag (0b11) marks use of extended compression methods */
+#define VARATT_CE_FLAG             0x3
+
+/*
+ * Extended compression info encoding (8-bit layout):
+ *
+ *   bit 7   6   5   4   3   2   1   0
+ *  +---+---+---+---+---+---+---+---+
+ *  |      compression_id       | M |
+ *  +---+---+---+---+---+---+---+---+
+ *
+ * • Bits 7–1: Compression method ID offset (cmid − 2)
+ *   Range: [0…127] maps to compression ID [2…129]
+ *
+ * • Bit 0 (M): Metadata flag (currently unused, always 0)
+ *   Reserved for future use to indicate extra compression metadata
+ */
+#define VARATT_CE_SET_COMPRESS_METHOD(va_ecinfo, cmid)		\
+	do {													\
+		uint8 _cmid = (uint8)(cmid);						\
+		Assert(_cmid >= 2 && _cmid <= 129);					\
+		(va_ecinfo) = (uint32)((_cmid - 2) << 1);			\
+	} while (0)
+
+#define VARATT_CE_GET_COMPRESS_METHOD(ecinfo)	((((uint8)(ecinfo) >> 1) & 0x7F) + 2)
+
+/* Test if varattrib_4b uses extended compression format */
+#define VARATT_IS_EXTENDED_COMPRESSED(ptr) \
+	((((varattrib_4b *)(ptr))->va_compressed_ext.va_tcinfo >> VARLENA_EXTSIZE_BITS) \
+		== VARATT_CE_FLAG)
+
+/* Access compressed data payload in extended format */
+#define VARDATA_EXTENDED_COMPRESSED(ptr) \
+	(((varattrib_4b *)(ptr))->va_compressed_ext.va_data)
+
+#define VARHDRSZ_EXTENDED_COMPRESSED	(offsetof(varattrib_4b, va_compressed_ext.va_data))
 
 #endif
-- 
2.47.1

#25Hannu Krosing
hannuk@google.com
In reply to: Nikhil Kumar Veldanda (#24)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

I have been evolving details for Direct TOAST design in
https://wiki.postgresql.org/wiki/DirectTOAST

The top level goals are

* 8-byte TOAST pointer - just (header:1, tag:1 and TID:6)
* all other info moved from toast pointer to actual toast record(s),
so heap rows are smaller and faster.
* all extra fields are bytea with internal encoding (maybe will create
full new types for these, or maybe just introspection functions are
enough)
the reasons for this are
- PostgresSQL arrays add 20 byte overhead
- bytea gives other freedoms in encoding for minimal space usage

No solution yet for va_toastrelid , but hope is
- to use some kind of mapping and find one or two free bits somewhere
(tid has one free),
- or add a 12-byte toast pointer just for this.
- or to make sure that CLUSTER and VACUUM FULL can be done without
needing va_toastrelid. I assume it is there for clustering the TOAST
which will be not possible separately from the main heap with direct
toast tid pointers anyway.

Please take a look and poke holes in it !

On Sun, Jul 20, 2025 at 10:28 AM Nikhil Kumar Veldanda
<veldanda.nikhilkumar17@gmail.com> wrote:

Show quoted text

Hi,

v26-0014-Design-to-extend-the-varattrib_4b-and-toast-poin.patch:
Design proposal covering varattrib_4b, TOAST pointer layouts, and
related macro updates.
v26-0015-Implement-Zstd-compression-no-dictionary-support.patch: Plain
ZSTD (non dict) support and few basic tests.

Sending v27 patch with a small update over v26 patch.

v27-0014-Design-to-extend-the-varattrib_4b-and-toast-poin.patch:
Design proposal covering varattrib_4b, TOAST pointer layouts, and
related macro updates.
v27-0015-Implement-Zstd-compression-no-dictionary-support.patch: Plain
ZSTD (non dict) support and few basic tests.

--
Nikhil Veldanda

--
Nikhil Veldanda

#26Nikita Malakhov
hukutoc@gmail.com
In reply to: Hannu Krosing (#25)
1 attachment(s)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

Hi!

Michael and Hannu, here's a POC patch with direct TIDs TOAST.
The simplest implementation where we store a chain of TIDs, each
chunk stores the next TID to be fetched. Patch applies on top of
commit 998b0b51d5ea763be081804434f177082ba6772b (origin/toast_64bit_v2)
Author: Michael Paquier <michael@paquier.xyz>
Date: Thu Jun 19 13:09:11 2025 +0900

While it is very fast on small data - I see several disadvantages:
- first of all, VACUUM should be revised to work with such tables;
- problematic batch insertion due to necessity to store TID chain.

It is just a POC implementation, so please don't blame me for
questionable decisions.

Any opinions and feedback welcome!

PS: Hannu, just seen your latest message, will check it out now.

On Mon, Jul 21, 2025 at 3:15 AM Hannu Krosing <hannuk@google.com> wrote:

I have been evolving details for Direct TOAST design in
https://wiki.postgresql.org/wiki/DirectTOAST

The top level goals are

* 8-byte TOAST pointer - just (header:1, tag:1 and TID:6)
* all other info moved from toast pointer to actual toast record(s),
so heap rows are smaller and faster.
* all extra fields are bytea with internal encoding (maybe will create
full new types for these, or maybe just introspection functions are
enough)
the reasons for this are
- PostgresSQL arrays add 20 byte overhead
- bytea gives other freedoms in encoding for minimal space usage

No solution yet for va_toastrelid , but hope is
- to use some kind of mapping and find one or two free bits somewhere
(tid has one free),
- or add a 12-byte toast pointer just for this.
- or to make sure that CLUSTER and VACUUM FULL can be done without
needing va_toastrelid. I assume it is there for clustering the TOAST
which will be not possible separately from the main heap with direct
toast tid pointers anyway.

Please take a look and poke holes in it !

--
Regards,
Nikita Malakhov
Postgres Professional
The Russian Postgres Company
https://postgrespro.ru/

Attachments:

v1-0001-direct-tids-toast.patchapplication/octet-stream; name=v1-0001-direct-tids-toast.patchDownload
From 105fa76fd903b91c9f7d9cd5a8103e88d00e6103 Mon Sep 17 00:00:00 2001
From: Nikita Malakhov <n.malakhov@postgrespro.ru>
Date: Sun, 13 Jul 2025 11:45:42 +0300
Subject: [PATCH] [POC] TOAST with direct TIDs TOAST table with direct TID
 chains instead of index. New TOAST pointer structure varatt_external_tid is
 introduced with corresponding vartag.

Default TOAST mechanics is suppressed by setting GUC
set toast_tid_enabled to true;
New TOAST tables contain 2 attributes - chunk_data
and next_tid. TOAST pointer stores first TID of chain,
and detoast fetches data chunks by tids.

Advantage: no TOAST index, very fast with small data.
Disadvantages: 1) VACUUM should be refactored for such
tables; 2) Slow on large data because tid chunks
are inserted one by one - we need to store inserted
tid in previous chunk, this cannot be done as a batch.

Usage example:
set toast_tid_enabled to true;
create table t (id int, t text);
alter table t alter column t set storage external; --to disable compression
insert into t values (1, repeat('a',20000));
select * from t;
---
 src/backend/access/common/detoast.c         |  19 +-
 src/backend/access/common/toast_external.c  |  65 ++++
 src/backend/access/common/toast_internals.c | 384 ++++++++++++++++++++
 src/backend/access/heap/heaptoast.c         |   8 +-
 src/backend/access/table/toast_helper.c     |  29 +-
 src/backend/catalog/toasting.c              | 190 +++++++++-
 src/backend/utils/misc/guc_tables.c         |   9 +
 src/include/access/toast_external.h         |   2 +
 src/include/access/toast_helper.h           |   2 +
 src/include/access/toast_internals.h        |  10 +
 src/include/access/toast_type.h             |   1 +
 src/include/varatt.h                        |  32 +-
 12 files changed, 740 insertions(+), 11 deletions(-)

diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 684e1b0b7d..06402b4b03 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -116,7 +116,11 @@ detoast_external_attr(struct varlena *attr)
 struct varlena *
 detoast_attr(struct varlena *attr)
 {
-	if (VARATT_IS_EXTERNAL_ONDISK(attr))
+	if (VARATT_IS_EXTERNAL_DIRECT_TIDS(attr))
+	{
+		attr = direct_tids_fetch_datum(PointerGetDatum(attr), 0, -1); //VARATT_EXTERNAL_GET_EXTSIZE((varatt_external_tid *) VARDATA_EXTERNAL(attr)));
+	}
+	else if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
 		/*
 		 * This is an externally stored datum --- fetch it back from there
@@ -281,6 +285,12 @@ detoast_attr_slice(struct varlena *attr,
 		/* pass it off to detoast_external_attr to flatten */
 		preslice = detoast_external_attr(attr);
 	}
+	else if (VARATT_IS_EXTERNAL_DIRECT_TIDS(attr))
+	{
+		/* pass it off to detoast_external_attr to flatten */
+		preslice = detoast_external_attr(attr);
+	}
+
 	else
 		preslice = attr;
 
@@ -457,7 +467,12 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 	toastrel = table_open(toast_pointer.toastrelid, AccessShareLock);
 
 	/* Fetch all chunks */
-	table_relation_fetch_toast_slice(toastrel,
+	if(VARATT_IS_EXTERNAL_DIRECT_TIDS(attr))
+	{
+		result = direct_tids_fetch_datum(PointerGetDatum(attr), sliceoffset, slicelength);
+	}
+	else
+		table_relation_fetch_toast_slice(toastrel,
 									 valueid,
 									 attrsize, sliceoffset, slicelength,
 									 result);
diff --git a/src/backend/access/common/toast_external.c b/src/backend/access/common/toast_external.c
index 0e79ac8aca..6fc08b12d9 100644
--- a/src/backend/access/common/toast_external.c
+++ b/src/backend/access/common/toast_external.c
@@ -38,6 +38,8 @@ static struct varlena *ondisk_oid_create_external_data(toast_external_data data)
 static uint64 ondisk_oid_get_new_value(Relation toastrel, Oid indexid,
 									   AttrNumber attnum);
 
+static void direct_tids_to_external_data(struct varlena *attr, toast_external_data *data);
+static struct varlena *direct_tids_create_external_data(toast_external_data data);
 
 /*
  * Size of an EXTERNAL datum that contains a standard TOAST pointer
@@ -51,6 +53,8 @@ static uint64 ondisk_oid_get_new_value(Relation toastrel, Oid indexid,
  */
 #define TOAST_POINTER_OID_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_oid))
 
+#define TOAST_POINTER_TID_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_oid))
+
 /*
  * For now there are only two types, all defined in this file.  For now this
  * is the maximum value of vartag_external, which is a historical choice.
@@ -79,6 +83,13 @@ static const toast_external_info toast_external_infos[TOAST_EXTERNAL_INFO_SIZE]
 		.create_external_data = ondisk_oid_create_external_data,
 		.get_new_value = ondisk_oid_get_new_value,
 	},
+	[VARTAG_ONDISK_TID] = {
+		.toast_pointer_size = TOAST_POINTER_TID_SIZE,
+		.maximum_chunk_size = TOAST_MAX_CHUNK_SIZE_OID,
+		.to_external_data = direct_tids_to_external_data,
+		.create_external_data = direct_tids_create_external_data,
+		.get_new_value = NULL,
+	},
 };
 
 
@@ -264,3 +275,57 @@ ondisk_oid_get_new_value(Relation toastrel, Oid indexid,
 {
 	return GetNewOidWithIndex(toastrel, indexid, attnum);
 }
+
+static struct varlena *
+direct_tids_create_external_data(toast_external_data data)
+{
+	struct varlena *result = NULL;
+	varatt_external_tid external;
+
+	external.va_rawsize = data.rawsize;
+
+	if (data.compression_method != TOAST_INVALID_COMPRESSION_ID)
+	{
+		/* Set size and compression method, in a single field. */
+		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(external,
+													 data.extsize,
+													 data.compression_method);
+	}
+	else
+		external.va_extinfo = data.extsize;
+
+	external.va_toastrelid = data.toastrelid;
+
+	result = (struct varlena *) palloc(TOAST_POINTER_TID_SIZE);
+	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_TID);
+	memcpy(VARDATA_EXTERNAL(result), &external, sizeof(external));
+
+	return result;
+}
+
+static void direct_tids_to_external_data(struct varlena *attr, toast_external_data *data)
+{
+
+	varatt_external_tid		external;
+
+	VARATT_EXTERNAL_GET_POINTER(external, attr);
+	data->rawsize = external.va_rawsize;
+
+	/*
+	 * External size and compression methods are stored in the same field,
+	 * extract.
+	 */
+	if (VARATT_EXTERNAL_IS_COMPRESSED(external))
+	{
+		data->extsize = VARATT_EXTERNAL_GET_EXTSIZE(external);
+		data->compression_method = VARATT_EXTERNAL_GET_COMPRESS_METHOD(external);
+	}
+	else
+	{
+		data->extsize = external.va_extinfo;
+		data->compression_method = TOAST_INVALID_COMPRESSION_ID;
+	}
+
+	ItemPointerCopy((ItemPointer) &external.tid, data->tid);
+	data->toastrelid = external.va_toastrelid;
+}
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index c6b2d1522c..eb5019eeff 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -24,11 +24,47 @@
 #include "catalog/catalog.h"
 #include "miscadmin.h"
 #include "utils/fmgroids.h"
+#include "utils/memutils.h"
 #include "utils/rel.h"
 #include "utils/snapmgr.h"
+#include "varatt.h"
 
 static bool toastrel_valueid_exists(Relation toastrel, uint64 valueid);
 static bool toastid_valueid_exists(Oid toastrelid, uint64 valueid);
+static Datum
+toast_save_datum_tids(Relation rel, Datum value,
+					   int options);
+
+#define VARATT_DIRECT_TIDS_HDRSZ \
+	sizeof(varatt_external_tid)
+
+#define VARATT_TIDS_GET_TOASTPOINTER(PTR) \
+	((varatt_external_tid *) VARDATA_EXTERNAL(PTR))
+
+#define VARATT_TIDS_GET_DATA_RAW_SIZE(PTR) \
+	(VARATT_TIDS_GET_TOASTPOINTER(PTR)->va_rawsize)
+
+#define VARATT_TIDS_SET_DATA_RAW_SIZE(PTR, V) \
+	((VARATT_TIDS_GET_TOASTPOINTER(PTR))->va_rawsize = (V))
+
+#define VARATT_TIDS_GET_DATA_SIZE(PTR) \
+	((VARATT_TIDS_GET_TOASTPOINTER(PTR))->va_extinfo)
+
+#define VARATT_TIDS_SET_DATA_SIZE(PTR, V) \
+	((VARATT_TIDS_GET_TOASTPOINTER(PTR))->va_extinfo = (V))
+
+#define VARATT_DIRECT_TIDS_SIZE \
+	((Size) VARHDRSZ_EXTERNAL + sizeof(varatt_external_tid))
+
+#define VARSIZE_TIDS(PTR)	VARATT_DIRECT_TIDS_SIZE
+
+#define VARATT_TID_GET_DATA(attr, data) \
+do { \
+	varattrib_1b_e *attrc = (varattrib_1b_e *)(attr); \
+	Assert(VARATT_IS_EXTERNAL(attrc)); \
+	Assert(VARSIZE_EXTERNAL(attrc) >= sizeof(varatt_external_tid)); \
+	memcpy(&(data), VARDATA_EXTERNAL(attrc), sizeof(varatt_external_tid)); \
+} while (0)
 
 /* ----------
  * toast_compress_datum -
@@ -104,6 +140,354 @@ toast_compress_datum(Datum value, char cmethod)
 	}
 }
 
+/* mostly  copy of heap_fetch but works more effective with buffers. */
+static bool
+heap_fetch_cached(Relation relation, Snapshot snapshot,
+				  HeapTuple tuple, Buffer *userbuf)
+{
+	ItemPointer tid = &(tuple->t_self);
+	ItemId		lp;
+	Page		page;
+	OffsetNumber	offnum;
+
+	if (BufferIsValid(*userbuf)) {
+		if (BufferGetBlockNumber(*userbuf) != ItemPointerGetBlockNumber(tid))
+		{
+			LockBuffer(*userbuf, BUFFER_LOCK_UNLOCK);
+			*userbuf = ReleaseAndReadBuffer(*userbuf, relation,
+										  ItemPointerGetBlockNumber(tid));
+			LockBuffer(*userbuf, BUFFER_LOCK_SHARE);
+		}
+	}
+	else
+	{
+		*userbuf = ReadBuffer(relation, ItemPointerGetBlockNumber(tid));
+		LockBuffer(*userbuf, BUFFER_LOCK_SHARE);
+	}
+
+	/*
+	 * Need share lock on buffer to examine tuple commit status.
+	 */
+	page = BufferGetPage(*userbuf);
+
+	/*
+	 * We'd better check for out-of-range offnum in case of VACUUM since the
+	 * TID was obtained.
+	 */
+	offnum = ItemPointerGetOffsetNumber(tid);
+	if (offnum < FirstOffsetNumber || offnum > PageGetMaxOffsetNumber(page))
+		return false;
+
+	/*
+	 * get the item line pointer corresponding to the requested tid
+	 */
+	lp = PageGetItemId(page, offnum);
+
+	/*
+	 * Must check for deleted tuple.
+	 */
+	if (!ItemIdIsNormal(lp))
+		return false;
+
+	/*
+	 * fill in *tuple fields
+	 */
+	tuple->t_data = (HeapTupleHeader) PageGetItem(page, lp);
+	tuple->t_len = ItemIdGetLength(lp);
+	tuple->t_tableOid = RelationGetRelid(relation);
+
+	/*
+	 * check tuple visibility, then release lock
+	 */
+	return HeapTupleSatisfiesVisibility(tuple, snapshot, *userbuf);
+}
+
+static void
+toast_fetch_toast_slice_tids(Relation toastrel, ItemPointer tid,
+						struct varlena *attr, int32 attrsize,
+						int32 sliceoffset, int32 slicelength,
+						struct varlena *result)
+{
+	TupleDesc	toasttupDesc = toastrel->rd_att;
+	HeapTuple	ttup;
+	Buffer		buf = InvalidBuffer;
+	bytea	   *chunk;
+	char	   *chunkdata;
+	int32		chunksize;
+	int32		copy_offset = 0;
+	ItemPointer	t;
+	Datum		values[2];
+	bool		isnull[2];
+
+	ttup = palloc(sizeof(HeapTupleData));
+
+	ItemPointerCopy(tid, &ttup->t_self);
+
+	while (ItemPointerIsValid(&ttup->t_self) &&
+		   copy_offset < sliceoffset + slicelength)
+	{
+//		elog(NOTICE, "fetch tid (%d, %d)", ItemPointerGetBlockNumber(&ttup->t_self), ItemPointerGetOffsetNumber(&ttup->t_self));
+		if (!heap_fetch_cached(toastrel, get_toast_snapshot(), ttup, &buf))
+			elog(ERROR, "could not find chunk (%u,%u)",
+				 ItemPointerGetBlockNumber(&ttup->t_self),
+				 ItemPointerGetOffsetNumber(&ttup->t_self));
+
+		Assert(ttup->t_data != NULL);
+
+		heap_deform_tuple(ttup, toasttupDesc, values, isnull);
+		Assert(isnull[0] == false && isnull[1] == false);
+
+		t = (ItemPointer)DatumGetPointer(values[0]);
+		chunk = DatumGetByteaP(values[1]);
+
+		chunkdata = VARDATA_ANY(chunk);
+		chunksize = VARSIZE_ANY_EXHDR(chunk);
+
+		if (copy_offset >= sliceoffset)
+		{
+			if (copy_offset < sliceoffset + slicelength)
+				memcpy(VARDATA(result) + copy_offset - sliceoffset,
+					   chunkdata,
+					   Min(sliceoffset + slicelength - copy_offset, chunksize));
+		}
+		else
+		{
+			if (copy_offset + chunksize > sliceoffset)
+				memcpy(VARDATA(result),
+					   chunkdata + sliceoffset - copy_offset,
+					   Min(copy_offset + chunksize - sliceoffset, slicelength));
+		}
+
+		copy_offset = copy_offset + chunksize;
+
+		/*
+		 * t points inside buffer, so copy pointer before buffer releasing
+		 */
+		ItemPointerCopy(t, &ttup->t_self);
+	}
+
+	if (BufferIsValid(buf))
+	{
+		LockBuffer(buf, BUFFER_LOCK_UNLOCK);
+		ReleaseBuffer(buf);
+	}
+
+	pfree(ttup);
+}
+
+static void
+toast_write_slice_tids(Relation toastrel,
+				  int32 slice_length, char *slice_data, int options,
+				  ItemPointer tid /* will return tid of first chunk */)
+{
+	TupleDesc	toasttupDesc = toastrel->rd_att;
+	bytea		*chunk_data;
+	int32		max_chunks_size = TOAST_MAX_CHUNK_SIZE;
+	int32		chunk_size;
+	int32		slice_start;
+	int32		offset;
+	Datum		t_values[2];
+	bool		t_isnull[2];
+	CommandId	mycid = GetCurrentCommandId(true);
+
+	chunk_size = slice_length % max_chunks_size;
+	slice_start = slice_length - chunk_size;
+	offset = slice_length - chunk_size;
+
+	chunk_data = palloc(TOAST_MAX_CHUNK_SIZE + VARHDRSZ);
+
+	t_values[0] = PointerGetDatum(tid);
+	t_values[1] = PointerGetDatum(chunk_data);
+	t_isnull[0] = false;
+	t_isnull[1] = false;
+
+	ItemPointerSetInvalid(tid);
+
+	while (slice_start >= 0)
+	{
+		HeapTuple	toasttup;
+
+		CHECK_FOR_INTERRUPTS();
+
+		SET_VARSIZE(chunk_data, chunk_size + VARHDRSZ);
+
+		memcpy(VARDATA(chunk_data), slice_data + offset, chunk_size);
+
+		toasttup = heap_form_tuple(toasttupDesc, t_values, t_isnull);
+
+		heap_insert(toastrel, toasttup, mycid, options, NULL);
+
+		ItemPointerCopy(&(toasttup->t_self), tid);
+
+		heap_freetuple(toasttup);
+
+		if(chunk_size != max_chunks_size)
+			chunk_size = max_chunks_size;
+		slice_start = slice_start - chunk_size;
+		offset = offset - chunk_size;
+	}
+
+	pfree(chunk_data);
+}
+
+static Datum
+toast_save_datum_tids(Relation rel, Datum value,
+					   int options)
+{
+	Relation		toastrel;
+	ItemPointerData tid;
+	struct varlena	*result;
+	varatt_external_tid toast_data;
+	char			*data_p;
+	int32			data_todo;
+	Pointer			dval = DatumGetPointer(value);
+
+	data_p = VARDATA(dval);
+	data_todo = VARSIZE_ANY_EXHDR(dval);
+
+	toastrel = table_open(rel->rd_rel->reltoastrelid, RowExclusiveLock);
+
+	if (OidIsValid(rel->rd_toastoid))
+		toast_data.va_toastrelid = rel->rd_toastoid;
+	else
+		toast_data.va_toastrelid = RelationGetRelid(toastrel);
+
+	toast_write_slice_tids(toastrel,
+					  data_todo, data_p,
+					  options, &tid);
+
+	ItemPointerCopy(&tid, (ItemPointer) &(toast_data.tid));
+
+	table_close(toastrel, NoLock);
+
+	result = (struct varlena *) palloc(VARHDRSZ + VARATT_DIRECT_TIDS_HDRSZ);
+
+	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_TID);
+	VARATT_TIDS_SET_DATA_RAW_SIZE(result, VARSIZE_ANY_EXHDR(dval) + VARATT_DIRECT_TIDS_HDRSZ);
+	VARATT_TIDS_SET_DATA_SIZE(result, VARATT_DIRECT_TIDS_HDRSZ);
+	memcpy(VARDATA_EXTERNAL(result), &toast_data, VARATT_DIRECT_TIDS_HDRSZ);
+
+	return PointerGetDatum(result);
+}
+
+Datum
+direct_tids_save_datum(Relation toast_rel, Datum value, Datum oldvalue,
+			 int options)
+{
+	Datum		detoasted_newval;
+	Datum		toasted_newval;
+
+	Assert(toast_rel != NULL);
+	detoasted_newval = PointerGetDatum(detoast_attr((struct varlena *) value));
+	toasted_newval = toast_save_datum_tids(toast_rel,
+											detoasted_newval, options);
+	return toasted_newval;
+}
+
+struct varlena*
+direct_tids_fetch_datum(Datum toast_ptr, int offset, int length)
+{
+	struct varlena *result = 0;
+	varatt_external_tid data;
+	int32		attrsize = 0;
+	int32		toasted_size = 0;
+	Relation toastrel;
+	struct varlena *tvalue = (struct varlena*)DatumGetPointer(toast_ptr);
+
+	if(VARATT_IS_EXTERNAL(tvalue))
+	{
+		VARATT_TID_GET_DATA(DatumGetPointer(toast_ptr), data);
+
+		toasted_size = data.va_extinfo;
+		attrsize = toasted_size;
+
+		if (offset >= attrsize)
+		{
+			offset = 0;
+			length = 0;
+		}
+
+		if (offset + length > attrsize || length < 0)
+			length = attrsize - offset;
+
+		result = (struct varlena *) palloc(length + VARHDRSZ);
+		SET_VARSIZE(result, length + VARHDRSZ);
+
+		if (length > 0)
+		{
+			toastrel = table_open(data.va_toastrelid, AccessShareLock);
+			toast_fetch_toast_slice_tids(toastrel, (ItemPointer) &data.tid,
+									(struct varlena *) toast_ptr,
+									toasted_size, offset, length,
+									result);
+			table_close(toastrel, NoLock);
+		}
+	}
+	else
+		result = tvalue;
+
+	return result;
+}
+
+void
+direct_tids_delete_datum(Relation rel, Datum value, bool is_speculative)
+{
+	TupleDesc	toasttupDesc;
+	HeapTuple	ttup;
+	Relation toastrel;
+	Buffer		buf;
+	ItemPointer tid;
+	bool		fetch_ind = true;
+	bool		isnull;
+	varatt_external_tid data;
+
+	VARATT_TID_GET_DATA(DatumGetPointer(value), data);
+
+	if(data.va_toastrelid)
+		toastrel = table_open(data.va_toastrelid, RowExclusiveLock);
+	else return;
+
+	toasttupDesc = toastrel->rd_att;
+	ttup = palloc(sizeof(HeapTupleData));
+	tid = palloc(sizeof(ItemPointerData));
+
+	ItemPointerCopy((ItemPointer) &data.tid, tid);
+
+	do
+	{
+		ItemPointerCopy(tid, &ttup->t_self);
+
+		if (!heap_fetch(toastrel, get_toast_snapshot(), ttup, &buf, true))
+			fetch_ind = false;
+
+		if(fetch_ind)
+		{
+			tid = (ItemPointer) DatumGetPointer(fastgetattr(ttup, 1, toasttupDesc, &isnull));
+
+			Assert(!isnull);
+			if (is_speculative)
+				heap_abort_speculative(toastrel, &(ttup->t_self));
+			else
+				simple_heap_delete(toastrel, &(ttup->t_self));
+
+			if(!ItemPointerIsValid(tid))
+				fetch_ind = false;
+		}
+		else
+			fetch_ind = false;
+	}
+	while(fetch_ind);
+
+	if (BufferIsValid(buf))
+	{
+		LockBuffer(buf, BUFFER_LOCK_UNLOCK);
+		ReleaseBuffer(buf);
+	}
+
+	table_close(toastrel, NoLock);
+	pfree(ttup);
+}
+
 /* ----------
  * toast_save_datum -
  *
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index 87f1630d85..d4c35214d1 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -165,7 +165,9 @@ heap_toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup,
 				 rel->rd_rel->reltoastrelid);
 		atttoast = (Form_pg_attribute) GETSTRUCT(atttuple);
 
-		if (atttoast->atttypid == OIDOID)
+		if(toast_tid_enabled)
+			vartag = VARTAG_ONDISK_TID;
+		else if (atttoast->atttypid == OIDOID)
 			vartag = VARTAG_ONDISK_OID;
 		else if (atttoast->atttypid == INT8OID)
 			vartag = VARTAG_ONDISK_INT8;
@@ -184,7 +186,9 @@ heap_toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup,
 		 */
 		uint8	vartag = VARTAG_ONDISK_OID;
 
-		if (default_toast_type == TOAST_TYPE_INT8)
+		if(toast_tid_enabled)
+			vartag = VARTAG_ONDISK_TID;
+		else if (default_toast_type == TOAST_TYPE_INT8)
 			vartag = VARTAG_ONDISK_INT8;
 		else if (default_toast_type == TOAST_TYPE_OID)
 			vartag = VARTAG_ONDISK_OID;
diff --git a/src/backend/access/table/toast_helper.c b/src/backend/access/table/toast_helper.c
index a2b44e093d..9cad7707f1 100644
--- a/src/backend/access/table/toast_helper.c
+++ b/src/backend/access/table/toast_helper.c
@@ -266,8 +266,17 @@ toast_tuple_externalize(ToastTupleContext *ttc, int attribute, int options)
 	ToastAttrInfo *attr = &ttc->ttc_attr[attribute];
 
 	attr->tai_colflags |= TOASTCOL_IGNORE;
-	*value = toast_save_datum(ttc->ttc_rel, old_value, attr->tai_oldexternal,
+	if(toast_tid_enabled)
+	{
+		*value = direct_tids_save_datum(ttc->ttc_rel, old_value, PointerGetDatum(attr->tai_oldexternal),
+							  options);
+	}
+	else
+	{
+		*value = toast_save_datum(ttc->ttc_rel, old_value, attr->tai_oldexternal,
 							  options);
+	}
+
 	if ((attr->tai_colflags & TOASTCOL_NEEDS_FREE) != 0)
 		pfree(DatumGetPointer(old_value));
 	attr->tai_colflags |= TOASTCOL_NEEDS_FREE;
@@ -311,7 +320,14 @@ toast_tuple_cleanup(ToastTupleContext *ttc)
 			ToastAttrInfo *attr = &ttc->ttc_attr[i];
 
 			if ((attr->tai_colflags & TOASTCOL_NEEDS_DELETE_OLD) != 0)
-				toast_delete_datum(ttc->ttc_rel, ttc->ttc_oldvalues[i], false);
+			{
+				if(toast_tid_enabled)
+				{
+					direct_tids_delete_datum(ttc->ttc_rel, ttc->ttc_oldvalues[i], false);
+				}
+				else
+					toast_delete_datum(ttc->ttc_rel, ttc->ttc_oldvalues[i], false);
+			}
 		}
 	}
 }
@@ -337,7 +353,14 @@ toast_delete_external(Relation rel, const Datum *values, const bool *isnull,
 			if (isnull[i])
 				continue;
 			else if (VARATT_IS_EXTERNAL_ONDISK(value))
-				toast_delete_datum(rel, value, is_speculative);
+			{
+				if(toast_tid_enabled)
+				{
+					direct_tids_delete_datum(rel, value, is_speculative);
+				}
+				else
+					toast_delete_datum(rel, value, is_speculative);
+			}
 		}
 	}
 }
diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c
index 3df83c9835..91866d0382 100644
--- a/src/backend/catalog/toasting.c
+++ b/src/backend/catalog/toasting.c
@@ -33,9 +33,11 @@
 #include "utils/fmgroids.h"
 #include "utils/rel.h"
 #include "utils/syscache.h"
+#include "utils/guc.h"
 
 /* GUC support */
 int			default_toast_type = TOAST_TYPE_OID;
+bool		toast_tid_enabled = false;
 
 static void CheckAndCreateToastTable(Oid relOid, Datum reloptions,
 									 LOCKMODE lockmode, bool check,
@@ -45,6 +47,15 @@ static bool create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 							   Oid OIDOldToast);
 static bool needs_toast_table(Relation rel);
 
+static bool
+create_toast_table_tid(Relation rel, Oid toastOid, Oid toastIndexOid,
+				   Datum reloptions, LOCKMODE lockmode, bool check,
+				   Oid OIDOldToast);
+static bool
+create_toast_table_default(Relation rel, Oid toastOid, Oid toastIndexOid,
+				   Datum reloptions, LOCKMODE lockmode, bool check,
+				   Oid OIDOldToast);
+
 
 /*
  * CreateToastTable variants
@@ -119,7 +130,6 @@ BootstrapToastTable(char *relName, Oid toastOid, Oid toastIndexOid)
 	table_close(rel, NoLock);
 }
 
-
 /*
  * create_toast_table --- internal workhorse
  *
@@ -131,6 +141,184 @@ static bool
 create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 				   Datum reloptions, LOCKMODE lockmode, bool check,
 				   Oid OIDOldToast)
+{
+	if(toast_tid_enabled)
+	{
+		return create_toast_table_tid(rel, toastOid, toastIndexOid,
+				   reloptions, lockmode, check,
+				   OIDOldToast);
+	}
+
+	return create_toast_table_default(rel, toastOid, toastIndexOid,
+			   reloptions, lockmode, check,
+			   OIDOldToast);
+
+}
+
+static bool
+create_toast_table_tid(Relation rel, Oid toastOid, Oid toastIndexOid,
+				   Datum reloptions, LOCKMODE lockmode, bool check,
+				   Oid OIDOldToast)
+{
+	Oid			relOid = RelationGetRelid(rel);
+	HeapTuple	reltup;
+	TupleDesc	tupdesc;
+	bool		shared_relation;
+	bool		mapped_relation;
+	Relation	toast_rel;
+	Relation	class_rel;
+	Oid			toast_relid;
+	Oid			namespaceid;
+	char		toast_relname[NAMEDATALEN];
+	ObjectAddress baseobject,
+				toastobject;
+
+	if (rel->rd_rel->reltoastrelid != InvalidOid)
+		return false;
+
+	if (!IsBinaryUpgrade)
+	{
+		if (!needs_toast_table(rel))
+			return false;
+	}
+	else
+	{
+		if (!OidIsValid(binary_upgrade_next_toast_pg_class_oid))
+			return false;
+	}
+
+	if (check && lockmode != AccessExclusiveLock)
+		elog(ERROR, "AccessExclusiveLock required to add toast table.");
+
+	snprintf(toast_relname, sizeof(toast_relname),
+			 "pg_toast_%u", relOid);
+
+	tupdesc = CreateTemplateTupleDesc(2);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 1,
+					   "next_tid",
+					   TIDOID,
+					   -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 2,
+					   "chunk_data",
+					   BYTEAOID,
+					   -1, 0);
+
+	TupleDescAttr(tupdesc, 0)->attstorage = TYPSTORAGE_PLAIN;
+	TupleDescAttr(tupdesc, 1)->attstorage = TYPSTORAGE_PLAIN;
+
+	TupleDescAttr(tupdesc, 0)->attcompression = InvalidCompressionMethod;
+	TupleDescAttr(tupdesc, 1)->attcompression = InvalidCompressionMethod;
+
+	if (isTempOrTempToastNamespace(rel->rd_rel->relnamespace))
+		namespaceid = GetTempToastNamespace();
+	else
+		namespaceid = PG_TOAST_NAMESPACE;
+
+	shared_relation = rel->rd_rel->relisshared;
+
+	mapped_relation = RelationIsMapped(rel);
+
+	toast_relid = heap_create_with_catalog(toast_relname,
+										   namespaceid,
+										   rel->rd_rel->reltablespace,
+										   toastOid,
+										   InvalidOid,
+										   InvalidOid,
+										   rel->rd_rel->relowner,
+										   table_relation_toast_am(rel),
+										   tupdesc,
+										   NIL,
+										   RELKIND_TOASTVALUE,
+										   rel->rd_rel->relpersistence,
+										   shared_relation,
+										   mapped_relation,
+										   ONCOMMIT_NOOP,
+										   reloptions,
+										   false,
+										   true,
+										   true,
+										   OIDOldToast,
+										   NULL);
+	Assert(toast_relid != InvalidOid);
+
+	CommandCounterIncrement();
+
+	toast_rel = table_open(toast_relid, ShareLock);
+
+	table_close(toast_rel, NoLock);
+
+	class_rel = table_open(RelationRelationId, RowExclusiveLock);
+
+	reltup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relOid));
+	if (!HeapTupleIsValid(reltup))
+		elog(ERROR, "cache lookup failed for relation %u", relOid);
+
+	((Form_pg_class) GETSTRUCT(reltup))->reltoastrelid = toast_relid;
+
+	if (!IsBootstrapProcessingMode())
+	{
+		/* normal case, use a transactional update */
+		reltup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relOid));
+		if (!HeapTupleIsValid(reltup))
+			elog(ERROR, "cache lookup failed for relation %u", relOid);
+
+		((Form_pg_class) GETSTRUCT(reltup))->reltoastrelid = toast_relid;
+
+		CatalogTupleUpdate(class_rel, &reltup->t_self, reltup);
+	}
+	else
+	{
+		/* While bootstrapping, we cannot UPDATE, so overwrite in-place */
+
+		ScanKeyData key[1];
+		void	   *state;
+
+		ScanKeyInit(&key[0],
+					Anum_pg_class_oid,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(relOid));
+		systable_inplace_update_begin(class_rel, ClassOidIndexId, true,
+									  NULL, 1, key, &reltup, &state);
+		if (!HeapTupleIsValid(reltup))
+			elog(ERROR, "cache lookup failed for relation %u", relOid);
+
+		((Form_pg_class) GETSTRUCT(reltup))->reltoastrelid = toast_relid;
+
+		systable_inplace_update_finish(state, reltup);
+	}
+
+	heap_freetuple(reltup);
+
+	table_close(class_rel, RowExclusiveLock);
+
+	if (!IsBootstrapProcessingMode())
+	{
+		baseobject.classId = RelationRelationId;
+		baseobject.objectId = relOid;
+		baseobject.objectSubId = 0;
+		toastobject.classId = RelationRelationId;
+		toastobject.objectId = toast_relid;
+		toastobject.objectSubId = 0;
+
+		recordDependencyOn(&toastobject, &baseobject, DEPENDENCY_INTERNAL);
+	}
+
+	CommandCounterIncrement();
+
+	return true;
+}
+
+/*
+ * create_toast_table --- internal workhorse
+ *
+ * rel is already opened and locked
+ * toastOid and toastIndexOid are normally InvalidOid, but during
+ * bootstrap they can be nonzero to specify hand-assigned OIDs
+ */
+static bool
+create_toast_table_default(Relation rel, Oid toastOid, Oid toastIndexOid,
+				   Datum reloptions, LOCKMODE lockmode, bool check,
+				   Oid OIDOldToast)
 {
 	Oid			relOid = RelationGetRelid(rel);
 	HeapTuple	reltup;
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 0999a2b00b..bb13786c42 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2151,6 +2151,15 @@ struct config_bool ConfigureNamesBool[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"toast_tid_enabled", PGC_USERSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Enables TOAST tables with direct TIDs."),
+		},
+		&toast_tid_enabled,
+		false,
+		NULL, NULL, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, false, NULL, NULL, NULL
diff --git a/src/include/access/toast_external.h b/src/include/access/toast_external.h
index 1a7c61454f..874e2612da 100644
--- a/src/include/access/toast_external.h
+++ b/src/include/access/toast_external.h
@@ -17,6 +17,7 @@
 
 #include "access/attnum.h"
 #include "access/toast_compression.h"
+#include "storage/itemptr.h"
 #include "utils/relcache.h"
 #include "varatt.h"
 
@@ -43,6 +44,7 @@ typedef struct toast_external_data
 	 * them.  InvalidToastId if invalid.
 	 */
 	uint64		value;
+	ItemPointer tid;
 } toast_external_data;
 
 /*
diff --git a/src/include/access/toast_helper.h b/src/include/access/toast_helper.h
index 729c593afe..a67460acc8 100644
--- a/src/include/access/toast_helper.h
+++ b/src/include/access/toast_helper.h
@@ -16,6 +16,8 @@
 
 #include "utils/rel.h"
 
+extern PGDLLIMPORT bool toast_tid_enabled;
+
 /*
  * Information about one column of a tuple being toasted.
  *
diff --git a/src/include/access/toast_internals.h b/src/include/access/toast_internals.h
index 06ae8583c1..413ada8667 100644
--- a/src/include/access/toast_internals.h
+++ b/src/include/access/toast_internals.h
@@ -60,4 +60,14 @@ extern void toast_close_indexes(Relation *toastidxs, int num_indexes,
 								LOCKMODE lock);
 extern Snapshot get_toast_snapshot(void);
 
+extern struct varlena*
+direct_tids_fetch_datum(Datum toast_ptr, int offset, int length);
+
+extern Datum
+direct_tids_save_datum(Relation toast_rel, Datum value, Datum oldvalue,
+			 int options);
+
+extern void
+direct_tids_delete_datum(Relation rel, Datum value, bool is_speculative);
+
 #endif							/* TOAST_INTERNALS_H */
diff --git a/src/include/access/toast_type.h b/src/include/access/toast_type.h
index 494c2a3e85..d0c3b5e163 100644
--- a/src/include/access/toast_type.h
+++ b/src/include/access/toast_type.h
@@ -19,6 +19,7 @@
  * Detault value type in toast table.
  */
 extern PGDLLIMPORT int default_toast_type;
+extern PGDLLIMPORT bool toast_tid_enabled;
 
 typedef enum ToastTypeId
 {
diff --git a/src/include/varatt.h b/src/include/varatt.h
index aa36e8e1f5..f98ec75e54 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -41,6 +41,27 @@ typedef struct varatt_external_oid
 	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
 }			varatt_external_oid;
 
+typedef struct TidBlk
+{
+	uint16		bi_hi;
+	uint16		bi_lo;
+} TidBlk;
+
+typedef struct TmpTid
+{
+	TidBlk ip_blkid;
+	uint16 ip_posid;
+} TmpTid;
+
+typedef struct varatt_external_tid
+{
+	int32		va_rawsize;		/* Original data size (includes header) */
+	uint32		va_extinfo;		/* External saved size (without header) and
+								 * compression method */
+	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
+	TmpTid		tid;				/* Placeholder for ItemPointer */
+}			varatt_external_tid;
+
 /*
  * struct varatt_external_int8 is a "larger" version of "TOAST pointer",
  * that uses an 8-byte integer as value.
@@ -64,7 +85,6 @@ typedef struct varatt_external_int8
 	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
 }			varatt_external_int8;
 
-
 /*
  * These macros define the "saved size" portion of va_extinfo.  Its remaining
  * two high-order bits identify the compression method.
@@ -115,6 +135,7 @@ typedef enum vartag_external
 	VARTAG_EXPANDED_RO = 2,
 	VARTAG_EXPANDED_RW = 3,
 	VARTAG_ONDISK_INT8 = 4,
+	VARTAG_ONDISK_TID  = 5,
 	VARTAG_ONDISK_OID = 18
 } vartag_external;
 
@@ -122,11 +143,15 @@ typedef enum vartag_external
 #define VARTAG_IS_EXPANDED(tag) \
 	(((tag) & ~1) == VARTAG_EXPANDED_RO)
 
+#define VARTAG_IS_DIRECTTIDS(tag) \
+	(((tag) & ~4) == VARTAG_ONDISK_TID)
+
 #define VARTAG_SIZE(tag) \
 	((tag) == VARTAG_INDIRECT ? sizeof(varatt_indirect) : \
 	 VARTAG_IS_EXPANDED(tag) ? sizeof(varatt_expanded) : \
 	 (tag) == VARTAG_ONDISK_OID ? sizeof(varatt_external_oid) : \
 	 (tag) == VARTAG_ONDISK_INT8 ? sizeof(varatt_external_int8) : \
+	 (tag) == VARTAG_ONDISK_TID ? sizeof(varatt_external_tid) : \
 	 (AssertMacro(false), 0))
 
 /*
@@ -321,8 +346,10 @@ typedef struct
 	(VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID)
 #define VARATT_IS_EXTERNAL_ONDISK_INT8(PTR) \
 	(VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_INT8)
+#define VARATT_IS_EXTERNAL_DIRECT_TIDS(PTR) \
+	(VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_TID)
 #define VARATT_IS_EXTERNAL_ONDISK(PTR) \
-	(VARATT_IS_EXTERNAL_ONDISK_OID(PTR) || VARATT_IS_EXTERNAL_ONDISK_INT8(PTR))
+	(VARATT_IS_EXTERNAL_ONDISK_OID(PTR) || VARATT_IS_EXTERNAL_ONDISK_INT8(PTR) || VARATT_IS_EXTERNAL_DIRECT_TIDS(PTR))
 #define VARATT_IS_EXTERNAL_INDIRECT(PTR) \
 	(VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_INDIRECT)
 #define VARATT_IS_EXTERNAL_EXPANDED_RO(PTR) \
@@ -393,5 +420,4 @@ typedef struct
  (VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) < \
   (toast_pointer).va_rawsize - VARHDRSZ)
 
-
 #endif
-- 
2.34.1

#27Nikita Malakhov
hukutoc@gmail.com
In reply to: Nikita Malakhov (#26)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

Hi!

I agree that storing reltoastrelid in each Toast pointer seems to be
a waste of disk space since the current Postgres state does not
allow multiple TOAST tables per relation.
But if we consider this as a viable option it could bring additional
advantages. I've successfully tried to use multiple TOAST tables,
with different variations - by type, by column and as-is just as
an extensible storage.

--
Regards,
Nikita Malakhov
Postgres Professional
The Russian Postgres Company
https://postgrespro.ru/

#28Michael Paquier
michael@paquier.xyz
In reply to: Nikita Malakhov (#27)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

On Mon, Jul 21, 2025 at 02:20:31PM +0300, Nikita Malakhov wrote:

I agree that storing reltoastrelid in each Toast pointer seems to be
a waste of disk space since the current Postgres state does not
allow multiple TOAST tables per relation.

va_toastrelid is a central part of the current system when dealing
with a TOAST relation rewrite, because we need to know to which
relation an on-disk TOAST pointer is part of. Or do you mean that we
don't need that with what you are proposing with TIDs? Perhaps yes,
sure, I've not studied this question when associated with your patch
(which has a bunch of duplicated code that could be avoided AFAIK).

But if we consider this as a viable option it could bring additional
advantages. I've successfully tried to use multiple TOAST tables,
with different variations - by type, by column and as-is just as
an extensible storage.

I don't think we need to be that ambitious. There is no way to say
such things could have any benefits in the long-term and there's the
catalog representation part. Even if we do that, I suspect that users
would never really know which setup makes sense because we would want
to control how things happen at a relation level and not purely
automate a behavior.
--
Michael

#29Michael Paquier
michael@paquier.xyz
In reply to: Nikita Malakhov (#26)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

On Mon, Jul 21, 2025 at 02:06:03PM +0300, Nikita Malakhov wrote:

While it is very fast on small data - I see several disadvantages:
- first of all, VACUUM should be revised to work with such tables;
- problematic batch insertion due to necessity to store TID chain.

It is just a POC implementation, so please don't blame me for
questionable decisions.

Any opinions and feedback welcome!

I think that this is going to be helpful in taking some decisions with
the refactoring pieces I am proposing for the external TOAST pointer
layer. You have some interesting points around
detoast_external_attr() and detoast_attr_slice(), as far as I can see.
One point of the on-disk TOAST refactoring is that we should be able
to entirely avoid this level of redirection. I get that this is a
POC, of course, but it provides pointers that what I've done may not
be sufficient in terms of extensibility so that seems worth digging
into.

The patch posted by Nikhil is also something that touches this area,
that I have on my tablets:
/messages/by-id/CAFAfj_E-QLiUq--+Kdyvb+-Gg79LLayZRcH8+mFPzVuDQOVaAw@mail.gmail.com

It touches a different point: we need to be smarter with
CompressionToastId and not use that as the value for what we store on
disk. Each vartag_external should be able to use the compression
methods values it wishes, with the toast_external layer being in
charge of translating that back-and-forth with the GUC value.
--
Michael

#30Nikita Malakhov
hukutoc@gmail.com
In reply to: Michael Paquier (#29)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

Hi Michael!

Yes, I know about relation rewrite and have already thought about how
we can avoid excessive storage of toastrelid and do not spoil rewrite,
still do not have a good enough solution.

You have some interesting points around
detoast_external_attr() and detoast_attr_slice(), as far as I can see.
One point of the on-disk TOAST refactoring is that we should be able
to entirely avoid this level of redirection. I get that this is a
POC, of course, but it provides pointers that what I've done may not
be sufficient in terms of extensibility so that seems worth digging
into.

I'm currently re-visiting our TOAST API patch set, there are some
good (in terms of simplicity and lightweightness) proposals, will mail
later.

Some more thoughts on TIDs:
TIDs could be stored as a list instead of a chain (as Hannu proposes
in his design). This allows batch operations and storage optimization
'cause TID lists are highly compressible, but significantly complicates
the code responsible for chunk processing.
Also, Toast pointer in current state must store raw size and external
size - these two are used by the executor, and we cannot get rid
of them so lightly.

Vacuuming such a table would be a pain in the ass, we have to
somehow prevent bloating tables with a high update rate.

Also, current toast mechanics is insert-only, it does not support
updates (just to remind - the whole toasted value is marked dead
and new one is inserted during update), this is a subject to change.
And logical replication, as I mentioned earlier, does not have any
means for replicating toast diffs.

--
Regards,
Nikita Malakhov
Postgres Professional
The Russian Postgres Company
https://postgrespro.ru/

#31Hannu Krosing
hannuk@google.com
In reply to: Nikita Malakhov (#30)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

On Tue, Jul 22, 2025 at 1:24 PM Nikita Malakhov <hukutoc@gmail.com> wrote:

Hi Michael!

Yes, I know about relation rewrite and have already thought about how
we can avoid excessive storage of toastrelid and do not spoil rewrite,
still do not have a good enough solution.

The high-level idea would be to any actual rewrite -- as opposed to
plain vacuum which frees empty space within the TOAST relation -- as
part of the vacuum of the main relation.
Another option would be to store a back-pointer to the heap tuple
inside the toast tuple and use that when rewriting, though it has its
own set of complexities.

You have some interesting points around
detoast_external_attr() and detoast_attr_slice(), as far as I can see.
One point of the on-disk TOAST refactoring is that we should be able
to entirely avoid this level of redirection. I get that this is a
POC, of course, but it provides pointers that what I've done may not
be sufficient in terms of extensibility so that seems worth digging
into.

I'm currently re-visiting our TOAST API patch set, there are some
good (in terms of simplicity and lightweightness) proposals, will mail
later.

Sounds interesting.

Some more thoughts on TIDs:
TIDs could be stored as a list instead of a chain (as Hannu proposes
in his design). This allows batch operations and storage optimization
'cause TID lists are highly compressible, but significantly complicates
the code responsible for chunk processing.

I would not say it complicates the *code* very much, especially when
you keep offsets in the toast tuples so that you can copy them into
the final materialized datum in any order.
And it does allow many optimisations in terms of batching,
pre-fetching and even parallelism in case of huge toasted values.

Also, Toast pointer in current state must store raw size and external
size - these two are used by the executor, and we cannot get rid
of them so lightly.

Are these ever used without actually using the data ?
When the data _is_ also used then the cost of getting the length from
the toast record with direct toast should mostly amortize over the
full query.

Can you point to where in the code this is done ?

In long run we may want to store also the actual size in the toast
record (not toast pointer) as well for types where length() !=
octetsize bacuse currently a simple call like length(text) has to
materialize the whole thing before getting the length, whereas
pg_colum_size() and octertsize() are instantaneous.

Vacuuming such a table would be a pain in the ass, we have to
somehow prevent bloating tables with a high update rate.

Normal Vacuum should work fine. It is the rewrite that cis tricky.

Also, current toast mechanics is insert-only, it does not support
updates (just to remind - the whole toasted value is marked dead
and new one is inserted during update), this is a subject to change.
And logical replication, as I mentioned earlier, does not have any
means for replicating toast diffs.

Which points to the need to (optionally) store the diff in the toast
as well when there are defined replication slots.
Once we have a way to actually do JSON(B) updates at SQL or function level.

We may even want to store the JSON in some base JSON + JSON_PATCH
format where we materialize at retrieval.

But this goes way beyond the current patch's scope. Though my design
should accommodate it nicely.

---
Hannu

#32Michael Paquier
michael@paquier.xyz
In reply to: Hannu Krosing (#31)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

On Tue, Jul 22, 2025 at 02:56:23PM +0200, Hannu Krosing wrote:

The high-level idea would be to any actual rewrite -- as opposed to
plain vacuum which frees empty space within the TOAST relation -- as
part of the vacuum of the main relation.
Another option would be to store a back-pointer to the heap tuple
inside the toast tuple and use that when rewriting, though it has its
own set of complexities.

Well. I would suggest to begin with simpler to begin with, finding
building pieces that can be relied on, and I tend to think that I've
sorted out the first basic one of these because we want to be able to
give the backend the possibility to understand more formats of
external on-dist TOAST pointers. A simple whole rewrite of the TOAST
engine is not something that can just happen in one day: that's a very
risky move, and we need to worry about backward-compatibility while
maintaining the legacy code. FWIW, I think that we should still move
on with the 8-byte implementation anyway with specific vartag_external
that can lead to variable-sized pointers (shorter if value is less
than UINT32_MAX, still stored in a int8 TOAST table), extending the
code so as it's possible to think about more on top of that. That's
basically risk-free if done right while taking the problem at its
root. That's also what Tom and Andres meant back in 2022 before the
problem drifted away to different issues, and that should allow the
addition of more compression methods if done correctly (quoted at the
top of this thread).

You have some interesting points around
detoast_external_attr() and detoast_attr_slice(), as far as I can see.
One point of the on-disk TOAST refactoring is that we should be able
to entirely avoid this level of redirection. I get that this is a
POC, of course, but it provides pointers that what I've done may not
be sufficient in terms of extensibility so that seems worth digging
into.

I'm currently re-visiting our TOAST API patch set, there are some
good (in terms of simplicity and lightweightness) proposals, will mail
later.

Sounds interesting.

If you have refactoring proposals, that could be considered
separately, yes.

Now, the reason why nothing has been done about the original problem
is the same reason as what's happening now on this thread: I am seeing
a lot of designs and assumptions for new solutions, without knowing
all the problems we are trying to solve or even if they actually make
sense at this level of the baclend engine. And there is little to no
argument made about backward-compatibility, which is also something
I've tried to design (single GUC for dump/restore/upgrade with TOAST
type based on attribute data type of the TOAST table, which could be
also a tid later on).

But this goes way beyond the current patch's scope. Though my design
should accommodate it nicely.

If you see independent useful pieces that are worth their own, nobody
is going to object to that. The trick is to find incremental pieces
good enough to be able to build upon with individual evaluations and
reviews, at least that's how I want to tackle the problem because I
would be the one who would be responsible for its maintenance by
default. Finding out these "simple" independent relevant pieces is
the hard part.
--
Michael

#33Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#16)
14 attachment(s)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

On Wed, Jul 09, 2025 at 08:20:31AM +0900, Michael Paquier wrote:

Sure, you could do that as well, but I suspect that we'll need the
steps of at least up to 0003 to be able to handle more easily multiple
external TOAST pointer types, or the code will be messier than it
currently is. :D

Please find attached a v3, that I have spent some time polishing to
fix the value ID problem of this thread. v2 had some conflicts, and
the CI previously failed with warning job (CI is green here now).

There are two things that have changed in this patch series:
- Addition of separate patch to rename varatt_external to
varatt_external_oid and VARTAG_ONDISK to VARTAG_ONDISK_OID, in 0003.
- In the refactoring to introduce the translation layer for external
ondisk TOAST pointers, aka 0004 in this set labelled v3, I was not
happy about the way we grab the vartag_external that gets assigned to
the TOAST varlena, and it depends on two factors in this patch set:
-- The type of TOAST table.
-- The value assigned. If, for example, we have a value less than 4
billion, we can make the TOAST pointer shorter. The interface of 0004
has been rethought to take that into consideration. That's the
function called toast_external_assign_vartag(), reused on heaptoast.c
to get TOAST_POINTER_SIZE as the compressibility threshold when
inserting a tuple and if it should be compressed.

As things stand, I am getting pretty happy with the patch set up to
0005 and how things are getting in shape for the interface, and I am
planning to begin applying this stuff up to 0005 in the next couple of
weeks.

This stuff introduces all the callbacks and the concept of
toast_external to get the refactoring into the tree first, which is
something that other patches that want to extend the compression
methods or the TID layer are going to need anyway. The rest of the
patch set is more a group shot, where they actually matter only when
the new vartag_external is added for the TOAST tables able to hold
8-byte values. As of now, we would need to add more one more
vartag_external:
- For values lower than 4 billion, perhaps we could just reuse the OID
vartag_external here? The code is modular now with
toast_external_assign_vartag(), and it is only a matter of using
VARTAG_ONDISK_OID if we have a value less than 4 billion. So there is
only one place in the code to change, and that's a one-line change
with the v3 patch set attached. And that's less duplication.
- The second one when we have more than 4 billion values.
--
Michael

Attachments:

v3-0001-Refactor-some-TOAST-value-ID-code-to-use-uint64-i.patchtext/x-diff; charset=us-asciiDownload
From d3821ee322e2e4f40a78f5788da9d394ab9ba7c6 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 19 Jun 2025 09:57:19 +0900
Subject: [PATCH v3 01/14] Refactor some TOAST value ID code to use uint64
 instead of Oid

This change is a mechanical switch to change most of the code paths that
assume TOAST value IDs to be Oids to become uint64, easing an upcoming
change to allow 8-byte TOAST values.

The areas touched are related to table AM, amcheck and logical
decoding's reorder buffer.  A good chunk of the changes involve
switching printf() markers from %u to PRIu64.
---
 src/include/access/heaptoast.h                |  2 +-
 src/include/access/tableam.h                  |  4 +-
 src/backend/access/common/toast_internals.c   |  8 +--
 src/backend/access/heap/heaptoast.c           | 12 ++--
 .../replication/logical/reorderbuffer.c       | 14 ++--
 contrib/amcheck/verify_heapam.c               | 69 +++++++++++--------
 6 files changed, 62 insertions(+), 47 deletions(-)

diff --git a/src/include/access/heaptoast.h b/src/include/access/heaptoast.h
index 6385a27caf83..6e3558cbd6d2 100644
--- a/src/include/access/heaptoast.h
+++ b/src/include/access/heaptoast.h
@@ -142,7 +142,7 @@ extern HeapTuple toast_build_flattened_tuple(TupleDesc tupleDesc,
  *	Fetch a slice from a toast value stored in a heap table.
  * ----------
  */
-extern void heap_fetch_toast_slice(Relation toastrel, Oid valueid,
+extern void heap_fetch_toast_slice(Relation toastrel, uint64 valueid,
 								   int32 attrsize, int32 sliceoffset,
 								   int32 slicelength, struct varlena *result);
 
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index 1c9e802a6b12..b640047d2fdc 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -740,7 +740,7 @@ typedef struct TableAmRoutine
 	 * table implemented by this AM.  See table_relation_fetch_toast_slice()
 	 * for more details.
 	 */
-	void		(*relation_fetch_toast_slice) (Relation toastrel, Oid valueid,
+	void		(*relation_fetch_toast_slice) (Relation toastrel, uint64 valueid,
 											   int32 attrsize,
 											   int32 sliceoffset,
 											   int32 slicelength,
@@ -1873,7 +1873,7 @@ table_relation_toast_am(Relation rel)
  * stored.
  */
 static inline void
-table_relation_fetch_toast_slice(Relation toastrel, Oid valueid,
+table_relation_fetch_toast_slice(Relation toastrel, uint64 valueid,
 								 int32 attrsize, int32 sliceoffset,
 								 int32 slicelength, struct varlena *result)
 {
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 7d8be8346ce5..4a1342da6e1b 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -26,8 +26,8 @@
 #include "utils/rel.h"
 #include "utils/snapmgr.h"
 
-static bool toastrel_valueid_exists(Relation toastrel, Oid valueid);
-static bool toastid_valueid_exists(Oid toastrelid, Oid valueid);
+static bool toastrel_valueid_exists(Relation toastrel, uint64 valueid);
+static bool toastid_valueid_exists(Oid toastrelid, uint64 valueid);
 
 /* ----------
  * toast_compress_datum -
@@ -456,7 +456,7 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
  * ----------
  */
 static bool
-toastrel_valueid_exists(Relation toastrel, Oid valueid)
+toastrel_valueid_exists(Relation toastrel, uint64 valueid)
 {
 	bool		result = false;
 	ScanKeyData toastkey;
@@ -504,7 +504,7 @@ toastrel_valueid_exists(Relation toastrel, Oid valueid)
  * ----------
  */
 static bool
-toastid_valueid_exists(Oid toastrelid, Oid valueid)
+toastid_valueid_exists(Oid toastrelid, uint64 valueid)
 {
 	bool		result;
 	Relation	toastrel;
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index cb1e57030f64..76936b2f4944 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -623,7 +623,7 @@ toast_build_flattened_tuple(TupleDesc tupleDesc,
  * result is the varlena into which the results should be written.
  */
 void
-heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
+heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 					   int32 sliceoffset, int32 slicelength,
 					   struct varlena *result)
 {
@@ -725,7 +725,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
 		else
 		{
 			/* should never happen */
-			elog(ERROR, "found toasted toast chunk for toast value %u in %s",
+			elog(ERROR, "found toasted toast chunk for toast value %" PRIu64 " in %s",
 				 valueid, RelationGetRelationName(toastrel));
 			chunksize = 0;		/* keep compiler quiet */
 			chunkdata = NULL;
@@ -737,13 +737,13 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
 		if (curchunk != expectedchunk)
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
-					 errmsg_internal("unexpected chunk number %d (expected %d) for toast value %u in %s",
+					 errmsg_internal("unexpected chunk number %d (expected %d) for toast value %" PRIu64 " in %s",
 									 curchunk, expectedchunk, valueid,
 									 RelationGetRelationName(toastrel))));
 		if (curchunk > endchunk)
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
-					 errmsg_internal("unexpected chunk number %d (out of range %d..%d) for toast value %u in %s",
+					 errmsg_internal("unexpected chunk number %d (out of range %d..%d) for toast value %" PRIu64 " in %s",
 									 curchunk,
 									 startchunk, endchunk, valueid,
 									 RelationGetRelationName(toastrel))));
@@ -752,7 +752,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
 		if (chunksize != expected_size)
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
-					 errmsg_internal("unexpected chunk size %d (expected %d) in chunk %d of %d for toast value %u in %s",
+					 errmsg_internal("unexpected chunk size %d (expected %d) in chunk %d of %d for toast value %" PRIu64 " in %s",
 									 chunksize, expected_size,
 									 curchunk, totalchunks, valueid,
 									 RelationGetRelationName(toastrel))));
@@ -781,7 +781,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
 	if (expectedchunk != (endchunk + 1))
 		ereport(ERROR,
 				(errcode(ERRCODE_DATA_CORRUPTED),
-				 errmsg_internal("missing chunk number %d for toast value %u in %s",
+				 errmsg_internal("missing chunk number %d for toast value %" PRIu64 " in %s",
 								 expectedchunk, valueid,
 								 RelationGetRelationName(toastrel))));
 
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 5febd154b6ba..3a2c6649dbd4 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -176,7 +176,7 @@ typedef struct ReorderBufferIterTXNState
 /* toast datastructures */
 typedef struct ReorderBufferToastEnt
 {
-	Oid			chunk_id;		/* toast_table.chunk_id */
+	uint64		chunk_id;		/* toast_table.chunk_id */
 	int32		last_chunk_seq; /* toast_table.chunk_seq of the last chunk we
 								 * have seen */
 	Size		num_chunks;		/* number of chunks we've already seen */
@@ -4944,7 +4944,7 @@ ReorderBufferToastInitHash(ReorderBuffer *rb, ReorderBufferTXN *txn)
 
 	Assert(txn->toast_hash == NULL);
 
-	hash_ctl.keysize = sizeof(Oid);
+	hash_ctl.keysize = sizeof(uint64);
 	hash_ctl.entrysize = sizeof(ReorderBufferToastEnt);
 	hash_ctl.hcxt = rb->context;
 	txn->toast_hash = hash_create("ReorderBufferToastHash", 5, &hash_ctl,
@@ -4968,7 +4968,7 @@ ReorderBufferToastAppendChunk(ReorderBuffer *rb, ReorderBufferTXN *txn,
 	bool		isnull;
 	Pointer		chunk;
 	TupleDesc	desc = RelationGetDescr(relation);
-	Oid			chunk_id;
+	uint64		chunk_id;
 	int32		chunk_seq;
 
 	if (txn->toast_hash == NULL)
@@ -4995,11 +4995,11 @@ ReorderBufferToastAppendChunk(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		dlist_init(&ent->chunks);
 
 		if (chunk_seq != 0)
-			elog(ERROR, "got sequence entry %d for toast chunk %u instead of seq 0",
+			elog(ERROR, "got sequence entry %d for toast chunk %" PRIu64 " instead of seq 0",
 				 chunk_seq, chunk_id);
 	}
 	else if (found && chunk_seq != ent->last_chunk_seq + 1)
-		elog(ERROR, "got sequence entry %d for toast chunk %u instead of seq %d",
+		elog(ERROR, "got sequence entry %d for toast chunk %" PRIu64 " instead of seq %d",
 			 chunk_seq, chunk_id, ent->last_chunk_seq + 1);
 
 	chunk = DatumGetPointer(fastgetattr(newtup, 3, desc, &isnull));
@@ -5108,6 +5108,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		struct varlena *reconstructed;
 		dlist_iter	it;
 		Size		data_done = 0;
+		uint64		toast_valueid;
 
 		/* system columns aren't toasted */
 		if (attr->attnum < 0)
@@ -5132,13 +5133,14 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 			continue;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, varlena);
+		toast_valueid = toast_pointer.va_valueid;
 
 		/*
 		 * Check whether the toast tuple changed, replace if so.
 		 */
 		ent = (ReorderBufferToastEnt *)
 			hash_search(txn->toast_hash,
-						&toast_pointer.va_valueid,
+						&toast_valueid,
 						HASH_FIND,
 						NULL);
 		if (ent == NULL)
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 4963e9245cb5..3b2bdced4cdc 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1556,11 +1556,18 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 				  uint32 extsize)
 {
 	int32		chunk_seq;
-	int32		last_chunk_seq = (extsize - 1) / TOAST_MAX_CHUNK_SIZE;
+	int32		last_chunk_seq;
 	Pointer		chunk;
 	bool		isnull;
 	int32		chunksize;
 	int32		expected_size;
+	uint64		toast_valueid;
+	int32		max_chunk_size;
+
+	toast_valueid = ta->toast_pointer.va_valueid;
+
+	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	last_chunk_seq = (extsize - 1) / max_chunk_size;
 
 	/* Sanity-check the sequence number. */
 	chunk_seq = DatumGetInt32(fastgetattr(toasttup, 2,
@@ -1568,16 +1575,16 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	if (isnull)
 	{
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u has toast chunk with null sequence number",
-										 ta->toast_pointer.va_valueid));
+								psprintf("toast value %" PRIu64 " has toast chunk with null sequence number",
+										 toast_valueid));
 		return;
 	}
 	if (chunk_seq != *expected_chunk_seq)
 	{
 		/* Either the TOAST index is corrupt, or we don't have all chunks. */
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u index scan returned chunk %d when expecting chunk %d",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value %" PRIu64 " index scan returned chunk %d when expecting chunk %d",
+										 toast_valueid,
 										 chunk_seq, *expected_chunk_seq));
 	}
 	*expected_chunk_seq = chunk_seq + 1;
@@ -1588,8 +1595,8 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	if (isnull)
 	{
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u chunk %d has null data",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value %" PRIu64 " chunk %d has null data",
+										 toast_valueid,
 										 chunk_seq));
 		return;
 	}
@@ -1608,8 +1615,8 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 		uint32		header = ((varattrib_4b *) chunk)->va_4byte.va_header;
 
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u chunk %d has invalid varlena header %0x",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value %" PRIu64 " chunk %d has invalid varlena header %0x",
+										 toast_valueid,
 										 chunk_seq, header));
 		return;
 	}
@@ -1620,19 +1627,19 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	if (chunk_seq > last_chunk_seq)
 	{
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u chunk %d follows last expected chunk %d",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value %" PRIu64 " chunk %d follows last expected chunk %d",
+										 toast_valueid,
 										 chunk_seq, last_chunk_seq));
 		return;
 	}
 
-	expected_size = chunk_seq < last_chunk_seq ? TOAST_MAX_CHUNK_SIZE
-		: extsize - (last_chunk_seq * TOAST_MAX_CHUNK_SIZE);
+	expected_size = chunk_seq < last_chunk_seq ? max_chunk_size
+		: extsize - (last_chunk_seq * max_chunk_size);
 
 	if (chunksize != expected_size)
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u chunk %d has size %u, but expected size %u",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value %" PRIu64 " chunk %d has size %u, but expected size %u",
+										 toast_valueid,
 										 chunk_seq, chunksize, expected_size));
 }
 
@@ -1663,6 +1670,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	struct varlena *attr;
 	char	   *tp;				/* pointer to the tuple data */
 	uint16		infomask;
+	uint64		toast_pointer_valueid;
 	CompactAttribute *thisatt;
 	struct varatt_external toast_pointer;
 
@@ -1766,6 +1774,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 		return true;
 
 	/* It is external, and we're looking at a page on disk */
+	toast_pointer_valueid = toast_pointer.va_valueid;
 
 	/*
 	 * Must copy attr into toast_pointer for alignment considerations
@@ -1775,8 +1784,8 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	/* Toasted attributes too large to be untoasted should never be stored */
 	if (toast_pointer.va_rawsize > VARLENA_SIZE_LIMIT)
 		report_corruption(ctx,
-						  psprintf("toast value %u rawsize %d exceeds limit %d",
-								   toast_pointer.va_valueid,
+						  psprintf("toast value %" PRIu64 " rawsize %d exceeds limit %d",
+								   toast_pointer_valueid,
 								   toast_pointer.va_rawsize,
 								   VARLENA_SIZE_LIMIT));
 
@@ -1803,16 +1812,16 @@ check_tuple_attribute(HeapCheckContext *ctx)
 		}
 		if (!valid)
 			report_corruption(ctx,
-							  psprintf("toast value %u has invalid compression method id %d",
-									   toast_pointer.va_valueid, cmid));
+							  psprintf("toast value %" PRIu64 " has invalid compression method id %d",
+									   toast_pointer_valueid, cmid));
 	}
 
 	/* The tuple header better claim to contain toasted values */
 	if (!(infomask & HEAP_HASEXTERNAL))
 	{
 		report_corruption(ctx,
-						  psprintf("toast value %u is external but tuple header flag HEAP_HASEXTERNAL not set",
-								   toast_pointer.va_valueid));
+						  psprintf("toast value %" PRIu64 " is external but tuple header flag HEAP_HASEXTERNAL not set",
+								   toast_pointer_valueid));
 		return true;
 	}
 
@@ -1820,8 +1829,8 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	if (!ctx->rel->rd_rel->reltoastrelid)
 	{
 		report_corruption(ctx,
-						  psprintf("toast value %u is external but relation has no toast relation",
-								   toast_pointer.va_valueid));
+						  psprintf("toast value %" PRIu64 " is external but relation has no toast relation",
+								   toast_pointer_valueid));
 		return true;
 	}
 
@@ -1866,9 +1875,11 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	uint32		extsize;
 	int32		expected_chunk_seq = 0;
 	int32		last_chunk_seq;
+	uint64		toast_valueid;
+	int32		max_chunk_size = TOAST_MAX_CHUNK_SIZE;
 
 	extsize = VARATT_EXTERNAL_GET_EXTSIZE(ta->toast_pointer);
-	last_chunk_seq = (extsize - 1) / TOAST_MAX_CHUNK_SIZE;
+	last_chunk_seq = (extsize - 1) / max_chunk_size;
 
 	/*
 	 * Setup a scan key to find chunks in toast table with matching va_valueid
@@ -1896,14 +1907,16 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	}
 	systable_endscan_ordered(toastscan);
 
+	toast_valueid = ta->toast_pointer.va_valueid;
+
 	if (!found_toasttup)
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u not found in toast table",
-										 ta->toast_pointer.va_valueid));
+								psprintf("toast value %" PRIu64 " not found in toast table",
+										 toast_valueid));
 	else if (expected_chunk_seq <= last_chunk_seq)
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u was expected to end at chunk %d, but ended while expecting chunk %d",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value %" PRIu64 " was expected to end at chunk %d, but ended while expecting chunk %d",
+										 toast_valueid,
 										 last_chunk_seq, expected_chunk_seq));
 }
 
-- 
2.50.0

v3-0002-Minimize-footprint-of-TOAST_MAX_CHUNK_SIZE-in-hea.patchtext/x-diff; charset=us-asciiDownload
From 5e32e1d6d235c4b1a019852c2678e755f55d8dd6 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 19 Jun 2025 10:03:46 +0900
Subject: [PATCH v3 02/14] Minimize footprint of TOAST_MAX_CHUNK_SIZE in heap
 TOAST code

This eases a follow-up change to support 8-byte TOAST value IDs, as the
maximum chunk size allowed for a single chunk of TOASTed data depends on
the size of the value ID.
---
 src/backend/access/heap/heaptoast.c | 20 ++++++++++++--------
 1 file changed, 12 insertions(+), 8 deletions(-)

diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index 76936b2f4944..ae8d502ddcd3 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -634,11 +634,12 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 	SysScanDesc toastscan;
 	HeapTuple	ttup;
 	int32		expectedchunk;
-	int32		totalchunks = ((attrsize - 1) / TOAST_MAX_CHUNK_SIZE) + 1;
+	int32		totalchunks;
 	int			startchunk;
 	int			endchunk;
 	int			num_indexes;
 	int			validIndex;
+	int32		max_chunk_size;
 
 	/* Look for the valid index of toast relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -646,8 +647,11 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 									&toastidxs,
 									&num_indexes);
 
-	startchunk = sliceoffset / TOAST_MAX_CHUNK_SIZE;
-	endchunk = (sliceoffset + slicelength - 1) / TOAST_MAX_CHUNK_SIZE;
+	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+
+	totalchunks = ((attrsize - 1) / max_chunk_size) + 1;
+	startchunk = sliceoffset / max_chunk_size;
+	endchunk = (sliceoffset + slicelength - 1) / max_chunk_size;
 	Assert(endchunk <= totalchunks);
 
 	/* Set up a scan key to fetch from the index. */
@@ -747,8 +751,8 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 									 curchunk,
 									 startchunk, endchunk, valueid,
 									 RelationGetRelationName(toastrel))));
-		expected_size = curchunk < totalchunks - 1 ? TOAST_MAX_CHUNK_SIZE
-			: attrsize - ((totalchunks - 1) * TOAST_MAX_CHUNK_SIZE);
+		expected_size = curchunk < totalchunks - 1 ? max_chunk_size
+			: attrsize - ((totalchunks - 1) * max_chunk_size);
 		if (chunksize != expected_size)
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
@@ -763,12 +767,12 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 		chcpystrt = 0;
 		chcpyend = chunksize - 1;
 		if (curchunk == startchunk)
-			chcpystrt = sliceoffset % TOAST_MAX_CHUNK_SIZE;
+			chcpystrt = sliceoffset % max_chunk_size;
 		if (curchunk == endchunk)
-			chcpyend = (sliceoffset + slicelength - 1) % TOAST_MAX_CHUNK_SIZE;
+			chcpyend = (sliceoffset + slicelength - 1) % max_chunk_size;
 
 		memcpy(VARDATA(result) +
-			   (curchunk * TOAST_MAX_CHUNK_SIZE - sliceoffset) + chcpystrt,
+			   (curchunk * max_chunk_size - sliceoffset) + chcpystrt,
 			   chunkdata + chcpystrt,
 			   (chcpyend - chcpystrt) + 1);
 
-- 
2.50.0

v3-0003-varatt_external-varatt_external_oid-and-VARTAG_ON.patchtext/x-diff; charset=us-asciiDownload
From 3b1ba25bd0a0d20b77733439664d1862681d46c1 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 1 Aug 2025 16:15:56 +0900
Subject: [PATCH v3 03/14] varatt_external->varatt_external_oid and
 VARTAG_ONDISK->VARTAG_ONDISK_OID

This rename is in preparation of a follow-up commit that aims at adding
support for multiple types of external on-disk TOAST pointers, where the
OID type is only one subset of them.
---
 src/include/access/detoast.h                  |  4 ++--
 src/include/varatt.h                          | 20 +++++++++----------
 src/backend/access/common/detoast.c           | 10 +++++-----
 src/backend/access/common/toast_compression.c |  2 +-
 src/backend/access/common/toast_internals.c   |  8 ++++----
 .../replication/logical/reorderbuffer.c       |  2 +-
 src/backend/utils/adt/varlena.c               |  2 +-
 contrib/amcheck/verify_heapam.c               |  6 +++---
 8 files changed, 27 insertions(+), 27 deletions(-)

diff --git a/src/include/access/detoast.h b/src/include/access/detoast.h
index e603a2276c38..d80a62e64fd5 100644
--- a/src/include/access/detoast.h
+++ b/src/include/access/detoast.h
@@ -14,7 +14,7 @@
 
 /*
  * Macro to fetch the possibly-unaligned contents of an EXTERNAL datum
- * into a local "struct varatt_external" toast pointer.  This should be
+ * into a local "struct varatt_external_oid" toast pointer.  This should be
  * just a memcpy, but some versions of gcc seem to produce broken code
  * that assumes the datum contents are aligned.  Introducing an explicit
  * intermediate "varattrib_1b_e *" variable seems to fix it.
@@ -28,7 +28,7 @@ do { \
 } while (0)
 
 /* Size of an EXTERNAL datum that contains a standard TOAST pointer */
-#define TOAST_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external))
+#define TOAST_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_oid))
 
 /* Size of an EXTERNAL datum that contains an indirection pointer */
 #define INDIRECT_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_indirect))
diff --git a/src/include/varatt.h b/src/include/varatt.h
index 2e8564d49980..8cd6312df432 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -16,7 +16,7 @@
 #define VARATT_H
 
 /*
- * struct varatt_external is a traditional "TOAST pointer", that is, the
+ * struct varatt_external_oid is a traditional "TOAST pointer", that is, the
  * information needed to fetch a Datum stored out-of-line in a TOAST table.
  * The data is compressed if and only if the external size stored in
  * va_extinfo is less than va_rawsize - VARHDRSZ.
@@ -29,14 +29,14 @@
  * you can look at these fields!  (The reason we use memcmp is to avoid
  * having to do that just to detect equality of two TOAST pointers...)
  */
-typedef struct varatt_external
+typedef struct varatt_external_oid
 {
 	int32		va_rawsize;		/* Original data size (includes header) */
 	uint32		va_extinfo;		/* External saved size (without header) and
 								 * compression method */
 	Oid			va_valueid;		/* Unique ID of value within TOAST table */
 	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
-}			varatt_external;
+}			varatt_external_oid;
 
 /*
  * These macros define the "saved size" portion of va_extinfo.  Its remaining
@@ -51,7 +51,7 @@ typedef struct varatt_external
  * The creator of such a Datum is entirely responsible that the referenced
  * storage survives for as long as referencing pointer Datums can exist.
  *
- * Note that just as for struct varatt_external, this struct is stored
+ * Note that just as for struct varatt_external_oid, this struct is stored
  * unaligned within any containing tuple.
  */
 typedef struct varatt_indirect
@@ -66,7 +66,7 @@ typedef struct varatt_indirect
  * storage.  APIs for this, in particular the definition of struct
  * ExpandedObjectHeader, are in src/include/utils/expandeddatum.h.
  *
- * Note that just as for struct varatt_external, this struct is stored
+ * Note that just as for struct varatt_external_oid, this struct is stored
  * unaligned within any containing tuple.
  */
 typedef struct ExpandedObjectHeader ExpandedObjectHeader;
@@ -78,7 +78,7 @@ typedef struct varatt_expanded
 
 /*
  * Type tag for the various sorts of "TOAST pointer" datums.  The peculiar
- * value for VARTAG_ONDISK comes from a requirement for on-disk compatibility
+ * value for VARTAG_ONDISK_OID comes from a requirement for on-disk compatibility
  * with a previous notion that the tag field was the pointer datum's length.
  */
 typedef enum vartag_external
@@ -86,7 +86,7 @@ typedef enum vartag_external
 	VARTAG_INDIRECT = 1,
 	VARTAG_EXPANDED_RO = 2,
 	VARTAG_EXPANDED_RW = 3,
-	VARTAG_ONDISK = 18
+	VARTAG_ONDISK_OID = 18
 } vartag_external;
 
 /* this test relies on the specific tag values above */
@@ -96,7 +96,7 @@ typedef enum vartag_external
 #define VARTAG_SIZE(tag) \
 	((tag) == VARTAG_INDIRECT ? sizeof(varatt_indirect) : \
 	 VARTAG_IS_EXPANDED(tag) ? sizeof(varatt_expanded) : \
-	 (tag) == VARTAG_ONDISK ? sizeof(varatt_external) : \
+	 (tag) == VARTAG_ONDISK_OID ? sizeof(varatt_external_oid) : \
 	 (AssertMacro(false), 0))
 
 /*
@@ -288,7 +288,7 @@ typedef struct
 #define VARATT_IS_COMPRESSED(PTR)			VARATT_IS_4B_C(PTR)
 #define VARATT_IS_EXTERNAL(PTR)				VARATT_IS_1B_E(PTR)
 #define VARATT_IS_EXTERNAL_ONDISK(PTR) \
-	(VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK)
+	(VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID)
 #define VARATT_IS_EXTERNAL_INDIRECT(PTR) \
 	(VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_INDIRECT)
 #define VARATT_IS_EXTERNAL_EXPANDED_RO(PTR) \
@@ -330,7 +330,7 @@ typedef struct
 #define VARDATA_COMPRESSED_GET_COMPRESS_METHOD(PTR) \
 	(((varattrib_4b *) (PTR))->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS)
 
-/* Same for external Datums; but note argument is a struct varatt_external */
+/* Same for external Datums; but note argument is a struct varatt_external_oid */
 #define VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) \
 	((toast_pointer).va_extinfo & VARLENA_EXTSIZE_MASK)
 #define VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) \
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 626517877422..04eedb474c74 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -225,7 +225,7 @@ detoast_attr_slice(struct varlena *attr,
 
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
-		struct varatt_external toast_pointer;
+		struct varatt_external_oid toast_pointer;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 
@@ -344,7 +344,7 @@ toast_fetch_datum(struct varlena *attr)
 {
 	Relation	toastrel;
 	struct varlena *result;
-	struct varatt_external toast_pointer;
+	struct varatt_external_oid toast_pointer;
 	int32		attrsize;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
@@ -398,7 +398,7 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 {
 	Relation	toastrel;
 	struct varlena *result;
-	struct varatt_external toast_pointer;
+	struct varatt_external_oid toast_pointer;
 	int32		attrsize;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
@@ -550,7 +550,7 @@ toast_raw_datum_size(Datum value)
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
 		/* va_rawsize is the size of the original datum -- including header */
-		struct varatt_external toast_pointer;
+		struct varatt_external_oid toast_pointer;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 		result = toast_pointer.va_rawsize;
@@ -610,7 +610,7 @@ toast_datum_size(Datum value)
 		 * compressed or not.  We do not count the size of the toast pointer
 		 * ... should we?
 		 */
-		struct varatt_external toast_pointer;
+		struct varatt_external_oid toast_pointer;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 		result = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c
index 926f1e4008ab..26aad84d367a 100644
--- a/src/backend/access/common/toast_compression.c
+++ b/src/backend/access/common/toast_compression.c
@@ -262,7 +262,7 @@ toast_get_compression_id(struct varlena *attr)
 	 */
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
-		struct varatt_external toast_pointer;
+		struct varatt_external_oid toast_pointer;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 4a1342da6e1b..0fbefd412783 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -127,7 +127,7 @@ toast_save_datum(Relation rel, Datum value,
 	bool		t_isnull[3];
 	CommandId	mycid = GetCurrentCommandId(true);
 	struct varlena *result;
-	struct varatt_external toast_pointer;
+	struct varatt_external_oid toast_pointer;
 	union
 	{
 		struct varlena hdr;
@@ -237,7 +237,7 @@ toast_save_datum(Relation rel, Datum value,
 		toast_pointer.va_valueid = InvalidOid;
 		if (oldexternal != NULL)
 		{
-			struct varatt_external old_toast_pointer;
+			struct varatt_external_oid old_toast_pointer;
 
 			Assert(VARATT_IS_EXTERNAL_ONDISK(oldexternal));
 			/* Must copy to access aligned fields */
@@ -369,7 +369,7 @@ toast_save_datum(Relation rel, Datum value,
 	 * Create the TOAST pointer value that we'll return
 	 */
 	result = (struct varlena *) palloc(TOAST_POINTER_SIZE);
-	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK);
+	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_OID);
 	memcpy(VARDATA_EXTERNAL(result), &toast_pointer, sizeof(toast_pointer));
 
 	return PointerGetDatum(result);
@@ -385,7 +385,7 @@ void
 toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 {
 	struct varlena *attr = (struct varlena *) DatumGetPointer(value);
-	struct varatt_external toast_pointer;
+	struct varatt_external_oid toast_pointer;
 	Relation	toastrel;
 	Relation   *toastidxs;
 	ScanKeyData toastkey;
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 3a2c6649dbd4..3542696f0d99 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -5102,7 +5102,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		struct varlena *varlena;
 
 		/* va_rawsize is the size of the original datum -- including header */
-		struct varatt_external toast_pointer;
+		struct varatt_external_oid toast_pointer;
 		struct varatt_indirect redirect_pointer;
 		struct varlena *new_datum = NULL;
 		struct varlena *reconstructed;
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index ffae8c23abfa..38d4e6d45f28 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -4219,7 +4219,7 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 {
 	int			typlen;
 	struct varlena *attr;
-	struct varatt_external toast_pointer;
+	struct varatt_external_oid toast_pointer;
 
 	/* On first call, get the input type's typlen, and save at *fn_extra */
 	if (fcinfo->flinfo->fn_extra == NULL)
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 3b2bdced4cdc..030f9fb64b51 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -73,7 +73,7 @@ typedef enum SkipPages
  */
 typedef struct ToastedAttribute
 {
-	struct varatt_external toast_pointer;
+	struct varatt_external_oid toast_pointer;
 	BlockNumber blkno;			/* block in main table */
 	OffsetNumber offnum;		/* offset in main table */
 	AttrNumber	attnum;			/* attribute in main table */
@@ -1672,7 +1672,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	uint16		infomask;
 	uint64		toast_pointer_valueid;
 	CompactAttribute *thisatt;
-	struct varatt_external toast_pointer;
+	struct varatt_external_oid toast_pointer;
 
 	infomask = ctx->tuphdr->t_infomask;
 	thisatt = TupleDescCompactAttr(RelationGetDescr(ctx->rel), ctx->attnum);
@@ -1731,7 +1731,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	{
 		uint8		va_tag = VARTAG_EXTERNAL(tp + ctx->offset);
 
-		if (va_tag != VARTAG_ONDISK)
+		if (va_tag != VARTAG_ONDISK_OID)
 		{
 			report_corruption(ctx,
 							  psprintf("toasted attribute has unexpected TOAST tag %u",
-- 
2.50.0

v3-0004-Refactor-external-TOAST-pointer-code-for-better-p.patchtext/x-diff; charset=us-asciiDownload
From 8b179d9a443f033136ea2b6dee2ae2474ceb0c05 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 1 Aug 2025 16:22:13 +0900
Subject: [PATCH v3 04/14] Refactor external TOAST pointer code for better
 pluggability

This commit introduces a new interface for external TOAST pointers,
which is able to make a translation of the varlena pointers stored on
disk to/from an new in-memory structure called toast_external.  The
types of varatt_external supported on disk need to be registered into a
new subsystem in a new file, called toast_external.[c|h], then define a
set of callbacks to allow the toasting and detoasting code to use it.

A follow-up change will rely on this refactoring to introduce new
vartag_external values with an associated varatt_external_* that is
able, which would be used in int8 TOAST tables.
---
 src/include/access/detoast.h                  |  12 +-
 src/include/access/heaptoast.h                |   5 +-
 src/include/access/toast_external.h           | 170 ++++++++++++++++++
 src/include/access/toast_helper.h             |   1 +
 src/include/varatt.h                          |  27 ++-
 src/backend/access/common/Makefile            |   1 +
 src/backend/access/common/detoast.c           |  57 +++---
 src/backend/access/common/meson.build         |   1 +
 src/backend/access/common/toast_compression.c |  10 +-
 src/backend/access/common/toast_external.c    | 156 ++++++++++++++++
 src/backend/access/common/toast_internals.c   |  84 +++++----
 src/backend/access/heap/heaptoast.c           |  20 ++-
 src/backend/access/table/toast_helper.c       |  12 +-
 .../replication/logical/reorderbuffer.c       |  13 +-
 src/backend/utils/adt/varlena.c               |   7 +-
 doc/src/sgml/storage.sgml                     |   2 +-
 contrib/amcheck/verify_heapam.c               |  35 ++--
 src/tools/pgindent/typedefs.list              |   2 +
 18 files changed, 502 insertions(+), 113 deletions(-)
 create mode 100644 src/include/access/toast_external.h
 create mode 100644 src/backend/access/common/toast_external.c

diff --git a/src/include/access/detoast.h b/src/include/access/detoast.h
index d80a62e64fd5..4195f7b5bdfd 100644
--- a/src/include/access/detoast.h
+++ b/src/include/access/detoast.h
@@ -14,10 +14,11 @@
 
 /*
  * Macro to fetch the possibly-unaligned contents of an EXTERNAL datum
- * into a local "struct varatt_external_oid" toast pointer.  This should be
- * just a memcpy, but some versions of gcc seem to produce broken code
- * that assumes the datum contents are aligned.  Introducing an explicit
- * intermediate "varattrib_1b_e *" variable seems to fix it.
+ * into a local "struct varatt_external_*" toast pointer, as supported
+ * in toast_external.c and varatt.h.  This should be just a memcpy, but
+ * some versions of gcc seem to produce broken code that assumes the datum
+ * contents are aligned.  Introducing an explicit intermediate
+ * "varattrib_1b_e *" variable seems to fix it.
  */
 #define VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr) \
 do { \
@@ -27,9 +28,6 @@ do { \
 	memcpy(&(toast_pointer), VARDATA_EXTERNAL(attre), sizeof(toast_pointer)); \
 } while (0)
 
-/* Size of an EXTERNAL datum that contains a standard TOAST pointer */
-#define TOAST_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_oid))
-
 /* Size of an EXTERNAL datum that contains an indirection pointer */
 #define INDIRECT_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_indirect))
 
diff --git a/src/include/access/heaptoast.h b/src/include/access/heaptoast.h
index 6e3558cbd6d2..673e96f5488c 100644
--- a/src/include/access/heaptoast.h
+++ b/src/include/access/heaptoast.h
@@ -81,13 +81,16 @@
 
 #define EXTERN_TUPLE_MAX_SIZE	MaximumBytesPerTuple(EXTERN_TUPLES_PER_PAGE)
 
-#define TOAST_MAX_CHUNK_SIZE	\
+#define TOAST_MAX_CHUNK_SIZE_OID	\
 	(EXTERN_TUPLE_MAX_SIZE -							\
 	 MAXALIGN(SizeofHeapTupleHeader) -					\
 	 sizeof(Oid) -										\
 	 sizeof(int32) -									\
 	 VARHDRSZ)
 
+/* Maximum size of chunk possible */
+#define TOAST_MAX_CHUNK_SIZE	TOAST_MAX_CHUNK_SIZE_OID
+
 /* ----------
  * heap_toast_insert_or_update -
  *
diff --git a/src/include/access/toast_external.h b/src/include/access/toast_external.h
new file mode 100644
index 000000000000..849cb1779b67
--- /dev/null
+++ b/src/include/access/toast_external.h
@@ -0,0 +1,170 @@
+/*-------------------------------------------------------------------------
+ *
+ * toast_external.h
+ *	  Support for on-disk external TOAST pointers
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1995, Regents of the University of California
+ *
+ * src/include/access/toast_external.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef TOAST_EXTERNAL_H
+#define TOAST_EXTERNAL_H
+
+#include "access/toast_compression.h"
+#include "varatt.h"
+
+/*
+ * Intermediate in-memory structure used when creating on-disk
+ * varatt_external_* or when deserializing varlena contents.
+ */
+typedef struct toast_external_data
+{
+	/* Original data size (includes header) */
+	int32       rawsize;
+
+	/* External saved size (without header) */
+	uint32      extsize;
+
+	/* compression method */
+	ToastCompressionId compression_method;
+
+	/* Relation OID of TOAST table containing the value */
+	Oid			toastrelid;
+
+	/*
+	 * Unique ID of value within TOAST table.  This could be an OID or an
+	 * int8 value.  This field is large enough to be able to store any of
+	 * them.
+	 */
+	uint64		value;
+} toast_external_data;
+
+/*
+ * Metadata for external TOAST pointer kinds, separated based on their
+ * vartag_external.
+ */
+typedef struct toast_external_info
+{
+	/*
+	 * Maximum chunk of data authorized for this type of external TOAST
+	 * pointer, when dividing an entry by chunks.  Sized depending on
+	 * the size of its varatt_external_* structure.
+	 */
+	int32		maximum_chunk_size;
+
+	/*
+	 * Size of an external TOAST pointer of this type, typically
+	 * (VARHDRSZ_EXTERNAL + sizeof(varatt_external_struct)).
+	 */
+	int32		toast_pointer_size;
+
+	/*
+	 * Map an input varlena to a toast_external_data, for consumption
+	 * in the backend code.  "data" is an input/output result.
+	 */
+	void		(*to_external_data) (struct varlena *attr,
+									 toast_external_data *data);
+
+	/*
+	 * Create a varlena that will be used on-disk for the given TOAST
+	 * type, based on the given input data.
+	 *
+	 * The result is the varlena created, for on-disk insertion.
+	 */
+	struct varlena  *(*create_external_data) (toast_external_data data);
+
+} toast_external_info;
+
+/* Retrieve a toast_external_info from a vartag */
+extern const toast_external_info *toast_external_get_info(uint8 tag);
+
+/* Retrieve toast_pointer_size using a TOAST attribute type */
+extern int32 toast_external_info_get_pointer_size(uint8 tag);
+
+/* Retrieve the vartag to assign to a TOAST typle */
+extern uint8 toast_external_assign_vartag(Oid toastrelid, uint64 value);
+
+/*
+ * Testing whether an externally-stored value is compressed now requires
+ * comparing size stored in extsize (the actual length of the external data)
+ * to rawsize (the original uncompressed datum's size).  The latter includes
+ * VARHDRSZ overhead, the former doesn't.  We never use compression unless it
+ * actually saves space, so we expect either equality or less-than.
+ */
+#define TOAST_EXTERNAL_IS_COMPRESSED(data) \
+	((data).extsize < (data).rawsize - VARHDRSZ)
+
+/* Full data structure */
+static inline void
+toast_external_info_get_data(struct varlena *attr, toast_external_data *data)
+{
+	uint8 tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+
+	info->to_external_data(attr, data);
+}
+
+/*
+ * Helper routines to recover specific fields in toast_external_data.  Most
+ * code paths doing work with on-disk external TOAST pointers care about
+ * these.
+ */
+
+/* Detoasted "raw" size */
+static inline Size
+toast_external_info_get_rawsize(struct varlena *attr)
+{
+	uint8 tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+	toast_external_data data;
+
+	info->to_external_data(attr, &data);
+
+	return data.rawsize;
+}
+
+/* External saved size */
+static inline Size
+toast_external_info_get_extsize(struct varlena *attr)
+{
+	uint8 tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+	toast_external_data data;
+
+	info->to_external_data(attr, &data);
+
+	return data.extsize;
+}
+
+/* Compression method ID */
+static inline ToastCompressionId
+toast_external_info_get_compression_method(struct varlena *attr)
+{
+	uint8 tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+	toast_external_data data;
+
+	info->to_external_data(attr, &data);
+
+	return data.compression_method;
+}
+
+/* Value ID */
+static inline Size
+toast_external_info_get_value(struct varlena *attr)
+{
+	uint8 tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+	toast_external_data data;
+
+	info->to_external_data(attr, &data);
+
+	return data.value;
+}
+
+#endif			/* TOAST_EXTERNAL_H */
diff --git a/src/include/access/toast_helper.h b/src/include/access/toast_helper.h
index e6ab8afffb67..729c593afebd 100644
--- a/src/include/access/toast_helper.h
+++ b/src/include/access/toast_helper.h
@@ -47,6 +47,7 @@ typedef struct
 	 * should be NULL in the case of an insert.
 	 */
 	Relation	ttc_rel;		/* the relation that contains the tuple */
+	int32		ttc_toast_pointer_size;	/* size of external TOAST pointer */
 	Datum	   *ttc_values;		/* values from the tuple columns */
 	bool	   *ttc_isnull;		/* null flags for the tuple columns */
 	Datum	   *ttc_oldvalues;	/* values from previous tuple */
diff --git a/src/include/varatt.h b/src/include/varatt.h
index 8cd6312df432..126e5e112a17 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -21,6 +21,9 @@
  * The data is compressed if and only if the external size stored in
  * va_extinfo is less than va_rawsize - VARHDRSZ.
  *
+ * The value ID is an OID, used for TOAST relations with OID as attribute
+ * for chunk_id.
+ *
  * This struct must not contain any padding, because we sometimes compare
  * these pointers using memcmp.
  *
@@ -51,7 +54,7 @@ typedef struct varatt_external_oid
  * The creator of such a Datum is entirely responsible that the referenced
  * storage survives for as long as referencing pointer Datums can exist.
  *
- * Note that just as for struct varatt_external_oid, this struct is stored
+ * Note that just as for struct varatt_external_*, this struct is stored
  * unaligned within any containing tuple.
  */
 typedef struct varatt_indirect
@@ -66,7 +69,7 @@ typedef struct varatt_indirect
  * storage.  APIs for this, in particular the definition of struct
  * ExpandedObjectHeader, are in src/include/utils/expandeddatum.h.
  *
- * Note that just as for struct varatt_external_oid, this struct is stored
+ * Note that just as for struct varatt_external_*, this struct is stored
  * unaligned within any containing tuple.
  */
 typedef struct ExpandedObjectHeader ExpandedObjectHeader;
@@ -78,8 +81,9 @@ typedef struct varatt_expanded
 
 /*
  * Type tag for the various sorts of "TOAST pointer" datums.  The peculiar
- * value for VARTAG_ONDISK_OID comes from a requirement for on-disk compatibility
- * with a previous notion that the tag field was the pointer datum's length.
+ * value for VARTAG_ONDISK_OID comes from a requirement for on-disk
+ * compatibility with a previous notion that the tag field was the pointer
+ * datum's length.
  */
 typedef enum vartag_external
 {
@@ -287,8 +291,10 @@ typedef struct
 
 #define VARATT_IS_COMPRESSED(PTR)			VARATT_IS_4B_C(PTR)
 #define VARATT_IS_EXTERNAL(PTR)				VARATT_IS_1B_E(PTR)
-#define VARATT_IS_EXTERNAL_ONDISK(PTR) \
+#define VARATT_IS_EXTERNAL_ONDISK_OID(PTR) \
 	(VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID)
+#define VARATT_IS_EXTERNAL_ONDISK(PTR) \
+	(VARATT_IS_EXTERNAL_ONDISK_OID(PTR))
 #define VARATT_IS_EXTERNAL_INDIRECT(PTR) \
 	(VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_INDIRECT)
 #define VARATT_IS_EXTERNAL_EXPANDED_RO(PTR) \
@@ -330,7 +336,10 @@ typedef struct
 #define VARDATA_COMPRESSED_GET_COMPRESS_METHOD(PTR) \
 	(((varattrib_4b *) (PTR))->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS)
 
-/* Same for external Datums; but note argument is a struct varatt_external_oid */
+/*
+ * Same for external Datums; but note argument is a struct
+ * varatt_external_oid.
+ */
 #define VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) \
 	((toast_pointer).va_extinfo & VARLENA_EXTSIZE_MASK)
 #define VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) \
@@ -351,8 +360,10 @@ typedef struct
  * VARHDRSZ overhead, the former doesn't.  We never use compression unless it
  * actually saves space, so we expect either equality or less-than.
  */
+
 #define VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer) \
-	(VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) < \
-	 (toast_pointer).va_rawsize - VARHDRSZ)
+ (VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) < \
+  (toast_pointer).va_rawsize - VARHDRSZ)
+
 
 #endif
diff --git a/src/backend/access/common/Makefile b/src/backend/access/common/Makefile
index e78de312659e..1ef86a245886 100644
--- a/src/backend/access/common/Makefile
+++ b/src/backend/access/common/Makefile
@@ -27,6 +27,7 @@ OBJS = \
 	syncscan.o \
 	tidstore.o \
 	toast_compression.o \
+	toast_external.o \
 	toast_internals.o \
 	tupconvert.o \
 	tupdesc.o
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 04eedb474c74..6a9b5200203c 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -16,6 +16,7 @@
 #include "access/detoast.h"
 #include "access/table.h"
 #include "access/tableam.h"
+#include "access/toast_external.h"
 #include "access/toast_internals.h"
 #include "common/int.h"
 #include "common/pg_lzcompress.h"
@@ -225,12 +226,12 @@ detoast_attr_slice(struct varlena *attr,
 
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
-		struct varatt_external_oid toast_pointer;
+		struct toast_external_data toast_pointer;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		toast_external_info_get_data(attr, &toast_pointer);
 
 		/* fast path for non-compressed external datums */
-		if (!VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+		if (!TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 			return toast_fetch_datum_slice(attr, sliceoffset, slicelength);
 
 		/*
@@ -240,7 +241,7 @@ detoast_attr_slice(struct varlena *attr,
 		 */
 		if (slicelimit >= 0)
 		{
-			int32		max_size = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+			int32		max_size = toast_pointer.extsize;
 
 			/*
 			 * Determine maximum amount of compressed data needed for a prefix
@@ -251,8 +252,7 @@ detoast_attr_slice(struct varlena *attr,
 			 * determine how much compressed data we need to be sure of being
 			 * able to decompress the required slice.
 			 */
-			if (VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) ==
-				TOAST_PGLZ_COMPRESSION_ID)
+			if (toast_pointer.compression_method == TOAST_PGLZ_COMPRESSION_ID)
 				max_size = pglz_maximum_compressed_size(slicelimit, max_size);
 
 			/*
@@ -344,20 +344,21 @@ toast_fetch_datum(struct varlena *attr)
 {
 	Relation	toastrel;
 	struct varlena *result;
-	struct varatt_external_oid toast_pointer;
+	struct toast_external_data toast_pointer;
 	int32		attrsize;
+	uint64		valueid;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		elog(ERROR, "toast_fetch_datum shouldn't be called for non-ondisk datums");
 
 	/* Must copy to access aligned fields */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_external_info_get_data(attr, &toast_pointer);
 
-	attrsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+	attrsize = toast_pointer.extsize;
 
 	result = (struct varlena *) palloc(attrsize + VARHDRSZ);
 
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+	if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 		SET_VARSIZE_COMPRESSED(result, attrsize + VARHDRSZ);
 	else
 		SET_VARSIZE(result, attrsize + VARHDRSZ);
@@ -365,14 +366,15 @@ toast_fetch_datum(struct varlena *attr)
 	if (attrsize == 0)
 		return result;			/* Probably shouldn't happen, but just in
 								 * case. */
+	valueid = toast_pointer.value;
 
 	/*
 	 * Open the toast relation and its indexes
 	 */
-	toastrel = table_open(toast_pointer.va_toastrelid, AccessShareLock);
+	toastrel = table_open(toast_pointer.toastrelid, AccessShareLock);
 
 	/* Fetch all chunks */
-	table_relation_fetch_toast_slice(toastrel, toast_pointer.va_valueid,
+	table_relation_fetch_toast_slice(toastrel, valueid,
 									 attrsize, 0, attrsize, result);
 
 	/* Close toast table */
@@ -398,23 +400,26 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 {
 	Relation	toastrel;
 	struct varlena *result;
-	struct varatt_external_oid toast_pointer;
+	struct toast_external_data toast_pointer;
 	int32		attrsize;
+	uint64		valueid;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		elog(ERROR, "toast_fetch_datum_slice shouldn't be called for non-ondisk datums");
 
 	/* Must copy to access aligned fields */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_external_info_get_data(attr, &toast_pointer);
+
+	valueid = toast_pointer.value;
 
 	/*
 	 * It's nonsense to fetch slices of a compressed datum unless when it's a
 	 * prefix -- this isn't lo_* we can't return a compressed datum which is
 	 * meaningful to toast later.
 	 */
-	Assert(!VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer) || 0 == sliceoffset);
+	Assert(!TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer) || 0 == sliceoffset);
 
-	attrsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+	attrsize = toast_pointer.extsize;
 
 	if (sliceoffset >= attrsize)
 	{
@@ -427,7 +432,7 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 	 * space required by va_tcinfo, which is stored at the beginning as an
 	 * int32 value.
 	 */
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer) && slicelength > 0)
+	if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer) && slicelength > 0)
 		slicelength = slicelength + sizeof(int32);
 
 	/*
@@ -440,7 +445,7 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 
 	result = (struct varlena *) palloc(slicelength + VARHDRSZ);
 
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+	if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 		SET_VARSIZE_COMPRESSED(result, slicelength + VARHDRSZ);
 	else
 		SET_VARSIZE(result, slicelength + VARHDRSZ);
@@ -449,10 +454,11 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 		return result;			/* Can save a lot of work at this point! */
 
 	/* Open the toast relation */
-	toastrel = table_open(toast_pointer.va_toastrelid, AccessShareLock);
+	toastrel = table_open(toast_pointer.toastrelid, AccessShareLock);
 
 	/* Fetch all chunks */
-	table_relation_fetch_toast_slice(toastrel, toast_pointer.va_valueid,
+	table_relation_fetch_toast_slice(toastrel,
+									 valueid,
 									 attrsize, sliceoffset, slicelength,
 									 result);
 
@@ -549,11 +555,7 @@ toast_raw_datum_size(Datum value)
 
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
-		/* va_rawsize is the size of the original datum -- including header */
-		struct varatt_external_oid toast_pointer;
-
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-		result = toast_pointer.va_rawsize;
+		result = toast_external_info_get_rawsize(attr);
 	}
 	else if (VARATT_IS_EXTERNAL_INDIRECT(attr))
 	{
@@ -610,10 +612,7 @@ toast_datum_size(Datum value)
 		 * compressed or not.  We do not count the size of the toast pointer
 		 * ... should we?
 		 */
-		struct varatt_external_oid toast_pointer;
-
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-		result = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+		result = toast_external_info_get_extsize(attr);
 	}
 	else if (VARATT_IS_EXTERNAL_INDIRECT(attr))
 	{
diff --git a/src/backend/access/common/meson.build b/src/backend/access/common/meson.build
index e3cdbe7a22e1..c20f2e88921e 100644
--- a/src/backend/access/common/meson.build
+++ b/src/backend/access/common/meson.build
@@ -15,6 +15,7 @@ backend_sources += files(
   'syncscan.c',
   'tidstore.c',
   'toast_compression.c',
+  'toast_external.c',
   'toast_internals.c',
   'tupconvert.c',
   'tupdesc.c',
diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c
index 26aad84d367a..94606a58c8fb 100644
--- a/src/backend/access/common/toast_compression.c
+++ b/src/backend/access/common/toast_compression.c
@@ -19,6 +19,7 @@
 
 #include "access/detoast.h"
 #include "access/toast_compression.h"
+#include "access/toast_external.h"
 #include "common/pg_lzcompress.h"
 #include "varatt.h"
 
@@ -261,14 +262,7 @@ toast_get_compression_id(struct varlena *attr)
 	 * toast compression header.
 	 */
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
-	{
-		struct varatt_external_oid toast_pointer;
-
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-
-		if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
-			cmid = VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer);
-	}
+		cmid = toast_external_info_get_compression_method(attr);
 	else if (VARATT_IS_COMPRESSED(attr))
 		cmid = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(attr);
 
diff --git a/src/backend/access/common/toast_external.c b/src/backend/access/common/toast_external.c
new file mode 100644
index 000000000000..5bfb6a6d434f
--- /dev/null
+++ b/src/backend/access/common/toast_external.c
@@ -0,0 +1,156 @@
+/*-------------------------------------------------------------------------
+ *
+ * toast_external.c
+ *	  Functions for the support of external on-disk TOAST pointers.
+ *
+ * Copyright (c) 2025, PostgreSQL Global Development Group
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/common/toast_external.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/detoast.h"
+#include "access/heaptoast.h"
+#include "access/toast_external.h"
+
+/* Callbacks for VARTAG_ONDISK_OID */
+static void ondisk_oid_to_external_data(struct varlena *attr,
+										toast_external_data *data);
+static struct varlena *ondisk_oid_create_external_data(toast_external_data data);
+
+
+/*
+ * Size of an EXTERNAL datum that contains a standard TOAST pointer (OID
+ * value).
+ */
+#define TOAST_POINTER_OID_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_oid))
+
+/*
+ * For now there are only two types, all defined in this file.  For now this
+ * is the maximum value of vartag_external, which is a historical choice.
+ */
+#define TOAST_EXTERNAL_INFO_SIZE	(VARTAG_ONDISK_OID + 1)
+
+/*
+ * The different kinds of on-disk external TOAST pointers, divided by
+ * vartag_external.
+ *
+ * See comments for struct toast_external_info about the details of the
+ * individual fields.
+ */
+static const toast_external_info toast_external_infos[TOAST_EXTERNAL_INFO_SIZE] = {
+	[VARTAG_ONDISK_OID] = {
+		.toast_pointer_size = TOAST_POINTER_OID_SIZE,
+		.maximum_chunk_size = TOAST_MAX_CHUNK_SIZE_OID,
+		.to_external_data = ondisk_oid_to_external_data,
+		.create_external_data = ondisk_oid_create_external_data,
+	},
+};
+
+
+/* Get toast_external_info of the defined vartag_external */
+const toast_external_info *
+toast_external_get_info(uint8 tag)
+{
+	return &toast_external_infos[tag];
+}
+
+/*
+ * Get external TOAST pointer size based on the attribute type of a TOAST
+ * value.
+ */
+int32
+toast_external_info_get_pointer_size(uint8 tag)
+{
+	return toast_external_infos[tag].toast_pointer_size;
+}
+
+/*
+ * Assign the vartag_external of a TOAST tuple, based on the TOAST relation
+ * it uses and its value.
+ */
+uint8
+toast_external_assign_vartag(Oid toastrelid, uint64 value)
+{
+	/*
+	 * If dealing with a code path where a TOAST relation may not be
+	 * assigned, like heap_toast_insert_or_update(), just use the legacy
+	 * vartag_external.
+	 */
+	if (!OidIsValid(toastrelid))
+		return VARTAG_ONDISK_OID;
+
+	/*
+	 * Currently there is only one type of vartag_external supported:
+	 * 4-byte value with OID for the chunk_id type.  This routine will
+	 * be extended to be able to use multiple vartag_external within
+	 * a single TOAST relation type, that may change depending on the
+	 * value used.
+	 */
+	return VARTAG_ONDISK_OID;
+}
+
+/*
+ * Helper routines able to translate the various varatt_external_* from/to
+ * the in-memory representation toast_external_data used in the backend.
+ */
+
+/* Callbacks for VARTAG_ONDISK_OID */
+static void
+ondisk_oid_to_external_data(struct varlena *attr, toast_external_data *data)
+{
+	varatt_external_oid		external;
+
+	VARATT_EXTERNAL_GET_POINTER(external, attr);
+	data->rawsize = external.va_rawsize;
+
+	/*
+	 * External size and compression methods are stored in the same field,
+	 * extract.
+	 */
+	if (VARATT_EXTERNAL_IS_COMPRESSED(external))
+	{
+		data->extsize = VARATT_EXTERNAL_GET_EXTSIZE(external);
+		data->compression_method = VARATT_EXTERNAL_GET_COMPRESS_METHOD(external);
+	}
+	else
+	{
+		data->extsize = external.va_extinfo;
+		data->compression_method = TOAST_INVALID_COMPRESSION_ID;
+	}
+
+	data->value = (uint64) external.va_valueid;
+	data->toastrelid = external.va_toastrelid;
+}
+
+static struct varlena *
+ondisk_oid_create_external_data(toast_external_data data)
+{
+	struct varlena *result = NULL;
+	varatt_external_oid external;
+
+	external.va_rawsize = data.rawsize;
+
+	if (data.compression_method != TOAST_INVALID_COMPRESSION_ID)
+	{
+		/* Set size and compression method, in a single field. */
+		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(external,
+													 data.extsize,
+													 data.compression_method);
+	}
+	else
+		external.va_extinfo = data.extsize;
+
+	external.va_toastrelid = data.toastrelid;
+	external.va_valueid = (Oid) data.value;
+
+	result = (struct varlena *) palloc(TOAST_POINTER_OID_SIZE);
+	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_OID);
+	memcpy(VARDATA_EXTERNAL(result), &external, sizeof(external));
+
+	return result;
+}
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 0fbefd412783..80bda9034cdf 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -18,6 +18,7 @@
 #include "access/heapam.h"
 #include "access/heaptoast.h"
 #include "access/table.h"
+#include "access/toast_external.h"
 #include "access/toast_internals.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -127,7 +128,7 @@ toast_save_datum(Relation rel, Datum value,
 	bool		t_isnull[3];
 	CommandId	mycid = GetCurrentCommandId(true);
 	struct varlena *result;
-	struct varatt_external_oid toast_pointer;
+	struct toast_external_data toast_pointer;
 	union
 	{
 		struct varlena hdr;
@@ -143,6 +144,8 @@ toast_save_datum(Relation rel, Datum value,
 	Pointer		dval = DatumGetPointer(value);
 	int			num_indexes;
 	int			validIndex;
+	const toast_external_info *info;
+	uint8		tag = VARTAG_INDIRECT;	/* init value does not matter */
 
 	Assert(!VARATT_IS_EXTERNAL(value));
 
@@ -174,28 +177,41 @@ toast_save_datum(Relation rel, Datum value,
 	{
 		data_p = VARDATA_SHORT(dval);
 		data_todo = VARSIZE_SHORT(dval) - VARHDRSZ_SHORT;
-		toast_pointer.va_rawsize = data_todo + VARHDRSZ;	/* as if not short */
-		toast_pointer.va_extinfo = data_todo;
+		toast_pointer.rawsize = data_todo + VARHDRSZ;	/* as if not short */
+		toast_pointer.extsize = data_todo;
+
+		/*
+		 * Note: we set compression_method to be able to build a correct
+		 * on-disk TOAST pointer.
+		 */
+		toast_pointer.compression_method = TOAST_INVALID_COMPRESSION_ID;
 	}
 	else if (VARATT_IS_COMPRESSED(dval))
 	{
 		data_p = VARDATA(dval);
 		data_todo = VARSIZE(dval) - VARHDRSZ;
 		/* rawsize in a compressed datum is just the size of the payload */
-		toast_pointer.va_rawsize = VARDATA_COMPRESSED_GET_EXTSIZE(dval) + VARHDRSZ;
+		toast_pointer.rawsize = VARDATA_COMPRESSED_GET_EXTSIZE(dval) + VARHDRSZ;
 
 		/* set external size and compression method */
-		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, data_todo,
-													 VARDATA_COMPRESSED_GET_COMPRESS_METHOD(dval));
+		toast_pointer.extsize = data_todo;
+		toast_pointer.compression_method = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(dval);
+
 		/* Assert that the numbers look like it's compressed */
-		Assert(VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer));
+		Assert(TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer));
 	}
 	else
 	{
 		data_p = VARDATA(dval);
 		data_todo = VARSIZE(dval) - VARHDRSZ;
-		toast_pointer.va_rawsize = VARSIZE(dval);
-		toast_pointer.va_extinfo = data_todo;
+		toast_pointer.rawsize = VARSIZE(dval);
+		toast_pointer.extsize = data_todo;
+
+		/*
+		 * Note: we set compression_method to be able to build a correct
+		 * on-disk TOAST pointer.
+		 */
+		toast_pointer.compression_method = TOAST_INVALID_COMPRESSION_ID;
 	}
 
 	/*
@@ -207,9 +223,9 @@ toast_save_datum(Relation rel, Datum value,
 	 * if we have to substitute such an OID.
 	 */
 	if (OidIsValid(rel->rd_toastoid))
-		toast_pointer.va_toastrelid = rel->rd_toastoid;
+		toast_pointer.toastrelid = rel->rd_toastoid;
 	else
-		toast_pointer.va_toastrelid = RelationGetRelid(toastrel);
+		toast_pointer.toastrelid = RelationGetRelid(toastrel);
 
 	/*
 	 * Choose an OID to use as the value ID for this toast value.
@@ -226,7 +242,7 @@ toast_save_datum(Relation rel, Datum value,
 	if (!OidIsValid(rel->rd_toastoid))
 	{
 		/* normal case: just choose an unused OID */
-		toast_pointer.va_valueid =
+		toast_pointer.value =
 			GetNewOidWithIndex(toastrel,
 							   RelationGetRelid(toastidxs[validIndex]),
 							   (AttrNumber) 1);
@@ -234,18 +250,18 @@ toast_save_datum(Relation rel, Datum value,
 	else
 	{
 		/* rewrite case: check to see if value was in old toast table */
-		toast_pointer.va_valueid = InvalidOid;
+		toast_pointer.value = InvalidOid;
 		if (oldexternal != NULL)
 		{
-			struct varatt_external_oid old_toast_pointer;
+			struct toast_external_data old_toast_pointer;
 
 			Assert(VARATT_IS_EXTERNAL_ONDISK(oldexternal));
-			/* Must copy to access aligned fields */
-			VARATT_EXTERNAL_GET_POINTER(old_toast_pointer, oldexternal);
-			if (old_toast_pointer.va_toastrelid == rel->rd_toastoid)
+			toast_external_info_get_data(oldexternal, &old_toast_pointer);
+
+			if (old_toast_pointer.toastrelid == rel->rd_toastoid)
 			{
 				/* This value came from the old toast table; reuse its OID */
-				toast_pointer.va_valueid = old_toast_pointer.va_valueid;
+				toast_pointer.value = old_toast_pointer.value;
 
 				/*
 				 * There is a corner case here: the table rewrite might have
@@ -265,14 +281,14 @@ toast_save_datum(Relation rel, Datum value,
 				 * be reclaimed by VACUUM.
 				 */
 				if (toastrel_valueid_exists(toastrel,
-											toast_pointer.va_valueid))
+											toast_pointer.value))
 				{
 					/* Match, so short-circuit the data storage loop below */
 					data_todo = 0;
 				}
 			}
 		}
-		if (toast_pointer.va_valueid == InvalidOid)
+		if (toast_pointer.value == InvalidOid)
 		{
 			/*
 			 * new value; must choose an OID that doesn't conflict in either
@@ -280,24 +296,32 @@ toast_save_datum(Relation rel, Datum value,
 			 */
 			do
 			{
-				toast_pointer.va_valueid =
+				toast_pointer.value =
 					GetNewOidWithIndex(toastrel,
 									   RelationGetRelid(toastidxs[validIndex]),
 									   (AttrNumber) 1);
 			} while (toastid_valueid_exists(rel->rd_toastoid,
-											toast_pointer.va_valueid));
+											toast_pointer.value));
 		}
 	}
 
 	/*
 	 * Initialize constant parts of the tuple data
 	 */
-	t_values[0] = ObjectIdGetDatum(toast_pointer.va_valueid);
+	t_values[0] = ObjectIdGetDatum(toast_pointer.value);
 	t_values[2] = PointerGetDatum(&chunk_data);
 	t_isnull[0] = false;
 	t_isnull[1] = false;
 	t_isnull[2] = false;
 
+	/*
+	 * Retrieve the vartag that can be assigned for the new TOAST tuple.
+	 * This depends on the type of TOAST table and its assigned value.
+	 */
+	tag = toast_external_assign_vartag(toast_pointer.toastrelid,
+									   toast_pointer.value);
+	info = toast_external_get_info(tag);
+
 	/*
 	 * Split up the item into chunks
 	 */
@@ -310,7 +334,7 @@ toast_save_datum(Relation rel, Datum value,
 		/*
 		 * Calculate the size of this chunk
 		 */
-		chunk_size = Min(TOAST_MAX_CHUNK_SIZE, data_todo);
+		chunk_size = Min(info->maximum_chunk_size, data_todo);
 
 		/*
 		 * Build a tuple and store it
@@ -368,9 +392,7 @@ toast_save_datum(Relation rel, Datum value,
 	/*
 	 * Create the TOAST pointer value that we'll return
 	 */
-	result = (struct varlena *) palloc(TOAST_POINTER_SIZE);
-	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_OID);
-	memcpy(VARDATA_EXTERNAL(result), &toast_pointer, sizeof(toast_pointer));
+	result = info->create_external_data(toast_pointer);
 
 	return PointerGetDatum(result);
 }
@@ -385,7 +407,7 @@ void
 toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 {
 	struct varlena *attr = (struct varlena *) DatumGetPointer(value);
-	struct varatt_external_oid toast_pointer;
+	struct toast_external_data toast_pointer;
 	Relation	toastrel;
 	Relation   *toastidxs;
 	ScanKeyData toastkey;
@@ -398,12 +420,12 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 		return;
 
 	/* Must copy to access aligned fields */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_external_info_get_data(attr, &toast_pointer);
 
 	/*
 	 * Open the toast relation and its indexes
 	 */
-	toastrel = table_open(toast_pointer.va_toastrelid, RowExclusiveLock);
+	toastrel = table_open(toast_pointer.toastrelid, RowExclusiveLock);
 
 	/* Fetch valid relation used for process */
 	validIndex = toast_open_indexes(toastrel,
@@ -417,7 +439,7 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	ScanKeyInit(&toastkey,
 				(AttrNumber) 1,
 				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(toast_pointer.va_valueid));
+				ObjectIdGetDatum(toast_pointer.value));
 
 	/*
 	 * Find all the chunks.  (We don't actually care whether we see them in
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index ae8d502ddcd3..47cc49be2aba 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -28,6 +28,7 @@
 #include "access/genam.h"
 #include "access/heapam.h"
 #include "access/heaptoast.h"
+#include "access/toast_external.h"
 #include "access/toast_helper.h"
 #include "access/toast_internals.h"
 #include "utils/fmgroids.h"
@@ -109,6 +110,7 @@ heap_toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup,
 	Datum		toast_oldvalues[MaxHeapAttributeNumber];
 	ToastAttrInfo toast_attr[MaxHeapAttributeNumber];
 	ToastTupleContext ttc;
+	uint8		tag;
 
 	/*
 	 * Ignore the INSERT_SPECULATIVE option. Speculative insertions/super
@@ -140,6 +142,16 @@ heap_toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup,
 	 * Prepare for toasting
 	 * ----------
 	 */
+
+	/*
+	 * Retrieve the toast pointer size based on the type of external TOAST
+	 * pointer assumed to be used.
+	 */
+
+	/* The default value is invalid, to work as a default. */
+	tag = toast_external_assign_vartag(rel->rd_rel->reltoastrelid, InvalidOid);
+	ttc.ttc_toast_pointer_size = toast_external_info_get_pointer_size(tag);
+
 	ttc.ttc_rel = rel;
 	ttc.ttc_values = toast_values;
 	ttc.ttc_isnull = toast_isnull;
@@ -640,6 +652,8 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 	int			num_indexes;
 	int			validIndex;
 	int32		max_chunk_size;
+	const toast_external_info *info;
+	uint8		tag = VARTAG_INDIRECT;  /* init value does not matter */
 
 	/* Look for the valid index of toast relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -647,7 +661,11 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 									&toastidxs,
 									&num_indexes);
 
-	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	/* Grab the information for toast_external_data */
+	tag = toast_external_assign_vartag(RelationGetRelid(toastrel), valueid);
+	info = toast_external_get_info(tag);
+
+	max_chunk_size = info->maximum_chunk_size;
 
 	totalchunks = ((attrsize - 1) / max_chunk_size) + 1;
 	startchunk = sliceoffset / max_chunk_size;
diff --git a/src/backend/access/table/toast_helper.c b/src/backend/access/table/toast_helper.c
index b60fab0a4d29..a2b44e093d79 100644
--- a/src/backend/access/table/toast_helper.c
+++ b/src/backend/access/table/toast_helper.c
@@ -171,8 +171,10 @@ toast_tuple_init(ToastTupleContext *ttc)
  * The column must have attstorage EXTERNAL or EXTENDED if check_main is
  * false, and must have attstorage MAIN if check_main is true.
  *
- * The column must have a minimum size of MAXALIGN(TOAST_POINTER_SIZE);
- * if not, no benefit is to be expected by compressing it.
+ * The column must have a minimum size of MAXALIGN(tcc_toast_pointer_size);
+ * if not, no benefit is to be expected by compressing it.  The TOAST
+ * pointer size is given by the caller, depending on the type of TOAST
+ * table we are dealing with.
  *
  * The return value is the index of the biggest suitable column, or
  * -1 if there is none.
@@ -184,10 +186,14 @@ toast_tuple_find_biggest_attribute(ToastTupleContext *ttc,
 	TupleDesc	tupleDesc = ttc->ttc_rel->rd_att;
 	int			numAttrs = tupleDesc->natts;
 	int			biggest_attno = -1;
-	int32		biggest_size = MAXALIGN(TOAST_POINTER_SIZE);
+	int32		biggest_size = 0;
 	int32		skip_colflags = TOASTCOL_IGNORE;
 	int			i;
 
+	/* Define the lower-bound */
+	biggest_size = MAXALIGN(ttc->ttc_toast_pointer_size);
+	Assert(biggest_size != 0);
+
 	if (for_compression)
 		skip_colflags |= TOASTCOL_INCOMPRESSIBLE;
 
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 3542696f0d99..26508bd01c86 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -92,6 +92,7 @@
 #include "access/detoast.h"
 #include "access/heapam.h"
 #include "access/rewriteheap.h"
+#include "access/toast_external.h"
 #include "access/transam.h"
 #include "access/xact.h"
 #include "access/xlog_internal.h"
@@ -5102,7 +5103,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		struct varlena *varlena;
 
 		/* va_rawsize is the size of the original datum -- including header */
-		struct varatt_external_oid toast_pointer;
+		struct toast_external_data toast_pointer;
 		struct varatt_indirect redirect_pointer;
 		struct varlena *new_datum = NULL;
 		struct varlena *reconstructed;
@@ -5132,8 +5133,8 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		if (!VARATT_IS_EXTERNAL(varlena))
 			continue;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, varlena);
-		toast_valueid = toast_pointer.va_valueid;
+		toast_external_info_get_data(varlena, &toast_pointer);
+		toast_valueid = toast_pointer.value;
 
 		/*
 		 * Check whether the toast tuple changed, replace if so.
@@ -5151,7 +5152,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 
 		free[natt] = true;
 
-		reconstructed = palloc0(toast_pointer.va_rawsize);
+		reconstructed = palloc0(toast_pointer.rawsize);
 
 		ent->reconstructed = reconstructed;
 
@@ -5176,10 +5177,10 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 				   VARSIZE(chunk) - VARHDRSZ);
 			data_done += VARSIZE(chunk) - VARHDRSZ;
 		}
-		Assert(data_done == VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer));
+		Assert(data_done == toast_pointer.extsize);
 
 		/* make sure its marked as compressed or not */
-		if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+		if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 			SET_VARSIZE_COMPRESSED(reconstructed, data_done + VARHDRSZ);
 		else
 			SET_VARSIZE(reconstructed, data_done + VARHDRSZ);
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 38d4e6d45f28..d76386407a08 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -19,6 +19,7 @@
 
 #include "access/detoast.h"
 #include "access/toast_compression.h"
+#include "access/toast_external.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_type.h"
 #include "common/hashfn.h"
@@ -4219,7 +4220,7 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 {
 	int			typlen;
 	struct varlena *attr;
-	struct varatt_external_oid toast_pointer;
+	uint64		toast_valueid;
 
 	/* On first call, get the input type's typlen, and save at *fn_extra */
 	if (fcinfo->flinfo->fn_extra == NULL)
@@ -4246,9 +4247,9 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		PG_RETURN_NULL();
 
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_valueid = toast_external_info_get_value(attr);
 
-	PG_RETURN_OID(toast_pointer.va_valueid);
+	PG_RETURN_OID(toast_valueid);
 }
 
 /*
diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml
index 61250799ec07..f3c6cd8860b5 100644
--- a/doc/src/sgml/storage.sgml
+++ b/doc/src/sgml/storage.sgml
@@ -415,7 +415,7 @@ described in more detail below.
 
 <para>
 Out-of-line values are divided (after compression if used) into chunks of at
-most <symbol>TOAST_MAX_CHUNK_SIZE</symbol> bytes (by default this value is chosen
+most <symbol>TOAST_MAX_CHUNK_SIZE_OID</symbol> bytes (by default this value is chosen
 so that four chunk rows will fit on a page, making it about 2000 bytes).
 Each chunk is stored as a separate row in the <acronym>TOAST</acronym> table
 belonging to the owning table.  Every
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 030f9fb64b51..11c4507ae6e2 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -16,6 +16,7 @@
 #include "access/multixact.h"
 #include "access/relation.h"
 #include "access/table.h"
+#include "access/toast_external.h"
 #include "access/toast_internals.h"
 #include "access/visibilitymap.h"
 #include "access/xact.h"
@@ -73,7 +74,8 @@ typedef enum SkipPages
  */
 typedef struct ToastedAttribute
 {
-	struct varatt_external_oid toast_pointer;
+	struct toast_external_data toast_pointer;
+	const toast_external_info *info;
 	BlockNumber blkno;			/* block in main table */
 	OffsetNumber offnum;		/* offset in main table */
 	AttrNumber	attnum;			/* attribute in main table */
@@ -1564,9 +1566,9 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	uint64		toast_valueid;
 	int32		max_chunk_size;
 
-	toast_valueid = ta->toast_pointer.va_valueid;
+	toast_valueid = ta->toast_pointer.value;
 
-	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	max_chunk_size = ta->info->maximum_chunk_size;
 	last_chunk_seq = (extsize - 1) / max_chunk_size;
 
 	/* Sanity-check the sequence number. */
@@ -1672,7 +1674,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	uint16		infomask;
 	uint64		toast_pointer_valueid;
 	CompactAttribute *thisatt;
-	struct varatt_external_oid toast_pointer;
+	struct toast_external_data toast_pointer;
 
 	infomask = ctx->tuphdr->t_infomask;
 	thisatt = TupleDescCompactAttr(RelationGetDescr(ctx->rel), ctx->attnum);
@@ -1774,28 +1776,28 @@ check_tuple_attribute(HeapCheckContext *ctx)
 		return true;
 
 	/* It is external, and we're looking at a page on disk */
-	toast_pointer_valueid = toast_pointer.va_valueid;
 
 	/*
 	 * Must copy attr into toast_pointer for alignment considerations
 	 */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_external_info_get_data(attr, &toast_pointer);
+	toast_pointer_valueid = toast_pointer.value;
 
 	/* Toasted attributes too large to be untoasted should never be stored */
-	if (toast_pointer.va_rawsize > VARLENA_SIZE_LIMIT)
+	if (toast_pointer.rawsize > VARLENA_SIZE_LIMIT)
 		report_corruption(ctx,
 						  psprintf("toast value %" PRIu64 " rawsize %d exceeds limit %d",
 								   toast_pointer_valueid,
-								   toast_pointer.va_rawsize,
+								   toast_pointer.rawsize,
 								   VARLENA_SIZE_LIMIT));
 
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+	if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 	{
 		ToastCompressionId cmid;
 		bool		valid = false;
 
 		/* Compressed attributes should have a valid compression method */
-		cmid = TOAST_COMPRESS_METHOD(&toast_pointer);
+		cmid = toast_pointer.compression_method;
 		switch (cmid)
 		{
 				/* List of all valid compression method IDs */
@@ -1849,7 +1851,8 @@ check_tuple_attribute(HeapCheckContext *ctx)
 
 		ta = (ToastedAttribute *) palloc0(sizeof(ToastedAttribute));
 
-		VARATT_EXTERNAL_GET_POINTER(ta->toast_pointer, attr);
+		toast_external_info_get_data(attr, &ta->toast_pointer);
+		ta->info = toast_external_get_info(VARTAG_EXTERNAL(attr));
 		ta->blkno = ctx->blkno;
 		ta->offnum = ctx->offnum;
 		ta->attnum = ctx->attnum;
@@ -1876,9 +1879,11 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	int32		expected_chunk_seq = 0;
 	int32		last_chunk_seq;
 	uint64		toast_valueid;
-	int32		max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	int32		max_chunk_size;
 
-	extsize = VARATT_EXTERNAL_GET_EXTSIZE(ta->toast_pointer);
+	extsize = ta->toast_pointer.extsize;
+
+	max_chunk_size = ta->info->maximum_chunk_size;
 	last_chunk_seq = (extsize - 1) / max_chunk_size;
 
 	/*
@@ -1887,7 +1892,7 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	ScanKeyInit(&toastkey,
 				(AttrNumber) 1,
 				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(ta->toast_pointer.va_valueid));
+				ObjectIdGetDatum(ta->toast_pointer.value));
 
 	/*
 	 * Check if any chunks for this toasted object exist in the toast table,
@@ -1907,7 +1912,7 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	}
 	systable_endscan_ordered(toastscan);
 
-	toast_valueid = ta->toast_pointer.va_valueid;
+	toast_valueid = ta->toast_pointer.value;
 
 	if (!found_toasttup)
 		report_toast_corruption(ctx, ta,
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index e6f2e93b2d6f..995dc1f28208 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -4143,6 +4143,8 @@ timeout_params
 timerCA
 tlist_vinfo
 toast_compress_header
+toast_external_data
+toast_external_info
 tokenize_error_callback_arg
 transferMode
 transfer_thread_arg
-- 
2.50.0

v3-0005-Introduce-new-callback-to-get-fresh-TOAST-values.patchtext/x-diff; charset=us-asciiDownload
From 5af3bac8abdbf38e6c7e584e95fb3038d0759d96 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 1 Aug 2025 16:29:33 +0900
Subject: [PATCH v3 05/14] Introduce new callback to get fresh TOAST values

This callback is called by toast_save_datum() to retrieve a new value
from a source related to the vartag_external we are dealing with.  As of
now, it is simply a wrapper around GetNewOidWithIndex() for the "OID"
on-disk TOAST external pointer.

This will be used later on by more external pointer types, like the int8
one.

InvalidToastId is introduced to track the concept of an "invalid" TOAST
value, required for toast_save_datum().
---
 src/include/access/toast_external.h         | 19 +++++++++++++-
 src/backend/access/common/toast_external.c  | 11 ++++++++
 src/backend/access/common/toast_internals.c | 28 ++++++++++++++-------
 src/backend/access/heap/heaptoast.c         |  2 +-
 4 files changed, 49 insertions(+), 11 deletions(-)

diff --git a/src/include/access/toast_external.h b/src/include/access/toast_external.h
index 849cb1779b67..1f580acd02db 100644
--- a/src/include/access/toast_external.h
+++ b/src/include/access/toast_external.h
@@ -15,9 +15,14 @@
 #ifndef TOAST_EXTERNAL_H
 #define TOAST_EXTERNAL_H
 
+#include "access/attnum.h"
 #include "access/toast_compression.h"
+#include "utils/relcache.h"
 #include "varatt.h"
 
+/* Invalid TOAST value ID */
+#define InvalidToastId 0
+
 /*
  * Intermediate in-memory structure used when creating on-disk
  * varatt_external_* or when deserializing varlena contents.
@@ -39,7 +44,7 @@ typedef struct toast_external_data
 	/*
 	 * Unique ID of value within TOAST table.  This could be an OID or an
 	 * int8 value.  This field is large enough to be able to store any of
-	 * them.
+	 * them.  InvalidToastId if invalid.
 	 */
 	uint64		value;
 } toast_external_data;
@@ -78,6 +83,18 @@ typedef struct toast_external_info
 	 */
 	struct varlena  *(*create_external_data) (toast_external_data data);
 
+	/*
+	 * Retrieve a new value, to be assigned for a TOAST entry that will
+	 * be saved.  "toastrel" is the relation where the entry is added.
+	 * "indexid" and "attnum" can be used to check if a value is already
+	 * in use in the TOAST relation where the new entry is inserted.
+	 *
+	 * When "check" is set to true, the value generated should be rechecked
+	 * with the existing TOAST index.
+	 */
+	uint64		(*get_new_value) (Relation toastrel, Oid indexid,
+								  AttrNumber attnum);
+
 } toast_external_info;
 
 /* Retrieve a toast_external_info from a vartag */
diff --git a/src/backend/access/common/toast_external.c b/src/backend/access/common/toast_external.c
index 5bfb6a6d434f..a17de2fa8b3d 100644
--- a/src/backend/access/common/toast_external.c
+++ b/src/backend/access/common/toast_external.c
@@ -16,11 +16,14 @@
 #include "access/detoast.h"
 #include "access/heaptoast.h"
 #include "access/toast_external.h"
+#include "catalog/catalog.h"
 
 /* Callbacks for VARTAG_ONDISK_OID */
 static void ondisk_oid_to_external_data(struct varlena *attr,
 										toast_external_data *data);
 static struct varlena *ondisk_oid_create_external_data(toast_external_data data);
+static uint64 ondisk_oid_get_new_value(Relation toastrel, Oid indexid,
+									   AttrNumber attnum);
 
 
 /*
@@ -48,6 +51,7 @@ static const toast_external_info toast_external_infos[TOAST_EXTERNAL_INFO_SIZE]
 		.maximum_chunk_size = TOAST_MAX_CHUNK_SIZE_OID,
 		.to_external_data = ondisk_oid_to_external_data,
 		.create_external_data = ondisk_oid_create_external_data,
+		.get_new_value = ondisk_oid_get_new_value,
 	},
 };
 
@@ -154,3 +158,10 @@ ondisk_oid_create_external_data(toast_external_data data)
 
 	return result;
 }
+
+static uint64
+ondisk_oid_get_new_value(Relation toastrel, Oid indexid,
+						 AttrNumber attnum)
+{
+	return GetNewOidWithIndex(toastrel, indexid, attnum);
+}
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 80bda9034cdf..31cb02a48e15 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -227,6 +227,16 @@ toast_save_datum(Relation rel, Datum value,
 	else
 		toast_pointer.toastrelid = RelationGetRelid(toastrel);
 
+	/*
+	 * Retrieve the external TOAST information, with the value still
+	 * unknown.  We need to do this again once we know the actual value
+	 * assigned, to define the correct vartag_external for the new TOAST
+	 * tuple.
+	 */
+	tag = toast_external_assign_vartag(toast_pointer.toastrelid,
+									   InvalidToastId);
+	info = toast_external_get_info(tag);
+
 	/*
 	 * Choose an OID to use as the value ID for this toast value.
 	 *
@@ -243,14 +253,14 @@ toast_save_datum(Relation rel, Datum value,
 	{
 		/* normal case: just choose an unused OID */
 		toast_pointer.value =
-			GetNewOidWithIndex(toastrel,
-							   RelationGetRelid(toastidxs[validIndex]),
-							   (AttrNumber) 1);
+			info->get_new_value(toastrel,
+								RelationGetRelid(toastidxs[validIndex]),
+								(AttrNumber) 1);
 	}
 	else
 	{
 		/* rewrite case: check to see if value was in old toast table */
-		toast_pointer.value = InvalidOid;
+		toast_pointer.value = InvalidToastId;
 		if (oldexternal != NULL)
 		{
 			struct toast_external_data old_toast_pointer;
@@ -288,7 +298,7 @@ toast_save_datum(Relation rel, Datum value,
 				}
 			}
 		}
-		if (toast_pointer.value == InvalidOid)
+		if (toast_pointer.value == InvalidToastId)
 		{
 			/*
 			 * new value; must choose an OID that doesn't conflict in either
@@ -297,9 +307,9 @@ toast_save_datum(Relation rel, Datum value,
 			do
 			{
 				toast_pointer.value =
-					GetNewOidWithIndex(toastrel,
-									   RelationGetRelid(toastidxs[validIndex]),
-									   (AttrNumber) 1);
+					info->get_new_value(toastrel,
+										RelationGetRelid(toastidxs[validIndex]),
+										(AttrNumber) 1);
 			} while (toastid_valueid_exists(rel->rd_toastoid,
 											toast_pointer.value));
 		}
@@ -316,7 +326,7 @@ toast_save_datum(Relation rel, Datum value,
 
 	/*
 	 * Retrieve the vartag that can be assigned for the new TOAST tuple.
-	 * This depends on the type of TOAST table and its assigned value.
+	 * This depends on the type of TOAST table and its now-assigned value.
 	 */
 	tag = toast_external_assign_vartag(toast_pointer.toastrelid,
 									   toast_pointer.value);
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index 47cc49be2aba..0c716be3860f 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -149,7 +149,7 @@ heap_toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup,
 	 */
 
 	/* The default value is invalid, to work as a default. */
-	tag = toast_external_assign_vartag(rel->rd_rel->reltoastrelid, InvalidOid);
+	tag = toast_external_assign_vartag(rel->rd_rel->reltoastrelid, InvalidToastId);
 	ttc.ttc_toast_pointer_size = toast_external_info_get_pointer_size(tag);
 
 	ttc.ttc_rel = rel;
-- 
2.50.0

v3-0006-Add-catcache-support-for-INT8OID.patchtext/x-diff; charset=us-asciiDownload
From 8189fc1c34c4476946d098b3ce18713b79b08a9b Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 18 Jun 2025 16:12:11 +0900
Subject: [PATCH v3 06/14] Add catcache support for INT8OID

This is required to be able to do catalog cache lookups of int8 fields
for toast values of the same type.
---
 src/backend/utils/cache/catcache.c | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index d1b25214376e..c77f571014e5 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -240,6 +240,18 @@ int4hashfast(Datum datum)
 	return murmurhash32((int32) DatumGetInt32(datum));
 }
 
+static bool
+int8eqfast(Datum a, Datum b)
+{
+	return DatumGetInt64(a) == DatumGetInt64(b);
+}
+
+static uint32
+int8hashfast(Datum datum)
+{
+	return murmurhash64((int64) DatumGetInt64(datum));
+}
+
 static bool
 texteqfast(Datum a, Datum b)
 {
@@ -300,6 +312,11 @@ GetCCHashEqFuncs(Oid keytype, CCHashFN *hashfunc, RegProcedure *eqfunc, CCFastEq
 			*fasteqfunc = int4eqfast;
 			*eqfunc = F_INT4EQ;
 			break;
+		case INT8OID:
+			*hashfunc = int8hashfast;
+			*fasteqfunc = int8eqfast;
+			*eqfunc = F_INT8EQ;
+			break;
 		case TEXTOID:
 			*hashfunc = texthashfast;
 			*fasteqfunc = texteqfast;
-- 
2.50.0

v3-0007-Add-GUC-default_toast_type.patchtext/x-diff; charset=us-asciiDownload
From 9e09e0d18c8f76e662aada516e4078daf235c62b Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 18 Jun 2025 16:15:19 +0900
Subject: [PATCH v3 07/14] Add GUC default_toast_type

This GUC controls the data type used for newly-created TOAST values,
with two modes supported: "oid" and "int8".  This will be used by an
upcoming patch.
---
 src/include/access/toast_type.h               | 30 +++++++++++++++++++
 src/backend/catalog/toasting.c                |  4 +++
 src/backend/utils/misc/guc_tables.c           | 19 ++++++++++++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 doc/src/sgml/config.sgml                      | 17 +++++++++++
 5 files changed, 71 insertions(+)
 create mode 100644 src/include/access/toast_type.h

diff --git a/src/include/access/toast_type.h b/src/include/access/toast_type.h
new file mode 100644
index 000000000000..494c2a3e852e
--- /dev/null
+++ b/src/include/access/toast_type.h
@@ -0,0 +1,30 @@
+/*-------------------------------------------------------------------------
+ *
+ * toast_type.h
+ *	  Internal definitions for the types supported by values in TOAST
+ *	  relations.
+ *
+ * Copyright (c) 2000-2025, PostgreSQL Global Development Group
+ *
+ * src/include/access/toast_type.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef TOAST_TYPE_H
+#define TOAST_TYPE_H
+
+/*
+ * GUC support
+ *
+ * Detault value type in toast table.
+ */
+extern PGDLLIMPORT int default_toast_type;
+
+typedef enum ToastTypeId
+{
+	TOAST_TYPE_INVALID = 0,
+	TOAST_TYPE_OID = 1,
+	TOAST_TYPE_INT8 = 2,
+} ToastTypeId;
+
+#endif							/* TOAST_TYPE_H */
diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c
index 874a8fc89adb..e595cb61b375 100644
--- a/src/backend/catalog/toasting.c
+++ b/src/backend/catalog/toasting.c
@@ -16,6 +16,7 @@
 
 #include "access/heapam.h"
 #include "access/toast_compression.h"
+#include "access/toast_type.h"
 #include "access/xact.h"
 #include "catalog/binary_upgrade.h"
 #include "catalog/catalog.h"
@@ -33,6 +34,9 @@
 #include "utils/rel.h"
 #include "utils/syscache.h"
 
+/* GUC support */
+int			default_toast_type = TOAST_TYPE_OID;
+
 static void CheckAndCreateToastTable(Oid relOid, Datum reloptions,
 									 LOCKMODE lockmode, bool check,
 									 Oid OIDOldToast);
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index d14b1678e7fe..be523c9ac094 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -33,6 +33,7 @@
 #include "access/gin.h"
 #include "access/slru.h"
 #include "access/toast_compression.h"
+#include "access/toast_type.h"
 #include "access/twophase.h"
 #include "access/xlog_internal.h"
 #include "access/xlogprefetcher.h"
@@ -464,6 +465,13 @@ static const struct config_enum_entry default_toast_compression_options[] = {
 	{NULL, 0, false}
 };
 
+
+static const struct config_enum_entry default_toast_type_options[] = {
+	{"oid", TOAST_TYPE_OID, false},
+	{"int8", TOAST_TYPE_INT8, false},
+	{NULL, 0, false}
+};
+
 static const struct config_enum_entry wal_compression_options[] = {
 	{"pglz", WAL_COMPRESSION_PGLZ, false},
 #ifdef USE_LZ4
@@ -5058,6 +5066,17 @@ struct config_enum ConfigureNamesEnum[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"default_toast_type", PGC_USERSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Sets the default type used for TOAST values."),
+			NULL
+		},
+		&default_toast_type,
+		TOAST_TYPE_OID,
+		default_toast_type_options,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"default_transaction_isolation", PGC_USERSET, CLIENT_CONN_STATEMENT,
 			gettext_noop("Sets the transaction isolation level of each new transaction."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index a9d8293474af..5f34b14ea39a 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -753,6 +753,7 @@ autovacuum_worker_slots = 16	# autovacuum worker slots to allocate
 #default_table_access_method = 'heap'
 #default_tablespace = ''		# a tablespace name, '' uses the default
 #default_toast_compression = 'pglz'	# 'pglz' or 'lz4'
+#default_toast_type = 'oid'		# 'oid' or 'int8'
 #temp_tablespaces = ''			# a list of tablespace names, '' uses
 					# only default tablespace
 #check_function_bodies = on
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 20ccb2d6b544..21ccef564f4e 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9834,6 +9834,23 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-default-toast-type" xreflabel="default_toast_type">
+      <term><varname>default_toast_type</varname> (<type>enum</type>)
+      <indexterm>
+       <primary><varname>default_toast_type</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        This variable sets the default type for
+        <link linkend="storage-toast">TOAST</link> values.
+        The value types supported are <literal>oid</literal> and
+        <literal>int8</literal>.
+        The default is <literal>oid</literal>.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-temp-tablespaces" xreflabel="temp_tablespaces">
       <term><varname>temp_tablespaces</varname> (<type>string</type>)
       <indexterm>
-- 
2.50.0

v3-0008-Introduce-global-64-bit-TOAST-ID-counter-in-contr.patchtext/x-diff; charset=us-asciiDownload
From 9ae44a7edcced3c8baa9c9e663cbb2399314bb57 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 8 Jul 2025 09:00:43 +0900
Subject: [PATCH v3 08/14] Introduce global 64-bit TOAST ID counter in control
 file

An 8 byte counter is added to the control file, providing a unique
64-bit-wide source for toast value IDs, with the same guarantees as OIDs
in terms of durability.  SQL functions and tools looking at the control
file are updated.  A WAL record is generated every 8k values generated,
that can be adjusted if required.

Requires a bump of WAL format.
Requires a bump of control file version.
Requires a catalog version bump.
---
 src/include/access/toast_counter.h            | 34 +++++++
 src/include/access/xlog.h                     |  1 +
 src/include/catalog/pg_control.h              |  4 +-
 src/include/catalog/pg_proc.dat               |  6 +-
 src/include/storage/lwlocklist.h              |  1 +
 src/backend/access/common/Makefile            |  1 +
 src/backend/access/common/meson.build         |  1 +
 src/backend/access/common/toast_counter.c     | 98 +++++++++++++++++++
 src/backend/access/rmgrdesc/xlogdesc.c        | 10 ++
 src/backend/access/transam/xlog.c             | 44 +++++++++
 src/backend/replication/logical/decode.c      |  1 +
 src/backend/storage/ipc/ipci.c                |  5 +-
 .../utils/activity/wait_event_names.txt       |  1 +
 src/backend/utils/misc/pg_controldata.c       | 23 +++--
 src/bin/pg_controldata/pg_controldata.c       |  2 +
 src/bin/pg_resetwal/pg_resetwal.c             |  2 +
 doc/src/sgml/func.sgml                        |  5 +
 src/tools/pgindent/typedefs.list              |  1 +
 18 files changed, 225 insertions(+), 15 deletions(-)
 create mode 100644 src/include/access/toast_counter.h
 create mode 100644 src/backend/access/common/toast_counter.c

diff --git a/src/include/access/toast_counter.h b/src/include/access/toast_counter.h
new file mode 100644
index 000000000000..e2bc79682771
--- /dev/null
+++ b/src/include/access/toast_counter.h
@@ -0,0 +1,34 @@
+/*-------------------------------------------------------------------------
+ *
+ * toast_counter.h
+ *	  Machinery for TOAST value counter.
+ *
+ * Copyright (c) 2000-2025, PostgreSQL Global Development Group
+ *
+ * src/include/access/toast_counter.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef TOAST_COUNTER_H
+#define TOAST_COUNTER_H
+
+#define FirstToastId	1		/* First TOAST value ID assigned */
+
+/*
+ * Structure in shared memory to track TOAST value counter activity.
+ * These are protected by ToastIdGenLock.
+ */
+typedef struct ToastCounterData
+{
+	uint64		nextId;			/* next TOAST value ID to assign */
+	uint32		idCount;		/* IDs available before WAL work */
+} ToastCounterData;
+
+extern PGDLLIMPORT ToastCounterData *ToastCounter;
+
+/* external declarations */
+extern Size ToastCounterShmemSize(void);
+extern void ToastCounterShmemInit(void);
+extern uint64 GetNewToastId(void);
+
+#endif							/* TOAST_TYPE_H */
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index d12798be3d80..6c5e5feb54bb 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -244,6 +244,7 @@ extern bool CreateCheckPoint(int flags);
 extern bool CreateRestartPoint(int flags);
 extern WALAvailability GetWALAvailability(XLogRecPtr targetLSN);
 extern void XLogPutNextOid(Oid nextOid);
+extern void XLogPutNextToastId(uint64 nextId);
 extern XLogRecPtr XLogRestorePoint(const char *rpName);
 extern void UpdateFullPageWrites(void);
 extern void GetFullPageWriteInfo(XLogRecPtr *RedoRecPtr_p, bool *doPageWrites_p);
diff --git a/src/include/catalog/pg_control.h b/src/include/catalog/pg_control.h
index 63e834a6ce47..1194b4928155 100644
--- a/src/include/catalog/pg_control.h
+++ b/src/include/catalog/pg_control.h
@@ -22,7 +22,7 @@
 
 
 /* Version identifier for this pg_control format */
-#define PG_CONTROL_VERSION	1800
+#define PG_CONTROL_VERSION	1900
 
 /* Nonce key length, see below */
 #define MOCK_AUTH_NONCE_LEN		32
@@ -45,6 +45,7 @@ typedef struct CheckPoint
 	Oid			nextOid;		/* next free OID */
 	MultiXactId nextMulti;		/* next free MultiXactId */
 	MultiXactOffset nextMultiOffset;	/* next free MultiXact offset */
+	uint64		nextToastId;	/* next free TOAST ID */
 	TransactionId oldestXid;	/* cluster-wide minimum datfrozenxid */
 	Oid			oldestXidDB;	/* database with minimum datfrozenxid */
 	MultiXactId oldestMulti;	/* cluster-wide minimum datminmxid */
@@ -80,6 +81,7 @@ typedef struct CheckPoint
 /* 0xC0 is used in Postgres 9.5-11 */
 #define XLOG_OVERWRITE_CONTRECORD		0xD0
 #define XLOG_CHECKPOINT_REDO			0xE0
+#define XLOG_NEXT_TOAST_ID				0xF0
 
 
 /*
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3ee8fed7e537..3dd547641065 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12318,9 +12318,9 @@
   descr => 'pg_controldata checkpoint state information as a function',
   proname => 'pg_control_checkpoint', provolatile => 'v',
   prorettype => 'record', proargtypes => '',
-  proallargtypes => '{pg_lsn,pg_lsn,text,int4,int4,bool,text,oid,xid,xid,xid,oid,xid,xid,oid,xid,xid,timestamptz}',
-  proargmodes => '{o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
-  proargnames => '{checkpoint_lsn,redo_lsn,redo_wal_file,timeline_id,prev_timeline_id,full_page_writes,next_xid,next_oid,next_multixact_id,next_multi_offset,oldest_xid,oldest_xid_dbid,oldest_active_xid,oldest_multi_xid,oldest_multi_dbid,oldest_commit_ts_xid,newest_commit_ts_xid,checkpoint_time}',
+  proallargtypes => '{pg_lsn,pg_lsn,text,int4,int4,bool,text,oid,xid,xid,int8,xid,oid,xid,xid,oid,xid,xid,timestamptz}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{checkpoint_lsn,redo_lsn,redo_wal_file,timeline_id,prev_timeline_id,full_page_writes,next_xid,next_oid,next_multixact_id,next_multi_offset,next_toast_id,oldest_xid,oldest_xid_dbid,oldest_active_xid,oldest_multi_xid,oldest_multi_dbid,oldest_commit_ts_xid,newest_commit_ts_xid,checkpoint_time}',
   prosrc => 'pg_control_checkpoint' },
 
 { oid => '3443',
diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h
index 208d2e3a8ed9..81abc58f0810 100644
--- a/src/include/storage/lwlocklist.h
+++ b/src/include/storage/lwlocklist.h
@@ -85,6 +85,7 @@ PG_LWLOCK(50, DSMRegistry)
 PG_LWLOCK(51, InjectionPoint)
 PG_LWLOCK(52, SerialControl)
 PG_LWLOCK(53, AioWorkerSubmissionQueue)
+PG_LWLOCK(54, ToastIdGen)
 
 /*
  * There also exist several built-in LWLock tranches.  As with the predefined
diff --git a/src/backend/access/common/Makefile b/src/backend/access/common/Makefile
index 1ef86a245886..6e9a3a430c19 100644
--- a/src/backend/access/common/Makefile
+++ b/src/backend/access/common/Makefile
@@ -27,6 +27,7 @@ OBJS = \
 	syncscan.o \
 	tidstore.o \
 	toast_compression.o \
+	toast_counter.o \
 	toast_external.o \
 	toast_internals.o \
 	tupconvert.o \
diff --git a/src/backend/access/common/meson.build b/src/backend/access/common/meson.build
index c20f2e88921e..4254132c8dfd 100644
--- a/src/backend/access/common/meson.build
+++ b/src/backend/access/common/meson.build
@@ -15,6 +15,7 @@ backend_sources += files(
   'syncscan.c',
   'tidstore.c',
   'toast_compression.c',
+  'toast_counter.c',
   'toast_external.c',
   'toast_internals.c',
   'tupconvert.c',
diff --git a/src/backend/access/common/toast_counter.c b/src/backend/access/common/toast_counter.c
new file mode 100644
index 000000000000..94d361d0d5c4
--- /dev/null
+++ b/src/backend/access/common/toast_counter.c
@@ -0,0 +1,98 @@
+/*-------------------------------------------------------------------------
+ *
+ * toast_counter.c
+ *	  Functions for TOAST value counter.
+ *
+ * Copyright (c) 2025, PostgreSQL Global Development Group
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/common/toast_counter.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/toast_counter.h"
+#include "access/xlog.h"
+#include "miscadmin.h"
+#include "storage/lwlock.h"
+#include "storage/shmem.h"
+
+/* Number of TOAST values to preallocate before WAL work */
+#define TOAST_ID_PREFETCH		8192
+
+/* pointer to variables struct in shared memory */
+ToastCounterData *ToastCounter = NULL;
+
+/*
+ * Initialization of shared memory for ToastCounter.
+ */
+Size
+ToastCounterShmemSize(void)
+{
+	return sizeof(ToastCounterData);
+}
+
+void
+ToastCounterShmemInit(void)
+{
+	bool		found;
+
+	/* Initialize shared state struct */
+	ToastCounter = ShmemInitStruct("ToastCounter",
+								   sizeof(ToastCounterData),
+								   &found);
+	if (!IsUnderPostmaster)
+	{
+		Assert(!found);
+		memset(ToastCounter, 0, sizeof(ToastCounterData));
+	}
+	else
+		Assert(found);
+}
+
+/*
+ * GetNewToastId
+ *
+ * Toast IDs are generated as a cluster-wide counter.  They are 64 bits
+ * wide, hence wraparound will unlikely happen.
+ */
+uint64
+GetNewToastId(void)
+{
+	uint64		result;
+
+	if (RecoveryInProgress())
+		elog(ERROR, "cannot assign TOAST IDs during recovery");
+
+	LWLockAcquire(ToastIdGenLock, LW_EXCLUSIVE);
+
+	/*
+	 * Check for initialization or wraparound of the toast counter ID.
+	 * InvalidToastId (0) should never be returned.  We are 64 bit-wide, hence
+	 * wraparound is unlikely going to happen, but this check is cheap so
+	 * let's play it safe.
+	 */
+	if (ToastCounter->nextId < ((uint64) FirstToastId))
+	{
+		/* Most-likely first bootstrap or initdb assignment */
+		ToastCounter->nextId = FirstToastId;
+		ToastCounter->idCount = 0;
+	}
+
+	/* If running out of logged for TOAST IDs, log more */
+	if (ToastCounter->idCount == 0)
+	{
+		XLogPutNextToastId(ToastCounter->nextId + TOAST_ID_PREFETCH);
+		ToastCounter->idCount = TOAST_ID_PREFETCH;
+	}
+
+	result = ToastCounter->nextId;
+	(ToastCounter->nextId)++;
+	(ToastCounter->idCount)--;
+
+	LWLockRelease(ToastIdGenLock);
+
+	return result;
+}
diff --git a/src/backend/access/rmgrdesc/xlogdesc.c b/src/backend/access/rmgrdesc/xlogdesc.c
index cd6c2a2f650a..6786af4064b8 100644
--- a/src/backend/access/rmgrdesc/xlogdesc.c
+++ b/src/backend/access/rmgrdesc/xlogdesc.c
@@ -96,6 +96,13 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
 		memcpy(&nextOid, rec, sizeof(Oid));
 		appendStringInfo(buf, "%u", nextOid);
 	}
+	else if (info == XLOG_NEXT_TOAST_ID)
+	{
+		uint64		nextId;
+
+		memcpy(&nextId, rec, sizeof(uint64));
+		appendStringInfo(buf, "%" PRIu64, nextId);
+	}
 	else if (info == XLOG_RESTORE_POINT)
 	{
 		xl_restore_point *xlrec = (xl_restore_point *) rec;
@@ -218,6 +225,9 @@ xlog_identify(uint8 info)
 		case XLOG_CHECKPOINT_REDO:
 			id = "CHECKPOINT_REDO";
 			break;
+		case XLOG_NEXT_TOAST_ID:
+			id = "NEXT_TOAST_ID";
+			break;
 	}
 
 	return id;
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index b0891998b243..4566d53aeba2 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -53,6 +53,7 @@
 #include "access/rewriteheap.h"
 #include "access/subtrans.h"
 #include "access/timeline.h"
+#include "access/toast_counter.h"
 #include "access/transam.h"
 #include "access/twophase.h"
 #include "access/xact.h"
@@ -5259,6 +5260,7 @@ BootStrapXLOG(uint32 data_checksum_version)
 	checkPoint.nextOid = FirstGenbkiObjectId;
 	checkPoint.nextMulti = FirstMultiXactId;
 	checkPoint.nextMultiOffset = 0;
+	checkPoint.nextToastId = FirstToastId;
 	checkPoint.oldestXid = FirstNormalTransactionId;
 	checkPoint.oldestXidDB = Template1DbOid;
 	checkPoint.oldestMulti = FirstMultiXactId;
@@ -5271,6 +5273,10 @@ BootStrapXLOG(uint32 data_checksum_version)
 	TransamVariables->nextXid = checkPoint.nextXid;
 	TransamVariables->nextOid = checkPoint.nextOid;
 	TransamVariables->oidCount = 0;
+
+	ToastCounter->nextId = checkPoint.nextToastId;
+	ToastCounter->idCount = 0;
+
 	MultiXactSetNextMXact(checkPoint.nextMulti, checkPoint.nextMultiOffset);
 	AdvanceOldestClogXid(checkPoint.oldestXid);
 	SetTransactionIdLimit(checkPoint.oldestXid, checkPoint.oldestXidDB);
@@ -5747,6 +5753,8 @@ StartupXLOG(void)
 	TransamVariables->nextXid = checkPoint.nextXid;
 	TransamVariables->nextOid = checkPoint.nextOid;
 	TransamVariables->oidCount = 0;
+	ToastCounter->nextId = checkPoint.nextToastId;
+	ToastCounter->idCount = 0;
 	MultiXactSetNextMXact(checkPoint.nextMulti, checkPoint.nextMultiOffset);
 	AdvanceOldestClogXid(checkPoint.oldestXid);
 	SetTransactionIdLimit(checkPoint.oldestXid, checkPoint.oldestXidDB);
@@ -7288,6 +7296,12 @@ CreateCheckPoint(int flags)
 		checkPoint.nextOid += TransamVariables->oidCount;
 	LWLockRelease(OidGenLock);
 
+	LWLockAcquire(ToastIdGenLock, LW_SHARED);
+	checkPoint.nextToastId = ToastCounter->nextId;
+	if (!shutdown)
+		checkPoint.nextToastId += ToastCounter->idCount;
+	LWLockRelease(ToastIdGenLock);
+
 	MultiXactGetCheckptMulti(shutdown,
 							 &checkPoint.nextMulti,
 							 &checkPoint.nextMultiOffset,
@@ -8223,6 +8237,22 @@ XLogPutNextOid(Oid nextOid)
 	 */
 }
 
+/*
+ * Write a NEXT_TOAST_ID log record.
+ */
+void
+XLogPutNextToastId(uint64 nextId)
+{
+	XLogBeginInsert();
+	XLogRegisterData(&nextId, sizeof(uint64));
+	(void) XLogInsert(RM_XLOG_ID, XLOG_NEXT_TOAST_ID);
+
+	/*
+	 * The next TOAST value ID is not flushed immediately, for the same reason
+	 * as above for the OIDs in XLogPutNextOid().
+	 */
+}
+
 /*
  * Write an XLOG SWITCH record.
  *
@@ -8438,6 +8468,16 @@ xlog_redo(XLogReaderState *record)
 		TransamVariables->oidCount = 0;
 		LWLockRelease(OidGenLock);
 	}
+	else if (info == XLOG_NEXT_TOAST_ID)
+	{
+		uint64		nextToastId;
+
+		memcpy(&nextToastId, XLogRecGetData(record), sizeof(uint64));
+		LWLockAcquire(ToastIdGenLock, LW_EXCLUSIVE);
+		ToastCounter->nextId = nextToastId;
+		ToastCounter->idCount = 0;
+		LWLockRelease(ToastIdGenLock);
+	}
 	else if (info == XLOG_CHECKPOINT_SHUTDOWN)
 	{
 		CheckPoint	checkPoint;
@@ -8452,6 +8492,10 @@ xlog_redo(XLogReaderState *record)
 		TransamVariables->nextOid = checkPoint.nextOid;
 		TransamVariables->oidCount = 0;
 		LWLockRelease(OidGenLock);
+		LWLockAcquire(ToastIdGenLock, LW_EXCLUSIVE);
+		ToastCounter->nextId = checkPoint.nextToastId;
+		ToastCounter->idCount = 0;
+		LWLockRelease(ToastIdGenLock);
 		MultiXactSetNextMXact(checkPoint.nextMulti,
 							  checkPoint.nextMultiOffset);
 
diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.c
index cc03f0706e9c..bb0337d37201 100644
--- a/src/backend/replication/logical/decode.c
+++ b/src/backend/replication/logical/decode.c
@@ -188,6 +188,7 @@ xlog_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
 		case XLOG_FPI:
 		case XLOG_OVERWRITE_CONTRECORD:
 		case XLOG_CHECKPOINT_REDO:
+		case XLOG_NEXT_TOAST_ID:
 			break;
 		default:
 			elog(ERROR, "unexpected RM_XLOG_ID record type: %u", info);
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index 2fa045e6b0f6..9102c267d7b0 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -20,6 +20,7 @@
 #include "access/nbtree.h"
 #include "access/subtrans.h"
 #include "access/syncscan.h"
+#include "access/toast_counter.h"
 #include "access/transam.h"
 #include "access/twophase.h"
 #include "access/xlogprefetcher.h"
@@ -119,6 +120,7 @@ CalculateShmemSize(int *num_semaphores)
 	size = add_size(size, ProcGlobalShmemSize());
 	size = add_size(size, XLogPrefetchShmemSize());
 	size = add_size(size, VarsupShmemSize());
+	size = add_size(size, ToastCounterShmemSize());
 	size = add_size(size, XLOGShmemSize());
 	size = add_size(size, XLogRecoveryShmemSize());
 	size = add_size(size, CLOGShmemSize());
@@ -280,8 +282,9 @@ CreateOrAttachShmemStructs(void)
 	DSMRegistryShmemInit();
 
 	/*
-	 * Set up xlog, clog, and buffers
+	 * Set up TOAST counter, xlog, clog, and buffers
 	 */
+	ToastCounterShmemInit();
 	VarsupShmemInit();
 	XLOGShmemInit();
 	XLogPrefetchShmemInit();
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 0be307d2ca04..a36969ac6659 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -352,6 +352,7 @@ DSMRegistry	"Waiting to read or update the dynamic shared memory registry."
 InjectionPoint	"Waiting to read or update information related to injection points."
 SerialControl	"Waiting to read or update shared <filename>pg_serial</filename> state."
 AioWorkerSubmissionQueue	"Waiting to access AIO worker submission queue."
+ToastIdGen	"Waiting to allocate a new TOAST value ID."
 
 #
 # END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE)
diff --git a/src/backend/utils/misc/pg_controldata.c b/src/backend/utils/misc/pg_controldata.c
index 6d036e3bf328..e4abf8593b8d 100644
--- a/src/backend/utils/misc/pg_controldata.c
+++ b/src/backend/utils/misc/pg_controldata.c
@@ -69,8 +69,8 @@ pg_control_system(PG_FUNCTION_ARGS)
 Datum
 pg_control_checkpoint(PG_FUNCTION_ARGS)
 {
-	Datum		values[18];
-	bool		nulls[18];
+	Datum		values[19];
+	bool		nulls[19];
 	TupleDesc	tupdesc;
 	HeapTuple	htup;
 	ControlFileData *ControlFile;
@@ -130,30 +130,33 @@ pg_control_checkpoint(PG_FUNCTION_ARGS)
 	values[9] = TransactionIdGetDatum(ControlFile->checkPointCopy.nextMultiOffset);
 	nulls[9] = false;
 
-	values[10] = TransactionIdGetDatum(ControlFile->checkPointCopy.oldestXid);
+	values[10] = UInt64GetDatum(ControlFile->checkPointCopy.nextToastId);
 	nulls[10] = false;
 
-	values[11] = ObjectIdGetDatum(ControlFile->checkPointCopy.oldestXidDB);
+	values[11] = TransactionIdGetDatum(ControlFile->checkPointCopy.oldestXid);
 	nulls[11] = false;
 
-	values[12] = TransactionIdGetDatum(ControlFile->checkPointCopy.oldestActiveXid);
+	values[12] = ObjectIdGetDatum(ControlFile->checkPointCopy.oldestXidDB);
 	nulls[12] = false;
 
-	values[13] = TransactionIdGetDatum(ControlFile->checkPointCopy.oldestMulti);
+	values[13] = TransactionIdGetDatum(ControlFile->checkPointCopy.oldestActiveXid);
 	nulls[13] = false;
 
-	values[14] = ObjectIdGetDatum(ControlFile->checkPointCopy.oldestMultiDB);
+	values[14] = TransactionIdGetDatum(ControlFile->checkPointCopy.oldestMulti);
 	nulls[14] = false;
 
-	values[15] = TransactionIdGetDatum(ControlFile->checkPointCopy.oldestCommitTsXid);
+	values[15] = ObjectIdGetDatum(ControlFile->checkPointCopy.oldestMultiDB);
 	nulls[15] = false;
 
-	values[16] = TransactionIdGetDatum(ControlFile->checkPointCopy.newestCommitTsXid);
+	values[16] = TransactionIdGetDatum(ControlFile->checkPointCopy.oldestCommitTsXid);
 	nulls[16] = false;
 
-	values[17] = TimestampTzGetDatum(time_t_to_timestamptz(ControlFile->checkPointCopy.time));
+	values[17] = TransactionIdGetDatum(ControlFile->checkPointCopy.newestCommitTsXid);
 	nulls[17] = false;
 
+	values[18] = TimestampTzGetDatum(time_t_to_timestamptz(ControlFile->checkPointCopy.time));
+	nulls[18] = false;
+
 	htup = heap_form_tuple(tupdesc, values, nulls);
 
 	PG_RETURN_DATUM(HeapTupleGetDatum(htup));
diff --git a/src/bin/pg_controldata/pg_controldata.c b/src/bin/pg_controldata/pg_controldata.c
index 10de058ce91f..99200262b57c 100644
--- a/src/bin/pg_controldata/pg_controldata.c
+++ b/src/bin/pg_controldata/pg_controldata.c
@@ -266,6 +266,8 @@ main(int argc, char *argv[])
 		   ControlFile->checkPointCopy.nextMulti);
 	printf(_("Latest checkpoint's NextMultiOffset:  %u\n"),
 		   ControlFile->checkPointCopy.nextMultiOffset);
+	printf(_("Latest checkpoint's NextToastID:      %" PRIu64 "\n"),
+		   ControlFile->checkPointCopy.nextToastId);
 	printf(_("Latest checkpoint's oldestXID:        %u\n"),
 		   ControlFile->checkPointCopy.oldestXid);
 	printf(_("Latest checkpoint's oldestXID's DB:   %u\n"),
diff --git a/src/bin/pg_resetwal/pg_resetwal.c b/src/bin/pg_resetwal/pg_resetwal.c
index e876f35f38ed..bb324c710911 100644
--- a/src/bin/pg_resetwal/pg_resetwal.c
+++ b/src/bin/pg_resetwal/pg_resetwal.c
@@ -45,6 +45,7 @@
 
 #include "access/heaptoast.h"
 #include "access/multixact.h"
+#include "access/toast_counter.h"
 #include "access/transam.h"
 #include "access/xlog.h"
 #include "access/xlog_internal.h"
@@ -686,6 +687,7 @@ GuessControlValues(void)
 	ControlFile.checkPointCopy.nextOid = FirstGenbkiObjectId;
 	ControlFile.checkPointCopy.nextMulti = FirstMultiXactId;
 	ControlFile.checkPointCopy.nextMultiOffset = 0;
+	ControlFile.checkPointCopy.nextToastId = FirstToastId;
 	ControlFile.checkPointCopy.oldestXid = FirstNormalTransactionId;
 	ControlFile.checkPointCopy.oldestXidDB = InvalidOid;
 	ControlFile.checkPointCopy.oldestMulti = FirstMultiXactId;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 74a16af04ad3..73e93a7b0ab3 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -28160,6 +28160,11 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
        <entry><type>xid</type></entry>
       </row>
 
+      <row>
+       <entry><structfield>next_toast_id</structfield></entry>
+       <entry><type>bigint</type></entry>
+      </row>
+
       <row>
        <entry><structfield>oldest_xid</structfield></entry>
        <entry><type>xid</type></entry>
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 995dc1f28208..d6bf4e47991b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3055,6 +3055,7 @@ TmFromChar
 TmToChar
 ToastAttrInfo
 ToastCompressionId
+ToastCounterData
 ToastTupleContext
 ToastedAttribute
 TocEntry
-- 
2.50.0

v3-0009-Switch-pg_column_toast_chunk_id-return-value-from.patchtext/x-diff; charset=us-asciiDownload
From f9f97e686a49b4d453a5771e63bc8106835665ee Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 19 Jun 2025 10:11:40 +0900
Subject: [PATCH v3 09/14] Switch pg_column_toast_chunk_id() return value from
 oid to bigint

This is required for a follow-up patch that will add support for 8-byte
TOAST values, with this function being changed so as it is able to
support the largest TOAST value type available.
---
 src/include/catalog/pg_proc.dat | 2 +-
 src/backend/utils/adt/varlena.c | 2 +-
 doc/src/sgml/func.sgml          | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3dd547641065..b8852cd8c581 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7735,7 +7735,7 @@
   proargtypes => 'any', prosrc => 'pg_column_compression' },
 { oid => '6316', descr => 'chunk ID of on-disk TOASTed value',
   proname => 'pg_column_toast_chunk_id', provolatile => 's',
-  prorettype => 'oid', proargtypes => 'any',
+  prorettype => 'int8', proargtypes => 'any',
   prosrc => 'pg_column_toast_chunk_id' },
 { oid => '2322',
   descr => 'total disk space usage for the specified tablespace',
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index d76386407a08..26c720449f7b 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -4249,7 +4249,7 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 
 	toast_valueid = toast_external_info_get_value(attr);
 
-	PG_RETURN_OID(toast_valueid);
+	PG_RETURN_UINT64(toast_valueid);
 }
 
 /*
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 73e93a7b0ab3..fd1b2565d510 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30132,7 +30132,7 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
          <primary>pg_column_toast_chunk_id</primary>
         </indexterm>
         <function>pg_column_toast_chunk_id</function> ( <type>"any"</type> )
-        <returnvalue>oid</returnvalue>
+        <returnvalue>bigint</returnvalue>
        </para>
        <para>
         Shows the <structfield>chunk_id</structfield> of an on-disk
-- 
2.50.0

v3-0010-Add-support-for-bigint-TOAST-values.patchtext/x-diff; charset=us-asciiDownload
From 2d2e7bac198d459f9b3f8feb51974c8545f49066 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 1 Aug 2025 17:17:51 +0900
Subject: [PATCH v3 10/14] Add support for bigint TOAST values

This commit adds the possibility to define TOAST tables with bigint as
value ID, based on the GUC default_toast_type.  All the external TOAST
pointers still rely on varatt_external and a single vartag, with all the
values inserted in the bigint TOAST tables fed from the existing OID
value generator.  This will be changed in an upcoming patch that adds
more vartag_external types and its associated structures, with the code
being able to use a different external TOAST pointer depending on the
attribute type of chunk_id in TOAST relations.

All the changes done here are mechanical, with all the TOAST code able
to do chunk ID lookups based on the two types supported.

XXX: Catalog version bump required.
---
 src/backend/access/common/toast_internals.c | 67 +++++++++++++++------
 src/backend/access/heap/heaptoast.c         | 20 ++++--
 src/backend/catalog/toasting.c              | 32 +++++++++-
 doc/src/sgml/storage.sgml                   |  7 ++-
 contrib/amcheck/verify_heapam.c             | 19 ++++--
 5 files changed, 115 insertions(+), 30 deletions(-)

diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 31cb02a48e15..663af02e4634 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -18,6 +18,7 @@
 #include "access/heapam.h"
 #include "access/heaptoast.h"
 #include "access/table.h"
+#include "access/toast_counter.h"
 #include "access/toast_external.h"
 #include "access/toast_internals.h"
 #include "access/xact.h"
@@ -26,6 +27,7 @@
 #include "utils/fmgroids.h"
 #include "utils/rel.h"
 #include "utils/snapmgr.h"
+#include "utils/lsyscache.h"
 
 static bool toastrel_valueid_exists(Relation toastrel, uint64 valueid);
 static bool toastid_valueid_exists(Oid toastrelid, uint64 valueid);
@@ -146,8 +148,10 @@ toast_save_datum(Relation rel, Datum value,
 	int			validIndex;
 	const toast_external_info *info;
 	uint8		tag = VARTAG_INDIRECT;	/* init value does not matter */
+	Oid			toast_typid = get_atttype(rel->rd_rel->reltoastrelid, 1);
 
 	Assert(!VARATT_IS_EXTERNAL(value));
+	Assert(OidIsValid(toast_typid));
 
 	/*
 	 * Open the toast relation and its indexes.  We can use the index to check
@@ -238,20 +242,23 @@ toast_save_datum(Relation rel, Datum value,
 	info = toast_external_get_info(tag);
 
 	/*
-	 * Choose an OID to use as the value ID for this toast value.
+	 * Choose a new value to use as the value ID for this toast value, be it
+	 * for OID or int8-based TOAST relations.
 	 *
-	 * Normally we just choose an unused OID within the toast table.  But
+	 * Normally we just choose an unused value within the toast table.  But
 	 * during table-rewriting operations where we are preserving an existing
-	 * toast table OID, we want to preserve toast value OIDs too.  So, if
+	 * toast table OID, we want to preserve toast value IDs too.  So, if
 	 * rd_toastoid is set and we had a prior external value from that same
 	 * toast table, re-use its value ID.  If we didn't have a prior external
 	 * value (which is a corner case, but possible if the table's attstorage
 	 * options have been changed), we have to pick a value ID that doesn't
-	 * conflict with either new or existing toast value OIDs.
+	 * conflict with either new or existing toast value IDs.  If the TOAST
+	 * table uses 8-byte value IDs, we should not really care much about
+	 * that.
 	 */
 	if (!OidIsValid(rel->rd_toastoid))
 	{
-		/* normal case: just choose an unused OID */
+		/* normal case: just choose an unused ID */
 		toast_pointer.value =
 			info->get_new_value(toastrel,
 								RelationGetRelid(toastidxs[validIndex]),
@@ -270,7 +277,7 @@ toast_save_datum(Relation rel, Datum value,
 
 			if (old_toast_pointer.toastrelid == rel->rd_toastoid)
 			{
-				/* This value came from the old toast table; reuse its OID */
+				/* This value came from the old toast table; reuse its ID */
 				toast_pointer.value = old_toast_pointer.value;
 
 				/*
@@ -301,8 +308,8 @@ toast_save_datum(Relation rel, Datum value,
 		if (toast_pointer.value == InvalidToastId)
 		{
 			/*
-			 * new value; must choose an OID that doesn't conflict in either
-			 * old or new toast table
+			 * new value; must choose a value that doesn't conflict in either
+			 * old or new toast table.
 			 */
 			do
 			{
@@ -318,7 +325,10 @@ toast_save_datum(Relation rel, Datum value,
 	/*
 	 * Initialize constant parts of the tuple data
 	 */
-	t_values[0] = ObjectIdGetDatum(toast_pointer.value);
+	if (toast_typid == OIDOID)
+		t_values[0] = ObjectIdGetDatum(toast_pointer.value);
+	else if (toast_typid == INT8OID)
+		t_values[0] = Int64GetDatum(toast_pointer.value);
 	t_values[2] = PointerGetDatum(&chunk_data);
 	t_isnull[0] = false;
 	t_isnull[1] = false;
@@ -425,6 +435,7 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	HeapTuple	toasttup;
 	int			num_indexes;
 	int			validIndex;
+	Oid			toast_typid;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		return;
@@ -436,6 +447,8 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	 * Open the toast relation and its indexes
 	 */
 	toastrel = table_open(toast_pointer.toastrelid, RowExclusiveLock);
+	toast_typid = TupleDescAttr(toastrel->rd_att, 0)->atttypid;
+	Assert(toast_typid == OIDOID || toast_typid == INT8OID);
 
 	/* Fetch valid relation used for process */
 	validIndex = toast_open_indexes(toastrel,
@@ -446,10 +459,18 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	/*
 	 * Setup a scan key to find chunks with matching va_valueid
 	 */
-	ScanKeyInit(&toastkey,
-				(AttrNumber) 1,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(toast_pointer.value));
+	if (toast_typid == OIDOID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(toast_pointer.value));
+	else if (toast_typid == INT8OID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_INT8EQ,
+					Int64GetDatum(toast_pointer.value));
+	else
+		Assert(false);
 
 	/*
 	 * Find all the chunks.  (We don't actually care whether we see them in
@@ -496,6 +517,7 @@ toastrel_valueid_exists(Relation toastrel, uint64 valueid)
 	int			num_indexes;
 	int			validIndex;
 	Relation   *toastidxs;
+	Oid			toast_typid;
 
 	/* Fetch a valid index relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -503,13 +525,24 @@ toastrel_valueid_exists(Relation toastrel, uint64 valueid)
 									&toastidxs,
 									&num_indexes);
 
+	toast_typid = TupleDescAttr(toastrel->rd_att, 0)->atttypid;
+	Assert(toast_typid == OIDOID || toast_typid == INT8OID);
+
 	/*
 	 * Setup a scan key to find chunks with matching va_valueid
 	 */
-	ScanKeyInit(&toastkey,
-				(AttrNumber) 1,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(valueid));
+	if (toast_typid == OIDOID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(valueid));
+	else if (toast_typid == INT8OID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_INT8EQ,
+					Int64GetDatum(valueid));
+	else
+		Assert(false);
 
 	/*
 	 * Is there any such chunk?
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index 0c716be3860f..3238d21830ff 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -654,6 +654,7 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 	int32		max_chunk_size;
 	const toast_external_info *info;
 	uint8		tag = VARTAG_INDIRECT;  /* init value does not matter */
+	Oid			toast_typid;
 
 	/* Look for the valid index of toast relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -667,16 +668,27 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 
 	max_chunk_size = info->maximum_chunk_size;
 
+	toast_typid = TupleDescAttr(toastrel->rd_att, 0)->atttypid;
+	Assert(toast_typid == OIDOID || toast_typid == INT8OID);
+
 	totalchunks = ((attrsize - 1) / max_chunk_size) + 1;
 	startchunk = sliceoffset / max_chunk_size;
 	endchunk = (sliceoffset + slicelength - 1) / max_chunk_size;
 	Assert(endchunk <= totalchunks);
 
 	/* Set up a scan key to fetch from the index. */
-	ScanKeyInit(&toastkey[0],
-				(AttrNumber) 1,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(valueid));
+	if (toast_typid == OIDOID)
+		ScanKeyInit(&toastkey[0],
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(valueid));
+	else if (toast_typid == INT8OID)
+		ScanKeyInit(&toastkey[0],
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_INT8EQ,
+					Int64GetDatum(valueid));
+	else
+		Assert(false);
 
 	/*
 	 * No additional condition if fetching all chunks. Otherwise, use an
diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c
index e595cb61b375..27295866c490 100644
--- a/src/backend/catalog/toasting.c
+++ b/src/backend/catalog/toasting.c
@@ -32,6 +32,7 @@
 #include "nodes/makefuncs.h"
 #include "utils/fmgroids.h"
 #include "utils/rel.h"
+#include "utils/lsyscache.h"
 #include "utils/syscache.h"
 
 /* GUC support */
@@ -149,6 +150,7 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	int16		coloptions[2];
 	ObjectAddress baseobject,
 				toastobject;
+	Oid			toast_typid = InvalidOid;
 
 	/*
 	 * Is it already toasted?
@@ -204,11 +206,34 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	snprintf(toast_idxname, sizeof(toast_idxname),
 			 "pg_toast_%u_index", relOid);
 
+	/*
+	 * Determine the type OID to use for the value.  If OIDOldToast is
+	 * defined, we need to rely on the existing table for the job because
+	 * we do not want to create an inconsistent relation that would conflict
+	 * with the parent and break the world.
+	 */
+	if (!OidIsValid(OIDOldToast))
+	{
+		if (default_toast_type == TOAST_TYPE_OID)
+			toast_typid = OIDOID;
+		else if (default_toast_type == TOAST_TYPE_INT8)
+			toast_typid = INT8OID;
+		else
+			Assert(false);
+	}
+	else
+	{
+		/* For the chunk_id type */
+		toast_typid = get_atttype(OIDOldToast, 1);
+		if (!OidIsValid(toast_typid))
+			elog(ERROR, "cache lookup failed for relation %u", OIDOldToast);
+	}
+
 	/* this is pretty painful...  need a tuple descriptor */
 	tupdesc = CreateTemplateTupleDesc(3);
 	TupleDescInitEntry(tupdesc, (AttrNumber) 1,
 					   "chunk_id",
-					   OIDOID,
+					   toast_typid,
 					   -1, 0);
 	TupleDescInitEntry(tupdesc, (AttrNumber) 2,
 					   "chunk_seq",
@@ -316,7 +341,10 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	collationIds[0] = InvalidOid;
 	collationIds[1] = InvalidOid;
 
-	opclassIds[0] = OID_BTREE_OPS_OID;
+	if (toast_typid == OIDOID)
+		opclassIds[0] = OID_BTREE_OPS_OID;
+	else if (toast_typid == INT8OID)
+		opclassIds[0] = INT8_BTREE_OPS_OID;
 	opclassIds[1] = INT4_BTREE_OPS_OID;
 
 	coloptions[0] = 0;
diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml
index f3c6cd8860b5..564783a1c559 100644
--- a/doc/src/sgml/storage.sgml
+++ b/doc/src/sgml/storage.sgml
@@ -419,14 +419,15 @@ most <symbol>TOAST_MAX_CHUNK_SIZE_OID</symbol> bytes (by default this value is c
 so that four chunk rows will fit on a page, making it about 2000 bytes).
 Each chunk is stored as a separate row in the <acronym>TOAST</acronym> table
 belonging to the owning table.  Every
-<acronym>TOAST</acronym> table has the columns <structfield>chunk_id</structfield> (an OID
-identifying the particular <acronym>TOAST</acronym>ed value),
+<acronym>TOAST</acronym> table has the columns
+<structfield>chunk_id</structfield> (an OID or an 8-byte integer identifying
+the particular <acronym>TOAST</acronym>ed value),
 <structfield>chunk_seq</structfield> (a sequence number for the chunk within its value),
 and <structfield>chunk_data</structfield> (the actual data of the chunk).  A unique index
 on <structfield>chunk_id</structfield> and <structfield>chunk_seq</structfield> provides fast
 retrieval of the values.  A pointer datum representing an out-of-line on-disk
 <acronym>TOAST</acronym>ed value therefore needs to store the OID of the
-<acronym>TOAST</acronym> table in which to look and the OID of the specific value
+<acronym>TOAST</acronym> table in which to look and the specific value
 (its <structfield>chunk_id</structfield>).  For convenience, pointer datums also store the
 logical datum size (original uncompressed data length), physical stored size
 (different if compression was applied), and the compression method used, if
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 11c4507ae6e2..833811c75437 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1880,6 +1880,9 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	int32		last_chunk_seq;
 	uint64		toast_valueid;
 	int32		max_chunk_size;
+	Oid			toast_typid;
+
+	toast_typid = TupleDescAttr(ctx->toast_rel->rd_att, 0)->atttypid;
 
 	extsize = ta->toast_pointer.extsize;
 
@@ -1889,10 +1892,18 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	/*
 	 * Setup a scan key to find chunks in toast table with matching va_valueid
 	 */
-	ScanKeyInit(&toastkey,
-				(AttrNumber) 1,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(ta->toast_pointer.value));
+	if (toast_typid == OIDOID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(ta->toast_pointer.value));
+	else if (toast_typid == INT8OID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_INT8EQ,
+					Int64GetDatum(ta->toast_pointer.value));
+	else
+		Assert(false);
 
 	/*
 	 * Check if any chunks for this toasted object exist in the toast table,
-- 
2.50.0

v3-0011-Add-tests-for-TOAST-relations-with-bigint-as-valu.patchtext/x-diff; charset=us-asciiDownload
From 35ec26478880efbff8eeac230f0af3cb057d345c Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 19 Jun 2025 11:15:48 +0900
Subject: [PATCH v3 11/14] Add tests for TOAST relations with bigint as value
 type

This adds coverage for relations created with default_toast_type =
'int8', for external TOAST pointers both compressed and uncompressed.
---
 src/test/regress/expected/strings.out | 238 ++++++++++++++++++++++----
 src/test/regress/sql/strings.sql      | 142 +++++++++++----
 2 files changed, 305 insertions(+), 75 deletions(-)

diff --git a/src/test/regress/expected/strings.out b/src/test/regress/expected/strings.out
index 1bfd33de3f3c..9cf643c9031a 100644
--- a/src/test/regress/expected/strings.out
+++ b/src/test/regress/expected/strings.out
@@ -1945,21 +1945,40 @@ SELECT text 'text' || varchar ' and varchar' AS "Concat text to varchar";
 (1 row)
 
 --
--- test substr with toasted text values
+-- test substr with toasted text values, for all types of TOAST relations
+-- supported.
 --
-CREATE TABLE toasttest(f1 text);
-insert into toasttest values(repeat('1234567890',10000));
-insert into toasttest values(repeat('1234567890',10000));
+SET default_toast_type = 'oid';
+CREATE TABLE toasttest_oid(f1 text);
+SET default_toast_type = 'int8';
+CREATE TABLE toasttest_int8(f1 text);
+RESET default_toast_type;
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_int8 values(repeat('1234567890',10000));
+insert into toasttest_int8 values(repeat('1234567890',10000));
 --
 -- Ensure that some values are uncompressed, to test the faster substring
 -- operation used in that case
 --
-alter table toasttest alter column f1 set storage external;
-insert into toasttest values(repeat('1234567890',10000));
-insert into toasttest values(repeat('1234567890',10000));
+alter table toasttest_oid alter column f1 set storage external;
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+alter table toasttest_int8 alter column f1 set storage external;
+insert into toasttest_int8 values(repeat('1234567890',10000));
+insert into toasttest_int8 values(repeat('1234567890',10000));
 -- If the starting position is zero or less, then return from the start of the string
 -- adjusting the length to be consistent with the "negative start" per SQL.
-SELECT substr(f1, -1, 5) from toasttest;
+SELECT substr(f1, -1, 5) from toasttest_oid;
+ substr 
+--------
+ 123
+ 123
+ 123
+ 123
+(4 rows)
+
+SELECT substr(f1, -1, 5) from toasttest_int8;
  substr 
 --------
  123
@@ -1969,11 +1988,22 @@ SELECT substr(f1, -1, 5) from toasttest;
 (4 rows)
 
 -- If the length is less than zero, an ERROR is thrown.
-SELECT substr(f1, 5, -1) from toasttest;
+SELECT substr(f1, 5, -1) from toasttest_oid;
+ERROR:  negative substring length not allowed
+SELECT substr(f1, 5, -1) from toasttest_int8;
 ERROR:  negative substring length not allowed
 -- If no third argument (length) is provided, the length to the end of the
 -- string is assumed.
-SELECT substr(f1, 99995) from toasttest;
+SELECT substr(f1, 99995) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995) from toasttest_int8;
  substr 
 --------
  567890
@@ -1984,7 +2014,7 @@ SELECT substr(f1, 99995) from toasttest;
 
 -- If start plus length is > string length, the result is truncated to
 -- string length
-SELECT substr(f1, 99995, 10) from toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
  substr 
 --------
  567890
@@ -1993,50 +2023,108 @@ SELECT substr(f1, 99995, 10) from toasttest;
  567890
 (4 rows)
 
-TRUNCATE TABLE toasttest;
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
+SELECT substr(f1, 99995, 10) from toasttest_int8;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+-- TRUNCATE cases for TOAST relations with OID values.
+TRUNCATE TABLE toasttest_oid;
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
 -- expect >0 blocks
 SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
-  FROM pg_class where relname = 'toasttest';
+  FROM pg_class where relname = 'toasttest_oid';
  is_empty 
 ----------
  f
 (1 row)
 
-TRUNCATE TABLE toasttest;
-ALTER TABLE toasttest set (toast_tuple_target = 4080);
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
+TRUNCATE TABLE toasttest_oid;
+ALTER TABLE toasttest_oid set (toast_tuple_target = 4080);
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
 -- expect 0 blocks
 SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
-  FROM pg_class where relname = 'toasttest';
+  FROM pg_class where relname = 'toasttest_oid';
  is_empty 
 ----------
  t
 (1 row)
 
-DROP TABLE toasttest;
+DROP TABLE toasttest_oid;
+-- TRUNCATE cases for TOAST relation with int8 values.
+TRUNCATE TABLE toasttest_int8;
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+-- expect >0 blocks
+SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
+  FROM pg_class where relname = 'toasttest_int8';
+ is_empty 
+----------
+ f
+(1 row)
+
+TRUNCATE TABLE toasttest_int8;
+ALTER TABLE toasttest_int8 set (toast_tuple_target = 4080);
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+-- expect 0 blocks
+SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
+  FROM pg_class where relname = 'toasttest_int8';
+ is_empty 
+----------
+ t
+(1 row)
+
+DROP TABLE toasttest_int8;
 --
--- test substr with toasted bytea values
+-- test substr with toasted bytea values, for all types of TOAST relations
+-- supported.
 --
-CREATE TABLE toasttest(f1 bytea);
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
+SET default_toast_type = 'oid';
+CREATE TABLE toasttest_oid(f1 bytea);
+SET default_toast_type = 'int8';
+CREATE TABLE toasttest_int8(f1 bytea);
+RESET default_toast_type;
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_int8 values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_int8 values(decode(repeat('1234567890',10000),'escape'));
 --
 -- Ensure that some values are uncompressed, to test the faster substring
 -- operation used in that case
 --
-alter table toasttest alter column f1 set storage external;
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
+alter table toasttest_oid alter column f1 set storage external;
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+alter table toasttest_int8 alter column f1 set storage external;
+insert into toasttest_int8 values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_int8 values(decode(repeat('1234567890',10000),'escape'));
 -- If the starting position is zero or less, then return from the start of the string
 -- adjusting the length to be consistent with the "negative start" per SQL.
-SELECT substr(f1, -1, 5) from toasttest;
+SELECT substr(f1, -1, 5) from toasttest_oid;
+ substr 
+--------
+ 123
+ 123
+ 123
+ 123
+(4 rows)
+
+SELECT substr(f1, -1, 5) from toasttest_int8;
  substr 
 --------
  123
@@ -2046,11 +2134,22 @@ SELECT substr(f1, -1, 5) from toasttest;
 (4 rows)
 
 -- If the length is less than zero, an ERROR is thrown.
-SELECT substr(f1, 5, -1) from toasttest;
+SELECT substr(f1, 5, -1) from toasttest_oid;
+ERROR:  negative substring length not allowed
+SELECT substr(f1, 5, -1) from toasttest_int8;
 ERROR:  negative substring length not allowed
 -- If no third argument (length) is provided, the length to the end of the
 -- string is assumed.
-SELECT substr(f1, 99995) from toasttest;
+SELECT substr(f1, 99995) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995) from toasttest_int8;
  substr 
 --------
  567890
@@ -2061,7 +2160,7 @@ SELECT substr(f1, 99995) from toasttest;
 
 -- If start plus length is > string length, the result is truncated to
 -- string length
-SELECT substr(f1, 99995, 10) from toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
  substr 
 --------
  567890
@@ -2070,7 +2169,72 @@ SELECT substr(f1, 99995, 10) from toasttest;
  567890
 (4 rows)
 
-DROP TABLE toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_int8;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+-- A relation rewrite leaves the TOAST value attributes unchanged.
+VACUUM FULL toasttest_oid;
+VACUUM FULL toasttest_int8;
+SELECT c1.relname, a.atttypid::regtype
+  FROM pg_attribute AS a,
+       pg_class AS c1,
+       pg_class AS c2
+  WHERE
+       c1.relname IN ('toasttest_oid', 'toasttest_int8') AND
+       c1.reltoastrelid = c2.oid AND
+       a.attrelid = c2.oid AND
+       a.attname = 'chunk_id'
+  ORDER BY c1.relname COLLATE "C";
+    relname     | atttypid 
+----------------+----------
+ toasttest_int8 | bigint
+ toasttest_oid  | oid
+(2 rows)
+
+-- Check that data slices are still accessible.
+SELECT substr(f1, 99995) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995) from toasttest_int8;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995, 10) from toasttest_int8;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+DROP TABLE toasttest_oid, toasttest_int8;
 -- test internally compressing datums
 -- this tests compressing a datum to a very small size which exercises a
 -- corner case in packed-varlena handling: even though small, the compressed
diff --git a/src/test/regress/sql/strings.sql b/src/test/regress/sql/strings.sql
index 92c445c24396..d4606bc04fe2 100644
--- a/src/test/regress/sql/strings.sql
+++ b/src/test/regress/sql/strings.sql
@@ -553,89 +553,155 @@ SELECT text 'text' || char(20) ' and characters' AS "Concat text to char";
 SELECT text 'text' || varchar ' and varchar' AS "Concat text to varchar";
 
 --
--- test substr with toasted text values
+-- test substr with toasted text values, for all types of TOAST relations
+-- supported.
 --
-CREATE TABLE toasttest(f1 text);
+SET default_toast_type = 'oid';
+CREATE TABLE toasttest_oid(f1 text);
+SET default_toast_type = 'int8';
+CREATE TABLE toasttest_int8(f1 text);
+RESET default_toast_type;
 
-insert into toasttest values(repeat('1234567890',10000));
-insert into toasttest values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_int8 values(repeat('1234567890',10000));
+insert into toasttest_int8 values(repeat('1234567890',10000));
 
 --
 -- Ensure that some values are uncompressed, to test the faster substring
 -- operation used in that case
 --
-alter table toasttest alter column f1 set storage external;
-insert into toasttest values(repeat('1234567890',10000));
-insert into toasttest values(repeat('1234567890',10000));
+alter table toasttest_oid alter column f1 set storage external;
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+alter table toasttest_int8 alter column f1 set storage external;
+insert into toasttest_int8 values(repeat('1234567890',10000));
+insert into toasttest_int8 values(repeat('1234567890',10000));
 
 -- If the starting position is zero or less, then return from the start of the string
 -- adjusting the length to be consistent with the "negative start" per SQL.
-SELECT substr(f1, -1, 5) from toasttest;
+SELECT substr(f1, -1, 5) from toasttest_oid;
+SELECT substr(f1, -1, 5) from toasttest_int8;
 
 -- If the length is less than zero, an ERROR is thrown.
-SELECT substr(f1, 5, -1) from toasttest;
+SELECT substr(f1, 5, -1) from toasttest_oid;
+SELECT substr(f1, 5, -1) from toasttest_int8;
 
 -- If no third argument (length) is provided, the length to the end of the
 -- string is assumed.
-SELECT substr(f1, 99995) from toasttest;
+SELECT substr(f1, 99995) from toasttest_oid;
+SELECT substr(f1, 99995) from toasttest_int8;
 
 -- If start plus length is > string length, the result is truncated to
 -- string length
-SELECT substr(f1, 99995, 10) from toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+SELECT substr(f1, 99995, 10) from toasttest_int8;
 
-TRUNCATE TABLE toasttest;
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
+-- TRUNCATE cases for TOAST relations with OID values.
+TRUNCATE TABLE toasttest_oid;
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
 -- expect >0 blocks
 SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
-  FROM pg_class where relname = 'toasttest';
-
-TRUNCATE TABLE toasttest;
-ALTER TABLE toasttest set (toast_tuple_target = 4080);
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
+  FROM pg_class where relname = 'toasttest_oid';
+TRUNCATE TABLE toasttest_oid;
+ALTER TABLE toasttest_oid set (toast_tuple_target = 4080);
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
 -- expect 0 blocks
 SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
-  FROM pg_class where relname = 'toasttest';
+  FROM pg_class where relname = 'toasttest_oid';
+DROP TABLE toasttest_oid;
 
-DROP TABLE toasttest;
+-- TRUNCATE cases for TOAST relation with int8 values.
+TRUNCATE TABLE toasttest_int8;
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+-- expect >0 blocks
+SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
+  FROM pg_class where relname = 'toasttest_int8';
+TRUNCATE TABLE toasttest_int8;
+ALTER TABLE toasttest_int8 set (toast_tuple_target = 4080);
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+-- expect 0 blocks
+SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
+  FROM pg_class where relname = 'toasttest_int8';
+DROP TABLE toasttest_int8;
 
 --
--- test substr with toasted bytea values
+-- test substr with toasted bytea values, for all types of TOAST relations
+-- supported.
 --
-CREATE TABLE toasttest(f1 bytea);
+SET default_toast_type = 'oid';
+CREATE TABLE toasttest_oid(f1 bytea);
+SET default_toast_type = 'int8';
+CREATE TABLE toasttest_int8(f1 bytea);
+RESET default_toast_type;
 
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_int8 values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_int8 values(decode(repeat('1234567890',10000),'escape'));
 
 --
 -- Ensure that some values are uncompressed, to test the faster substring
 -- operation used in that case
 --
-alter table toasttest alter column f1 set storage external;
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
+alter table toasttest_oid alter column f1 set storage external;
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+alter table toasttest_int8 alter column f1 set storage external;
+insert into toasttest_int8 values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_int8 values(decode(repeat('1234567890',10000),'escape'));
 
 -- If the starting position is zero or less, then return from the start of the string
 -- adjusting the length to be consistent with the "negative start" per SQL.
-SELECT substr(f1, -1, 5) from toasttest;
+SELECT substr(f1, -1, 5) from toasttest_oid;
+SELECT substr(f1, -1, 5) from toasttest_int8;
 
 -- If the length is less than zero, an ERROR is thrown.
-SELECT substr(f1, 5, -1) from toasttest;
+SELECT substr(f1, 5, -1) from toasttest_oid;
+SELECT substr(f1, 5, -1) from toasttest_int8;
 
 -- If no third argument (length) is provided, the length to the end of the
 -- string is assumed.
-SELECT substr(f1, 99995) from toasttest;
+SELECT substr(f1, 99995) from toasttest_oid;
+SELECT substr(f1, 99995) from toasttest_int8;
 
 -- If start plus length is > string length, the result is truncated to
 -- string length
-SELECT substr(f1, 99995, 10) from toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+SELECT substr(f1, 99995, 10) from toasttest_int8;
 
-DROP TABLE toasttest;
+-- A relation rewrite leaves the TOAST value attributes unchanged.
+VACUUM FULL toasttest_oid;
+VACUUM FULL toasttest_int8;
+SELECT c1.relname, a.atttypid::regtype
+  FROM pg_attribute AS a,
+       pg_class AS c1,
+       pg_class AS c2
+  WHERE
+       c1.relname IN ('toasttest_oid', 'toasttest_int8') AND
+       c1.reltoastrelid = c2.oid AND
+       a.attrelid = c2.oid AND
+       a.attname = 'chunk_id'
+  ORDER BY c1.relname COLLATE "C";
+-- Check that data slices are still accessible.
+SELECT substr(f1, 99995) from toasttest_oid;
+SELECT substr(f1, 99995) from toasttest_int8;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+SELECT substr(f1, 99995, 10) from toasttest_int8;
+
+DROP TABLE toasttest_oid, toasttest_int8;
 
 -- test internally compressing datums
 
-- 
2.50.0

v3-0012-Add-support-for-TOAST-table-types-in-pg_dump-and-.patchtext/x-diff; charset=us-asciiDownload
From 976bfe270f4112a3e63e2d8349c30a62a1c02c84 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 19 Jun 2025 11:51:52 +0900
Subject: [PATCH v3 12/14] Add support for TOAST table types in pg_dump and
 pg_restore

This includes the possibility to perform binary upgrades with TOAST
table types applied to a new cluster, relying on SET commands based on
default_toast_type to apply one type of TOAST table or the other.

Some tests are included, this is a pretty mechanical change.

Dump format is bumped to 1.17 due to the addition of the TOAST table
type in the custom format.
---
 src/bin/pg_dump/pg_backup.h          |  2 +
 src/bin/pg_dump/pg_backup_archiver.c | 69 +++++++++++++++++++++++++++-
 src/bin/pg_dump/pg_backup_archiver.h |  6 ++-
 src/bin/pg_dump/pg_dump.c            | 21 +++++++++
 src/bin/pg_dump/pg_dump.h            |  1 +
 src/bin/pg_dump/pg_dumpall.c         |  5 ++
 src/bin/pg_dump/pg_restore.c         |  4 ++
 src/bin/pg_dump/t/002_pg_dump.pl     | 35 ++++++++++++++
 doc/src/sgml/ref/pg_dump.sgml        | 12 +++++
 doc/src/sgml/ref/pg_dumpall.sgml     | 12 +++++
 doc/src/sgml/ref/pg_restore.sgml     | 12 +++++
 11 files changed, 177 insertions(+), 2 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 4ebef1e86445..f99d5f0d3d1b 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -99,6 +99,7 @@ typedef struct _restoreOptions
 	int			noOwner;		/* Don't try to match original object owner */
 	int			noTableAm;		/* Don't issue table-AM-related commands */
 	int			noTablespace;	/* Don't issue tablespace-related commands */
+	int			noToastType;	/* Don't issue TOAST-type-related commands */
 	int			disable_triggers;	/* disable triggers during data-only
 									 * restore */
 	int			use_setsessauth;	/* Use SET SESSION AUTHORIZATION commands
@@ -192,6 +193,7 @@ typedef struct _dumpOptions
 	int			disable_triggers;
 	int			outputNoTableAm;
 	int			outputNoTablespaces;
+	int			outputNoToastType;
 	int			use_setsessauth;
 	int			enable_row_security;
 	int			load_via_partition_root;
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index dce88f040ace..5690c0850559 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -185,6 +185,7 @@ dumpOptionsFromRestoreOptions(RestoreOptions *ropt)
 	dopt->outputNoOwner = ropt->noOwner;
 	dopt->outputNoTableAm = ropt->noTableAm;
 	dopt->outputNoTablespaces = ropt->noTablespace;
+	dopt->outputNoToastType = ropt->noToastType;
 	dopt->disable_triggers = ropt->disable_triggers;
 	dopt->use_setsessauth = ropt->use_setsessauth;
 	dopt->disable_dollar_quoting = ropt->disable_dollar_quoting;
@@ -1244,6 +1245,7 @@ ArchiveEntry(Archive *AHX, CatalogId catalogId, DumpId dumpId,
 	newToc->namespace = opts->namespace ? pg_strdup(opts->namespace) : NULL;
 	newToc->tablespace = opts->tablespace ? pg_strdup(opts->tablespace) : NULL;
 	newToc->tableam = opts->tableam ? pg_strdup(opts->tableam) : NULL;
+	newToc->toasttype = opts->toasttype ? pg_strdup(opts->toasttype) : NULL;
 	newToc->relkind = opts->relkind;
 	newToc->owner = opts->owner ? pg_strdup(opts->owner) : NULL;
 	newToc->desc = pg_strdup(opts->description);
@@ -2403,6 +2405,7 @@ _allocAH(const char *FileSpec, const ArchiveFormat fmt,
 	AH->currSchema = NULL;		/* ditto */
 	AH->currTablespace = NULL;	/* ditto */
 	AH->currTableAm = NULL;		/* ditto */
+	AH->currToastType = NULL;		/* ditto */
 
 	AH->toc = (TocEntry *) pg_malloc0(sizeof(TocEntry));
 
@@ -2670,6 +2673,7 @@ WriteToc(ArchiveHandle *AH)
 		WriteStr(AH, te->tablespace);
 		WriteStr(AH, te->tableam);
 		WriteInt(AH, te->relkind);
+		WriteStr(AH, te->toasttype);
 		WriteStr(AH, te->owner);
 		WriteStr(AH, "false");
 
@@ -2778,6 +2782,9 @@ ReadToc(ArchiveHandle *AH)
 		if (AH->version >= K_VERS_1_16)
 			te->relkind = ReadInt(AH);
 
+		if (AH->version >= K_VERS_1_17)
+			te->toasttype = ReadStr(AH);
+
 		te->owner = ReadStr(AH);
 		is_supported = true;
 		if (AH->version < K_VERS_1_9)
@@ -3477,6 +3484,9 @@ _reconnectToDB(ArchiveHandle *AH, const char *dbname)
 	free(AH->currTablespace);
 	AH->currTablespace = NULL;
 
+	free(AH->currToastType);
+	AH->currToastType = NULL;
+
 	/* re-establish fixed state */
 	_doSetFixedOutputState(AH);
 }
@@ -3682,6 +3692,56 @@ _selectTableAccessMethod(ArchiveHandle *AH, const char *tableam)
 	AH->currTableAm = pg_strdup(want);
 }
 
+
+/*
+ * Set the proper default_toast_type value for the table.
+ */
+static void
+_selectToastType(ArchiveHandle *AH, const char *toasttype)
+{
+	RestoreOptions *ropt = AH->public.ropt;
+	PQExpBuffer cmd;
+	const char *want,
+			   *have;
+
+	/* do nothing in --no-toast-type mode */
+	if (ropt->noToastType)
+		return;
+
+	have = AH->currToastType;
+	want = toasttype;
+
+	if (!want)
+		return;
+
+	if (have && strcmp(want, have) == 0)
+		return;
+
+	cmd = createPQExpBuffer();
+
+	appendPQExpBuffer(cmd, "SET default_toast_type = %s;", fmtId(toasttype));
+
+	if (RestoringToDB(AH))
+	{
+		PGresult   *res;
+
+		res = PQexec(AH->connection, cmd->data);
+
+		if (!res || PQresultStatus(res) != PGRES_COMMAND_OK)
+			warn_or_exit_horribly(AH,
+								  "could not set \"default_toast_type\": %s",
+								  PQerrorMessage(AH->connection));
+		PQclear(res);
+	}
+	else
+		ahprintf(AH, "%s\n\n", cmd->data);
+
+	destroyPQExpBuffer(cmd);
+
+	free(AH->currToastType);
+	AH->currToastType = pg_strdup(want);
+}
+
 /*
  * Set the proper default table access method for a table without storage.
  * Currently, this is required only for partitioned tables with a table AM.
@@ -3837,13 +3897,16 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, const char *pfx)
 	 * Select owner, schema, tablespace and default AM as necessary. The
 	 * default access method for partitioned tables is handled after
 	 * generating the object definition, as it requires an ALTER command
-	 * rather than SET.
+	 * rather than SET.  Partitioned tables do not have TOAST tables.
 	 */
 	_becomeOwner(AH, te);
 	_selectOutputSchema(AH, te->namespace);
 	_selectTablespace(AH, te->tablespace);
 	if (te->relkind != RELKIND_PARTITIONED_TABLE)
+	{
 		_selectTableAccessMethod(AH, te->tableam);
+		_selectToastType(AH, te->toasttype);
+	}
 
 	/* Emit header comment for item */
 	if (!AH->noTocComments)
@@ -4402,6 +4465,8 @@ restore_toc_entries_prefork(ArchiveHandle *AH, TocEntry *pending_list)
 	AH->currTablespace = NULL;
 	free(AH->currTableAm);
 	AH->currTableAm = NULL;
+	free(AH->currToastType);
+	AH->currToastType = NULL;
 }
 
 /*
@@ -5139,6 +5204,7 @@ CloneArchive(ArchiveHandle *AH)
 	clone->currSchema = NULL;
 	clone->currTableAm = NULL;
 	clone->currTablespace = NULL;
+	clone->currToastType = NULL;
 
 	/* savedPassword must be local in case we change it while connecting */
 	if (clone->savedPassword)
@@ -5198,6 +5264,7 @@ DeCloneArchive(ArchiveHandle *AH)
 	free(AH->currSchema);
 	free(AH->currTablespace);
 	free(AH->currTableAm);
+	free(AH->currToastType);
 	free(AH->savedPassword);
 
 	free(AH);
diff --git a/src/bin/pg_dump/pg_backup_archiver.h b/src/bin/pg_dump/pg_backup_archiver.h
index 325b53fc9bd4..a9f8f75d4382 100644
--- a/src/bin/pg_dump/pg_backup_archiver.h
+++ b/src/bin/pg_dump/pg_backup_archiver.h
@@ -71,10 +71,11 @@
 #define K_VERS_1_16 MAKE_ARCHIVE_VERSION(1, 16, 0)	/* BLOB METADATA entries
 													 * and multiple BLOBS,
 													 * relkind */
+#define K_VERS_1_17 MAKE_ARCHIVE_VERSION(1, 17, 0)	/* TOAST type */
 
 /* Current archive version number (the format we can output) */
 #define K_VERS_MAJOR 1
-#define K_VERS_MINOR 16
+#define K_VERS_MINOR 17
 #define K_VERS_REV 0
 #define K_VERS_SELF MAKE_ARCHIVE_VERSION(K_VERS_MAJOR, K_VERS_MINOR, K_VERS_REV)
 
@@ -325,6 +326,7 @@ struct _archiveHandle
 	char	   *currSchema;		/* current schema, or NULL */
 	char	   *currTablespace; /* current tablespace, or NULL */
 	char	   *currTableAm;	/* current table access method, or NULL */
+	char	   *currToastType;	/* current TOAST type, or NULL */
 
 	/* in --transaction-size mode, this counts objects emitted in cur xact */
 	int			txnCount;
@@ -359,6 +361,7 @@ struct _tocEntry
 	char	   *tablespace;		/* null if not in a tablespace; empty string
 								 * means use database default */
 	char	   *tableam;		/* table access method, only for TABLE tags */
+	char	   *toasttype;		/* TOAST table type, only for TABLE tags */
 	char		relkind;		/* relation kind, only for TABLE tags */
 	char	   *owner;
 	char	   *desc;
@@ -404,6 +407,7 @@ typedef struct _archiveOpts
 	const char *namespace;
 	const char *tablespace;
 	const char *tableam;
+	const char *toasttype;
 	char		relkind;
 	const char *owner;
 	const char *description;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 273117c977c5..ebedbd3e01f5 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -508,6 +508,7 @@ main(int argc, char **argv)
 		{"lock-wait-timeout", required_argument, NULL, 2},
 		{"no-table-access-method", no_argument, &dopt.outputNoTableAm, 1},
 		{"no-tablespaces", no_argument, &dopt.outputNoTablespaces, 1},
+		{"no-toast-type", no_argument, &dopt.outputNoToastType, 1},
 		{"quote-all-identifiers", no_argument, &quote_all_identifiers, 1},
 		{"load-via-partition-root", no_argument, &dopt.load_via_partition_root, 1},
 		{"role", required_argument, NULL, 3},
@@ -1225,6 +1226,7 @@ main(int argc, char **argv)
 	ropt->noOwner = dopt.outputNoOwner;
 	ropt->noTableAm = dopt.outputNoTableAm;
 	ropt->noTablespace = dopt.outputNoTablespaces;
+	ropt->noToastType = dopt.outputNoToastType;
 	ropt->disable_triggers = dopt.disable_triggers;
 	ropt->use_setsessauth = dopt.use_setsessauth;
 	ropt->disable_dollar_quoting = dopt.disable_dollar_quoting;
@@ -1347,6 +1349,7 @@ help(const char *progname)
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
 	printf(_("  --no-toast-compression       do not dump TOAST compression methods\n"));
+	printf(_("  --no-toast-type              do not dump TOAST table type\n"));
 	printf(_("  --no-unlogged-table-data     do not dump unlogged table data\n"));
 	printf(_("  --on-conflict-do-nothing     add ON CONFLICT DO NOTHING to INSERT commands\n"));
 	printf(_("  --quote-all-identifiers      quote all identifiers, even if not key words\n"));
@@ -7111,6 +7114,7 @@ getTables(Archive *fout, int *numTables)
 	int			i_relfrozenxid;
 	int			i_toastfrozenxid;
 	int			i_toastoid;
+	int			i_toasttype;
 	int			i_relminmxid;
 	int			i_toastminmxid;
 	int			i_reloptions;
@@ -7165,6 +7169,14 @@ getTables(Archive *fout, int *numTables)
 						 "ELSE 0 END AS foreignserver, "
 						 "c.relfrozenxid, tc.relfrozenxid AS tfrozenxid, "
 						 "tc.oid AS toid, "
+						 "CASE WHEN c.reltoastrelid <> 0 THEN "
+						 " (SELECT CASE "
+						 "   WHEN a.atttypid::regtype = 'oid'::regtype THEN 'oid'::text "
+						 "   WHEN a.atttypid::regtype = 'bigint'::regtype THEN 'int8'::text "
+						 "   ELSE NULL END"
+						 "  FROM pg_attribute AS a "
+						 "  WHERE a.attrelid = tc.oid AND a.attname = 'chunk_id') "
+						 " ELSE NULL END AS toasttype, "
 						 "tc.relpages AS toastpages, "
 						 "tc.reloptions AS toast_reloptions, "
 						 "d.refobjid AS owning_tab, "
@@ -7335,6 +7347,7 @@ getTables(Archive *fout, int *numTables)
 	i_relfrozenxid = PQfnumber(res, "relfrozenxid");
 	i_toastfrozenxid = PQfnumber(res, "tfrozenxid");
 	i_toastoid = PQfnumber(res, "toid");
+	i_toasttype = PQfnumber(res, "toasttype");
 	i_relminmxid = PQfnumber(res, "relminmxid");
 	i_toastminmxid = PQfnumber(res, "tminmxid");
 	i_reloptions = PQfnumber(res, "reloptions");
@@ -7413,6 +7426,10 @@ getTables(Archive *fout, int *numTables)
 		tblinfo[i].frozenxid = atooid(PQgetvalue(res, i, i_relfrozenxid));
 		tblinfo[i].toast_frozenxid = atooid(PQgetvalue(res, i, i_toastfrozenxid));
 		tblinfo[i].toast_oid = atooid(PQgetvalue(res, i, i_toastoid));
+		if (PQgetisnull(res, i, i_toasttype))
+			tblinfo[i].toast_type = NULL;
+		else
+			tblinfo[i].toast_type = pg_strdup(PQgetvalue(res, i, i_toasttype));
 		tblinfo[i].minmxid = atooid(PQgetvalue(res, i, i_relminmxid));
 		tblinfo[i].toast_minmxid = atooid(PQgetvalue(res, i, i_toastminmxid));
 		tblinfo[i].reloptions = pg_strdup(PQgetvalue(res, i, i_reloptions));
@@ -17874,6 +17891,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 	{
 		char	   *tablespace = NULL;
 		char	   *tableam = NULL;
+		char	   *toasttype = NULL;
 
 		/*
 		 * _selectTablespace() relies on tablespace-enabled objects in the
@@ -17888,12 +17906,15 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 		if (RELKIND_HAS_TABLE_AM(tbinfo->relkind) ||
 			tbinfo->relkind == RELKIND_PARTITIONED_TABLE)
 			tableam = tbinfo->amname;
+		if (OidIsValid(tbinfo->toast_oid))
+			toasttype = tbinfo->toast_type;
 
 		ArchiveEntry(fout, tbinfo->dobj.catId, tbinfo->dobj.dumpId,
 					 ARCHIVE_OPTS(.tag = tbinfo->dobj.name,
 								  .namespace = tbinfo->dobj.namespace->dobj.name,
 								  .tablespace = tablespace,
 								  .tableam = tableam,
+								  .toasttype = toasttype,
 								  .relkind = tbinfo->relkind,
 								  .owner = tbinfo->rolname,
 								  .description = reltypename,
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index dde85ed156cc..26ad5425bd4c 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -325,6 +325,7 @@ typedef struct _tableInfo
 	uint32		frozenxid;		/* table's relfrozenxid */
 	uint32		minmxid;		/* table's relminmxid */
 	Oid			toast_oid;		/* toast table's OID, or 0 if none */
+	char	   *toast_type;		/* toast table type, or NULL if none */
 	uint32		toast_frozenxid;	/* toast table's relfrozenxid, if any */
 	uint32		toast_minmxid;	/* toast table's relminmxid */
 	int			ncheck;			/* # of CHECK expressions */
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 87d10df07c41..81761b2e096c 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -93,6 +93,7 @@ static int	if_exists = 0;
 static int	inserts = 0;
 static int	no_table_access_method = 0;
 static int	no_tablespaces = 0;
+static int	no_toast_type = 0;
 static int	use_setsessauth = 0;
 static int	no_comments = 0;
 static int	no_policies = 0;
@@ -164,6 +165,7 @@ main(int argc, char *argv[])
 		{"lock-wait-timeout", required_argument, NULL, 2},
 		{"no-table-access-method", no_argument, &no_table_access_method, 1},
 		{"no-tablespaces", no_argument, &no_tablespaces, 1},
+		{"no-toast-type", no_argument, &no_tablespaces, 1},
 		{"quote-all-identifiers", no_argument, &quote_all_identifiers, 1},
 		{"load-via-partition-root", no_argument, &load_via_partition_root, 1},
 		{"role", required_argument, NULL, 3},
@@ -449,6 +451,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --no-table-access-method");
 	if (no_tablespaces)
 		appendPQExpBufferStr(pgdumpopts, " --no-tablespaces");
+	if (no_toast_type)
+		appendPQExpBufferStr(pgdumpopts, " --no-toast-type");
 	if (quote_all_identifiers)
 		appendPQExpBufferStr(pgdumpopts, " --quote-all-identifiers");
 	if (load_via_partition_root)
@@ -707,6 +711,7 @@ help(void)
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
 	printf(_("  --no-toast-compression       do not dump TOAST compression methods\n"));
+	printf(_("  --no-toast-type              do not dump TOAST table types\n"));
 	printf(_("  --no-unlogged-table-data     do not dump unlogged table data\n"));
 	printf(_("  --on-conflict-do-nothing     add ON CONFLICT DO NOTHING to INSERT commands\n"));
 	printf(_("  --quote-all-identifiers      quote all identifiers, even if not key words\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index b4e1acdb63fb..1b75ad74508d 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -71,6 +71,7 @@ main(int argc, char **argv)
 	static int	no_data_for_failed_tables = 0;
 	static int	outputNoTableAm = 0;
 	static int	outputNoTablespaces = 0;
+	static int	outputNoToastType = 0;
 	static int	use_setsessauth = 0;
 	static int	no_comments = 0;
 	static int	no_data = 0;
@@ -126,6 +127,7 @@ main(int argc, char **argv)
 		{"no-data-for-failed-tables", no_argument, &no_data_for_failed_tables, 1},
 		{"no-table-access-method", no_argument, &outputNoTableAm, 1},
 		{"no-tablespaces", no_argument, &outputNoTablespaces, 1},
+		{"no-toast-type", no_argument, &outputNoToastType, 1},
 		{"role", required_argument, NULL, 2},
 		{"section", required_argument, NULL, 3},
 		{"strict-names", no_argument, &strict_names, 1},
@@ -417,6 +419,7 @@ main(int argc, char **argv)
 	opts->noDataForFailedTables = no_data_for_failed_tables;
 	opts->noTableAm = outputNoTableAm;
 	opts->noTablespace = outputNoTablespaces;
+	opts->noToastType = outputNoToastType;
 	opts->use_setsessauth = use_setsessauth;
 	opts->no_comments = no_comments;
 	opts->no_policies = no_policies;
@@ -548,6 +551,7 @@ usage(const char *progname)
 	printf(_("  --no-subscriptions           do not restore subscriptions\n"));
 	printf(_("  --no-table-access-method     do not restore table access methods\n"));
 	printf(_("  --no-tablespaces             do not restore tablespace assignments\n"));
+	printf(_("  --no-toast-type              do not restore TOAST table types\n"));
 	printf(_("  --section=SECTION            restore named section (pre-data, data, or post-data)\n"));
 	printf(_("  --statistics-only            restore only the statistics, not schema or data\n"));
 	printf(_("  --strict-names               require table and/or schema include patterns to\n"
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 6c7ec80e271c..871b5e886b50 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -659,6 +659,15 @@ my %pgdump_runs = (
 			'postgres',
 		],
 	},
+	no_toast_type => {
+		dump_cmd => [
+			'pg_dump', '--no-sync',
+			'--file' => "$tempdir/no_toast_type.sql",
+			'--no-toast-type',
+			'--with-statistics',
+			'postgres',
+		],
+	},
 	only_dump_test_schema => {
 		dump_cmd => [
 			'pg_dump', '--no-sync',
@@ -881,6 +890,7 @@ my %full_runs = (
 	no_privs => 1,
 	no_statistics => 1,
 	no_table_access_method => 1,
+	no_toast_type => 1,
 	pg_dumpall_dbprivs => 1,
 	pg_dumpall_exclude => 1,
 	schema_only => 1,
@@ -4941,6 +4951,31 @@ my %tests = (
 		},
 	},
 
+	# Test the case of multiple TOAST table types.
+	'CREATE TABLE regress_toast_type' => {
+		create_order => 13,
+		create_sql => '
+			SET default_toast_type = int8;
+			CREATE TABLE dump_test.regress_toast_type_int8 (col1 text);
+			SET default_toast_type = oid;
+			CREATE TABLE dump_test.regress_toast_type_oid (col1 text);
+			RESET default_toast_type;',
+		regexp => qr/^
+			\QSET default_toast_type = int8;\E
+			(\n(?!SET[^;]+;)[^\n]*)*
+			\n\QCREATE TABLE dump_test.regress_toast_type_int8 (\E
+			\n\s+\Qcol1 text\E
+			\n\);/xm,
+		like => {
+			%full_runs, %dump_test_schema_runs, section_pre_data => 1,
+		},
+		unlike => {
+			exclude_dump_test_schema => 1,
+			no_toast_type => 1,
+			only_dump_measurement => 1,
+		},
+	},
+
 	#
 	# TABLE and MATVIEW stats will end up in SECTION_DATA.
 	# INDEX stats (expression columns only) will end up in SECTION_POST_DATA.
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index 2ae084b5fa6f..d0efc5955505 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -1208,6 +1208,18 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-toast-type</option></term>
+      <listitem>
+       <para>
+        Do not output commands to set <acronym>TOAST</acronym> table
+        types.
+        With this option, all <acronym>TOAST</acronym> tables will be
+        restored with the default type.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-unlogged-table-data</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_dumpall.sgml b/doc/src/sgml/ref/pg_dumpall.sgml
index f4cbc8288e3a..eeb9e6d55372 100644
--- a/doc/src/sgml/ref/pg_dumpall.sgml
+++ b/doc/src/sgml/ref/pg_dumpall.sgml
@@ -550,6 +550,18 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-toast-type</option></term>
+      <listitem>
+       <para>
+        Do not output commands to set <acronym>TOAST</acronym> table
+        types.
+        With this option, all <acronym>TOAST</acronym> tables will be
+        restored with the default type.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-unlogged-table-data</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml
index 2abe05d47e93..d7dcb7b60ffd 100644
--- a/doc/src/sgml/ref/pg_restore.sgml
+++ b/doc/src/sgml/ref/pg_restore.sgml
@@ -796,6 +796,18 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-toast-type</option></term>
+      <listitem>
+       <para>
+        Do not output commands to select <acronym>TOAST</acronym> table
+        types.
+        With this option, all <acronym>TOAST</acronym> tables will be
+        created with whichever type is the default during restore.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
        <term><option>--section=<replaceable class="parameter">sectionname</replaceable></option></term>
        <listitem>
-- 
2.50.0

v3-0013-Add-new-vartag_external-for-8-byte-TOAST-values.patchtext/x-diff; charset=us-asciiDownload
From bc34eae687be3a62dbd73a588e389001042f0ae5 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 8 Jul 2025 09:02:40 +0900
Subject: [PATCH v3 13/14] Add new vartag_external for 8-byte TOAST values

This is a new type of external TOAST pointer, able to be fed 8-byte
TOAST values.  It uses a dedicated vartag_external, which is used when
a TOAST table uses bigint for its chunk_id.

The relevant callbacks are added to toast_external.c.
---
 src/include/access/heaptoast.h                |   8 +-
 src/include/varatt.h                          |  31 +++-
 src/backend/access/common/toast_external.c    | 161 ++++++++++++++++--
 src/backend/access/common/toast_internals.c   |   1 -
 src/backend/access/heap/heaptoast.c           |   4 +-
 .../replication/logical/reorderbuffer.c       |  10 +-
 doc/src/sgml/storage.sgml                     |   6 +-
 contrib/amcheck/verify_heapam.c               |   2 +-
 8 files changed, 203 insertions(+), 20 deletions(-)

diff --git a/src/include/access/heaptoast.h b/src/include/access/heaptoast.h
index 673e96f5488c..a39ad79a5ae9 100644
--- a/src/include/access/heaptoast.h
+++ b/src/include/access/heaptoast.h
@@ -81,6 +81,12 @@
 
 #define EXTERN_TUPLE_MAX_SIZE	MaximumBytesPerTuple(EXTERN_TUPLES_PER_PAGE)
 
+#define TOAST_MAX_CHUNK_SIZE_INT8	\
+	(EXTERN_TUPLE_MAX_SIZE -							\
+	 MAXALIGN(SizeofHeapTupleHeader) -					\
+	 (sizeof(uint32) * 2) -								\
+	 sizeof(int32) -									\
+	 VARHDRSZ)
 #define TOAST_MAX_CHUNK_SIZE_OID	\
 	(EXTERN_TUPLE_MAX_SIZE -							\
 	 MAXALIGN(SizeofHeapTupleHeader) -					\
@@ -89,7 +95,7 @@
 	 VARHDRSZ)
 
 /* Maximum size of chunk possible */
-#define TOAST_MAX_CHUNK_SIZE	TOAST_MAX_CHUNK_SIZE_OID
+#define TOAST_MAX_CHUNK_SIZE	Max(TOAST_MAX_CHUNK_SIZE_INT8, TOAST_MAX_CHUNK_SIZE_OID)
 
 /* ----------
  * heap_toast_insert_or_update -
diff --git a/src/include/varatt.h b/src/include/varatt.h
index 126e5e112a17..46c831092887 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -41,6 +41,29 @@ typedef struct varatt_external_oid
 	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
 }			varatt_external_oid;
 
+/*
+ * struct varatt_external_int8 is a "larger" version of "TOAST pointer",
+ * that uses an 8-byte integer as value.
+ *
+ * This follows the same properties as varatt_external_oid, except that
+ * this is used in TOAST relations with int8 as attribute for chunk_id.
+ */
+typedef struct varatt_external_int8
+{
+	int32		va_rawsize;		/* Original data size (includes header) */
+	uint32		va_extinfo;		/* External saved size (without header) and
+								 * compression method */
+	/*
+	 * Unique ID of value within TOAST table, as two uint32 for alignment
+	 * and padding.
+	 * XXX: think for example about the addition of an extra field for
+	 * meta-data and/or more compression data, even if it's OK here).
+	 */
+	uint32		va_valueid_lo;
+	uint32		va_valueid_hi;
+	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
+}			varatt_external_int8;
+
 /*
  * These macros define the "saved size" portion of va_extinfo.  Its remaining
  * two high-order bits identify the compression method.
@@ -90,6 +113,7 @@ typedef enum vartag_external
 	VARTAG_INDIRECT = 1,
 	VARTAG_EXPANDED_RO = 2,
 	VARTAG_EXPANDED_RW = 3,
+	VARTAG_ONDISK_INT8 = 4,
 	VARTAG_ONDISK_OID = 18
 } vartag_external;
 
@@ -101,6 +125,7 @@ typedef enum vartag_external
 	((tag) == VARTAG_INDIRECT ? sizeof(varatt_indirect) : \
 	 VARTAG_IS_EXPANDED(tag) ? sizeof(varatt_expanded) : \
 	 (tag) == VARTAG_ONDISK_OID ? sizeof(varatt_external_oid) : \
+	 (tag) == VARTAG_ONDISK_INT8 ? sizeof(varatt_external_int8) : \
 	 (AssertMacro(false), 0))
 
 /*
@@ -293,8 +318,10 @@ typedef struct
 #define VARATT_IS_EXTERNAL(PTR)				VARATT_IS_1B_E(PTR)
 #define VARATT_IS_EXTERNAL_ONDISK_OID(PTR) \
 	(VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID)
+#define VARATT_IS_EXTERNAL_ONDISK_INT8(PTR) \
+	(VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_INT8)
 #define VARATT_IS_EXTERNAL_ONDISK(PTR) \
-	(VARATT_IS_EXTERNAL_ONDISK_OID(PTR))
+	(VARATT_IS_EXTERNAL_ONDISK_OID(PTR) || VARATT_IS_EXTERNAL_ONDISK_INT8(PTR))
 #define VARATT_IS_EXTERNAL_INDIRECT(PTR) \
 	(VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_INDIRECT)
 #define VARATT_IS_EXTERNAL_EXPANDED_RO(PTR) \
@@ -338,7 +365,7 @@ typedef struct
 
 /*
  * Same for external Datums; but note argument is a struct
- * varatt_external_oid.
+ * varatt_external_oid or varatt_external_int8.
  */
 #define VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) \
 	((toast_pointer).va_extinfo & VARLENA_EXTSIZE_MASK)
diff --git a/src/backend/access/common/toast_external.c b/src/backend/access/common/toast_external.c
index a17de2fa8b3d..57c3f8084548 100644
--- a/src/backend/access/common/toast_external.c
+++ b/src/backend/access/common/toast_external.c
@@ -14,9 +14,24 @@
 #include "postgres.h"
 
 #include "access/detoast.h"
+#include "access/genam.h"
 #include "access/heaptoast.h"
+#include "access/toast_counter.h"
 #include "access/toast_external.h"
+#include "access/toast_type.h"
 #include "catalog/catalog.h"
+#include "miscadmin.h"
+#include "utils/fmgroids.h"
+#include "utils/snapmgr.h"
+#include "utils/lsyscache.h"
+
+
+/* Callbacks for VARTAG_ONDISK_INT8 */
+static void ondisk_int8_to_external_data(struct varlena *attr,
+										 toast_external_data *data);
+static struct varlena *ondisk_int8_create_external_data(toast_external_data data);
+static uint64 ondisk_int8_get_new_value(Relation toastrel, Oid indexid,
+										AttrNumber attnum);
 
 /* Callbacks for VARTAG_ONDISK_OID */
 static void ondisk_oid_to_external_data(struct varlena *attr,
@@ -26,6 +41,12 @@ static uint64 ondisk_oid_get_new_value(Relation toastrel, Oid indexid,
 									   AttrNumber attnum);
 
 
+/*
+ * Size of an EXTERNAL datum that contains a standard TOAST pointer
+ * (int8 value).
+ */
+#define TOAST_POINTER_INT8_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_int8))
+
 /*
  * Size of an EXTERNAL datum that contains a standard TOAST pointer (OID
  * value).
@@ -46,6 +67,13 @@ static uint64 ondisk_oid_get_new_value(Relation toastrel, Oid indexid,
  * individual fields.
  */
 static const toast_external_info toast_external_infos[TOAST_EXTERNAL_INFO_SIZE] = {
+	[VARTAG_ONDISK_INT8] = {
+		.toast_pointer_size = TOAST_POINTER_INT8_SIZE,
+		.maximum_chunk_size = TOAST_MAX_CHUNK_SIZE_INT8,
+		.to_external_data = ondisk_int8_to_external_data,
+		.create_external_data = ondisk_int8_create_external_data,
+		.get_new_value = ondisk_int8_get_new_value,
+	},
 	[VARTAG_ONDISK_OID] = {
 		.toast_pointer_size = TOAST_POINTER_OID_SIZE,
 		.maximum_chunk_size = TOAST_MAX_CHUNK_SIZE_OID,
@@ -80,21 +108,32 @@ toast_external_info_get_pointer_size(uint8 tag)
 uint8
 toast_external_assign_vartag(Oid toastrelid, uint64 value)
 {
-	/*
-	 * If dealing with a code path where a TOAST relation may not be
-	 * assigned, like heap_toast_insert_or_update(), just use the legacy
-	 * vartag_external.
-	 */
-	if (!OidIsValid(toastrelid))
-		return VARTAG_ONDISK_OID;
+	Oid		toast_typid;
 
 	/*
-	 * Currently there is only one type of vartag_external supported:
-	 * 4-byte value with OID for the chunk_id type.  This routine will
-	 * be extended to be able to use multiple vartag_external within
-	 * a single TOAST relation type, that may change depending on the
-	 * value used.
+	 * If dealing with a code path where a TOAST relation may not be
+	 * assigned, like heap_toast_insert_or_update(), just use the
+	 * vartag_external that can be guessed based on the GUC
+	 * default_toast_type.
+	 *
+	 * In bootstrap mode, we should not do any kind of syscache lookups,
+	 * so do the same and rely on the value of default_toast_type.
 	 */
+	if (!OidIsValid(toastrelid) || IsBootstrapProcessingMode())
+	{
+		if (default_toast_type == TOAST_TYPE_INT8)
+			return VARTAG_ONDISK_INT8;
+		return VARTAG_ONDISK_OID;
+	}
+
+	/*
+	 * Two types of vartag_external are currently supported: OID and int8,
+	 * which depend on the type assigned to "chunk_id" for the TOAST table.
+	 */
+	toast_typid = get_atttype(toastrelid, 1);
+	if (toast_typid == INT8OID)
+		return VARTAG_ONDISK_INT8;
+
 	return VARTAG_ONDISK_OID;
 }
 
@@ -103,6 +142,104 @@ toast_external_assign_vartag(Oid toastrelid, uint64 value)
  * the in-memory representation toast_external_data used in the backend.
  */
 
+/* Callbacks for VARTAG_ONDISK_INT8 */
+static void
+ondisk_int8_to_external_data(struct varlena *attr, toast_external_data *data)
+{
+	varatt_external_int8	external;
+
+	VARATT_EXTERNAL_GET_POINTER(external, attr);
+	data->rawsize = external.va_rawsize;
+
+	/* External size and compression methods are stored in the same field */
+	if (VARATT_EXTERNAL_IS_COMPRESSED(external))
+	{
+		data->extsize = VARATT_EXTERNAL_GET_EXTSIZE(external);
+		data->compression_method = VARATT_EXTERNAL_GET_COMPRESS_METHOD(external);
+	}
+	else
+	{
+		data->extsize = external.va_extinfo;
+		data->compression_method = TOAST_INVALID_COMPRESSION_ID;
+	}
+
+	data->value = (((uint64) external.va_valueid_hi) << 32) |
+		external.va_valueid_lo;
+	data->toastrelid = external.va_toastrelid;
+
+}
+
+static struct varlena *
+ondisk_int8_create_external_data(toast_external_data data)
+{
+	struct varlena *result = NULL;
+	varatt_external_int8 external;
+
+	external.va_rawsize = data.rawsize;
+
+	if (data.compression_method != TOAST_INVALID_COMPRESSION_ID)
+	{
+		/* Set size and compression method, in a single field. */
+		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(external,
+													 data.extsize,
+													 data.compression_method);
+	}
+	else
+		external.va_extinfo = data.extsize;
+
+	external.va_toastrelid = data.toastrelid;
+	external.va_valueid_hi = (((uint64) data.value) >> 32);
+	external.va_valueid_lo = (uint32) data.value;
+
+	result = (struct varlena *) palloc(TOAST_POINTER_INT8_SIZE);
+	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_INT8);
+	memcpy(VARDATA_EXTERNAL(result), &external, sizeof(external));
+
+	return result;
+}
+
+static uint64
+ondisk_int8_get_new_value(Relation toastrel, Oid indexid,
+						  AttrNumber attnum)
+{
+	uint64		new_value;
+	SysScanDesc	scan;
+	ScanKeyData	key;
+	bool		collides = false;
+
+retry:
+	new_value = GetNewToastId();
+
+	/* No indexes in bootstrap mode, so leave */
+	if (IsBootstrapProcessingMode())
+		return new_value;
+
+	Assert(IsSystemRelation(toastrel));
+
+	CHECK_FOR_INTERRUPTS();
+
+	/*
+	 * Check if the new value picked already exists in the toast relation.
+	 * If there is a conflict, retry.
+	 */
+	ScanKeyInit(&key,
+				attnum,
+				BTEqualStrategyNumber, F_INT8EQ,
+				Int64GetDatum(new_value));
+
+	/* see notes in GetNewOidWithIndex() above about using SnapshotAny */
+	scan = systable_beginscan(toastrel, indexid, true,
+							  SnapshotAny, 1, &key);
+	collides = HeapTupleIsValid(systable_getnext(scan));
+	systable_endscan(scan);
+
+	if (collides)
+		goto retry;
+
+	return new_value;
+}
+
+
 /* Callbacks for VARTAG_ONDISK_OID */
 static void
 ondisk_oid_to_external_data(struct varlena *attr, toast_external_data *data)
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 663af02e4634..4f71f9ee399a 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -18,7 +18,6 @@
 #include "access/heapam.h"
 #include "access/heaptoast.h"
 #include "access/table.h"
-#include "access/toast_counter.h"
 #include "access/toast_external.h"
 #include "access/toast_internals.h"
 #include "access/xact.h"
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index 3238d21830ff..3ecd3993aea4 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -31,7 +31,9 @@
 #include "access/toast_external.h"
 #include "access/toast_helper.h"
 #include "access/toast_internals.h"
+#include "access/toast_type.h"
 #include "utils/fmgroids.h"
+#include "utils/syscache.h"
 
 
 /* ----------
@@ -654,7 +656,7 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 	int32		max_chunk_size;
 	const toast_external_info *info;
 	uint8		tag = VARTAG_INDIRECT;  /* init value does not matter */
-	Oid			toast_typid;
+	Oid			toast_typid = InvalidOid;
 
 	/* Look for the valid index of toast relation */
 	validIndex = toast_open_indexes(toastrel,
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 26508bd01c86..b8932700ce6e 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -4971,14 +4971,22 @@ ReorderBufferToastAppendChunk(ReorderBuffer *rb, ReorderBufferTXN *txn,
 	TupleDesc	desc = RelationGetDescr(relation);
 	uint64		chunk_id;
 	int32		chunk_seq;
+	Oid			toast_typid;
 
 	if (txn->toast_hash == NULL)
 		ReorderBufferToastInitHash(rb, txn);
+	toast_typid = TupleDescAttr(desc, 0)->atttypid;
 
 	Assert(IsToastRelation(relation));
 
 	newtup = change->data.tp.newtuple;
-	chunk_id = DatumGetObjectId(fastgetattr(newtup, 1, desc, &isnull));
+	/* This depends on the type of TOAST value dealt with. */
+	if (toast_typid == OIDOID)
+		chunk_id = DatumGetObjectId(fastgetattr(newtup, 1, desc, &isnull));
+	else if (toast_typid == INT8OID)
+		chunk_id = DatumGetUInt64(fastgetattr(newtup, 1, desc, &isnull));
+	else
+		Assert(false);
 	Assert(!isnull);
 	chunk_seq = DatumGetInt32(fastgetattr(newtup, 2, desc, &isnull));
 	Assert(!isnull);
diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml
index 564783a1c559..29ba80e8423c 100644
--- a/doc/src/sgml/storage.sgml
+++ b/doc/src/sgml/storage.sgml
@@ -415,7 +415,11 @@ described in more detail below.
 
 <para>
 Out-of-line values are divided (after compression if used) into chunks of at
-most <symbol>TOAST_MAX_CHUNK_SIZE_OID</symbol> bytes (by default this value is chosen
+most <symbol>TOAST_MAX_CHUNK_SIZE_OID</symbol> bytes if the
+<acronym>TOAST</acronym> relation uses the <literal>oid</literal> type for
+<literal>chunk_id</literal>, or <symbol>TOAST_MAX_CHUNK_SIZE_INT8</symbol>
+bytes if the <acronym>TOAST</acronym> relation uses the <literal>int8</literal>
+type for <literal>chunk_id</literal> (by default these values are chosen
 so that four chunk rows will fit on a page, making it about 2000 bytes).
 Each chunk is stored as a separate row in the <acronym>TOAST</acronym> table
 belonging to the owning table.  Every
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 833811c75437..958b1451b4ff 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1733,7 +1733,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	{
 		uint8		va_tag = VARTAG_EXTERNAL(tp + ctx->offset);
 
-		if (va_tag != VARTAG_ONDISK_OID)
+		if (va_tag != VARTAG_ONDISK_OID && va_tag != VARTAG_ONDISK_INT8)
 		{
 			report_corruption(ctx,
 							  psprintf("toasted attribute has unexpected TOAST tag %u",
-- 
2.50.0

v3-0014-amcheck-Add-test-cases-for-8-byte-TOAST-values.patchtext/x-diff; charset=us-asciiDownload
From f846e063d7de7b5cfa6e4b5e915ee9958ee6baac Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 19 Jun 2025 13:09:11 +0900
Subject: [PATCH v3 14/14] amcheck: Add test cases for 8-byte TOAST values

This patch is a proof of concept to show what is required to change in
the tests of pg_amcheck to be able to work with the new type of external
TOAST pointer.
---
 src/bin/pg_amcheck/t/004_verify_heapam.pl | 15 +++++++++------
 1 file changed, 9 insertions(+), 6 deletions(-)

diff --git a/src/bin/pg_amcheck/t/004_verify_heapam.pl b/src/bin/pg_amcheck/t/004_verify_heapam.pl
index 72693660fb64..5f82608b5c72 100644
--- a/src/bin/pg_amcheck/t/004_verify_heapam.pl
+++ b/src/bin/pg_amcheck/t/004_verify_heapam.pl
@@ -83,8 +83,8 @@ use Test::More;
 # it is convenient enough to do it this way.  We define packing code
 # constants here, where they can be compared easily against the layout.
 
-use constant HEAPTUPLE_PACK_CODE => 'LLLSSSSSCCLLCCCCCCCCCCllLL';
-use constant HEAPTUPLE_PACK_LENGTH => 58;    # Total size
+use constant HEAPTUPLE_PACK_CODE => 'LLLSSSSSCCLLCCCCCCCCCCllLLL';
+use constant HEAPTUPLE_PACK_LENGTH => 62;    # Total size
 
 # Read a tuple of our table from a heap page.
 #
@@ -129,7 +129,8 @@ sub read_tuple
 		c_va_vartag => shift,
 		c_va_rawsize => shift,
 		c_va_extinfo => shift,
-		c_va_valueid => shift,
+		c_va_valueid_lo => shift,
+		c_va_valueid_hi => shift,
 		c_va_toastrelid => shift);
 	# Stitch together the text for column 'b'
 	$tup{b} = join('', map { chr($tup{"b_body$_"}) } (1 .. 7));
@@ -163,7 +164,8 @@ sub write_tuple
 		$tup->{b_body6}, $tup->{b_body7},
 		$tup->{c_va_header}, $tup->{c_va_vartag},
 		$tup->{c_va_rawsize}, $tup->{c_va_extinfo},
-		$tup->{c_va_valueid}, $tup->{c_va_toastrelid});
+		$tup->{c_va_valueid_lo}, $tup->{c_va_valueid_hi},
+		$tup->{c_va_toastrelid});
 	sysseek($fh, $offset, 0)
 	  or BAIL_OUT("sysseek failed: $!");
 	defined(syswrite($fh, $buffer, HEAPTUPLE_PACK_LENGTH))
@@ -184,6 +186,7 @@ my $node = PostgreSQL::Test::Cluster->new('test');
 $node->init(no_data_checksums => 1);
 $node->append_conf('postgresql.conf', 'autovacuum=off');
 $node->append_conf('postgresql.conf', 'max_prepared_transactions=10');
+$node->append_conf('postgresql.conf', 'default_toast_type=int8');
 
 # Start the node and load the extensions.  We depend on both
 # amcheck and pageinspect for this test.
@@ -496,7 +499,7 @@ for (my $tupidx = 0; $tupidx < $ROWCOUNT; $tupidx++)
 		$tup->{t_hoff} += 128;
 
 		push @expected,
-		  qr/${$header}data begins at offset 152 beyond the tuple length 58/,
+		  qr/${$header}data begins at offset 152 beyond the tuple length 62/,
 		  qr/${$header}tuple data should begin at byte 24, but actually begins at byte 152 \(3 attributes, no nulls\)/;
 	}
 	elsif ($offnum == 6)
@@ -581,7 +584,7 @@ for (my $tupidx = 0; $tupidx < $ROWCOUNT; $tupidx++)
 	elsif ($offnum == 13)
 	{
 		# Corrupt the bits in column 'c' toast pointer
-		$tup->{c_va_valueid} = 0xFFFFFFFF;
+		$tup->{c_va_valueid_lo} = 0xFFFFFFFF;
 
 		$header = header(0, $offnum, 2);
 		push @expected, qr/${header}toast value \d+ not found in toast table/;
-- 
2.50.0

#34Jim Nasby
jnasby@upgrade.com
In reply to: Michael Paquier (#33)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

On Fri, Aug 1, 2025 at 4:03 AM Michael Paquier <michael@paquier.xyz> wrote:

- Addition of separate patch to rename varatt_external to
varatt_external_oid and VARTAG_ONDISK to VARTAG_ONDISK_OID, in 0003.

Since you're already renaming things... ISTM "ondisk" has the potential for
confusion, assuming that at some point we'll have the ability to store
large datums directly in the filesystem (instead of breaking into chunks to
live in a relation). VARTAG_DURABLE might be a better option.

#35Michael Paquier
michael@paquier.xyz
In reply to: Jim Nasby (#34)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

On Mon, Aug 04, 2025 at 02:37:19PM -0500, Jim Nasby wrote:

On Fri, Aug 1, 2025 at 4:03 AM Michael Paquier <michael@paquier.xyz> wrote:

- Addition of separate patch to rename varatt_external to
varatt_external_oid and VARTAG_ONDISK to VARTAG_ONDISK_OID, in 0003.

Since you're already renaming things... ISTM "ondisk" has the potential for
confusion, assuming that at some point we'll have the ability to store
large datums directly in the filesystem (instead of breaking into chunks to
live in a relation). VARTAG_DURABLE might be a better option.

Hmm. I don't know about this one. Durable is an ACID property that
does not apply to all relation kinds. For example, take an unlogged
table: its data is not durable but its TOAST pointers would be marked
with a VARTAG_DURABLE. With that in mind ONDISK still sounds kind of
OK for me to use as a name.
--
Michael

#36Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#33)
15 attachment(s)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

On Fri, Aug 01, 2025 at 06:03:11PM +0900, Michael Paquier wrote:

Please find attached a v3, that I have spent some time polishing to
fix the value ID problem of this thread. v2 had some conflicts, and
the CI previously failed with warning job (CI is green here now).

Attached is a v4, due to conflicts mainly caused by the recent changes
in varatt.h done by e035863c9a04. This had an interesting side
benefit when rebasing, where I have been able to isolate most of the
knowledge related to the struct varatt_external (well
varatt_external_oid in the patch set) into toast_external.c, at the
exception of VARTAG_SIZE. That's done in a separate patch, numbered
0006.

The rest of the patch set has a couple of adjustements to document
better the new API expectations for toast_external.{c,h}, comment
adjustments, some more beautification changes, some indentation
applied, etc.

As things stand, I am getting pretty happy with the patch set up to
0005 and how things are getting in shape for the interface, and I am
planning to begin applying this stuff up to 0005 in the next couple of
weeks.

As of this patch set, this means a new target of 0006, to get the
TOAST code refactored so as it is able to support more than 1 type of
external on-disk pointer with the 8-byte value problem in scope. Any
comments?
--
Michael

Attachments:

v4-0001-Refactor-some-TOAST-value-ID-code-to-use-uint64-i.patchtext/x-diff; charset=us-asciiDownload
From f10c5b42c0c990897a8a654828d90f0304330b3e Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 19 Jun 2025 09:57:19 +0900
Subject: [PATCH v4 01/15] Refactor some TOAST value ID code to use uint64
 instead of Oid

This change is a mechanical switch to change most of the code paths that
assume TOAST value IDs to be Oids to become uint64, easing an upcoming
change to allow 8-byte TOAST values.

The areas touched are related to table AM, amcheck and logical
decoding's reorder buffer.  A good chunk of the changes involve
switching printf() markers from %u to PRIu64.
---
 src/include/access/heaptoast.h                |  2 +-
 src/include/access/tableam.h                  |  4 +-
 src/backend/access/common/toast_internals.c   |  8 +--
 src/backend/access/heap/heaptoast.c           | 12 ++--
 .../replication/logical/reorderbuffer.c       | 14 ++--
 contrib/amcheck/verify_heapam.c               | 69 +++++++++++--------
 6 files changed, 62 insertions(+), 47 deletions(-)

diff --git a/src/include/access/heaptoast.h b/src/include/access/heaptoast.h
index 6385a27caf83..6e3558cbd6d2 100644
--- a/src/include/access/heaptoast.h
+++ b/src/include/access/heaptoast.h
@@ -142,7 +142,7 @@ extern HeapTuple toast_build_flattened_tuple(TupleDesc tupleDesc,
  *	Fetch a slice from a toast value stored in a heap table.
  * ----------
  */
-extern void heap_fetch_toast_slice(Relation toastrel, Oid valueid,
+extern void heap_fetch_toast_slice(Relation toastrel, uint64 valueid,
 								   int32 attrsize, int32 sliceoffset,
 								   int32 slicelength, struct varlena *result);
 
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index 1c9e802a6b12..b640047d2fdc 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -740,7 +740,7 @@ typedef struct TableAmRoutine
 	 * table implemented by this AM.  See table_relation_fetch_toast_slice()
 	 * for more details.
 	 */
-	void		(*relation_fetch_toast_slice) (Relation toastrel, Oid valueid,
+	void		(*relation_fetch_toast_slice) (Relation toastrel, uint64 valueid,
 											   int32 attrsize,
 											   int32 sliceoffset,
 											   int32 slicelength,
@@ -1873,7 +1873,7 @@ table_relation_toast_am(Relation rel)
  * stored.
  */
 static inline void
-table_relation_fetch_toast_slice(Relation toastrel, Oid valueid,
+table_relation_fetch_toast_slice(Relation toastrel, uint64 valueid,
 								 int32 attrsize, int32 sliceoffset,
 								 int32 slicelength, struct varlena *result)
 {
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 196e06115e93..0016a16afc95 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -26,8 +26,8 @@
 #include "utils/rel.h"
 #include "utils/snapmgr.h"
 
-static bool toastrel_valueid_exists(Relation toastrel, Oid valueid);
-static bool toastid_valueid_exists(Oid toastrelid, Oid valueid);
+static bool toastrel_valueid_exists(Relation toastrel, uint64 valueid);
+static bool toastid_valueid_exists(Oid toastrelid, uint64 valueid);
 
 /* ----------
  * toast_compress_datum -
@@ -456,7 +456,7 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
  * ----------
  */
 static bool
-toastrel_valueid_exists(Relation toastrel, Oid valueid)
+toastrel_valueid_exists(Relation toastrel, uint64 valueid)
 {
 	bool		result = false;
 	ScanKeyData toastkey;
@@ -504,7 +504,7 @@ toastrel_valueid_exists(Relation toastrel, Oid valueid)
  * ----------
  */
 static bool
-toastid_valueid_exists(Oid toastrelid, Oid valueid)
+toastid_valueid_exists(Oid toastrelid, uint64 valueid)
 {
 	bool		result;
 	Relation	toastrel;
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index cb1e57030f64..76936b2f4944 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -623,7 +623,7 @@ toast_build_flattened_tuple(TupleDesc tupleDesc,
  * result is the varlena into which the results should be written.
  */
 void
-heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
+heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 					   int32 sliceoffset, int32 slicelength,
 					   struct varlena *result)
 {
@@ -725,7 +725,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
 		else
 		{
 			/* should never happen */
-			elog(ERROR, "found toasted toast chunk for toast value %u in %s",
+			elog(ERROR, "found toasted toast chunk for toast value %" PRIu64 " in %s",
 				 valueid, RelationGetRelationName(toastrel));
 			chunksize = 0;		/* keep compiler quiet */
 			chunkdata = NULL;
@@ -737,13 +737,13 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
 		if (curchunk != expectedchunk)
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
-					 errmsg_internal("unexpected chunk number %d (expected %d) for toast value %u in %s",
+					 errmsg_internal("unexpected chunk number %d (expected %d) for toast value %" PRIu64 " in %s",
 									 curchunk, expectedchunk, valueid,
 									 RelationGetRelationName(toastrel))));
 		if (curchunk > endchunk)
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
-					 errmsg_internal("unexpected chunk number %d (out of range %d..%d) for toast value %u in %s",
+					 errmsg_internal("unexpected chunk number %d (out of range %d..%d) for toast value %" PRIu64 " in %s",
 									 curchunk,
 									 startchunk, endchunk, valueid,
 									 RelationGetRelationName(toastrel))));
@@ -752,7 +752,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
 		if (chunksize != expected_size)
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
-					 errmsg_internal("unexpected chunk size %d (expected %d) in chunk %d of %d for toast value %u in %s",
+					 errmsg_internal("unexpected chunk size %d (expected %d) in chunk %d of %d for toast value %" PRIu64 " in %s",
 									 chunksize, expected_size,
 									 curchunk, totalchunks, valueid,
 									 RelationGetRelationName(toastrel))));
@@ -781,7 +781,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
 	if (expectedchunk != (endchunk + 1))
 		ereport(ERROR,
 				(errcode(ERRCODE_DATA_CORRUPTED),
-				 errmsg_internal("missing chunk number %d for toast value %u in %s",
+				 errmsg_internal("missing chunk number %d for toast value %" PRIu64 " in %s",
 								 expectedchunk, valueid,
 								 RelationGetRelationName(toastrel))));
 
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 34cf05668ae8..36e2a4360e18 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -176,7 +176,7 @@ typedef struct ReorderBufferIterTXNState
 /* toast datastructures */
 typedef struct ReorderBufferToastEnt
 {
-	Oid			chunk_id;		/* toast_table.chunk_id */
+	uint64		chunk_id;		/* toast_table.chunk_id */
 	int32		last_chunk_seq; /* toast_table.chunk_seq of the last chunk we
 								 * have seen */
 	Size		num_chunks;		/* number of chunks we've already seen */
@@ -4944,7 +4944,7 @@ ReorderBufferToastInitHash(ReorderBuffer *rb, ReorderBufferTXN *txn)
 
 	Assert(txn->toast_hash == NULL);
 
-	hash_ctl.keysize = sizeof(Oid);
+	hash_ctl.keysize = sizeof(uint64);
 	hash_ctl.entrysize = sizeof(ReorderBufferToastEnt);
 	hash_ctl.hcxt = rb->context;
 	txn->toast_hash = hash_create("ReorderBufferToastHash", 5, &hash_ctl,
@@ -4968,7 +4968,7 @@ ReorderBufferToastAppendChunk(ReorderBuffer *rb, ReorderBufferTXN *txn,
 	bool		isnull;
 	Pointer		chunk;
 	TupleDesc	desc = RelationGetDescr(relation);
-	Oid			chunk_id;
+	uint64		chunk_id;
 	int32		chunk_seq;
 
 	if (txn->toast_hash == NULL)
@@ -4995,11 +4995,11 @@ ReorderBufferToastAppendChunk(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		dlist_init(&ent->chunks);
 
 		if (chunk_seq != 0)
-			elog(ERROR, "got sequence entry %d for toast chunk %u instead of seq 0",
+			elog(ERROR, "got sequence entry %d for toast chunk %" PRIu64 " instead of seq 0",
 				 chunk_seq, chunk_id);
 	}
 	else if (found && chunk_seq != ent->last_chunk_seq + 1)
-		elog(ERROR, "got sequence entry %d for toast chunk %u instead of seq %d",
+		elog(ERROR, "got sequence entry %d for toast chunk %" PRIu64 " instead of seq %d",
 			 chunk_seq, chunk_id, ent->last_chunk_seq + 1);
 
 	chunk = DatumGetPointer(fastgetattr(newtup, 3, desc, &isnull));
@@ -5108,6 +5108,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		struct varlena *reconstructed;
 		dlist_iter	it;
 		Size		data_done = 0;
+		uint64		toast_valueid;
 
 		/* system columns aren't toasted */
 		if (attr->attnum < 0)
@@ -5132,13 +5133,14 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 			continue;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, varlena);
+		toast_valueid = toast_pointer.va_valueid;
 
 		/*
 		 * Check whether the toast tuple changed, replace if so.
 		 */
 		ent = (ReorderBufferToastEnt *)
 			hash_search(txn->toast_hash,
-						&toast_pointer.va_valueid,
+						&toast_valueid,
 						HASH_FIND,
 						NULL);
 		if (ent == NULL)
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 4963e9245cb5..3b2bdced4cdc 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1556,11 +1556,18 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 				  uint32 extsize)
 {
 	int32		chunk_seq;
-	int32		last_chunk_seq = (extsize - 1) / TOAST_MAX_CHUNK_SIZE;
+	int32		last_chunk_seq;
 	Pointer		chunk;
 	bool		isnull;
 	int32		chunksize;
 	int32		expected_size;
+	uint64		toast_valueid;
+	int32		max_chunk_size;
+
+	toast_valueid = ta->toast_pointer.va_valueid;
+
+	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	last_chunk_seq = (extsize - 1) / max_chunk_size;
 
 	/* Sanity-check the sequence number. */
 	chunk_seq = DatumGetInt32(fastgetattr(toasttup, 2,
@@ -1568,16 +1575,16 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	if (isnull)
 	{
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u has toast chunk with null sequence number",
-										 ta->toast_pointer.va_valueid));
+								psprintf("toast value %" PRIu64 " has toast chunk with null sequence number",
+										 toast_valueid));
 		return;
 	}
 	if (chunk_seq != *expected_chunk_seq)
 	{
 		/* Either the TOAST index is corrupt, or we don't have all chunks. */
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u index scan returned chunk %d when expecting chunk %d",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value %" PRIu64 " index scan returned chunk %d when expecting chunk %d",
+										 toast_valueid,
 										 chunk_seq, *expected_chunk_seq));
 	}
 	*expected_chunk_seq = chunk_seq + 1;
@@ -1588,8 +1595,8 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	if (isnull)
 	{
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u chunk %d has null data",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value %" PRIu64 " chunk %d has null data",
+										 toast_valueid,
 										 chunk_seq));
 		return;
 	}
@@ -1608,8 +1615,8 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 		uint32		header = ((varattrib_4b *) chunk)->va_4byte.va_header;
 
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u chunk %d has invalid varlena header %0x",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value %" PRIu64 " chunk %d has invalid varlena header %0x",
+										 toast_valueid,
 										 chunk_seq, header));
 		return;
 	}
@@ -1620,19 +1627,19 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	if (chunk_seq > last_chunk_seq)
 	{
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u chunk %d follows last expected chunk %d",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value %" PRIu64 " chunk %d follows last expected chunk %d",
+										 toast_valueid,
 										 chunk_seq, last_chunk_seq));
 		return;
 	}
 
-	expected_size = chunk_seq < last_chunk_seq ? TOAST_MAX_CHUNK_SIZE
-		: extsize - (last_chunk_seq * TOAST_MAX_CHUNK_SIZE);
+	expected_size = chunk_seq < last_chunk_seq ? max_chunk_size
+		: extsize - (last_chunk_seq * max_chunk_size);
 
 	if (chunksize != expected_size)
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u chunk %d has size %u, but expected size %u",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value %" PRIu64 " chunk %d has size %u, but expected size %u",
+										 toast_valueid,
 										 chunk_seq, chunksize, expected_size));
 }
 
@@ -1663,6 +1670,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	struct varlena *attr;
 	char	   *tp;				/* pointer to the tuple data */
 	uint16		infomask;
+	uint64		toast_pointer_valueid;
 	CompactAttribute *thisatt;
 	struct varatt_external toast_pointer;
 
@@ -1766,6 +1774,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 		return true;
 
 	/* It is external, and we're looking at a page on disk */
+	toast_pointer_valueid = toast_pointer.va_valueid;
 
 	/*
 	 * Must copy attr into toast_pointer for alignment considerations
@@ -1775,8 +1784,8 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	/* Toasted attributes too large to be untoasted should never be stored */
 	if (toast_pointer.va_rawsize > VARLENA_SIZE_LIMIT)
 		report_corruption(ctx,
-						  psprintf("toast value %u rawsize %d exceeds limit %d",
-								   toast_pointer.va_valueid,
+						  psprintf("toast value %" PRIu64 " rawsize %d exceeds limit %d",
+								   toast_pointer_valueid,
 								   toast_pointer.va_rawsize,
 								   VARLENA_SIZE_LIMIT));
 
@@ -1803,16 +1812,16 @@ check_tuple_attribute(HeapCheckContext *ctx)
 		}
 		if (!valid)
 			report_corruption(ctx,
-							  psprintf("toast value %u has invalid compression method id %d",
-									   toast_pointer.va_valueid, cmid));
+							  psprintf("toast value %" PRIu64 " has invalid compression method id %d",
+									   toast_pointer_valueid, cmid));
 	}
 
 	/* The tuple header better claim to contain toasted values */
 	if (!(infomask & HEAP_HASEXTERNAL))
 	{
 		report_corruption(ctx,
-						  psprintf("toast value %u is external but tuple header flag HEAP_HASEXTERNAL not set",
-								   toast_pointer.va_valueid));
+						  psprintf("toast value %" PRIu64 " is external but tuple header flag HEAP_HASEXTERNAL not set",
+								   toast_pointer_valueid));
 		return true;
 	}
 
@@ -1820,8 +1829,8 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	if (!ctx->rel->rd_rel->reltoastrelid)
 	{
 		report_corruption(ctx,
-						  psprintf("toast value %u is external but relation has no toast relation",
-								   toast_pointer.va_valueid));
+						  psprintf("toast value %" PRIu64 " is external but relation has no toast relation",
+								   toast_pointer_valueid));
 		return true;
 	}
 
@@ -1866,9 +1875,11 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	uint32		extsize;
 	int32		expected_chunk_seq = 0;
 	int32		last_chunk_seq;
+	uint64		toast_valueid;
+	int32		max_chunk_size = TOAST_MAX_CHUNK_SIZE;
 
 	extsize = VARATT_EXTERNAL_GET_EXTSIZE(ta->toast_pointer);
-	last_chunk_seq = (extsize - 1) / TOAST_MAX_CHUNK_SIZE;
+	last_chunk_seq = (extsize - 1) / max_chunk_size;
 
 	/*
 	 * Setup a scan key to find chunks in toast table with matching va_valueid
@@ -1896,14 +1907,16 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	}
 	systable_endscan_ordered(toastscan);
 
+	toast_valueid = ta->toast_pointer.va_valueid;
+
 	if (!found_toasttup)
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u not found in toast table",
-										 ta->toast_pointer.va_valueid));
+								psprintf("toast value %" PRIu64 " not found in toast table",
+										 toast_valueid));
 	else if (expected_chunk_seq <= last_chunk_seq)
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u was expected to end at chunk %d, but ended while expecting chunk %d",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value %" PRIu64 " was expected to end at chunk %d, but ended while expecting chunk %d",
+										 toast_valueid,
 										 last_chunk_seq, expected_chunk_seq));
 }
 
-- 
2.50.0

v4-0002-Minimize-footprint-of-TOAST_MAX_CHUNK_SIZE-in-hea.patchtext/x-diff; charset=us-asciiDownload
From a25391ebd3bc27dcffd7119b86e7632ace01b512 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 19 Jun 2025 10:03:46 +0900
Subject: [PATCH v4 02/15] Minimize footprint of TOAST_MAX_CHUNK_SIZE in heap
 TOAST code

This eases a follow-up change to support 8-byte TOAST value IDs, as the
maximum chunk size allowed for a single chunk of TOASTed data depends on
the size of the value ID.
---
 src/backend/access/heap/heaptoast.c | 20 ++++++++++++--------
 1 file changed, 12 insertions(+), 8 deletions(-)

diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index 76936b2f4944..ae8d502ddcd3 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -634,11 +634,12 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 	SysScanDesc toastscan;
 	HeapTuple	ttup;
 	int32		expectedchunk;
-	int32		totalchunks = ((attrsize - 1) / TOAST_MAX_CHUNK_SIZE) + 1;
+	int32		totalchunks;
 	int			startchunk;
 	int			endchunk;
 	int			num_indexes;
 	int			validIndex;
+	int32		max_chunk_size;
 
 	/* Look for the valid index of toast relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -646,8 +647,11 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 									&toastidxs,
 									&num_indexes);
 
-	startchunk = sliceoffset / TOAST_MAX_CHUNK_SIZE;
-	endchunk = (sliceoffset + slicelength - 1) / TOAST_MAX_CHUNK_SIZE;
+	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+
+	totalchunks = ((attrsize - 1) / max_chunk_size) + 1;
+	startchunk = sliceoffset / max_chunk_size;
+	endchunk = (sliceoffset + slicelength - 1) / max_chunk_size;
 	Assert(endchunk <= totalchunks);
 
 	/* Set up a scan key to fetch from the index. */
@@ -747,8 +751,8 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 									 curchunk,
 									 startchunk, endchunk, valueid,
 									 RelationGetRelationName(toastrel))));
-		expected_size = curchunk < totalchunks - 1 ? TOAST_MAX_CHUNK_SIZE
-			: attrsize - ((totalchunks - 1) * TOAST_MAX_CHUNK_SIZE);
+		expected_size = curchunk < totalchunks - 1 ? max_chunk_size
+			: attrsize - ((totalchunks - 1) * max_chunk_size);
 		if (chunksize != expected_size)
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
@@ -763,12 +767,12 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 		chcpystrt = 0;
 		chcpyend = chunksize - 1;
 		if (curchunk == startchunk)
-			chcpystrt = sliceoffset % TOAST_MAX_CHUNK_SIZE;
+			chcpystrt = sliceoffset % max_chunk_size;
 		if (curchunk == endchunk)
-			chcpyend = (sliceoffset + slicelength - 1) % TOAST_MAX_CHUNK_SIZE;
+			chcpyend = (sliceoffset + slicelength - 1) % max_chunk_size;
 
 		memcpy(VARDATA(result) +
-			   (curchunk * TOAST_MAX_CHUNK_SIZE - sliceoffset) + chcpystrt,
+			   (curchunk * max_chunk_size - sliceoffset) + chcpystrt,
 			   chunkdata + chcpystrt,
 			   (chcpyend - chcpystrt) + 1);
 
-- 
2.50.0

v4-0003-varatt_external-varatt_external_oid-and-VARTAG_ON.patchtext/x-diff; charset=us-asciiDownload
From 742631089dfe7f2f247ee2cd652e749081d68c47 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 8 Aug 2025 14:50:06 +0900
Subject: [PATCH v4 03/15] varatt_external->varatt_external_oid and
 VARTAG_ONDISK->VARTAG_ONDISK_OID

This rename is in preparation of a follow-up commit that aims at adding
support for multiple types of external on-disk TOAST pointers, where the
OID type is only one subset of them.
---
 src/include/access/detoast.h                  |  4 +--
 src/include/varatt.h                          | 34 +++++++++++--------
 src/backend/access/common/detoast.c           | 10 +++---
 src/backend/access/common/toast_compression.c |  2 +-
 src/backend/access/common/toast_internals.c   |  8 ++---
 .../replication/logical/reorderbuffer.c       |  2 +-
 src/backend/utils/adt/varlena.c               |  2 +-
 contrib/amcheck/verify_heapam.c               |  6 ++--
 8 files changed, 36 insertions(+), 32 deletions(-)

diff --git a/src/include/access/detoast.h b/src/include/access/detoast.h
index e603a2276c38..d80a62e64fd5 100644
--- a/src/include/access/detoast.h
+++ b/src/include/access/detoast.h
@@ -14,7 +14,7 @@
 
 /*
  * Macro to fetch the possibly-unaligned contents of an EXTERNAL datum
- * into a local "struct varatt_external" toast pointer.  This should be
+ * into a local "struct varatt_external_oid" toast pointer.  This should be
  * just a memcpy, but some versions of gcc seem to produce broken code
  * that assumes the datum contents are aligned.  Introducing an explicit
  * intermediate "varattrib_1b_e *" variable seems to fix it.
@@ -28,7 +28,7 @@ do { \
 } while (0)
 
 /* Size of an EXTERNAL datum that contains a standard TOAST pointer */
-#define TOAST_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external))
+#define TOAST_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_oid))
 
 /* Size of an EXTERNAL datum that contains an indirection pointer */
 #define INDIRECT_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_indirect))
diff --git a/src/include/varatt.h b/src/include/varatt.h
index aeeabf9145b5..fcf2dffaa2fd 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -16,7 +16,7 @@
 #define VARATT_H
 
 /*
- * struct varatt_external is a traditional "TOAST pointer", that is, the
+ * struct varatt_external_oid is a traditional "TOAST pointer", that is, the
  * information needed to fetch a Datum stored out-of-line in a TOAST table.
  * The data is compressed if and only if the external size stored in
  * va_extinfo is less than va_rawsize - VARHDRSZ.
@@ -29,14 +29,14 @@
  * you can look at these fields!  (The reason we use memcmp is to avoid
  * having to do that just to detect equality of two TOAST pointers...)
  */
-typedef struct varatt_external
+typedef struct varatt_external_oid
 {
 	int32		va_rawsize;		/* Original data size (includes header) */
 	uint32		va_extinfo;		/* External saved size (without header) and
 								 * compression method */
 	Oid			va_valueid;		/* Unique ID of value within TOAST table */
 	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
-}			varatt_external;
+}			varatt_external_oid;
 
 /*
  * These macros define the "saved size" portion of va_extinfo.  Its remaining
@@ -51,7 +51,7 @@ typedef struct varatt_external
  * The creator of such a Datum is entirely responsible that the referenced
  * storage survives for as long as referencing pointer Datums can exist.
  *
- * Note that just as for struct varatt_external, this struct is stored
+ * Note that just as for struct varatt_external_oid, this struct is stored
  * unaligned within any containing tuple.
  */
 typedef struct varatt_indirect
@@ -66,7 +66,7 @@ typedef struct varatt_indirect
  * storage.  APIs for this, in particular the definition of struct
  * ExpandedObjectHeader, are in src/include/utils/expandeddatum.h.
  *
- * Note that just as for struct varatt_external, this struct is stored
+ * Note that just as for struct varatt_external_oid, this struct is stored
  * unaligned within any containing tuple.
  */
 typedef struct ExpandedObjectHeader ExpandedObjectHeader;
@@ -78,15 +78,16 @@ typedef struct varatt_expanded
 
 /*
  * Type tag for the various sorts of "TOAST pointer" datums.  The peculiar
- * value for VARTAG_ONDISK comes from a requirement for on-disk compatibility
- * with a previous notion that the tag field was the pointer datum's length.
+ * value for VARTAG_ONDISK_OID comes from a requirement for on-disk
+ * compatibility with a previous notion that the tag field was the pointer
+ * datum's length.
  */
 typedef enum vartag_external
 {
 	VARTAG_INDIRECT = 1,
 	VARTAG_EXPANDED_RO = 2,
 	VARTAG_EXPANDED_RW = 3,
-	VARTAG_ONDISK = 18
+	VARTAG_ONDISK_OID = 18
 } vartag_external;
 
 /* Is a TOAST pointer either type of expanded-object pointer? */
@@ -105,8 +106,8 @@ VARTAG_SIZE(vartag_external tag)
 		return sizeof(varatt_indirect);
 	else if (VARTAG_IS_EXPANDED(tag))
 		return sizeof(varatt_expanded);
-	else if (tag == VARTAG_ONDISK)
-		return sizeof(varatt_external);
+	else if (tag == VARTAG_ONDISK_OID)
+		return sizeof(varatt_external_oid);
 	else
 	{
 		Assert(false);
@@ -360,7 +361,7 @@ VARATT_IS_EXTERNAL(const void *PTR)
 static inline bool
 VARATT_IS_EXTERNAL_ONDISK(const void *PTR)
 {
-	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK;
+	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID;
 }
 
 /* Is varlena datum an indirect pointer? */
@@ -502,15 +503,18 @@ VARDATA_COMPRESSED_GET_COMPRESS_METHOD(const void *PTR)
 	return ((varattrib_4b *) PTR)->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS;
 }
 
-/* Same for external Datums; but note argument is a struct varatt_external */
+/*
+ * Same for external Datums; but note argument is a struct
+ * varatt_external_oid.
+ */
 static inline Size
-VARATT_EXTERNAL_GET_EXTSIZE(struct varatt_external toast_pointer)
+VARATT_EXTERNAL_GET_EXTSIZE(struct varatt_external_oid toast_pointer)
 {
 	return toast_pointer.va_extinfo & VARLENA_EXTSIZE_MASK;
 }
 
 static inline uint32
-VARATT_EXTERNAL_GET_COMPRESS_METHOD(struct varatt_external toast_pointer)
+VARATT_EXTERNAL_GET_COMPRESS_METHOD(struct varatt_external_oid toast_pointer)
 {
 	return toast_pointer.va_extinfo >> VARLENA_EXTSIZE_BITS;
 }
@@ -533,7 +537,7 @@ VARATT_EXTERNAL_GET_COMPRESS_METHOD(struct varatt_external toast_pointer)
  * actually saves space, so we expect either equality or less-than.
  */
 static inline bool
-VARATT_EXTERNAL_IS_COMPRESSED(struct varatt_external toast_pointer)
+VARATT_EXTERNAL_IS_COMPRESSED(struct varatt_external_oid toast_pointer)
 {
 	return VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) <
 		(Size) (toast_pointer.va_rawsize - VARHDRSZ);
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 626517877422..04eedb474c74 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -225,7 +225,7 @@ detoast_attr_slice(struct varlena *attr,
 
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
-		struct varatt_external toast_pointer;
+		struct varatt_external_oid toast_pointer;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 
@@ -344,7 +344,7 @@ toast_fetch_datum(struct varlena *attr)
 {
 	Relation	toastrel;
 	struct varlena *result;
-	struct varatt_external toast_pointer;
+	struct varatt_external_oid toast_pointer;
 	int32		attrsize;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
@@ -398,7 +398,7 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 {
 	Relation	toastrel;
 	struct varlena *result;
-	struct varatt_external toast_pointer;
+	struct varatt_external_oid toast_pointer;
 	int32		attrsize;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
@@ -550,7 +550,7 @@ toast_raw_datum_size(Datum value)
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
 		/* va_rawsize is the size of the original datum -- including header */
-		struct varatt_external toast_pointer;
+		struct varatt_external_oid toast_pointer;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 		result = toast_pointer.va_rawsize;
@@ -610,7 +610,7 @@ toast_datum_size(Datum value)
 		 * compressed or not.  We do not count the size of the toast pointer
 		 * ... should we?
 		 */
-		struct varatt_external toast_pointer;
+		struct varatt_external_oid toast_pointer;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 		result = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c
index 926f1e4008ab..26aad84d367a 100644
--- a/src/backend/access/common/toast_compression.c
+++ b/src/backend/access/common/toast_compression.c
@@ -262,7 +262,7 @@ toast_get_compression_id(struct varlena *attr)
 	 */
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
-		struct varatt_external toast_pointer;
+		struct varatt_external_oid toast_pointer;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 0016a16afc95..d555c02e908e 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -127,7 +127,7 @@ toast_save_datum(Relation rel, Datum value,
 	bool		t_isnull[3];
 	CommandId	mycid = GetCurrentCommandId(true);
 	struct varlena *result;
-	struct varatt_external toast_pointer;
+	struct varatt_external_oid toast_pointer;
 	union
 	{
 		struct varlena hdr;
@@ -237,7 +237,7 @@ toast_save_datum(Relation rel, Datum value,
 		toast_pointer.va_valueid = InvalidOid;
 		if (oldexternal != NULL)
 		{
-			struct varatt_external old_toast_pointer;
+			struct varatt_external_oid old_toast_pointer;
 
 			Assert(VARATT_IS_EXTERNAL_ONDISK(oldexternal));
 			/* Must copy to access aligned fields */
@@ -369,7 +369,7 @@ toast_save_datum(Relation rel, Datum value,
 	 * Create the TOAST pointer value that we'll return
 	 */
 	result = (struct varlena *) palloc(TOAST_POINTER_SIZE);
-	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK);
+	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_OID);
 	memcpy(VARDATA_EXTERNAL(result), &toast_pointer, sizeof(toast_pointer));
 
 	return PointerGetDatum(result);
@@ -385,7 +385,7 @@ void
 toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 {
 	struct varlena *attr = (struct varlena *) DatumGetPointer(value);
-	struct varatt_external toast_pointer;
+	struct varatt_external_oid toast_pointer;
 	Relation	toastrel;
 	Relation   *toastidxs;
 	ScanKeyData toastkey;
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 36e2a4360e18..0eb99b4f1377 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -5102,7 +5102,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		struct varlena *varlena;
 
 		/* va_rawsize is the size of the original datum -- including header */
-		struct varatt_external toast_pointer;
+		struct varatt_external_oid toast_pointer;
 		struct varatt_indirect redirect_pointer;
 		struct varlena *new_datum = NULL;
 		struct varlena *reconstructed;
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index ffae8c23abfa..38d4e6d45f28 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -4219,7 +4219,7 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 {
 	int			typlen;
 	struct varlena *attr;
-	struct varatt_external toast_pointer;
+	struct varatt_external_oid toast_pointer;
 
 	/* On first call, get the input type's typlen, and save at *fn_extra */
 	if (fcinfo->flinfo->fn_extra == NULL)
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 3b2bdced4cdc..030f9fb64b51 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -73,7 +73,7 @@ typedef enum SkipPages
  */
 typedef struct ToastedAttribute
 {
-	struct varatt_external toast_pointer;
+	struct varatt_external_oid toast_pointer;
 	BlockNumber blkno;			/* block in main table */
 	OffsetNumber offnum;		/* offset in main table */
 	AttrNumber	attnum;			/* attribute in main table */
@@ -1672,7 +1672,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	uint16		infomask;
 	uint64		toast_pointer_valueid;
 	CompactAttribute *thisatt;
-	struct varatt_external toast_pointer;
+	struct varatt_external_oid toast_pointer;
 
 	infomask = ctx->tuphdr->t_infomask;
 	thisatt = TupleDescCompactAttr(RelationGetDescr(ctx->rel), ctx->attnum);
@@ -1731,7 +1731,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	{
 		uint8		va_tag = VARTAG_EXTERNAL(tp + ctx->offset);
 
-		if (va_tag != VARTAG_ONDISK)
+		if (va_tag != VARTAG_ONDISK_OID)
 		{
 			report_corruption(ctx,
 							  psprintf("toasted attribute has unexpected TOAST tag %u",
-- 
2.50.0

v4-0004-Refactor-external-TOAST-pointer-code-for-better-p.patchtext/x-diff; charset=us-asciiDownload
From 39305f534c19648592b955b8bdc265ae16e50f67 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 8 Aug 2025 15:31:19 +0900
Subject: [PATCH v4 04/15] Refactor external TOAST pointer code for better
 pluggability

This commit introduces a new interface for external TOAST pointers,
which is able to make a translation of the varlena pointers stored on
disk to/from an new in-memory structure called toast_external.  The
types of varatt_external supported on disk need to be registered into a
new subsystem in a new file, called toast_external.[c|h], then define a
set of callbacks to allow the toasting and detoasting code to use it.

A follow-up change will rely on this refactoring to introduce new
vartag_external values with an associated varatt_external_* that is
able, which would be used in int8 TOAST tables.
---
 src/include/access/detoast.h                  |  12 +-
 src/include/access/heaptoast.h                |   5 +-
 src/include/access/toast_external.h           | 176 ++++++++++++++++++
 src/include/access/toast_helper.h             |   1 +
 src/include/varatt.h                          |  16 +-
 src/backend/access/common/Makefile            |   1 +
 src/backend/access/common/detoast.c           |  57 +++---
 src/backend/access/common/meson.build         |   1 +
 src/backend/access/common/toast_compression.c |  10 +-
 src/backend/access/common/toast_external.c    | 161 ++++++++++++++++
 src/backend/access/common/toast_internals.c   |  84 ++++++---
 src/backend/access/heap/heaptoast.c           |  20 +-
 src/backend/access/table/toast_helper.c       |  12 +-
 .../replication/logical/reorderbuffer.c       |  13 +-
 src/backend/utils/adt/varlena.c               |   7 +-
 doc/src/sgml/storage.sgml                     |   2 +-
 contrib/amcheck/verify_heapam.c               |  35 ++--
 src/tools/pgindent/typedefs.list              |   2 +
 18 files changed, 507 insertions(+), 108 deletions(-)
 create mode 100644 src/include/access/toast_external.h
 create mode 100644 src/backend/access/common/toast_external.c

diff --git a/src/include/access/detoast.h b/src/include/access/detoast.h
index d80a62e64fd5..4195f7b5bdfd 100644
--- a/src/include/access/detoast.h
+++ b/src/include/access/detoast.h
@@ -14,10 +14,11 @@
 
 /*
  * Macro to fetch the possibly-unaligned contents of an EXTERNAL datum
- * into a local "struct varatt_external_oid" toast pointer.  This should be
- * just a memcpy, but some versions of gcc seem to produce broken code
- * that assumes the datum contents are aligned.  Introducing an explicit
- * intermediate "varattrib_1b_e *" variable seems to fix it.
+ * into a local "struct varatt_external_*" toast pointer, as supported
+ * in toast_external.c and varatt.h.  This should be just a memcpy, but
+ * some versions of gcc seem to produce broken code that assumes the datum
+ * contents are aligned.  Introducing an explicit intermediate
+ * "varattrib_1b_e *" variable seems to fix it.
  */
 #define VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr) \
 do { \
@@ -27,9 +28,6 @@ do { \
 	memcpy(&(toast_pointer), VARDATA_EXTERNAL(attre), sizeof(toast_pointer)); \
 } while (0)
 
-/* Size of an EXTERNAL datum that contains a standard TOAST pointer */
-#define TOAST_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_oid))
-
 /* Size of an EXTERNAL datum that contains an indirection pointer */
 #define INDIRECT_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_indirect))
 
diff --git a/src/include/access/heaptoast.h b/src/include/access/heaptoast.h
index 6e3558cbd6d2..673e96f5488c 100644
--- a/src/include/access/heaptoast.h
+++ b/src/include/access/heaptoast.h
@@ -81,13 +81,16 @@
 
 #define EXTERN_TUPLE_MAX_SIZE	MaximumBytesPerTuple(EXTERN_TUPLES_PER_PAGE)
 
-#define TOAST_MAX_CHUNK_SIZE	\
+#define TOAST_MAX_CHUNK_SIZE_OID	\
 	(EXTERN_TUPLE_MAX_SIZE -							\
 	 MAXALIGN(SizeofHeapTupleHeader) -					\
 	 sizeof(Oid) -										\
 	 sizeof(int32) -									\
 	 VARHDRSZ)
 
+/* Maximum size of chunk possible */
+#define TOAST_MAX_CHUNK_SIZE	TOAST_MAX_CHUNK_SIZE_OID
+
 /* ----------
  * heap_toast_insert_or_update -
  *
diff --git a/src/include/access/toast_external.h b/src/include/access/toast_external.h
new file mode 100644
index 000000000000..ad2a10f35c01
--- /dev/null
+++ b/src/include/access/toast_external.h
@@ -0,0 +1,176 @@
+/*-------------------------------------------------------------------------
+ *
+ * toast_external.h
+ *	  Support for on-disk external TOAST pointers
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1995, Regents of the University of California
+ *
+ * src/include/access/toast_external.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef TOAST_EXTERNAL_H
+#define TOAST_EXTERNAL_H
+
+#include "access/toast_compression.h"
+#include "varatt.h"
+
+/*
+ * Intermediate in-memory structure used when creating on-disk
+ * varatt_external_* or when deserializing varlena contents.
+ */
+typedef struct toast_external_data
+{
+	/* Original data size (includes header) */
+	int32		rawsize;
+
+	/* External saved size (without header) */
+	uint32		extsize;
+
+	/*
+	 * Compression method.
+	 *
+	 * If not compressed, set to TOAST_INVALID_COMPRESSION_ID.
+	 */
+	ToastCompressionId compression_method;
+
+	/* Relation OID of TOAST table containing the value */
+	Oid			toastrelid;
+
+	/*
+	 * Unique ID of value within TOAST table.  This could be an OID or an int8
+	 * value.  This field is large enough to be able to store any of these.
+	 */
+	uint64		value;
+} toast_external_data;
+
+/*
+ * Metadata for external TOAST pointer kinds, separated based on their
+ * vartag_external.
+ */
+typedef struct toast_external_info
+{
+	/*
+	 * Maximum chunk of data authorized for this type of external TOAST
+	 * pointer, when dividing an entry by chunks.  Sized depending on the size
+	 * of its varatt_external_* structure.
+	 */
+	int32		maximum_chunk_size;
+
+	/*
+	 * Size of an external TOAST pointer of this type, typically
+	 * (VARHDRSZ_EXTERNAL + sizeof(varatt_external_struct)).
+	 */
+	int32		toast_pointer_size;
+
+	/*
+	 * Map an input varlena to a toast_external_data, for consumption in the
+	 * backend code.  "data" is an input/output result.
+	 */
+	void		(*to_external_data) (struct varlena *attr,
+									 toast_external_data *data);
+
+	/*
+	 * Create a varlena that will be used on-disk for the given TOAST type,
+	 * based on the given input data.
+	 *
+	 * The result is the varlena created, for on-disk insertion.
+	 */
+	struct varlena *(*create_external_data) (toast_external_data data);
+
+} toast_external_info;
+
+/* Retrieve a toast_external_info from a vartag */
+extern const toast_external_info *toast_external_get_info(uint8 tag);
+
+/* Retrieve toast_pointer_size using a TOAST attribute type */
+extern int32 toast_external_info_get_pointer_size(uint8 tag);
+
+/* Retrieve the vartag to assign to a TOAST typle */
+extern uint8 toast_external_assign_vartag(Oid toastrelid, uint64 value);
+
+/*
+ * Testing whether an externally-stored value is compressed now requires
+ * comparing size stored in extsize (the actual length of the external data)
+ * to rawsize (the original uncompressed datum's size).  The latter includes
+ * VARHDRSZ overhead, the former doesn't.  We never use compression unless it
+ * actually saves space, so we expect either equality or less-than.
+ */
+static inline bool
+TOAST_EXTERNAL_IS_COMPRESSED(toast_external_data data)
+{
+	return data.extsize < (data.rawsize - VARHDRSZ);
+}
+
+/* Full data structure */
+static inline void
+toast_external_info_get_data(struct varlena *attr, toast_external_data *data)
+{
+	uint8		tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+
+	info->to_external_data(attr, data);
+}
+
+/*
+ * Helper routines to recover specific fields in toast_external_data.  Most
+ * code paths doing work with on-disk external TOAST pointers care about
+ * these.
+ */
+
+/* Detoasted "raw" size */
+static inline Size
+toast_external_info_get_rawsize(struct varlena *attr)
+{
+	uint8		tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+	toast_external_data data;
+
+	info->to_external_data(attr, &data);
+
+	return data.rawsize;
+}
+
+/* External saved size */
+static inline Size
+toast_external_info_get_extsize(struct varlena *attr)
+{
+	uint8		tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+	toast_external_data data;
+
+	info->to_external_data(attr, &data);
+
+	return data.extsize;
+}
+
+/* Compression method ID */
+static inline ToastCompressionId
+toast_external_info_get_compression_method(struct varlena *attr)
+{
+	uint8		tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+	toast_external_data data;
+
+	info->to_external_data(attr, &data);
+
+	return data.compression_method;
+}
+
+/* Value ID */
+static inline Size
+toast_external_info_get_value(struct varlena *attr)
+{
+	uint8		tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+	toast_external_data data;
+
+	info->to_external_data(attr, &data);
+
+	return data.value;
+}
+
+#endif							/* TOAST_EXTERNAL_H */
diff --git a/src/include/access/toast_helper.h b/src/include/access/toast_helper.h
index e6ab8afffb67..6bc912809f34 100644
--- a/src/include/access/toast_helper.h
+++ b/src/include/access/toast_helper.h
@@ -47,6 +47,7 @@ typedef struct
 	 * should be NULL in the case of an insert.
 	 */
 	Relation	ttc_rel;		/* the relation that contains the tuple */
+	int32		ttc_toast_pointer_size; /* size of external TOAST pointer */
 	Datum	   *ttc_values;		/* values from the tuple columns */
 	bool	   *ttc_isnull;		/* null flags for the tuple columns */
 	Datum	   *ttc_oldvalues;	/* values from previous tuple */
diff --git a/src/include/varatt.h b/src/include/varatt.h
index fcf2dffaa2fd..b791ce7847ed 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -21,6 +21,9 @@
  * The data is compressed if and only if the external size stored in
  * va_extinfo is less than va_rawsize - VARHDRSZ.
  *
+ * The value ID is an OID, used for TOAST relations with OID as attribute
+ * for chunk_id.
+ *
  * This struct must not contain any padding, because we sometimes compare
  * these pointers using memcmp.
  *
@@ -51,7 +54,7 @@ typedef struct varatt_external_oid
  * The creator of such a Datum is entirely responsible that the referenced
  * storage survives for as long as referencing pointer Datums can exist.
  *
- * Note that just as for struct varatt_external_oid, this struct is stored
+ * Note that just as for struct varatt_external_*, this struct is stored
  * unaligned within any containing tuple.
  */
 typedef struct varatt_indirect
@@ -66,7 +69,7 @@ typedef struct varatt_indirect
  * storage.  APIs for this, in particular the definition of struct
  * ExpandedObjectHeader, are in src/include/utils/expandeddatum.h.
  *
- * Note that just as for struct varatt_external_oid, this struct is stored
+ * Note that just as for struct varatt_external_*, this struct is stored
  * unaligned within any containing tuple.
  */
 typedef struct ExpandedObjectHeader ExpandedObjectHeader;
@@ -357,11 +360,18 @@ VARATT_IS_EXTERNAL(const void *PTR)
 	return VARATT_IS_1B_E(PTR);
 }
 
+/* Is varlena datum a pointer to on-disk toasted data with OID value? */
+static inline bool
+VARATT_IS_EXTERNAL_ONDISK_OID(const void *PTR)
+{
+	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID;
+}
+
 /* Is varlena datum a pointer to on-disk toasted data? */
 static inline bool
 VARATT_IS_EXTERNAL_ONDISK(const void *PTR)
 {
-	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID;
+	return VARATT_IS_EXTERNAL_ONDISK_OID(PTR);
 }
 
 /* Is varlena datum an indirect pointer? */
diff --git a/src/backend/access/common/Makefile b/src/backend/access/common/Makefile
index e78de312659e..1ef86a245886 100644
--- a/src/backend/access/common/Makefile
+++ b/src/backend/access/common/Makefile
@@ -27,6 +27,7 @@ OBJS = \
 	syncscan.o \
 	tidstore.o \
 	toast_compression.o \
+	toast_external.o \
 	toast_internals.o \
 	tupconvert.o \
 	tupdesc.o
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 04eedb474c74..6a9b5200203c 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -16,6 +16,7 @@
 #include "access/detoast.h"
 #include "access/table.h"
 #include "access/tableam.h"
+#include "access/toast_external.h"
 #include "access/toast_internals.h"
 #include "common/int.h"
 #include "common/pg_lzcompress.h"
@@ -225,12 +226,12 @@ detoast_attr_slice(struct varlena *attr,
 
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
-		struct varatt_external_oid toast_pointer;
+		struct toast_external_data toast_pointer;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		toast_external_info_get_data(attr, &toast_pointer);
 
 		/* fast path for non-compressed external datums */
-		if (!VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+		if (!TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 			return toast_fetch_datum_slice(attr, sliceoffset, slicelength);
 
 		/*
@@ -240,7 +241,7 @@ detoast_attr_slice(struct varlena *attr,
 		 */
 		if (slicelimit >= 0)
 		{
-			int32		max_size = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+			int32		max_size = toast_pointer.extsize;
 
 			/*
 			 * Determine maximum amount of compressed data needed for a prefix
@@ -251,8 +252,7 @@ detoast_attr_slice(struct varlena *attr,
 			 * determine how much compressed data we need to be sure of being
 			 * able to decompress the required slice.
 			 */
-			if (VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) ==
-				TOAST_PGLZ_COMPRESSION_ID)
+			if (toast_pointer.compression_method == TOAST_PGLZ_COMPRESSION_ID)
 				max_size = pglz_maximum_compressed_size(slicelimit, max_size);
 
 			/*
@@ -344,20 +344,21 @@ toast_fetch_datum(struct varlena *attr)
 {
 	Relation	toastrel;
 	struct varlena *result;
-	struct varatt_external_oid toast_pointer;
+	struct toast_external_data toast_pointer;
 	int32		attrsize;
+	uint64		valueid;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		elog(ERROR, "toast_fetch_datum shouldn't be called for non-ondisk datums");
 
 	/* Must copy to access aligned fields */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_external_info_get_data(attr, &toast_pointer);
 
-	attrsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+	attrsize = toast_pointer.extsize;
 
 	result = (struct varlena *) palloc(attrsize + VARHDRSZ);
 
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+	if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 		SET_VARSIZE_COMPRESSED(result, attrsize + VARHDRSZ);
 	else
 		SET_VARSIZE(result, attrsize + VARHDRSZ);
@@ -365,14 +366,15 @@ toast_fetch_datum(struct varlena *attr)
 	if (attrsize == 0)
 		return result;			/* Probably shouldn't happen, but just in
 								 * case. */
+	valueid = toast_pointer.value;
 
 	/*
 	 * Open the toast relation and its indexes
 	 */
-	toastrel = table_open(toast_pointer.va_toastrelid, AccessShareLock);
+	toastrel = table_open(toast_pointer.toastrelid, AccessShareLock);
 
 	/* Fetch all chunks */
-	table_relation_fetch_toast_slice(toastrel, toast_pointer.va_valueid,
+	table_relation_fetch_toast_slice(toastrel, valueid,
 									 attrsize, 0, attrsize, result);
 
 	/* Close toast table */
@@ -398,23 +400,26 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 {
 	Relation	toastrel;
 	struct varlena *result;
-	struct varatt_external_oid toast_pointer;
+	struct toast_external_data toast_pointer;
 	int32		attrsize;
+	uint64		valueid;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		elog(ERROR, "toast_fetch_datum_slice shouldn't be called for non-ondisk datums");
 
 	/* Must copy to access aligned fields */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_external_info_get_data(attr, &toast_pointer);
+
+	valueid = toast_pointer.value;
 
 	/*
 	 * It's nonsense to fetch slices of a compressed datum unless when it's a
 	 * prefix -- this isn't lo_* we can't return a compressed datum which is
 	 * meaningful to toast later.
 	 */
-	Assert(!VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer) || 0 == sliceoffset);
+	Assert(!TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer) || 0 == sliceoffset);
 
-	attrsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+	attrsize = toast_pointer.extsize;
 
 	if (sliceoffset >= attrsize)
 	{
@@ -427,7 +432,7 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 	 * space required by va_tcinfo, which is stored at the beginning as an
 	 * int32 value.
 	 */
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer) && slicelength > 0)
+	if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer) && slicelength > 0)
 		slicelength = slicelength + sizeof(int32);
 
 	/*
@@ -440,7 +445,7 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 
 	result = (struct varlena *) palloc(slicelength + VARHDRSZ);
 
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+	if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 		SET_VARSIZE_COMPRESSED(result, slicelength + VARHDRSZ);
 	else
 		SET_VARSIZE(result, slicelength + VARHDRSZ);
@@ -449,10 +454,11 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 		return result;			/* Can save a lot of work at this point! */
 
 	/* Open the toast relation */
-	toastrel = table_open(toast_pointer.va_toastrelid, AccessShareLock);
+	toastrel = table_open(toast_pointer.toastrelid, AccessShareLock);
 
 	/* Fetch all chunks */
-	table_relation_fetch_toast_slice(toastrel, toast_pointer.va_valueid,
+	table_relation_fetch_toast_slice(toastrel,
+									 valueid,
 									 attrsize, sliceoffset, slicelength,
 									 result);
 
@@ -549,11 +555,7 @@ toast_raw_datum_size(Datum value)
 
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
-		/* va_rawsize is the size of the original datum -- including header */
-		struct varatt_external_oid toast_pointer;
-
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-		result = toast_pointer.va_rawsize;
+		result = toast_external_info_get_rawsize(attr);
 	}
 	else if (VARATT_IS_EXTERNAL_INDIRECT(attr))
 	{
@@ -610,10 +612,7 @@ toast_datum_size(Datum value)
 		 * compressed or not.  We do not count the size of the toast pointer
 		 * ... should we?
 		 */
-		struct varatt_external_oid toast_pointer;
-
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-		result = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+		result = toast_external_info_get_extsize(attr);
 	}
 	else if (VARATT_IS_EXTERNAL_INDIRECT(attr))
 	{
diff --git a/src/backend/access/common/meson.build b/src/backend/access/common/meson.build
index e3cdbe7a22e1..c20f2e88921e 100644
--- a/src/backend/access/common/meson.build
+++ b/src/backend/access/common/meson.build
@@ -15,6 +15,7 @@ backend_sources += files(
   'syncscan.c',
   'tidstore.c',
   'toast_compression.c',
+  'toast_external.c',
   'toast_internals.c',
   'tupconvert.c',
   'tupdesc.c',
diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c
index 26aad84d367a..94606a58c8fb 100644
--- a/src/backend/access/common/toast_compression.c
+++ b/src/backend/access/common/toast_compression.c
@@ -19,6 +19,7 @@
 
 #include "access/detoast.h"
 #include "access/toast_compression.h"
+#include "access/toast_external.h"
 #include "common/pg_lzcompress.h"
 #include "varatt.h"
 
@@ -261,14 +262,7 @@ toast_get_compression_id(struct varlena *attr)
 	 * toast compression header.
 	 */
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
-	{
-		struct varatt_external_oid toast_pointer;
-
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-
-		if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
-			cmid = VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer);
-	}
+		cmid = toast_external_info_get_compression_method(attr);
 	else if (VARATT_IS_COMPRESSED(attr))
 		cmid = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(attr);
 
diff --git a/src/backend/access/common/toast_external.c b/src/backend/access/common/toast_external.c
new file mode 100644
index 000000000000..96ea7be8966e
--- /dev/null
+++ b/src/backend/access/common/toast_external.c
@@ -0,0 +1,161 @@
+/*-------------------------------------------------------------------------
+ *
+ * toast_external.c
+ *	  Functions for the support of external on-disk TOAST pointers.
+ *
+ * Copyright (c) 2025, PostgreSQL Global Development Group
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/common/toast_external.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/detoast.h"
+#include "access/heaptoast.h"
+#include "access/toast_external.h"
+
+/* Callbacks for VARTAG_ONDISK_OID */
+static void ondisk_oid_to_external_data(struct varlena *attr,
+										toast_external_data *data);
+static struct varlena *ondisk_oid_create_external_data(toast_external_data data);
+
+
+/*
+ * Size of an EXTERNAL datum that contains a standard TOAST pointer (OID
+ * value).
+ */
+#define TOAST_POINTER_OID_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_oid))
+
+/*
+ * For now there are only two types, all defined in this file.  For now this
+ * is the maximum value of vartag_external, which is a historical choice.
+ */
+#define TOAST_EXTERNAL_INFO_SIZE	(VARTAG_ONDISK_OID + 1)
+
+/*
+ * The different kinds of on-disk external TOAST pointers, divided by
+ * vartag_external.
+ *
+ * See comments for struct toast_external_info about the details of the
+ * individual fields.
+ */
+static const toast_external_info toast_external_infos[TOAST_EXTERNAL_INFO_SIZE] = {
+	[VARTAG_ONDISK_OID] = {
+		.toast_pointer_size = TOAST_POINTER_OID_SIZE,
+		.maximum_chunk_size = TOAST_MAX_CHUNK_SIZE_OID,
+		.to_external_data = ondisk_oid_to_external_data,
+		.create_external_data = ondisk_oid_create_external_data,
+	},
+};
+
+
+/* Get toast_external_info of the defined vartag_external */
+const toast_external_info *
+toast_external_get_info(uint8 tag)
+{
+	return &toast_external_infos[tag];
+}
+
+/*
+ * Get external TOAST pointer size based on the attribute type of a TOAST
+ * value.
+ */
+int32
+toast_external_info_get_pointer_size(uint8 tag)
+{
+	return toast_external_infos[tag].toast_pointer_size;
+}
+
+/*
+ * Assign the vartag_external of a TOAST tuple, based on the TOAST relation
+ * it uses and its value.
+ *
+ * An invalid value can be given by the caller of this routine, in which
+ * case a default vartag should be provided based on only the toast relation
+ * used.
+ */
+uint8
+toast_external_assign_vartag(Oid toastrelid, uint64 value)
+{
+	/*
+	 * If dealing with a code path where a TOAST relation may not be assigned,
+	 * like heap_toast_insert_or_update(), just use the legacy
+	 * vartag_external.
+	 */
+	if (!OidIsValid(toastrelid))
+		return VARTAG_ONDISK_OID;
+
+	/*
+	 * Currently there is only one type of vartag_external supported: 4-byte
+	 * value with OID for the chunk_id type.
+	 *
+	 * Note: This routine will be extended to be able to use multiple
+	 * vartag_external within a single TOAST relation type, that may change
+	 * depending on the value used.
+	 */
+	return VARTAG_ONDISK_OID;
+}
+
+/*
+ * Helper routines able to translate the various varatt_external_* from/to
+ * the in-memory representation toast_external_data used in the backend.
+ */
+
+/* Callbacks for VARTAG_ONDISK_OID */
+static void
+ondisk_oid_to_external_data(struct varlena *attr, toast_external_data *data)
+{
+	varatt_external_oid external;
+
+	VARATT_EXTERNAL_GET_POINTER(external, attr);
+	data->rawsize = external.va_rawsize;
+
+	/*
+	 * External size and compression methods are stored in the same field,
+	 * extract.
+	 */
+	if (VARATT_EXTERNAL_IS_COMPRESSED(external))
+	{
+		data->extsize = VARATT_EXTERNAL_GET_EXTSIZE(external);
+		data->compression_method = VARATT_EXTERNAL_GET_COMPRESS_METHOD(external);
+	}
+	else
+	{
+		data->extsize = external.va_extinfo;
+		data->compression_method = TOAST_INVALID_COMPRESSION_ID;
+	}
+
+	data->value = (uint64) external.va_valueid;
+	data->toastrelid = external.va_toastrelid;
+}
+
+static struct varlena *
+ondisk_oid_create_external_data(toast_external_data data)
+{
+	struct varlena *result = NULL;
+	varatt_external_oid external;
+
+	external.va_rawsize = data.rawsize;
+
+	if (data.compression_method != TOAST_INVALID_COMPRESSION_ID)
+	{
+		/* Set size and compression method, in a single field. */
+		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(external,
+													 data.extsize,
+													 data.compression_method);
+	}
+	else
+		external.va_extinfo = data.extsize;
+
+	external.va_toastrelid = data.toastrelid;
+	external.va_valueid = (Oid) data.value;
+
+	result = (struct varlena *) palloc(TOAST_POINTER_OID_SIZE);
+	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_OID);
+	memcpy(VARDATA_EXTERNAL(result), &external, sizeof(external));
+
+	return result;
+}
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index d555c02e908e..f659ec41df34 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -18,6 +18,7 @@
 #include "access/heapam.h"
 #include "access/heaptoast.h"
 #include "access/table.h"
+#include "access/toast_external.h"
 #include "access/toast_internals.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -127,7 +128,7 @@ toast_save_datum(Relation rel, Datum value,
 	bool		t_isnull[3];
 	CommandId	mycid = GetCurrentCommandId(true);
 	struct varlena *result;
-	struct varatt_external_oid toast_pointer;
+	struct toast_external_data toast_pointer;
 	union
 	{
 		struct varlena hdr;
@@ -143,6 +144,8 @@ toast_save_datum(Relation rel, Datum value,
 	Pointer		dval = DatumGetPointer(value);
 	int			num_indexes;
 	int			validIndex;
+	const toast_external_info *info;
+	uint8		tag = VARTAG_INDIRECT;	/* init value does not matter */
 
 	Assert(!VARATT_IS_EXTERNAL(dval));
 
@@ -174,28 +177,41 @@ toast_save_datum(Relation rel, Datum value,
 	{
 		data_p = VARDATA_SHORT(dval);
 		data_todo = VARSIZE_SHORT(dval) - VARHDRSZ_SHORT;
-		toast_pointer.va_rawsize = data_todo + VARHDRSZ;	/* as if not short */
-		toast_pointer.va_extinfo = data_todo;
+		toast_pointer.rawsize = data_todo + VARHDRSZ;	/* as if not short */
+		toast_pointer.extsize = data_todo;
+
+		/*
+		 * TOAST_INVALID_COMPRESSION_ID means that the varlena is not
+		 * compressed, see toast_get_compression_id().
+		 */
+		toast_pointer.compression_method = TOAST_INVALID_COMPRESSION_ID;
 	}
 	else if (VARATT_IS_COMPRESSED(dval))
 	{
 		data_p = VARDATA(dval);
 		data_todo = VARSIZE(dval) - VARHDRSZ;
 		/* rawsize in a compressed datum is just the size of the payload */
-		toast_pointer.va_rawsize = VARDATA_COMPRESSED_GET_EXTSIZE(dval) + VARHDRSZ;
+		toast_pointer.rawsize = VARDATA_COMPRESSED_GET_EXTSIZE(dval) + VARHDRSZ;
 
 		/* set external size and compression method */
-		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, data_todo,
-													 VARDATA_COMPRESSED_GET_COMPRESS_METHOD(dval));
+		toast_pointer.extsize = data_todo;
+		toast_pointer.compression_method = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(dval);
+
 		/* Assert that the numbers look like it's compressed */
-		Assert(VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer));
+		Assert(TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer));
 	}
 	else
 	{
 		data_p = VARDATA(dval);
 		data_todo = VARSIZE(dval) - VARHDRSZ;
-		toast_pointer.va_rawsize = VARSIZE(dval);
-		toast_pointer.va_extinfo = data_todo;
+		toast_pointer.rawsize = VARSIZE(dval);
+		toast_pointer.extsize = data_todo;
+
+		/*
+		 * TOAST_INVALID_COMPRESSION_ID means that the varlena is not
+		 * compressed, see toast_get_compression_id().
+		 */
+		toast_pointer.compression_method = TOAST_INVALID_COMPRESSION_ID;
 	}
 
 	/*
@@ -207,9 +223,9 @@ toast_save_datum(Relation rel, Datum value,
 	 * if we have to substitute such an OID.
 	 */
 	if (OidIsValid(rel->rd_toastoid))
-		toast_pointer.va_toastrelid = rel->rd_toastoid;
+		toast_pointer.toastrelid = rel->rd_toastoid;
 	else
-		toast_pointer.va_toastrelid = RelationGetRelid(toastrel);
+		toast_pointer.toastrelid = RelationGetRelid(toastrel);
 
 	/*
 	 * Choose an OID to use as the value ID for this toast value.
@@ -226,7 +242,7 @@ toast_save_datum(Relation rel, Datum value,
 	if (!OidIsValid(rel->rd_toastoid))
 	{
 		/* normal case: just choose an unused OID */
-		toast_pointer.va_valueid =
+		toast_pointer.value =
 			GetNewOidWithIndex(toastrel,
 							   RelationGetRelid(toastidxs[validIndex]),
 							   (AttrNumber) 1);
@@ -234,18 +250,18 @@ toast_save_datum(Relation rel, Datum value,
 	else
 	{
 		/* rewrite case: check to see if value was in old toast table */
-		toast_pointer.va_valueid = InvalidOid;
+		toast_pointer.value = InvalidOid;
 		if (oldexternal != NULL)
 		{
-			struct varatt_external_oid old_toast_pointer;
+			struct toast_external_data old_toast_pointer;
 
 			Assert(VARATT_IS_EXTERNAL_ONDISK(oldexternal));
-			/* Must copy to access aligned fields */
-			VARATT_EXTERNAL_GET_POINTER(old_toast_pointer, oldexternal);
-			if (old_toast_pointer.va_toastrelid == rel->rd_toastoid)
+			toast_external_info_get_data(oldexternal, &old_toast_pointer);
+
+			if (old_toast_pointer.toastrelid == rel->rd_toastoid)
 			{
 				/* This value came from the old toast table; reuse its OID */
-				toast_pointer.va_valueid = old_toast_pointer.va_valueid;
+				toast_pointer.value = old_toast_pointer.value;
 
 				/*
 				 * There is a corner case here: the table rewrite might have
@@ -265,14 +281,14 @@ toast_save_datum(Relation rel, Datum value,
 				 * be reclaimed by VACUUM.
 				 */
 				if (toastrel_valueid_exists(toastrel,
-											toast_pointer.va_valueid))
+											toast_pointer.value))
 				{
 					/* Match, so short-circuit the data storage loop below */
 					data_todo = 0;
 				}
 			}
 		}
-		if (toast_pointer.va_valueid == InvalidOid)
+		if (toast_pointer.value == InvalidOid)
 		{
 			/*
 			 * new value; must choose an OID that doesn't conflict in either
@@ -280,24 +296,32 @@ toast_save_datum(Relation rel, Datum value,
 			 */
 			do
 			{
-				toast_pointer.va_valueid =
+				toast_pointer.value =
 					GetNewOidWithIndex(toastrel,
 									   RelationGetRelid(toastidxs[validIndex]),
 									   (AttrNumber) 1);
 			} while (toastid_valueid_exists(rel->rd_toastoid,
-											toast_pointer.va_valueid));
+											toast_pointer.value));
 		}
 	}
 
 	/*
 	 * Initialize constant parts of the tuple data
 	 */
-	t_values[0] = ObjectIdGetDatum(toast_pointer.va_valueid);
+	t_values[0] = ObjectIdGetDatum(toast_pointer.value);
 	t_values[2] = PointerGetDatum(&chunk_data);
 	t_isnull[0] = false;
 	t_isnull[1] = false;
 	t_isnull[2] = false;
 
+	/*
+	 * Retrieve the vartag that can be assigned for the new TOAST tuple. This
+	 * depends on the type of TOAST table and its assigned value.
+	 */
+	tag = toast_external_assign_vartag(toast_pointer.toastrelid,
+									   toast_pointer.value);
+	info = toast_external_get_info(tag);
+
 	/*
 	 * Split up the item into chunks
 	 */
@@ -310,7 +334,7 @@ toast_save_datum(Relation rel, Datum value,
 		/*
 		 * Calculate the size of this chunk
 		 */
-		chunk_size = Min(TOAST_MAX_CHUNK_SIZE, data_todo);
+		chunk_size = Min(info->maximum_chunk_size, data_todo);
 
 		/*
 		 * Build a tuple and store it
@@ -368,9 +392,7 @@ toast_save_datum(Relation rel, Datum value,
 	/*
 	 * Create the TOAST pointer value that we'll return
 	 */
-	result = (struct varlena *) palloc(TOAST_POINTER_SIZE);
-	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_OID);
-	memcpy(VARDATA_EXTERNAL(result), &toast_pointer, sizeof(toast_pointer));
+	result = info->create_external_data(toast_pointer);
 
 	return PointerGetDatum(result);
 }
@@ -385,7 +407,7 @@ void
 toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 {
 	struct varlena *attr = (struct varlena *) DatumGetPointer(value);
-	struct varatt_external_oid toast_pointer;
+	struct toast_external_data toast_pointer;
 	Relation	toastrel;
 	Relation   *toastidxs;
 	ScanKeyData toastkey;
@@ -398,12 +420,12 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 		return;
 
 	/* Must copy to access aligned fields */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_external_info_get_data(attr, &toast_pointer);
 
 	/*
 	 * Open the toast relation and its indexes
 	 */
-	toastrel = table_open(toast_pointer.va_toastrelid, RowExclusiveLock);
+	toastrel = table_open(toast_pointer.toastrelid, RowExclusiveLock);
 
 	/* Fetch valid relation used for process */
 	validIndex = toast_open_indexes(toastrel,
@@ -417,7 +439,7 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	ScanKeyInit(&toastkey,
 				(AttrNumber) 1,
 				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(toast_pointer.va_valueid));
+				ObjectIdGetDatum(toast_pointer.value));
 
 	/*
 	 * Find all the chunks.  (We don't actually care whether we see them in
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index ae8d502ddcd3..47bf9e84963a 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -28,6 +28,7 @@
 #include "access/genam.h"
 #include "access/heapam.h"
 #include "access/heaptoast.h"
+#include "access/toast_external.h"
 #include "access/toast_helper.h"
 #include "access/toast_internals.h"
 #include "utils/fmgroids.h"
@@ -109,6 +110,7 @@ heap_toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup,
 	Datum		toast_oldvalues[MaxHeapAttributeNumber];
 	ToastAttrInfo toast_attr[MaxHeapAttributeNumber];
 	ToastTupleContext ttc;
+	uint8		tag;
 
 	/*
 	 * Ignore the INSERT_SPECULATIVE option. Speculative insertions/super
@@ -140,6 +142,16 @@ heap_toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup,
 	 * Prepare for toasting
 	 * ----------
 	 */
+
+	/*
+	 * Retrieve the toast pointer size based on the type of external TOAST
+	 * pointer assumed to be used.
+	 */
+
+	/* The default value is invalid, to work as a default. */
+	tag = toast_external_assign_vartag(rel->rd_rel->reltoastrelid, InvalidOid);
+	ttc.ttc_toast_pointer_size = toast_external_info_get_pointer_size(tag);
+
 	ttc.ttc_rel = rel;
 	ttc.ttc_values = toast_values;
 	ttc.ttc_isnull = toast_isnull;
@@ -640,6 +652,8 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 	int			num_indexes;
 	int			validIndex;
 	int32		max_chunk_size;
+	const toast_external_info *info;
+	uint8		tag = VARTAG_INDIRECT;	/* init value does not matter */
 
 	/* Look for the valid index of toast relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -647,7 +661,11 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 									&toastidxs,
 									&num_indexes);
 
-	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	/* Grab the information for toast_external_data */
+	tag = toast_external_assign_vartag(RelationGetRelid(toastrel), valueid);
+	info = toast_external_get_info(tag);
+
+	max_chunk_size = info->maximum_chunk_size;
 
 	totalchunks = ((attrsize - 1) / max_chunk_size) + 1;
 	startchunk = sliceoffset / max_chunk_size;
diff --git a/src/backend/access/table/toast_helper.c b/src/backend/access/table/toast_helper.c
index 11f97d65367d..76a7cfe6174e 100644
--- a/src/backend/access/table/toast_helper.c
+++ b/src/backend/access/table/toast_helper.c
@@ -171,8 +171,10 @@ toast_tuple_init(ToastTupleContext *ttc)
  * The column must have attstorage EXTERNAL or EXTENDED if check_main is
  * false, and must have attstorage MAIN if check_main is true.
  *
- * The column must have a minimum size of MAXALIGN(TOAST_POINTER_SIZE);
- * if not, no benefit is to be expected by compressing it.
+ * The column must have a minimum size of MAXALIGN(tcc_toast_pointer_size);
+ * if not, no benefit is to be expected by compressing it.  The TOAST
+ * pointer size is given by the caller, depending on the type of TOAST
+ * table we are dealing with.
  *
  * The return value is the index of the biggest suitable column, or
  * -1 if there is none.
@@ -184,10 +186,14 @@ toast_tuple_find_biggest_attribute(ToastTupleContext *ttc,
 	TupleDesc	tupleDesc = ttc->ttc_rel->rd_att;
 	int			numAttrs = tupleDesc->natts;
 	int			biggest_attno = -1;
-	int32		biggest_size = MAXALIGN(TOAST_POINTER_SIZE);
+	int32		biggest_size = 0;
 	int32		skip_colflags = TOASTCOL_IGNORE;
 	int			i;
 
+	/* Define the lower-bound */
+	biggest_size = MAXALIGN(ttc->ttc_toast_pointer_size);
+	Assert(biggest_size != 0);
+
 	if (for_compression)
 		skip_colflags |= TOASTCOL_INCOMPRESSIBLE;
 
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 0eb99b4f1377..be33e7de6f8d 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -92,6 +92,7 @@
 #include "access/detoast.h"
 #include "access/heapam.h"
 #include "access/rewriteheap.h"
+#include "access/toast_external.h"
 #include "access/transam.h"
 #include "access/xact.h"
 #include "access/xlog_internal.h"
@@ -5102,7 +5103,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		struct varlena *varlena;
 
 		/* va_rawsize is the size of the original datum -- including header */
-		struct varatt_external_oid toast_pointer;
+		struct toast_external_data toast_pointer;
 		struct varatt_indirect redirect_pointer;
 		struct varlena *new_datum = NULL;
 		struct varlena *reconstructed;
@@ -5132,8 +5133,8 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		if (!VARATT_IS_EXTERNAL(varlena))
 			continue;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, varlena);
-		toast_valueid = toast_pointer.va_valueid;
+		toast_external_info_get_data(varlena, &toast_pointer);
+		toast_valueid = toast_pointer.value;
 
 		/*
 		 * Check whether the toast tuple changed, replace if so.
@@ -5151,7 +5152,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 
 		free[natt] = true;
 
-		reconstructed = palloc0(toast_pointer.va_rawsize);
+		reconstructed = palloc0(toast_pointer.rawsize);
 
 		ent->reconstructed = reconstructed;
 
@@ -5176,10 +5177,10 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 				   VARSIZE(chunk) - VARHDRSZ);
 			data_done += VARSIZE(chunk) - VARHDRSZ;
 		}
-		Assert(data_done == VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer));
+		Assert(data_done == toast_pointer.extsize);
 
 		/* make sure its marked as compressed or not */
-		if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+		if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 			SET_VARSIZE_COMPRESSED(reconstructed, data_done + VARHDRSZ);
 		else
 			SET_VARSIZE(reconstructed, data_done + VARHDRSZ);
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 38d4e6d45f28..d76386407a08 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -19,6 +19,7 @@
 
 #include "access/detoast.h"
 #include "access/toast_compression.h"
+#include "access/toast_external.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_type.h"
 #include "common/hashfn.h"
@@ -4219,7 +4220,7 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 {
 	int			typlen;
 	struct varlena *attr;
-	struct varatt_external_oid toast_pointer;
+	uint64		toast_valueid;
 
 	/* On first call, get the input type's typlen, and save at *fn_extra */
 	if (fcinfo->flinfo->fn_extra == NULL)
@@ -4246,9 +4247,9 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		PG_RETURN_NULL();
 
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_valueid = toast_external_info_get_value(attr);
 
-	PG_RETURN_OID(toast_pointer.va_valueid);
+	PG_RETURN_OID(toast_valueid);
 }
 
 /*
diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml
index 61250799ec07..f3c6cd8860b5 100644
--- a/doc/src/sgml/storage.sgml
+++ b/doc/src/sgml/storage.sgml
@@ -415,7 +415,7 @@ described in more detail below.
 
 <para>
 Out-of-line values are divided (after compression if used) into chunks of at
-most <symbol>TOAST_MAX_CHUNK_SIZE</symbol> bytes (by default this value is chosen
+most <symbol>TOAST_MAX_CHUNK_SIZE_OID</symbol> bytes (by default this value is chosen
 so that four chunk rows will fit on a page, making it about 2000 bytes).
 Each chunk is stored as a separate row in the <acronym>TOAST</acronym> table
 belonging to the owning table.  Every
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 030f9fb64b51..11c4507ae6e2 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -16,6 +16,7 @@
 #include "access/multixact.h"
 #include "access/relation.h"
 #include "access/table.h"
+#include "access/toast_external.h"
 #include "access/toast_internals.h"
 #include "access/visibilitymap.h"
 #include "access/xact.h"
@@ -73,7 +74,8 @@ typedef enum SkipPages
  */
 typedef struct ToastedAttribute
 {
-	struct varatt_external_oid toast_pointer;
+	struct toast_external_data toast_pointer;
+	const toast_external_info *info;
 	BlockNumber blkno;			/* block in main table */
 	OffsetNumber offnum;		/* offset in main table */
 	AttrNumber	attnum;			/* attribute in main table */
@@ -1564,9 +1566,9 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	uint64		toast_valueid;
 	int32		max_chunk_size;
 
-	toast_valueid = ta->toast_pointer.va_valueid;
+	toast_valueid = ta->toast_pointer.value;
 
-	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	max_chunk_size = ta->info->maximum_chunk_size;
 	last_chunk_seq = (extsize - 1) / max_chunk_size;
 
 	/* Sanity-check the sequence number. */
@@ -1672,7 +1674,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	uint16		infomask;
 	uint64		toast_pointer_valueid;
 	CompactAttribute *thisatt;
-	struct varatt_external_oid toast_pointer;
+	struct toast_external_data toast_pointer;
 
 	infomask = ctx->tuphdr->t_infomask;
 	thisatt = TupleDescCompactAttr(RelationGetDescr(ctx->rel), ctx->attnum);
@@ -1774,28 +1776,28 @@ check_tuple_attribute(HeapCheckContext *ctx)
 		return true;
 
 	/* It is external, and we're looking at a page on disk */
-	toast_pointer_valueid = toast_pointer.va_valueid;
 
 	/*
 	 * Must copy attr into toast_pointer for alignment considerations
 	 */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_external_info_get_data(attr, &toast_pointer);
+	toast_pointer_valueid = toast_pointer.value;
 
 	/* Toasted attributes too large to be untoasted should never be stored */
-	if (toast_pointer.va_rawsize > VARLENA_SIZE_LIMIT)
+	if (toast_pointer.rawsize > VARLENA_SIZE_LIMIT)
 		report_corruption(ctx,
 						  psprintf("toast value %" PRIu64 " rawsize %d exceeds limit %d",
 								   toast_pointer_valueid,
-								   toast_pointer.va_rawsize,
+								   toast_pointer.rawsize,
 								   VARLENA_SIZE_LIMIT));
 
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+	if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 	{
 		ToastCompressionId cmid;
 		bool		valid = false;
 
 		/* Compressed attributes should have a valid compression method */
-		cmid = TOAST_COMPRESS_METHOD(&toast_pointer);
+		cmid = toast_pointer.compression_method;
 		switch (cmid)
 		{
 				/* List of all valid compression method IDs */
@@ -1849,7 +1851,8 @@ check_tuple_attribute(HeapCheckContext *ctx)
 
 		ta = (ToastedAttribute *) palloc0(sizeof(ToastedAttribute));
 
-		VARATT_EXTERNAL_GET_POINTER(ta->toast_pointer, attr);
+		toast_external_info_get_data(attr, &ta->toast_pointer);
+		ta->info = toast_external_get_info(VARTAG_EXTERNAL(attr));
 		ta->blkno = ctx->blkno;
 		ta->offnum = ctx->offnum;
 		ta->attnum = ctx->attnum;
@@ -1876,9 +1879,11 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	int32		expected_chunk_seq = 0;
 	int32		last_chunk_seq;
 	uint64		toast_valueid;
-	int32		max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	int32		max_chunk_size;
 
-	extsize = VARATT_EXTERNAL_GET_EXTSIZE(ta->toast_pointer);
+	extsize = ta->toast_pointer.extsize;
+
+	max_chunk_size = ta->info->maximum_chunk_size;
 	last_chunk_seq = (extsize - 1) / max_chunk_size;
 
 	/*
@@ -1887,7 +1892,7 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	ScanKeyInit(&toastkey,
 				(AttrNumber) 1,
 				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(ta->toast_pointer.va_valueid));
+				ObjectIdGetDatum(ta->toast_pointer.value));
 
 	/*
 	 * Check if any chunks for this toasted object exist in the toast table,
@@ -1907,7 +1912,7 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	}
 	systable_endscan_ordered(toastscan);
 
-	toast_valueid = ta->toast_pointer.va_valueid;
+	toast_valueid = ta->toast_pointer.value;
 
 	if (!found_toasttup)
 		report_toast_corruption(ctx, ta,
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index e6f2e93b2d6f..995dc1f28208 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -4143,6 +4143,8 @@ timeout_params
 timerCA
 tlist_vinfo
 toast_compress_header
+toast_external_data
+toast_external_info
 tokenize_error_callback_arg
 transferMode
 transfer_thread_arg
-- 
2.50.0

v4-0005-Move-static-inline-routines-of-varatt_external_oi.patchtext/x-diff; charset=us-asciiDownload
From f91260e4b14d11a9189034aed1edcab81da766e8 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 8 Aug 2025 15:40:04 +0900
Subject: [PATCH v4 05/15] Move static inline routines of varatt_external_oid
 to toast_external.c

This isolates most of the knowledge of varatt_external_oid into the
local area where it is manipulated through the toast_external transition
type, with the backend code not requiring it.  Extension code should not
need it either, as toast_external should be the layer to use when
looking at external on-dist TOAST varlenas.
---
 src/include/varatt.h                       | 30 -----------------
 src/backend/access/common/toast_external.c | 39 ++++++++++++++++++++--
 2 files changed, 36 insertions(+), 33 deletions(-)

diff --git a/src/include/varatt.h b/src/include/varatt.h
index b791ce7847ed..631aa2ecc494 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -513,22 +513,6 @@ VARDATA_COMPRESSED_GET_COMPRESS_METHOD(const void *PTR)
 	return ((varattrib_4b *) PTR)->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS;
 }
 
-/*
- * Same for external Datums; but note argument is a struct
- * varatt_external_oid.
- */
-static inline Size
-VARATT_EXTERNAL_GET_EXTSIZE(struct varatt_external_oid toast_pointer)
-{
-	return toast_pointer.va_extinfo & VARLENA_EXTSIZE_MASK;
-}
-
-static inline uint32
-VARATT_EXTERNAL_GET_COMPRESS_METHOD(struct varatt_external_oid toast_pointer)
-{
-	return toast_pointer.va_extinfo >> VARLENA_EXTSIZE_BITS;
-}
-
 /* Set size and compress method of an externally-stored varlena datum */
 /* This has to remain a macro; beware multiple evaluations! */
 #define VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, len, cm) \
@@ -539,18 +523,4 @@ VARATT_EXTERNAL_GET_COMPRESS_METHOD(struct varatt_external_oid toast_pointer)
 			(len) | ((uint32) (cm) << VARLENA_EXTSIZE_BITS)); \
 	} while (0)
 
-/*
- * Testing whether an externally-stored value is compressed now requires
- * comparing size stored in va_extinfo (the actual length of the external data)
- * to rawsize (the original uncompressed datum's size).  The latter includes
- * VARHDRSZ overhead, the former doesn't.  We never use compression unless it
- * actually saves space, so we expect either equality or less-than.
- */
-static inline bool
-VARATT_EXTERNAL_IS_COMPRESSED(struct varatt_external_oid toast_pointer)
-{
-	return VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) <
-		(Size) (toast_pointer.va_rawsize - VARHDRSZ);
-}
-
 #endif
diff --git a/src/backend/access/common/toast_external.c b/src/backend/access/common/toast_external.c
index 96ea7be8966e..79ae02873748 100644
--- a/src/backend/access/common/toast_external.c
+++ b/src/backend/access/common/toast_external.c
@@ -22,6 +22,39 @@ static void ondisk_oid_to_external_data(struct varlena *attr,
 										toast_external_data *data);
 static struct varlena *ondisk_oid_create_external_data(toast_external_data data);
 
+/*
+ * Decompressed size of an on-disk varlena; but note argument is a struct
+ * varatt_external_oid.
+ */
+static inline Size
+varatt_external_oid_get_extsize(struct varatt_external_oid toast_pointer)
+{
+	return toast_pointer.va_extinfo & VARLENA_EXTSIZE_MASK;
+}
+
+/*
+ * Compression method of an on-disk varlena; but note argument is a struct
+ *  varatt_external_oid.
+ */
+static inline uint32
+varatt_external_oid_get_compress_method(struct varatt_external_oid toast_pointer)
+{
+	return toast_pointer.va_extinfo >> VARLENA_EXTSIZE_BITS;
+}
+
+/*
+ * Testing whether an externally-stored TOAST value is compressed now requires
+ * comparing size stored in va_extinfo (the actual length of the external data)
+ * to rawsize (the original uncompressed datum's size).  The latter includes
+ * VARHDRSZ overhead, the former doesn't.  We never use compression unless it
+ * actually saves space, so we expect either equality or less-than.
+ */
+static inline bool
+varatt_external_oid_is_compressed(struct varatt_external_oid toast_pointer)
+{
+	return varatt_external_oid_get_extsize(toast_pointer) <
+		(Size) (toast_pointer.va_rawsize - VARHDRSZ);
+}
 
 /*
  * Size of an EXTERNAL datum that contains a standard TOAST pointer (OID
@@ -117,10 +150,10 @@ ondisk_oid_to_external_data(struct varlena *attr, toast_external_data *data)
 	 * External size and compression methods are stored in the same field,
 	 * extract.
 	 */
-	if (VARATT_EXTERNAL_IS_COMPRESSED(external))
+	if (varatt_external_oid_is_compressed(external))
 	{
-		data->extsize = VARATT_EXTERNAL_GET_EXTSIZE(external);
-		data->compression_method = VARATT_EXTERNAL_GET_COMPRESS_METHOD(external);
+		data->extsize = varatt_external_oid_get_extsize(external);
+		data->compression_method = varatt_external_oid_get_compress_method(external);
 	}
 	else
 	{
-- 
2.50.0

v4-0006-Introduce-new-callback-to-get-fresh-TOAST-values.patchtext/x-diff; charset=us-asciiDownload
From 3e53ede276101c1eddb5c52a3460c635e67656b4 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 8 Aug 2025 15:44:50 +0900
Subject: [PATCH v4 06/15] Introduce new callback to get fresh TOAST values

This callback is called by toast_save_datum() to retrieve a new value
from a source related to the vartag_external we are dealing with.  As of
now, it is simply a wrapper around GetNewOidWithIndex() for the "OID"
on-disk TOAST external pointer.

This will be used later on by more external pointer types, like the int8
one.

InvalidToastId is introduced to track the concept of an "invalid" TOAST
value, required for toast_save_datum().
---
 src/include/access/toast_external.h         | 18 +++++++++++++++
 src/backend/access/common/toast_external.c  | 11 +++++++++
 src/backend/access/common/toast_internals.c | 25 ++++++++++++++-------
 src/backend/access/heap/heaptoast.c         |  2 +-
 4 files changed, 47 insertions(+), 9 deletions(-)

diff --git a/src/include/access/toast_external.h b/src/include/access/toast_external.h
index ad2a10f35c01..9f3177d60e5d 100644
--- a/src/include/access/toast_external.h
+++ b/src/include/access/toast_external.h
@@ -15,9 +15,14 @@
 #ifndef TOAST_EXTERNAL_H
 #define TOAST_EXTERNAL_H
 
+#include "access/attnum.h"
 #include "access/toast_compression.h"
+#include "utils/relcache.h"
 #include "varatt.h"
 
+/* Invalid TOAST value ID */
+#define InvalidToastId 0
+
 /*
  * Intermediate in-memory structure used when creating on-disk
  * varatt_external_* or when deserializing varlena contents.
@@ -43,6 +48,7 @@ typedef struct toast_external_data
 	/*
 	 * Unique ID of value within TOAST table.  This could be an OID or an int8
 	 * value.  This field is large enough to be able to store any of these.
+	 * InvalidToastId if invalid.
 	 */
 	uint64		value;
 } toast_external_data;
@@ -81,6 +87,18 @@ typedef struct toast_external_info
 	 */
 	struct varlena *(*create_external_data) (toast_external_data data);
 
+	/*
+	 * Retrieve a new value, to be assigned for a TOAST entry that will be
+	 * saved.  "toastrel" is the relation where the entry is added. "indexid"
+	 * and "attnum" can be used to check if a value is already in use in the
+	 * TOAST relation where the new entry is inserted.
+	 *
+	 * When "check" is set to true, the value generated should be rechecked
+	 * with the existing TOAST index.
+	 */
+	uint64		(*get_new_value) (Relation toastrel, Oid indexid,
+								  AttrNumber attnum);
+
 } toast_external_info;
 
 /* Retrieve a toast_external_info from a vartag */
diff --git a/src/backend/access/common/toast_external.c b/src/backend/access/common/toast_external.c
index 79ae02873748..5c36e5a11392 100644
--- a/src/backend/access/common/toast_external.c
+++ b/src/backend/access/common/toast_external.c
@@ -16,11 +16,14 @@
 #include "access/detoast.h"
 #include "access/heaptoast.h"
 #include "access/toast_external.h"
+#include "catalog/catalog.h"
 
 /* Callbacks for VARTAG_ONDISK_OID */
 static void ondisk_oid_to_external_data(struct varlena *attr,
 										toast_external_data *data);
 static struct varlena *ondisk_oid_create_external_data(toast_external_data data);
+static uint64 ondisk_oid_get_new_value(Relation toastrel, Oid indexid,
+									   AttrNumber attnum);
 
 /*
  * Decompressed size of an on-disk varlena; but note argument is a struct
@@ -81,6 +84,7 @@ static const toast_external_info toast_external_infos[TOAST_EXTERNAL_INFO_SIZE]
 		.maximum_chunk_size = TOAST_MAX_CHUNK_SIZE_OID,
 		.to_external_data = ondisk_oid_to_external_data,
 		.create_external_data = ondisk_oid_create_external_data,
+		.get_new_value = ondisk_oid_get_new_value,
 	},
 };
 
@@ -192,3 +196,10 @@ ondisk_oid_create_external_data(toast_external_data data)
 
 	return result;
 }
+
+static uint64
+ondisk_oid_get_new_value(Relation toastrel, Oid indexid,
+						 AttrNumber attnum)
+{
+	return GetNewOidWithIndex(toastrel, indexid, attnum);
+}
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index f659ec41df34..b0a0ce1e1b9a 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -227,6 +227,15 @@ toast_save_datum(Relation rel, Datum value,
 	else
 		toast_pointer.toastrelid = RelationGetRelid(toastrel);
 
+	/*
+	 * Retrieve the external TOAST information, with the value still unknown.
+	 * We need to do this again once we know the actual value assigned, to
+	 * define the correct vartag_external for the new TOAST tuple.
+	 */
+	tag = toast_external_assign_vartag(toast_pointer.toastrelid,
+									   InvalidToastId);
+	info = toast_external_get_info(tag);
+
 	/*
 	 * Choose an OID to use as the value ID for this toast value.
 	 *
@@ -243,14 +252,14 @@ toast_save_datum(Relation rel, Datum value,
 	{
 		/* normal case: just choose an unused OID */
 		toast_pointer.value =
-			GetNewOidWithIndex(toastrel,
-							   RelationGetRelid(toastidxs[validIndex]),
-							   (AttrNumber) 1);
+			info->get_new_value(toastrel,
+								RelationGetRelid(toastidxs[validIndex]),
+								(AttrNumber) 1);
 	}
 	else
 	{
 		/* rewrite case: check to see if value was in old toast table */
-		toast_pointer.value = InvalidOid;
+		toast_pointer.value = InvalidToastId;
 		if (oldexternal != NULL)
 		{
 			struct toast_external_data old_toast_pointer;
@@ -288,7 +297,7 @@ toast_save_datum(Relation rel, Datum value,
 				}
 			}
 		}
-		if (toast_pointer.value == InvalidOid)
+		if (toast_pointer.value == InvalidToastId)
 		{
 			/*
 			 * new value; must choose an OID that doesn't conflict in either
@@ -297,9 +306,9 @@ toast_save_datum(Relation rel, Datum value,
 			do
 			{
 				toast_pointer.value =
-					GetNewOidWithIndex(toastrel,
-									   RelationGetRelid(toastidxs[validIndex]),
-									   (AttrNumber) 1);
+					info->get_new_value(toastrel,
+										RelationGetRelid(toastidxs[validIndex]),
+										(AttrNumber) 1);
 			} while (toastid_valueid_exists(rel->rd_toastoid,
 											toast_pointer.value));
 		}
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index 47bf9e84963a..c215263eb76a 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -149,7 +149,7 @@ heap_toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup,
 	 */
 
 	/* The default value is invalid, to work as a default. */
-	tag = toast_external_assign_vartag(rel->rd_rel->reltoastrelid, InvalidOid);
+	tag = toast_external_assign_vartag(rel->rd_rel->reltoastrelid, InvalidToastId);
 	ttc.ttc_toast_pointer_size = toast_external_info_get_pointer_size(tag);
 
 	ttc.ttc_rel = rel;
-- 
2.50.0

v4-0007-Add-catcache-support-for-INT8OID.patchtext/x-diff; charset=us-asciiDownload
From d751cd1cb244827f17f9c8752312d3dcd8ee330b Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 18 Jun 2025 16:12:11 +0900
Subject: [PATCH v4 07/15] Add catcache support for INT8OID

This is required to be able to do catalog cache lookups of int8 fields
for toast values of the same type.
---
 src/backend/utils/cache/catcache.c | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index e2cd3feaf81d..55ebf55c7b88 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -240,6 +240,18 @@ int4hashfast(Datum datum)
 	return murmurhash32((int32) DatumGetInt32(datum));
 }
 
+static bool
+int8eqfast(Datum a, Datum b)
+{
+	return DatumGetInt64(a) == DatumGetInt64(b);
+}
+
+static uint32
+int8hashfast(Datum datum)
+{
+	return murmurhash64((int64) DatumGetInt64(datum));
+}
+
 static bool
 texteqfast(Datum a, Datum b)
 {
@@ -300,6 +312,11 @@ GetCCHashEqFuncs(Oid keytype, CCHashFN *hashfunc, RegProcedure *eqfunc, CCFastEq
 			*fasteqfunc = int4eqfast;
 			*eqfunc = F_INT4EQ;
 			break;
+		case INT8OID:
+			*hashfunc = int8hashfast;
+			*fasteqfunc = int8eqfast;
+			*eqfunc = F_INT8EQ;
+			break;
 		case TEXTOID:
 			*hashfunc = texthashfast;
 			*fasteqfunc = texteqfast;
-- 
2.50.0

v4-0008-Add-GUC-default_toast_type.patchtext/x-diff; charset=us-asciiDownload
From 4a8638ee29a1ff85428895831e1290cc6f128a1c Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 18 Jun 2025 16:15:19 +0900
Subject: [PATCH v4 08/15] Add GUC default_toast_type

This GUC controls the data type used for newly-created TOAST values,
with two modes supported: "oid" and "int8".  This will be used by an
upcoming patch.
---
 src/include/access/toast_type.h               | 30 +++++++++++++++++++
 src/backend/catalog/toasting.c                |  4 +++
 src/backend/utils/misc/guc_tables.c           | 19 ++++++++++++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 doc/src/sgml/config.sgml                      | 17 +++++++++++
 5 files changed, 71 insertions(+)
 create mode 100644 src/include/access/toast_type.h

diff --git a/src/include/access/toast_type.h b/src/include/access/toast_type.h
new file mode 100644
index 000000000000..494c2a3e852e
--- /dev/null
+++ b/src/include/access/toast_type.h
@@ -0,0 +1,30 @@
+/*-------------------------------------------------------------------------
+ *
+ * toast_type.h
+ *	  Internal definitions for the types supported by values in TOAST
+ *	  relations.
+ *
+ * Copyright (c) 2000-2025, PostgreSQL Global Development Group
+ *
+ * src/include/access/toast_type.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef TOAST_TYPE_H
+#define TOAST_TYPE_H
+
+/*
+ * GUC support
+ *
+ * Detault value type in toast table.
+ */
+extern PGDLLIMPORT int default_toast_type;
+
+typedef enum ToastTypeId
+{
+	TOAST_TYPE_INVALID = 0,
+	TOAST_TYPE_OID = 1,
+	TOAST_TYPE_INT8 = 2,
+} ToastTypeId;
+
+#endif							/* TOAST_TYPE_H */
diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c
index 874a8fc89adb..e595cb61b375 100644
--- a/src/backend/catalog/toasting.c
+++ b/src/backend/catalog/toasting.c
@@ -16,6 +16,7 @@
 
 #include "access/heapam.h"
 #include "access/toast_compression.h"
+#include "access/toast_type.h"
 #include "access/xact.h"
 #include "catalog/binary_upgrade.h"
 #include "catalog/catalog.h"
@@ -33,6 +34,9 @@
 #include "utils/rel.h"
 #include "utils/syscache.h"
 
+/* GUC support */
+int			default_toast_type = TOAST_TYPE_OID;
+
 static void CheckAndCreateToastTable(Oid relOid, Datum reloptions,
 									 LOCKMODE lockmode, bool check,
 									 Oid OIDOldToast);
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index d14b1678e7fe..be523c9ac094 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -33,6 +33,7 @@
 #include "access/gin.h"
 #include "access/slru.h"
 #include "access/toast_compression.h"
+#include "access/toast_type.h"
 #include "access/twophase.h"
 #include "access/xlog_internal.h"
 #include "access/xlogprefetcher.h"
@@ -464,6 +465,13 @@ static const struct config_enum_entry default_toast_compression_options[] = {
 	{NULL, 0, false}
 };
 
+
+static const struct config_enum_entry default_toast_type_options[] = {
+	{"oid", TOAST_TYPE_OID, false},
+	{"int8", TOAST_TYPE_INT8, false},
+	{NULL, 0, false}
+};
+
 static const struct config_enum_entry wal_compression_options[] = {
 	{"pglz", WAL_COMPRESSION_PGLZ, false},
 #ifdef USE_LZ4
@@ -5058,6 +5066,17 @@ struct config_enum ConfigureNamesEnum[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"default_toast_type", PGC_USERSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Sets the default type used for TOAST values."),
+			NULL
+		},
+		&default_toast_type,
+		TOAST_TYPE_OID,
+		default_toast_type_options,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"default_transaction_isolation", PGC_USERSET, CLIENT_CONN_STATEMENT,
 			gettext_noop("Sets the transaction isolation level of each new transaction."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index a9d8293474af..5f34b14ea39a 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -753,6 +753,7 @@ autovacuum_worker_slots = 16	# autovacuum worker slots to allocate
 #default_table_access_method = 'heap'
 #default_tablespace = ''		# a tablespace name, '' uses the default
 #default_toast_compression = 'pglz'	# 'pglz' or 'lz4'
+#default_toast_type = 'oid'		# 'oid' or 'int8'
 #temp_tablespaces = ''			# a list of tablespace names, '' uses
 					# only default tablespace
 #check_function_bodies = on
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 20ccb2d6b544..21ccef564f4e 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9834,6 +9834,23 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-default-toast-type" xreflabel="default_toast_type">
+      <term><varname>default_toast_type</varname> (<type>enum</type>)
+      <indexterm>
+       <primary><varname>default_toast_type</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        This variable sets the default type for
+        <link linkend="storage-toast">TOAST</link> values.
+        The value types supported are <literal>oid</literal> and
+        <literal>int8</literal>.
+        The default is <literal>oid</literal>.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-temp-tablespaces" xreflabel="temp_tablespaces">
       <term><varname>temp_tablespaces</varname> (<type>string</type>)
       <indexterm>
-- 
2.50.0

v4-0009-Introduce-global-64-bit-TOAST-ID-counter-in-contr.patchtext/x-diff; charset=us-asciiDownload
From 41e7d9cbbdaac566cf90e52892361c4139e8bc7a Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 8 Jul 2025 09:00:43 +0900
Subject: [PATCH v4 09/15] Introduce global 64-bit TOAST ID counter in control
 file

An 8 byte counter is added to the control file, providing a unique
64-bit-wide source for toast value IDs, with the same guarantees as OIDs
in terms of durability.  SQL functions and tools looking at the control
file are updated.  A WAL record is generated every 8k values generated,
that can be adjusted if required.

Requires a bump of WAL format.
Requires a bump of control file version.
Requires a catalog version bump.
---
 src/include/access/toast_counter.h            | 34 +++++++
 src/include/access/xlog.h                     |  1 +
 src/include/catalog/pg_control.h              |  4 +-
 src/include/catalog/pg_proc.dat               |  6 +-
 src/include/storage/lwlocklist.h              |  1 +
 src/backend/access/common/Makefile            |  1 +
 src/backend/access/common/meson.build         |  1 +
 src/backend/access/common/toast_counter.c     | 98 +++++++++++++++++++
 src/backend/access/rmgrdesc/xlogdesc.c        | 10 ++
 src/backend/access/transam/xlog.c             | 44 +++++++++
 src/backend/replication/logical/decode.c      |  1 +
 src/backend/storage/ipc/ipci.c                |  5 +-
 .../utils/activity/wait_event_names.txt       |  1 +
 src/backend/utils/misc/pg_controldata.c       | 23 +++--
 src/bin/pg_controldata/pg_controldata.c       |  2 +
 src/bin/pg_resetwal/pg_resetwal.c             |  2 +
 doc/src/sgml/func/func-info.sgml              |  5 +
 src/tools/pgindent/typedefs.list              |  1 +
 18 files changed, 225 insertions(+), 15 deletions(-)
 create mode 100644 src/include/access/toast_counter.h
 create mode 100644 src/backend/access/common/toast_counter.c

diff --git a/src/include/access/toast_counter.h b/src/include/access/toast_counter.h
new file mode 100644
index 000000000000..e2bc79682771
--- /dev/null
+++ b/src/include/access/toast_counter.h
@@ -0,0 +1,34 @@
+/*-------------------------------------------------------------------------
+ *
+ * toast_counter.h
+ *	  Machinery for TOAST value counter.
+ *
+ * Copyright (c) 2000-2025, PostgreSQL Global Development Group
+ *
+ * src/include/access/toast_counter.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef TOAST_COUNTER_H
+#define TOAST_COUNTER_H
+
+#define FirstToastId	1		/* First TOAST value ID assigned */
+
+/*
+ * Structure in shared memory to track TOAST value counter activity.
+ * These are protected by ToastIdGenLock.
+ */
+typedef struct ToastCounterData
+{
+	uint64		nextId;			/* next TOAST value ID to assign */
+	uint32		idCount;		/* IDs available before WAL work */
+} ToastCounterData;
+
+extern PGDLLIMPORT ToastCounterData *ToastCounter;
+
+/* external declarations */
+extern Size ToastCounterShmemSize(void);
+extern void ToastCounterShmemInit(void);
+extern uint64 GetNewToastId(void);
+
+#endif							/* TOAST_TYPE_H */
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index d12798be3d80..6c5e5feb54bb 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -244,6 +244,7 @@ extern bool CreateCheckPoint(int flags);
 extern bool CreateRestartPoint(int flags);
 extern WALAvailability GetWALAvailability(XLogRecPtr targetLSN);
 extern void XLogPutNextOid(Oid nextOid);
+extern void XLogPutNextToastId(uint64 nextId);
 extern XLogRecPtr XLogRestorePoint(const char *rpName);
 extern void UpdateFullPageWrites(void);
 extern void GetFullPageWriteInfo(XLogRecPtr *RedoRecPtr_p, bool *doPageWrites_p);
diff --git a/src/include/catalog/pg_control.h b/src/include/catalog/pg_control.h
index 63e834a6ce47..1194b4928155 100644
--- a/src/include/catalog/pg_control.h
+++ b/src/include/catalog/pg_control.h
@@ -22,7 +22,7 @@
 
 
 /* Version identifier for this pg_control format */
-#define PG_CONTROL_VERSION	1800
+#define PG_CONTROL_VERSION	1900
 
 /* Nonce key length, see below */
 #define MOCK_AUTH_NONCE_LEN		32
@@ -45,6 +45,7 @@ typedef struct CheckPoint
 	Oid			nextOid;		/* next free OID */
 	MultiXactId nextMulti;		/* next free MultiXactId */
 	MultiXactOffset nextMultiOffset;	/* next free MultiXact offset */
+	uint64		nextToastId;	/* next free TOAST ID */
 	TransactionId oldestXid;	/* cluster-wide minimum datfrozenxid */
 	Oid			oldestXidDB;	/* database with minimum datfrozenxid */
 	MultiXactId oldestMulti;	/* cluster-wide minimum datminmxid */
@@ -80,6 +81,7 @@ typedef struct CheckPoint
 /* 0xC0 is used in Postgres 9.5-11 */
 #define XLOG_OVERWRITE_CONTRECORD		0xD0
 #define XLOG_CHECKPOINT_REDO			0xE0
+#define XLOG_NEXT_TOAST_ID				0xF0
 
 
 /*
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 118d6da1ace0..1f6df80ad312 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12318,9 +12318,9 @@
   descr => 'pg_controldata checkpoint state information as a function',
   proname => 'pg_control_checkpoint', provolatile => 'v',
   prorettype => 'record', proargtypes => '',
-  proallargtypes => '{pg_lsn,pg_lsn,text,int4,int4,bool,text,oid,xid,xid,xid,oid,xid,xid,oid,xid,xid,timestamptz}',
-  proargmodes => '{o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
-  proargnames => '{checkpoint_lsn,redo_lsn,redo_wal_file,timeline_id,prev_timeline_id,full_page_writes,next_xid,next_oid,next_multixact_id,next_multi_offset,oldest_xid,oldest_xid_dbid,oldest_active_xid,oldest_multi_xid,oldest_multi_dbid,oldest_commit_ts_xid,newest_commit_ts_xid,checkpoint_time}',
+  proallargtypes => '{pg_lsn,pg_lsn,text,int4,int4,bool,text,oid,xid,xid,int8,xid,oid,xid,xid,oid,xid,xid,timestamptz}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{checkpoint_lsn,redo_lsn,redo_wal_file,timeline_id,prev_timeline_id,full_page_writes,next_xid,next_oid,next_multixact_id,next_multi_offset,next_toast_id,oldest_xid,oldest_xid_dbid,oldest_active_xid,oldest_multi_xid,oldest_multi_dbid,oldest_commit_ts_xid,newest_commit_ts_xid,checkpoint_time}',
   prosrc => 'pg_control_checkpoint' },
 
 { oid => '3443',
diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h
index 208d2e3a8ed9..81abc58f0810 100644
--- a/src/include/storage/lwlocklist.h
+++ b/src/include/storage/lwlocklist.h
@@ -85,6 +85,7 @@ PG_LWLOCK(50, DSMRegistry)
 PG_LWLOCK(51, InjectionPoint)
 PG_LWLOCK(52, SerialControl)
 PG_LWLOCK(53, AioWorkerSubmissionQueue)
+PG_LWLOCK(54, ToastIdGen)
 
 /*
  * There also exist several built-in LWLock tranches.  As with the predefined
diff --git a/src/backend/access/common/Makefile b/src/backend/access/common/Makefile
index 1ef86a245886..6e9a3a430c19 100644
--- a/src/backend/access/common/Makefile
+++ b/src/backend/access/common/Makefile
@@ -27,6 +27,7 @@ OBJS = \
 	syncscan.o \
 	tidstore.o \
 	toast_compression.o \
+	toast_counter.o \
 	toast_external.o \
 	toast_internals.o \
 	tupconvert.o \
diff --git a/src/backend/access/common/meson.build b/src/backend/access/common/meson.build
index c20f2e88921e..4254132c8dfd 100644
--- a/src/backend/access/common/meson.build
+++ b/src/backend/access/common/meson.build
@@ -15,6 +15,7 @@ backend_sources += files(
   'syncscan.c',
   'tidstore.c',
   'toast_compression.c',
+  'toast_counter.c',
   'toast_external.c',
   'toast_internals.c',
   'tupconvert.c',
diff --git a/src/backend/access/common/toast_counter.c b/src/backend/access/common/toast_counter.c
new file mode 100644
index 000000000000..94d361d0d5c4
--- /dev/null
+++ b/src/backend/access/common/toast_counter.c
@@ -0,0 +1,98 @@
+/*-------------------------------------------------------------------------
+ *
+ * toast_counter.c
+ *	  Functions for TOAST value counter.
+ *
+ * Copyright (c) 2025, PostgreSQL Global Development Group
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/common/toast_counter.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/toast_counter.h"
+#include "access/xlog.h"
+#include "miscadmin.h"
+#include "storage/lwlock.h"
+#include "storage/shmem.h"
+
+/* Number of TOAST values to preallocate before WAL work */
+#define TOAST_ID_PREFETCH		8192
+
+/* pointer to variables struct in shared memory */
+ToastCounterData *ToastCounter = NULL;
+
+/*
+ * Initialization of shared memory for ToastCounter.
+ */
+Size
+ToastCounterShmemSize(void)
+{
+	return sizeof(ToastCounterData);
+}
+
+void
+ToastCounterShmemInit(void)
+{
+	bool		found;
+
+	/* Initialize shared state struct */
+	ToastCounter = ShmemInitStruct("ToastCounter",
+								   sizeof(ToastCounterData),
+								   &found);
+	if (!IsUnderPostmaster)
+	{
+		Assert(!found);
+		memset(ToastCounter, 0, sizeof(ToastCounterData));
+	}
+	else
+		Assert(found);
+}
+
+/*
+ * GetNewToastId
+ *
+ * Toast IDs are generated as a cluster-wide counter.  They are 64 bits
+ * wide, hence wraparound will unlikely happen.
+ */
+uint64
+GetNewToastId(void)
+{
+	uint64		result;
+
+	if (RecoveryInProgress())
+		elog(ERROR, "cannot assign TOAST IDs during recovery");
+
+	LWLockAcquire(ToastIdGenLock, LW_EXCLUSIVE);
+
+	/*
+	 * Check for initialization or wraparound of the toast counter ID.
+	 * InvalidToastId (0) should never be returned.  We are 64 bit-wide, hence
+	 * wraparound is unlikely going to happen, but this check is cheap so
+	 * let's play it safe.
+	 */
+	if (ToastCounter->nextId < ((uint64) FirstToastId))
+	{
+		/* Most-likely first bootstrap or initdb assignment */
+		ToastCounter->nextId = FirstToastId;
+		ToastCounter->idCount = 0;
+	}
+
+	/* If running out of logged for TOAST IDs, log more */
+	if (ToastCounter->idCount == 0)
+	{
+		XLogPutNextToastId(ToastCounter->nextId + TOAST_ID_PREFETCH);
+		ToastCounter->idCount = TOAST_ID_PREFETCH;
+	}
+
+	result = ToastCounter->nextId;
+	(ToastCounter->nextId)++;
+	(ToastCounter->idCount)--;
+
+	LWLockRelease(ToastIdGenLock);
+
+	return result;
+}
diff --git a/src/backend/access/rmgrdesc/xlogdesc.c b/src/backend/access/rmgrdesc/xlogdesc.c
index cd6c2a2f650a..6786af4064b8 100644
--- a/src/backend/access/rmgrdesc/xlogdesc.c
+++ b/src/backend/access/rmgrdesc/xlogdesc.c
@@ -96,6 +96,13 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
 		memcpy(&nextOid, rec, sizeof(Oid));
 		appendStringInfo(buf, "%u", nextOid);
 	}
+	else if (info == XLOG_NEXT_TOAST_ID)
+	{
+		uint64		nextId;
+
+		memcpy(&nextId, rec, sizeof(uint64));
+		appendStringInfo(buf, "%" PRIu64, nextId);
+	}
 	else if (info == XLOG_RESTORE_POINT)
 	{
 		xl_restore_point *xlrec = (xl_restore_point *) rec;
@@ -218,6 +225,9 @@ xlog_identify(uint8 info)
 		case XLOG_CHECKPOINT_REDO:
 			id = "CHECKPOINT_REDO";
 			break;
+		case XLOG_NEXT_TOAST_ID:
+			id = "NEXT_TOAST_ID";
+			break;
 	}
 
 	return id;
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 9a4de1616bcc..172ee27fe4cf 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -53,6 +53,7 @@
 #include "access/rewriteheap.h"
 #include "access/subtrans.h"
 #include "access/timeline.h"
+#include "access/toast_counter.h"
 #include "access/transam.h"
 #include "access/twophase.h"
 #include "access/xact.h"
@@ -5259,6 +5260,7 @@ BootStrapXLOG(uint32 data_checksum_version)
 	checkPoint.nextOid = FirstGenbkiObjectId;
 	checkPoint.nextMulti = FirstMultiXactId;
 	checkPoint.nextMultiOffset = 0;
+	checkPoint.nextToastId = FirstToastId;
 	checkPoint.oldestXid = FirstNormalTransactionId;
 	checkPoint.oldestXidDB = Template1DbOid;
 	checkPoint.oldestMulti = FirstMultiXactId;
@@ -5271,6 +5273,10 @@ BootStrapXLOG(uint32 data_checksum_version)
 	TransamVariables->nextXid = checkPoint.nextXid;
 	TransamVariables->nextOid = checkPoint.nextOid;
 	TransamVariables->oidCount = 0;
+
+	ToastCounter->nextId = checkPoint.nextToastId;
+	ToastCounter->idCount = 0;
+
 	MultiXactSetNextMXact(checkPoint.nextMulti, checkPoint.nextMultiOffset);
 	AdvanceOldestClogXid(checkPoint.oldestXid);
 	SetTransactionIdLimit(checkPoint.oldestXid, checkPoint.oldestXidDB);
@@ -5752,6 +5758,8 @@ StartupXLOG(void)
 	TransamVariables->nextXid = checkPoint.nextXid;
 	TransamVariables->nextOid = checkPoint.nextOid;
 	TransamVariables->oidCount = 0;
+	ToastCounter->nextId = checkPoint.nextToastId;
+	ToastCounter->idCount = 0;
 	MultiXactSetNextMXact(checkPoint.nextMulti, checkPoint.nextMultiOffset);
 	AdvanceOldestClogXid(checkPoint.oldestXid);
 	SetTransactionIdLimit(checkPoint.oldestXid, checkPoint.oldestXidDB);
@@ -7299,6 +7307,12 @@ CreateCheckPoint(int flags)
 		checkPoint.nextOid += TransamVariables->oidCount;
 	LWLockRelease(OidGenLock);
 
+	LWLockAcquire(ToastIdGenLock, LW_SHARED);
+	checkPoint.nextToastId = ToastCounter->nextId;
+	if (!shutdown)
+		checkPoint.nextToastId += ToastCounter->idCount;
+	LWLockRelease(ToastIdGenLock);
+
 	MultiXactGetCheckptMulti(shutdown,
 							 &checkPoint.nextMulti,
 							 &checkPoint.nextMultiOffset,
@@ -8234,6 +8248,22 @@ XLogPutNextOid(Oid nextOid)
 	 */
 }
 
+/*
+ * Write a NEXT_TOAST_ID log record.
+ */
+void
+XLogPutNextToastId(uint64 nextId)
+{
+	XLogBeginInsert();
+	XLogRegisterData(&nextId, sizeof(uint64));
+	(void) XLogInsert(RM_XLOG_ID, XLOG_NEXT_TOAST_ID);
+
+	/*
+	 * The next TOAST value ID is not flushed immediately, for the same reason
+	 * as above for the OIDs in XLogPutNextOid().
+	 */
+}
+
 /*
  * Write an XLOG SWITCH record.
  *
@@ -8449,6 +8479,16 @@ xlog_redo(XLogReaderState *record)
 		TransamVariables->oidCount = 0;
 		LWLockRelease(OidGenLock);
 	}
+	else if (info == XLOG_NEXT_TOAST_ID)
+	{
+		uint64		nextToastId;
+
+		memcpy(&nextToastId, XLogRecGetData(record), sizeof(uint64));
+		LWLockAcquire(ToastIdGenLock, LW_EXCLUSIVE);
+		ToastCounter->nextId = nextToastId;
+		ToastCounter->idCount = 0;
+		LWLockRelease(ToastIdGenLock);
+	}
 	else if (info == XLOG_CHECKPOINT_SHUTDOWN)
 	{
 		CheckPoint	checkPoint;
@@ -8463,6 +8503,10 @@ xlog_redo(XLogReaderState *record)
 		TransamVariables->nextOid = checkPoint.nextOid;
 		TransamVariables->oidCount = 0;
 		LWLockRelease(OidGenLock);
+		LWLockAcquire(ToastIdGenLock, LW_EXCLUSIVE);
+		ToastCounter->nextId = checkPoint.nextToastId;
+		ToastCounter->idCount = 0;
+		LWLockRelease(ToastIdGenLock);
 		MultiXactSetNextMXact(checkPoint.nextMulti,
 							  checkPoint.nextMultiOffset);
 
diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.c
index cc03f0706e9c..bb0337d37201 100644
--- a/src/backend/replication/logical/decode.c
+++ b/src/backend/replication/logical/decode.c
@@ -188,6 +188,7 @@ xlog_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
 		case XLOG_FPI:
 		case XLOG_OVERWRITE_CONTRECORD:
 		case XLOG_CHECKPOINT_REDO:
+		case XLOG_NEXT_TOAST_ID:
 			break;
 		default:
 			elog(ERROR, "unexpected RM_XLOG_ID record type: %u", info);
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index 2fa045e6b0f6..9102c267d7b0 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -20,6 +20,7 @@
 #include "access/nbtree.h"
 #include "access/subtrans.h"
 #include "access/syncscan.h"
+#include "access/toast_counter.h"
 #include "access/transam.h"
 #include "access/twophase.h"
 #include "access/xlogprefetcher.h"
@@ -119,6 +120,7 @@ CalculateShmemSize(int *num_semaphores)
 	size = add_size(size, ProcGlobalShmemSize());
 	size = add_size(size, XLogPrefetchShmemSize());
 	size = add_size(size, VarsupShmemSize());
+	size = add_size(size, ToastCounterShmemSize());
 	size = add_size(size, XLOGShmemSize());
 	size = add_size(size, XLogRecoveryShmemSize());
 	size = add_size(size, CLOGShmemSize());
@@ -280,8 +282,9 @@ CreateOrAttachShmemStructs(void)
 	DSMRegistryShmemInit();
 
 	/*
-	 * Set up xlog, clog, and buffers
+	 * Set up TOAST counter, xlog, clog, and buffers
 	 */
+	ToastCounterShmemInit();
 	VarsupShmemInit();
 	XLOGShmemInit();
 	XLogPrefetchShmemInit();
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 0be307d2ca04..a36969ac6659 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -352,6 +352,7 @@ DSMRegistry	"Waiting to read or update the dynamic shared memory registry."
 InjectionPoint	"Waiting to read or update information related to injection points."
 SerialControl	"Waiting to read or update shared <filename>pg_serial</filename> state."
 AioWorkerSubmissionQueue	"Waiting to access AIO worker submission queue."
+ToastIdGen	"Waiting to allocate a new TOAST value ID."
 
 #
 # END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE)
diff --git a/src/backend/utils/misc/pg_controldata.c b/src/backend/utils/misc/pg_controldata.c
index 6d036e3bf328..e4abf8593b8d 100644
--- a/src/backend/utils/misc/pg_controldata.c
+++ b/src/backend/utils/misc/pg_controldata.c
@@ -69,8 +69,8 @@ pg_control_system(PG_FUNCTION_ARGS)
 Datum
 pg_control_checkpoint(PG_FUNCTION_ARGS)
 {
-	Datum		values[18];
-	bool		nulls[18];
+	Datum		values[19];
+	bool		nulls[19];
 	TupleDesc	tupdesc;
 	HeapTuple	htup;
 	ControlFileData *ControlFile;
@@ -130,30 +130,33 @@ pg_control_checkpoint(PG_FUNCTION_ARGS)
 	values[9] = TransactionIdGetDatum(ControlFile->checkPointCopy.nextMultiOffset);
 	nulls[9] = false;
 
-	values[10] = TransactionIdGetDatum(ControlFile->checkPointCopy.oldestXid);
+	values[10] = UInt64GetDatum(ControlFile->checkPointCopy.nextToastId);
 	nulls[10] = false;
 
-	values[11] = ObjectIdGetDatum(ControlFile->checkPointCopy.oldestXidDB);
+	values[11] = TransactionIdGetDatum(ControlFile->checkPointCopy.oldestXid);
 	nulls[11] = false;
 
-	values[12] = TransactionIdGetDatum(ControlFile->checkPointCopy.oldestActiveXid);
+	values[12] = ObjectIdGetDatum(ControlFile->checkPointCopy.oldestXidDB);
 	nulls[12] = false;
 
-	values[13] = TransactionIdGetDatum(ControlFile->checkPointCopy.oldestMulti);
+	values[13] = TransactionIdGetDatum(ControlFile->checkPointCopy.oldestActiveXid);
 	nulls[13] = false;
 
-	values[14] = ObjectIdGetDatum(ControlFile->checkPointCopy.oldestMultiDB);
+	values[14] = TransactionIdGetDatum(ControlFile->checkPointCopy.oldestMulti);
 	nulls[14] = false;
 
-	values[15] = TransactionIdGetDatum(ControlFile->checkPointCopy.oldestCommitTsXid);
+	values[15] = ObjectIdGetDatum(ControlFile->checkPointCopy.oldestMultiDB);
 	nulls[15] = false;
 
-	values[16] = TransactionIdGetDatum(ControlFile->checkPointCopy.newestCommitTsXid);
+	values[16] = TransactionIdGetDatum(ControlFile->checkPointCopy.oldestCommitTsXid);
 	nulls[16] = false;
 
-	values[17] = TimestampTzGetDatum(time_t_to_timestamptz(ControlFile->checkPointCopy.time));
+	values[17] = TransactionIdGetDatum(ControlFile->checkPointCopy.newestCommitTsXid);
 	nulls[17] = false;
 
+	values[18] = TimestampTzGetDatum(time_t_to_timestamptz(ControlFile->checkPointCopy.time));
+	nulls[18] = false;
+
 	htup = heap_form_tuple(tupdesc, values, nulls);
 
 	PG_RETURN_DATUM(HeapTupleGetDatum(htup));
diff --git a/src/bin/pg_controldata/pg_controldata.c b/src/bin/pg_controldata/pg_controldata.c
index 10de058ce91f..99200262b57c 100644
--- a/src/bin/pg_controldata/pg_controldata.c
+++ b/src/bin/pg_controldata/pg_controldata.c
@@ -266,6 +266,8 @@ main(int argc, char *argv[])
 		   ControlFile->checkPointCopy.nextMulti);
 	printf(_("Latest checkpoint's NextMultiOffset:  %u\n"),
 		   ControlFile->checkPointCopy.nextMultiOffset);
+	printf(_("Latest checkpoint's NextToastID:      %" PRIu64 "\n"),
+		   ControlFile->checkPointCopy.nextToastId);
 	printf(_("Latest checkpoint's oldestXID:        %u\n"),
 		   ControlFile->checkPointCopy.oldestXid);
 	printf(_("Latest checkpoint's oldestXID's DB:   %u\n"),
diff --git a/src/bin/pg_resetwal/pg_resetwal.c b/src/bin/pg_resetwal/pg_resetwal.c
index e876f35f38ed..bb324c710911 100644
--- a/src/bin/pg_resetwal/pg_resetwal.c
+++ b/src/bin/pg_resetwal/pg_resetwal.c
@@ -45,6 +45,7 @@
 
 #include "access/heaptoast.h"
 #include "access/multixact.h"
+#include "access/toast_counter.h"
 #include "access/transam.h"
 #include "access/xlog.h"
 #include "access/xlog_internal.h"
@@ -686,6 +687,7 @@ GuessControlValues(void)
 	ControlFile.checkPointCopy.nextOid = FirstGenbkiObjectId;
 	ControlFile.checkPointCopy.nextMulti = FirstMultiXactId;
 	ControlFile.checkPointCopy.nextMultiOffset = 0;
+	ControlFile.checkPointCopy.nextToastId = FirstToastId;
 	ControlFile.checkPointCopy.oldestXid = FirstNormalTransactionId;
 	ControlFile.checkPointCopy.oldestXidDB = InvalidOid;
 	ControlFile.checkPointCopy.oldestMulti = FirstMultiXactId;
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index b507bfaf64b1..9d7548825e5a 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3395,6 +3395,11 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
        <entry><type>xid</type></entry>
       </row>
 
+      <row>
+       <entry><structfield>next_toast_id</structfield></entry>
+       <entry><type>bigint</type></entry>
+      </row>
+
       <row>
        <entry><structfield>oldest_xid</structfield></entry>
        <entry><type>xid</type></entry>
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 995dc1f28208..d6bf4e47991b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3055,6 +3055,7 @@ TmFromChar
 TmToChar
 ToastAttrInfo
 ToastCompressionId
+ToastCounterData
 ToastTupleContext
 ToastedAttribute
 TocEntry
-- 
2.50.0

v4-0010-Switch-pg_column_toast_chunk_id-return-value-from.patchtext/x-diff; charset=us-asciiDownload
From f841b4bbafe5203fd9edc90b6fd8ad049ea253a0 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 8 Aug 2025 15:52:05 +0900
Subject: [PATCH v4 10/15] Switch pg_column_toast_chunk_id() return value from
 oid to bigint

This is required for a follow-up patch that will add support for 8-byte
TOAST values, with this function being changed so as it is able to
support the largest TOAST value type available.

XXX: Bump catalog version.
---
 src/include/catalog/pg_proc.dat   | 2 +-
 src/backend/utils/adt/varlena.c   | 2 +-
 doc/src/sgml/func/func-admin.sgml | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 1f6df80ad312..27887436ede0 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7735,7 +7735,7 @@
   proargtypes => 'any', prosrc => 'pg_column_compression' },
 { oid => '6316', descr => 'chunk ID of on-disk TOASTed value',
   proname => 'pg_column_toast_chunk_id', provolatile => 's',
-  prorettype => 'oid', proargtypes => 'any',
+  prorettype => 'int8', proargtypes => 'any',
   prosrc => 'pg_column_toast_chunk_id' },
 { oid => '2322',
   descr => 'total disk space usage for the specified tablespace',
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index d76386407a08..26c720449f7b 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -4249,7 +4249,7 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 
 	toast_valueid = toast_external_info_get_value(attr);
 
-	PG_RETURN_OID(toast_valueid);
+	PG_RETURN_UINT64(toast_valueid);
 }
 
 /*
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 446fdfe56f4f..1e83584ac579 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1571,7 +1571,7 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
          <primary>pg_column_toast_chunk_id</primary>
         </indexterm>
         <function>pg_column_toast_chunk_id</function> ( <type>"any"</type> )
-        <returnvalue>oid</returnvalue>
+        <returnvalue>bigint</returnvalue>
        </para>
        <para>
         Shows the <structfield>chunk_id</structfield> of an on-disk
-- 
2.50.0

v4-0011-Add-support-for-bigint-TOAST-values.patchtext/x-diff; charset=us-asciiDownload
From 27275a6a55857aa7543c4571d3315c6ad462c15c Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 1 Aug 2025 17:17:51 +0900
Subject: [PATCH v4 11/15] Add support for bigint TOAST values

This commit adds the possibility to define TOAST tables with bigint as
value ID, based on the GUC default_toast_type.  All the external TOAST
pointers still rely on varatt_external and a single vartag, with all the
values inserted in the bigint TOAST tables fed from the existing OID
value generator.  This will be changed in an upcoming patch that adds
more vartag_external types and its associated structures, with the code
being able to use a different external TOAST pointer depending on the
attribute type of chunk_id in TOAST relations.

All the changes done here are mechanical, with all the TOAST code able
to do chunk ID lookups based on the two types supported.

XXX: Catalog version bump required.
---
 src/backend/access/common/toast_internals.c | 67 +++++++++++++++------
 src/backend/access/heap/heaptoast.c         | 20 ++++--
 src/backend/catalog/toasting.c              | 32 +++++++++-
 doc/src/sgml/storage.sgml                   |  7 ++-
 contrib/amcheck/verify_heapam.c             | 19 ++++--
 5 files changed, 115 insertions(+), 30 deletions(-)

diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index b0a0ce1e1b9a..98a240242127 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -18,6 +18,7 @@
 #include "access/heapam.h"
 #include "access/heaptoast.h"
 #include "access/table.h"
+#include "access/toast_counter.h"
 #include "access/toast_external.h"
 #include "access/toast_internals.h"
 #include "access/xact.h"
@@ -26,6 +27,7 @@
 #include "utils/fmgroids.h"
 #include "utils/rel.h"
 #include "utils/snapmgr.h"
+#include "utils/lsyscache.h"
 
 static bool toastrel_valueid_exists(Relation toastrel, uint64 valueid);
 static bool toastid_valueid_exists(Oid toastrelid, uint64 valueid);
@@ -146,8 +148,10 @@ toast_save_datum(Relation rel, Datum value,
 	int			validIndex;
 	const toast_external_info *info;
 	uint8		tag = VARTAG_INDIRECT;	/* init value does not matter */
+	Oid			toast_typid = get_atttype(rel->rd_rel->reltoastrelid, 1);
 
 	Assert(!VARATT_IS_EXTERNAL(dval));
+	Assert(OidIsValid(toast_typid));
 
 	/*
 	 * Open the toast relation and its indexes.  We can use the index to check
@@ -237,20 +241,23 @@ toast_save_datum(Relation rel, Datum value,
 	info = toast_external_get_info(tag);
 
 	/*
-	 * Choose an OID to use as the value ID for this toast value.
+	 * Choose a new value to use as the value ID for this toast value, be it
+	 * for OID or int8-based TOAST relations.
 	 *
-	 * Normally we just choose an unused OID within the toast table.  But
+	 * Normally we just choose an unused value within the toast table.  But
 	 * during table-rewriting operations where we are preserving an existing
-	 * toast table OID, we want to preserve toast value OIDs too.  So, if
+	 * toast table OID, we want to preserve toast value IDs too.  So, if
 	 * rd_toastoid is set and we had a prior external value from that same
 	 * toast table, re-use its value ID.  If we didn't have a prior external
 	 * value (which is a corner case, but possible if the table's attstorage
 	 * options have been changed), we have to pick a value ID that doesn't
-	 * conflict with either new or existing toast value OIDs.
+	 * conflict with either new or existing toast value IDs.  If the TOAST
+	 * table uses 8-byte value IDs, we should not really care much about
+	 * that.
 	 */
 	if (!OidIsValid(rel->rd_toastoid))
 	{
-		/* normal case: just choose an unused OID */
+		/* normal case: just choose an unused ID */
 		toast_pointer.value =
 			info->get_new_value(toastrel,
 								RelationGetRelid(toastidxs[validIndex]),
@@ -269,7 +276,7 @@ toast_save_datum(Relation rel, Datum value,
 
 			if (old_toast_pointer.toastrelid == rel->rd_toastoid)
 			{
-				/* This value came from the old toast table; reuse its OID */
+				/* This value came from the old toast table; reuse its ID */
 				toast_pointer.value = old_toast_pointer.value;
 
 				/*
@@ -300,8 +307,8 @@ toast_save_datum(Relation rel, Datum value,
 		if (toast_pointer.value == InvalidToastId)
 		{
 			/*
-			 * new value; must choose an OID that doesn't conflict in either
-			 * old or new toast table
+			 * new value; must choose a value that doesn't conflict in either
+			 * old or new toast table.
 			 */
 			do
 			{
@@ -317,7 +324,10 @@ toast_save_datum(Relation rel, Datum value,
 	/*
 	 * Initialize constant parts of the tuple data
 	 */
-	t_values[0] = ObjectIdGetDatum(toast_pointer.value);
+	if (toast_typid == OIDOID)
+		t_values[0] = ObjectIdGetDatum(toast_pointer.value);
+	else if (toast_typid == INT8OID)
+		t_values[0] = Int64GetDatum(toast_pointer.value);
 	t_values[2] = PointerGetDatum(&chunk_data);
 	t_isnull[0] = false;
 	t_isnull[1] = false;
@@ -424,6 +434,7 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	HeapTuple	toasttup;
 	int			num_indexes;
 	int			validIndex;
+	Oid			toast_typid;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		return;
@@ -435,6 +446,8 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	 * Open the toast relation and its indexes
 	 */
 	toastrel = table_open(toast_pointer.toastrelid, RowExclusiveLock);
+	toast_typid = TupleDescAttr(toastrel->rd_att, 0)->atttypid;
+	Assert(toast_typid == OIDOID || toast_typid == INT8OID);
 
 	/* Fetch valid relation used for process */
 	validIndex = toast_open_indexes(toastrel,
@@ -445,10 +458,18 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	/*
 	 * Setup a scan key to find chunks with matching va_valueid
 	 */
-	ScanKeyInit(&toastkey,
-				(AttrNumber) 1,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(toast_pointer.value));
+	if (toast_typid == OIDOID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(toast_pointer.value));
+	else if (toast_typid == INT8OID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_INT8EQ,
+					Int64GetDatum(toast_pointer.value));
+	else
+		Assert(false);
 
 	/*
 	 * Find all the chunks.  (We don't actually care whether we see them in
@@ -495,6 +516,7 @@ toastrel_valueid_exists(Relation toastrel, uint64 valueid)
 	int			num_indexes;
 	int			validIndex;
 	Relation   *toastidxs;
+	Oid			toast_typid;
 
 	/* Fetch a valid index relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -502,13 +524,24 @@ toastrel_valueid_exists(Relation toastrel, uint64 valueid)
 									&toastidxs,
 									&num_indexes);
 
+	toast_typid = TupleDescAttr(toastrel->rd_att, 0)->atttypid;
+	Assert(toast_typid == OIDOID || toast_typid == INT8OID);
+
 	/*
 	 * Setup a scan key to find chunks with matching va_valueid
 	 */
-	ScanKeyInit(&toastkey,
-				(AttrNumber) 1,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(valueid));
+	if (toast_typid == OIDOID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(valueid));
+	else if (toast_typid == INT8OID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_INT8EQ,
+					Int64GetDatum(valueid));
+	else
+		Assert(false);
 
 	/*
 	 * Is there any such chunk?
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index c215263eb76a..a01c1d627c6f 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -654,6 +654,7 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 	int32		max_chunk_size;
 	const toast_external_info *info;
 	uint8		tag = VARTAG_INDIRECT;	/* init value does not matter */
+	Oid			toast_typid;
 
 	/* Look for the valid index of toast relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -667,16 +668,27 @@ heap_fetch_toast_slice(Relation toastrel, uint64 valueid, int32 attrsize,
 
 	max_chunk_size = info->maximum_chunk_size;
 
+	toast_typid = TupleDescAttr(toastrel->rd_att, 0)->atttypid;
+	Assert(toast_typid == OIDOID || toast_typid == INT8OID);
+
 	totalchunks = ((attrsize - 1) / max_chunk_size) + 1;
 	startchunk = sliceoffset / max_chunk_size;
 	endchunk = (sliceoffset + slicelength - 1) / max_chunk_size;
 	Assert(endchunk <= totalchunks);
 
 	/* Set up a scan key to fetch from the index. */
-	ScanKeyInit(&toastkey[0],
-				(AttrNumber) 1,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(valueid));
+	if (toast_typid == OIDOID)
+		ScanKeyInit(&toastkey[0],
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(valueid));
+	else if (toast_typid == INT8OID)
+		ScanKeyInit(&toastkey[0],
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_INT8EQ,
+					Int64GetDatum(valueid));
+	else
+		Assert(false);
 
 	/*
 	 * No additional condition if fetching all chunks. Otherwise, use an
diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c
index e595cb61b375..27295866c490 100644
--- a/src/backend/catalog/toasting.c
+++ b/src/backend/catalog/toasting.c
@@ -32,6 +32,7 @@
 #include "nodes/makefuncs.h"
 #include "utils/fmgroids.h"
 #include "utils/rel.h"
+#include "utils/lsyscache.h"
 #include "utils/syscache.h"
 
 /* GUC support */
@@ -149,6 +150,7 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	int16		coloptions[2];
 	ObjectAddress baseobject,
 				toastobject;
+	Oid			toast_typid = InvalidOid;
 
 	/*
 	 * Is it already toasted?
@@ -204,11 +206,34 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	snprintf(toast_idxname, sizeof(toast_idxname),
 			 "pg_toast_%u_index", relOid);
 
+	/*
+	 * Determine the type OID to use for the value.  If OIDOldToast is
+	 * defined, we need to rely on the existing table for the job because
+	 * we do not want to create an inconsistent relation that would conflict
+	 * with the parent and break the world.
+	 */
+	if (!OidIsValid(OIDOldToast))
+	{
+		if (default_toast_type == TOAST_TYPE_OID)
+			toast_typid = OIDOID;
+		else if (default_toast_type == TOAST_TYPE_INT8)
+			toast_typid = INT8OID;
+		else
+			Assert(false);
+	}
+	else
+	{
+		/* For the chunk_id type */
+		toast_typid = get_atttype(OIDOldToast, 1);
+		if (!OidIsValid(toast_typid))
+			elog(ERROR, "cache lookup failed for relation %u", OIDOldToast);
+	}
+
 	/* this is pretty painful...  need a tuple descriptor */
 	tupdesc = CreateTemplateTupleDesc(3);
 	TupleDescInitEntry(tupdesc, (AttrNumber) 1,
 					   "chunk_id",
-					   OIDOID,
+					   toast_typid,
 					   -1, 0);
 	TupleDescInitEntry(tupdesc, (AttrNumber) 2,
 					   "chunk_seq",
@@ -316,7 +341,10 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	collationIds[0] = InvalidOid;
 	collationIds[1] = InvalidOid;
 
-	opclassIds[0] = OID_BTREE_OPS_OID;
+	if (toast_typid == OIDOID)
+		opclassIds[0] = OID_BTREE_OPS_OID;
+	else if (toast_typid == INT8OID)
+		opclassIds[0] = INT8_BTREE_OPS_OID;
 	opclassIds[1] = INT4_BTREE_OPS_OID;
 
 	coloptions[0] = 0;
diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml
index f3c6cd8860b5..564783a1c559 100644
--- a/doc/src/sgml/storage.sgml
+++ b/doc/src/sgml/storage.sgml
@@ -419,14 +419,15 @@ most <symbol>TOAST_MAX_CHUNK_SIZE_OID</symbol> bytes (by default this value is c
 so that four chunk rows will fit on a page, making it about 2000 bytes).
 Each chunk is stored as a separate row in the <acronym>TOAST</acronym> table
 belonging to the owning table.  Every
-<acronym>TOAST</acronym> table has the columns <structfield>chunk_id</structfield> (an OID
-identifying the particular <acronym>TOAST</acronym>ed value),
+<acronym>TOAST</acronym> table has the columns
+<structfield>chunk_id</structfield> (an OID or an 8-byte integer identifying
+the particular <acronym>TOAST</acronym>ed value),
 <structfield>chunk_seq</structfield> (a sequence number for the chunk within its value),
 and <structfield>chunk_data</structfield> (the actual data of the chunk).  A unique index
 on <structfield>chunk_id</structfield> and <structfield>chunk_seq</structfield> provides fast
 retrieval of the values.  A pointer datum representing an out-of-line on-disk
 <acronym>TOAST</acronym>ed value therefore needs to store the OID of the
-<acronym>TOAST</acronym> table in which to look and the OID of the specific value
+<acronym>TOAST</acronym> table in which to look and the specific value
 (its <structfield>chunk_id</structfield>).  For convenience, pointer datums also store the
 logical datum size (original uncompressed data length), physical stored size
 (different if compression was applied), and the compression method used, if
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 11c4507ae6e2..833811c75437 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1880,6 +1880,9 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	int32		last_chunk_seq;
 	uint64		toast_valueid;
 	int32		max_chunk_size;
+	Oid			toast_typid;
+
+	toast_typid = TupleDescAttr(ctx->toast_rel->rd_att, 0)->atttypid;
 
 	extsize = ta->toast_pointer.extsize;
 
@@ -1889,10 +1892,18 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	/*
 	 * Setup a scan key to find chunks in toast table with matching va_valueid
 	 */
-	ScanKeyInit(&toastkey,
-				(AttrNumber) 1,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(ta->toast_pointer.value));
+	if (toast_typid == OIDOID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(ta->toast_pointer.value));
+	else if (toast_typid == INT8OID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_INT8EQ,
+					Int64GetDatum(ta->toast_pointer.value));
+	else
+		Assert(false);
 
 	/*
 	 * Check if any chunks for this toasted object exist in the toast table,
-- 
2.50.0

v4-0012-Add-tests-for-TOAST-relations-with-bigint-as-valu.patchtext/x-diff; charset=us-asciiDownload
From 63c32ec1f0d50b2f1278418b38269bd8a3410666 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 19 Jun 2025 11:15:48 +0900
Subject: [PATCH v4 12/15] Add tests for TOAST relations with bigint as value
 type

This adds coverage for relations created with default_toast_type =
'int8', for external TOAST pointers both compressed and uncompressed.
---
 src/test/regress/expected/strings.out | 238 ++++++++++++++++++++++----
 src/test/regress/sql/strings.sql      | 142 +++++++++++----
 2 files changed, 305 insertions(+), 75 deletions(-)

diff --git a/src/test/regress/expected/strings.out b/src/test/regress/expected/strings.out
index ba302da51e7b..9b881b4b7c57 100644
--- a/src/test/regress/expected/strings.out
+++ b/src/test/regress/expected/strings.out
@@ -1945,21 +1945,40 @@ SELECT text 'text' || varchar ' and varchar' AS "Concat text to varchar";
 (1 row)
 
 --
--- test substr with toasted text values
+-- test substr with toasted text values, for all types of TOAST relations
+-- supported.
 --
-CREATE TABLE toasttest(f1 text);
-insert into toasttest values(repeat('1234567890',10000));
-insert into toasttest values(repeat('1234567890',10000));
+SET default_toast_type = 'oid';
+CREATE TABLE toasttest_oid(f1 text);
+SET default_toast_type = 'int8';
+CREATE TABLE toasttest_int8(f1 text);
+RESET default_toast_type;
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_int8 values(repeat('1234567890',10000));
+insert into toasttest_int8 values(repeat('1234567890',10000));
 --
 -- Ensure that some values are uncompressed, to test the faster substring
 -- operation used in that case
 --
-alter table toasttest alter column f1 set storage external;
-insert into toasttest values(repeat('1234567890',10000));
-insert into toasttest values(repeat('1234567890',10000));
+alter table toasttest_oid alter column f1 set storage external;
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+alter table toasttest_int8 alter column f1 set storage external;
+insert into toasttest_int8 values(repeat('1234567890',10000));
+insert into toasttest_int8 values(repeat('1234567890',10000));
 -- If the starting position is zero or less, then return from the start of the string
 -- adjusting the length to be consistent with the "negative start" per SQL.
-SELECT substr(f1, -1, 5) from toasttest;
+SELECT substr(f1, -1, 5) from toasttest_oid;
+ substr 
+--------
+ 123
+ 123
+ 123
+ 123
+(4 rows)
+
+SELECT substr(f1, -1, 5) from toasttest_int8;
  substr 
 --------
  123
@@ -1969,11 +1988,22 @@ SELECT substr(f1, -1, 5) from toasttest;
 (4 rows)
 
 -- If the length is less than zero, an ERROR is thrown.
-SELECT substr(f1, 5, -1) from toasttest;
+SELECT substr(f1, 5, -1) from toasttest_oid;
+ERROR:  negative substring length not allowed
+SELECT substr(f1, 5, -1) from toasttest_int8;
 ERROR:  negative substring length not allowed
 -- If no third argument (length) is provided, the length to the end of the
 -- string is assumed.
-SELECT substr(f1, 99995) from toasttest;
+SELECT substr(f1, 99995) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995) from toasttest_int8;
  substr 
 --------
  567890
@@ -1984,7 +2014,7 @@ SELECT substr(f1, 99995) from toasttest;
 
 -- If start plus length is > string length, the result is truncated to
 -- string length
-SELECT substr(f1, 99995, 10) from toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
  substr 
 --------
  567890
@@ -1993,50 +2023,108 @@ SELECT substr(f1, 99995, 10) from toasttest;
  567890
 (4 rows)
 
-TRUNCATE TABLE toasttest;
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
+SELECT substr(f1, 99995, 10) from toasttest_int8;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+-- TRUNCATE cases for TOAST relations with OID values.
+TRUNCATE TABLE toasttest_oid;
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
 -- expect >0 blocks
 SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
-  FROM pg_class where relname = 'toasttest';
+  FROM pg_class where relname = 'toasttest_oid';
  is_empty 
 ----------
  f
 (1 row)
 
-TRUNCATE TABLE toasttest;
-ALTER TABLE toasttest set (toast_tuple_target = 4080);
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
+TRUNCATE TABLE toasttest_oid;
+ALTER TABLE toasttest_oid set (toast_tuple_target = 4080);
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
 -- expect 0 blocks
 SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
-  FROM pg_class where relname = 'toasttest';
+  FROM pg_class where relname = 'toasttest_oid';
  is_empty 
 ----------
  t
 (1 row)
 
-DROP TABLE toasttest;
+DROP TABLE toasttest_oid;
+-- TRUNCATE cases for TOAST relation with int8 values.
+TRUNCATE TABLE toasttest_int8;
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+-- expect >0 blocks
+SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
+  FROM pg_class where relname = 'toasttest_int8';
+ is_empty 
+----------
+ f
+(1 row)
+
+TRUNCATE TABLE toasttest_int8;
+ALTER TABLE toasttest_int8 set (toast_tuple_target = 4080);
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+-- expect 0 blocks
+SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
+  FROM pg_class where relname = 'toasttest_int8';
+ is_empty 
+----------
+ t
+(1 row)
+
+DROP TABLE toasttest_int8;
 --
--- test substr with toasted bytea values
+-- test substr with toasted bytea values, for all types of TOAST relations
+-- supported.
 --
-CREATE TABLE toasttest(f1 bytea);
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
+SET default_toast_type = 'oid';
+CREATE TABLE toasttest_oid(f1 bytea);
+SET default_toast_type = 'int8';
+CREATE TABLE toasttest_int8(f1 bytea);
+RESET default_toast_type;
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_int8 values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_int8 values(decode(repeat('1234567890',10000),'escape'));
 --
 -- Ensure that some values are uncompressed, to test the faster substring
 -- operation used in that case
 --
-alter table toasttest alter column f1 set storage external;
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
+alter table toasttest_oid alter column f1 set storage external;
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+alter table toasttest_int8 alter column f1 set storage external;
+insert into toasttest_int8 values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_int8 values(decode(repeat('1234567890',10000),'escape'));
 -- If the starting position is zero or less, then return from the start of the string
 -- adjusting the length to be consistent with the "negative start" per SQL.
-SELECT substr(f1, -1, 5) from toasttest;
+SELECT substr(f1, -1, 5) from toasttest_oid;
+ substr 
+--------
+ 123
+ 123
+ 123
+ 123
+(4 rows)
+
+SELECT substr(f1, -1, 5) from toasttest_int8;
  substr 
 --------
  123
@@ -2046,11 +2134,22 @@ SELECT substr(f1, -1, 5) from toasttest;
 (4 rows)
 
 -- If the length is less than zero, an ERROR is thrown.
-SELECT substr(f1, 5, -1) from toasttest;
+SELECT substr(f1, 5, -1) from toasttest_oid;
+ERROR:  negative substring length not allowed
+SELECT substr(f1, 5, -1) from toasttest_int8;
 ERROR:  negative substring length not allowed
 -- If no third argument (length) is provided, the length to the end of the
 -- string is assumed.
-SELECT substr(f1, 99995) from toasttest;
+SELECT substr(f1, 99995) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995) from toasttest_int8;
  substr 
 --------
  567890
@@ -2061,7 +2160,7 @@ SELECT substr(f1, 99995) from toasttest;
 
 -- If start plus length is > string length, the result is truncated to
 -- string length
-SELECT substr(f1, 99995, 10) from toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
  substr 
 --------
  567890
@@ -2070,7 +2169,72 @@ SELECT substr(f1, 99995, 10) from toasttest;
  567890
 (4 rows)
 
-DROP TABLE toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_int8;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+-- A relation rewrite leaves the TOAST value attributes unchanged.
+VACUUM FULL toasttest_oid;
+VACUUM FULL toasttest_int8;
+SELECT c1.relname, a.atttypid::regtype
+  FROM pg_attribute AS a,
+       pg_class AS c1,
+       pg_class AS c2
+  WHERE
+       c1.relname IN ('toasttest_oid', 'toasttest_int8') AND
+       c1.reltoastrelid = c2.oid AND
+       a.attrelid = c2.oid AND
+       a.attname = 'chunk_id'
+  ORDER BY c1.relname COLLATE "C";
+    relname     | atttypid 
+----------------+----------
+ toasttest_int8 | bigint
+ toasttest_oid  | oid
+(2 rows)
+
+-- Check that data slices are still accessible.
+SELECT substr(f1, 99995) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995) from toasttest_int8;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995, 10) from toasttest_int8;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+DROP TABLE toasttest_oid, toasttest_int8;
 -- test internally compressing datums
 -- this tests compressing a datum to a very small size which exercises a
 -- corner case in packed-varlena handling: even though small, the compressed
diff --git a/src/test/regress/sql/strings.sql b/src/test/regress/sql/strings.sql
index b94004cc08ce..6dc2bbfed70a 100644
--- a/src/test/regress/sql/strings.sql
+++ b/src/test/regress/sql/strings.sql
@@ -553,89 +553,155 @@ SELECT text 'text' || char(20) ' and characters' AS "Concat text to char";
 SELECT text 'text' || varchar ' and varchar' AS "Concat text to varchar";
 
 --
--- test substr with toasted text values
+-- test substr with toasted text values, for all types of TOAST relations
+-- supported.
 --
-CREATE TABLE toasttest(f1 text);
+SET default_toast_type = 'oid';
+CREATE TABLE toasttest_oid(f1 text);
+SET default_toast_type = 'int8';
+CREATE TABLE toasttest_int8(f1 text);
+RESET default_toast_type;
 
-insert into toasttest values(repeat('1234567890',10000));
-insert into toasttest values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_int8 values(repeat('1234567890',10000));
+insert into toasttest_int8 values(repeat('1234567890',10000));
 
 --
 -- Ensure that some values are uncompressed, to test the faster substring
 -- operation used in that case
 --
-alter table toasttest alter column f1 set storage external;
-insert into toasttest values(repeat('1234567890',10000));
-insert into toasttest values(repeat('1234567890',10000));
+alter table toasttest_oid alter column f1 set storage external;
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+alter table toasttest_int8 alter column f1 set storage external;
+insert into toasttest_int8 values(repeat('1234567890',10000));
+insert into toasttest_int8 values(repeat('1234567890',10000));
 
 -- If the starting position is zero or less, then return from the start of the string
 -- adjusting the length to be consistent with the "negative start" per SQL.
-SELECT substr(f1, -1, 5) from toasttest;
+SELECT substr(f1, -1, 5) from toasttest_oid;
+SELECT substr(f1, -1, 5) from toasttest_int8;
 
 -- If the length is less than zero, an ERROR is thrown.
-SELECT substr(f1, 5, -1) from toasttest;
+SELECT substr(f1, 5, -1) from toasttest_oid;
+SELECT substr(f1, 5, -1) from toasttest_int8;
 
 -- If no third argument (length) is provided, the length to the end of the
 -- string is assumed.
-SELECT substr(f1, 99995) from toasttest;
+SELECT substr(f1, 99995) from toasttest_oid;
+SELECT substr(f1, 99995) from toasttest_int8;
 
 -- If start plus length is > string length, the result is truncated to
 -- string length
-SELECT substr(f1, 99995, 10) from toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+SELECT substr(f1, 99995, 10) from toasttest_int8;
 
-TRUNCATE TABLE toasttest;
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
+-- TRUNCATE cases for TOAST relations with OID values.
+TRUNCATE TABLE toasttest_oid;
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
 -- expect >0 blocks
 SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
-  FROM pg_class where relname = 'toasttest';
-
-TRUNCATE TABLE toasttest;
-ALTER TABLE toasttest set (toast_tuple_target = 4080);
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
+  FROM pg_class where relname = 'toasttest_oid';
+TRUNCATE TABLE toasttest_oid;
+ALTER TABLE toasttest_oid set (toast_tuple_target = 4080);
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
 -- expect 0 blocks
 SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
-  FROM pg_class where relname = 'toasttest';
+  FROM pg_class where relname = 'toasttest_oid';
+DROP TABLE toasttest_oid;
 
-DROP TABLE toasttest;
+-- TRUNCATE cases for TOAST relation with int8 values.
+TRUNCATE TABLE toasttest_int8;
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+-- expect >0 blocks
+SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
+  FROM pg_class where relname = 'toasttest_int8';
+TRUNCATE TABLE toasttest_int8;
+ALTER TABLE toasttest_int8 set (toast_tuple_target = 4080);
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+INSERT INTO toasttest_int8 values (repeat('1234567890',300));
+-- expect 0 blocks
+SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
+  FROM pg_class where relname = 'toasttest_int8';
+DROP TABLE toasttest_int8;
 
 --
--- test substr with toasted bytea values
+-- test substr with toasted bytea values, for all types of TOAST relations
+-- supported.
 --
-CREATE TABLE toasttest(f1 bytea);
+SET default_toast_type = 'oid';
+CREATE TABLE toasttest_oid(f1 bytea);
+SET default_toast_type = 'int8';
+CREATE TABLE toasttest_int8(f1 bytea);
+RESET default_toast_type;
 
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_int8 values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_int8 values(decode(repeat('1234567890',10000),'escape'));
 
 --
 -- Ensure that some values are uncompressed, to test the faster substring
 -- operation used in that case
 --
-alter table toasttest alter column f1 set storage external;
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
+alter table toasttest_oid alter column f1 set storage external;
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+alter table toasttest_int8 alter column f1 set storage external;
+insert into toasttest_int8 values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_int8 values(decode(repeat('1234567890',10000),'escape'));
 
 -- If the starting position is zero or less, then return from the start of the string
 -- adjusting the length to be consistent with the "negative start" per SQL.
-SELECT substr(f1, -1, 5) from toasttest;
+SELECT substr(f1, -1, 5) from toasttest_oid;
+SELECT substr(f1, -1, 5) from toasttest_int8;
 
 -- If the length is less than zero, an ERROR is thrown.
-SELECT substr(f1, 5, -1) from toasttest;
+SELECT substr(f1, 5, -1) from toasttest_oid;
+SELECT substr(f1, 5, -1) from toasttest_int8;
 
 -- If no third argument (length) is provided, the length to the end of the
 -- string is assumed.
-SELECT substr(f1, 99995) from toasttest;
+SELECT substr(f1, 99995) from toasttest_oid;
+SELECT substr(f1, 99995) from toasttest_int8;
 
 -- If start plus length is > string length, the result is truncated to
 -- string length
-SELECT substr(f1, 99995, 10) from toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+SELECT substr(f1, 99995, 10) from toasttest_int8;
 
-DROP TABLE toasttest;
+-- A relation rewrite leaves the TOAST value attributes unchanged.
+VACUUM FULL toasttest_oid;
+VACUUM FULL toasttest_int8;
+SELECT c1.relname, a.atttypid::regtype
+  FROM pg_attribute AS a,
+       pg_class AS c1,
+       pg_class AS c2
+  WHERE
+       c1.relname IN ('toasttest_oid', 'toasttest_int8') AND
+       c1.reltoastrelid = c2.oid AND
+       a.attrelid = c2.oid AND
+       a.attname = 'chunk_id'
+  ORDER BY c1.relname COLLATE "C";
+-- Check that data slices are still accessible.
+SELECT substr(f1, 99995) from toasttest_oid;
+SELECT substr(f1, 99995) from toasttest_int8;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+SELECT substr(f1, 99995, 10) from toasttest_int8;
+
+DROP TABLE toasttest_oid, toasttest_int8;
 
 -- test internally compressing datums
 
-- 
2.50.0

v4-0013-Add-support-for-TOAST-table-types-in-pg_dump-and-.patchtext/x-diff; charset=us-asciiDownload
From 88c8491844ef525d23c46cf9b290836dfb5db0da Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 8 Aug 2025 16:04:54 +0900
Subject: [PATCH v4 13/15] Add support for TOAST table types in pg_dump and
 pg_restore

This includes the possibility to perform binary upgrades with TOAST
table types applied to a new cluster, relying on SET commands based on
default_toast_type to apply one type of TOAST table or the other.

Some tests are included, this is a pretty mechanical change.

Dump format is bumped to 1.17 due to the addition of the TOAST table
type in the custom format.
---
 src/bin/pg_dump/pg_backup.h          |  2 +
 src/bin/pg_dump/pg_backup_archiver.c | 69 +++++++++++++++++++++++++++-
 src/bin/pg_dump/pg_backup_archiver.h |  6 ++-
 src/bin/pg_dump/pg_dump.c            | 21 +++++++++
 src/bin/pg_dump/pg_dump.h            |  1 +
 src/bin/pg_dump/pg_dumpall.c         |  5 ++
 src/bin/pg_dump/pg_restore.c         |  4 ++
 src/bin/pg_dump/t/002_pg_dump.pl     | 35 ++++++++++++++
 doc/src/sgml/ref/pg_dump.sgml        | 12 +++++
 doc/src/sgml/ref/pg_dumpall.sgml     | 12 +++++
 doc/src/sgml/ref/pg_restore.sgml     | 12 +++++
 11 files changed, 177 insertions(+), 2 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 4ebef1e86445..f99d5f0d3d1b 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -99,6 +99,7 @@ typedef struct _restoreOptions
 	int			noOwner;		/* Don't try to match original object owner */
 	int			noTableAm;		/* Don't issue table-AM-related commands */
 	int			noTablespace;	/* Don't issue tablespace-related commands */
+	int			noToastType;	/* Don't issue TOAST-type-related commands */
 	int			disable_triggers;	/* disable triggers during data-only
 									 * restore */
 	int			use_setsessauth;	/* Use SET SESSION AUTHORIZATION commands
@@ -192,6 +193,7 @@ typedef struct _dumpOptions
 	int			disable_triggers;
 	int			outputNoTableAm;
 	int			outputNoTablespaces;
+	int			outputNoToastType;
 	int			use_setsessauth;
 	int			enable_row_security;
 	int			load_via_partition_root;
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index dce88f040ace..5690c0850559 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -185,6 +185,7 @@ dumpOptionsFromRestoreOptions(RestoreOptions *ropt)
 	dopt->outputNoOwner = ropt->noOwner;
 	dopt->outputNoTableAm = ropt->noTableAm;
 	dopt->outputNoTablespaces = ropt->noTablespace;
+	dopt->outputNoToastType = ropt->noToastType;
 	dopt->disable_triggers = ropt->disable_triggers;
 	dopt->use_setsessauth = ropt->use_setsessauth;
 	dopt->disable_dollar_quoting = ropt->disable_dollar_quoting;
@@ -1244,6 +1245,7 @@ ArchiveEntry(Archive *AHX, CatalogId catalogId, DumpId dumpId,
 	newToc->namespace = opts->namespace ? pg_strdup(opts->namespace) : NULL;
 	newToc->tablespace = opts->tablespace ? pg_strdup(opts->tablespace) : NULL;
 	newToc->tableam = opts->tableam ? pg_strdup(opts->tableam) : NULL;
+	newToc->toasttype = opts->toasttype ? pg_strdup(opts->toasttype) : NULL;
 	newToc->relkind = opts->relkind;
 	newToc->owner = opts->owner ? pg_strdup(opts->owner) : NULL;
 	newToc->desc = pg_strdup(opts->description);
@@ -2403,6 +2405,7 @@ _allocAH(const char *FileSpec, const ArchiveFormat fmt,
 	AH->currSchema = NULL;		/* ditto */
 	AH->currTablespace = NULL;	/* ditto */
 	AH->currTableAm = NULL;		/* ditto */
+	AH->currToastType = NULL;		/* ditto */
 
 	AH->toc = (TocEntry *) pg_malloc0(sizeof(TocEntry));
 
@@ -2670,6 +2673,7 @@ WriteToc(ArchiveHandle *AH)
 		WriteStr(AH, te->tablespace);
 		WriteStr(AH, te->tableam);
 		WriteInt(AH, te->relkind);
+		WriteStr(AH, te->toasttype);
 		WriteStr(AH, te->owner);
 		WriteStr(AH, "false");
 
@@ -2778,6 +2782,9 @@ ReadToc(ArchiveHandle *AH)
 		if (AH->version >= K_VERS_1_16)
 			te->relkind = ReadInt(AH);
 
+		if (AH->version >= K_VERS_1_17)
+			te->toasttype = ReadStr(AH);
+
 		te->owner = ReadStr(AH);
 		is_supported = true;
 		if (AH->version < K_VERS_1_9)
@@ -3477,6 +3484,9 @@ _reconnectToDB(ArchiveHandle *AH, const char *dbname)
 	free(AH->currTablespace);
 	AH->currTablespace = NULL;
 
+	free(AH->currToastType);
+	AH->currToastType = NULL;
+
 	/* re-establish fixed state */
 	_doSetFixedOutputState(AH);
 }
@@ -3682,6 +3692,56 @@ _selectTableAccessMethod(ArchiveHandle *AH, const char *tableam)
 	AH->currTableAm = pg_strdup(want);
 }
 
+
+/*
+ * Set the proper default_toast_type value for the table.
+ */
+static void
+_selectToastType(ArchiveHandle *AH, const char *toasttype)
+{
+	RestoreOptions *ropt = AH->public.ropt;
+	PQExpBuffer cmd;
+	const char *want,
+			   *have;
+
+	/* do nothing in --no-toast-type mode */
+	if (ropt->noToastType)
+		return;
+
+	have = AH->currToastType;
+	want = toasttype;
+
+	if (!want)
+		return;
+
+	if (have && strcmp(want, have) == 0)
+		return;
+
+	cmd = createPQExpBuffer();
+
+	appendPQExpBuffer(cmd, "SET default_toast_type = %s;", fmtId(toasttype));
+
+	if (RestoringToDB(AH))
+	{
+		PGresult   *res;
+
+		res = PQexec(AH->connection, cmd->data);
+
+		if (!res || PQresultStatus(res) != PGRES_COMMAND_OK)
+			warn_or_exit_horribly(AH,
+								  "could not set \"default_toast_type\": %s",
+								  PQerrorMessage(AH->connection));
+		PQclear(res);
+	}
+	else
+		ahprintf(AH, "%s\n\n", cmd->data);
+
+	destroyPQExpBuffer(cmd);
+
+	free(AH->currToastType);
+	AH->currToastType = pg_strdup(want);
+}
+
 /*
  * Set the proper default table access method for a table without storage.
  * Currently, this is required only for partitioned tables with a table AM.
@@ -3837,13 +3897,16 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, const char *pfx)
 	 * Select owner, schema, tablespace and default AM as necessary. The
 	 * default access method for partitioned tables is handled after
 	 * generating the object definition, as it requires an ALTER command
-	 * rather than SET.
+	 * rather than SET.  Partitioned tables do not have TOAST tables.
 	 */
 	_becomeOwner(AH, te);
 	_selectOutputSchema(AH, te->namespace);
 	_selectTablespace(AH, te->tablespace);
 	if (te->relkind != RELKIND_PARTITIONED_TABLE)
+	{
 		_selectTableAccessMethod(AH, te->tableam);
+		_selectToastType(AH, te->toasttype);
+	}
 
 	/* Emit header comment for item */
 	if (!AH->noTocComments)
@@ -4402,6 +4465,8 @@ restore_toc_entries_prefork(ArchiveHandle *AH, TocEntry *pending_list)
 	AH->currTablespace = NULL;
 	free(AH->currTableAm);
 	AH->currTableAm = NULL;
+	free(AH->currToastType);
+	AH->currToastType = NULL;
 }
 
 /*
@@ -5139,6 +5204,7 @@ CloneArchive(ArchiveHandle *AH)
 	clone->currSchema = NULL;
 	clone->currTableAm = NULL;
 	clone->currTablespace = NULL;
+	clone->currToastType = NULL;
 
 	/* savedPassword must be local in case we change it while connecting */
 	if (clone->savedPassword)
@@ -5198,6 +5264,7 @@ DeCloneArchive(ArchiveHandle *AH)
 	free(AH->currSchema);
 	free(AH->currTablespace);
 	free(AH->currTableAm);
+	free(AH->currToastType);
 	free(AH->savedPassword);
 
 	free(AH);
diff --git a/src/bin/pg_dump/pg_backup_archiver.h b/src/bin/pg_dump/pg_backup_archiver.h
index 325b53fc9bd4..a9f8f75d4382 100644
--- a/src/bin/pg_dump/pg_backup_archiver.h
+++ b/src/bin/pg_dump/pg_backup_archiver.h
@@ -71,10 +71,11 @@
 #define K_VERS_1_16 MAKE_ARCHIVE_VERSION(1, 16, 0)	/* BLOB METADATA entries
 													 * and multiple BLOBS,
 													 * relkind */
+#define K_VERS_1_17 MAKE_ARCHIVE_VERSION(1, 17, 0)	/* TOAST type */
 
 /* Current archive version number (the format we can output) */
 #define K_VERS_MAJOR 1
-#define K_VERS_MINOR 16
+#define K_VERS_MINOR 17
 #define K_VERS_REV 0
 #define K_VERS_SELF MAKE_ARCHIVE_VERSION(K_VERS_MAJOR, K_VERS_MINOR, K_VERS_REV)
 
@@ -325,6 +326,7 @@ struct _archiveHandle
 	char	   *currSchema;		/* current schema, or NULL */
 	char	   *currTablespace; /* current tablespace, or NULL */
 	char	   *currTableAm;	/* current table access method, or NULL */
+	char	   *currToastType;	/* current TOAST type, or NULL */
 
 	/* in --transaction-size mode, this counts objects emitted in cur xact */
 	int			txnCount;
@@ -359,6 +361,7 @@ struct _tocEntry
 	char	   *tablespace;		/* null if not in a tablespace; empty string
 								 * means use database default */
 	char	   *tableam;		/* table access method, only for TABLE tags */
+	char	   *toasttype;		/* TOAST table type, only for TABLE tags */
 	char		relkind;		/* relation kind, only for TABLE tags */
 	char	   *owner;
 	char	   *desc;
@@ -404,6 +407,7 @@ typedef struct _archiveOpts
 	const char *namespace;
 	const char *tablespace;
 	const char *tableam;
+	const char *toasttype;
 	char		relkind;
 	const char *owner;
 	const char *description;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index f3a353a61a58..7368cbb936b4 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -506,6 +506,7 @@ main(int argc, char **argv)
 		{"lock-wait-timeout", required_argument, NULL, 2},
 		{"no-table-access-method", no_argument, &dopt.outputNoTableAm, 1},
 		{"no-tablespaces", no_argument, &dopt.outputNoTablespaces, 1},
+		{"no-toast-type", no_argument, &dopt.outputNoToastType, 1},
 		{"quote-all-identifiers", no_argument, &quote_all_identifiers, 1},
 		{"load-via-partition-root", no_argument, &dopt.load_via_partition_root, 1},
 		{"role", required_argument, NULL, 3},
@@ -1215,6 +1216,7 @@ main(int argc, char **argv)
 	ropt->noOwner = dopt.outputNoOwner;
 	ropt->noTableAm = dopt.outputNoTableAm;
 	ropt->noTablespace = dopt.outputNoTablespaces;
+	ropt->noToastType = dopt.outputNoToastType;
 	ropt->disable_triggers = dopt.disable_triggers;
 	ropt->use_setsessauth = dopt.use_setsessauth;
 	ropt->disable_dollar_quoting = dopt.disable_dollar_quoting;
@@ -1337,6 +1339,7 @@ help(const char *progname)
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
 	printf(_("  --no-toast-compression       do not dump TOAST compression methods\n"));
+	printf(_("  --no-toast-type              do not dump TOAST table type\n"));
 	printf(_("  --no-unlogged-table-data     do not dump unlogged table data\n"));
 	printf(_("  --on-conflict-do-nothing     add ON CONFLICT DO NOTHING to INSERT commands\n"));
 	printf(_("  --quote-all-identifiers      quote all identifiers, even if not key words\n"));
@@ -7099,6 +7102,7 @@ getTables(Archive *fout, int *numTables)
 	int			i_relfrozenxid;
 	int			i_toastfrozenxid;
 	int			i_toastoid;
+	int			i_toasttype;
 	int			i_relminmxid;
 	int			i_toastminmxid;
 	int			i_reloptions;
@@ -7153,6 +7157,14 @@ getTables(Archive *fout, int *numTables)
 						 "ELSE 0 END AS foreignserver, "
 						 "c.relfrozenxid, tc.relfrozenxid AS tfrozenxid, "
 						 "tc.oid AS toid, "
+						 "CASE WHEN c.reltoastrelid <> 0 THEN "
+						 " (SELECT CASE "
+						 "   WHEN a.atttypid::regtype = 'oid'::regtype THEN 'oid'::text "
+						 "   WHEN a.atttypid::regtype = 'bigint'::regtype THEN 'int8'::text "
+						 "   ELSE NULL END"
+						 "  FROM pg_attribute AS a "
+						 "  WHERE a.attrelid = tc.oid AND a.attname = 'chunk_id') "
+						 " ELSE NULL END AS toasttype, "
 						 "tc.relpages AS toastpages, "
 						 "tc.reloptions AS toast_reloptions, "
 						 "d.refobjid AS owning_tab, "
@@ -7323,6 +7335,7 @@ getTables(Archive *fout, int *numTables)
 	i_relfrozenxid = PQfnumber(res, "relfrozenxid");
 	i_toastfrozenxid = PQfnumber(res, "tfrozenxid");
 	i_toastoid = PQfnumber(res, "toid");
+	i_toasttype = PQfnumber(res, "toasttype");
 	i_relminmxid = PQfnumber(res, "relminmxid");
 	i_toastminmxid = PQfnumber(res, "tminmxid");
 	i_reloptions = PQfnumber(res, "reloptions");
@@ -7401,6 +7414,10 @@ getTables(Archive *fout, int *numTables)
 		tblinfo[i].frozenxid = atooid(PQgetvalue(res, i, i_relfrozenxid));
 		tblinfo[i].toast_frozenxid = atooid(PQgetvalue(res, i, i_toastfrozenxid));
 		tblinfo[i].toast_oid = atooid(PQgetvalue(res, i, i_toastoid));
+		if (PQgetisnull(res, i, i_toasttype))
+			tblinfo[i].toast_type = NULL;
+		else
+			tblinfo[i].toast_type = pg_strdup(PQgetvalue(res, i, i_toasttype));
 		tblinfo[i].minmxid = atooid(PQgetvalue(res, i, i_relminmxid));
 		tblinfo[i].toast_minmxid = atooid(PQgetvalue(res, i, i_toastminmxid));
 		tblinfo[i].reloptions = pg_strdup(PQgetvalue(res, i, i_reloptions));
@@ -17862,6 +17879,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 	{
 		char	   *tablespace = NULL;
 		char	   *tableam = NULL;
+		char	   *toasttype = NULL;
 
 		/*
 		 * _selectTablespace() relies on tablespace-enabled objects in the
@@ -17876,12 +17894,15 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 		if (RELKIND_HAS_TABLE_AM(tbinfo->relkind) ||
 			tbinfo->relkind == RELKIND_PARTITIONED_TABLE)
 			tableam = tbinfo->amname;
+		if (OidIsValid(tbinfo->toast_oid))
+			toasttype = tbinfo->toast_type;
 
 		ArchiveEntry(fout, tbinfo->dobj.catId, tbinfo->dobj.dumpId,
 					 ARCHIVE_OPTS(.tag = tbinfo->dobj.name,
 								  .namespace = tbinfo->dobj.namespace->dobj.name,
 								  .tablespace = tablespace,
 								  .tableam = tableam,
+								  .toasttype = toasttype,
 								  .relkind = tbinfo->relkind,
 								  .owner = tbinfo->rolname,
 								  .description = reltypename,
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index dde85ed156cc..26ad5425bd4c 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -325,6 +325,7 @@ typedef struct _tableInfo
 	uint32		frozenxid;		/* table's relfrozenxid */
 	uint32		minmxid;		/* table's relminmxid */
 	Oid			toast_oid;		/* toast table's OID, or 0 if none */
+	char	   *toast_type;		/* toast table type, or NULL if none */
 	uint32		toast_frozenxid;	/* toast table's relfrozenxid, if any */
 	uint32		toast_minmxid;	/* toast table's relminmxid */
 	int			ncheck;			/* # of CHECK expressions */
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 27aa1b656989..20834c3094d3 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -93,6 +93,7 @@ static int	if_exists = 0;
 static int	inserts = 0;
 static int	no_table_access_method = 0;
 static int	no_tablespaces = 0;
+static int	no_toast_type = 0;
 static int	use_setsessauth = 0;
 static int	no_comments = 0;
 static int	no_policies = 0;
@@ -162,6 +163,7 @@ main(int argc, char *argv[])
 		{"lock-wait-timeout", required_argument, NULL, 2},
 		{"no-table-access-method", no_argument, &no_table_access_method, 1},
 		{"no-tablespaces", no_argument, &no_tablespaces, 1},
+		{"no-toast-type", no_argument, &no_tablespaces, 1},
 		{"quote-all-identifiers", no_argument, &quote_all_identifiers, 1},
 		{"load-via-partition-root", no_argument, &load_via_partition_root, 1},
 		{"role", required_argument, NULL, 3},
@@ -445,6 +447,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --no-table-access-method");
 	if (no_tablespaces)
 		appendPQExpBufferStr(pgdumpopts, " --no-tablespaces");
+	if (no_toast_type)
+		appendPQExpBufferStr(pgdumpopts, " --no-toast-type");
 	if (quote_all_identifiers)
 		appendPQExpBufferStr(pgdumpopts, " --quote-all-identifiers");
 	if (load_via_partition_root)
@@ -699,6 +703,7 @@ help(void)
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
 	printf(_("  --no-toast-compression       do not dump TOAST compression methods\n"));
+	printf(_("  --no-toast-type              do not dump TOAST table types\n"));
 	printf(_("  --no-unlogged-table-data     do not dump unlogged table data\n"));
 	printf(_("  --on-conflict-do-nothing     add ON CONFLICT DO NOTHING to INSERT commands\n"));
 	printf(_("  --quote-all-identifiers      quote all identifiers, even if not key words\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index 6c129278bc52..df1c6e9eee88 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -71,6 +71,7 @@ main(int argc, char **argv)
 	static int	no_data_for_failed_tables = 0;
 	static int	outputNoTableAm = 0;
 	static int	outputNoTablespaces = 0;
+	static int	outputNoToastType = 0;
 	static int	use_setsessauth = 0;
 	static int	no_comments = 0;
 	static int	no_data = 0;
@@ -124,6 +125,7 @@ main(int argc, char **argv)
 		{"no-data-for-failed-tables", no_argument, &no_data_for_failed_tables, 1},
 		{"no-table-access-method", no_argument, &outputNoTableAm, 1},
 		{"no-tablespaces", no_argument, &outputNoTablespaces, 1},
+		{"no-toast-type", no_argument, &outputNoToastType, 1},
 		{"role", required_argument, NULL, 2},
 		{"section", required_argument, NULL, 3},
 		{"strict-names", no_argument, &strict_names, 1},
@@ -415,6 +417,7 @@ main(int argc, char **argv)
 	opts->noDataForFailedTables = no_data_for_failed_tables;
 	opts->noTableAm = outputNoTableAm;
 	opts->noTablespace = outputNoTablespaces;
+	opts->noToastType = outputNoToastType;
 	opts->use_setsessauth = use_setsessauth;
 	opts->no_comments = no_comments;
 	opts->no_policies = no_policies;
@@ -546,6 +549,7 @@ usage(const char *progname)
 	printf(_("  --no-subscriptions           do not restore subscriptions\n"));
 	printf(_("  --no-table-access-method     do not restore table access methods\n"));
 	printf(_("  --no-tablespaces             do not restore tablespace assignments\n"));
+	printf(_("  --no-toast-type              do not restore TOAST table types\n"));
 	printf(_("  --section=SECTION            restore named section (pre-data, data, or post-data)\n"));
 	printf(_("  --statistics                 restore the statistics\n"));
 	printf(_("  --statistics-only            restore only the statistics, not schema or data\n"));
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index a86b38466de1..3c5d7c959356 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -659,6 +659,15 @@ my %pgdump_runs = (
 			'postgres',
 		],
 	},
+	no_toast_type => {
+		dump_cmd => [
+			'pg_dump', '--no-sync',
+			'--file' => "$tempdir/no_toast_type.sql",
+			'--no-toast-type',
+			'--statistics',
+			'postgres',
+		],
+	},
 	only_dump_test_schema => {
 		dump_cmd => [
 			'pg_dump', '--no-sync',
@@ -874,6 +883,7 @@ my %full_runs = (
 	no_privs => 1,
 	no_statistics => 1,
 	no_table_access_method => 1,
+	no_toast_type => 1,
 	pg_dumpall_dbprivs => 1,
 	pg_dumpall_exclude => 1,
 	schema_only => 1,
@@ -4934,6 +4944,31 @@ my %tests = (
 		},
 	},
 
+	# Test the case of multiple TOAST table types.
+	'CREATE TABLE regress_toast_type' => {
+		create_order => 13,
+		create_sql => '
+			SET default_toast_type = int8;
+			CREATE TABLE dump_test.regress_toast_type_int8 (col1 text);
+			SET default_toast_type = oid;
+			CREATE TABLE dump_test.regress_toast_type_oid (col1 text);
+			RESET default_toast_type;',
+		regexp => qr/^
+			\QSET default_toast_type = int8;\E
+			(\n(?!SET[^;]+;)[^\n]*)*
+			\n\QCREATE TABLE dump_test.regress_toast_type_int8 (\E
+			\n\s+\Qcol1 text\E
+			\n\);/xm,
+		like => {
+			%full_runs, %dump_test_schema_runs, section_pre_data => 1,
+		},
+		unlike => {
+			exclude_dump_test_schema => 1,
+			no_toast_type => 1,
+			only_dump_measurement => 1,
+		},
+	},
+
 	#
 	# TABLE and MATVIEW stats will end up in SECTION_DATA.
 	# INDEX stats (expression columns only) will end up in SECTION_POST_DATA.
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index 0bc7609bdf81..c78d0b33c878 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -1208,6 +1208,18 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-toast-type</option></term>
+      <listitem>
+       <para>
+        Do not output commands to set <acronym>TOAST</acronym> table
+        types.
+        With this option, all <acronym>TOAST</acronym> tables will be
+        restored with the default type.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-unlogged-table-data</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_dumpall.sgml b/doc/src/sgml/ref/pg_dumpall.sgml
index 364442f00f28..2ae4c503a8af 100644
--- a/doc/src/sgml/ref/pg_dumpall.sgml
+++ b/doc/src/sgml/ref/pg_dumpall.sgml
@@ -550,6 +550,18 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-toast-type</option></term>
+      <listitem>
+       <para>
+        Do not output commands to set <acronym>TOAST</acronym> table
+        types.
+        With this option, all <acronym>TOAST</acronym> tables will be
+        restored with the default type.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-unlogged-table-data</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml
index 261ead150395..c85e7e015df0 100644
--- a/doc/src/sgml/ref/pg_restore.sgml
+++ b/doc/src/sgml/ref/pg_restore.sgml
@@ -796,6 +796,18 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-toast-type</option></term>
+      <listitem>
+       <para>
+        Do not output commands to select <acronym>TOAST</acronym> table
+        types.
+        With this option, all <acronym>TOAST</acronym> tables will be
+        created with whichever type is the default during restore.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
        <term><option>--section=<replaceable class="parameter">sectionname</replaceable></option></term>
        <listitem>
-- 
2.50.0

v4-0014-Add-new-vartag_external-for-8-byte-TOAST-values.patchtext/x-diff; charset=us-asciiDownload
From b1be9990e57bc2e2c33ad7feb076a6d0cf521625 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 8 Aug 2025 16:19:33 +0900
Subject: [PATCH v4 14/15] Add new vartag_external for 8-byte TOAST values

This is a new type of external TOAST pointer, able to be fed 8-byte
TOAST values.  It uses a dedicated vartag_external, which is used when
a TOAST table uses bigint for its chunk_id.

The relevant callbacks are added to toast_external.c.
---
 src/include/access/heaptoast.h                |   8 +-
 src/include/varatt.h                          |  34 +++-
 src/backend/access/common/toast_external.c    | 182 ++++++++++++++++--
 src/backend/access/common/toast_internals.c   |   1 -
 src/backend/access/heap/heaptoast.c           |   2 +
 .../replication/logical/reorderbuffer.c       |  10 +-
 doc/src/sgml/storage.sgml                     |   6 +-
 contrib/amcheck/verify_heapam.c               |   2 +-
 8 files changed, 225 insertions(+), 20 deletions(-)

diff --git a/src/include/access/heaptoast.h b/src/include/access/heaptoast.h
index 673e96f5488c..a39ad79a5ae9 100644
--- a/src/include/access/heaptoast.h
+++ b/src/include/access/heaptoast.h
@@ -81,6 +81,12 @@
 
 #define EXTERN_TUPLE_MAX_SIZE	MaximumBytesPerTuple(EXTERN_TUPLES_PER_PAGE)
 
+#define TOAST_MAX_CHUNK_SIZE_INT8	\
+	(EXTERN_TUPLE_MAX_SIZE -							\
+	 MAXALIGN(SizeofHeapTupleHeader) -					\
+	 (sizeof(uint32) * 2) -								\
+	 sizeof(int32) -									\
+	 VARHDRSZ)
 #define TOAST_MAX_CHUNK_SIZE_OID	\
 	(EXTERN_TUPLE_MAX_SIZE -							\
 	 MAXALIGN(SizeofHeapTupleHeader) -					\
@@ -89,7 +95,7 @@
 	 VARHDRSZ)
 
 /* Maximum size of chunk possible */
-#define TOAST_MAX_CHUNK_SIZE	TOAST_MAX_CHUNK_SIZE_OID
+#define TOAST_MAX_CHUNK_SIZE	Max(TOAST_MAX_CHUNK_SIZE_INT8, TOAST_MAX_CHUNK_SIZE_OID)
 
 /* ----------
  * heap_toast_insert_or_update -
diff --git a/src/include/varatt.h b/src/include/varatt.h
index 631aa2ecc494..a4b85031f06c 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -41,6 +41,27 @@ typedef struct varatt_external_oid
 	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
 }			varatt_external_oid;
 
+/*
+ * struct varatt_external_int8 is a "larger" version of "TOAST pointer",
+ * that uses an 8-byte integer as value.
+ *
+ * This follows the same properties as varatt_external_oid, except that
+ * this is used in TOAST relations with int8 as attribute for chunk_id.
+ */
+typedef struct varatt_external_int8
+{
+	int32		va_rawsize;		/* Original data size (includes header) */
+	uint32		va_extinfo;		/* External saved size (without header) and
+								 * compression method */
+	/*
+	 * Unique ID of value within TOAST table, as two uint32 for alignment
+	 * and padding.
+	 */
+	uint32		va_valueid_lo;
+	uint32		va_valueid_hi;
+	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
+}			varatt_external_int8;
+
 /*
  * These macros define the "saved size" portion of va_extinfo.  Its remaining
  * two high-order bits identify the compression method.
@@ -90,6 +111,7 @@ typedef enum vartag_external
 	VARTAG_INDIRECT = 1,
 	VARTAG_EXPANDED_RO = 2,
 	VARTAG_EXPANDED_RW = 3,
+	VARTAG_ONDISK_INT8 = 4,
 	VARTAG_ONDISK_OID = 18
 } vartag_external;
 
@@ -111,6 +133,8 @@ VARTAG_SIZE(vartag_external tag)
 		return sizeof(varatt_expanded);
 	else if (tag == VARTAG_ONDISK_OID)
 		return sizeof(varatt_external_oid);
+	else if (tag == VARTAG_ONDISK_INT8)
+		return sizeof(varatt_external_int8);
 	else
 	{
 		Assert(false);
@@ -367,11 +391,19 @@ VARATT_IS_EXTERNAL_ONDISK_OID(const void *PTR)
 	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID;
 }
 
+/* Is varlena datum a pointer to on-disk toasted data with int8 value? */
+static inline bool
+VARATT_IS_EXTERNAL_ONDISK_INT8(const void *PTR)
+{
+	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_INT8;
+}
+
 /* Is varlena datum a pointer to on-disk toasted data? */
 static inline bool
 VARATT_IS_EXTERNAL_ONDISK(const void *PTR)
 {
-	return VARATT_IS_EXTERNAL_ONDISK_OID(PTR);
+	return VARATT_IS_EXTERNAL_ONDISK_OID(PTR) ||
+		VARATT_IS_EXTERNAL_ONDISK_INT8(PTR);
 }
 
 /* Is varlena datum an indirect pointer? */
diff --git a/src/backend/access/common/toast_external.c b/src/backend/access/common/toast_external.c
index 5c36e5a11392..f02a707d7671 100644
--- a/src/backend/access/common/toast_external.c
+++ b/src/backend/access/common/toast_external.c
@@ -14,9 +14,24 @@
 #include "postgres.h"
 
 #include "access/detoast.h"
+#include "access/genam.h"
 #include "access/heaptoast.h"
+#include "access/toast_counter.h"
 #include "access/toast_external.h"
+#include "access/toast_type.h"
 #include "catalog/catalog.h"
+#include "miscadmin.h"
+#include "utils/fmgroids.h"
+#include "utils/snapmgr.h"
+#include "utils/lsyscache.h"
+
+
+/* Callbacks for VARTAG_ONDISK_INT8 */
+static void ondisk_int8_to_external_data(struct varlena *attr,
+										 toast_external_data *data);
+static struct varlena *ondisk_int8_create_external_data(toast_external_data data);
+static uint64 ondisk_int8_get_new_value(Relation toastrel, Oid indexid,
+										AttrNumber attnum);
 
 /* Callbacks for VARTAG_ONDISK_OID */
 static void ondisk_oid_to_external_data(struct varlena *attr,
@@ -27,7 +42,7 @@ static uint64 ondisk_oid_get_new_value(Relation toastrel, Oid indexid,
 
 /*
  * Decompressed size of an on-disk varlena; but note argument is a struct
- * varatt_external_oid.
+ * varatt_external_oid or varatt_external_int8.
  */
 static inline Size
 varatt_external_oid_get_extsize(struct varatt_external_oid toast_pointer)
@@ -35,9 +50,15 @@ varatt_external_oid_get_extsize(struct varatt_external_oid toast_pointer)
 	return toast_pointer.va_extinfo & VARLENA_EXTSIZE_MASK;
 }
 
+static inline Size
+varatt_external_int8_get_extsize(struct varatt_external_int8 toast_pointer)
+{
+	return toast_pointer.va_extinfo & VARLENA_EXTSIZE_MASK;
+}
+
 /*
  * Compression method of an on-disk varlena; but note argument is a struct
- *  varatt_external_oid.
+ *  varatt_external_oid or varatt_external_int8.
  */
 static inline uint32
 varatt_external_oid_get_compress_method(struct varatt_external_oid toast_pointer)
@@ -45,6 +66,12 @@ varatt_external_oid_get_compress_method(struct varatt_external_oid toast_pointer
 	return toast_pointer.va_extinfo >> VARLENA_EXTSIZE_BITS;
 }
 
+static inline uint32
+varatt_external_int8_get_compress_method(struct varatt_external_int8 toast_pointer)
+{
+	return toast_pointer.va_extinfo >> VARLENA_EXTSIZE_BITS;
+}
+
 /*
  * Testing whether an externally-stored TOAST value is compressed now requires
  * comparing size stored in va_extinfo (the actual length of the external data)
@@ -59,6 +86,19 @@ varatt_external_oid_is_compressed(struct varatt_external_oid toast_pointer)
 		(Size) (toast_pointer.va_rawsize - VARHDRSZ);
 }
 
+static inline bool
+varatt_external_int8_is_compressed(struct varatt_external_int8 toast_pointer)
+{
+	return varatt_external_int8_get_extsize(toast_pointer) <
+		(Size) (toast_pointer.va_rawsize - VARHDRSZ);
+}
+
+/*
+ * Size of an EXTERNAL datum that contains a standard TOAST pointer
+ * (int8 value).
+ */
+#define TOAST_POINTER_INT8_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_int8))
+
 /*
  * Size of an EXTERNAL datum that contains a standard TOAST pointer (OID
  * value).
@@ -79,6 +119,13 @@ varatt_external_oid_is_compressed(struct varatt_external_oid toast_pointer)
  * individual fields.
  */
 static const toast_external_info toast_external_infos[TOAST_EXTERNAL_INFO_SIZE] = {
+	[VARTAG_ONDISK_INT8] = {
+		.toast_pointer_size = TOAST_POINTER_INT8_SIZE,
+		.maximum_chunk_size = TOAST_MAX_CHUNK_SIZE_INT8,
+		.to_external_data = ondisk_int8_to_external_data,
+		.create_external_data = ondisk_int8_create_external_data,
+		.get_new_value = ondisk_int8_get_new_value,
+	},
 	[VARTAG_ONDISK_OID] = {
 		.toast_pointer_size = TOAST_POINTER_OID_SIZE,
 		.maximum_chunk_size = TOAST_MAX_CHUNK_SIZE_OID,
@@ -117,22 +164,31 @@ toast_external_info_get_pointer_size(uint8 tag)
 uint8
 toast_external_assign_vartag(Oid toastrelid, uint64 value)
 {
-	/*
-	 * If dealing with a code path where a TOAST relation may not be assigned,
-	 * like heap_toast_insert_or_update(), just use the legacy
-	 * vartag_external.
-	 */
-	if (!OidIsValid(toastrelid))
-		return VARTAG_ONDISK_OID;
+	Oid		toast_typid;
 
 	/*
-	 * Currently there is only one type of vartag_external supported: 4-byte
-	 * value with OID for the chunk_id type.
+	 * If dealing with a code path where a TOAST relation may not be assigned
+	 * like heap_toast_insert_or_update(), just use the vartag_external that
+	 * can be guessed based on the GUC default_toast_type.
 	 *
-	 * Note: This routine will be extended to be able to use multiple
-	 * vartag_external within a single TOAST relation type, that may change
-	 * depending on the value used.
+	 * In bootstrap mode, we should not do any kind of syscache lookups,
+	 * so do the same and rely on the value of default_toast_type.
 	 */
+	if (!OidIsValid(toastrelid) || IsBootstrapProcessingMode())
+	{
+		if (default_toast_type == TOAST_TYPE_INT8)
+			return VARTAG_ONDISK_INT8;
+		return VARTAG_ONDISK_OID;
+	}
+
+	/*
+	 * Two types of vartag_external are currently supported: OID and int8,
+	 * which depend on the type assigned to "chunk_id" for the TOAST table.
+	 */
+	toast_typid = get_atttype(toastrelid, 1);
+	if (toast_typid == INT8OID)
+		return VARTAG_ONDISK_INT8;
+
 	return VARTAG_ONDISK_OID;
 }
 
@@ -141,6 +197,104 @@ toast_external_assign_vartag(Oid toastrelid, uint64 value)
  * the in-memory representation toast_external_data used in the backend.
  */
 
+/* Callbacks for VARTAG_ONDISK_INT8 */
+static void
+ondisk_int8_to_external_data(struct varlena *attr, toast_external_data *data)
+{
+	varatt_external_int8	external;
+
+	VARATT_EXTERNAL_GET_POINTER(external, attr);
+	data->rawsize = external.va_rawsize;
+
+	/* External size and compression methods are stored in the same field */
+	if (varatt_external_int8_is_compressed(external))
+	{
+		data->extsize = varatt_external_int8_get_extsize(external);
+		data->compression_method = varatt_external_int8_get_compress_method(external);
+	}
+	else
+	{
+		data->extsize = external.va_extinfo;
+		data->compression_method = TOAST_INVALID_COMPRESSION_ID;
+	}
+
+	data->value = (((uint64) external.va_valueid_hi) << 32) |
+		external.va_valueid_lo;
+	data->toastrelid = external.va_toastrelid;
+
+}
+
+static struct varlena *
+ondisk_int8_create_external_data(toast_external_data data)
+{
+	struct varlena *result = NULL;
+	varatt_external_int8 external;
+
+	external.va_rawsize = data.rawsize;
+
+	if (data.compression_method != TOAST_INVALID_COMPRESSION_ID)
+	{
+		/* Set size and compression method, in a single field. */
+		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(external,
+													 data.extsize,
+													 data.compression_method);
+	}
+	else
+		external.va_extinfo = data.extsize;
+
+	external.va_toastrelid = data.toastrelid;
+	external.va_valueid_hi = (((uint64) data.value) >> 32);
+	external.va_valueid_lo = (uint32) data.value;
+
+	result = (struct varlena *) palloc(TOAST_POINTER_INT8_SIZE);
+	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_INT8);
+	memcpy(VARDATA_EXTERNAL(result), &external, sizeof(external));
+
+	return result;
+}
+
+static uint64
+ondisk_int8_get_new_value(Relation toastrel, Oid indexid,
+						  AttrNumber attnum)
+{
+	uint64		new_value;
+	SysScanDesc	scan;
+	ScanKeyData	key;
+	bool		collides = false;
+
+retry:
+	new_value = GetNewToastId();
+
+	/* No indexes in bootstrap mode, so leave */
+	if (IsBootstrapProcessingMode())
+		return new_value;
+
+	Assert(IsSystemRelation(toastrel));
+
+	CHECK_FOR_INTERRUPTS();
+
+	/*
+	 * Check if the new value picked already exists in the toast relation.
+	 * If there is a conflict, retry.
+	 */
+	ScanKeyInit(&key,
+				attnum,
+				BTEqualStrategyNumber, F_INT8EQ,
+				Int64GetDatum(new_value));
+
+	/* see notes in GetNewOidWithIndex() above about using SnapshotAny */
+	scan = systable_beginscan(toastrel, indexid, true,
+							  SnapshotAny, 1, &key);
+	collides = HeapTupleIsValid(systable_getnext(scan));
+	systable_endscan(scan);
+
+	if (collides)
+		goto retry;
+
+	return new_value;
+}
+
+
 /* Callbacks for VARTAG_ONDISK_OID */
 static void
 ondisk_oid_to_external_data(struct varlena *attr, toast_external_data *data)
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 98a240242127..ee7ef99181b3 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -18,7 +18,6 @@
 #include "access/heapam.h"
 #include "access/heaptoast.h"
 #include "access/table.h"
-#include "access/toast_counter.h"
 #include "access/toast_external.h"
 #include "access/toast_internals.h"
 #include "access/xact.h"
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index a01c1d627c6f..e3ace3acc9b9 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -31,7 +31,9 @@
 #include "access/toast_external.h"
 #include "access/toast_helper.h"
 #include "access/toast_internals.h"
+#include "access/toast_type.h"
 #include "utils/fmgroids.h"
+#include "utils/syscache.h"
 
 
 /* ----------
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index be33e7de6f8d..6c6526ab9c74 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -4971,14 +4971,22 @@ ReorderBufferToastAppendChunk(ReorderBuffer *rb, ReorderBufferTXN *txn,
 	TupleDesc	desc = RelationGetDescr(relation);
 	uint64		chunk_id;
 	int32		chunk_seq;
+	Oid			toast_typid;
 
 	if (txn->toast_hash == NULL)
 		ReorderBufferToastInitHash(rb, txn);
+	toast_typid = TupleDescAttr(desc, 0)->atttypid;
 
 	Assert(IsToastRelation(relation));
 
 	newtup = change->data.tp.newtuple;
-	chunk_id = DatumGetObjectId(fastgetattr(newtup, 1, desc, &isnull));
+	/* This depends on the type of TOAST value dealt with. */
+	if (toast_typid == OIDOID)
+		chunk_id = DatumGetObjectId(fastgetattr(newtup, 1, desc, &isnull));
+	else if (toast_typid == INT8OID)
+		chunk_id = DatumGetUInt64(fastgetattr(newtup, 1, desc, &isnull));
+	else
+		Assert(false);
 	Assert(!isnull);
 	chunk_seq = DatumGetInt32(fastgetattr(newtup, 2, desc, &isnull));
 	Assert(!isnull);
diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml
index 564783a1c559..29ba80e8423c 100644
--- a/doc/src/sgml/storage.sgml
+++ b/doc/src/sgml/storage.sgml
@@ -415,7 +415,11 @@ described in more detail below.
 
 <para>
 Out-of-line values are divided (after compression if used) into chunks of at
-most <symbol>TOAST_MAX_CHUNK_SIZE_OID</symbol> bytes (by default this value is chosen
+most <symbol>TOAST_MAX_CHUNK_SIZE_OID</symbol> bytes if the
+<acronym>TOAST</acronym> relation uses the <literal>oid</literal> type for
+<literal>chunk_id</literal>, or <symbol>TOAST_MAX_CHUNK_SIZE_INT8</symbol>
+bytes if the <acronym>TOAST</acronym> relation uses the <literal>int8</literal>
+type for <literal>chunk_id</literal> (by default these values are chosen
 so that four chunk rows will fit on a page, making it about 2000 bytes).
 Each chunk is stored as a separate row in the <acronym>TOAST</acronym> table
 belonging to the owning table.  Every
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 833811c75437..958b1451b4ff 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1733,7 +1733,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	{
 		uint8		va_tag = VARTAG_EXTERNAL(tp + ctx->offset);
 
-		if (va_tag != VARTAG_ONDISK_OID)
+		if (va_tag != VARTAG_ONDISK_OID && va_tag != VARTAG_ONDISK_INT8)
 		{
 			report_corruption(ctx,
 							  psprintf("toasted attribute has unexpected TOAST tag %u",
-- 
2.50.0

v4-0015-amcheck-Add-test-cases-for-8-byte-TOAST-values.patchtext/x-diff; charset=us-asciiDownload
From 93ee230abe55e39a811507847c292baed0e8fd2a Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 19 Jun 2025 13:09:11 +0900
Subject: [PATCH v4 15/15] amcheck: Add test cases for 8-byte TOAST values

This patch is a proof of concept to show what is required to change in
the tests of pg_amcheck to be able to work with the new type of external
TOAST pointer.
---
 src/bin/pg_amcheck/t/004_verify_heapam.pl | 15 +++++++++------
 1 file changed, 9 insertions(+), 6 deletions(-)

diff --git a/src/bin/pg_amcheck/t/004_verify_heapam.pl b/src/bin/pg_amcheck/t/004_verify_heapam.pl
index 72693660fb64..5f82608b5c72 100644
--- a/src/bin/pg_amcheck/t/004_verify_heapam.pl
+++ b/src/bin/pg_amcheck/t/004_verify_heapam.pl
@@ -83,8 +83,8 @@ use Test::More;
 # it is convenient enough to do it this way.  We define packing code
 # constants here, where they can be compared easily against the layout.
 
-use constant HEAPTUPLE_PACK_CODE => 'LLLSSSSSCCLLCCCCCCCCCCllLL';
-use constant HEAPTUPLE_PACK_LENGTH => 58;    # Total size
+use constant HEAPTUPLE_PACK_CODE => 'LLLSSSSSCCLLCCCCCCCCCCllLLL';
+use constant HEAPTUPLE_PACK_LENGTH => 62;    # Total size
 
 # Read a tuple of our table from a heap page.
 #
@@ -129,7 +129,8 @@ sub read_tuple
 		c_va_vartag => shift,
 		c_va_rawsize => shift,
 		c_va_extinfo => shift,
-		c_va_valueid => shift,
+		c_va_valueid_lo => shift,
+		c_va_valueid_hi => shift,
 		c_va_toastrelid => shift);
 	# Stitch together the text for column 'b'
 	$tup{b} = join('', map { chr($tup{"b_body$_"}) } (1 .. 7));
@@ -163,7 +164,8 @@ sub write_tuple
 		$tup->{b_body6}, $tup->{b_body7},
 		$tup->{c_va_header}, $tup->{c_va_vartag},
 		$tup->{c_va_rawsize}, $tup->{c_va_extinfo},
-		$tup->{c_va_valueid}, $tup->{c_va_toastrelid});
+		$tup->{c_va_valueid_lo}, $tup->{c_va_valueid_hi},
+		$tup->{c_va_toastrelid});
 	sysseek($fh, $offset, 0)
 	  or BAIL_OUT("sysseek failed: $!");
 	defined(syswrite($fh, $buffer, HEAPTUPLE_PACK_LENGTH))
@@ -184,6 +186,7 @@ my $node = PostgreSQL::Test::Cluster->new('test');
 $node->init(no_data_checksums => 1);
 $node->append_conf('postgresql.conf', 'autovacuum=off');
 $node->append_conf('postgresql.conf', 'max_prepared_transactions=10');
+$node->append_conf('postgresql.conf', 'default_toast_type=int8');
 
 # Start the node and load the extensions.  We depend on both
 # amcheck and pageinspect for this test.
@@ -496,7 +499,7 @@ for (my $tupidx = 0; $tupidx < $ROWCOUNT; $tupidx++)
 		$tup->{t_hoff} += 128;
 
 		push @expected,
-		  qr/${$header}data begins at offset 152 beyond the tuple length 58/,
+		  qr/${$header}data begins at offset 152 beyond the tuple length 62/,
 		  qr/${$header}tuple data should begin at byte 24, but actually begins at byte 152 \(3 attributes, no nulls\)/;
 	}
 	elsif ($offnum == 6)
@@ -581,7 +584,7 @@ for (my $tupidx = 0; $tupidx < $ROWCOUNT; $tupidx++)
 	elsif ($offnum == 13)
 	{
 		# Corrupt the bits in column 'c' toast pointer
-		$tup->{c_va_valueid} = 0xFFFFFFFF;
+		$tup->{c_va_valueid_lo} = 0xFFFFFFFF;
 
 		$header = header(0, $offnum, 2);
 		push @expected, qr/${header}toast value \d+ not found in toast table/;
-- 
2.50.0

#37Nikhil Kumar Veldanda
veldanda.nikhilkumar17@gmail.com
In reply to: Michael Paquier (#36)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

Hi michael,

I have a question regarding TOAST pointer handling.

As I understand, in the current design, each attribute in a HeapTuple
can have its own TOAST pointer, and TOAST pointers are possible only
for top-level attributes.

Would it make sense to maintain an array for ttc_toast_pointer_size in
ToastTupleContext, allowing us to estimate the size per attribute
based on compression or other criteria?

This approach could make the logic more generic in my opinion, but it
would require changes in toast_tuple_find_biggest_attribute and other
places.

I’d like to hear your thoughts on this.

On Fri, Aug 8, 2025 at 12:52 AM Michael Paquier <michael@paquier.xyz> wrote:

On Fri, Aug 01, 2025 at 06:03:11PM +0900, Michael Paquier wrote:

Please find attached a v3, that I have spent some time polishing to
fix the value ID problem of this thread. v2 had some conflicts, and
the CI previously failed with warning job (CI is green here now).

Attached is a v4, due to conflicts mainly caused by the recent changes
in varatt.h done by e035863c9a04. This had an interesting side
benefit when rebasing, where I have been able to isolate most of the
knowledge related to the struct varatt_external (well
varatt_external_oid in the patch set) into toast_external.c, at the
exception of VARTAG_SIZE. That's done in a separate patch, numbered
0006.

The rest of the patch set has a couple of adjustements to document
better the new API expectations for toast_external.{c,h}, comment
adjustments, some more beautification changes, some indentation
applied, etc.

As things stand, I am getting pretty happy with the patch set up to
0005 and how things are getting in shape for the interface, and I am
planning to begin applying this stuff up to 0005 in the next couple of
weeks.

As of this patch set, this means a new target of 0006, to get the
TOAST code refactored so as it is able to support more than 1 type of
external on-disk pointer with the 8-byte value problem in scope. Any
comments?
--
Michael

--
Nikhil Veldanda

#38Tom Lane
tgl@sss.pgh.pa.us
In reply to: Michael Paquier (#36)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

Michael Paquier <michael@paquier.xyz> writes:

Attached is a v4, due to conflicts mainly caused by the recent changes
in varatt.h done by e035863c9a04.

I found some time to look at the v4 patchset, and have a bunch of
comments of different sizes.

0001:

I'm good with widening all these values to 64 bits, but I wonder
if it's a great idea to use unadorned "uint64" as the data type.
That's impossible to grep for, doesn't convey anything much about
what the variables are, etc. I'm tempted to propose instead
inventing a typedef "BigOid" or some such name (bikeshedding
welcome). The elog's could be handled with, say,
#define PRIBO PRIu64
This suggestion isn't made with the idea that we'd someday switch
to an even wider type, but just with the idea of making it clearer
what these values are being used for. When you see "Oid" you
know it's some sort of object identifier, and I'm sad to give
that up here.

This hunk is flat out buggy:

@@ -1766,6 +1774,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
return true;

/* It is external, and we're looking at a page on disk */
+ toast_pointer_valueid = toast_pointer.va_valueid;

/*
* Must copy attr into toast_pointer for alignment considerations

toast_pointer isn't initialized at this point. I see you fixed that
in 0004, but it doesn't help to split the patch series if the
intermediate steps are broken.

0002:

OK, although some of the hunks in 0001 seem to belong here.

0003:

No objection to the struct renaming, but does this go far
enough? Aren't we going to need to rename TOAST_POINTER_SIZE
to TOAST_OID_POINTER_SIZE, etc, so that we can have similar
symbols for the wider version? I'm suspicious of not renaming
the functions that work on these, too. (Oh, it looks like you
did some of that in later parts.)

BTW, given that varatt.h has "typedef struct varatt_external {...}
varatt_external", I'm certain that the vast majority of uses of
"struct varatt_external" could be shortened to "varatt_external".
And I think we should do that, because using "struct foo" not "foo"
is not project style. This patch would be a fine time to do that.

0004:

Shouldn't VARATT_EXTERNAL_GET_POINTER go away entirely?
It looks to me like every use of that should be replaced by
toast_external_info_get_data().

I wonder if we shouldn't try to get rid of the phraseology "standard
TOAST pointer", and instead write something like "short TOAST pointer"
or "small TOAST pointer". These aren't really going to be more
"standard" than the wider ones, IMO.

I don't like replacing "va_valueid" with just "value". Dropping
the "id" is not an improvement, because now a reader might be
confused about whether this is somehow the actual value of the
toasted datum.

Functions in toast_external.c are under-documented. Some lack
any header comment whatever, and others have one that doesn't
actually say what they do.

I kind of wonder whether the run-time indirection this design causes
is going to be a performance problem. Perhaps not, given the expenses
involved in accessing a toasted value, but it has a faint feeling
of overdesign.

It looks like a lot of this code would just flat out dump core if
passed an invalid vartag value. Probably not good enough, given
that we might look at corrupted data.

0005:

Nice!

0006:

get_new_value is very confusingly documented. Is the indexid that of
the toastrel's index? What attribute is the attnum for? Why should
the caller need to provide either, rather than get_new_value knowing
that internally? Also, again you are using "value" for something
that is not the value of the to-be-toasted datum. I'd suggest
something more like "get_new_toast_identifier".

I kind of feel that treating this operation as a toast_external_info
method is the wrong thing, since where it looks like you are going
is to fetch an identifier and only then decide which vartag you
need to use. That ugliness comes out here:

+	/*
+	 * Retrieve the external TOAST information, with the value still unknown.
+	 * We need to do this again once we know the actual value assigned, to
+	 * define the correct vartag_external for the new TOAST tuple.
+	 */

0007:

Surely we do not need the cast in this:

+ return murmurhash64((int64) DatumGetInt64(datum));

I see you copied that from int4hashfast, but that's not good
style either.

More generally, though, why do we need catcache support for int8?
There aren't any caches on toast chunks, and I doubt we are going to
introduce any. Sure there might be a need for this down the road,
but I don't see that this patch series is the time to add it.

0008:

I totally hate the idea of introducing a GUC for this. This should be
user-transparent. Or if it isn't user-transparent, a GUC is still
the wrong thing; some kind of relation option would be more sensible.

0009:

I do not love this either. I think the right thing is just to widen
the existing nextOid counter to 64 bits, and increment it in-memory as
64 bits, and then return either all 64 bits when a 64-bit Oid is asked
for, or just the low-order 32 bits when a 32-bit OID is asked for
(with appropriate hacking for 32-bit wraparound). Having a separate
counter will make it too easy for the same value to be produced by
nearby calls to the 32-bit and 64-bit Oid generators, which is likely
a bad thing, if only because of potential confusion.

0010:

OK

0011:

Not reviewing this yet, because I disagree with the basic design.
I didn't look at the later patches either.

regards, tom lane

#39Andres Freund
andres@anarazel.de
In reply to: Tom Lane (#38)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

Hi,

On 2025-08-08 15:32:16 -0400, Tom Lane wrote:

Michael Paquier <michael@paquier.xyz> writes:

Attached is a v4, due to conflicts mainly caused by the recent changes
in varatt.h done by e035863c9a04.

I found some time to look at the v4 patchset, and have a bunch of
comments of different sizes.

0001:

I'm good with widening all these values to 64 bits, but I wonder
if it's a great idea to use unadorned "uint64" as the data type.
That's impossible to grep for, doesn't convey anything much about
what the variables are, etc. I'm tempted to propose instead
inventing a typedef "BigOid" or some such name (bikeshedding
welcome). The elog's could be handled with, say,
#define PRIBO PRIu64
This suggestion isn't made with the idea that we'd someday switch
to an even wider type, but just with the idea of making it clearer
what these values are being used for. When you see "Oid" you
know it's some sort of object identifier, and I'm sad to give
that up here.

I think we should consider introducing Oid64, instead of having a toast
specific type. I doubt this is the last place that we'll want to use 64 bit
wide types for cataloged entities.

0004:

Shouldn't VARATT_EXTERNAL_GET_POINTER go away entirely?
It looks to me like every use of that should be replaced by
toast_external_info_get_data().

I wonder if we shouldn't try to get rid of the phraseology "standard
TOAST pointer", and instead write something like "short TOAST pointer"
or "small TOAST pointer". These aren't really going to be more
"standard" than the wider ones, IMO.

I don't like replacing "va_valueid" with just "value". Dropping
the "id" is not an improvement, because now a reader might be
confused about whether this is somehow the actual value of the
toasted datum.

Functions in toast_external.c are under-documented. Some lack
any header comment whatever, and others have one that doesn't
actually say what they do.

I kind of wonder whether the run-time indirection this design causes
is going to be a performance problem. Perhaps not, given the expenses
involved in accessing a toasted value, but it has a faint feeling
of overdesign.

+1

0008:

I totally hate the idea of introducing a GUC for this. This should be
user-transparent. Or if it isn't user-transparent, a GUC is still
the wrong thing; some kind of relation option would be more sensible.

Agreed. I think we need backward compatibility for pg_upgrade purposes, but
that doesn't require a GUC, it just requires an option when creating tables in
binary upgrade mode.

0009:

I do not love this either. I think the right thing is just to widen
the existing nextOid counter to 64 bits, and increment it in-memory as
64 bits, and then return either all 64 bits when a 64-bit Oid is asked
for, or just the low-order 32 bits when a 32-bit OID is asked for
(with appropriate hacking for 32-bit wraparound). Having a separate
counter will make it too easy for the same value to be produced by
nearby calls to the 32-bit and 64-bit Oid generators, which is likely
a bad thing, if only because of potential confusion.

I'm not convinced that the global counter, be it a 32 or a 64 bit, approach
has merit in general, and I'm rather sure it's the wrong thing for toast
values. There's no straightforward path to move away from the global counter
for plain oids, but I would suggest simply not using the global oid counter
for toast IDs.

A large portion of the cases I've seen where toast ID assignments were a
problem were when the global OID counter wrapped around due to activity on
*other* tables (and/or temporary table creation). If you instead had a
per-toast-table sequence for assigning chunk IDs, that problem would largely
vanish.

With 64bit toast IDs we shouldn't need to search the index for a
non-conflicting toast IDs, there can't be wraparounds (we'd hit wraparound of
LSNs well before that and that's not practically reachable).

Greetings,

Andres Freund

#40Michael Paquier
michael@paquier.xyz
In reply to: Andres Freund (#39)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

On Fri, Aug 08, 2025 at 05:02:46PM -0400, Andres Freund wrote:

On 2025-08-08 15:32:16 -0400, Tom Lane wrote:

I found some time to look at the v4 patchset, and have a bunch of
comments of different sizes.

Thanks for the input. This patch set exists as a result of a
discussion between you and Andres back in 2022.

(Replying here regarding the points that Andres is quoting, I'll add a
second mail for some of the other things later.)

I'm good with widening all these values to 64 bits, but I wonder
if it's a great idea to use unadorned "uint64" as the data type.
That's impossible to grep for, doesn't convey anything much about
what the variables are, etc. I'm tempted to propose instead
inventing a typedef "BigOid" or some such name (bikeshedding
welcome). The elog's could be handled with, say,
#define PRIBO PRIu64
This suggestion isn't made with the idea that we'd someday switch
to an even wider type, but just with the idea of making it clearer
what these values are being used for. When you see "Oid" you
know it's some sort of object identifier, and I'm sad to give
that up here.

I think we should consider introducing Oid64, instead of having a toast
specific type. I doubt this is the last place that we'll want to use 64 bit
wide types for cataloged entities.

Works for me for the grepping argument. Using "64" as a number of
bits in the type name sounds a bit strange to me, not in line with
what's done with bigint/int8 or float, where we use bytes. Naming a
dedicated type "bigoid" with a structure named BigOid behind that's
used for TOAST or in the future for other code paths is OK by me.
AFAIK, the itchy point with unsigned 64b would be the casting, but my
take would be to introduce no casts in the first implementation,
particularly for the bigint -> bigoid case.

0004:

Shouldn't VARATT_EXTERNAL_GET_POINTER go away entirely?
It looks to me like every use of that should be replaced by
toast_external_info_get_data().

Indirect toast pointers still wanted it. But perhaps this could just
be renamed VARATT_EXTERNAL_INDIRECT_GET_POINTER() or something like
that if jt's used only for TOAST pointers?

I wonder if we shouldn't try to get rid of the phraseology "standard
TOAST pointer", and instead write something like "short TOAST pointer"
or "small TOAST pointer". These aren't really going to be more
"standard" than the wider ones, IMO.

I don't like replacing "va_valueid" with just "value". Dropping
the "id" is not an improvement, because now a reader might be
confused about whether this is somehow the actual value of the
toasted datum.

Okay, sure.

Functions in toast_external.c are under-documented. Some lack
any header comment whatever, and others have one that doesn't
actually say what they do.

Okay. Will work on that. I am not sure if it's worth doing yet, it
does not seem like there's a clear agreement about patch 0004 and the
toast_external business I am proposing.

I kind of wonder whether the run-time indirection this design causes
is going to be a performance problem. Perhaps not, given the expenses
involved in accessing a toasted value, but it has a faint feeling
of overdesign.

toast_external_info_get_data() is called in 8 places as of the patch
set:
1) Twice for amcheck.
2) Twice for reorderbuffer.
3) Three times in detoast.c
3-1) When fetching a value in full, toast_fetch_datum(), which goes
through the slice API.
3-2) toast_fetch_datum_slice(), to retrieve a slice of the toasted
data.
3-3) detoast_attr_slice(), falling back to the slice fetch.
4) Twice in toast_internals.c:
4-1) Saving a datum.
4-2) Deleting a datum.

And if you see here, upthread, I've defined the worst case as
retrieving a minimal toasted slice stored uncompressed, to measure the
cost of the conversion to this toast_external, without seeing any
effects, the btree conflict lookups doing most of the work:
/messages/by-id/aGzLiDUB_18-8aVQ@paquier.xyz

Thoughts and counter-arguments are welcome, of course.

+1

Well, one reason why this design exists is a point from 2023, made by
Robert H., around here:
/messages/by-id/CA+TgmoaVcjUkmtWdc_9QjBzvSShjDBYk-5XFNaOvYLgGROjJMA@mail.gmail.com

The argument is that it's hard to miss for the existing code and
extensions how a new vartag should be handled. Inventing a new layer
means that extensions and existing code need to do a switch once, then
they cannot really miss be missed when we add a new vartag because the
from/to indirection between the toast_external layer and the varlenas
is handled in its own place.

A second reason why I've used this design is the problem related to
compression IDs, where the idea would be to add a new vartag for
zstandard for example. This still needs more work due to the existing
limitations that we currently have with the CompressionToastId and its
limitation to four values. The idea is that the deserialization of
this data into this toast_external proposal makes the future additions
easier. It would be of course more brutal and more efficient to just
extend the code paths where the toast_external layer (I mean where
VARATT_IS_EXTERNAL_ONDISK() is used currently) with an extra branch
made of a VARATT_IS_EXTERNAL_ONDISK_BIGOID(), but I've decided to
digest the argument from Robert instead, where I'm aiming at isolating
most of the code related to on-disk external TOAST pointers into a
single file, named toast_external.c here.

I'm OK with the "brutal" method if the patch presented here is
thought as overengineered and overdesigned, just wanted to say that
there are a couple of reasons behind these choices. It's great that
both of you are able to take some time to comment on these choices.
If you disagree with this method and just say that we should go ahead
with a more direct VARATT-like method, that's also fine by me as it
would still solve the value limitation problem. And that's primarily
what I want to solve here as it's a pain for some in the field.

0008:

I totally hate the idea of introducing a GUC for this. This should be
user-transparent. Or if it isn't user-transparent, a GUC is still
the wrong thing; some kind of relation option would be more sensible.

Agreed. I think we need backward compatibility for pg_upgrade purposes, but
that doesn't require a GUC, it just requires an option when creating tables in
binary upgrade mode.

So, you basically mean that we should just make the 8-byte case the
default for all the new tables created on a new cluster at upgrade,
and just carry across upgrades the TOAST tables with OID values? If
this stuff uses a reloption or an option at DDL level, that's going to
need some dump/restore parts. Not sure that I'm betting the full
picture of what the default behavior should be. I have read on the
2022 thread where both of you have discussed this issue a point about
a "legacy" mode, to give users a way to get the old behavior if they
wanted. A GUC fit nicely around that idea, because it is them
possible to choose how all tables should behave, or perhaps I just did
not design correctly what you meant through that.

0009:

I do not love this either. I think the right thing is just to widen
the existing nextOid counter to 64 bits, and increment it in-memory as
64 bits, and then return either all 64 bits when a 64-bit Oid is asked
for, or just the low-order 32 bits when a 32-bit OID is asked for
(with appropriate hacking for 32-bit wraparound). Having a separate
counter will make it too easy for the same value to be produced by
nearby calls to the 32-bit and 64-bit Oid generators, which is likely
a bad thing, if only because of potential confusion.

I was wondering about doing that as well. FWIW, this approach works
for me, eating only 4 more bytes in the control file.

I'm not convinced that the global counter, be it a 32 or a 64 bit, approach
has merit in general, and I'm rather sure it's the wrong thing for toast
values. There's no straightforward path to move away from the global counter
for plain oids, but I would suggest simply not using the global oid counter
for toast IDs.

A large portion of the cases I've seen where toast ID assignments were a
problem were when the global OID counter wrapped around due to activity on
*other* tables (and/or temporary table creation). If you instead had a
per-toast-table sequence for assigning chunk IDs, that problem would largely
vanish.

I've thought about this piece already and the reason why I have not
taken this approach is mostly simplicity. It's mentioned upthread,
with potentially pieces referring to sequence AMs and "toast"
sequences that could be optimized for the purpose of TOAST relations:
/messages/by-id/aG2iY26tXj1_MHfH@paquier.xyz

Yep. All the requirement boxes are filled with a sequence assigned to
the TOAST relation. Using a single 8-byte counter is actually
cheaper, even if the overhead when generating the TOAST tuples are in
the btree lookups and the insertions themselves. It is also possible
to do that as a two-step process:
- First have TOAST relations with 8-byte values.
- Add local sequences later on.
What matters is having the code in place to check for index lookups
upon values conflicting when a new sequence is attached to an existing
TOAST relation. If we assign sequences from the start, that would not
matter, of course. Another reason on top of the simplicity piece
behind the control file is that it's dead cheap to acquire a new
value when saving a chunk externally.
--
Michael

#41Michael Paquier
michael@paquier.xyz
In reply to: Tom Lane (#38)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

On Fri, Aug 08, 2025 at 03:32:16PM -0400, Tom Lane wrote:

I found some time to look at the v4 patchset, and have a bunch of
comments of different sizes.

Thanks for the comments. I have replied to some of the items here:
/messages/by-id/aJbygEBqJgmLS0wF@paquier.xyz

Will try to answer the rest here.

0001:

I'm good with widening all these values to 64 bits, but I wonder
if it's a great idea to use unadorned "uint64" as the data type.
That's impossible to grep for, doesn't convey anything much about
what the variables are, etc. I'm tempted to propose instead
inventing a typedef "BigOid" or some such name (bikeshedding
welcome). The elog's could be handled with, say,
#define PRIBO PRIu64
This suggestion isn't made with the idea that we'd someday switch
to an even wider type, but just with the idea of making it clearer
what these values are being used for. When you see "Oid" you
know it's some sort of object identifier, and I'm sad to give
that up here.

Already mentioned on the other message, but I'm OK with a "bigoid"
type and a BigOid type. Honestly, I'm not sure about a replacement
for PRIu64, as we don't really do that for Oids with %u.

toast_pointer isn't initialized at this point. I see you fixed that
in 0004, but it doesn't help to split the patch series if the
intermediate steps are broken.

Oops. FWIW, I've rebased this patch set much more than 4 times. It
looks like I've messed up some of the diffs. Sorry about that.

0003:

No objection to the struct renaming, but does this go far
enough? Aren't we going to need to rename TOAST_POINTER_SIZE
to TOAST_OID_POINTER_SIZE, etc, so that we can have similar
symbols for the wider version? I'm suspicious of not renaming
the functions that work on these, too. (Oh, it looks like you
did some of that in later parts.)

Yeah. I've stuck that into the later parts where the int8 bits have
been added. Perhaps I've not been ambitious enough.

BTW, given that varatt.h has "typedef struct varatt_external {...}
varatt_external", I'm certain that the vast majority of uses of
"struct varatt_external" could be shortened to "varatt_external".
And I think we should do that, because using "struct foo" not "foo"
is not project style. This patch would be a fine time to do that.

Good point.

0004:

Shouldn't VARATT_EXTERNAL_GET_POINTER go away entirely?
It looks to me like every use of that should be replaced by
toast_external_info_get_data().

Point about indirect pointers mentioned on the other message, where we
could rename VARATT_EXTERNAL_GET_POINTER to
VARATT_EXTERNAL_INDIRECT_GET_POINTER or equivalent to limit the
confusion?

I wonder if we shouldn't try to get rid of the phraseology "standard
TOAST pointer", and instead write something like "short TOAST pointer"
or "small TOAST pointer". These aren't really going to be more
"standard" than the wider ones, IMO.

I'm seeing one place in arrayfuncs.c and one in detoast.h using this
term on HEAD. I would do simpler: no standard and no short, just with
a removal of the "standard" part if I were to change something.

I don't like replacing "va_valueid" with just "value". Dropping
the "id" is not an improvement, because now a reader might be
confused about whether this is somehow the actual value of the
toasted datum.

Okay, sure. One reason behind the field renaming was also to track
down all the areas in need to be updated.

Functions in toast_external.c are under-documented. Some lack
any header comment whatever, and others have one that doesn't
actually say what they do.

Okay.

I kind of wonder whether the run-time indirection this design causes
is going to be a performance problem. Perhaps not, given the expenses
involved in accessing a toasted value, but it has a faint feeling
of overdesign.

Mentioned on the other message, linked to this message:
/messages/by-id/aGzLiDUB_18-8aVQ@paquier.xyz

It looks like a lot of this code would just flat out dump core if
passed an invalid vartag value. Probably not good enough, given
that we might look at corrupted data.

Right. That's where we would need an error path in
toast_external_get_info() if nothing is found. That's a cheap
defense, I guess. Good thing is that the lookups are centralized in a
single code path, at least with this design.

I kind of feel that treating this operation as a toast_external_info
method is the wrong thing, since where it looks like you are going
is to fetch an identifier and only then decide which vartag you
need to use. That ugliness comes out here:

+	/*
+	 * Retrieve the external TOAST information, with the value still unknown.
+	 * We need to do this again once we know the actual value assigned, to
+	 * define the correct vartag_external for the new TOAST tuple.
+	 */

Yeah, I was feeling a bit depressed when writing the concept of what a
"default" method should be before assigning the value, but that was
still feeling right.

0007:

Surely we do not need the cast in this:

+ return murmurhash64((int64) DatumGetInt64(datum));

I see you copied that from int4hashfast, but that's not good
style either.

Okay.

More generally, though, why do we need catcache support for int8?
There aren't any caches on toast chunks, and I doubt we are going to
introduce any. Sure there might be a need for this down the road,
but I don't see that this patch series is the time to add it.

I recall that this part was required for the value conflict lookups
and also the TOAST value retrievals, so we are going to need it.

0008:

I totally hate the idea of introducing a GUC for this. This should be
user-transparent. Or if it isn't user-transparent, a GUC is still
the wrong thing; some kind of relation option would be more sensible.

Per other email, I'm not sure what you entirely mean here: should 8B
values be the default with existing TOAST OID values kept as they are
across upgrades? Or something else?

0009:

I do not love this either. I think the right thing is just to widen
the existing nextOid counter to 64 bits, and increment it in-memory as
64 bits, and then return either all 64 bits when a 64-bit Oid is asked
for, or just the low-order 32 bits when a 32-bit OID is asked for
(with appropriate hacking for 32-bit wraparound). Having a separate
counter will make it too easy for the same value to be produced by
nearby calls to the 32-bit and 64-bit Oid generators, which is likely
a bad thing, if only because of potential confusion.

Acknowledged on other message, fine my be.

0011:

Not reviewing this yet, because I disagree with the basic design.
I didn't look at the later patches either.

Without an agreement about the design choices related to the first
patches up to 0006, I doubt there this is any need to review any of
the follow-up patches yet because the choices of the first patches
influence the next patches in the series. Thanks for the feedback!
--
Michael

#42Michael Paquier
michael@paquier.xyz
In reply to: Nikhil Kumar Veldanda (#37)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

On Fri, Aug 08, 2025 at 09:09:20AM -0700, Nikhil Kumar Veldanda wrote:

I have a question regarding TOAST pointer handling.

As I understand, in the current design, each attribute in a HeapTuple
can have its own TOAST pointer, and TOAST pointers are possible only
for top-level attributes.

Would it make sense to maintain an array for ttc_toast_pointer_size in
ToastTupleContext, allowing us to estimate the size per attribute
based on compression or other criteria?

This approach could make the logic more generic in my opinion, but it
would require changes in toast_tuple_find_biggest_attribute and other
places.

I’d like to hear your thoughts on this.

Yes, that's some complexity that you would need if plugging in more
vartags if these are related to compression methods, and also
something that we may need if we use multiple vartags depending on a
value ID assigned (aka for the TOAST table with 8-byte values, short
Datum if we have a value less than 4 billion with one vartag, longer
Datum if value more than 4 billion with a second vartag).

For the 4-byte vs 8-byte value case, I was wondering if we should be
simpler and less optimistic and assume that we are only going to use
the wider one depending on the type of chunk_id in the TOAST table, as
a minimum threshold when checking if a tuple should be toasted or not.
Perhaps my vision of things is too simple, but I cannot think about a
good reason that would justify making this code more complicated than
it already is.
--
Michael

#43Nikita Malakhov
hukutoc@gmail.com
In reply to: Michael Paquier (#42)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

Hi!

Michael, I'm looking into your patch set for Sequence AMs.
I'm not very happy with single global OID counter for TOAST
tables, so I've decided to investigate your work on sequences
and try to adapt it to TOAST tables. Would report my progress.

Some time ago I've proposed individual TOAST counter
to get rid of table lookups for unused ID, but later decided
that neither reloptions nor pg_class are not a good place
to store it due to lots of related catalog updates including
locks, so sequences look much more useful for this,
as you wrote above.

Personally I'm not too happy with
toast_external_infos[TOAST_EXTERNAL_INFO_SIZE]
array and for me it seems that lookups using VARTAG
should be straightened out with more error-proof, currently
using wrong vartags would result in core dumps.

On Sat, Aug 9, 2025 at 10:40 AM Michael Paquier <michael@paquier.xyz> wrote:

For the 4-byte vs 8-byte value case, I was wondering if we should be
simpler and less optimistic and assume that we are only going to use
the wider one depending on the type of chunk_id in the TOAST table, as
a minimum threshold when checking if a tuple should be toasted or not.
Perhaps my vision of things is too simple, but I cannot think about a
good reason that would justify making this code more complicated than
it already is.
--
Michael

--
Regards,
Nikita Malakhov
Postgres Professional
The Russian Postgres Company
https://postgrespro.ru/

#44Jim Nasby
jnasby@upgrade.com
In reply to: Andres Freund (#39)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

On Fri, Aug 8, 2025 at 4:03 PM Andres Freund <andres@anarazel.de> wrote:

I'm not convinced that the global counter, be it a 32 or a 64 bit, approach
has merit in general, and I'm rather sure it's the wrong thing for toast
values. There's no straightforward path to move away from the global
counter
for plain oids, but I would suggest simply not using the global oid counter
for toast IDs.

A large portion of the cases I've seen where toast ID assignments were a
problem were when the global OID counter wrapped around due to activity on
*other* tables (and/or temporary table creation). If you instead had a
per-toast-table sequence for assigning chunk IDs, that problem would
largely
vanish.

That's been my experience as well. I was actually toying with the idea of
simply switching from OIDs to per-table counters when I came across this,
specifically to address the problem of OID wraparound induced performance
problems.

#45Michael Paquier
michael@paquier.xyz
In reply to: Jim Nasby (#44)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

On Wed, Aug 13, 2025 at 03:06:16PM -0500, Jim Nasby wrote:

On Fri, Aug 8, 2025 at 4:03 PM Andres Freund <andres@anarazel.de> wrote:

I'm not convinced that the global counter, be it a 32 or a 64 bit, approach
has merit in general, and I'm rather sure it's the wrong thing for toast
values. There's no straightforward path to move away from the global
counter
for plain oids, but I would suggest simply not using the global oid counter
for toast IDs.

A large portion of the cases I've seen where toast ID assignments were a
problem were when the global OID counter wrapped around due to activity on
*other* tables (and/or temporary table creation). If you instead had a
per-toast-table sequence for assigning chunk IDs, that problem would
largely
vanish.

That's been my experience as well. I was actually toying with the idea of
simply switching from OIDs to per-table counters when I came across this,
specifically to address the problem of OID wraparound induced performance
problems.

Yep, that exists. My original use case is not this one, where I have
a class of users able to reach the OID limit on a single table even
within the 32TB limit, meaning that TOAST blobs are large enough to
reach the external threshold, still small enough to have 4 billions of
them within the single-table limit.

Implementation-wise, switching to 8B would solve both things, and
it's so much cheaper to grab a value from a single source. I don't
really object to using 4B local values, but that does not really solve
the original issue that's causing the efforts I've initiated on this
thread. But yes, I'm also trying to implement things so as such an
addition like this one would be slightly easier. It just does not
have to be me doing it ;)
--
Michael

#46Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#41)
15 attachment(s)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

On Sat, Aug 09, 2025 at 04:31:05PM +0900, Michael Paquier wrote:

Without an agreement about the design choices related to the first
patches up to 0006, I doubt there this is any need to review any of
the follow-up patches yet because the choices of the first patches
influence the next patches in the series. Thanks for the feedback!

So, please find attached a rebased version set that addresses all the
feedback that has been provided, hopefully:
- 0001 is the requested data type for 64b OIDs, which is called here
oid8 for the data type and Oid8 for the internal name. I'm not
completely wedded to these names, but anyway. This comes with a
package that make this data type useful for other purposes than this
patch set, basically everything where I've wanted toys for TOAST:
hash/btree operators, min/max, a couple of casts (oid -> oid8,
integers, etc). The rest of the patch set builds upon that.
- 0002 changes a couple of APIs related to TOAST to use Oid8 (in
previous iterations this was uint64). In the last version, the patch
was mixing parts related to max_chunk_size, that should be split
cleanly now.
- 0003 is a preparatory change, reducing the footprint of
TOAST_MAX_CHUNK_SIZE, because we'd want to have something that changes
depending on the vartag we're dealing with.
- 0004 renames things around vartag_external to vartag_external_oid.
Based on the previous feedback, this is more aggressive now with the
renames, renaming a few more things like MAX_CHUNK_SIZE,
TOAST_POINTER_SIZE, etc.
- 0005 is the refactoring piece, that introduces toast_external.c. I
have included the previous feedback, cleaning up the code, adding more
documentation, adding a failure fallback if the vartag given in input
is incorrect, to serve as a corruption defense. And a few more
things.
- 0006 is a follow-up cleanup of varatt.h. It would make more sense
to merge that with 0005, but I've decided to keep that separate as it
makes the review slightly cleaner.
- 0007 is related to the previous feedback, and a follow-up cleanup
that could be merged with one of the previous steps. It changes the
remaining pieces of VARATT_EXTERNAL_GET_POINTER to be related to
indirect TOAST pointers, as it's only used there.
- 0008 is a previous piece, switching pg_column_toast_chunk_id() to
use oid8 as result instead.
- 0009 adds the catcache bits for OID8OID, required for toast values
lookups and deletions, in the upcoming patches. Same as previous.
- 0010 is a new piece, based on the previous feedback. This is an
independent piece that adds an extra step in binary upgrades to be
able to pass down the attribute type of chunk_id. Without oid8
support for the values, this does not bring much of course, but it's
less code churn in the follow-up patches.
- 0011 adds a new piece, based on the previous feedback, where
the existing nextOid is enlarged to 8 bytes, keeping compatibility for
the existing 4-byte OIDs where we don't want values within the [0,
FirstNormalObjectId] range. The next patches rely on the new API to
get 8-byte values.
- 0012 is a new piece requested: reloption able to define the
attribute type of chunk_id when a TOAST relation is initially created
for a table, replacing the previous GUC approach which is now gone.
- 0013 adds support for oid8 in TOAST relations, extending the new
reloption and the binary upgrade paths previously introduced.
- 0014 adds some tests with toast OID8 case, leaving some relations
around for pg_upgrade, covering the binary upgrade path for oid8.
- 0015 is the last patch, adding a new vartag for OID8. It would make
most sense to merge that with 0013, perhaps, the split is here to ease
reviews.

I have dropped the amcheck test patch for now, which was fun but it's
not really necessary for the "basics". I have done also more tests,
playing for example with pg_resetwal, installcheck and pg_upgrade
scenarios. I am wondering if it would be worth doing a pg_resetwal in
the node doing an installcheck on the instance to be upgraded, bumping
its next OID to be much larger than 4 billion, actually..
--
Michael

Attachments:

v5-0001-Implement-oid8-data-type.patchtext/x-diff; charset=us-asciiDownload
From 03ee1e96425f885348617f244037c1e9e9a4675b Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 11:03:17 +0900
Subject: [PATCH v5 01/15] Implement oid8 data type

This new identifier type will be used for 8-byte TOAST values, and can
be useful for other purposes, not yet defined as of writing this patch.
The following operators are added for this data type:
- Casts with integer types and OID.
- btree and hash operators
- min/max functions.
- Tests and documentation.

XXX: Requires catversion bump.
---
 src/include/c.h                           |  11 +-
 src/include/catalog/pg_aggregate.dat      |   6 +
 src/include/catalog/pg_amop.dat           |  23 +++
 src/include/catalog/pg_amproc.dat         |  12 ++
 src/include/catalog/pg_cast.dat           |  14 ++
 src/include/catalog/pg_opclass.dat        |   4 +
 src/include/catalog/pg_operator.dat       |  26 +++
 src/include/catalog/pg_opfamily.dat       |   4 +
 src/include/catalog/pg_proc.dat           |  64 +++++++
 src/include/catalog/pg_type.dat           |   5 +
 src/include/fmgr.h                        |   2 +
 src/include/postgres.h                    |  20 +++
 src/include/postgres_ext.h                |   1 -
 src/backend/access/nbtree/nbtcompare.c    |  82 +++++++++
 src/backend/bootstrap/bootstrap.c         |   2 +
 src/backend/utils/adt/Makefile            |   1 +
 src/backend/utils/adt/int8.c              |   8 +
 src/backend/utils/adt/meson.build         |   1 +
 src/backend/utils/adt/oid8.c              | 171 +++++++++++++++++++
 src/fe_utils/print.c                      |   1 +
 src/test/regress/expected/oid8.out        | 196 ++++++++++++++++++++++
 src/test/regress/expected/oid8.sql        |   0
 src/test/regress/expected/opr_sanity.out  |   7 +
 src/test/regress/expected/type_sanity.out |   1 +
 src/test/regress/parallel_schedule        |   2 +-
 src/test/regress/sql/oid8.sql             |  57 +++++++
 src/test/regress/sql/type_sanity.sql      |   1 +
 doc/src/sgml/datatype.sgml                |  11 ++
 doc/src/sgml/func/func-aggregate.sgml     |   8 +-
 29 files changed, 734 insertions(+), 7 deletions(-)
 create mode 100644 src/backend/utils/adt/oid8.c
 create mode 100644 src/test/regress/expected/oid8.out
 create mode 100644 src/test/regress/expected/oid8.sql
 create mode 100644 src/test/regress/sql/oid8.sql

diff --git a/src/include/c.h b/src/include/c.h
index 39022f8a9dd7..ea2d5d4a1640 100644
--- a/src/include/c.h
+++ b/src/include/c.h
@@ -530,6 +530,7 @@ typedef uint32 bits32;			/* >= 32 bits */
 /* snprintf format strings to use for 64-bit integers */
 #define INT64_FORMAT "%" PRId64
 #define UINT64_FORMAT "%" PRIu64
+#define OID8_FORMAT "%" PRIu64
 
 /*
  * 128-bit signed and unsigned integers
@@ -616,7 +617,7 @@ typedef double float8;
 #define FLOAT8PASSBYVAL true
 
 /*
- * Oid, RegProcedure, TransactionId, SubTransactionId, MultiXactId,
+ * Oid, Oid8, RegProcedure, TransactionId, SubTransactionId, MultiXactId,
  * CommandId
  */
 
@@ -648,6 +649,12 @@ typedef uint32 CommandId;
 #define FirstCommandId	((CommandId) 0)
 #define InvalidCommandId	(~(CommandId)0)
 
+/* 8-byte Object ID */
+typedef uint64 Oid8;
+
+#define InvalidOid8		((Oid8) 0)
+#define OID8_MAX	UINT64_MAX
+#define atooid8(x) ((Oid8) strtou64((x), NULL, 10))
 
 /* ----------------
  *		Variable-length datatypes all share the 'struct varlena' header.
@@ -754,6 +761,8 @@ typedef NameData *Name;
 
 #define OidIsValid(objectId)  ((bool) ((objectId) != InvalidOid))
 
+#define Oid8IsValid(objectId)  ((bool) ((objectId) != InvalidOid8))
+
 #define RegProcedureIsValid(p)	OidIsValid(p)
 
 
diff --git a/src/include/catalog/pg_aggregate.dat b/src/include/catalog/pg_aggregate.dat
index d6aa1f6ec478..75acf4ef96cd 100644
--- a/src/include/catalog/pg_aggregate.dat
+++ b/src/include/catalog/pg_aggregate.dat
@@ -104,6 +104,9 @@
 { aggfnoid => 'max(oid)', aggtransfn => 'oidlarger',
   aggcombinefn => 'oidlarger', aggsortop => '>(oid,oid)',
   aggtranstype => 'oid' },
+{ aggfnoid => 'max(oid8)', aggtransfn => 'oid8larger',
+  aggcombinefn => 'oid8larger', aggsortop => '>(oid8,oid8)',
+  aggtranstype => 'oid8' },
 { aggfnoid => 'max(float4)', aggtransfn => 'float4larger',
   aggcombinefn => 'float4larger', aggsortop => '>(float4,float4)',
   aggtranstype => 'float4' },
@@ -178,6 +181,9 @@
 { aggfnoid => 'min(oid)', aggtransfn => 'oidsmaller',
   aggcombinefn => 'oidsmaller', aggsortop => '<(oid,oid)',
   aggtranstype => 'oid' },
+{ aggfnoid => 'min(oid8)', aggtransfn => 'oid8smaller',
+  aggcombinefn => 'oid8smaller', aggsortop => '<(oid8,oid8)',
+  aggtranstype => 'oid8' },
 { aggfnoid => 'min(float4)', aggtransfn => 'float4smaller',
   aggcombinefn => 'float4smaller', aggsortop => '<(float4,float4)',
   aggtranstype => 'float4' },
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 2a693cfc31c6..2c3004d53611 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -180,6 +180,24 @@
 { amopfamily => 'btree/oid_ops', amoplefttype => 'oid', amoprighttype => 'oid',
   amopstrategy => '5', amopopr => '>(oid,oid)', amopmethod => 'btree' },
 
+# btree oid8_ops
+
+{ amopfamily => 'btree/oid8_ops', amoplefttype => 'oid8',
+  amoprighttype => 'oid8', amopstrategy => '1', amopopr => '<(oid8,oid8)',
+  amopmethod => 'btree' },
+{ amopfamily => 'btree/oid8_ops', amoplefttype => 'oid8',
+  amoprighttype => 'oid8', amopstrategy => '2', amopopr => '<=(oid8,oid8)',
+  amopmethod => 'btree' },
+{ amopfamily => 'btree/oid8_ops', amoplefttype => 'oid8',
+  amoprighttype => 'oid8', amopstrategy => '3', amopopr => '=(oid8,oid8)',
+  amopmethod => 'btree' },
+{ amopfamily => 'btree/oid8_ops', amoplefttype => 'oid8',
+  amoprighttype => 'oid8', amopstrategy => '4', amopopr => '>=(oid8,oid8)',
+  amopmethod => 'btree' },
+{ amopfamily => 'btree/oid8_ops', amoplefttype => 'oid8',
+  amoprighttype => 'oid8', amopstrategy => '5', amopopr => '>(oid8,oid8)',
+  amopmethod => 'btree' },
+
 # btree xid8_ops
 
 { amopfamily => 'btree/xid8_ops', amoplefttype => 'xid8',
@@ -974,6 +992,11 @@
 { amopfamily => 'hash/oid_ops', amoplefttype => 'oid', amoprighttype => 'oid',
   amopstrategy => '1', amopopr => '=(oid,oid)', amopmethod => 'hash' },
 
+# oid8_ops
+{ amopfamily => 'hash/oid8_ops', amoplefttype => 'oid8',
+  amoprighttype => 'oid8', amopstrategy => '1', amopopr => '=(oid8,oid8)',
+  amopmethod => 'hash' },
+
 # oidvector_ops
 { amopfamily => 'hash/oidvector_ops', amoplefttype => 'oidvector',
   amoprighttype => 'oidvector', amopstrategy => '1',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index e3477500baa7..d3719b3610c4 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -213,6 +213,14 @@
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' },
+{ amprocfamily => 'btree/oid8_ops', amproclefttype => 'oid8',
+  amprocrighttype => 'oid8', amprocnum => '1', amproc => 'btoid8cmp' },
+{ amprocfamily => 'btree/oid8_ops', amproclefttype => 'oid8',
+  amprocrighttype => 'oid8', amprocnum => '2', amproc => 'btoid8sortsupport' },
+{ amprocfamily => 'btree/oid8_ops', amproclefttype => 'oid8',
+  amprocrighttype => 'oid8', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/oid8_ops', amproclefttype => 'oid8',
+  amprocrighttype => 'oid8', amprocnum => '6', amproc => 'btoid8skipsupport' },
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
@@ -432,6 +440,10 @@
   amprocrighttype => 'xid8', amprocnum => '1', amproc => 'hashxid8' },
 { amprocfamily => 'hash/xid8_ops', amproclefttype => 'xid8',
   amprocrighttype => 'xid8', amprocnum => '2', amproc => 'hashxid8extended' },
+{ amprocfamily => 'hash/oid8_ops', amproclefttype => 'oid8',
+  amprocrighttype => 'oid8', amprocnum => '1', amproc => 'hashoid8' },
+{ amprocfamily => 'hash/oid8_ops', amproclefttype => 'oid8',
+  amprocrighttype => 'oid8', amprocnum => '2', amproc => 'hashoid8extended' },
 { amprocfamily => 'hash/cid_ops', amproclefttype => 'cid',
   amprocrighttype => 'cid', amprocnum => '1', amproc => 'hashcid' },
 { amprocfamily => 'hash/cid_ops', amproclefttype => 'cid',
diff --git a/src/include/catalog/pg_cast.dat b/src/include/catalog/pg_cast.dat
index fbfd669587f0..695f6b2a5e73 100644
--- a/src/include/catalog/pg_cast.dat
+++ b/src/include/catalog/pg_cast.dat
@@ -296,6 +296,20 @@
 { castsource => 'regdatabase', casttarget => 'int4', castfunc => '0',
   castcontext => 'a', castmethod => 'b' },
 
+# OID8 category: allow implicit conversion from any integral type (including
+# int8), as well as assignment coercion to int8.
+{ castsource => 'int8', casttarget => 'oid8', castfunc => '0',
+  castcontext => 'i', castmethod => 'b' },
+{ castsource => 'int2', casttarget => 'oid8', castfunc => 'int8(int2)',
+  castcontext => 'i', castmethod => 'f' },
+{ castsource => 'int4', casttarget => 'oid8', castfunc => 'int8(int4)',
+  castcontext => 'i', castmethod => 'f' },
+{ castsource => 'oid8', casttarget => 'int8', castfunc => '0',
+  castcontext => 'a', castmethod => 'b' },
+# Assignment coercion from oid to oid8.
+{ castsource => 'oid', casttarget => 'oid8', castfunc => 'oid8(oid)',
+  castcontext => 'a', castmethod => 'f' },
+
 # String category
 { castsource => 'text', casttarget => 'bpchar', castfunc => '0',
   castcontext => 'i', castmethod => 'b' },
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 4a9624802aa5..c0de88fabc49 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -177,6 +177,10 @@
   opcintype => 'xid8' },
 { opcmethod => 'btree', opcname => 'xid8_ops', opcfamily => 'btree/xid8_ops',
   opcintype => 'xid8' },
+{ opcmethod => 'hash', opcname => 'oid8_ops', opcfamily => 'hash/oid8_ops',
+  opcintype => 'oid8' },
+{ opcmethod => 'btree', opcname => 'oid8_ops', opcfamily => 'btree/oid8_ops',
+  opcintype => 'oid8' },
 { opcmethod => 'hash', opcname => 'cid_ops', opcfamily => 'hash/cid_ops',
   opcintype => 'cid' },
 { opcmethod => 'hash', opcname => 'tid_ops', opcfamily => 'hash/tid_ops',
diff --git a/src/include/catalog/pg_operator.dat b/src/include/catalog/pg_operator.dat
index 6d9dc1528d6e..87a7255490a7 100644
--- a/src/include/catalog/pg_operator.dat
+++ b/src/include/catalog/pg_operator.dat
@@ -3460,4 +3460,30 @@
   oprcode => 'multirange_after_multirange', oprrest => 'multirangesel',
   oprjoin => 'scalargtjoinsel' },
 
+{ oid => '8262', descr => 'equal',
+  oprname => '=', oprcanmerge => 't', oprcanhash => 't', oprleft => 'oid8',
+  oprright => 'oid8', oprresult => 'bool', oprcom => '=(oid8,oid8)',
+  oprnegate => '<>(oid8,oid8)', oprcode => 'oid8eq', oprrest => 'eqsel',
+  oprjoin => 'eqjoinsel' },
+{ oid => '8263', descr => 'not equal',
+  oprname => '<>', oprleft => 'oid8', oprright => 'oid8', oprresult => 'bool',
+  oprcom => '<>(oid8,oid8)', oprnegate => '=(oid8,oid8)', oprcode => 'oid8ne',
+  oprrest => 'neqsel', oprjoin => 'neqjoinsel' },
+{ oid => '8264', descr => 'less than',
+  oprname => '<', oprleft => 'oid8', oprright => 'oid8', oprresult => 'bool',
+  oprcom => '>(oid8,oid8)', oprnegate => '>=(oid8,oid8)', oprcode => 'oid8lt',
+  oprrest => 'scalarltsel', oprjoin => 'scalarltjoinsel' },
+{ oid => '8265', descr => 'greater than',
+  oprname => '>', oprleft => 'oid8', oprright => 'oid8', oprresult => 'bool',
+  oprcom => '<(oid8,oid8)', oprnegate => '<=(oid8,oid8)', oprcode => 'oid8gt',
+  oprrest => 'scalargtsel', oprjoin => 'scalargtjoinsel' },
+{ oid => '8266', descr => 'less than or equal',
+  oprname => '<=', oprleft => 'oid8', oprright => 'oid8', oprresult => 'bool',
+  oprcom => '>=(oid8,oid8)', oprnegate => '>(oid8,oid8)', oprcode => 'oid8le',
+  oprrest => 'scalarlesel', oprjoin => 'scalarlejoinsel' },
+{ oid => '8267', descr => 'greater than or equal',
+  oprname => '>=', oprleft => 'oid8', oprright => 'oid8', oprresult => 'bool',
+  oprcom => '<=(oid8,oid8)', oprnegate => '<(oid8,oid8)', oprcode => 'oid8ge',
+  oprrest => 'scalargesel', oprjoin => 'scalargejoinsel' },
+
 ]
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index f7dcb96b43ce..54472ce97dcd 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -116,6 +116,10 @@
   opfmethod => 'hash', opfname => 'xid8_ops' },
 { oid => '5067',
   opfmethod => 'btree', opfname => 'xid8_ops' },
+{ oid => '8278',
+  opfmethod => 'hash', opfname => 'oid8_ops' },
+{ oid => '8279',
+  opfmethod => 'btree', opfname => 'oid8_ops' },
 { oid => '2226',
   opfmethod => 'hash', opfname => 'cid_ops' },
 { oid => '2227',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 118d6da1ace0..b78508e83b9b 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1046,6 +1046,15 @@
 { oid => '6405', descr => 'skip support',
   proname => 'btoidskipsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btoidskipsupport' },
+{ oid => '8282', descr => 'less-equal-greater',
+  proname => 'btoid8cmp', proleakproof => 't', prorettype => 'int4',
+  proargtypes => 'oid8 oid8', prosrc => 'btoid8cmp' },
+{ oid => '8283', descr => 'sort support',
+  proname => 'btoid8sortsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoid8sortsupport' },
+{ oid => '8284', descr => 'skip support',
+  proname => 'btoid8skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoid8skipsupport' },
 { oid => '404', descr => 'less-equal-greater',
   proname => 'btoidvectorcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'oidvector oidvector', prosrc => 'btoidvectorcmp' },
@@ -12576,4 +12585,59 @@
   proargnames => '{pid,io_id,io_generation,state,operation,off,length,target,handle_data_len,raw_result,result,target_desc,f_sync,f_localmem,f_buffered}',
   prosrc => 'pg_get_aios' },
 
+# oid8 related functions
+{ oid => '8255', descr => 'convert oid to oid8',
+  proname => 'oid8', prorettype => 'oid8', proargtypes => 'oid',
+  prosrc => 'oidtooid8' },
+{ oid => '8257', descr => 'I/O',
+  proname => 'oid8in', prorettype => 'oid8', proargtypes => 'cstring',
+  prosrc => 'oid8in' },
+{ oid => '8258', descr => 'I/O',
+  proname => 'oid8out', prorettype => 'cstring', proargtypes => 'oid8',
+  prosrc => 'oid8out' },
+{ oid => '8259', descr => 'I/O',
+  proname => 'oid8recv', prorettype => 'oid8', proargtypes => 'internal',
+  prosrc => 'oid8recv' },
+{ oid => '8260', descr => 'I/O',
+  proname => 'oid8send', prorettype => 'bytea', proargtypes => 'oid8',
+  prosrc => 'oid8send' },
+# Comparators
+{ oid => '8268',
+  proname => 'oid8eq', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'oid8 oid8', prosrc => 'oid8eq' },
+{ oid => '8269',
+  proname => 'oid8ne', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'oid8 oid8', prosrc => 'oid8ne' },
+{ oid => '8270',
+  proname => 'oid8lt', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'oid8 oid8', prosrc => 'oid8lt' },
+{ oid => '8271',
+  proname => 'oid8le', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'oid8 oid8', prosrc => 'oid8le' },
+{ oid => '8272',
+  proname => 'oid8gt', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'oid8 oid8', prosrc => 'oid8gt' },
+{ oid => '8273',
+  proname => 'oid8ge', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'oid8 oid8', prosrc => 'oid8ge' },
+# Aggregates
+{ oid => '8274', descr => 'larger of two',
+  proname => 'oid8larger', prorettype => 'oid8', proargtypes => 'oid8 oid8',
+  prosrc => 'oid8larger' },
+{ oid => '8275', descr => 'smaller of two',
+  proname => 'oid8smaller', prorettype => 'oid8', proargtypes => 'oid8 oid8',
+  prosrc => 'oid8smaller' },
+{ oid => '8276', descr => 'maximum value of all oid8 input values',
+  proname => 'max', prokind => 'a', proisstrict => 'f', prorettype => 'oid8',
+  proargtypes => 'oid8', prosrc => 'aggregate_dummy' },
+{ oid => '8277', descr => 'minimum value of all oid8 input values',
+  proname => 'min', prokind => 'a', proisstrict => 'f', prorettype => 'oid8',
+  proargtypes => 'oid8', prosrc => 'aggregate_dummy' },
+{ oid => '8280', descr => 'hash',
+  proname => 'hashoid8', prorettype => 'int4', proargtypes => 'oid8',
+  prosrc => 'hashoid8' },
+{ oid => '8281', descr => 'hash',
+  proname => 'hashoid8extended', prorettype => 'int8',
+  proargtypes => 'oid8 int8', prosrc => 'hashoid8extended' },
+
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index cb730aeac864..704f2890cb28 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -700,4 +700,9 @@
   typreceive => 'brin_minmax_multi_summary_recv',
   typsend => 'brin_minmax_multi_summary_send', typalign => 'i',
   typstorage => 'x', typcollation => 'default' },
+{ oid => '8256', array_type_oid => '8261',
+  descr => 'object identifier(oid8), 8 bytes',
+  typname => 'oid8', typlen => '8', typbyval => 't',
+  typcategory => 'N', typinput => 'oid8in', typoutput => 'oid8out',
+  typreceive => 'oid8recv', typsend => 'oid8send', typalign => 'd' },
 ]
diff --git a/src/include/fmgr.h b/src/include/fmgr.h
index c7236e429724..111588f75c86 100644
--- a/src/include/fmgr.h
+++ b/src/include/fmgr.h
@@ -273,6 +273,7 @@ extern struct varlena *pg_detoast_datum_packed(struct varlena *datum);
 #define PG_GETARG_CHAR(n)	 DatumGetChar(PG_GETARG_DATUM(n))
 #define PG_GETARG_BOOL(n)	 DatumGetBool(PG_GETARG_DATUM(n))
 #define PG_GETARG_OID(n)	 DatumGetObjectId(PG_GETARG_DATUM(n))
+#define PG_GETARG_OID8(n)	 DatumGetObjectId8(PG_GETARG_DATUM(n))
 #define PG_GETARG_POINTER(n) DatumGetPointer(PG_GETARG_DATUM(n))
 #define PG_GETARG_CSTRING(n) DatumGetCString(PG_GETARG_DATUM(n))
 #define PG_GETARG_NAME(n)	 DatumGetName(PG_GETARG_DATUM(n))
@@ -358,6 +359,7 @@ extern struct varlena *pg_detoast_datum_packed(struct varlena *datum);
 #define PG_RETURN_CHAR(x)	 return CharGetDatum(x)
 #define PG_RETURN_BOOL(x)	 return BoolGetDatum(x)
 #define PG_RETURN_OID(x)	 return ObjectIdGetDatum(x)
+#define PG_RETURN_OID8(x)	 return ObjectId8GetDatum(x)
 #define PG_RETURN_POINTER(x) return PointerGetDatum(x)
 #define PG_RETURN_CSTRING(x) return CStringGetDatum(x)
 #define PG_RETURN_NAME(x)	 return NameGetDatum(x)
diff --git a/src/include/postgres.h b/src/include/postgres.h
index 357cbd6fd961..a5a0e3b7cbfa 100644
--- a/src/include/postgres.h
+++ b/src/include/postgres.h
@@ -264,6 +264,26 @@ ObjectIdGetDatum(Oid X)
 	return (Datum) X;
 }
 
+/*
+ * DatumGetObjectId8
+ *		Returns 8-byte object identifier value of a datum.
+ */
+static inline Oid8
+DatumGetObjectId8(Datum X)
+{
+	return (Oid8) X;
+}
+
+/*
+ * ObjectId8GetDatum
+ *		Returns datum representation for an 8-byte object identifier
+ */
+static inline Datum
+ObjectId8GetDatum(Oid8 X)
+{
+	return (Datum) X;
+}
+
 /*
  * DatumGetTransactionId
  *		Returns transaction identifier value of a datum.
diff --git a/src/include/postgres_ext.h b/src/include/postgres_ext.h
index bf45c50dcf31..c80b195bf235 100644
--- a/src/include/postgres_ext.h
+++ b/src/include/postgres_ext.h
@@ -41,7 +41,6 @@ typedef unsigned int Oid;
 #define atooid(x) ((Oid) strtoul((x), NULL, 10))
 /* the above needs <stdlib.h> */
 
-
 /*
  * Identifiers of error message fields.  Kept here to keep common
  * between frontend and backend, and also to export them to libpq
diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c
index 188c27b4925f..3f59ba3f1ad0 100644
--- a/src/backend/access/nbtree/nbtcompare.c
+++ b/src/backend/access/nbtree/nbtcompare.c
@@ -498,6 +498,88 @@ btoidskipsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+Datum
+btoid8cmp(PG_FUNCTION_ARGS)
+{
+	Oid8		a = PG_GETARG_OID8(0);
+	Oid8		b = PG_GETARG_OID8(1);
+
+	if (a > b)
+		PG_RETURN_INT32(A_GREATER_THAN_B);
+	else if (a == b)
+		PG_RETURN_INT32(0);
+	else
+		PG_RETURN_INT32(A_LESS_THAN_B);
+}
+
+static int
+btoid8fastcmp(Datum x, Datum y, SortSupport ssup)
+{
+	Oid8		a = DatumGetObjectId8(x);
+	Oid8		b = DatumGetObjectId8(y);
+
+	if (a > b)
+		return A_GREATER_THAN_B;
+	else if (a == b)
+		return 0;
+	else
+		return A_LESS_THAN_B;
+}
+
+Datum
+btoid8sortsupport(PG_FUNCTION_ARGS)
+{
+	SortSupport ssup = (SortSupport) PG_GETARG_POINTER(0);
+
+	ssup->comparator = btoid8fastcmp;
+	PG_RETURN_VOID();
+}
+
+static Datum
+oid8_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Oid8		oexisting = DatumGetObjectId8(existing);
+
+	if (oexisting == InvalidOid8)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return ObjectId8GetDatum(oexisting - 1);
+}
+
+static Datum
+oid8_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Oid8		oexisting = DatumGetObjectId8(existing);
+
+	if (oexisting == OID8_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return ObjectId8GetDatum(oexisting + 1);
+}
+
+Datum
+btoid8skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = oid8_decrement;
+	sksup->increment = oid8_increment;
+	sksup->low_elem = ObjectId8GetDatum(InvalidOid8);
+	sksup->high_elem = ObjectId8GetDatum(OID8_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btoidvectorcmp(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/bootstrap/bootstrap.c b/src/backend/bootstrap/bootstrap.c
index fc8638c1b61b..48e6966e6b48 100644
--- a/src/backend/bootstrap/bootstrap.c
+++ b/src/backend/bootstrap/bootstrap.c
@@ -115,6 +115,8 @@ static const struct typinfo TypInfo[] = {
 	F_TEXTIN, F_TEXTOUT},
 	{"oid", OIDOID, 0, 4, true, TYPALIGN_INT, TYPSTORAGE_PLAIN, InvalidOid,
 	F_OIDIN, F_OIDOUT},
+	{"oid8", OID8OID, 0, 8, true, TYPALIGN_DOUBLE, TYPSTORAGE_PLAIN, InvalidOid,
+	F_OID8IN, F_OID8OUT},
 	{"tid", TIDOID, 0, 6, false, TYPALIGN_SHORT, TYPSTORAGE_PLAIN, InvalidOid,
 	F_TIDIN, F_TIDOUT},
 	{"xid", XIDOID, 0, 4, true, TYPALIGN_INT, TYPSTORAGE_PLAIN, InvalidOid,
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index ffeacf2b819f..42d7e1db2433 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -76,6 +76,7 @@ OBJS = \
 	numeric.o \
 	numutils.o \
 	oid.o \
+	oid8.o \
 	oracle_compat.o \
 	orderedsetaggs.o \
 	partitionfuncs.o \
diff --git a/src/backend/utils/adt/int8.c b/src/backend/utils/adt/int8.c
index bdea490202a6..9f7466e47b79 100644
--- a/src/backend/utils/adt/int8.c
+++ b/src/backend/utils/adt/int8.c
@@ -1323,6 +1323,14 @@ oidtoi8(PG_FUNCTION_ARGS)
 	PG_RETURN_INT64((int64) arg);
 }
 
+Datum
+oidtooid8(PG_FUNCTION_ARGS)
+{
+	Oid			arg = PG_GETARG_OID(0);
+
+	PG_RETURN_OID8((Oid8) arg);
+}
+
 /*
  * non-persistent numeric series generator
  */
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index ed9bbd7b9266..74926c32321b 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -63,6 +63,7 @@ backend_sources += files(
   'numeric.c',
   'numutils.c',
   'oid.c',
+  'oid8.c',
   'oracle_compat.c',
   'orderedsetaggs.c',
   'partitionfuncs.c',
diff --git a/src/backend/utils/adt/oid8.c b/src/backend/utils/adt/oid8.c
new file mode 100644
index 000000000000..6e9ffd96303f
--- /dev/null
+++ b/src/backend/utils/adt/oid8.c
@@ -0,0 +1,171 @@
+/*-------------------------------------------------------------------------
+ *
+ * oid8.c
+ *	  Functions for the built-in type Oid8
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/oid8.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include <ctype.h>
+#include <limits.h>
+
+#include "catalog/pg_type.h"
+#include "libpq/pqformat.h"
+#include "utils/builtins.h"
+
+#define MAXOID8LEN 20
+
+/*****************************************************************************
+ *	 USER I/O ROUTINES														 *
+ *****************************************************************************/
+
+Datum
+oid8in(PG_FUNCTION_ARGS)
+{
+	char	   *s = PG_GETARG_CSTRING(0);
+	Oid8		result;
+
+	result = uint64in_subr(s, NULL, "oid8", fcinfo->context);
+	PG_RETURN_OID8(result);
+}
+
+Datum
+oid8out(PG_FUNCTION_ARGS)
+{
+	Oid8		val = PG_GETARG_OID8(0);
+	char		buf[MAXOID8LEN + 1];
+	char	   *result;
+	int			len;
+
+	len = pg_ulltoa_n(val, buf) + 1;
+	buf[len - 1] = '\0';
+
+	/*
+	 * Since the length is already known, we do a manual palloc() and memcpy()
+	 * to avoid the strlen() call that would otherwise be done in pstrdup().
+	 */
+	result = palloc(len);
+	memcpy(result, buf, len);
+	PG_RETURN_CSTRING(result);
+}
+
+/*
+ *		oid8recv			- converts external binary format to oid8
+ */
+Datum
+oid8recv(PG_FUNCTION_ARGS)
+{
+	StringInfo	buf = (StringInfo) PG_GETARG_POINTER(0);
+
+	PG_RETURN_OID8(pq_getmsgint64(buf));
+}
+
+/*
+ *		oid8send			- converts oid8 to binary format
+ */
+Datum
+oid8send(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	StringInfoData buf;
+
+	pq_begintypsend(&buf);
+	pq_sendint64(&buf, arg1);
+	PG_RETURN_BYTEA_P(pq_endtypsend(&buf));
+}
+
+/*****************************************************************************
+ *	 PUBLIC ROUTINES														 *
+ *****************************************************************************/
+
+Datum
+oid8eq(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	Oid8		arg2 = PG_GETARG_OID8(1);
+
+	PG_RETURN_BOOL(arg1 == arg2);
+}
+
+Datum
+oid8ne(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	Oid8		arg2 = PG_GETARG_OID8(1);
+
+	PG_RETURN_BOOL(arg1 != arg2);
+}
+
+Datum
+oid8lt(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	Oid8		arg2 = PG_GETARG_OID8(1);
+
+	PG_RETURN_BOOL(arg1 < arg2);
+}
+
+Datum
+oid8le(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	Oid8		arg2 = PG_GETARG_OID8(1);
+
+	PG_RETURN_BOOL(arg1 <= arg2);
+}
+
+Datum
+oid8ge(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	Oid8		arg2 = PG_GETARG_OID8(1);
+
+	PG_RETURN_BOOL(arg1 >= arg2);
+}
+
+Datum
+oid8gt(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	Oid8		arg2 = PG_GETARG_OID8(1);
+
+	PG_RETURN_BOOL(arg1 > arg2);
+}
+
+Datum
+hashoid8(PG_FUNCTION_ARGS)
+{
+	return hashint8(fcinfo);
+}
+
+Datum
+hashoid8extended(PG_FUNCTION_ARGS)
+{
+	return hashint8extended(fcinfo);
+}
+
+Datum
+oid8larger(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	Oid8		arg2 = PG_GETARG_OID8(1);
+
+	PG_RETURN_OID8((arg1 > arg2) ? arg1 : arg2);
+}
+
+Datum
+oid8smaller(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	Oid8		arg2 = PG_GETARG_OID8(1);
+
+	PG_RETURN_OID8((arg1 < arg2) ? arg1 : arg2);
+}
diff --git a/src/fe_utils/print.c b/src/fe_utils/print.c
index 4af0f32f2fc0..221624707892 100644
--- a/src/fe_utils/print.c
+++ b/src/fe_utils/print.c
@@ -3624,6 +3624,7 @@ column_type_alignment(Oid ftype)
 		case FLOAT8OID:
 		case NUMERICOID:
 		case OIDOID:
+		case OID8OID:
 		case XIDOID:
 		case XID8OID:
 		case CIDOID:
diff --git a/src/test/regress/expected/oid8.out b/src/test/regress/expected/oid8.out
new file mode 100644
index 000000000000..80529214ca53
--- /dev/null
+++ b/src/test/regress/expected/oid8.out
@@ -0,0 +1,196 @@
+--
+-- OID8
+--
+CREATE TABLE OID8_TBL(f1 oid8);
+INSERT INTO OID8_TBL(f1) VALUES ('1234');
+INSERT INTO OID8_TBL(f1) VALUES ('1235');
+INSERT INTO OID8_TBL(f1) VALUES ('987');
+INSERT INTO OID8_TBL(f1) VALUES ('-1040');
+INSERT INTO OID8_TBL(f1) VALUES ('99999999');
+INSERT INTO OID8_TBL(f1) VALUES ('5     ');
+INSERT INTO OID8_TBL(f1) VALUES ('   10  ');
+-- leading/trailing hard tab is also allowed
+INSERT INTO OID8_TBL(f1) VALUES ('	  15 	  ');
+-- bad inputs
+INSERT INTO OID8_TBL(f1) VALUES ('');
+ERROR:  invalid input syntax for type oid8: ""
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('');
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES ('    ');
+ERROR:  invalid input syntax for type oid8: "    "
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('    ');
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES ('asdfasd');
+ERROR:  invalid input syntax for type oid8: "asdfasd"
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('asdfasd');
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES ('99asdfasd');
+ERROR:  invalid input syntax for type oid8: "99asdfasd"
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('99asdfasd');
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES ('5    d');
+ERROR:  invalid input syntax for type oid8: "5    d"
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('5    d');
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES ('    5d');
+ERROR:  invalid input syntax for type oid8: "    5d"
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('    5d');
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES ('5    5');
+ERROR:  invalid input syntax for type oid8: "5    5"
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('5    5');
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES (' - 500');
+ERROR:  invalid input syntax for type oid8: " - 500"
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES (' - 500');
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES ('3908203590239580293850293850329485');
+ERROR:  value "3908203590239580293850293850329485" is out of range for type oid8
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('39082035902395802938502938...
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES ('-1204982019841029840928340329840934');
+ERROR:  value "-1204982019841029840928340329840934" is out of range for type oid8
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('-1204982019841029840928340...
+                                         ^
+SELECT * FROM OID8_TBL;
+          f1          
+----------------------
+                 1234
+                 1235
+                  987
+ 18446744073709550576
+             99999999
+                    5
+                   10
+                   15
+(8 rows)
+
+-- Also try it with non-error-throwing API
+SELECT pg_input_is_valid('1234', 'oid8');
+ pg_input_is_valid 
+-------------------
+ t
+(1 row)
+
+SELECT pg_input_is_valid('01XYZ', 'oid8');
+ pg_input_is_valid 
+-------------------
+ f
+(1 row)
+
+SELECT * FROM pg_input_error_info('01XYZ', 'oid8');
+                   message                   | detail | hint | sql_error_code 
+---------------------------------------------+--------+------+----------------
+ invalid input syntax for type oid8: "01XYZ" |        |      | 22P02
+(1 row)
+
+SELECT pg_input_is_valid('3908203590239580293850293850329485', 'oid8');
+ pg_input_is_valid 
+-------------------
+ f
+(1 row)
+
+SELECT * FROM pg_input_error_info('-1204982019841029840928340329840934', 'oid8');
+                                  message                                  | detail | hint | sql_error_code 
+---------------------------------------------------------------------------+--------+------+----------------
+ value "-1204982019841029840928340329840934" is out of range for type oid8 |        |      | 22003
+(1 row)
+
+-- Operators
+SELECT o.* FROM OID8_TBL o WHERE o.f1 = 1234;
+  f1  
+------
+ 1234
+(1 row)
+
+SELECT o.* FROM OID8_TBL o WHERE o.f1 <> '1234';
+          f1          
+----------------------
+                 1235
+                  987
+ 18446744073709550576
+             99999999
+                    5
+                   10
+                   15
+(7 rows)
+
+SELECT o.* FROM OID8_TBL o WHERE o.f1 <= '1234';
+  f1  
+------
+ 1234
+  987
+    5
+   10
+   15
+(5 rows)
+
+SELECT o.* FROM OID8_TBL o WHERE o.f1 < '1234';
+ f1  
+-----
+ 987
+   5
+  10
+  15
+(4 rows)
+
+SELECT o.* FROM OID8_TBL o WHERE o.f1 >= '1234';
+          f1          
+----------------------
+                 1234
+                 1235
+ 18446744073709550576
+             99999999
+(4 rows)
+
+SELECT o.* FROM OID8_TBL o WHERE o.f1 > '1234';
+          f1          
+----------------------
+                 1235
+ 18446744073709550576
+             99999999
+(3 rows)
+
+-- Casts
+SELECT 1::int2::oid8;
+ oid8 
+------
+    1
+(1 row)
+
+SELECT 1::int4::oid8;
+ oid8 
+------
+    1
+(1 row)
+
+SELECT 1::int8::oid8;
+ oid8 
+------
+    1
+(1 row)
+
+SELECT 1::oid8::int8;
+ int8 
+------
+    1
+(1 row)
+
+SELECT 1::oid::oid8; -- ok
+ oid8 
+------
+    1
+(1 row)
+
+SELECT 1::oid8::oid; -- not ok
+ERROR:  cannot cast type oid8 to oid
+LINE 1: SELECT 1::oid8::oid;
+                      ^
+-- Aggregates
+SELECT min(f1), max(f1) FROM OID8_TBL;
+ min |         max          
+-----+----------------------
+   5 | 18446744073709550576
+(1 row)
+
+DROP TABLE OID8_TBL;
diff --git a/src/test/regress/expected/oid8.sql b/src/test/regress/expected/oid8.sql
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 20bf9ea9cdf7..1b2a1641029d 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -880,6 +880,13 @@ bytea(integer)
 bytea(bigint)
 bytea_larger(bytea,bytea)
 bytea_smaller(bytea,bytea)
+oid8eq(oid8,oid8)
+oid8ne(oid8,oid8)
+oid8lt(oid8,oid8)
+oid8le(oid8,oid8)
+oid8gt(oid8,oid8)
+oid8ge(oid8,oid8)
+btoid8cmp(oid8,oid8)
 -- Check that functions without argument are not marked as leakproof.
 SELECT p1.oid::regprocedure
 FROM pg_proc p1 JOIN pg_namespace pn
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 943e56506bf1..9ddcacec6bf4 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -702,6 +702,7 @@ CREATE TABLE tab_core_types AS SELECT
   'abc'::refcursor,
   '1 2'::int2vector,
   '1 2'::oidvector,
+  '1234'::oid8,
   format('%I=UC/%I', USER, USER)::aclitem AS aclitem,
   'a fat cat sat on a mat and ate a fat rat'::tsvector,
   'fat & rat'::tsquery,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index fbffc67ae601..56e129ce4aa0 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -28,7 +28,7 @@ test: strings md5 numerology point lseg line box path polygon circle date time t
 # geometry depends on point, lseg, line, box, path, polygon, circle
 # horology depends on date, time, timetz, timestamp, timestamptz, interval
 # ----------
-test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import oid8
 
 # ----------
 # Load huge amounts of data
diff --git a/src/test/regress/sql/oid8.sql b/src/test/regress/sql/oid8.sql
new file mode 100644
index 000000000000..c4f2ae6a2e57
--- /dev/null
+++ b/src/test/regress/sql/oid8.sql
@@ -0,0 +1,57 @@
+--
+-- OID8
+--
+
+CREATE TABLE OID8_TBL(f1 oid8);
+
+INSERT INTO OID8_TBL(f1) VALUES ('1234');
+INSERT INTO OID8_TBL(f1) VALUES ('1235');
+INSERT INTO OID8_TBL(f1) VALUES ('987');
+INSERT INTO OID8_TBL(f1) VALUES ('-1040');
+INSERT INTO OID8_TBL(f1) VALUES ('99999999');
+INSERT INTO OID8_TBL(f1) VALUES ('5     ');
+INSERT INTO OID8_TBL(f1) VALUES ('   10  ');
+-- leading/trailing hard tab is also allowed
+INSERT INTO OID8_TBL(f1) VALUES ('	  15 	  ');
+
+-- bad inputs
+INSERT INTO OID8_TBL(f1) VALUES ('');
+INSERT INTO OID8_TBL(f1) VALUES ('    ');
+INSERT INTO OID8_TBL(f1) VALUES ('asdfasd');
+INSERT INTO OID8_TBL(f1) VALUES ('99asdfasd');
+INSERT INTO OID8_TBL(f1) VALUES ('5    d');
+INSERT INTO OID8_TBL(f1) VALUES ('    5d');
+INSERT INTO OID8_TBL(f1) VALUES ('5    5');
+INSERT INTO OID8_TBL(f1) VALUES (' - 500');
+INSERT INTO OID8_TBL(f1) VALUES ('3908203590239580293850293850329485');
+INSERT INTO OID8_TBL(f1) VALUES ('-1204982019841029840928340329840934');
+
+SELECT * FROM OID8_TBL;
+
+-- Also try it with non-error-throwing API
+SELECT pg_input_is_valid('1234', 'oid8');
+SELECT pg_input_is_valid('01XYZ', 'oid8');
+SELECT * FROM pg_input_error_info('01XYZ', 'oid8');
+SELECT pg_input_is_valid('3908203590239580293850293850329485', 'oid8');
+SELECT * FROM pg_input_error_info('-1204982019841029840928340329840934', 'oid8');
+
+-- Operators
+SELECT o.* FROM OID8_TBL o WHERE o.f1 = 1234;
+SELECT o.* FROM OID8_TBL o WHERE o.f1 <> '1234';
+SELECT o.* FROM OID8_TBL o WHERE o.f1 <= '1234';
+SELECT o.* FROM OID8_TBL o WHERE o.f1 < '1234';
+SELECT o.* FROM OID8_TBL o WHERE o.f1 >= '1234';
+SELECT o.* FROM OID8_TBL o WHERE o.f1 > '1234';
+
+-- Casts
+SELECT 1::int2::oid8;
+SELECT 1::int4::oid8;
+SELECT 1::int8::oid8;
+SELECT 1::oid8::int8;
+SELECT 1::oid::oid8; -- ok
+SELECT 1::oid8::oid; -- not ok
+
+-- Aggregates
+SELECT min(f1), max(f1) FROM OID8_TBL;
+
+DROP TABLE OID8_TBL;
diff --git a/src/test/regress/sql/type_sanity.sql b/src/test/regress/sql/type_sanity.sql
index df795759bb4c..c2496823d90e 100644
--- a/src/test/regress/sql/type_sanity.sql
+++ b/src/test/regress/sql/type_sanity.sql
@@ -530,6 +530,7 @@ CREATE TABLE tab_core_types AS SELECT
   'abc'::refcursor,
   '1 2'::int2vector,
   '1 2'::oidvector,
+  '1234'::oid8,
   format('%I=UC/%I', USER, USER)::aclitem AS aclitem,
   'a fat cat sat on a mat and ate a fat rat'::tsvector,
   'fat & rat'::tsquery,
diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml
index b81d89e26080..66c6aa7f349a 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -4723,6 +4723,10 @@ INSERT INTO mytable VALUES(-1);  -- fails
     <primary>oid</primary>
    </indexterm>
 
+   <indexterm zone="datatype-oid">
+    <primary>oid8</primary>
+   </indexterm>
+
    <indexterm zone="datatype-oid">
     <primary>regclass</primary>
    </indexterm>
@@ -4805,6 +4809,13 @@ INSERT INTO mytable VALUES(-1);  -- fails
     individual tables.
    </para>
 
+   <para>
+    In some contexts, a 64-bit variant <type>oid8</type> is used.
+    It is implemented as an unsigned eight-byte integer. Unlike its
+    <type>oid</type> counterpart, it can ensure uniqueness in large
+    individual tables.
+   </para>
+
    <para>
     The <type>oid</type> type itself has few operations beyond comparison.
     It can be cast to integer, however, and then manipulated using the
diff --git a/doc/src/sgml/func/func-aggregate.sgml b/doc/src/sgml/func/func-aggregate.sgml
index f50b692516b6..a5396048adf3 100644
--- a/doc/src/sgml/func/func-aggregate.sgml
+++ b/doc/src/sgml/func/func-aggregate.sgml
@@ -508,8 +508,8 @@
         Computes the maximum of the non-null input
         values.  Available for any numeric, string, date/time, or enum type,
         as well as <type>bytea</type>, <type>inet</type>, <type>interval</type>,
-        <type>money</type>, <type>oid</type>, <type>pg_lsn</type>,
-        <type>tid</type>, <type>xid8</type>,
+        <type>money</type>, <type>oid</type>, <type>oid8</type>,
+        <type>pg_lsn</type>, <type>tid</type>, <type>xid8</type>,
         and also arrays and composite types containing sortable data types.
        </para></entry>
        <entry>Yes</entry>
@@ -527,8 +527,8 @@
         Computes the minimum of the non-null input
         values.  Available for any numeric, string, date/time, or enum type,
         as well as <type>bytea</type>, <type>inet</type>, <type>interval</type>,
-        <type>money</type>, <type>oid</type>, <type>pg_lsn</type>,
-        <type>tid</type>, <type>xid8</type>,
+        <type>money</type>, <type>oid</type>, <type>oid8</type>,
+        <type>pg_lsn</type>, <type>tid</type>, <type>xid8</type>,
         and also arrays and composite types containing sortable data types.
        </para></entry>
        <entry>Yes</entry>
-- 
2.50.0

v5-0002-Refactor-some-TOAST-value-ID-code-to-use-Oid8-ins.patchtext/x-diff; charset=us-asciiDownload
From bd5f275e0afffa4909ce5839f9033abed448684c Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 13 Aug 2025 17:26:36 +0900
Subject: [PATCH v5 02/15] Refactor some TOAST value ID code to use Oid8
 instead of Oid

This change is a mechanical switch to change most of the code paths that
assume TOAST value IDs to be Oids to become Oid8, easing an upcoming
change to allow larger TOAST values, at 8 bytes.

The areas touched are related to table AM, amcheck and logical
decoding's reorder buffer.  A good chunk of the changes involve
switching printf() markers from %u to OID8_FORMAT.
---
 src/include/access/heaptoast.h                |  2 +-
 src/include/access/tableam.h                  |  4 +-
 src/backend/access/common/toast_internals.c   |  8 +--
 src/backend/access/heap/heaptoast.c           | 12 ++--
 .../replication/logical/reorderbuffer.c       | 14 +++--
 contrib/amcheck/verify_heapam.c               | 56 +++++++++++--------
 6 files changed, 53 insertions(+), 43 deletions(-)

diff --git a/src/include/access/heaptoast.h b/src/include/access/heaptoast.h
index 6385a27caf83..fdc8d00d7099 100644
--- a/src/include/access/heaptoast.h
+++ b/src/include/access/heaptoast.h
@@ -142,7 +142,7 @@ extern HeapTuple toast_build_flattened_tuple(TupleDesc tupleDesc,
  *	Fetch a slice from a toast value stored in a heap table.
  * ----------
  */
-extern void heap_fetch_toast_slice(Relation toastrel, Oid valueid,
+extern void heap_fetch_toast_slice(Relation toastrel, Oid8 valueid,
 								   int32 attrsize, int32 sliceoffset,
 								   int32 slicelength, struct varlena *result);
 
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index 1c9e802a6b12..0164083ddc75 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -740,7 +740,7 @@ typedef struct TableAmRoutine
 	 * table implemented by this AM.  See table_relation_fetch_toast_slice()
 	 * for more details.
 	 */
-	void		(*relation_fetch_toast_slice) (Relation toastrel, Oid valueid,
+	void		(*relation_fetch_toast_slice) (Relation toastrel, Oid8 valueid,
 											   int32 attrsize,
 											   int32 sliceoffset,
 											   int32 slicelength,
@@ -1873,7 +1873,7 @@ table_relation_toast_am(Relation rel)
  * stored.
  */
 static inline void
-table_relation_fetch_toast_slice(Relation toastrel, Oid valueid,
+table_relation_fetch_toast_slice(Relation toastrel, Oid8 valueid,
 								 int32 attrsize, int32 sliceoffset,
 								 int32 slicelength, struct varlena *result)
 {
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index a1d0eed8953b..8d8f12a0c256 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -26,8 +26,8 @@
 #include "utils/rel.h"
 #include "utils/snapmgr.h"
 
-static bool toastrel_valueid_exists(Relation toastrel, Oid valueid);
-static bool toastid_valueid_exists(Oid toastrelid, Oid valueid);
+static bool toastrel_valueid_exists(Relation toastrel, Oid8 valueid);
+static bool toastid_valueid_exists(Oid toastrelid, Oid8 valueid);
 
 /* ----------
  * toast_compress_datum -
@@ -456,7 +456,7 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
  * ----------
  */
 static bool
-toastrel_valueid_exists(Relation toastrel, Oid valueid)
+toastrel_valueid_exists(Relation toastrel, Oid8 valueid)
 {
 	bool		result = false;
 	ScanKeyData toastkey;
@@ -504,7 +504,7 @@ toastrel_valueid_exists(Relation toastrel, Oid valueid)
  * ----------
  */
 static bool
-toastid_valueid_exists(Oid toastrelid, Oid valueid)
+toastid_valueid_exists(Oid toastrelid, Oid8 valueid)
 {
 	bool		result;
 	Relation	toastrel;
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index cb1e57030f64..d4b600de3aca 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -623,7 +623,7 @@ toast_build_flattened_tuple(TupleDesc tupleDesc,
  * result is the varlena into which the results should be written.
  */
 void
-heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
+heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 					   int32 sliceoffset, int32 slicelength,
 					   struct varlena *result)
 {
@@ -725,7 +725,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
 		else
 		{
 			/* should never happen */
-			elog(ERROR, "found toasted toast chunk for toast value %u in %s",
+			elog(ERROR, "found toasted toast chunk for toast value " OID8_FORMAT " in %s",
 				 valueid, RelationGetRelationName(toastrel));
 			chunksize = 0;		/* keep compiler quiet */
 			chunkdata = NULL;
@@ -737,13 +737,13 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
 		if (curchunk != expectedchunk)
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
-					 errmsg_internal("unexpected chunk number %d (expected %d) for toast value %u in %s",
+					 errmsg_internal("unexpected chunk number %d (expected %d) for toast value " OID8_FORMAT " in %s",
 									 curchunk, expectedchunk, valueid,
 									 RelationGetRelationName(toastrel))));
 		if (curchunk > endchunk)
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
-					 errmsg_internal("unexpected chunk number %d (out of range %d..%d) for toast value %u in %s",
+					 errmsg_internal("unexpected chunk number %d (out of range %d..%d) for toast value " OID8_FORMAT " in %s",
 									 curchunk,
 									 startchunk, endchunk, valueid,
 									 RelationGetRelationName(toastrel))));
@@ -752,7 +752,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
 		if (chunksize != expected_size)
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
-					 errmsg_internal("unexpected chunk size %d (expected %d) in chunk %d of %d for toast value %u in %s",
+					 errmsg_internal("unexpected chunk size %d (expected %d) in chunk %d of %d for toast value " OID8_FORMAT " in %s",
 									 chunksize, expected_size,
 									 curchunk, totalchunks, valueid,
 									 RelationGetRelationName(toastrel))));
@@ -781,7 +781,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
 	if (expectedchunk != (endchunk + 1))
 		ereport(ERROR,
 				(errcode(ERRCODE_DATA_CORRUPTED),
-				 errmsg_internal("missing chunk number %d for toast value %u in %s",
+				 errmsg_internal("missing chunk number %d for toast value " OID8_FORMAT " in %s",
 								 expectedchunk, valueid,
 								 RelationGetRelationName(toastrel))));
 
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 34cf05668ae8..1c1c203b4145 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -176,7 +176,7 @@ typedef struct ReorderBufferIterTXNState
 /* toast datastructures */
 typedef struct ReorderBufferToastEnt
 {
-	Oid			chunk_id;		/* toast_table.chunk_id */
+	Oid8		chunk_id;		/* toast_table.chunk_id */
 	int32		last_chunk_seq; /* toast_table.chunk_seq of the last chunk we
 								 * have seen */
 	Size		num_chunks;		/* number of chunks we've already seen */
@@ -4944,7 +4944,7 @@ ReorderBufferToastInitHash(ReorderBuffer *rb, ReorderBufferTXN *txn)
 
 	Assert(txn->toast_hash == NULL);
 
-	hash_ctl.keysize = sizeof(Oid);
+	hash_ctl.keysize = sizeof(Oid8);
 	hash_ctl.entrysize = sizeof(ReorderBufferToastEnt);
 	hash_ctl.hcxt = rb->context;
 	txn->toast_hash = hash_create("ReorderBufferToastHash", 5, &hash_ctl,
@@ -4968,7 +4968,7 @@ ReorderBufferToastAppendChunk(ReorderBuffer *rb, ReorderBufferTXN *txn,
 	bool		isnull;
 	Pointer		chunk;
 	TupleDesc	desc = RelationGetDescr(relation);
-	Oid			chunk_id;
+	Oid8		chunk_id;
 	int32		chunk_seq;
 
 	if (txn->toast_hash == NULL)
@@ -4995,11 +4995,11 @@ ReorderBufferToastAppendChunk(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		dlist_init(&ent->chunks);
 
 		if (chunk_seq != 0)
-			elog(ERROR, "got sequence entry %d for toast chunk %u instead of seq 0",
+			elog(ERROR, "got sequence entry %d for toast chunk " OID8_FORMAT " instead of seq 0",
 				 chunk_seq, chunk_id);
 	}
 	else if (found && chunk_seq != ent->last_chunk_seq + 1)
-		elog(ERROR, "got sequence entry %d for toast chunk %u instead of seq %d",
+		elog(ERROR, "got sequence entry %d for toast chunk " OID8_FORMAT " instead of seq %d",
 			 chunk_seq, chunk_id, ent->last_chunk_seq + 1);
 
 	chunk = DatumGetPointer(fastgetattr(newtup, 3, desc, &isnull));
@@ -5108,6 +5108,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		struct varlena *reconstructed;
 		dlist_iter	it;
 		Size		data_done = 0;
+		Oid8		toast_valueid;
 
 		/* system columns aren't toasted */
 		if (attr->attnum < 0)
@@ -5132,13 +5133,14 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 			continue;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, varlena);
+		toast_valueid = toast_pointer.va_valueid;
 
 		/*
 		 * Check whether the toast tuple changed, replace if so.
 		 */
 		ent = (ReorderBufferToastEnt *)
 			hash_search(txn->toast_hash,
-						&toast_pointer.va_valueid,
+						&toast_valueid,
 						HASH_FIND,
 						NULL);
 		if (ent == NULL)
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 4963e9245cb5..eb353c40249e 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1561,6 +1561,9 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	bool		isnull;
 	int32		chunksize;
 	int32		expected_size;
+	Oid8		toast_valueid;
+
+	toast_valueid = ta->toast_pointer.va_valueid;
 
 	/* Sanity-check the sequence number. */
 	chunk_seq = DatumGetInt32(fastgetattr(toasttup, 2,
@@ -1568,16 +1571,16 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	if (isnull)
 	{
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u has toast chunk with null sequence number",
-										 ta->toast_pointer.va_valueid));
+								psprintf("toast value " OID8_FORMAT " has toast chunk with null sequence number",
+										 toast_valueid));
 		return;
 	}
 	if (chunk_seq != *expected_chunk_seq)
 	{
 		/* Either the TOAST index is corrupt, or we don't have all chunks. */
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u index scan returned chunk %d when expecting chunk %d",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value " OID8_FORMAT " index scan returned chunk %d when expecting chunk %d",
+										 toast_valueid,
 										 chunk_seq, *expected_chunk_seq));
 	}
 	*expected_chunk_seq = chunk_seq + 1;
@@ -1588,8 +1591,8 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	if (isnull)
 	{
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u chunk %d has null data",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value " OID8_FORMAT " chunk %d has null data",
+										 toast_valueid,
 										 chunk_seq));
 		return;
 	}
@@ -1608,8 +1611,8 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 		uint32		header = ((varattrib_4b *) chunk)->va_4byte.va_header;
 
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u chunk %d has invalid varlena header %0x",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value " OID8_FORMAT " chunk %d has invalid varlena header %0x",
+										 toast_valueid,
 										 chunk_seq, header));
 		return;
 	}
@@ -1620,8 +1623,8 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	if (chunk_seq > last_chunk_seq)
 	{
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u chunk %d follows last expected chunk %d",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value " OID8_FORMAT " chunk %d follows last expected chunk %d",
+										 toast_valueid,
 										 chunk_seq, last_chunk_seq));
 		return;
 	}
@@ -1631,8 +1634,8 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 
 	if (chunksize != expected_size)
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u chunk %d has size %u, but expected size %u",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value " OID8_FORMAT " chunk %d has size %u, but expected size %u",
+										 toast_valueid,
 										 chunk_seq, chunksize, expected_size));
 }
 
@@ -1663,6 +1666,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	struct varlena *attr;
 	char	   *tp;				/* pointer to the tuple data */
 	uint16		infomask;
+	Oid8		toast_pointer_valueid;
 	CompactAttribute *thisatt;
 	struct varatt_external toast_pointer;
 
@@ -1771,12 +1775,13 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	 * Must copy attr into toast_pointer for alignment considerations
 	 */
 	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_pointer_valueid = toast_pointer.va_valueid;
 
 	/* Toasted attributes too large to be untoasted should never be stored */
 	if (toast_pointer.va_rawsize > VARLENA_SIZE_LIMIT)
 		report_corruption(ctx,
-						  psprintf("toast value %u rawsize %d exceeds limit %d",
-								   toast_pointer.va_valueid,
+						  psprintf("toast value " OID8_FORMAT " rawsize %d exceeds limit %d",
+								   toast_pointer_valueid,
 								   toast_pointer.va_rawsize,
 								   VARLENA_SIZE_LIMIT));
 
@@ -1803,16 +1808,16 @@ check_tuple_attribute(HeapCheckContext *ctx)
 		}
 		if (!valid)
 			report_corruption(ctx,
-							  psprintf("toast value %u has invalid compression method id %d",
-									   toast_pointer.va_valueid, cmid));
+							  psprintf("toast value " OID8_FORMAT " has invalid compression method id %d",
+									   toast_pointer_valueid, cmid));
 	}
 
 	/* The tuple header better claim to contain toasted values */
 	if (!(infomask & HEAP_HASEXTERNAL))
 	{
 		report_corruption(ctx,
-						  psprintf("toast value %u is external but tuple header flag HEAP_HASEXTERNAL not set",
-								   toast_pointer.va_valueid));
+						  psprintf("toast value " OID8_FORMAT " is external but tuple header flag HEAP_HASEXTERNAL not set",
+								   toast_pointer_valueid));
 		return true;
 	}
 
@@ -1820,8 +1825,8 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	if (!ctx->rel->rd_rel->reltoastrelid)
 	{
 		report_corruption(ctx,
-						  psprintf("toast value %u is external but relation has no toast relation",
-								   toast_pointer.va_valueid));
+						  psprintf("toast value " OID8_FORMAT " is external but relation has no toast relation",
+								   toast_pointer_valueid));
 		return true;
 	}
 
@@ -1866,6 +1871,7 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	uint32		extsize;
 	int32		expected_chunk_seq = 0;
 	int32		last_chunk_seq;
+	Oid8		toast_valueid;
 
 	extsize = VARATT_EXTERNAL_GET_EXTSIZE(ta->toast_pointer);
 	last_chunk_seq = (extsize - 1) / TOAST_MAX_CHUNK_SIZE;
@@ -1896,14 +1902,16 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	}
 	systable_endscan_ordered(toastscan);
 
+	toast_valueid = ta->toast_pointer.va_valueid;
+
 	if (!found_toasttup)
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u not found in toast table",
-										 ta->toast_pointer.va_valueid));
+								psprintf("toast value " OID8_FORMAT " not found in toast table",
+										 toast_valueid));
 	else if (expected_chunk_seq <= last_chunk_seq)
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u was expected to end at chunk %d, but ended while expecting chunk %d",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value " OID8_FORMAT " was expected to end at chunk %d, but ended while expecting chunk %d",
+										 toast_valueid,
 										 last_chunk_seq, expected_chunk_seq));
 }
 
-- 
2.50.0

v5-0003-Minimize-footprint-of-TOAST_MAX_CHUNK_SIZE-in-hea.patchtext/x-diff; charset=us-asciiDownload
From 4277df8a3ba4b5773f407073ddea40cdd9cc4e3c Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 13 Aug 2025 17:40:13 +0900
Subject: [PATCH v5 03/15] Minimize footprint of TOAST_MAX_CHUNK_SIZE in heap
 and amcheck

This eases a follow-up change to support 8-byte TOAST value IDs, as the
maximum chunk size allowed for a single chunk of TOASTed data depends on
the size of the value ID.
---
 src/backend/access/heap/heaptoast.c | 20 ++++++++++++--------
 contrib/amcheck/verify_heapam.c     | 13 +++++++++----
 2 files changed, 21 insertions(+), 12 deletions(-)

diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index d4b600de3aca..a3933e48c8c8 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -634,11 +634,12 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 	SysScanDesc toastscan;
 	HeapTuple	ttup;
 	int32		expectedchunk;
-	int32		totalchunks = ((attrsize - 1) / TOAST_MAX_CHUNK_SIZE) + 1;
+	int32		totalchunks;
 	int			startchunk;
 	int			endchunk;
 	int			num_indexes;
 	int			validIndex;
+	int32		max_chunk_size;
 
 	/* Look for the valid index of toast relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -646,8 +647,11 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 									&toastidxs,
 									&num_indexes);
 
-	startchunk = sliceoffset / TOAST_MAX_CHUNK_SIZE;
-	endchunk = (sliceoffset + slicelength - 1) / TOAST_MAX_CHUNK_SIZE;
+	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+
+	totalchunks = ((attrsize - 1) / max_chunk_size) + 1;
+	startchunk = sliceoffset / max_chunk_size;
+	endchunk = (sliceoffset + slicelength - 1) / max_chunk_size;
 	Assert(endchunk <= totalchunks);
 
 	/* Set up a scan key to fetch from the index. */
@@ -747,8 +751,8 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 									 curchunk,
 									 startchunk, endchunk, valueid,
 									 RelationGetRelationName(toastrel))));
-		expected_size = curchunk < totalchunks - 1 ? TOAST_MAX_CHUNK_SIZE
-			: attrsize - ((totalchunks - 1) * TOAST_MAX_CHUNK_SIZE);
+		expected_size = curchunk < totalchunks - 1 ? max_chunk_size
+			: attrsize - ((totalchunks - 1) * max_chunk_size);
 		if (chunksize != expected_size)
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
@@ -763,12 +767,12 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 		chcpystrt = 0;
 		chcpyend = chunksize - 1;
 		if (curchunk == startchunk)
-			chcpystrt = sliceoffset % TOAST_MAX_CHUNK_SIZE;
+			chcpystrt = sliceoffset % max_chunk_size;
 		if (curchunk == endchunk)
-			chcpyend = (sliceoffset + slicelength - 1) % TOAST_MAX_CHUNK_SIZE;
+			chcpyend = (sliceoffset + slicelength - 1) % max_chunk_size;
 
 		memcpy(VARDATA(result) +
-			   (curchunk * TOAST_MAX_CHUNK_SIZE - sliceoffset) + chcpystrt,
+			   (curchunk * max_chunk_size - sliceoffset) + chcpystrt,
 			   chunkdata + chcpystrt,
 			   (chcpyend - chcpystrt) + 1);
 
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index eb353c40249e..164ced37583a 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1556,15 +1556,19 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 				  uint32 extsize)
 {
 	int32		chunk_seq;
-	int32		last_chunk_seq = (extsize - 1) / TOAST_MAX_CHUNK_SIZE;
+	int32		last_chunk_seq;
 	Pointer		chunk;
 	bool		isnull;
 	int32		chunksize;
 	int32		expected_size;
 	Oid8		toast_valueid;
+	int32		max_chunk_size;
 
 	toast_valueid = ta->toast_pointer.va_valueid;
 
+	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	last_chunk_seq = (extsize - 1) / max_chunk_size;
+
 	/* Sanity-check the sequence number. */
 	chunk_seq = DatumGetInt32(fastgetattr(toasttup, 2,
 										  ctx->toast_rel->rd_att, &isnull));
@@ -1629,8 +1633,8 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 		return;
 	}
 
-	expected_size = chunk_seq < last_chunk_seq ? TOAST_MAX_CHUNK_SIZE
-		: extsize - (last_chunk_seq * TOAST_MAX_CHUNK_SIZE);
+	expected_size = chunk_seq < last_chunk_seq ? max_chunk_size
+		: extsize - (last_chunk_seq * max_chunk_size);
 
 	if (chunksize != expected_size)
 		report_toast_corruption(ctx, ta,
@@ -1872,9 +1876,10 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	int32		expected_chunk_seq = 0;
 	int32		last_chunk_seq;
 	Oid8		toast_valueid;
+	int32		max_chunk_size = TOAST_MAX_CHUNK_SIZE;
 
 	extsize = VARATT_EXTERNAL_GET_EXTSIZE(ta->toast_pointer);
-	last_chunk_seq = (extsize - 1) / TOAST_MAX_CHUNK_SIZE;
+	last_chunk_seq = (extsize - 1) / max_chunk_size;
 
 	/*
 	 * Setup a scan key to find chunks in toast table with matching va_valueid
-- 
2.50.0

v5-0004-Renames-around-varatt_external-varatt_external_oi.patchtext/x-diff; charset=us-asciiDownload
From 72aef61f262eed6458968b3ad1662ed767862e4c Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 13 Aug 2025 18:28:10 +0900
Subject: [PATCH v5 04/15] Renames around varatt_external->varatt_external_oid

This impacts a few things:
- VARTAG_ONDISK -> VARTAG_ONDISK_OID
- TOAST_POINTER_SIZE -> TOAST_OID_POINTER_SIZE
- TOAST_MAX_CHUNK_SIZE -> TOAST_OID_MAX_CHUNK_SIZE

The "struct" around varatt_external is cleaned up in most places, while
on it.

This rename is in preparation of a follow-up commit that aims at adding
support for multiple types of external on-disk TOAST pointers, where the
OID type is only one subset of them.
---
 src/include/access/detoast.h                  |  4 +--
 src/include/access/heaptoast.h                |  6 ++--
 src/include/varatt.h                          | 34 +++++++++++--------
 src/backend/access/common/detoast.c           | 10 +++---
 src/backend/access/common/toast_compression.c |  2 +-
 src/backend/access/common/toast_internals.c   | 14 ++++----
 src/backend/access/heap/heaptoast.c           |  2 +-
 src/backend/access/table/toast_helper.c       |  4 +--
 src/backend/access/transam/xlog.c             |  8 ++---
 .../replication/logical/reorderbuffer.c       |  2 +-
 src/backend/utils/adt/varlena.c               |  2 +-
 src/bin/pg_resetwal/pg_resetwal.c             |  2 +-
 doc/src/sgml/func/func-info.sgml              |  2 +-
 doc/src/sgml/storage.sgml                     |  2 +-
 contrib/amcheck/verify_heapam.c               | 10 +++---
 15 files changed, 54 insertions(+), 50 deletions(-)

diff --git a/src/include/access/detoast.h b/src/include/access/detoast.h
index e603a2276c38..6435597b1127 100644
--- a/src/include/access/detoast.h
+++ b/src/include/access/detoast.h
@@ -14,7 +14,7 @@
 
 /*
  * Macro to fetch the possibly-unaligned contents of an EXTERNAL datum
- * into a local "struct varatt_external" toast pointer.  This should be
+ * into a local "varatt_external_oid" toast pointer.  This should be
  * just a memcpy, but some versions of gcc seem to produce broken code
  * that assumes the datum contents are aligned.  Introducing an explicit
  * intermediate "varattrib_1b_e *" variable seems to fix it.
@@ -28,7 +28,7 @@ do { \
 } while (0)
 
 /* Size of an EXTERNAL datum that contains a standard TOAST pointer */
-#define TOAST_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external))
+#define TOAST_OID_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_oid))
 
 /* Size of an EXTERNAL datum that contains an indirection pointer */
 #define INDIRECT_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_indirect))
diff --git a/src/include/access/heaptoast.h b/src/include/access/heaptoast.h
index fdc8d00d7099..59c82b2cb1a3 100644
--- a/src/include/access/heaptoast.h
+++ b/src/include/access/heaptoast.h
@@ -69,19 +69,19 @@
 
 /*
  * When we store an oversize datum externally, we divide it into chunks
- * containing at most TOAST_MAX_CHUNK_SIZE data bytes.  This number *must*
+ * containing at most TOAST_OID_MAX_CHUNK_SIZE data bytes.  This number *must*
  * be small enough that the completed toast-table tuple (including the
  * ID and sequence fields and all overhead) will fit on a page.
  * The coding here sets the size on the theory that we want to fit
  * EXTERN_TUPLES_PER_PAGE tuples of maximum size onto a page.
  *
- * NB: Changing TOAST_MAX_CHUNK_SIZE requires an initdb.
+ * NB: Changing TOAST_OID_MAX_CHUNK_SIZE requires an initdb.
  */
 #define EXTERN_TUPLES_PER_PAGE	4	/* tweak only this */
 
 #define EXTERN_TUPLE_MAX_SIZE	MaximumBytesPerTuple(EXTERN_TUPLES_PER_PAGE)
 
-#define TOAST_MAX_CHUNK_SIZE	\
+#define TOAST_OID_MAX_CHUNK_SIZE	\
 	(EXTERN_TUPLE_MAX_SIZE -							\
 	 MAXALIGN(SizeofHeapTupleHeader) -					\
 	 sizeof(Oid) -										\
diff --git a/src/include/varatt.h b/src/include/varatt.h
index aeeabf9145b5..c873a59bb1c9 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -16,7 +16,7 @@
 #define VARATT_H
 
 /*
- * struct varatt_external is a traditional "TOAST pointer", that is, the
+ * varatt_external_oid is a traditional "TOAST pointer", that is, the
  * information needed to fetch a Datum stored out-of-line in a TOAST table.
  * The data is compressed if and only if the external size stored in
  * va_extinfo is less than va_rawsize - VARHDRSZ.
@@ -29,14 +29,14 @@
  * you can look at these fields!  (The reason we use memcmp is to avoid
  * having to do that just to detect equality of two TOAST pointers...)
  */
-typedef struct varatt_external
+typedef struct varatt_external_oid
 {
 	int32		va_rawsize;		/* Original data size (includes header) */
 	uint32		va_extinfo;		/* External saved size (without header) and
 								 * compression method */
 	Oid			va_valueid;		/* Unique ID of value within TOAST table */
 	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
-}			varatt_external;
+}			varatt_external_oid;
 
 /*
  * These macros define the "saved size" portion of va_extinfo.  Its remaining
@@ -51,7 +51,7 @@ typedef struct varatt_external
  * The creator of such a Datum is entirely responsible that the referenced
  * storage survives for as long as referencing pointer Datums can exist.
  *
- * Note that just as for struct varatt_external, this struct is stored
+ * Note that just as for varatt_external_oid, this struct is stored
  * unaligned within any containing tuple.
  */
 typedef struct varatt_indirect
@@ -66,7 +66,7 @@ typedef struct varatt_indirect
  * storage.  APIs for this, in particular the definition of struct
  * ExpandedObjectHeader, are in src/include/utils/expandeddatum.h.
  *
- * Note that just as for struct varatt_external, this struct is stored
+ * Note that just as for varatt_external_oid, this struct is stored
  * unaligned within any containing tuple.
  */
 typedef struct ExpandedObjectHeader ExpandedObjectHeader;
@@ -78,15 +78,16 @@ typedef struct varatt_expanded
 
 /*
  * Type tag for the various sorts of "TOAST pointer" datums.  The peculiar
- * value for VARTAG_ONDISK comes from a requirement for on-disk compatibility
- * with a previous notion that the tag field was the pointer datum's length.
+ * value for VARTAG_ONDISK_OID comes from a requirement for on-disk
+ * compatibility with a previous notion that the tag field was the pointer
+ * datum's length.
  */
 typedef enum vartag_external
 {
 	VARTAG_INDIRECT = 1,
 	VARTAG_EXPANDED_RO = 2,
 	VARTAG_EXPANDED_RW = 3,
-	VARTAG_ONDISK = 18
+	VARTAG_ONDISK_OID = 18
 } vartag_external;
 
 /* Is a TOAST pointer either type of expanded-object pointer? */
@@ -105,8 +106,8 @@ VARTAG_SIZE(vartag_external tag)
 		return sizeof(varatt_indirect);
 	else if (VARTAG_IS_EXPANDED(tag))
 		return sizeof(varatt_expanded);
-	else if (tag == VARTAG_ONDISK)
-		return sizeof(varatt_external);
+	else if (tag == VARTAG_ONDISK_OID)
+		return sizeof(varatt_external_oid);
 	else
 	{
 		Assert(false);
@@ -360,7 +361,7 @@ VARATT_IS_EXTERNAL(const void *PTR)
 static inline bool
 VARATT_IS_EXTERNAL_ONDISK(const void *PTR)
 {
-	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK;
+	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID;
 }
 
 /* Is varlena datum an indirect pointer? */
@@ -502,15 +503,18 @@ VARDATA_COMPRESSED_GET_COMPRESS_METHOD(const void *PTR)
 	return ((varattrib_4b *) PTR)->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS;
 }
 
-/* Same for external Datums; but note argument is a struct varatt_external */
+/*
+ * Same for external Datums; but note argument is a struct
+ * varatt_external_oid.
+ */
 static inline Size
-VARATT_EXTERNAL_GET_EXTSIZE(struct varatt_external toast_pointer)
+VARATT_EXTERNAL_GET_EXTSIZE(varatt_external_oid toast_pointer)
 {
 	return toast_pointer.va_extinfo & VARLENA_EXTSIZE_MASK;
 }
 
 static inline uint32
-VARATT_EXTERNAL_GET_COMPRESS_METHOD(struct varatt_external toast_pointer)
+VARATT_EXTERNAL_GET_COMPRESS_METHOD(varatt_external_oid toast_pointer)
 {
 	return toast_pointer.va_extinfo >> VARLENA_EXTSIZE_BITS;
 }
@@ -533,7 +537,7 @@ VARATT_EXTERNAL_GET_COMPRESS_METHOD(struct varatt_external toast_pointer)
  * actually saves space, so we expect either equality or less-than.
  */
 static inline bool
-VARATT_EXTERNAL_IS_COMPRESSED(struct varatt_external toast_pointer)
+VARATT_EXTERNAL_IS_COMPRESSED(varatt_external_oid toast_pointer)
 {
 	return VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) <
 		(Size) (toast_pointer.va_rawsize - VARHDRSZ);
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 626517877422..c187c32d96dd 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -225,7 +225,7 @@ detoast_attr_slice(struct varlena *attr,
 
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
-		struct varatt_external toast_pointer;
+		varatt_external_oid toast_pointer;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 
@@ -344,7 +344,7 @@ toast_fetch_datum(struct varlena *attr)
 {
 	Relation	toastrel;
 	struct varlena *result;
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 	int32		attrsize;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
@@ -398,7 +398,7 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 {
 	Relation	toastrel;
 	struct varlena *result;
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 	int32		attrsize;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
@@ -550,7 +550,7 @@ toast_raw_datum_size(Datum value)
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
 		/* va_rawsize is the size of the original datum -- including header */
-		struct varatt_external toast_pointer;
+		varatt_external_oid toast_pointer;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 		result = toast_pointer.va_rawsize;
@@ -610,7 +610,7 @@ toast_datum_size(Datum value)
 		 * compressed or not.  We do not count the size of the toast pointer
 		 * ... should we?
 		 */
-		struct varatt_external toast_pointer;
+		varatt_external_oid toast_pointer;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 		result = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c
index 926f1e4008ab..08f572f31eed 100644
--- a/src/backend/access/common/toast_compression.c
+++ b/src/backend/access/common/toast_compression.c
@@ -262,7 +262,7 @@ toast_get_compression_id(struct varlena *attr)
 	 */
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
-		struct varatt_external toast_pointer;
+		varatt_external_oid toast_pointer;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 8d8f12a0c256..07df7edc47ce 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -127,12 +127,12 @@ toast_save_datum(Relation rel, Datum value,
 	bool		t_isnull[3];
 	CommandId	mycid = GetCurrentCommandId(true);
 	struct varlena *result;
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 	union
 	{
 		struct varlena hdr;
 		/* this is to make the union big enough for a chunk: */
-		char		data[TOAST_MAX_CHUNK_SIZE + VARHDRSZ];
+		char		data[TOAST_OID_MAX_CHUNK_SIZE + VARHDRSZ];
 		/* ensure union is aligned well enough: */
 		int32		align_it;
 	}			chunk_data;
@@ -237,7 +237,7 @@ toast_save_datum(Relation rel, Datum value,
 		toast_pointer.va_valueid = InvalidOid;
 		if (oldexternal != NULL)
 		{
-			struct varatt_external old_toast_pointer;
+			varatt_external_oid old_toast_pointer;
 
 			Assert(VARATT_IS_EXTERNAL_ONDISK(oldexternal));
 			/* Must copy to access aligned fields */
@@ -310,7 +310,7 @@ toast_save_datum(Relation rel, Datum value,
 		/*
 		 * Calculate the size of this chunk
 		 */
-		chunk_size = Min(TOAST_MAX_CHUNK_SIZE, data_todo);
+		chunk_size = Min(TOAST_OID_MAX_CHUNK_SIZE, data_todo);
 
 		/*
 		 * Build a tuple and store it
@@ -368,8 +368,8 @@ toast_save_datum(Relation rel, Datum value,
 	/*
 	 * Create the TOAST pointer value that we'll return
 	 */
-	result = (struct varlena *) palloc(TOAST_POINTER_SIZE);
-	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK);
+	result = (struct varlena *) palloc(TOAST_OID_POINTER_SIZE);
+	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_OID);
 	memcpy(VARDATA_EXTERNAL(result), &toast_pointer, sizeof(toast_pointer));
 
 	return PointerGetDatum(result);
@@ -385,7 +385,7 @@ void
 toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 {
 	struct varlena *attr = (struct varlena *) DatumGetPointer(value);
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 	Relation	toastrel;
 	Relation   *toastidxs;
 	ScanKeyData toastkey;
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index a3933e48c8c8..ddde7fcf79a4 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -647,7 +647,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 									&toastidxs,
 									&num_indexes);
 
-	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
 
 	totalchunks = ((attrsize - 1) / max_chunk_size) + 1;
 	startchunk = sliceoffset / max_chunk_size;
diff --git a/src/backend/access/table/toast_helper.c b/src/backend/access/table/toast_helper.c
index 11f97d65367d..0c58c6c32565 100644
--- a/src/backend/access/table/toast_helper.c
+++ b/src/backend/access/table/toast_helper.c
@@ -171,7 +171,7 @@ toast_tuple_init(ToastTupleContext *ttc)
  * The column must have attstorage EXTERNAL or EXTENDED if check_main is
  * false, and must have attstorage MAIN if check_main is true.
  *
- * The column must have a minimum size of MAXALIGN(TOAST_POINTER_SIZE);
+ * The column must have a minimum size of MAXALIGN(TOAST_OID_POINTER_SIZE);
  * if not, no benefit is to be expected by compressing it.
  *
  * The return value is the index of the biggest suitable column, or
@@ -184,7 +184,7 @@ toast_tuple_find_biggest_attribute(ToastTupleContext *ttc,
 	TupleDesc	tupleDesc = ttc->ttc_rel->rd_att;
 	int			numAttrs = tupleDesc->natts;
 	int			biggest_attno = -1;
-	int32		biggest_size = MAXALIGN(TOAST_POINTER_SIZE);
+	int32		biggest_size = MAXALIGN(TOAST_OID_POINTER_SIZE);
 	int32		skip_colflags = TOASTCOL_IGNORE;
 	int			i;
 
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index e8909406686d..fdc4f987f9ac 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4387,7 +4387,7 @@ WriteControlFile(void)
 	ControlFile->nameDataLen = NAMEDATALEN;
 	ControlFile->indexMaxKeys = INDEX_MAX_KEYS;
 
-	ControlFile->toast_max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	ControlFile->toast_max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
 	ControlFile->loblksize = LOBLKSIZE;
 
 	ControlFile->float8ByVal = true;	/* vestigial */
@@ -4630,15 +4630,15 @@ ReadControlFile(void)
 						   "INDEX_MAX_KEYS", ControlFile->indexMaxKeys,
 						   "INDEX_MAX_KEYS", INDEX_MAX_KEYS),
 				 errhint("It looks like you need to recompile or initdb.")));
-	if (ControlFile->toast_max_chunk_size != TOAST_MAX_CHUNK_SIZE)
+	if (ControlFile->toast_max_chunk_size != TOAST_OID_MAX_CHUNK_SIZE)
 		ereport(FATAL,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("database files are incompatible with server"),
 		/* translator: %s is a variable name and %d is its value */
 				 errdetail("The database cluster was initialized with %s %d,"
 						   " but the server was compiled with %s %d.",
-						   "TOAST_MAX_CHUNK_SIZE", ControlFile->toast_max_chunk_size,
-						   "TOAST_MAX_CHUNK_SIZE", (int) TOAST_MAX_CHUNK_SIZE),
+						   "TOAST_OID_MAX_CHUNK_SIZE", ControlFile->toast_max_chunk_size,
+						   "TOAST_OID_MAX_CHUNK_SIZE", (int) TOAST_OID_MAX_CHUNK_SIZE),
 				 errhint("It looks like you need to recompile or initdb.")));
 	if (ControlFile->loblksize != LOBLKSIZE)
 		ereport(FATAL,
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 1c1c203b4145..29d594375de0 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -5102,7 +5102,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		struct varlena *varlena;
 
 		/* va_rawsize is the size of the original datum -- including header */
-		struct varatt_external toast_pointer;
+		varatt_external_oid toast_pointer;
 		struct varatt_indirect redirect_pointer;
 		struct varlena *new_datum = NULL;
 		struct varlena *reconstructed;
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 2c398cd9e5cb..4aff647fccfd 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -4211,7 +4211,7 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 {
 	int			typlen;
 	struct varlena *attr;
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 
 	/* On first call, get the input type's typlen, and save at *fn_extra */
 	if (fcinfo->flinfo->fn_extra == NULL)
diff --git a/src/bin/pg_resetwal/pg_resetwal.c b/src/bin/pg_resetwal/pg_resetwal.c
index 7a4e4eb95706..638b41c922ba 100644
--- a/src/bin/pg_resetwal/pg_resetwal.c
+++ b/src/bin/pg_resetwal/pg_resetwal.c
@@ -717,7 +717,7 @@ GuessControlValues(void)
 	ControlFile.xlog_seg_size = DEFAULT_XLOG_SEG_SIZE;
 	ControlFile.nameDataLen = NAMEDATALEN;
 	ControlFile.indexMaxKeys = INDEX_MAX_KEYS;
-	ControlFile.toast_max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	ControlFile.toast_max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
 	ControlFile.loblksize = LOBLKSIZE;
 	ControlFile.float8ByVal = true; /* vestigial */
 
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index c393832d94c6..ba6b592cdb3f 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3533,7 +3533,7 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
       </row>
 
       <row>
-       <entry><structfield>max_toast_chunk_size</structfield></entry>
+       <entry><structfield>max_toast_oid_chunk_size</structfield></entry>
        <entry><type>integer</type></entry>
       </row>
 
diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml
index 02ddfda834a2..67600fd974d7 100644
--- a/doc/src/sgml/storage.sgml
+++ b/doc/src/sgml/storage.sgml
@@ -417,7 +417,7 @@ described in more detail below.
 
 <para>
 Out-of-line values are divided (after compression if used) into chunks of at
-most <symbol>TOAST_MAX_CHUNK_SIZE</symbol> bytes (by default this value is chosen
+most <symbol>TOAST_OID_MAX_CHUNK_SIZE</symbol> bytes (by default this value is chosen
 so that four chunk rows will fit on a page, making it about 2000 bytes).
 Each chunk is stored as a separate row in the <acronym>TOAST</acronym> table
 belonging to the owning table.  Every
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 164ced37583a..7ec6cef118fb 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -73,7 +73,7 @@ typedef enum SkipPages
  */
 typedef struct ToastedAttribute
 {
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 	BlockNumber blkno;			/* block in main table */
 	OffsetNumber offnum;		/* offset in main table */
 	AttrNumber	attnum;			/* attribute in main table */
@@ -1566,7 +1566,7 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 
 	toast_valueid = ta->toast_pointer.va_valueid;
 
-	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
 	last_chunk_seq = (extsize - 1) / max_chunk_size;
 
 	/* Sanity-check the sequence number. */
@@ -1672,7 +1672,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	uint16		infomask;
 	Oid8		toast_pointer_valueid;
 	CompactAttribute *thisatt;
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 
 	infomask = ctx->tuphdr->t_infomask;
 	thisatt = TupleDescCompactAttr(RelationGetDescr(ctx->rel), ctx->attnum);
@@ -1731,7 +1731,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	{
 		uint8		va_tag = VARTAG_EXTERNAL(tp + ctx->offset);
 
-		if (va_tag != VARTAG_ONDISK)
+		if (va_tag != VARTAG_ONDISK_OID)
 		{
 			report_corruption(ctx,
 							  psprintf("toasted attribute has unexpected TOAST tag %u",
@@ -1876,7 +1876,7 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	int32		expected_chunk_seq = 0;
 	int32		last_chunk_seq;
 	Oid8		toast_valueid;
-	int32		max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	int32		max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
 
 	extsize = VARATT_EXTERNAL_GET_EXTSIZE(ta->toast_pointer);
 	last_chunk_seq = (extsize - 1) / max_chunk_size;
-- 
2.50.0

v5-0005-Refactor-external-TOAST-pointer-code-for-better-p.patchtext/x-diff; charset=us-asciiDownload
From c6a93bc3320a8712d200f44838b9a867906d0733 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 13 Aug 2025 19:15:43 +0900
Subject: [PATCH v5 05/15] Refactor external TOAST pointer code for better
 pluggability

This commit introduces a new interface for external TOAST pointers,
which is able to make a translation of the varlena pointers stored on
disk to/from an new in-memory structure called toast_external.  The
types of varatt_external supported on disk need to be registered into a
new subsystem in a new file, called toast_external.[c|h], then define a
set of callbacks to allow the toasting and detoasting code to use it.

A follow-up change will rely on this refactoring to introduce new
vartag_external values with an associated varatt_external_* that is
able, which would be used in int8 TOAST tables.
---
 src/include/access/detoast.h                  |  12 +-
 src/include/access/heaptoast.h                |   3 +
 src/include/access/toast_external.h           | 176 ++++++++++++++++
 src/include/access/toast_helper.h             |   1 +
 src/include/varatt.h                          |  16 +-
 src/backend/access/common/Makefile            |   1 +
 src/backend/access/common/detoast.c           |  57 +++---
 src/backend/access/common/meson.build         |   1 +
 src/backend/access/common/toast_compression.c |  10 +-
 src/backend/access/common/toast_external.c    | 191 ++++++++++++++++++
 src/backend/access/common/toast_internals.c   |  86 +++++---
 src/backend/access/heap/heaptoast.c           |  20 +-
 src/backend/access/table/toast_helper.c       |  12 +-
 src/backend/access/transam/xlog.c             |   8 +-
 .../replication/logical/reorderbuffer.c       |  13 +-
 src/backend/utils/adt/varlena.c               |   7 +-
 src/bin/pg_resetwal/pg_resetwal.c             |   2 +-
 contrib/amcheck/verify_heapam.c               |  35 ++--
 src/tools/pgindent/typedefs.list              |   2 +
 19 files changed, 541 insertions(+), 112 deletions(-)
 create mode 100644 src/include/access/toast_external.h
 create mode 100644 src/backend/access/common/toast_external.c

diff --git a/src/include/access/detoast.h b/src/include/access/detoast.h
index 6435597b1127..b3ebad8b7cf9 100644
--- a/src/include/access/detoast.h
+++ b/src/include/access/detoast.h
@@ -14,10 +14,11 @@
 
 /*
  * Macro to fetch the possibly-unaligned contents of an EXTERNAL datum
- * into a local "varatt_external_oid" toast pointer.  This should be
- * just a memcpy, but some versions of gcc seem to produce broken code
- * that assumes the datum contents are aligned.  Introducing an explicit
- * intermediate "varattrib_1b_e *" variable seems to fix it.
+ * into a local "varatt_external_*" toast pointer, as supported
+ * in toast_external.c and varatt.h.  This should be just a memcpy, but
+ * some versions of gcc seem to produce broken code that assumes the datum
+ * contents are aligned.  Introducing an explicit intermediate
+ * "varattrib_1b_e *" variable seems to fix it.
  */
 #define VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr) \
 do { \
@@ -27,9 +28,6 @@ do { \
 	memcpy(&(toast_pointer), VARDATA_EXTERNAL(attre), sizeof(toast_pointer)); \
 } while (0)
 
-/* Size of an EXTERNAL datum that contains a standard TOAST pointer */
-#define TOAST_OID_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_oid))
-
 /* Size of an EXTERNAL datum that contains an indirection pointer */
 #define INDIRECT_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_indirect))
 
diff --git a/src/include/access/heaptoast.h b/src/include/access/heaptoast.h
index 59c82b2cb1a3..afa3d8ca95f7 100644
--- a/src/include/access/heaptoast.h
+++ b/src/include/access/heaptoast.h
@@ -88,6 +88,9 @@
 	 sizeof(int32) -									\
 	 VARHDRSZ)
 
+/* Maximum size of chunk possible */
+#define TOAST_MAX_CHUNK_SIZE	TOAST_OID_MAX_CHUNK_SIZE
+
 /* ----------
  * heap_toast_insert_or_update -
  *
diff --git a/src/include/access/toast_external.h b/src/include/access/toast_external.h
new file mode 100644
index 000000000000..6450343eab25
--- /dev/null
+++ b/src/include/access/toast_external.h
@@ -0,0 +1,176 @@
+/*-------------------------------------------------------------------------
+ *
+ * toast_external.h
+ *	  Support for on-disk external TOAST pointers
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1995, Regents of the University of California
+ *
+ * src/include/access/toast_external.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef TOAST_EXTERNAL_H
+#define TOAST_EXTERNAL_H
+
+#include "access/toast_compression.h"
+#include "varatt.h"
+
+/*
+ * Intermediate in-memory structure used when creating on-disk
+ * varatt_external_* or when deserializing varlena contents.
+ */
+typedef struct toast_external_data
+{
+	/* Original data size (includes header) */
+	int32		rawsize;
+
+	/* External saved size (without header) */
+	uint32		extsize;
+
+	/*
+	 * Compression method.
+	 *
+	 * If not compressed, set to TOAST_INVALID_COMPRESSION_ID.
+	 */
+	ToastCompressionId compression_method;
+
+	/* Relation OID of TOAST table containing the value */
+	Oid			toastrelid;
+
+	/*
+	 * Unique ID of value within TOAST table.  This could be an OID or an Oid8
+	 * value.  This field is large enough to be able to store any of these.
+	 */
+	Oid8		valueid;
+} toast_external_data;
+
+/*
+ * Metadata for external TOAST pointer kinds, separated based on their
+ * vartag_external.
+ */
+typedef struct toast_external_info
+{
+	/*
+	 * Maximum chunk of data authorized for this type of external TOAST
+	 * pointer, when dividing an entry by chunks.  Sized depending on the size
+	 * of its varatt_external_* structure.
+	 */
+	int32		maximum_chunk_size;
+
+	/*
+	 * Size of an external TOAST pointer of this type, typically
+	 * (VARHDRSZ_EXTERNAL + sizeof(varatt_external_struct)).
+	 */
+	int32		toast_pointer_size;
+
+	/*
+	 * Map an input varlena to a toast_external_data, for consumption in the
+	 * backend code.  "data" is an input/output result.
+	 */
+	void		(*to_external_data) (struct varlena *attr,
+									 toast_external_data *data);
+
+	/*
+	 * Create a varlena that will be used on-disk for the given TOAST type,
+	 * based on the given input data.
+	 *
+	 * The result is the varlena created, for on-disk insertion.
+	 */
+	struct varlena *(*create_external_data) (toast_external_data data);
+
+} toast_external_info;
+
+/* Retrieve a toast_external_info from a vartag */
+extern const toast_external_info *toast_external_get_info(uint8 tag);
+
+/* Retrieve toast_pointer_size using a TOAST attribute type */
+extern int32 toast_external_info_get_pointer_size(uint8 tag);
+
+/* Retrieve the vartag to assign to a TOAST typle */
+extern uint8 toast_external_assign_vartag(Oid toastrelid, Oid8 value);
+
+/*
+ * Testing whether an externally-stored value is compressed now requires
+ * comparing size stored in extsize (the actual length of the external data)
+ * to rawsize (the original uncompressed datum's size).  The latter includes
+ * VARHDRSZ overhead, the former doesn't.  We never use compression unless it
+ * actually saves space, so we expect either equality or less-than.
+ */
+static inline bool
+TOAST_EXTERNAL_IS_COMPRESSED(toast_external_data data)
+{
+	return data.extsize < (data.rawsize - VARHDRSZ);
+}
+
+/* Full data structure */
+static inline void
+toast_external_info_get_data(struct varlena *attr, toast_external_data *data)
+{
+	uint8		tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+
+	info->to_external_data(attr, data);
+}
+
+/*
+ * Helper routines to recover specific fields in toast_external_data.  Most
+ * code paths doing work with on-disk external TOAST pointers care about
+ * these.
+ */
+
+/* Detoasted "raw" size */
+static inline Size
+toast_external_info_get_rawsize(struct varlena *attr)
+{
+	uint8		tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+	toast_external_data data;
+
+	info->to_external_data(attr, &data);
+
+	return data.rawsize;
+}
+
+/* External saved size */
+static inline Size
+toast_external_info_get_extsize(struct varlena *attr)
+{
+	uint8		tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+	toast_external_data data;
+
+	info->to_external_data(attr, &data);
+
+	return data.extsize;
+}
+
+/* Compression method ID */
+static inline ToastCompressionId
+toast_external_info_get_compression_method(struct varlena *attr)
+{
+	uint8		tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+	toast_external_data data;
+
+	info->to_external_data(attr, &data);
+
+	return data.compression_method;
+}
+
+/* Value ID */
+static inline Oid8
+toast_external_info_get_valueid(struct varlena *attr)
+{
+	uint8		tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+	toast_external_data data;
+
+	info->to_external_data(attr, &data);
+
+	return data.valueid;
+}
+
+#endif							/* TOAST_EXTERNAL_H */
diff --git a/src/include/access/toast_helper.h b/src/include/access/toast_helper.h
index e6ab8afffb67..6bc912809f34 100644
--- a/src/include/access/toast_helper.h
+++ b/src/include/access/toast_helper.h
@@ -47,6 +47,7 @@ typedef struct
 	 * should be NULL in the case of an insert.
 	 */
 	Relation	ttc_rel;		/* the relation that contains the tuple */
+	int32		ttc_toast_pointer_size; /* size of external TOAST pointer */
 	Datum	   *ttc_values;		/* values from the tuple columns */
 	bool	   *ttc_isnull;		/* null flags for the tuple columns */
 	Datum	   *ttc_oldvalues;	/* values from previous tuple */
diff --git a/src/include/varatt.h b/src/include/varatt.h
index c873a59bb1c9..790d9f844c91 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -21,6 +21,9 @@
  * The data is compressed if and only if the external size stored in
  * va_extinfo is less than va_rawsize - VARHDRSZ.
  *
+ * The value ID is an OID, used for TOAST relations with OID as attribute
+ * for chunk_id.
+ *
  * This struct must not contain any padding, because we sometimes compare
  * these pointers using memcmp.
  *
@@ -51,7 +54,7 @@ typedef struct varatt_external_oid
  * The creator of such a Datum is entirely responsible that the referenced
  * storage survives for as long as referencing pointer Datums can exist.
  *
- * Note that just as for varatt_external_oid, this struct is stored
+ * Note that just as for varatt_external_*, this struct is stored
  * unaligned within any containing tuple.
  */
 typedef struct varatt_indirect
@@ -66,7 +69,7 @@ typedef struct varatt_indirect
  * storage.  APIs for this, in particular the definition of struct
  * ExpandedObjectHeader, are in src/include/utils/expandeddatum.h.
  *
- * Note that just as for varatt_external_oid, this struct is stored
+ * Note that just as for varatt_external_*, this struct is stored
  * unaligned within any containing tuple.
  */
 typedef struct ExpandedObjectHeader ExpandedObjectHeader;
@@ -357,11 +360,18 @@ VARATT_IS_EXTERNAL(const void *PTR)
 	return VARATT_IS_1B_E(PTR);
 }
 
+/* Is varlena datum a pointer to on-disk toasted data with OID value? */
+static inline bool
+VARATT_IS_EXTERNAL_ONDISK_OID(const void *PTR)
+{
+	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID;
+}
+
 /* Is varlena datum a pointer to on-disk toasted data? */
 static inline bool
 VARATT_IS_EXTERNAL_ONDISK(const void *PTR)
 {
-	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID;
+	return VARATT_IS_EXTERNAL_ONDISK_OID(PTR);
 }
 
 /* Is varlena datum an indirect pointer? */
diff --git a/src/backend/access/common/Makefile b/src/backend/access/common/Makefile
index e78de312659e..1ef86a245886 100644
--- a/src/backend/access/common/Makefile
+++ b/src/backend/access/common/Makefile
@@ -27,6 +27,7 @@ OBJS = \
 	syncscan.o \
 	tidstore.o \
 	toast_compression.o \
+	toast_external.o \
 	toast_internals.o \
 	tupconvert.o \
 	tupdesc.o
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index c187c32d96dd..8531c27439e4 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -16,6 +16,7 @@
 #include "access/detoast.h"
 #include "access/table.h"
 #include "access/tableam.h"
+#include "access/toast_external.h"
 #include "access/toast_internals.h"
 #include "common/int.h"
 #include "common/pg_lzcompress.h"
@@ -225,12 +226,12 @@ detoast_attr_slice(struct varlena *attr,
 
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
-		varatt_external_oid toast_pointer;
+		toast_external_data toast_pointer;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		toast_external_info_get_data(attr, &toast_pointer);
 
 		/* fast path for non-compressed external datums */
-		if (!VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+		if (!TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 			return toast_fetch_datum_slice(attr, sliceoffset, slicelength);
 
 		/*
@@ -240,7 +241,7 @@ detoast_attr_slice(struct varlena *attr,
 		 */
 		if (slicelimit >= 0)
 		{
-			int32		max_size = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+			int32		max_size = toast_pointer.extsize;
 
 			/*
 			 * Determine maximum amount of compressed data needed for a prefix
@@ -251,8 +252,7 @@ detoast_attr_slice(struct varlena *attr,
 			 * determine how much compressed data we need to be sure of being
 			 * able to decompress the required slice.
 			 */
-			if (VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) ==
-				TOAST_PGLZ_COMPRESSION_ID)
+			if (toast_pointer.compression_method == TOAST_PGLZ_COMPRESSION_ID)
 				max_size = pglz_maximum_compressed_size(slicelimit, max_size);
 
 			/*
@@ -344,20 +344,21 @@ toast_fetch_datum(struct varlena *attr)
 {
 	Relation	toastrel;
 	struct varlena *result;
-	varatt_external_oid toast_pointer;
+	toast_external_data toast_pointer;
 	int32		attrsize;
+	Oid8		valueid;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		elog(ERROR, "toast_fetch_datum shouldn't be called for non-ondisk datums");
 
 	/* Must copy to access aligned fields */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_external_info_get_data(attr, &toast_pointer);
 
-	attrsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+	attrsize = toast_pointer.extsize;
 
 	result = (struct varlena *) palloc(attrsize + VARHDRSZ);
 
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+	if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 		SET_VARSIZE_COMPRESSED(result, attrsize + VARHDRSZ);
 	else
 		SET_VARSIZE(result, attrsize + VARHDRSZ);
@@ -365,14 +366,15 @@ toast_fetch_datum(struct varlena *attr)
 	if (attrsize == 0)
 		return result;			/* Probably shouldn't happen, but just in
 								 * case. */
+	valueid = toast_pointer.valueid;
 
 	/*
 	 * Open the toast relation and its indexes
 	 */
-	toastrel = table_open(toast_pointer.va_toastrelid, AccessShareLock);
+	toastrel = table_open(toast_pointer.toastrelid, AccessShareLock);
 
 	/* Fetch all chunks */
-	table_relation_fetch_toast_slice(toastrel, toast_pointer.va_valueid,
+	table_relation_fetch_toast_slice(toastrel, valueid,
 									 attrsize, 0, attrsize, result);
 
 	/* Close toast table */
@@ -398,23 +400,26 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 {
 	Relation	toastrel;
 	struct varlena *result;
-	varatt_external_oid toast_pointer;
+	toast_external_data toast_pointer;
 	int32		attrsize;
+	Oid8		valueid;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		elog(ERROR, "toast_fetch_datum_slice shouldn't be called for non-ondisk datums");
 
 	/* Must copy to access aligned fields */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_external_info_get_data(attr, &toast_pointer);
+
+	valueid = toast_pointer.valueid;
 
 	/*
 	 * It's nonsense to fetch slices of a compressed datum unless when it's a
 	 * prefix -- this isn't lo_* we can't return a compressed datum which is
 	 * meaningful to toast later.
 	 */
-	Assert(!VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer) || 0 == sliceoffset);
+	Assert(!TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer) || 0 == sliceoffset);
 
-	attrsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+	attrsize = toast_pointer.extsize;
 
 	if (sliceoffset >= attrsize)
 	{
@@ -427,7 +432,7 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 	 * space required by va_tcinfo, which is stored at the beginning as an
 	 * int32 value.
 	 */
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer) && slicelength > 0)
+	if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer) && slicelength > 0)
 		slicelength = slicelength + sizeof(int32);
 
 	/*
@@ -440,7 +445,7 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 
 	result = (struct varlena *) palloc(slicelength + VARHDRSZ);
 
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+	if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 		SET_VARSIZE_COMPRESSED(result, slicelength + VARHDRSZ);
 	else
 		SET_VARSIZE(result, slicelength + VARHDRSZ);
@@ -449,10 +454,11 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 		return result;			/* Can save a lot of work at this point! */
 
 	/* Open the toast relation */
-	toastrel = table_open(toast_pointer.va_toastrelid, AccessShareLock);
+	toastrel = table_open(toast_pointer.toastrelid, AccessShareLock);
 
 	/* Fetch all chunks */
-	table_relation_fetch_toast_slice(toastrel, toast_pointer.va_valueid,
+	table_relation_fetch_toast_slice(toastrel,
+									 valueid,
 									 attrsize, sliceoffset, slicelength,
 									 result);
 
@@ -549,11 +555,7 @@ toast_raw_datum_size(Datum value)
 
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
-		/* va_rawsize is the size of the original datum -- including header */
-		varatt_external_oid toast_pointer;
-
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-		result = toast_pointer.va_rawsize;
+		result = toast_external_info_get_rawsize(attr);
 	}
 	else if (VARATT_IS_EXTERNAL_INDIRECT(attr))
 	{
@@ -610,10 +612,7 @@ toast_datum_size(Datum value)
 		 * compressed or not.  We do not count the size of the toast pointer
 		 * ... should we?
 		 */
-		varatt_external_oid toast_pointer;
-
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-		result = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+		result = toast_external_info_get_extsize(attr);
 	}
 	else if (VARATT_IS_EXTERNAL_INDIRECT(attr))
 	{
diff --git a/src/backend/access/common/meson.build b/src/backend/access/common/meson.build
index e3cdbe7a22e1..c20f2e88921e 100644
--- a/src/backend/access/common/meson.build
+++ b/src/backend/access/common/meson.build
@@ -15,6 +15,7 @@ backend_sources += files(
   'syncscan.c',
   'tidstore.c',
   'toast_compression.c',
+  'toast_external.c',
   'toast_internals.c',
   'tupconvert.c',
   'tupdesc.c',
diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c
index 08f572f31eed..94606a58c8fb 100644
--- a/src/backend/access/common/toast_compression.c
+++ b/src/backend/access/common/toast_compression.c
@@ -19,6 +19,7 @@
 
 #include "access/detoast.h"
 #include "access/toast_compression.h"
+#include "access/toast_external.h"
 #include "common/pg_lzcompress.h"
 #include "varatt.h"
 
@@ -261,14 +262,7 @@ toast_get_compression_id(struct varlena *attr)
 	 * toast compression header.
 	 */
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
-	{
-		varatt_external_oid toast_pointer;
-
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-
-		if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
-			cmid = VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer);
-	}
+		cmid = toast_external_info_get_compression_method(attr);
 	else if (VARATT_IS_COMPRESSED(attr))
 		cmid = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(attr);
 
diff --git a/src/backend/access/common/toast_external.c b/src/backend/access/common/toast_external.c
new file mode 100644
index 000000000000..5c8679a0f485
--- /dev/null
+++ b/src/backend/access/common/toast_external.c
@@ -0,0 +1,191 @@
+/*-------------------------------------------------------------------------
+ *
+ * toast_external.c
+ *	  Functions for the support of external on-disk TOAST pointers.
+ *
+ * This includes all the types of external on-disk TOAST pointers supported
+ * by the backend, based on the callbacks and data defined in
+ * external_toast.h.
+ *
+ * Copyright (c) 2025, PostgreSQL Global Development Group
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/common/toast_external.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/detoast.h"
+#include "access/heaptoast.h"
+#include "access/toast_external.h"
+
+/* Callbacks for VARTAG_ONDISK_OID */
+static void ondisk_oid_to_external_data(struct varlena *attr,
+										toast_external_data *data);
+static struct varlena *ondisk_oid_create_external_data(toast_external_data data);
+
+/*
+ * Size of an EXTERNAL datum that contains a standard TOAST pointer (OID
+ * value).
+ */
+#define TOAST_OID_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_oid))
+
+/*
+ * For now there are only two types, all defined in this file.  For now this
+ * is the maximum value of vartag_external, which is a historical choice.
+ */
+#define TOAST_EXTERNAL_INFO_SIZE	(VARTAG_ONDISK_OID + 1)
+
+/*
+ * The different kinds of on-disk external TOAST pointers, divided by
+ * vartag_external.
+ *
+ * See comments for struct toast_external_info about the details of the
+ * individual fields.
+ */
+static const toast_external_info toast_external_infos[TOAST_EXTERNAL_INFO_SIZE] = {
+	[VARTAG_ONDISK_OID] = {
+		.toast_pointer_size = TOAST_OID_POINTER_SIZE,
+		.maximum_chunk_size = TOAST_OID_MAX_CHUNK_SIZE,
+		.to_external_data = ondisk_oid_to_external_data,
+		.create_external_data = ondisk_oid_create_external_data,
+	},
+};
+
+/*
+ * toast_external_get_info
+ *
+ * Get toast_external_info of the defined vartag_external, central set of
+ * callbacks, based on a "tag", which is a vartag_external value for an
+ * on-disk external varlena.
+ */
+const toast_external_info *
+toast_external_get_info(uint8 tag)
+{
+	const toast_external_info *res = &toast_external_infos[tag];
+
+	/* sanity check, as it could be possible that corrupted data is read */
+	if (res == NULL)
+		elog(ERROR, "incorrect value %u for toast_external_info", tag);
+	return res;
+}
+
+/*
+ * toast_external_info_get_pointer_size
+ *
+ * Get external TOAST pointer size based on the attribute type of a TOAST
+ * value.  "tag" is a vartag_external value.
+ */
+int32
+toast_external_info_get_pointer_size(uint8 tag)
+{
+	return toast_external_infos[tag].toast_pointer_size;
+}
+
+/*
+ * toast_external_assign_vartag
+ *
+ * Assign the vartag_external of a TOAST tuple, based on the TOAST relation
+ * it uses and its value.
+ *
+ * An invalid value can be given by the caller of this routine, in which
+ * case a default vartag should be provided based on only the toast relation
+ * used.
+ */
+uint8
+toast_external_assign_vartag(Oid toastrelid, Oid8 valueid)
+{
+	/*
+	 * If dealing with a code path where a TOAST relation may not be assigned,
+	 * like heap_toast_insert_or_update(), just use the legacy
+	 * vartag_external.
+	 */
+	if (!OidIsValid(toastrelid))
+		return VARTAG_ONDISK_OID;
+
+	/*
+	 * Currently there is only one type of vartag_external supported: 4-byte
+	 * value with OID for the chunk_id type.
+	 *
+	 * Note: This routine will be extended to be able to use multiple
+	 * vartag_external within a single TOAST relation type, that may change
+	 * depending on the value used.
+	 */
+	return VARTAG_ONDISK_OID;
+}
+
+/*
+ * Helper routines able to translate the various varatt_external_* from/to
+ * the in-memory representation toast_external_data used in the backend.
+ */
+
+/* Callbacks for VARTAG_ONDISK_OID */
+
+/*
+ * ondisk_oid_to_external_data
+ *
+ * Translate a varlena to its toast_external_data representation, to be used
+ * by the backend code.
+ */
+static void
+ondisk_oid_to_external_data(struct varlena *attr, toast_external_data *data)
+{
+	varatt_external_oid external;
+
+	VARATT_EXTERNAL_GET_POINTER(external, attr);
+	data->rawsize = external.va_rawsize;
+
+	/*
+	 * External size and compression methods are stored in the same field,
+	 * extract.
+	 */
+	if (VARATT_EXTERNAL_IS_COMPRESSED(external))
+	{
+		data->extsize = VARATT_EXTERNAL_GET_EXTSIZE(external);
+		data->compression_method = VARATT_EXTERNAL_GET_COMPRESS_METHOD(external);
+	}
+	else
+	{
+		data->extsize = external.va_extinfo;
+		data->compression_method = TOAST_INVALID_COMPRESSION_ID;
+	}
+
+	data->valueid = (Oid8) external.va_valueid;
+	data->toastrelid = external.va_toastrelid;
+}
+
+/*
+ * ondisk_oid_create_external_data
+ *
+ * Create a new varlena based on the input toast_external_data, to be used
+ * when saving a new TOAST value.
+ */
+static struct varlena *
+ondisk_oid_create_external_data(toast_external_data data)
+{
+	struct varlena *result = NULL;
+	varatt_external_oid external;
+
+	external.va_rawsize = data.rawsize;
+
+	if (data.compression_method != TOAST_INVALID_COMPRESSION_ID)
+	{
+		/* Set size and compression method, in a single field. */
+		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(external,
+													 data.extsize,
+													 data.compression_method);
+	}
+	else
+		external.va_extinfo = data.extsize;
+
+	external.va_toastrelid = data.toastrelid;
+	external.va_valueid = (Oid) data.valueid;
+
+	result = (struct varlena *) palloc(TOAST_OID_POINTER_SIZE);
+	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_OID);
+	memcpy(VARDATA_EXTERNAL(result), &external, sizeof(external));
+
+	return result;
+}
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 07df7edc47ce..5d0aa664fc91 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -18,6 +18,7 @@
 #include "access/heapam.h"
 #include "access/heaptoast.h"
 #include "access/table.h"
+#include "access/toast_external.h"
 #include "access/toast_internals.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -127,12 +128,12 @@ toast_save_datum(Relation rel, Datum value,
 	bool		t_isnull[3];
 	CommandId	mycid = GetCurrentCommandId(true);
 	struct varlena *result;
-	varatt_external_oid toast_pointer;
+	toast_external_data toast_pointer;
 	union
 	{
 		struct varlena hdr;
 		/* this is to make the union big enough for a chunk: */
-		char		data[TOAST_OID_MAX_CHUNK_SIZE + VARHDRSZ];
+		char		data[TOAST_MAX_CHUNK_SIZE + VARHDRSZ];
 		/* ensure union is aligned well enough: */
 		int32		align_it;
 	}			chunk_data;
@@ -143,6 +144,8 @@ toast_save_datum(Relation rel, Datum value,
 	Pointer		dval = DatumGetPointer(value);
 	int			num_indexes;
 	int			validIndex;
+	const toast_external_info *info;
+	uint8		tag = VARTAG_INDIRECT;	/* init value does not matter */
 
 	Assert(!VARATT_IS_EXTERNAL(dval));
 
@@ -174,28 +177,41 @@ toast_save_datum(Relation rel, Datum value,
 	{
 		data_p = VARDATA_SHORT(dval);
 		data_todo = VARSIZE_SHORT(dval) - VARHDRSZ_SHORT;
-		toast_pointer.va_rawsize = data_todo + VARHDRSZ;	/* as if not short */
-		toast_pointer.va_extinfo = data_todo;
+		toast_pointer.rawsize = data_todo + VARHDRSZ;	/* as if not short */
+		toast_pointer.extsize = data_todo;
+
+		/*
+		 * TOAST_INVALID_COMPRESSION_ID means that the varlena is not
+		 * compressed, see toast_get_compression_id().
+		 */
+		toast_pointer.compression_method = TOAST_INVALID_COMPRESSION_ID;
 	}
 	else if (VARATT_IS_COMPRESSED(dval))
 	{
 		data_p = VARDATA(dval);
 		data_todo = VARSIZE(dval) - VARHDRSZ;
 		/* rawsize in a compressed datum is just the size of the payload */
-		toast_pointer.va_rawsize = VARDATA_COMPRESSED_GET_EXTSIZE(dval) + VARHDRSZ;
+		toast_pointer.rawsize = VARDATA_COMPRESSED_GET_EXTSIZE(dval) + VARHDRSZ;
 
 		/* set external size and compression method */
-		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, data_todo,
-													 VARDATA_COMPRESSED_GET_COMPRESS_METHOD(dval));
+		toast_pointer.extsize = data_todo;
+		toast_pointer.compression_method = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(dval);
+
 		/* Assert that the numbers look like it's compressed */
-		Assert(VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer));
+		Assert(TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer));
 	}
 	else
 	{
 		data_p = VARDATA(dval);
 		data_todo = VARSIZE(dval) - VARHDRSZ;
-		toast_pointer.va_rawsize = VARSIZE(dval);
-		toast_pointer.va_extinfo = data_todo;
+		toast_pointer.rawsize = VARSIZE(dval);
+		toast_pointer.extsize = data_todo;
+
+		/*
+		 * TOAST_INVALID_COMPRESSION_ID means that the varlena is not
+		 * compressed, see toast_get_compression_id().
+		 */
+		toast_pointer.compression_method = TOAST_INVALID_COMPRESSION_ID;
 	}
 
 	/*
@@ -207,9 +223,9 @@ toast_save_datum(Relation rel, Datum value,
 	 * if we have to substitute such an OID.
 	 */
 	if (OidIsValid(rel->rd_toastoid))
-		toast_pointer.va_toastrelid = rel->rd_toastoid;
+		toast_pointer.toastrelid = rel->rd_toastoid;
 	else
-		toast_pointer.va_toastrelid = RelationGetRelid(toastrel);
+		toast_pointer.toastrelid = RelationGetRelid(toastrel);
 
 	/*
 	 * Choose an OID to use as the value ID for this toast value.
@@ -226,7 +242,7 @@ toast_save_datum(Relation rel, Datum value,
 	if (!OidIsValid(rel->rd_toastoid))
 	{
 		/* normal case: just choose an unused OID */
-		toast_pointer.va_valueid =
+		toast_pointer.valueid =
 			GetNewOidWithIndex(toastrel,
 							   RelationGetRelid(toastidxs[validIndex]),
 							   (AttrNumber) 1);
@@ -234,18 +250,18 @@ toast_save_datum(Relation rel, Datum value,
 	else
 	{
 		/* rewrite case: check to see if value was in old toast table */
-		toast_pointer.va_valueid = InvalidOid;
+		toast_pointer.valueid = InvalidOid8;
 		if (oldexternal != NULL)
 		{
-			varatt_external_oid old_toast_pointer;
+			toast_external_data old_toast_pointer;
 
 			Assert(VARATT_IS_EXTERNAL_ONDISK(oldexternal));
-			/* Must copy to access aligned fields */
-			VARATT_EXTERNAL_GET_POINTER(old_toast_pointer, oldexternal);
-			if (old_toast_pointer.va_toastrelid == rel->rd_toastoid)
+			toast_external_info_get_data(oldexternal, &old_toast_pointer);
+
+			if (old_toast_pointer.toastrelid == rel->rd_toastoid)
 			{
 				/* This value came from the old toast table; reuse its OID */
-				toast_pointer.va_valueid = old_toast_pointer.va_valueid;
+				toast_pointer.valueid = old_toast_pointer.valueid;
 
 				/*
 				 * There is a corner case here: the table rewrite might have
@@ -265,14 +281,14 @@ toast_save_datum(Relation rel, Datum value,
 				 * be reclaimed by VACUUM.
 				 */
 				if (toastrel_valueid_exists(toastrel,
-											toast_pointer.va_valueid))
+											toast_pointer.valueid))
 				{
 					/* Match, so short-circuit the data storage loop below */
 					data_todo = 0;
 				}
 			}
 		}
-		if (toast_pointer.va_valueid == InvalidOid)
+		if (toast_pointer.valueid == InvalidOid8)
 		{
 			/*
 			 * new value; must choose an OID that doesn't conflict in either
@@ -280,24 +296,32 @@ toast_save_datum(Relation rel, Datum value,
 			 */
 			do
 			{
-				toast_pointer.va_valueid =
+				toast_pointer.valueid =
 					GetNewOidWithIndex(toastrel,
 									   RelationGetRelid(toastidxs[validIndex]),
 									   (AttrNumber) 1);
 			} while (toastid_valueid_exists(rel->rd_toastoid,
-											toast_pointer.va_valueid));
+											toast_pointer.valueid));
 		}
 	}
 
 	/*
 	 * Initialize constant parts of the tuple data
 	 */
-	t_values[0] = ObjectIdGetDatum(toast_pointer.va_valueid);
+	t_values[0] = ObjectIdGetDatum(toast_pointer.valueid);
 	t_values[2] = PointerGetDatum(&chunk_data);
 	t_isnull[0] = false;
 	t_isnull[1] = false;
 	t_isnull[2] = false;
 
+	/*
+	 * Retrieve the vartag that can be assigned for the new TOAST tuple. This
+	 * depends on the type of TOAST table and its assigned value.
+	 */
+	tag = toast_external_assign_vartag(toast_pointer.toastrelid,
+									   toast_pointer.valueid);
+	info = toast_external_get_info(tag);
+
 	/*
 	 * Split up the item into chunks
 	 */
@@ -310,7 +334,7 @@ toast_save_datum(Relation rel, Datum value,
 		/*
 		 * Calculate the size of this chunk
 		 */
-		chunk_size = Min(TOAST_OID_MAX_CHUNK_SIZE, data_todo);
+		chunk_size = Min(info->maximum_chunk_size, data_todo);
 
 		/*
 		 * Build a tuple and store it
@@ -368,9 +392,7 @@ toast_save_datum(Relation rel, Datum value,
 	/*
 	 * Create the TOAST pointer value that we'll return
 	 */
-	result = (struct varlena *) palloc(TOAST_OID_POINTER_SIZE);
-	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_OID);
-	memcpy(VARDATA_EXTERNAL(result), &toast_pointer, sizeof(toast_pointer));
+	result = info->create_external_data(toast_pointer);
 
 	return PointerGetDatum(result);
 }
@@ -385,7 +407,7 @@ void
 toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 {
 	struct varlena *attr = (struct varlena *) DatumGetPointer(value);
-	varatt_external_oid toast_pointer;
+	toast_external_data toast_pointer;
 	Relation	toastrel;
 	Relation   *toastidxs;
 	ScanKeyData toastkey;
@@ -398,12 +420,12 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 		return;
 
 	/* Must copy to access aligned fields */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_external_info_get_data(attr, &toast_pointer);
 
 	/*
 	 * Open the toast relation and its indexes
 	 */
-	toastrel = table_open(toast_pointer.va_toastrelid, RowExclusiveLock);
+	toastrel = table_open(toast_pointer.toastrelid, RowExclusiveLock);
 
 	/* Fetch valid relation used for process */
 	validIndex = toast_open_indexes(toastrel,
@@ -417,7 +439,7 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	ScanKeyInit(&toastkey,
 				(AttrNumber) 1,
 				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(toast_pointer.va_valueid));
+				ObjectIdGetDatum(toast_pointer.valueid));
 
 	/*
 	 * Find all the chunks.  (We don't actually care whether we see them in
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index ddde7fcf79a4..230f2a6f35eb 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -28,6 +28,7 @@
 #include "access/genam.h"
 #include "access/heapam.h"
 #include "access/heaptoast.h"
+#include "access/toast_external.h"
 #include "access/toast_helper.h"
 #include "access/toast_internals.h"
 #include "utils/fmgroids.h"
@@ -109,6 +110,7 @@ heap_toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup,
 	Datum		toast_oldvalues[MaxHeapAttributeNumber];
 	ToastAttrInfo toast_attr[MaxHeapAttributeNumber];
 	ToastTupleContext ttc;
+	uint8		tag;
 
 	/*
 	 * Ignore the INSERT_SPECULATIVE option. Speculative insertions/super
@@ -140,6 +142,16 @@ heap_toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup,
 	 * Prepare for toasting
 	 * ----------
 	 */
+
+	/*
+	 * Retrieve the toast pointer size based on the type of external TOAST
+	 * pointer assumed to be used.
+	 */
+
+	/* The default value is invalid, to work as a default. */
+	tag = toast_external_assign_vartag(rel->rd_rel->reltoastrelid, InvalidOid8);
+	ttc.ttc_toast_pointer_size = toast_external_info_get_pointer_size(tag);
+
 	ttc.ttc_rel = rel;
 	ttc.ttc_values = toast_values;
 	ttc.ttc_isnull = toast_isnull;
@@ -640,6 +652,8 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 	int			num_indexes;
 	int			validIndex;
 	int32		max_chunk_size;
+	const toast_external_info *info;
+	uint8		tag = VARTAG_INDIRECT;	/* init value does not matter */
 
 	/* Look for the valid index of toast relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -647,7 +661,11 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 									&toastidxs,
 									&num_indexes);
 
-	max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
+	/* Grab the information for toast_external_data */
+	tag = toast_external_assign_vartag(RelationGetRelid(toastrel), valueid);
+	info = toast_external_get_info(tag);
+
+	max_chunk_size = info->maximum_chunk_size;
 
 	totalchunks = ((attrsize - 1) / max_chunk_size) + 1;
 	startchunk = sliceoffset / max_chunk_size;
diff --git a/src/backend/access/table/toast_helper.c b/src/backend/access/table/toast_helper.c
index 0c58c6c32565..76a7cfe6174e 100644
--- a/src/backend/access/table/toast_helper.c
+++ b/src/backend/access/table/toast_helper.c
@@ -171,8 +171,10 @@ toast_tuple_init(ToastTupleContext *ttc)
  * The column must have attstorage EXTERNAL or EXTENDED if check_main is
  * false, and must have attstorage MAIN if check_main is true.
  *
- * The column must have a minimum size of MAXALIGN(TOAST_OID_POINTER_SIZE);
- * if not, no benefit is to be expected by compressing it.
+ * The column must have a minimum size of MAXALIGN(tcc_toast_pointer_size);
+ * if not, no benefit is to be expected by compressing it.  The TOAST
+ * pointer size is given by the caller, depending on the type of TOAST
+ * table we are dealing with.
  *
  * The return value is the index of the biggest suitable column, or
  * -1 if there is none.
@@ -184,10 +186,14 @@ toast_tuple_find_biggest_attribute(ToastTupleContext *ttc,
 	TupleDesc	tupleDesc = ttc->ttc_rel->rd_att;
 	int			numAttrs = tupleDesc->natts;
 	int			biggest_attno = -1;
-	int32		biggest_size = MAXALIGN(TOAST_OID_POINTER_SIZE);
+	int32		biggest_size = 0;
 	int32		skip_colflags = TOASTCOL_IGNORE;
 	int			i;
 
+	/* Define the lower-bound */
+	biggest_size = MAXALIGN(ttc->ttc_toast_pointer_size);
+	Assert(biggest_size != 0);
+
 	if (for_compression)
 		skip_colflags |= TOASTCOL_INCOMPRESSIBLE;
 
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index fdc4f987f9ac..e8909406686d 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4387,7 +4387,7 @@ WriteControlFile(void)
 	ControlFile->nameDataLen = NAMEDATALEN;
 	ControlFile->indexMaxKeys = INDEX_MAX_KEYS;
 
-	ControlFile->toast_max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
+	ControlFile->toast_max_chunk_size = TOAST_MAX_CHUNK_SIZE;
 	ControlFile->loblksize = LOBLKSIZE;
 
 	ControlFile->float8ByVal = true;	/* vestigial */
@@ -4630,15 +4630,15 @@ ReadControlFile(void)
 						   "INDEX_MAX_KEYS", ControlFile->indexMaxKeys,
 						   "INDEX_MAX_KEYS", INDEX_MAX_KEYS),
 				 errhint("It looks like you need to recompile or initdb.")));
-	if (ControlFile->toast_max_chunk_size != TOAST_OID_MAX_CHUNK_SIZE)
+	if (ControlFile->toast_max_chunk_size != TOAST_MAX_CHUNK_SIZE)
 		ereport(FATAL,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("database files are incompatible with server"),
 		/* translator: %s is a variable name and %d is its value */
 				 errdetail("The database cluster was initialized with %s %d,"
 						   " but the server was compiled with %s %d.",
-						   "TOAST_OID_MAX_CHUNK_SIZE", ControlFile->toast_max_chunk_size,
-						   "TOAST_OID_MAX_CHUNK_SIZE", (int) TOAST_OID_MAX_CHUNK_SIZE),
+						   "TOAST_MAX_CHUNK_SIZE", ControlFile->toast_max_chunk_size,
+						   "TOAST_MAX_CHUNK_SIZE", (int) TOAST_MAX_CHUNK_SIZE),
 				 errhint("It looks like you need to recompile or initdb.")));
 	if (ControlFile->loblksize != LOBLKSIZE)
 		ereport(FATAL,
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 29d594375de0..9a690a59db2c 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -92,6 +92,7 @@
 #include "access/detoast.h"
 #include "access/heapam.h"
 #include "access/rewriteheap.h"
+#include "access/toast_external.h"
 #include "access/transam.h"
 #include "access/xact.h"
 #include "access/xlog_internal.h"
@@ -5102,7 +5103,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		struct varlena *varlena;
 
 		/* va_rawsize is the size of the original datum -- including header */
-		varatt_external_oid toast_pointer;
+		toast_external_data toast_pointer;
 		struct varatt_indirect redirect_pointer;
 		struct varlena *new_datum = NULL;
 		struct varlena *reconstructed;
@@ -5132,8 +5133,8 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		if (!VARATT_IS_EXTERNAL(varlena))
 			continue;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, varlena);
-		toast_valueid = toast_pointer.va_valueid;
+		toast_external_info_get_data(varlena, &toast_pointer);
+		toast_valueid = toast_pointer.valueid;
 
 		/*
 		 * Check whether the toast tuple changed, replace if so.
@@ -5151,7 +5152,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 
 		free[natt] = true;
 
-		reconstructed = palloc0(toast_pointer.va_rawsize);
+		reconstructed = palloc0(toast_pointer.rawsize);
 
 		ent->reconstructed = reconstructed;
 
@@ -5176,10 +5177,10 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 				   VARSIZE(chunk) - VARHDRSZ);
 			data_done += VARSIZE(chunk) - VARHDRSZ;
 		}
-		Assert(data_done == VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer));
+		Assert(data_done == toast_pointer.extsize);
 
 		/* make sure its marked as compressed or not */
-		if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+		if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 			SET_VARSIZE_COMPRESSED(reconstructed, data_done + VARHDRSZ);
 		else
 			SET_VARSIZE(reconstructed, data_done + VARHDRSZ);
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 4aff647fccfd..78b3e65b2396 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -19,6 +19,7 @@
 
 #include "access/detoast.h"
 #include "access/toast_compression.h"
+#include "access/toast_external.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_type.h"
 #include "common/hashfn.h"
@@ -4211,7 +4212,7 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 {
 	int			typlen;
 	struct varlena *attr;
-	varatt_external_oid toast_pointer;
+	Oid8		toast_valueid;
 
 	/* On first call, get the input type's typlen, and save at *fn_extra */
 	if (fcinfo->flinfo->fn_extra == NULL)
@@ -4238,9 +4239,9 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		PG_RETURN_NULL();
 
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_valueid = toast_external_info_get_valueid(attr);
 
-	PG_RETURN_OID(toast_pointer.va_valueid);
+	PG_RETURN_OID(toast_valueid);
 }
 
 /*
diff --git a/src/bin/pg_resetwal/pg_resetwal.c b/src/bin/pg_resetwal/pg_resetwal.c
index 638b41c922ba..7a4e4eb95706 100644
--- a/src/bin/pg_resetwal/pg_resetwal.c
+++ b/src/bin/pg_resetwal/pg_resetwal.c
@@ -717,7 +717,7 @@ GuessControlValues(void)
 	ControlFile.xlog_seg_size = DEFAULT_XLOG_SEG_SIZE;
 	ControlFile.nameDataLen = NAMEDATALEN;
 	ControlFile.indexMaxKeys = INDEX_MAX_KEYS;
-	ControlFile.toast_max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
+	ControlFile.toast_max_chunk_size = TOAST_MAX_CHUNK_SIZE;
 	ControlFile.loblksize = LOBLKSIZE;
 	ControlFile.float8ByVal = true; /* vestigial */
 
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 7ec6cef118fb..9cf3c081bf01 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -16,6 +16,7 @@
 #include "access/multixact.h"
 #include "access/relation.h"
 #include "access/table.h"
+#include "access/toast_external.h"
 #include "access/toast_internals.h"
 #include "access/visibilitymap.h"
 #include "access/xact.h"
@@ -73,7 +74,8 @@ typedef enum SkipPages
  */
 typedef struct ToastedAttribute
 {
-	varatt_external_oid toast_pointer;
+	toast_external_data toast_pointer;
+	const toast_external_info *info;
 	BlockNumber blkno;			/* block in main table */
 	OffsetNumber offnum;		/* offset in main table */
 	AttrNumber	attnum;			/* attribute in main table */
@@ -1564,9 +1566,9 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	Oid8		toast_valueid;
 	int32		max_chunk_size;
 
-	toast_valueid = ta->toast_pointer.va_valueid;
+	toast_valueid = ta->toast_pointer.valueid;
 
-	max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
+	max_chunk_size = ta->info->maximum_chunk_size;
 	last_chunk_seq = (extsize - 1) / max_chunk_size;
 
 	/* Sanity-check the sequence number. */
@@ -1672,7 +1674,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	uint16		infomask;
 	Oid8		toast_pointer_valueid;
 	CompactAttribute *thisatt;
-	varatt_external_oid toast_pointer;
+	toast_external_data toast_pointer;
 
 	infomask = ctx->tuphdr->t_infomask;
 	thisatt = TupleDescCompactAttr(RelationGetDescr(ctx->rel), ctx->attnum);
@@ -1778,24 +1780,24 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	/*
 	 * Must copy attr into toast_pointer for alignment considerations
 	 */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-	toast_pointer_valueid = toast_pointer.va_valueid;
+	toast_external_info_get_data(attr, &toast_pointer);
+	toast_pointer_valueid = toast_pointer.valueid;
 
 	/* Toasted attributes too large to be untoasted should never be stored */
-	if (toast_pointer.va_rawsize > VARLENA_SIZE_LIMIT)
+	if (toast_pointer.rawsize > VARLENA_SIZE_LIMIT)
 		report_corruption(ctx,
 						  psprintf("toast value " OID8_FORMAT " rawsize %d exceeds limit %d",
 								   toast_pointer_valueid,
-								   toast_pointer.va_rawsize,
+								   toast_pointer.rawsize,
 								   VARLENA_SIZE_LIMIT));
 
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+	if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 	{
 		ToastCompressionId cmid;
 		bool		valid = false;
 
 		/* Compressed attributes should have a valid compression method */
-		cmid = TOAST_COMPRESS_METHOD(&toast_pointer);
+		cmid = toast_pointer.compression_method;
 		switch (cmid)
 		{
 				/* List of all valid compression method IDs */
@@ -1849,7 +1851,8 @@ check_tuple_attribute(HeapCheckContext *ctx)
 
 		ta = (ToastedAttribute *) palloc0(sizeof(ToastedAttribute));
 
-		VARATT_EXTERNAL_GET_POINTER(ta->toast_pointer, attr);
+		toast_external_info_get_data(attr, &ta->toast_pointer);
+		ta->info = toast_external_get_info(VARTAG_EXTERNAL(attr));
 		ta->blkno = ctx->blkno;
 		ta->offnum = ctx->offnum;
 		ta->attnum = ctx->attnum;
@@ -1876,9 +1879,11 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	int32		expected_chunk_seq = 0;
 	int32		last_chunk_seq;
 	Oid8		toast_valueid;
-	int32		max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
+	int32		max_chunk_size;
 
-	extsize = VARATT_EXTERNAL_GET_EXTSIZE(ta->toast_pointer);
+	extsize = ta->toast_pointer.extsize;
+
+	max_chunk_size = ta->info->maximum_chunk_size;
 	last_chunk_seq = (extsize - 1) / max_chunk_size;
 
 	/*
@@ -1887,7 +1892,7 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	ScanKeyInit(&toastkey,
 				(AttrNumber) 1,
 				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(ta->toast_pointer.va_valueid));
+				ObjectIdGetDatum(ta->toast_pointer.valueid));
 
 	/*
 	 * Check if any chunks for this toasted object exist in the toast table,
@@ -1907,7 +1912,7 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	}
 	systable_endscan_ordered(toastscan);
 
-	toast_valueid = ta->toast_pointer.va_valueid;
+	toast_valueid = ta->toast_pointer.valueid;
 
 	if (!found_toasttup)
 		report_toast_corruption(ctx, ta,
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index e6f2e93b2d6f..995dc1f28208 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -4143,6 +4143,8 @@ timeout_params
 timerCA
 tlist_vinfo
 toast_compress_header
+toast_external_data
+toast_external_info
 tokenize_error_callback_arg
 transferMode
 transfer_thread_arg
-- 
2.50.0

v5-0006-Move-static-inline-routines-of-varatt_external_oi.patchtext/x-diff; charset=us-asciiDownload
From d3a04843fd969d3e15b2f950f14a57a8b74716ab Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 8 Aug 2025 15:40:04 +0900
Subject: [PATCH v5 06/15] Move static inline routines of varatt_external_oid
 to toast_external.c

This isolates most of the knowledge of varatt_external_oid into the
local area where it is manipulated through the toast_external transition
type, with the backend code not requiring it.  Extension code should not
need it either, as toast_external should be the layer to use when
looking at external on-dist TOAST varlenas.
---
 src/include/varatt.h                       | 31 -----------------
 src/backend/access/common/toast_external.c | 40 ++++++++++++++++++++--
 2 files changed, 37 insertions(+), 34 deletions(-)

diff --git a/src/include/varatt.h b/src/include/varatt.h
index 790d9f844c91..035c0f95e5b6 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -513,22 +513,6 @@ VARDATA_COMPRESSED_GET_COMPRESS_METHOD(const void *PTR)
 	return ((varattrib_4b *) PTR)->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS;
 }
 
-/*
- * Same for external Datums; but note argument is a struct
- * varatt_external_oid.
- */
-static inline Size
-VARATT_EXTERNAL_GET_EXTSIZE(varatt_external_oid toast_pointer)
-{
-	return toast_pointer.va_extinfo & VARLENA_EXTSIZE_MASK;
-}
-
-static inline uint32
-VARATT_EXTERNAL_GET_COMPRESS_METHOD(varatt_external_oid toast_pointer)
-{
-	return toast_pointer.va_extinfo >> VARLENA_EXTSIZE_BITS;
-}
-
 /* Set size and compress method of an externally-stored varlena datum */
 /* This has to remain a macro; beware multiple evaluations! */
 #define VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, len, cm) \
@@ -538,19 +522,4 @@ VARATT_EXTERNAL_GET_COMPRESS_METHOD(varatt_external_oid toast_pointer)
 		((toast_pointer).va_extinfo = \
 			(len) | ((uint32) (cm) << VARLENA_EXTSIZE_BITS)); \
 	} while (0)
-
-/*
- * Testing whether an externally-stored value is compressed now requires
- * comparing size stored in va_extinfo (the actual length of the external data)
- * to rawsize (the original uncompressed datum's size).  The latter includes
- * VARHDRSZ overhead, the former doesn't.  We never use compression unless it
- * actually saves space, so we expect either equality or less-than.
- */
-static inline bool
-VARATT_EXTERNAL_IS_COMPRESSED(varatt_external_oid toast_pointer)
-{
-	return VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) <
-		(Size) (toast_pointer.va_rawsize - VARHDRSZ);
-}
-
 #endif
diff --git a/src/backend/access/common/toast_external.c b/src/backend/access/common/toast_external.c
index 5c8679a0f485..d7bf2f7c69b2 100644
--- a/src/backend/access/common/toast_external.c
+++ b/src/backend/access/common/toast_external.c
@@ -26,6 +26,40 @@ static void ondisk_oid_to_external_data(struct varlena *attr,
 										toast_external_data *data);
 static struct varlena *ondisk_oid_create_external_data(toast_external_data data);
 
+/*
+ * Decompressed size of an on-disk varlena; but note argument is a struct
+ * varatt_external_oid.
+ */
+static inline Size
+varatt_external_oid_get_extsize(varatt_external_oid toast_pointer)
+{
+	return toast_pointer.va_extinfo & VARLENA_EXTSIZE_MASK;
+}
+
+/*
+ * Compression method of an on-disk varlena; but note argument is a struct
+ *  varatt_external_oid.
+ */
+static inline uint32
+varatt_external_oid_get_compress_method(varatt_external_oid toast_pointer)
+{
+	return toast_pointer.va_extinfo >> VARLENA_EXTSIZE_BITS;
+}
+
+/*
+ * Testing whether an externally-stored TOAST value is compressed now requires
+ * comparing size stored in va_extinfo (the actual length of the external data)
+ * to rawsize (the original uncompressed datum's size).  The latter includes
+ * VARHDRSZ overhead, the former doesn't.  We never use compression unless it
+ * actually saves space, so we expect either equality or less-than.
+ */
+static inline bool
+varatt_external_oid_is_compressed(varatt_external_oid toast_pointer)
+{
+	return varatt_external_oid_get_extsize(toast_pointer) <
+		(Size) (toast_pointer.va_rawsize - VARHDRSZ);
+}
+
 /*
  * Size of an EXTERNAL datum that contains a standard TOAST pointer (OID
  * value).
@@ -141,10 +175,10 @@ ondisk_oid_to_external_data(struct varlena *attr, toast_external_data *data)
 	 * External size and compression methods are stored in the same field,
 	 * extract.
 	 */
-	if (VARATT_EXTERNAL_IS_COMPRESSED(external))
+	if (varatt_external_oid_is_compressed(external))
 	{
-		data->extsize = VARATT_EXTERNAL_GET_EXTSIZE(external);
-		data->compression_method = VARATT_EXTERNAL_GET_COMPRESS_METHOD(external);
+		data->extsize = varatt_external_oid_get_extsize(external);
+		data->compression_method = varatt_external_oid_get_compress_method(external);
 	}
 	else
 	{
-- 
2.50.0

v5-0007-Split-VARATT_EXTERNAL_GET_POINTER-for-indirect-an.patchtext/x-diff; charset=us-asciiDownload
From 5fecf226bc1a44bf2240199f7bd7c13fc9f3cc16 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 13 Aug 2025 19:38:50 +0900
Subject: [PATCH v5 07/15] Split VARATT_EXTERNAL_GET_POINTER for indirect and
 OID TOAST pointers

VARATT_EXTERNAL_GET_POINTER() is renamed to
VARATT_INDIRECT_GET_POINTER() with the external on-disk TOAST pointers
for OID values being now located within toast_external.c, splitting both
concepts completely.
---
 src/include/access/detoast.h               | 16 ++++++++--------
 src/backend/access/common/detoast.c        | 10 +++++-----
 src/backend/access/common/toast_external.c | 21 ++++++++++++++++++++-
 src/backend/utils/adt/expandeddatum.c      |  2 +-
 4 files changed, 34 insertions(+), 15 deletions(-)

diff --git a/src/include/access/detoast.h b/src/include/access/detoast.h
index b3ebad8b7cf9..31e9786848ef 100644
--- a/src/include/access/detoast.h
+++ b/src/include/access/detoast.h
@@ -13,17 +13,17 @@
 #define DETOAST_H
 
 /*
- * Macro to fetch the possibly-unaligned contents of an EXTERNAL datum
- * into a local "varatt_external_*" toast pointer, as supported
- * in toast_external.c and varatt.h.  This should be just a memcpy, but
- * some versions of gcc seem to produce broken code that assumes the datum
- * contents are aligned.  Introducing an explicit intermediate
- * "varattrib_1b_e *" variable seems to fix it.
+ * Macro to fetch the possibly-unaligned contents of an indirect datum
+ * into a local "varatt_indirect" toast pointer, as supported
+ * in varatt.h.  This should be just a memcpy, but some versions of gcc
+ * seem to produce broken code that assumes the datum contents are aligned.
+ * Introducing an explicit intermediate "varattrib_1b_e *" variable seems
+ * to fix it.
  */
-#define VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr) \
+#define VARATT_INDIRECT_GET_POINTER(toast_pointer, attr) \
 do { \
 	varattrib_1b_e *attre = (varattrib_1b_e *) (attr); \
-	Assert(VARATT_IS_EXTERNAL(attre)); \
+	Assert(VARATT_IS_EXTERNAL_INDIRECT(attre)); \
 	Assert(VARSIZE_EXTERNAL(attre) == sizeof(toast_pointer) + VARHDRSZ_EXTERNAL); \
 	memcpy(&(toast_pointer), VARDATA_EXTERNAL(attre), sizeof(toast_pointer)); \
 } while (0)
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 8531c27439e4..b645988667f0 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -61,7 +61,7 @@ detoast_external_attr(struct varlena *attr)
 		 */
 		struct varatt_indirect redirect;
 
-		VARATT_EXTERNAL_GET_POINTER(redirect, attr);
+		VARATT_INDIRECT_GET_POINTER(redirect, attr);
 		attr = (struct varlena *) redirect.pointer;
 
 		/* nested indirect Datums aren't allowed */
@@ -138,7 +138,7 @@ detoast_attr(struct varlena *attr)
 		 */
 		struct varatt_indirect redirect;
 
-		VARATT_EXTERNAL_GET_POINTER(redirect, attr);
+		VARATT_INDIRECT_GET_POINTER(redirect, attr);
 		attr = (struct varlena *) redirect.pointer;
 
 		/* nested indirect Datums aren't allowed */
@@ -268,7 +268,7 @@ detoast_attr_slice(struct varlena *attr,
 	{
 		struct varatt_indirect redirect;
 
-		VARATT_EXTERNAL_GET_POINTER(redirect, attr);
+		VARATT_INDIRECT_GET_POINTER(redirect, attr);
 
 		/* nested indirect Datums aren't allowed */
 		Assert(!VARATT_IS_EXTERNAL_INDIRECT(redirect.pointer));
@@ -561,7 +561,7 @@ toast_raw_datum_size(Datum value)
 	{
 		struct varatt_indirect toast_pointer;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		VARATT_INDIRECT_GET_POINTER(toast_pointer, attr);
 
 		/* nested indirect Datums aren't allowed */
 		Assert(!VARATT_IS_EXTERNAL_INDIRECT(toast_pointer.pointer));
@@ -618,7 +618,7 @@ toast_datum_size(Datum value)
 	{
 		struct varatt_indirect toast_pointer;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		VARATT_INDIRECT_GET_POINTER(toast_pointer, attr);
 
 		/* nested indirect Datums aren't allowed */
 		Assert(!VARATT_IS_EXTERNAL_INDIRECT(attr));
diff --git a/src/backend/access/common/toast_external.c b/src/backend/access/common/toast_external.c
index d7bf2f7c69b2..8f58195051cf 100644
--- a/src/backend/access/common/toast_external.c
+++ b/src/backend/access/common/toast_external.c
@@ -26,6 +26,25 @@ static void ondisk_oid_to_external_data(struct varlena *attr,
 										toast_external_data *data);
 static struct varlena *ondisk_oid_create_external_data(toast_external_data data);
 
+/*
+ * Fetch the possibly-unaligned contents of an on-disk external TOAST with
+ * OID values into a local "varatt_external_oid" pointer.
+ *
+ * This should be just a memcpy, but some versions of gcc seem to produce
+ * broken code that assumes the datum contents are aligned.  Introducing
+ * an explicit intermediate "varattrib_1b_e *" variable seems to fix it.
+ */
+static inline void
+varatt_external_oid_get_pointer(varatt_external_oid *toast_pointer,
+								struct varlena *attr)
+{
+	varattrib_1b_e *attre = (varattrib_1b_e *) attr;
+
+	Assert(VARATT_IS_EXTERNAL_ONDISK_OID(attre));
+	Assert(VARSIZE_EXTERNAL(attre) == sizeof(varatt_external_oid) + VARHDRSZ_EXTERNAL);
+	memcpy(toast_pointer, VARDATA_EXTERNAL(attre), sizeof(varatt_external_oid));
+}
+
 /*
  * Decompressed size of an on-disk varlena; but note argument is a struct
  * varatt_external_oid.
@@ -168,7 +187,7 @@ ondisk_oid_to_external_data(struct varlena *attr, toast_external_data *data)
 {
 	varatt_external_oid external;
 
-	VARATT_EXTERNAL_GET_POINTER(external, attr);
+	varatt_external_oid_get_pointer(&external, attr);
 	data->rawsize = external.va_rawsize;
 
 	/*
diff --git a/src/backend/utils/adt/expandeddatum.c b/src/backend/utils/adt/expandeddatum.c
index 6b4b8eaf005c..4c04671d23ed 100644
--- a/src/backend/utils/adt/expandeddatum.c
+++ b/src/backend/utils/adt/expandeddatum.c
@@ -23,7 +23,7 @@
  * Given a Datum that is an expanded-object reference, extract the pointer.
  *
  * This is a bit tedious since the pointer may not be properly aligned;
- * compare VARATT_EXTERNAL_GET_POINTER().
+ * compare VARATT_INDIRECT_GET_POINTER().
  */
 ExpandedObjectHeader *
 DatumGetEOHP(Datum d)
-- 
2.50.0

v5-0008-Switch-pg_column_toast_chunk_id-return-value-from.patchtext/x-diff; charset=us-asciiDownload
From aaba280fbfb28a87b24145aad7c1761408720cfa Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 13 Aug 2025 19:55:39 +0900
Subject: [PATCH v5 08/15] Switch pg_column_toast_chunk_id() return value from
 oid to oid8

This is required for a follow-up patch that will add support for 8-byte
TOAST values, with this function being changed so as it is able to
support the largest TOAST value type available.

XXX: Bump catalog version.
---
 src/include/catalog/pg_proc.dat              | 2 +-
 src/backend/utils/adt/varlena.c              | 2 +-
 src/test/regress/expected/misc_functions.out | 2 +-
 src/test/regress/sql/misc_functions.sql      | 2 +-
 doc/src/sgml/func/func-admin.sgml            | 2 +-
 5 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index b78508e83b9b..49b830de2099 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7744,7 +7744,7 @@
   proargtypes => 'any', prosrc => 'pg_column_compression' },
 { oid => '6316', descr => 'chunk ID of on-disk TOASTed value',
   proname => 'pg_column_toast_chunk_id', provolatile => 's',
-  prorettype => 'oid', proargtypes => 'any',
+  prorettype => 'oid8', proargtypes => 'any',
   prosrc => 'pg_column_toast_chunk_id' },
 { oid => '2322',
   descr => 'total disk space usage for the specified tablespace',
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 78b3e65b2396..a176a4fab0e5 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -4241,7 +4241,7 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 
 	toast_valueid = toast_external_info_get_valueid(attr);
 
-	PG_RETURN_OID(toast_valueid);
+	PG_RETURN_OID8(toast_valueid);
 }
 
 /*
diff --git a/src/test/regress/expected/misc_functions.out b/src/test/regress/expected/misc_functions.out
index c3b2b9d86034..9fa9e3466761 100644
--- a/src/test/regress/expected/misc_functions.out
+++ b/src/test/regress/expected/misc_functions.out
@@ -881,7 +881,7 @@ SELECT t.relname AS toastrel FROM pg_class c
   WHERE c.relname = 'test_chunk_id'
 \gset
 SELECT pg_column_toast_chunk_id(a) IS NULL,
-  pg_column_toast_chunk_id(b) IN (SELECT chunk_id FROM pg_toast.:toastrel)
+  pg_column_toast_chunk_id(b) IN (SELECT chunk_id::oid8 FROM pg_toast.:toastrel)
   FROM test_chunk_id;
  ?column? | ?column? 
 ----------+----------
diff --git a/src/test/regress/sql/misc_functions.sql b/src/test/regress/sql/misc_functions.sql
index 23792c4132a1..5fb79f315e37 100644
--- a/src/test/regress/sql/misc_functions.sql
+++ b/src/test/regress/sql/misc_functions.sql
@@ -395,7 +395,7 @@ SELECT t.relname AS toastrel FROM pg_class c
   WHERE c.relname = 'test_chunk_id'
 \gset
 SELECT pg_column_toast_chunk_id(a) IS NULL,
-  pg_column_toast_chunk_id(b) IN (SELECT chunk_id FROM pg_toast.:toastrel)
+  pg_column_toast_chunk_id(b) IN (SELECT chunk_id::oid8 FROM pg_toast.:toastrel)
   FROM test_chunk_id;
 DROP TABLE test_chunk_id;
 DROP FUNCTION explain_mask_costs(text, bool, bool, bool, bool);
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 446fdfe56f4f..160a48f05ee3 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1571,7 +1571,7 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
          <primary>pg_column_toast_chunk_id</primary>
         </indexterm>
         <function>pg_column_toast_chunk_id</function> ( <type>"any"</type> )
-        <returnvalue>oid</returnvalue>
+        <returnvalue>oid8</returnvalue>
        </para>
        <para>
         Shows the <structfield>chunk_id</structfield> of an on-disk
-- 
2.50.0

v5-0009-Add-catcache-support-for-OID8OID.patchtext/x-diff; charset=us-asciiDownload
From 54ed8c93b392aeb8a86b7612853554d46948f3d0 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 13 Aug 2025 20:00:30 +0900
Subject: [PATCH v5 09/15] Add catcache support for OID8OID

This is required to be able to do catalog cache lookups of oid8 fields
for toast values of the same type.
---
 src/backend/utils/cache/catcache.c | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index e2cd3feaf81d..fee2e67c7830 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -240,6 +240,18 @@ int4hashfast(Datum datum)
 	return murmurhash32((int32) DatumGetInt32(datum));
 }
 
+static bool
+oid8eqfast(Datum a, Datum b)
+{
+	return DatumGetObjectId8(a) == DatumGetObjectId8(b);
+}
+
+static uint32
+oid8hashfast(Datum datum)
+{
+	return murmurhash64(DatumGetObjectId8(datum));
+}
+
 static bool
 texteqfast(Datum a, Datum b)
 {
@@ -300,6 +312,11 @@ GetCCHashEqFuncs(Oid keytype, CCHashFN *hashfunc, RegProcedure *eqfunc, CCFastEq
 			*fasteqfunc = int4eqfast;
 			*eqfunc = F_INT4EQ;
 			break;
+		case OID8OID:
+			*hashfunc = oid8hashfast;
+			*fasteqfunc = oid8eqfast;
+			*eqfunc = F_OID8EQ;
+			break;
 		case TEXTOID:
 			*hashfunc = texthashfast;
 			*fasteqfunc = texteqfast;
-- 
2.50.0

v5-0010-Add-support-for-TOAST-chunk_id-type-in-binary-upg.patchtext/x-diff; charset=us-asciiDownload
From 379820592d0d7df18adf7ec18589a828bdefb07b Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 10:57:59 +0900
Subject: [PATCH v5 10/15] Add support for TOAST chunk_id type in binary
 upgrades

This commit adds a new function, which would set the type of a chunk_id
attribute for a TOAST table across upgrades.  This piece currently works
only with chunk_id = OIDOID, but it is required in a follow-up patch
where support for chunk_id = OID8OID is supported on top of the existing
one.
---
 src/include/catalog/binary_upgrade.h          |  1 +
 src/include/catalog/pg_proc.dat               |  4 ++++
 src/backend/catalog/heap.c                    |  1 +
 src/backend/catalog/toasting.c                | 20 ++++++++++++++++++-
 src/backend/utils/adt/pg_upgrade_support.c    | 11 ++++++++++
 src/bin/pg_dump/pg_dump.c                     | 10 +++++++++-
 .../expected/spgist_name_ops.out              |  6 ++++--
 7 files changed, 49 insertions(+), 4 deletions(-)

diff --git a/src/include/catalog/binary_upgrade.h b/src/include/catalog/binary_upgrade.h
index 6fcc59edebd8..3deb0423d795 100644
--- a/src/include/catalog/binary_upgrade.h
+++ b/src/include/catalog/binary_upgrade.h
@@ -29,6 +29,7 @@ extern PGDLLIMPORT Oid binary_upgrade_next_index_pg_class_oid;
 extern PGDLLIMPORT RelFileNumber binary_upgrade_next_index_pg_class_relfilenumber;
 extern PGDLLIMPORT Oid binary_upgrade_next_toast_pg_class_oid;
 extern PGDLLIMPORT RelFileNumber binary_upgrade_next_toast_pg_class_relfilenumber;
+extern PGDLLIMPORT Oid binary_upgrade_next_toast_chunk_id_typoid;
 
 extern PGDLLIMPORT Oid binary_upgrade_next_pg_enum_oid;
 extern PGDLLIMPORT Oid binary_upgrade_next_pg_authid_oid;
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 49b830de2099..183ef6fd6ac2 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11759,6 +11759,10 @@
   proname => 'binary_upgrade_set_next_toast_pg_class_oid', provolatile => 'v',
   proparallel => 'r', prorettype => 'void', proargtypes => 'oid',
   prosrc => 'binary_upgrade_set_next_toast_pg_class_oid' },
+{ oid => '8219', descr => 'for use by pg_upgrade',
+  proname => 'binary_upgrade_set_next_toast_chunk_id_typoid', provolatile => 'v',
+  proparallel => 'r', prorettype => 'void', proargtypes => 'oid',
+  prosrc => 'binary_upgrade_set_next_toast_chunk_id_typoid' },
 { oid => '3589', descr => 'for use by pg_upgrade',
   proname => 'binary_upgrade_set_next_pg_enum_oid', provolatile => 'v',
   proparallel => 'r', prorettype => 'void', proargtypes => 'oid',
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index fd6537567ea2..e5cc98937055 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -80,6 +80,7 @@
 /* Potentially set by pg_upgrade_support functions */
 Oid			binary_upgrade_next_heap_pg_class_oid = InvalidOid;
 Oid			binary_upgrade_next_toast_pg_class_oid = InvalidOid;
+Oid			binary_upgrade_next_toast_chunk_id_typoid = InvalidOid;
 RelFileNumber binary_upgrade_next_heap_pg_class_relfilenumber = InvalidRelFileNumber;
 RelFileNumber binary_upgrade_next_toast_pg_class_relfilenumber = InvalidRelFileNumber;
 
diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c
index 874a8fc89adb..f1d76d8acd51 100644
--- a/src/backend/catalog/toasting.c
+++ b/src/backend/catalog/toasting.c
@@ -145,6 +145,7 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	int16		coloptions[2];
 	ObjectAddress baseobject,
 				toastobject;
+	Oid			toast_chunkid_typid = OIDOID;
 
 	/*
 	 * Is it already toasted?
@@ -183,6 +184,23 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 		 */
 		if (!OidIsValid(binary_upgrade_next_toast_pg_class_oid))
 			return false;
+
+		/*
+		 * The attribute type for chunk_id should have been set when requesting
+		 * a TOAST table creation.
+		 */
+		if (!OidIsValid(binary_upgrade_next_toast_chunk_id_typoid))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("toast chunk_id type not set while in binary upgrade mode")));
+		if (binary_upgrade_next_toast_chunk_id_typoid != OIDOID)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("cannot support toast chunk_id type %u in binary upgrade mode",
+							binary_upgrade_next_toast_chunk_id_typoid)));
+
+		toast_chunkid_typid = binary_upgrade_next_toast_chunk_id_typoid;
+		binary_upgrade_next_toast_chunk_id_typoid = InvalidOid;
 	}
 
 	/*
@@ -204,7 +222,7 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	tupdesc = CreateTemplateTupleDesc(3);
 	TupleDescInitEntry(tupdesc, (AttrNumber) 1,
 					   "chunk_id",
-					   OIDOID,
+					   toast_chunkid_typid,
 					   -1, 0);
 	TupleDescInitEntry(tupdesc, (AttrNumber) 2,
 					   "chunk_seq",
diff --git a/src/backend/utils/adt/pg_upgrade_support.c b/src/backend/utils/adt/pg_upgrade_support.c
index a4f8b4faa90d..200ffcdbab44 100644
--- a/src/backend/utils/adt/pg_upgrade_support.c
+++ b/src/backend/utils/adt/pg_upgrade_support.c
@@ -149,6 +149,17 @@ binary_upgrade_set_next_toast_pg_class_oid(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+Datum
+binary_upgrade_set_next_toast_chunk_id_typoid(PG_FUNCTION_ARGS)
+{
+	Oid			typoid = PG_GETARG_OID(0);
+
+	CHECK_IS_BINARY_UPGRADE;
+	binary_upgrade_next_toast_chunk_id_typoid = typoid;
+
+	PG_RETURN_VOID();
+}
+
 Datum
 binary_upgrade_set_next_toast_relfilenode(PG_FUNCTION_ARGS)
 {
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index fc7a66391633..ef2f6c6bf332 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -103,6 +103,7 @@ typedef struct
 	RelFileNumber relfilenumber;	/* object filenode */
 	Oid			toast_oid;		/* toast table OID */
 	RelFileNumber toast_relfilenumber;	/* toast table filenode */
+	Oid			toast_chunk_id_typoid;	/* type of chunk_id attribute */
 	Oid			toast_index_oid;	/* toast table index OID */
 	RelFileNumber toast_index_relfilenumber;	/* toast table index filenode */
 } BinaryUpgradeClassOidItem;
@@ -5731,7 +5732,10 @@ collectBinaryUpgradeClassOids(Archive *fout)
 	const char *query;
 
 	query = "SELECT c.oid, c.relkind, c.relfilenode, c.reltoastrelid, "
-		"ct.relfilenode, i.indexrelid, cti.relfilenode "
+		"ct.relfilenode, i.indexrelid, cti.relfilenode, "
+		"(SELECT a.atttypid FROM pg_attribute AS a "
+		"  WHERE a.attrelid = c.reltoastrelid AND attname = 'chunk_id'::text) "
+		"  AS toastchunktypid "
 		"FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_index i "
 		"ON (c.reltoastrelid = i.indrelid AND i.indisvalid) "
 		"LEFT JOIN pg_catalog.pg_class ct ON (c.reltoastrelid = ct.oid) "
@@ -5753,6 +5757,7 @@ collectBinaryUpgradeClassOids(Archive *fout)
 		binaryUpgradeClassOids[i].toast_relfilenumber = atooid(PQgetvalue(res, i, 4));
 		binaryUpgradeClassOids[i].toast_index_oid = atooid(PQgetvalue(res, i, 5));
 		binaryUpgradeClassOids[i].toast_index_relfilenumber = atooid(PQgetvalue(res, i, 6));
+		binaryUpgradeClassOids[i].toast_chunk_id_typoid = atooid(PQgetvalue(res, i, 7));
 	}
 
 	PQclear(res);
@@ -5817,6 +5822,9 @@ binary_upgrade_set_pg_class_oids(Archive *fout,
 			appendPQExpBuffer(upgrade_buffer,
 							  "SELECT pg_catalog.binary_upgrade_set_next_toast_relfilenode('%u'::pg_catalog.oid);\n",
 							  entry->toast_relfilenumber);
+			appendPQExpBuffer(upgrade_buffer,
+							  "SELECT pg_catalog.binary_upgrade_set_next_toast_chunk_id_typoid('%u'::pg_catalog.oid);\n",
+							  entry->toast_chunk_id_typoid);
 
 			/* every toast table has an index */
 			appendPQExpBuffer(upgrade_buffer,
diff --git a/src/test/modules/spgist_name_ops/expected/spgist_name_ops.out b/src/test/modules/spgist_name_ops/expected/spgist_name_ops.out
index 1ee65ede2430..35e59d0cd83c 100644
--- a/src/test/modules/spgist_name_ops/expected/spgist_name_ops.out
+++ b/src/test/modules/spgist_name_ops/expected/spgist_name_ops.out
@@ -61,9 +61,10 @@ select * from t
  binary_upgrade_set_next_pg_enum_oid                  |    | binary_upgrade_set_next_pg_enum_oid
  binary_upgrade_set_next_pg_tablespace_oid            |    | binary_upgrade_set_next_pg_tablespace_oid
  binary_upgrade_set_next_pg_type_oid                  |    | binary_upgrade_set_next_pg_type_oid
+ binary_upgrade_set_next_toast_chunk_id_typoid        |    | binary_upgrade_set_next_toast_chunk_id_typoid
  binary_upgrade_set_next_toast_pg_class_oid           |  1 | binary_upgrade_set_next_toast_pg_class_oid
  binary_upgrade_set_next_toast_relfilenode            |    | binary_upgrade_set_next_toast_relfilenode
-(13 rows)
+(14 rows)
 
 -- Verify clean failure when INCLUDE'd columns result in overlength tuple
 -- The error message details are platform-dependent, so show only SQLSTATE
@@ -110,9 +111,10 @@ select * from t
  binary_upgrade_set_next_pg_enum_oid                  |    | binary_upgrade_set_next_pg_enum_oid
  binary_upgrade_set_next_pg_tablespace_oid            |    | binary_upgrade_set_next_pg_tablespace_oid
  binary_upgrade_set_next_pg_type_oid                  |    | binary_upgrade_set_next_pg_type_oid
+ binary_upgrade_set_next_toast_chunk_id_typoid        |    | binary_upgrade_set_next_toast_chunk_id_typoid
  binary_upgrade_set_next_toast_pg_class_oid           |  1 | binary_upgrade_set_next_toast_pg_class_oid
  binary_upgrade_set_next_toast_relfilenode            |    | binary_upgrade_set_next_toast_relfilenode
-(13 rows)
+(14 rows)
 
 \set VERBOSITY sqlstate
 insert into t values(repeat('xyzzy', 12), 42, repeat('xyzzy', 4000));
-- 
2.50.0

v5-0011-Enlarge-OID-generation-to-8-bytes.patchtext/x-diff; charset=us-asciiDownload
From 1cbccca63cdb1dd7a9e31be70f570f71940e01e9 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 12:15:15 +0900
Subject: [PATCH v5 11/15] Enlarge OID generation to 8 bytes

This adds a new routine called GetNewObjectId8() in varsup.c, which is
able to retrieve a 64b OID.  GetNewObjectId() is kept compatible with
its origin, where we still check that the lower 32 bits of the counter
do not wraparound, handling the FirstNormalObjectId case.

pg_resetwal -o/--next-oid is updated to be able to handle 8-byte OIDs.
---
 src/include/access/transam.h              |  3 +-
 src/include/access/xlog.h                 |  2 +-
 src/include/catalog/pg_control.h          |  2 +-
 src/backend/access/rmgrdesc/xlogdesc.c    |  8 +--
 src/backend/access/transam/varsup.c       | 62 ++++++++++++++++-------
 src/backend/access/transam/xlog.c         |  8 +--
 src/backend/access/transam/xlogrecovery.c |  2 +-
 src/bin/pg_controldata/pg_controldata.c   |  2 +-
 src/bin/pg_resetwal/pg_resetwal.c         | 10 ++--
 doc/src/sgml/ref/pg_resetwal.sgml         |  6 +--
 10 files changed, 66 insertions(+), 39 deletions(-)

diff --git a/src/include/access/transam.h b/src/include/access/transam.h
index 7d82cd2eb562..2ef3000bdb1f 100644
--- a/src/include/access/transam.h
+++ b/src/include/access/transam.h
@@ -211,7 +211,7 @@ typedef struct TransamVariablesData
 	/*
 	 * These fields are protected by OidGenLock.
 	 */
-	Oid			nextOid;		/* next OID to assign */
+	Oid8		nextOid;		/* next OID (8 bytes) to assign */
 	uint32		oidCount;		/* OIDs available before must do XLOG work */
 
 	/*
@@ -293,6 +293,7 @@ extern void SetTransactionIdLimit(TransactionId oldest_datfrozenxid,
 extern void AdvanceOldestClogXid(TransactionId oldest_datfrozenxid);
 extern bool ForceTransactionIdLimitUpdate(void);
 extern Oid	GetNewObjectId(void);
+extern Oid8 GetNewObjectId8(void);
 extern void StopGeneratingPinnedObjectIds(void);
 
 #ifdef USE_ASSERT_CHECKING
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index d12798be3d80..21d915ae5802 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -243,7 +243,7 @@ extern void ShutdownXLOG(int code, Datum arg);
 extern bool CreateCheckPoint(int flags);
 extern bool CreateRestartPoint(int flags);
 extern WALAvailability GetWALAvailability(XLogRecPtr targetLSN);
-extern void XLogPutNextOid(Oid nextOid);
+extern void XLogPutNextOid(Oid8 nextOid);
 extern XLogRecPtr XLogRestorePoint(const char *rpName);
 extern void UpdateFullPageWrites(void);
 extern void GetFullPageWriteInfo(XLogRecPtr *RedoRecPtr_p, bool *doPageWrites_p);
diff --git a/src/include/catalog/pg_control.h b/src/include/catalog/pg_control.h
index 63e834a6ce47..c85c84bbfdbb 100644
--- a/src/include/catalog/pg_control.h
+++ b/src/include/catalog/pg_control.h
@@ -42,7 +42,7 @@ typedef struct CheckPoint
 	bool		fullPageWrites; /* current full_page_writes */
 	int			wal_level;		/* current wal_level */
 	FullTransactionId nextXid;	/* next free transaction ID */
-	Oid			nextOid;		/* next free OID */
+	Oid8		nextOid;		/* next free OID */
 	MultiXactId nextMulti;		/* next free MultiXactId */
 	MultiXactOffset nextMultiOffset;	/* next free MultiXact offset */
 	TransactionId oldestXid;	/* cluster-wide minimum datfrozenxid */
diff --git a/src/backend/access/rmgrdesc/xlogdesc.c b/src/backend/access/rmgrdesc/xlogdesc.c
index cd6c2a2f650a..23920d32092a 100644
--- a/src/backend/access/rmgrdesc/xlogdesc.c
+++ b/src/backend/access/rmgrdesc/xlogdesc.c
@@ -66,7 +66,7 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
 		CheckPoint *checkpoint = (CheckPoint *) rec;
 
 		appendStringInfo(buf, "redo %X/%08X; "
-						 "tli %u; prev tli %u; fpw %s; wal_level %s; xid %u:%u; oid %u; multi %u; offset %u; "
+						 "tli %u; prev tli %u; fpw %s; wal_level %s; xid %u:%u; oid " OID8_FORMAT "; multi %u; offset %u; "
 						 "oldest xid %u in DB %u; oldest multi %u in DB %u; "
 						 "oldest/newest commit timestamp xid: %u/%u; "
 						 "oldest running xid %u; %s",
@@ -91,10 +91,10 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
 	}
 	else if (info == XLOG_NEXTOID)
 	{
-		Oid			nextOid;
+		Oid8		nextOid;
 
-		memcpy(&nextOid, rec, sizeof(Oid));
-		appendStringInfo(buf, "%u", nextOid);
+		memcpy(&nextOid, rec, sizeof(Oid8));
+		appendStringInfo(buf, OID8_FORMAT, nextOid);
 	}
 	else if (info == XLOG_RESTORE_POINT)
 	{
diff --git a/src/backend/access/transam/varsup.c b/src/backend/access/transam/varsup.c
index fe895787cb72..7dcac9e210e4 100644
--- a/src/backend/access/transam/varsup.c
+++ b/src/backend/access/transam/varsup.c
@@ -542,31 +542,51 @@ ForceTransactionIdLimitUpdate(void)
 
 
 /*
- * GetNewObjectId -- allocate a new OID
+ * GetNewObjectId -- allocate a new OID (4 bytes)
  *
- * OIDs are generated by a cluster-wide counter.  Since they are only 32 bits
- * wide, counter wraparound will occur eventually, and therefore it is unwise
- * to assume they are unique unless precautions are taken to make them so.
- * Hence, this routine should generally not be used directly.  The only direct
- * callers should be GetNewOidWithIndex() and GetNewRelFileNumber() in
- * catalog/catalog.c.
+ * OIDs are generated by a cluster-wide counter.  The callers of this routine
+ * expect a 32 bit-wide counter, and counter wraparound will occur eventually,
+ * and therefore it is unwise to assume they are unique unless precautions are
+ * taken to make them so.  This routine should generally not be used directly.
+ * The only direct callers should be GetNewOidWithIndex() and
+ * GetNewRelFileNumber() in catalog/catalog.c.
  */
 Oid
 GetNewObjectId(void)
 {
-	Oid			result;
+	return (Oid) GetNewObjectId8();
+}
+
+/*
+ * GetNewObjectId8 -- allocate a new OID (8 bytes)
+ *
+ * This routine can be called directly if the consumer of the OID allocated
+ * stores the counter in an 8-byte space, where wraparound does not matter.
+ * We still need to care about the wraparound case in the low 32 bits of the
+ * space allocated, GetNewObjectId() expecting OIDs to never be allocated
+ * up to FirstNormalObjectId.
+ */
+Oid8
+GetNewObjectId8(void)
+{
+	Oid8		result;
+	Oid			nextoid_lo;
+	uint32		nextoid_hi;
 
 	/* safety check, we should never get this far in a HS standby */
 	if (RecoveryInProgress())
 		elog(ERROR, "cannot assign OIDs during recovery");
 
 	LWLockAcquire(OidGenLock, LW_EXCLUSIVE);
+	nextoid_lo = (Oid) TransamVariables->nextOid;
+	nextoid_hi = (uint32) (TransamVariables->nextOid >> 32);
 
 	/*
-	 * Check for wraparound of the OID counter.  We *must* not return 0
-	 * (InvalidOid), and in normal operation we mustn't return anything below
-	 * FirstNormalObjectId since that range is reserved for initdb (see
-	 * IsCatalogRelationOid()).  Note we are relying on unsigned comparison.
+	 * Check for wraparound of the OID counter in its lower 4 bytes.  We
+	 * *must* not return 0 (InvalidOid), and in normal operation we
+	 * mustn't return anything below FirstNormalObjectId since that range
+	 * is reserved for initdb (see IsCatalogRelationOid()).  Note we are
+	 * relying on unsigned comparison.
 	 *
 	 * During initdb, we start the OID generator at FirstGenbkiObjectId, so we
 	 * only wrap if before that point when in bootstrap or standalone mode.
@@ -576,26 +596,32 @@ GetNewObjectId(void)
 	 * available for automatic assignment during initdb, while ensuring they
 	 * will never conflict with user-assigned OIDs.
 	 */
-	if (TransamVariables->nextOid < ((Oid) FirstNormalObjectId))
+	if (nextoid_lo < ((Oid) FirstNormalObjectId))
 	{
 		if (IsPostmasterEnvironment)
 		{
 			/* wraparound, or first post-initdb assignment, in normal mode */
-			TransamVariables->nextOid = FirstNormalObjectId;
+			nextoid_lo = FirstNormalObjectId;
 			TransamVariables->oidCount = 0;
 		}
 		else
 		{
 			/* we may be bootstrapping, so don't enforce the full range */
-			if (TransamVariables->nextOid < ((Oid) FirstGenbkiObjectId))
+			if (nextoid_lo < ((Oid) FirstGenbkiObjectId))
 			{
 				/* wraparound in standalone mode (unlikely but possible) */
-				TransamVariables->nextOid = FirstNormalObjectId;
+				nextoid_lo = FirstNormalObjectId;
 				TransamVariables->oidCount = 0;
 			}
 		}
 	}
 
+	/*
+	 * Set next OID in its 8-byte space, skipping the first post-init
+	 * assignment.
+	 */
+	TransamVariables->nextOid = ((Oid8) nextoid_hi) << 32 | nextoid_lo;
+
 	/* If we run out of logged for use oids then we must log more */
 	if (TransamVariables->oidCount == 0)
 	{
@@ -620,7 +646,7 @@ GetNewObjectId(void)
  * to the specified value.
  */
 static void
-SetNextObjectId(Oid nextOid)
+SetNextObjectId(Oid8 nextOid)
 {
 	/* Safety check, this is only allowable during initdb */
 	if (IsPostmasterEnvironment)
@@ -630,7 +656,7 @@ SetNextObjectId(Oid nextOid)
 	LWLockAcquire(OidGenLock, LW_EXCLUSIVE);
 
 	if (TransamVariables->nextOid > nextOid)
-		elog(ERROR, "too late to advance OID counter to %u, it is now %u",
+		elog(ERROR, "too late to advance OID counter to " OID8_FORMAT ", it is now " OID8_FORMAT,
 			 nextOid, TransamVariables->nextOid);
 
 	TransamVariables->nextOid = nextOid;
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index e8909406686d..6194bf5ef5aa 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -8192,10 +8192,10 @@ KeepLogSeg(XLogRecPtr recptr, XLogSegNo *logSegNo)
  * Write a NEXTOID log record
  */
 void
-XLogPutNextOid(Oid nextOid)
+XLogPutNextOid(Oid8 nextOid)
 {
 	XLogBeginInsert();
-	XLogRegisterData(&nextOid, sizeof(Oid));
+	XLogRegisterData(&nextOid, sizeof(Oid8));
 	(void) XLogInsert(RM_XLOG_ID, XLOG_NEXTOID);
 
 	/*
@@ -8418,7 +8418,7 @@ xlog_redo(XLogReaderState *record)
 
 	if (info == XLOG_NEXTOID)
 	{
-		Oid			nextOid;
+		Oid8		nextOid;
 
 		/*
 		 * We used to try to take the maximum of TransamVariables->nextOid and
@@ -8427,7 +8427,7 @@ xlog_redo(XLogReaderState *record)
 		 * anyway, better to just believe the record exactly.  We still take
 		 * OidGenLock while setting the variable, just in case.
 		 */
-		memcpy(&nextOid, XLogRecGetData(record), sizeof(Oid));
+		memcpy(&nextOid, XLogRecGetData(record), sizeof(Oid8));
 		LWLockAcquire(OidGenLock, LW_EXCLUSIVE);
 		TransamVariables->nextOid = nextOid;
 		TransamVariables->oidCount = 0;
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index f23ec8969c27..00807eb66874 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -880,7 +880,7 @@ InitWalRecovery(ControlFileData *ControlFile, bool *wasShutdown_ptr,
 							LSN_FORMAT_ARGS(checkPoint.redo),
 							wasShutdown ? "true" : "false"));
 	ereport(DEBUG1,
-			(errmsg_internal("next transaction ID: " UINT64_FORMAT "; next OID: %u",
+			(errmsg_internal("next transaction ID: " UINT64_FORMAT "; next OID: " OID8_FORMAT,
 							 U64FromFullTransactionId(checkPoint.nextXid),
 							 checkPoint.nextOid)));
 	ereport(DEBUG1,
diff --git a/src/bin/pg_controldata/pg_controldata.c b/src/bin/pg_controldata/pg_controldata.c
index 10de058ce91f..992111d3a1d2 100644
--- a/src/bin/pg_controldata/pg_controldata.c
+++ b/src/bin/pg_controldata/pg_controldata.c
@@ -260,7 +260,7 @@ main(int argc, char *argv[])
 	printf(_("Latest checkpoint's NextXID:          %u:%u\n"),
 		   EpochFromFullTransactionId(ControlFile->checkPointCopy.nextXid),
 		   XidFromFullTransactionId(ControlFile->checkPointCopy.nextXid));
-	printf(_("Latest checkpoint's NextOID:          %u\n"),
+	printf(_("Latest checkpoint's NextOID:          " OID8_FORMAT "\n"),
 		   ControlFile->checkPointCopy.nextOid);
 	printf(_("Latest checkpoint's NextMultiXactId:  %u\n"),
 		   ControlFile->checkPointCopy.nextMulti);
diff --git a/src/bin/pg_resetwal/pg_resetwal.c b/src/bin/pg_resetwal/pg_resetwal.c
index 7a4e4eb95706..c1039a8a4d16 100644
--- a/src/bin/pg_resetwal/pg_resetwal.c
+++ b/src/bin/pg_resetwal/pg_resetwal.c
@@ -68,7 +68,7 @@ static TransactionId set_oldest_xid = 0;
 static TransactionId set_xid = 0;
 static TransactionId set_oldest_commit_ts_xid = 0;
 static TransactionId set_newest_commit_ts_xid = 0;
-static Oid	set_oid = 0;
+static Oid8 set_oid = 0;
 static MultiXactId set_mxid = 0;
 static MultiXactOffset set_mxoff = (MultiXactOffset) -1;
 static TimeLineID minXlogTli = 0;
@@ -225,7 +225,7 @@ main(int argc, char *argv[])
 
 			case 'o':
 				errno = 0;
-				set_oid = strtoul(optarg, &endptr, 0);
+				set_oid = strtou64(optarg, &endptr, 0);
 				if (endptr == optarg || *endptr != '\0' || errno != 0)
 				{
 					pg_log_error("invalid argument for option %s", "-o");
@@ -755,7 +755,7 @@ PrintControlValues(bool guessed)
 	printf(_("Latest checkpoint's NextXID:          %u:%u\n"),
 		   EpochFromFullTransactionId(ControlFile.checkPointCopy.nextXid),
 		   XidFromFullTransactionId(ControlFile.checkPointCopy.nextXid));
-	printf(_("Latest checkpoint's NextOID:          %u\n"),
+	printf(_("Latest checkpoint's NextOID:          " OID8_FORMAT "\n"),
 		   ControlFile.checkPointCopy.nextOid);
 	printf(_("Latest checkpoint's NextMultiXactId:  %u\n"),
 		   ControlFile.checkPointCopy.nextMulti);
@@ -839,7 +839,7 @@ PrintNewControlValues(void)
 
 	if (set_oid != 0)
 	{
-		printf(_("NextOID:                              %u\n"),
+		printf(_("NextOID:                              " OID8_FORMAT "\n"),
 			   ControlFile.checkPointCopy.nextOid);
 	}
 
@@ -1208,7 +1208,7 @@ usage(void)
 	printf(_("  -e, --epoch=XIDEPOCH             set next transaction ID epoch\n"));
 	printf(_("  -l, --next-wal-file=WALFILE      set minimum starting location for new WAL\n"));
 	printf(_("  -m, --multixact-ids=MXID,MXID    set next and oldest multitransaction ID\n"));
-	printf(_("  -o, --next-oid=OID               set next OID\n"));
+	printf(_("  -o, --next-oid=OID8              set next OID (8 bytes)\n"));
 	printf(_("  -O, --multixact-offset=OFFSET    set next multitransaction offset\n"));
 	printf(_("  -u, --oldest-transaction-id=XID  set oldest transaction ID\n"));
 	printf(_("  -x, --next-transaction-id=XID    set next transaction ID\n"));
diff --git a/doc/src/sgml/ref/pg_resetwal.sgml b/doc/src/sgml/ref/pg_resetwal.sgml
index 2c019c2aac6e..b03251cedbbe 100644
--- a/doc/src/sgml/ref/pg_resetwal.sgml
+++ b/doc/src/sgml/ref/pg_resetwal.sgml
@@ -279,11 +279,11 @@ PostgreSQL documentation
    </varlistentry>
 
    <varlistentry>
-    <term><option>-o <replaceable class="parameter">oid</replaceable></option></term>
-    <term><option>--next-oid=<replaceable class="parameter">oid</replaceable></option></term>
+    <term><option>-o <replaceable class="parameter">oid8</replaceable></option></term>
+    <term><option>--next-oid=<replaceable class="parameter">oid8</replaceable></option></term>
     <listitem>
      <para>
-      Manually set the next OID.
+      Manually set the next OID (8 bytes).
      </para>
 
      <para>
-- 
2.50.0

v5-0012-Add-relation-option-toast_value_type.patchtext/x-diff; charset=us-asciiDownload
From 5a238393e412c2284a93fe193b902b0db39d3bcc Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 12:54:58 +0900
Subject: [PATCH v5 12/15] Add relation option toast_value_type

This relation option gives the possibility to define the attribute type
that can be used for chunk_id in a TOAST table when it is created
initially.  This parameter has no effect if a TOAST table exists, even
after it is modified later on, even on rewrites.

This can be set only to "oid" currently, and will be expanded with a
second mode later.

Note: perhaps it would make sense to introduce that only when support
for 8-byte OID values are added to TOAST, the split is here to ease
review.
---
 src/include/utils/rel.h                | 17 +++++++++++++++++
 src/backend/access/common/reloptions.c | 21 +++++++++++++++++++++
 src/backend/catalog/toasting.c         |  6 ++++++
 src/bin/psql/tab-complete.in.c         |  1 +
 doc/src/sgml/ref/create_table.sgml     | 18 ++++++++++++++++++
 5 files changed, 63 insertions(+)

diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index b552359915f1..b846bd42103e 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -337,11 +337,20 @@ typedef enum StdRdOptIndexCleanup
 	STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON,
 } StdRdOptIndexCleanup;
 
+/* StdRdOptions->toast_value_type values */
+typedef enum StdRdOptToastValueType
+{
+	STDRD_OPTION_TOAST_VALUE_TYPE_INVALID = 0,
+	STDRD_OPTION_TOAST_VALUE_TYPE_OID,
+} StdRdOptToastValueType;
+
 typedef struct StdRdOptions
 {
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	int			fillfactor;		/* page fill factor in percent (0..100) */
 	int			toast_tuple_target; /* target for tuple toasting */
+	StdRdOptToastValueType	toast_value_type;	/* type assigned to chunk_id
+												 * at toast table creation */
 	AutoVacOpts autovacuum;		/* autovacuum-related options */
 	bool		user_catalog_table; /* use as an additional catalog relation */
 	int			parallel_workers;	/* max number of parallel workers */
@@ -367,6 +376,14 @@ typedef struct StdRdOptions
 	((relation)->rd_options ? \
 	 ((StdRdOptions *) (relation)->rd_options)->toast_tuple_target : (defaulttarg))
 
+/*
+ * RelationGetToastValueType
+ *		Returns the relation's toast_value_type.  Note multiple eval of argument!
+ */
+#define RelationGetToastValueType(relation, defaulttarg) \
+	((relation)->rd_options ? \
+	 ((StdRdOptions *) (relation)->rd_options)->toast_value_type : defaulttarg)
+
 /*
  * RelationGetFillFactor
  *		Returns the relation's fillfactor.  Note multiple eval of argument!
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 0af3fea68fa4..b0447d9e39bb 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -516,6 +516,14 @@ static relopt_enum_elt_def viewCheckOptValues[] =
 	{(const char *) NULL}		/* list terminator */
 };
 
+/* values from StdRdOptToastValueType */
+static relopt_enum_elt_def StdRdOptToastValueTypes[] =
+{
+	/* no value for INVALID */
+	{"oid", STDRD_OPTION_TOAST_VALUE_TYPE_OID},
+	{(const char *) NULL}		/* list terminator */
+};
+
 static relopt_enum enumRelOpts[] =
 {
 	{
@@ -529,6 +537,17 @@ static relopt_enum enumRelOpts[] =
 		STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO,
 		gettext_noop("Valid values are \"on\", \"off\", and \"auto\".")
 	},
+	{
+		{
+			"toast_value_type",
+			"Controls the attribute type of chunk_id at toast table creation",
+			RELOPT_KIND_HEAP,
+			ShareUpdateExclusiveLock
+		},
+		StdRdOptToastValueTypes,
+		STDRD_OPTION_TOAST_VALUE_TYPE_OID,
+		gettext_noop("Valid values are \"oid\".")
+	},
 	{
 		{
 			"buffering",
@@ -1898,6 +1917,8 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
 		offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_min_duration)},
 		{"toast_tuple_target", RELOPT_TYPE_INT,
 		offsetof(StdRdOptions, toast_tuple_target)},
+		{"toast_value_type", RELOPT_TYPE_ENUM,
+		offsetof(StdRdOptions, toast_value_type)},
 		{"autovacuum_vacuum_cost_delay", RELOPT_TYPE_REAL,
 		offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_delay)},
 		{"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c
index f1d76d8acd51..545983b5be9d 100644
--- a/src/backend/catalog/toasting.c
+++ b/src/backend/catalog/toasting.c
@@ -158,9 +158,15 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	 */
 	if (!IsBinaryUpgrade)
 	{
+		StdRdOptToastValueType value_type;
+
 		/* Normal mode, normal check */
 		if (!needs_toast_table(rel))
 			return false;
+
+		value_type = RelationGetToastValueType(rel, STDRD_OPTION_TOAST_VALUE_TYPE_OID);
+		if (value_type == STDRD_OPTION_TOAST_VALUE_TYPE_OID)
+			toast_chunkid_typid = OIDOID;
 	}
 	else
 	{
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 8b10f2313f39..2b1cf063ebc2 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -1431,6 +1431,7 @@ static const char *const table_storage_parameters[] = {
 	"toast.vacuum_max_eager_freeze_failure_rate",
 	"toast.vacuum_truncate",
 	"toast_tuple_target",
+	"toast_value_type",
 	"user_catalog_table",
 	"vacuum_index_cleanup",
 	"vacuum_max_eager_freeze_failure_rate",
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index dc000e913c14..84ad78afa3de 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1632,6 +1632,24 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     </listitem>
    </varlistentry>
 
+   <varlistentry id="reloption-toast-value-type" xreflabel="toast_value_type">
+    <term><literal>toast_value_type</literal> (<type>enum</type>)
+    <indexterm>
+     <primary><varname>toast_value_type</varname> storage parameter</primary>
+    </indexterm>
+    </term>
+    <listitem>
+     <para>
+      The toast_value_type specifies the attribute type of
+      <literal>chunk_id</literal> used when initially creating  a toast
+      relation for this table.
+      By default this parameter is <literal>oid</literal>, to assign
+      <type>oid</type> as attribute type to <literal>chunk_id</literal>.
+      This parameter cannot be set for TOAST tables.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="reloption-parallel-workers" xreflabel="parallel_workers">
     <term><literal>parallel_workers</literal> (<type>integer</type>)
      <indexterm>
-- 
2.50.0

v5-0013-Add-support-for-oid8-TOAST-values.patchtext/x-diff; charset=us-asciiDownload
From d2d0ed6ea1e3bbe87874a95b0630599a10d9118d Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 13:43:03 +0900
Subject: [PATCH v5 13/15] Add support for oid8 TOAST values

This commit adds the possibility to define TOAST tables with oid8 as
value ID, based on the reloption toast_value_type.  All the external
TOAST pointers still rely on varatt_external and a single vartag, with
all the values inserted in the bigint TOAST tables fed from the existing
OID value generator.  This will be changed in an upcoming patch that
adds more vartag_external types and its associated structures, with the
code being able to use a different external TOAST pointer depending on
the attribute type of chunk_id in TOAST relations.

All the changes done here are mechanical, with all the TOAST code able
to do chunk ID lookups based on the two types now supported.

XXX: Catalog version bump required.
---
 src/include/catalog/pg_opclass.dat          |  3 +-
 src/include/utils/rel.h                     |  1 +
 src/backend/access/common/reloptions.c      |  1 +
 src/backend/access/common/toast_internals.c | 94 +++++++++++++++------
 src/backend/access/heap/heaptoast.c         | 20 ++++-
 src/backend/catalog/toasting.c              | 24 +++++-
 doc/src/sgml/storage.sgml                   |  7 +-
 contrib/amcheck/verify_heapam.c             | 19 ++++-
 8 files changed, 129 insertions(+), 40 deletions(-)

diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index c0de88fabc49..b8f2bc2d69c4 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -179,7 +179,8 @@
   opcintype => 'xid8' },
 { opcmethod => 'hash', opcname => 'oid8_ops', opcfamily => 'hash/oid8_ops',
   opcintype => 'oid8' },
-{ opcmethod => 'btree', opcname => 'oid8_ops', opcfamily => 'btree/oid8_ops',
+{ oid => '8285', oid_symbol => 'OID8_BTREE_OPS_OID',
+  opcmethod => 'btree', opcname => 'oid8_ops', opcfamily => 'btree/oid8_ops',
   opcintype => 'oid8' },
 { opcmethod => 'hash', opcname => 'cid_ops', opcfamily => 'hash/cid_ops',
   opcintype => 'cid' },
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index b846bd42103e..52646d43ebdc 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -342,6 +342,7 @@ typedef enum StdRdOptToastValueType
 {
 	STDRD_OPTION_TOAST_VALUE_TYPE_INVALID = 0,
 	STDRD_OPTION_TOAST_VALUE_TYPE_OID,
+	STDRD_OPTION_TOAST_VALUE_TYPE_OID8,
 } StdRdOptToastValueType;
 
 typedef struct StdRdOptions
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index b0447d9e39bb..f05eaacfa006 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -521,6 +521,7 @@ static relopt_enum_elt_def StdRdOptToastValueTypes[] =
 {
 	/* no value for INVALID */
 	{"oid", STDRD_OPTION_TOAST_VALUE_TYPE_OID},
+	{"oid8", STDRD_OPTION_TOAST_VALUE_TYPE_OID8},
 	{(const char *) NULL}		/* list terminator */
 };
 
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 5d0aa664fc91..42a6a59d7536 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -26,6 +26,7 @@
 #include "utils/fmgroids.h"
 #include "utils/rel.h"
 #include "utils/snapmgr.h"
+#include "utils/lsyscache.h"
 
 static bool toastrel_valueid_exists(Relation toastrel, Oid8 valueid);
 static bool toastid_valueid_exists(Oid toastrelid, Oid8 valueid);
@@ -146,8 +147,10 @@ toast_save_datum(Relation rel, Datum value,
 	int			validIndex;
 	const toast_external_info *info;
 	uint8		tag = VARTAG_INDIRECT;	/* init value does not matter */
+	Oid			toast_typid = get_atttype(rel->rd_rel->reltoastrelid, 1);
 
 	Assert(!VARATT_IS_EXTERNAL(dval));
+	Assert(OidIsValid(toast_typid));
 
 	/*
 	 * Open the toast relation and its indexes.  We can use the index to check
@@ -228,24 +231,32 @@ toast_save_datum(Relation rel, Datum value,
 		toast_pointer.toastrelid = RelationGetRelid(toastrel);
 
 	/*
-	 * Choose an OID to use as the value ID for this toast value.
+	 * Choose a new value to use as the value ID for this toast value, be it
+	 * for OID or int8-based TOAST relations.
 	 *
-	 * Normally we just choose an unused OID within the toast table.  But
+	 * Normally we just choose an unused value within the toast table.  But
 	 * during table-rewriting operations where we are preserving an existing
-	 * toast table OID, we want to preserve toast value OIDs too.  So, if
+	 * toast table OID, we want to preserve toast value IDs too.  So, if
 	 * rd_toastoid is set and we had a prior external value from that same
 	 * toast table, re-use its value ID.  If we didn't have a prior external
 	 * value (which is a corner case, but possible if the table's attstorage
 	 * options have been changed), we have to pick a value ID that doesn't
-	 * conflict with either new or existing toast value OIDs.
+	 * conflict with either new or existing toast value IDs.  If the TOAST
+	 * table uses 8-byte value IDs, we should not really care much about
+	 * that.
 	 */
 	if (!OidIsValid(rel->rd_toastoid))
 	{
 		/* normal case: just choose an unused OID */
-		toast_pointer.valueid =
-			GetNewOidWithIndex(toastrel,
-							   RelationGetRelid(toastidxs[validIndex]),
-							   (AttrNumber) 1);
+		if (toast_typid == OIDOID)
+			toast_pointer.valueid =
+				GetNewOidWithIndex(toastrel,
+								   RelationGetRelid(toastidxs[validIndex]),
+								   (AttrNumber) 1);
+		else if (toast_typid == OID8OID)
+			toast_pointer.valueid = GetNewObjectId8();
+		else
+			Assert(false);
 	}
 	else
 	{
@@ -291,24 +302,32 @@ toast_save_datum(Relation rel, Datum value,
 		if (toast_pointer.valueid == InvalidOid8)
 		{
 			/*
-			 * new value; must choose an OID that doesn't conflict in either
-			 * old or new toast table
+			 * new value; must choose a value that doesn't conflict in either
+			 * old or new toast table.
 			 */
-			do
+			if (toast_typid == OIDOID)
 			{
-				toast_pointer.valueid =
-					GetNewOidWithIndex(toastrel,
-									   RelationGetRelid(toastidxs[validIndex]),
-									   (AttrNumber) 1);
-			} while (toastid_valueid_exists(rel->rd_toastoid,
-											toast_pointer.valueid));
+				do
+				{
+					toast_pointer.valueid =
+						GetNewOidWithIndex(toastrel,
+										   RelationGetRelid(toastidxs[validIndex]),
+										   (AttrNumber) 1);
+				} while (toastid_valueid_exists(rel->rd_toastoid,
+												toast_pointer.valueid));
+			}
+			else if (toast_typid == OID8OID)
+				toast_pointer.valueid = GetNewObjectId8();
 		}
 	}
 
 	/*
 	 * Initialize constant parts of the tuple data
 	 */
-	t_values[0] = ObjectIdGetDatum(toast_pointer.valueid);
+	if (toast_typid == OIDOID)
+		t_values[0] = ObjectIdGetDatum(toast_pointer.valueid);
+	else if (toast_typid == OID8OID)
+		t_values[0] = ObjectId8GetDatum(toast_pointer.valueid);
 	t_values[2] = PointerGetDatum(&chunk_data);
 	t_isnull[0] = false;
 	t_isnull[1] = false;
@@ -415,6 +434,7 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	HeapTuple	toasttup;
 	int			num_indexes;
 	int			validIndex;
+	Oid			toast_typid;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		return;
@@ -426,6 +446,8 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	 * Open the toast relation and its indexes
 	 */
 	toastrel = table_open(toast_pointer.toastrelid, RowExclusiveLock);
+	toast_typid = TupleDescAttr(toastrel->rd_att, 0)->atttypid;
+	Assert(toast_typid == OIDOID || toast_typid == OID8OID);
 
 	/* Fetch valid relation used for process */
 	validIndex = toast_open_indexes(toastrel,
@@ -436,10 +458,18 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	/*
 	 * Setup a scan key to find chunks with matching va_valueid
 	 */
-	ScanKeyInit(&toastkey,
-				(AttrNumber) 1,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(toast_pointer.valueid));
+	if (toast_typid == OIDOID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(toast_pointer.valueid));
+	else if (toast_typid == OID8OID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OID8EQ,
+					ObjectId8GetDatum(toast_pointer.valueid));
+	else
+		Assert(false);
 
 	/*
 	 * Find all the chunks.  (We don't actually care whether we see them in
@@ -486,6 +516,7 @@ toastrel_valueid_exists(Relation toastrel, Oid8 valueid)
 	int			num_indexes;
 	int			validIndex;
 	Relation   *toastidxs;
+	Oid			toast_typid;
 
 	/* Fetch a valid index relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -493,13 +524,24 @@ toastrel_valueid_exists(Relation toastrel, Oid8 valueid)
 									&toastidxs,
 									&num_indexes);
 
+	toast_typid = TupleDescAttr(toastrel->rd_att, 0)->atttypid;
+	Assert(toast_typid == OIDOID || toast_typid == OID8OID);
+
 	/*
 	 * Setup a scan key to find chunks with matching va_valueid
 	 */
-	ScanKeyInit(&toastkey,
-				(AttrNumber) 1,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(valueid));
+	if (toast_typid == OIDOID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(valueid));
+	else if (toast_typid == OID8OID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OID8EQ,
+					ObjectId8GetDatum(valueid));
+	else
+		Assert(false);
 
 	/*
 	 * Is there any such chunk?
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index 230f2a6f35eb..50e9bf9047f9 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -654,6 +654,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 	int32		max_chunk_size;
 	const toast_external_info *info;
 	uint8		tag = VARTAG_INDIRECT;	/* init value does not matter */
+	Oid			toast_typid;
 
 	/* Look for the valid index of toast relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -667,16 +668,27 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 
 	max_chunk_size = info->maximum_chunk_size;
 
+	toast_typid = TupleDescAttr(toastrel->rd_att, 0)->atttypid;
+	Assert(toast_typid == OIDOID || toast_typid == OID8OID);
+
 	totalchunks = ((attrsize - 1) / max_chunk_size) + 1;
 	startchunk = sliceoffset / max_chunk_size;
 	endchunk = (sliceoffset + slicelength - 1) / max_chunk_size;
 	Assert(endchunk <= totalchunks);
 
 	/* Set up a scan key to fetch from the index. */
-	ScanKeyInit(&toastkey[0],
-				(AttrNumber) 1,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(valueid));
+	if (toast_typid == OIDOID)
+		ScanKeyInit(&toastkey[0],
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(valueid));
+	else if (toast_typid == OID8OID)
+		ScanKeyInit(&toastkey[0],
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OID8EQ,
+					ObjectId8GetDatum(valueid));
+	else
+		Assert(false);
 
 	/*
 	 * No additional condition if fetching all chunks. Otherwise, use an
diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c
index 545983b5be9d..2288311b22a4 100644
--- a/src/backend/catalog/toasting.c
+++ b/src/backend/catalog/toasting.c
@@ -31,6 +31,7 @@
 #include "nodes/makefuncs.h"
 #include "utils/fmgroids.h"
 #include "utils/rel.h"
+#include "utils/lsyscache.h"
 #include "utils/syscache.h"
 
 static void CheckAndCreateToastTable(Oid relOid, Datum reloptions,
@@ -167,6 +168,8 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 		value_type = RelationGetToastValueType(rel, STDRD_OPTION_TOAST_VALUE_TYPE_OID);
 		if (value_type == STDRD_OPTION_TOAST_VALUE_TYPE_OID)
 			toast_chunkid_typid = OIDOID;
+		else if (value_type == STDRD_OPTION_TOAST_VALUE_TYPE_OID8)
+			toast_chunkid_typid = OID8OID;
 	}
 	else
 	{
@@ -199,7 +202,8 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("toast chunk_id type not set while in binary upgrade mode")));
-		if (binary_upgrade_next_toast_chunk_id_typoid != OIDOID)
+		if (binary_upgrade_next_toast_chunk_id_typoid != OIDOID &&
+			binary_upgrade_next_toast_chunk_id_typoid != OID8OID)
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("cannot support toast chunk_id type %u in binary upgrade mode",
@@ -224,6 +228,19 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	snprintf(toast_idxname, sizeof(toast_idxname),
 			 "pg_toast_%u_index", relOid);
 
+	/*
+	 * Special case here.  If OIDOldToast is defined, we need to rely on the
+	 * existing table for the job because we do not want to create an
+	 * inconsistent relation that would conflict with the parent and break
+	 * the world.
+	 */
+	if (OidIsValid(OIDOldToast))
+	{
+		toast_chunkid_typid = get_atttype(OIDOldToast, 1);
+		if (!OidIsValid(toast_chunkid_typid))
+			elog(ERROR, "cache lookup failed for relation %u", OIDOldToast);
+	}
+
 	/* this is pretty painful...  need a tuple descriptor */
 	tupdesc = CreateTemplateTupleDesc(3);
 	TupleDescInitEntry(tupdesc, (AttrNumber) 1,
@@ -336,7 +353,10 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	collationIds[0] = InvalidOid;
 	collationIds[1] = InvalidOid;
 
-	opclassIds[0] = OID_BTREE_OPS_OID;
+	if (toast_chunkid_typid == OIDOID)
+		opclassIds[0] = OID_BTREE_OPS_OID;
+	else if (toast_chunkid_typid == OID8OID)
+		opclassIds[0] = OID8_BTREE_OPS_OID;
 	opclassIds[1] = INT4_BTREE_OPS_OID;
 
 	coloptions[0] = 0;
diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml
index 67600fd974d7..afddf663fec5 100644
--- a/doc/src/sgml/storage.sgml
+++ b/doc/src/sgml/storage.sgml
@@ -421,14 +421,15 @@ most <symbol>TOAST_OID_MAX_CHUNK_SIZE</symbol> bytes (by default this value is c
 so that four chunk rows will fit on a page, making it about 2000 bytes).
 Each chunk is stored as a separate row in the <acronym>TOAST</acronym> table
 belonging to the owning table.  Every
-<acronym>TOAST</acronym> table has the columns <structfield>chunk_id</structfield> (an OID
-identifying the particular <acronym>TOAST</acronym>ed value),
+<acronym>TOAST</acronym> table has the columns
+<structfield>chunk_id</structfield> (an OID or an 8-byte integer identifying
+the particular <acronym>TOAST</acronym>ed value),
 <structfield>chunk_seq</structfield> (a sequence number for the chunk within its value),
 and <structfield>chunk_data</structfield> (the actual data of the chunk).  A unique index
 on <structfield>chunk_id</structfield> and <structfield>chunk_seq</structfield> provides fast
 retrieval of the values.  A pointer datum representing an out-of-line on-disk
 <acronym>TOAST</acronym>ed value therefore needs to store the OID of the
-<acronym>TOAST</acronym> table in which to look and the OID of the specific value
+<acronym>TOAST</acronym> table in which to look and the specific value
 (its <structfield>chunk_id</structfield>).  For convenience, pointer datums also store the
 logical datum size (original uncompressed data length), physical stored size
 (different if compression was applied), and the compression method used, if
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 9cf3c081bf01..143e6baa35cf 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1880,6 +1880,9 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	int32		last_chunk_seq;
 	Oid8		toast_valueid;
 	int32		max_chunk_size;
+	Oid			toast_typid;
+
+	toast_typid = TupleDescAttr(ctx->toast_rel->rd_att, 0)->atttypid;
 
 	extsize = ta->toast_pointer.extsize;
 
@@ -1889,10 +1892,18 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	/*
 	 * Setup a scan key to find chunks in toast table with matching va_valueid
 	 */
-	ScanKeyInit(&toastkey,
-				(AttrNumber) 1,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(ta->toast_pointer.valueid));
+	if (toast_typid == OIDOID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(ta->toast_pointer.valueid));
+	else if (toast_typid == OID8OID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OID8EQ,
+					ObjectId8GetDatum(ta->toast_pointer.valueid));
+	else
+		Assert(false);
 
 	/*
 	 * Check if any chunks for this toasted object exist in the toast table,
-- 
2.50.0

v5-0014-Add-tests-for-TOAST-relations-with-bigint-as-valu.patchtext/x-diff; charset=us-asciiDownload
From 133fa7af8ef2c043fb34d906f38dfe743448a91f Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 13:43:19 +0900
Subject: [PATCH v5 14/15] Add tests for TOAST relations with bigint as value
 type

This adds coverage for relations created with default_toast_type =
'int8', for external TOAST pointers both compressed and uncompressed.
---
 src/test/regress/expected/strings.out     | 231 ++++++++++++++++++----
 src/test/regress/expected/type_sanity.out |   6 +-
 src/test/regress/sql/strings.sql          | 134 +++++++++----
 src/test/regress/sql/type_sanity.sql      |   6 +-
 4 files changed, 296 insertions(+), 81 deletions(-)

diff --git a/src/test/regress/expected/strings.out b/src/test/regress/expected/strings.out
index ba302da51e7b..5af8d13517ca 100644
--- a/src/test/regress/expected/strings.out
+++ b/src/test/regress/expected/strings.out
@@ -1945,21 +1945,37 @@ SELECT text 'text' || varchar ' and varchar' AS "Concat text to varchar";
 (1 row)
 
 --
--- test substr with toasted text values
+-- Test substr with toasted text values, for all types of TOAST relations
+-- supported.
 --
-CREATE TABLE toasttest(f1 text);
-insert into toasttest values(repeat('1234567890',10000));
-insert into toasttest values(repeat('1234567890',10000));
+CREATE TABLE toasttest_oid(f1 text) with (toast_value_type = 'oid');
+CREATE TABLE toasttest_oid8(f1 text) with (toast_value_type = 'oid8');
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid8 values(repeat('1234567890',10000));
+insert into toasttest_oid8 values(repeat('1234567890',10000));
 --
 -- Ensure that some values are uncompressed, to test the faster substring
 -- operation used in that case
 --
-alter table toasttest alter column f1 set storage external;
-insert into toasttest values(repeat('1234567890',10000));
-insert into toasttest values(repeat('1234567890',10000));
+alter table toasttest_oid alter column f1 set storage external;
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+alter table toasttest_oid8 alter column f1 set storage external;
+insert into toasttest_oid8 values(repeat('1234567890',10000));
+insert into toasttest_oid8 values(repeat('1234567890',10000));
 -- If the starting position is zero or less, then return from the start of the string
 -- adjusting the length to be consistent with the "negative start" per SQL.
-SELECT substr(f1, -1, 5) from toasttest;
+SELECT substr(f1, -1, 5) from toasttest_oid;
+ substr 
+--------
+ 123
+ 123
+ 123
+ 123
+(4 rows)
+
+SELECT substr(f1, -1, 5) from toasttest_oid8;
  substr 
 --------
  123
@@ -1969,11 +1985,22 @@ SELECT substr(f1, -1, 5) from toasttest;
 (4 rows)
 
 -- If the length is less than zero, an ERROR is thrown.
-SELECT substr(f1, 5, -1) from toasttest;
+SELECT substr(f1, 5, -1) from toasttest_oid;
+ERROR:  negative substring length not allowed
+SELECT substr(f1, 5, -1) from toasttest_oid8;
 ERROR:  negative substring length not allowed
 -- If no third argument (length) is provided, the length to the end of the
 -- string is assumed.
-SELECT substr(f1, 99995) from toasttest;
+SELECT substr(f1, 99995) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995) from toasttest_oid8;
  substr 
 --------
  567890
@@ -1984,7 +2011,7 @@ SELECT substr(f1, 99995) from toasttest;
 
 -- If start plus length is > string length, the result is truncated to
 -- string length
-SELECT substr(f1, 99995, 10) from toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
  substr 
 --------
  567890
@@ -1993,50 +2020,105 @@ SELECT substr(f1, 99995, 10) from toasttest;
  567890
 (4 rows)
 
-TRUNCATE TABLE toasttest;
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
+SELECT substr(f1, 99995, 10) from toasttest_oid8;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+-- TRUNCATE cases for TOAST relations with OID values.
+TRUNCATE TABLE toasttest_oid;
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
 -- expect >0 blocks
 SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
-  FROM pg_class where relname = 'toasttest';
+  FROM pg_class where relname = 'toasttest_oid';
  is_empty 
 ----------
  f
 (1 row)
 
-TRUNCATE TABLE toasttest;
-ALTER TABLE toasttest set (toast_tuple_target = 4080);
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
+TRUNCATE TABLE toasttest_oid;
+ALTER TABLE toasttest_oid set (toast_tuple_target = 4080);
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
 -- expect 0 blocks
 SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
-  FROM pg_class where relname = 'toasttest';
+  FROM pg_class where relname = 'toasttest_oid';
  is_empty 
 ----------
  t
 (1 row)
 
-DROP TABLE toasttest;
+DROP TABLE toasttest_oid;
+-- TRUNCATE cases for TOAST relation with int8 values.
+TRUNCATE TABLE toasttest_oid8;
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+-- expect >0 blocks
+SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
+  FROM pg_class where relname = 'toasttest_oid8';
+ is_empty 
+----------
+ f
+(1 row)
+
+TRUNCATE TABLE toasttest_oid8;
+ALTER TABLE toasttest_oid8 set (toast_tuple_target = 4080);
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+-- expect 0 blocks
+SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
+  FROM pg_class where relname = 'toasttest_oid8';
+ is_empty 
+----------
+ t
+(1 row)
+
+DROP TABLE toasttest_oid8;
 --
--- test substr with toasted bytea values
+-- test substr with toasted bytea values, for all types of TOAST relations
+-- supported. Do not drop these two relations, for pg_upgrade.
 --
-CREATE TABLE toasttest(f1 bytea);
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
+CREATE TABLE toasttest_oid(f1 bytea) WITH (toast_value_type = 'oid');
+CREATE TABLE toasttest_oid8(f1 bytea) WITH (toast_value_type = 'oid8');
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
 --
 -- Ensure that some values are uncompressed, to test the faster substring
 -- operation used in that case
 --
-alter table toasttest alter column f1 set storage external;
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
+alter table toasttest_oid alter column f1 set storage external;
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+alter table toasttest_oid8 alter column f1 set storage external;
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
 -- If the starting position is zero or less, then return from the start of the string
 -- adjusting the length to be consistent with the "negative start" per SQL.
-SELECT substr(f1, -1, 5) from toasttest;
+SELECT substr(f1, -1, 5) from toasttest_oid;
+ substr 
+--------
+ 123
+ 123
+ 123
+ 123
+(4 rows)
+
+SELECT substr(f1, -1, 5) from toasttest_oid8;
  substr 
 --------
  123
@@ -2046,11 +2128,22 @@ SELECT substr(f1, -1, 5) from toasttest;
 (4 rows)
 
 -- If the length is less than zero, an ERROR is thrown.
-SELECT substr(f1, 5, -1) from toasttest;
+SELECT substr(f1, 5, -1) from toasttest_oid;
+ERROR:  negative substring length not allowed
+SELECT substr(f1, 5, -1) from toasttest_oid8;
 ERROR:  negative substring length not allowed
 -- If no third argument (length) is provided, the length to the end of the
 -- string is assumed.
-SELECT substr(f1, 99995) from toasttest;
+SELECT substr(f1, 99995) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995) from toasttest_oid8;
  substr 
 --------
  567890
@@ -2061,7 +2154,72 @@ SELECT substr(f1, 99995) from toasttest;
 
 -- If start plus length is > string length, the result is truncated to
 -- string length
-SELECT substr(f1, 99995, 10) from toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995, 10) from toasttest_oid8;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+-- A relation rewrite leaves the TOAST value attributes unchanged.
+VACUUM FULL toasttest_oid;
+VACUUM FULL toasttest_oid8;
+SELECT c1.relname, a.atttypid::regtype
+  FROM pg_attribute AS a,
+       pg_class AS c1,
+       pg_class AS c2
+  WHERE
+       c1.relname IN ('toasttest_oid', 'toasttest_oid8') AND
+       c1.reltoastrelid = c2.oid AND
+       a.attrelid = c2.oid AND
+       a.attname = 'chunk_id'
+  ORDER BY c1.relname COLLATE "C";
+    relname     | atttypid 
+----------------+----------
+ toasttest_oid  | oid
+ toasttest_oid8 | oid8
+(2 rows)
+
+-- Check that data slices are still accessible.
+SELECT substr(f1, 99995) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995) from toasttest_oid8;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995, 10) from toasttest_oid8;
  substr 
 --------
  567890
@@ -2070,7 +2228,6 @@ SELECT substr(f1, 99995, 10) from toasttest;
  567890
 (4 rows)
 
-DROP TABLE toasttest;
 -- test internally compressing datums
 -- this tests compressing a datum to a very small size which exercises a
 -- corner case in packed-varlena handling: even though small, the compressed
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 9ddcacec6bf4..88faa57772c3 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -578,15 +578,15 @@ WHERE c1.relnatts != (SELECT count(*) FROM pg_attribute AS a1
 (0 rows)
 
 -- Cross-check against pg_type entry
--- NOTE: we allow attstorage to be 'plain' even when typstorage is not;
--- this is mainly for toast tables.
+-- NOTE: we allow attstorage to be 'plain' or 'external' even when typstorage
+-- is not; this is mainly for toast tables.
 SELECT a1.attrelid, a1.attname, t1.oid, t1.typname
 FROM pg_attribute AS a1, pg_type AS t1
 WHERE a1.atttypid = t1.oid AND
     (a1.attlen != t1.typlen OR
      a1.attalign != t1.typalign OR
      a1.attbyval != t1.typbyval OR
-     (a1.attstorage != t1.typstorage AND a1.attstorage != 'p'));
+     (a1.attstorage != t1.typstorage AND a1.attstorage NOT IN ('e', 'p')));
  attrelid | attname | oid | typname 
 ----------+---------+-----+---------
 (0 rows)
diff --git a/src/test/regress/sql/strings.sql b/src/test/regress/sql/strings.sql
index b94004cc08ce..eb2ebff7076f 100644
--- a/src/test/regress/sql/strings.sql
+++ b/src/test/regress/sql/strings.sql
@@ -553,89 +553,147 @@ SELECT text 'text' || char(20) ' and characters' AS "Concat text to char";
 SELECT text 'text' || varchar ' and varchar' AS "Concat text to varchar";
 
 --
--- test substr with toasted text values
+-- Test substr with toasted text values, for all types of TOAST relations
+-- supported.
 --
-CREATE TABLE toasttest(f1 text);
+CREATE TABLE toasttest_oid(f1 text) with (toast_value_type = 'oid');
+CREATE TABLE toasttest_oid8(f1 text) with (toast_value_type = 'oid8');
 
-insert into toasttest values(repeat('1234567890',10000));
-insert into toasttest values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid8 values(repeat('1234567890',10000));
+insert into toasttest_oid8 values(repeat('1234567890',10000));
 
 --
 -- Ensure that some values are uncompressed, to test the faster substring
 -- operation used in that case
 --
-alter table toasttest alter column f1 set storage external;
-insert into toasttest values(repeat('1234567890',10000));
-insert into toasttest values(repeat('1234567890',10000));
+alter table toasttest_oid alter column f1 set storage external;
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+alter table toasttest_oid8 alter column f1 set storage external;
+insert into toasttest_oid8 values(repeat('1234567890',10000));
+insert into toasttest_oid8 values(repeat('1234567890',10000));
 
 -- If the starting position is zero or less, then return from the start of the string
 -- adjusting the length to be consistent with the "negative start" per SQL.
-SELECT substr(f1, -1, 5) from toasttest;
+SELECT substr(f1, -1, 5) from toasttest_oid;
+SELECT substr(f1, -1, 5) from toasttest_oid8;
 
 -- If the length is less than zero, an ERROR is thrown.
-SELECT substr(f1, 5, -1) from toasttest;
+SELECT substr(f1, 5, -1) from toasttest_oid;
+SELECT substr(f1, 5, -1) from toasttest_oid8;
 
 -- If no third argument (length) is provided, the length to the end of the
 -- string is assumed.
-SELECT substr(f1, 99995) from toasttest;
+SELECT substr(f1, 99995) from toasttest_oid;
+SELECT substr(f1, 99995) from toasttest_oid8;
 
 -- If start plus length is > string length, the result is truncated to
 -- string length
-SELECT substr(f1, 99995, 10) from toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+SELECT substr(f1, 99995, 10) from toasttest_oid8;
 
-TRUNCATE TABLE toasttest;
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
+-- TRUNCATE cases for TOAST relations with OID values.
+TRUNCATE TABLE toasttest_oid;
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
 -- expect >0 blocks
 SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
-  FROM pg_class where relname = 'toasttest';
-
-TRUNCATE TABLE toasttest;
-ALTER TABLE toasttest set (toast_tuple_target = 4080);
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
+  FROM pg_class where relname = 'toasttest_oid';
+TRUNCATE TABLE toasttest_oid;
+ALTER TABLE toasttest_oid set (toast_tuple_target = 4080);
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
 -- expect 0 blocks
 SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
-  FROM pg_class where relname = 'toasttest';
+  FROM pg_class where relname = 'toasttest_oid';
+DROP TABLE toasttest_oid;
 
-DROP TABLE toasttest;
+-- TRUNCATE cases for TOAST relation with int8 values.
+TRUNCATE TABLE toasttest_oid8;
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+-- expect >0 blocks
+SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
+  FROM pg_class where relname = 'toasttest_oid8';
+TRUNCATE TABLE toasttest_oid8;
+ALTER TABLE toasttest_oid8 set (toast_tuple_target = 4080);
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+-- expect 0 blocks
+SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
+  FROM pg_class where relname = 'toasttest_oid8';
+DROP TABLE toasttest_oid8;
 
 --
--- test substr with toasted bytea values
+-- test substr with toasted bytea values, for all types of TOAST relations
+-- supported. Do not drop these two relations, for pg_upgrade.
 --
-CREATE TABLE toasttest(f1 bytea);
+CREATE TABLE toasttest_oid(f1 bytea) WITH (toast_value_type = 'oid');
+CREATE TABLE toasttest_oid8(f1 bytea) WITH (toast_value_type = 'oid8');
 
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
 
 --
 -- Ensure that some values are uncompressed, to test the faster substring
 -- operation used in that case
 --
-alter table toasttest alter column f1 set storage external;
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
+alter table toasttest_oid alter column f1 set storage external;
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+alter table toasttest_oid8 alter column f1 set storage external;
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
 
 -- If the starting position is zero or less, then return from the start of the string
 -- adjusting the length to be consistent with the "negative start" per SQL.
-SELECT substr(f1, -1, 5) from toasttest;
+SELECT substr(f1, -1, 5) from toasttest_oid;
+SELECT substr(f1, -1, 5) from toasttest_oid8;
 
 -- If the length is less than zero, an ERROR is thrown.
-SELECT substr(f1, 5, -1) from toasttest;
+SELECT substr(f1, 5, -1) from toasttest_oid;
+SELECT substr(f1, 5, -1) from toasttest_oid8;
 
 -- If no third argument (length) is provided, the length to the end of the
 -- string is assumed.
-SELECT substr(f1, 99995) from toasttest;
+SELECT substr(f1, 99995) from toasttest_oid;
+SELECT substr(f1, 99995) from toasttest_oid8;
 
 -- If start plus length is > string length, the result is truncated to
 -- string length
-SELECT substr(f1, 99995, 10) from toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+SELECT substr(f1, 99995, 10) from toasttest_oid8;
 
-DROP TABLE toasttest;
+-- A relation rewrite leaves the TOAST value attributes unchanged.
+VACUUM FULL toasttest_oid;
+VACUUM FULL toasttest_oid8;
+SELECT c1.relname, a.atttypid::regtype
+  FROM pg_attribute AS a,
+       pg_class AS c1,
+       pg_class AS c2
+  WHERE
+       c1.relname IN ('toasttest_oid', 'toasttest_oid8') AND
+       c1.reltoastrelid = c2.oid AND
+       a.attrelid = c2.oid AND
+       a.attname = 'chunk_id'
+  ORDER BY c1.relname COLLATE "C";
+-- Check that data slices are still accessible.
+SELECT substr(f1, 99995) from toasttest_oid;
+SELECT substr(f1, 99995) from toasttest_oid8;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+SELECT substr(f1, 99995, 10) from toasttest_oid8;
 
 -- test internally compressing datums
 
diff --git a/src/test/regress/sql/type_sanity.sql b/src/test/regress/sql/type_sanity.sql
index c2496823d90e..a0d2e8bcf00b 100644
--- a/src/test/regress/sql/type_sanity.sql
+++ b/src/test/regress/sql/type_sanity.sql
@@ -420,8 +420,8 @@ WHERE c1.relnatts != (SELECT count(*) FROM pg_attribute AS a1
                       WHERE a1.attrelid = c1.oid AND a1.attnum > 0);
 
 -- Cross-check against pg_type entry
--- NOTE: we allow attstorage to be 'plain' even when typstorage is not;
--- this is mainly for toast tables.
+-- NOTE: we allow attstorage to be 'plain' or 'external' even when typstorage
+-- is not; this is mainly for toast tables.
 
 SELECT a1.attrelid, a1.attname, t1.oid, t1.typname
 FROM pg_attribute AS a1, pg_type AS t1
@@ -429,7 +429,7 @@ WHERE a1.atttypid = t1.oid AND
     (a1.attlen != t1.typlen OR
      a1.attalign != t1.typalign OR
      a1.attbyval != t1.typbyval OR
-     (a1.attstorage != t1.typstorage AND a1.attstorage != 'p'));
+     (a1.attstorage != t1.typstorage AND a1.attstorage NOT IN ('e', 'p')));
 
 -- Look for IsCatalogTextUniqueIndexOid() omissions.
 
-- 
2.50.0

v5-0015-Add-new-vartag_external-for-8-byte-TOAST-values.patchtext/x-diff; charset=us-asciiDownload
From b429fdf74595bedfa8ff158b96c7462d29db1661 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 14:10:36 +0900
Subject: [PATCH v5 15/15] Add new vartag_external for 8-byte TOAST values

This is a new type of external TOAST pointer, able to be fed 8-byte
TOAST values.  It uses a dedicated vartag_external, which is used when
a TOAST table uses bigint for its chunk_id.

The relevant callbacks are added to toast_external.c.
---
 src/include/access/heaptoast.h                |   8 +-
 src/include/varatt.h                          |  34 +++-
 src/backend/access/common/toast_external.c    | 145 ++++++++++++++++--
 src/backend/access/heap/heaptoast.c           |   1 +
 .../replication/logical/reorderbuffer.c       |  10 +-
 doc/src/sgml/storage.sgml                     |   6 +-
 contrib/amcheck/verify_heapam.c               |   2 +-
 7 files changed, 189 insertions(+), 17 deletions(-)

diff --git a/src/include/access/heaptoast.h b/src/include/access/heaptoast.h
index afa3d8ca95f7..e944d5f8420c 100644
--- a/src/include/access/heaptoast.h
+++ b/src/include/access/heaptoast.h
@@ -81,6 +81,12 @@
 
 #define EXTERN_TUPLE_MAX_SIZE	MaximumBytesPerTuple(EXTERN_TUPLES_PER_PAGE)
 
+#define TOAST_OID8_MAX_CHUNK_SIZE	\
+	(EXTERN_TUPLE_MAX_SIZE -							\
+	 MAXALIGN(SizeofHeapTupleHeader) -					\
+	 (sizeof(uint32) * 2) -								\
+	 sizeof(int32) -									\
+	 VARHDRSZ)
 #define TOAST_OID_MAX_CHUNK_SIZE	\
 	(EXTERN_TUPLE_MAX_SIZE -							\
 	 MAXALIGN(SizeofHeapTupleHeader) -					\
@@ -89,7 +95,7 @@
 	 VARHDRSZ)
 
 /* Maximum size of chunk possible */
-#define TOAST_MAX_CHUNK_SIZE	TOAST_OID_MAX_CHUNK_SIZE
+#define TOAST_MAX_CHUNK_SIZE	Max(TOAST_OID_MAX_CHUNK_SIZE, TOAST_OID8_MAX_CHUNK_SIZE)
 
 /* ----------
  * heap_toast_insert_or_update -
diff --git a/src/include/varatt.h b/src/include/varatt.h
index 035c0f95e5b6..de38d1cd1ce1 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -41,6 +41,27 @@ typedef struct varatt_external_oid
 	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
 }			varatt_external_oid;
 
+/*
+ * struct varatt_external_oid8 is a "larger" version of "TOAST pointer",
+ * that uses an 8-byte integer as value.
+ *
+ * This follows the same properties as varatt_external_oid, except that
+ * this is used in TOAST relations with oid8 as attribute for chunk_id.
+ */
+typedef struct varatt_external_oid8
+{
+	int32		va_rawsize;		/* Original data size (includes header) */
+	uint32		va_extinfo;		/* External saved size (without header) and
+								 * compression method */
+	/*
+	 * Unique ID of value within TOAST table, as two uint32 for alignment
+	 * and padding.
+	 */
+	uint32		va_valueid_lo;
+	uint32		va_valueid_hi;
+	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
+}			varatt_external_oid8;
+
 /*
  * These macros define the "saved size" portion of va_extinfo.  Its remaining
  * two high-order bits identify the compression method.
@@ -90,6 +111,7 @@ typedef enum vartag_external
 	VARTAG_INDIRECT = 1,
 	VARTAG_EXPANDED_RO = 2,
 	VARTAG_EXPANDED_RW = 3,
+	VARTAG_ONDISK_OID8 = 4,
 	VARTAG_ONDISK_OID = 18
 } vartag_external;
 
@@ -111,6 +133,8 @@ VARTAG_SIZE(vartag_external tag)
 		return sizeof(varatt_expanded);
 	else if (tag == VARTAG_ONDISK_OID)
 		return sizeof(varatt_external_oid);
+	else if (tag == VARTAG_ONDISK_OID8)
+		return sizeof(varatt_external_oid8);
 	else
 	{
 		Assert(false);
@@ -367,11 +391,19 @@ VARATT_IS_EXTERNAL_ONDISK_OID(const void *PTR)
 	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID;
 }
 
+/* Is varlena datum a pointer to on-disk toasted data with OID8 value? */
+static inline bool
+VARATT_IS_EXTERNAL_ONDISK_OID8(const void *PTR)
+{
+	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID8;
+}
+
 /* Is varlena datum a pointer to on-disk toasted data? */
 static inline bool
 VARATT_IS_EXTERNAL_ONDISK(const void *PTR)
 {
-	return VARATT_IS_EXTERNAL_ONDISK_OID(PTR);
+	return VARATT_IS_EXTERNAL_ONDISK_OID(PTR) ||
+		VARATT_IS_EXTERNAL_ONDISK_OID8(PTR);
 }
 
 /* Is varlena datum an indirect pointer? */
diff --git a/src/backend/access/common/toast_external.c b/src/backend/access/common/toast_external.c
index 8f58195051cf..f0f718085e8d 100644
--- a/src/backend/access/common/toast_external.c
+++ b/src/backend/access/common/toast_external.c
@@ -18,8 +18,19 @@
 #include "postgres.h"
 
 #include "access/detoast.h"
+#include "access/genam.h"
 #include "access/heaptoast.h"
 #include "access/toast_external.h"
+#include "catalog/catalog.h"
+#include "miscadmin.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+
+
+/* Callbacks for VARTAG_ONDISK_OID8 */
+static void ondisk_oid8_to_external_data(struct varlena *attr,
+										 toast_external_data *data);
+static struct varlena *ondisk_oid8_create_external_data(toast_external_data data);
 
 /* Callbacks for VARTAG_ONDISK_OID */
 static void ondisk_oid_to_external_data(struct varlena *attr,
@@ -28,7 +39,7 @@ static struct varlena *ondisk_oid_create_external_data(toast_external_data data)
 
 /*
  * Fetch the possibly-unaligned contents of an on-disk external TOAST with
- * OID values into a local "varatt_external_oid" pointer.
+ * OID or OID8 values into a local "varatt_external_*" pointer.
  *
  * This should be just a memcpy, but some versions of gcc seem to produce
  * broken code that assumes the datum contents are aligned.  Introducing
@@ -45,9 +56,20 @@ varatt_external_oid_get_pointer(varatt_external_oid *toast_pointer,
 	memcpy(toast_pointer, VARDATA_EXTERNAL(attre), sizeof(varatt_external_oid));
 }
 
+static inline void
+varatt_external_oid8_get_pointer(varatt_external_oid8 *toast_pointer,
+								 struct varlena *attr)
+{
+	varattrib_1b_e *attre = (varattrib_1b_e *) attr;
+
+	Assert(VARATT_IS_EXTERNAL_ONDISK_OID8(attre));
+	Assert(VARSIZE_EXTERNAL(attre) == sizeof(varatt_external_oid8) + VARHDRSZ_EXTERNAL);
+	memcpy(toast_pointer, VARDATA_EXTERNAL(attre), sizeof(varatt_external_oid8));
+}
+
 /*
  * Decompressed size of an on-disk varlena; but note argument is a struct
- * varatt_external_oid.
+ * varatt_external_oid or varatt_external_oid8.
  */
 static inline Size
 varatt_external_oid_get_extsize(varatt_external_oid toast_pointer)
@@ -55,9 +77,15 @@ varatt_external_oid_get_extsize(varatt_external_oid toast_pointer)
 	return toast_pointer.va_extinfo & VARLENA_EXTSIZE_MASK;
 }
 
+static inline Size
+varatt_external_oid8_get_extsize(varatt_external_oid8 toast_pointer)
+{
+	return toast_pointer.va_extinfo & VARLENA_EXTSIZE_MASK;
+}
+
 /*
  * Compression method of an on-disk varlena; but note argument is a struct
- *  varatt_external_oid.
+ *  varatt_external_oid or varatt_external_oid8.
  */
 static inline uint32
 varatt_external_oid_get_compress_method(varatt_external_oid toast_pointer)
@@ -65,6 +93,12 @@ varatt_external_oid_get_compress_method(varatt_external_oid toast_pointer)
 	return toast_pointer.va_extinfo >> VARLENA_EXTSIZE_BITS;
 }
 
+static inline uint32
+varatt_external_oid8_get_compress_method(varatt_external_oid8 toast_pointer)
+{
+	return toast_pointer.va_extinfo >> VARLENA_EXTSIZE_BITS;
+}
+
 /*
  * Testing whether an externally-stored TOAST value is compressed now requires
  * comparing size stored in va_extinfo (the actual length of the external data)
@@ -79,6 +113,19 @@ varatt_external_oid_is_compressed(varatt_external_oid toast_pointer)
 		(Size) (toast_pointer.va_rawsize - VARHDRSZ);
 }
 
+static inline bool
+varatt_external_oid8_is_compressed(varatt_external_oid8 toast_pointer)
+{
+	return varatt_external_oid8_get_extsize(toast_pointer) <
+		(Size) (toast_pointer.va_rawsize - VARHDRSZ);
+}
+
+/*
+ * Size of an EXTERNAL datum that contains a standard TOAST pointer
+ * (oid8 value).
+ */
+#define TOAST_POINTER_OID8_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_oid8))
+
 /*
  * Size of an EXTERNAL datum that contains a standard TOAST pointer (OID
  * value).
@@ -99,6 +146,12 @@ varatt_external_oid_is_compressed(varatt_external_oid toast_pointer)
  * individual fields.
  */
 static const toast_external_info toast_external_infos[TOAST_EXTERNAL_INFO_SIZE] = {
+	[VARTAG_ONDISK_OID8] = {
+		.toast_pointer_size = TOAST_POINTER_OID8_SIZE,
+		.maximum_chunk_size = TOAST_OID_MAX_CHUNK_SIZE,
+		.to_external_data = ondisk_oid8_to_external_data,
+		.create_external_data = ondisk_oid8_create_external_data,
+	},
 	[VARTAG_ONDISK_OID] = {
 		.toast_pointer_size = TOAST_OID_POINTER_SIZE,
 		.maximum_chunk_size = TOAST_OID_MAX_CHUNK_SIZE,
@@ -150,22 +203,33 @@ toast_external_info_get_pointer_size(uint8 tag)
 uint8
 toast_external_assign_vartag(Oid toastrelid, Oid8 valueid)
 {
+	Oid		toast_typid;
+
 	/*
-	 * If dealing with a code path where a TOAST relation may not be assigned,
-	 * like heap_toast_insert_or_update(), just use the legacy
-	 * vartag_external.
+	 * If dealing with a code path where a TOAST relation may not be assigned
+	 * like heap_toast_insert_or_update(), just use the default with an OID
+	 * type.
+	 *
+	 * In bootstrap mode, we should not do any kind of syscache lookups,
+	 * so also rely on OID.
 	 */
-	if (!OidIsValid(toastrelid))
+	if (!OidIsValid(toastrelid) || IsBootstrapProcessingMode())
 		return VARTAG_ONDISK_OID;
 
 	/*
-	 * Currently there is only one type of vartag_external supported: 4-byte
-	 * value with OID for the chunk_id type.
+	 * Two types of vartag_external are currently supported: OID and OID8,
+	 * which depend on the type assigned to "chunk_id" for the TOAST table.
 	 *
-	 * Note: This routine will be extended to be able to use multiple
-	 * vartag_external within a single TOAST relation type, that may change
-	 * depending on the value used.
+	 * XXX: Should we assign from the start an OID vartag if dealing with
+	 * a TOAST relation with OID8 as value if the value assigned is less
+	 * than UINT_MAX?  This just takes the "safe" approach of assigning
+	 * the larger vartag in all cases, but this can be made cheaper
+	 * depending on the OID consumption.
 	 */
+	toast_typid = get_atttype(toastrelid, 1);
+	if (toast_typid == OID8OID)
+		return VARTAG_ONDISK_OID8;
+
 	return VARTAG_ONDISK_OID;
 }
 
@@ -174,6 +238,63 @@ toast_external_assign_vartag(Oid toastrelid, Oid8 valueid)
  * the in-memory representation toast_external_data used in the backend.
  */
 
+/* Callbacks for VARTAG_ONDISK_OID8 */
+static void
+ondisk_oid8_to_external_data(struct varlena *attr, toast_external_data *data)
+{
+	varatt_external_oid8	external;
+
+	varatt_external_oid8_get_pointer(&external, attr);
+	data->rawsize = external.va_rawsize;
+
+	/* External size and compression methods are stored in the same field */
+	if (varatt_external_oid8_is_compressed(external))
+	{
+		data->extsize = varatt_external_oid8_get_extsize(external);
+		data->compression_method = varatt_external_oid8_get_compress_method(external);
+	}
+	else
+	{
+		data->extsize = external.va_extinfo;
+		data->compression_method = TOAST_INVALID_COMPRESSION_ID;
+	}
+
+	data->valueid = (((uint64) external.va_valueid_hi) << 32) |
+		external.va_valueid_lo;
+	data->toastrelid = external.va_toastrelid;
+
+}
+
+static struct varlena *
+ondisk_oid8_create_external_data(toast_external_data data)
+{
+	struct varlena *result = NULL;
+	varatt_external_oid8 external;
+
+	external.va_rawsize = data.rawsize;
+
+	if (data.compression_method != TOAST_INVALID_COMPRESSION_ID)
+	{
+		/* Set size and compression method, in a single field. */
+		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(external,
+													 data.extsize,
+													 data.compression_method);
+	}
+	else
+		external.va_extinfo = data.extsize;
+
+	external.va_toastrelid = data.toastrelid;
+	external.va_valueid_hi = (((uint64) data.valueid) >> 32);
+	external.va_valueid_lo = (uint32) data.valueid;
+
+	result = (struct varlena *) palloc(TOAST_POINTER_OID8_SIZE);
+	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_OID8);
+	memcpy(VARDATA_EXTERNAL(result), &external, sizeof(external));
+
+	return result;
+}
+
+
 /* Callbacks for VARTAG_ONDISK_OID */
 
 /*
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index 50e9bf9047f9..cba6e14ea805 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -32,6 +32,7 @@
 #include "access/toast_helper.h"
 #include "access/toast_internals.h"
 #include "utils/fmgroids.h"
+#include "utils/syscache.h"
 
 
 /* ----------
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 9a690a59db2c..f8a207b09157 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -4971,14 +4971,22 @@ ReorderBufferToastAppendChunk(ReorderBuffer *rb, ReorderBufferTXN *txn,
 	TupleDesc	desc = RelationGetDescr(relation);
 	Oid8		chunk_id;
 	int32		chunk_seq;
+	Oid			toast_typid;
 
 	if (txn->toast_hash == NULL)
 		ReorderBufferToastInitHash(rb, txn);
+	toast_typid = TupleDescAttr(desc, 0)->atttypid;
 
 	Assert(IsToastRelation(relation));
 
 	newtup = change->data.tp.newtuple;
-	chunk_id = DatumGetObjectId(fastgetattr(newtup, 1, desc, &isnull));
+	/* This depends on the type of TOAST value dealt with. */
+	if (toast_typid == OIDOID)
+		chunk_id = DatumGetObjectId(fastgetattr(newtup, 1, desc, &isnull));
+	else if (toast_typid == INT8OID)
+		chunk_id = DatumGetUInt64(fastgetattr(newtup, 1, desc, &isnull));
+	else
+		Assert(false);
 	Assert(!isnull);
 	chunk_seq = DatumGetInt32(fastgetattr(newtup, 2, desc, &isnull));
 	Assert(!isnull);
diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml
index afddf663fec5..dbec30d48b4a 100644
--- a/doc/src/sgml/storage.sgml
+++ b/doc/src/sgml/storage.sgml
@@ -417,7 +417,11 @@ described in more detail below.
 
 <para>
 Out-of-line values are divided (after compression if used) into chunks of at
-most <symbol>TOAST_OID_MAX_CHUNK_SIZE</symbol> bytes (by default this value is chosen
+most <symbol>TOAST_OID_MAX_CHUNK_SIZE</symbol> bytes if the
+<acronym>TOAST</acronym> relation uses the <literal>oid</literal> type for
+<literal>chunk_id</literal>, or <symbol>TOAST_OID8_MAX_CHUNK_SIZE</symbol>
+bytes if the <acronym>TOAST</acronym> relation uses the <literal>oid8</literal>
+type for <literal>chunk_id</literal> (by default these values are chosen
 so that four chunk rows will fit on a page, making it about 2000 bytes).
 Each chunk is stored as a separate row in the <acronym>TOAST</acronym> table
 belonging to the owning table.  Every
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 143e6baa35cf..8cea9ad31bcd 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1733,7 +1733,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	{
 		uint8		va_tag = VARTAG_EXTERNAL(tp + ctx->offset);
 
-		if (va_tag != VARTAG_ONDISK_OID)
+		if (va_tag != VARTAG_ONDISK_OID && va_tag != VARTAG_ONDISK_OID8)
 		{
 			report_corruption(ctx,
 							  psprintf("toasted attribute has unexpected TOAST tag %u",
-- 
2.50.0

#47Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#46)
15 attachment(s)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

On Thu, Aug 14, 2025 at 02:49:06PM +0900, Michael Paquier wrote:

I have dropped the amcheck test patch for now, which was fun but it's
not really necessary for the "basics". I have done also more tests,
playing for example with pg_resetwal, installcheck and pg_upgrade
scenarios. I am wondering if it would be worth doing a pg_resetwal in
the node doing an installcheck on the instance to be upgraded, bumping
its next OID to be much larger than 4 billion, actually..

Four patches had conflicts with 748caa9dcb68, so rebased as v6.
--
Michael

Attachments:

v6-0001-Implement-oid8-data-type.patchtext/x-diff; charset=us-asciiDownload
From 21652f43f29713f4e394ac0ff7573056fece7a32 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 11:03:17 +0900
Subject: [PATCH v6 01/15] Implement oid8 data type

This new identifier type will be used for 8-byte TOAST values, and can
be useful for other purposes, not yet defined as of writing this patch.
The following operators are added for this data type:
- Casts with integer types and OID.
- btree and hash operators
- min/max functions.
- Tests and documentation.

XXX: Requires catversion bump.
---
 src/include/c.h                           |  11 +-
 src/include/catalog/pg_aggregate.dat      |   6 +
 src/include/catalog/pg_amop.dat           |  23 +++
 src/include/catalog/pg_amproc.dat         |  12 ++
 src/include/catalog/pg_cast.dat           |  14 ++
 src/include/catalog/pg_opclass.dat        |   4 +
 src/include/catalog/pg_operator.dat       |  26 +++
 src/include/catalog/pg_opfamily.dat       |   4 +
 src/include/catalog/pg_proc.dat           |  64 +++++++
 src/include/catalog/pg_type.dat           |   5 +
 src/include/fmgr.h                        |   2 +
 src/include/postgres.h                    |  20 +++
 src/include/postgres_ext.h                |   1 -
 src/backend/access/nbtree/nbtcompare.c    |  82 +++++++++
 src/backend/bootstrap/bootstrap.c         |   2 +
 src/backend/utils/adt/Makefile            |   1 +
 src/backend/utils/adt/int8.c              |   8 +
 src/backend/utils/adt/meson.build         |   1 +
 src/backend/utils/adt/oid8.c              | 171 +++++++++++++++++++
 src/fe_utils/print.c                      |   1 +
 src/test/regress/expected/oid8.out        | 196 ++++++++++++++++++++++
 src/test/regress/expected/oid8.sql        |   0
 src/test/regress/expected/opr_sanity.out  |   7 +
 src/test/regress/expected/type_sanity.out |   1 +
 src/test/regress/parallel_schedule        |   2 +-
 src/test/regress/sql/oid8.sql             |  57 +++++++
 src/test/regress/sql/type_sanity.sql      |   1 +
 doc/src/sgml/datatype.sgml                |  11 ++
 doc/src/sgml/func/func-aggregate.sgml     |   8 +-
 29 files changed, 734 insertions(+), 7 deletions(-)
 create mode 100644 src/backend/utils/adt/oid8.c
 create mode 100644 src/test/regress/expected/oid8.out
 create mode 100644 src/test/regress/expected/oid8.sql
 create mode 100644 src/test/regress/sql/oid8.sql

diff --git a/src/include/c.h b/src/include/c.h
index f303ba0605a4..9751b8155433 100644
--- a/src/include/c.h
+++ b/src/include/c.h
@@ -529,6 +529,7 @@ typedef uint32 bits32;			/* >= 32 bits */
 /* snprintf format strings to use for 64-bit integers */
 #define INT64_FORMAT "%" PRId64
 #define UINT64_FORMAT "%" PRIu64
+#define OID8_FORMAT "%" PRIu64
 
 /*
  * 128-bit signed and unsigned integers
@@ -615,7 +616,7 @@ typedef double float8;
 #define FLOAT8PASSBYVAL true
 
 /*
- * Oid, RegProcedure, TransactionId, SubTransactionId, MultiXactId,
+ * Oid, Oid8, RegProcedure, TransactionId, SubTransactionId, MultiXactId,
  * CommandId
  */
 
@@ -647,6 +648,12 @@ typedef uint32 CommandId;
 #define FirstCommandId	((CommandId) 0)
 #define InvalidCommandId	(~(CommandId)0)
 
+/* 8-byte Object ID */
+typedef uint64 Oid8;
+
+#define InvalidOid8		((Oid8) 0)
+#define OID8_MAX	UINT64_MAX
+#define atooid8(x) ((Oid8) strtou64((x), NULL, 10))
 
 /* ----------------
  *		Variable-length datatypes all share the 'struct varlena' header.
@@ -753,6 +760,8 @@ typedef NameData *Name;
 
 #define OidIsValid(objectId)  ((bool) ((objectId) != InvalidOid))
 
+#define Oid8IsValid(objectId)  ((bool) ((objectId) != InvalidOid8))
+
 #define RegProcedureIsValid(p)	OidIsValid(p)
 
 
diff --git a/src/include/catalog/pg_aggregate.dat b/src/include/catalog/pg_aggregate.dat
index d6aa1f6ec478..75acf4ef96cd 100644
--- a/src/include/catalog/pg_aggregate.dat
+++ b/src/include/catalog/pg_aggregate.dat
@@ -104,6 +104,9 @@
 { aggfnoid => 'max(oid)', aggtransfn => 'oidlarger',
   aggcombinefn => 'oidlarger', aggsortop => '>(oid,oid)',
   aggtranstype => 'oid' },
+{ aggfnoid => 'max(oid8)', aggtransfn => 'oid8larger',
+  aggcombinefn => 'oid8larger', aggsortop => '>(oid8,oid8)',
+  aggtranstype => 'oid8' },
 { aggfnoid => 'max(float4)', aggtransfn => 'float4larger',
   aggcombinefn => 'float4larger', aggsortop => '>(float4,float4)',
   aggtranstype => 'float4' },
@@ -178,6 +181,9 @@
 { aggfnoid => 'min(oid)', aggtransfn => 'oidsmaller',
   aggcombinefn => 'oidsmaller', aggsortop => '<(oid,oid)',
   aggtranstype => 'oid' },
+{ aggfnoid => 'min(oid8)', aggtransfn => 'oid8smaller',
+  aggcombinefn => 'oid8smaller', aggsortop => '<(oid8,oid8)',
+  aggtranstype => 'oid8' },
 { aggfnoid => 'min(float4)', aggtransfn => 'float4smaller',
   aggcombinefn => 'float4smaller', aggsortop => '<(float4,float4)',
   aggtranstype => 'float4' },
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 2a693cfc31c6..2c3004d53611 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -180,6 +180,24 @@
 { amopfamily => 'btree/oid_ops', amoplefttype => 'oid', amoprighttype => 'oid',
   amopstrategy => '5', amopopr => '>(oid,oid)', amopmethod => 'btree' },
 
+# btree oid8_ops
+
+{ amopfamily => 'btree/oid8_ops', amoplefttype => 'oid8',
+  amoprighttype => 'oid8', amopstrategy => '1', amopopr => '<(oid8,oid8)',
+  amopmethod => 'btree' },
+{ amopfamily => 'btree/oid8_ops', amoplefttype => 'oid8',
+  amoprighttype => 'oid8', amopstrategy => '2', amopopr => '<=(oid8,oid8)',
+  amopmethod => 'btree' },
+{ amopfamily => 'btree/oid8_ops', amoplefttype => 'oid8',
+  amoprighttype => 'oid8', amopstrategy => '3', amopopr => '=(oid8,oid8)',
+  amopmethod => 'btree' },
+{ amopfamily => 'btree/oid8_ops', amoplefttype => 'oid8',
+  amoprighttype => 'oid8', amopstrategy => '4', amopopr => '>=(oid8,oid8)',
+  amopmethod => 'btree' },
+{ amopfamily => 'btree/oid8_ops', amoplefttype => 'oid8',
+  amoprighttype => 'oid8', amopstrategy => '5', amopopr => '>(oid8,oid8)',
+  amopmethod => 'btree' },
+
 # btree xid8_ops
 
 { amopfamily => 'btree/xid8_ops', amoplefttype => 'xid8',
@@ -974,6 +992,11 @@
 { amopfamily => 'hash/oid_ops', amoplefttype => 'oid', amoprighttype => 'oid',
   amopstrategy => '1', amopopr => '=(oid,oid)', amopmethod => 'hash' },
 
+# oid8_ops
+{ amopfamily => 'hash/oid8_ops', amoplefttype => 'oid8',
+  amoprighttype => 'oid8', amopstrategy => '1', amopopr => '=(oid8,oid8)',
+  amopmethod => 'hash' },
+
 # oidvector_ops
 { amopfamily => 'hash/oidvector_ops', amoplefttype => 'oidvector',
   amoprighttype => 'oidvector', amopstrategy => '1',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index e3477500baa7..d3719b3610c4 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -213,6 +213,14 @@
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' },
+{ amprocfamily => 'btree/oid8_ops', amproclefttype => 'oid8',
+  amprocrighttype => 'oid8', amprocnum => '1', amproc => 'btoid8cmp' },
+{ amprocfamily => 'btree/oid8_ops', amproclefttype => 'oid8',
+  amprocrighttype => 'oid8', amprocnum => '2', amproc => 'btoid8sortsupport' },
+{ amprocfamily => 'btree/oid8_ops', amproclefttype => 'oid8',
+  amprocrighttype => 'oid8', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/oid8_ops', amproclefttype => 'oid8',
+  amprocrighttype => 'oid8', amprocnum => '6', amproc => 'btoid8skipsupport' },
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
@@ -432,6 +440,10 @@
   amprocrighttype => 'xid8', amprocnum => '1', amproc => 'hashxid8' },
 { amprocfamily => 'hash/xid8_ops', amproclefttype => 'xid8',
   amprocrighttype => 'xid8', amprocnum => '2', amproc => 'hashxid8extended' },
+{ amprocfamily => 'hash/oid8_ops', amproclefttype => 'oid8',
+  amprocrighttype => 'oid8', amprocnum => '1', amproc => 'hashoid8' },
+{ amprocfamily => 'hash/oid8_ops', amproclefttype => 'oid8',
+  amprocrighttype => 'oid8', amprocnum => '2', amproc => 'hashoid8extended' },
 { amprocfamily => 'hash/cid_ops', amproclefttype => 'cid',
   amprocrighttype => 'cid', amprocnum => '1', amproc => 'hashcid' },
 { amprocfamily => 'hash/cid_ops', amproclefttype => 'cid',
diff --git a/src/include/catalog/pg_cast.dat b/src/include/catalog/pg_cast.dat
index fbfd669587f0..695f6b2a5e73 100644
--- a/src/include/catalog/pg_cast.dat
+++ b/src/include/catalog/pg_cast.dat
@@ -296,6 +296,20 @@
 { castsource => 'regdatabase', casttarget => 'int4', castfunc => '0',
   castcontext => 'a', castmethod => 'b' },
 
+# OID8 category: allow implicit conversion from any integral type (including
+# int8), as well as assignment coercion to int8.
+{ castsource => 'int8', casttarget => 'oid8', castfunc => '0',
+  castcontext => 'i', castmethod => 'b' },
+{ castsource => 'int2', casttarget => 'oid8', castfunc => 'int8(int2)',
+  castcontext => 'i', castmethod => 'f' },
+{ castsource => 'int4', casttarget => 'oid8', castfunc => 'int8(int4)',
+  castcontext => 'i', castmethod => 'f' },
+{ castsource => 'oid8', casttarget => 'int8', castfunc => '0',
+  castcontext => 'a', castmethod => 'b' },
+# Assignment coercion from oid to oid8.
+{ castsource => 'oid', casttarget => 'oid8', castfunc => 'oid8(oid)',
+  castcontext => 'a', castmethod => 'f' },
+
 # String category
 { castsource => 'text', casttarget => 'bpchar', castfunc => '0',
   castcontext => 'i', castmethod => 'b' },
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 4a9624802aa5..c0de88fabc49 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -177,6 +177,10 @@
   opcintype => 'xid8' },
 { opcmethod => 'btree', opcname => 'xid8_ops', opcfamily => 'btree/xid8_ops',
   opcintype => 'xid8' },
+{ opcmethod => 'hash', opcname => 'oid8_ops', opcfamily => 'hash/oid8_ops',
+  opcintype => 'oid8' },
+{ opcmethod => 'btree', opcname => 'oid8_ops', opcfamily => 'btree/oid8_ops',
+  opcintype => 'oid8' },
 { opcmethod => 'hash', opcname => 'cid_ops', opcfamily => 'hash/cid_ops',
   opcintype => 'cid' },
 { opcmethod => 'hash', opcname => 'tid_ops', opcfamily => 'hash/tid_ops',
diff --git a/src/include/catalog/pg_operator.dat b/src/include/catalog/pg_operator.dat
index 6d9dc1528d6e..87a7255490a7 100644
--- a/src/include/catalog/pg_operator.dat
+++ b/src/include/catalog/pg_operator.dat
@@ -3460,4 +3460,30 @@
   oprcode => 'multirange_after_multirange', oprrest => 'multirangesel',
   oprjoin => 'scalargtjoinsel' },
 
+{ oid => '8262', descr => 'equal',
+  oprname => '=', oprcanmerge => 't', oprcanhash => 't', oprleft => 'oid8',
+  oprright => 'oid8', oprresult => 'bool', oprcom => '=(oid8,oid8)',
+  oprnegate => '<>(oid8,oid8)', oprcode => 'oid8eq', oprrest => 'eqsel',
+  oprjoin => 'eqjoinsel' },
+{ oid => '8263', descr => 'not equal',
+  oprname => '<>', oprleft => 'oid8', oprright => 'oid8', oprresult => 'bool',
+  oprcom => '<>(oid8,oid8)', oprnegate => '=(oid8,oid8)', oprcode => 'oid8ne',
+  oprrest => 'neqsel', oprjoin => 'neqjoinsel' },
+{ oid => '8264', descr => 'less than',
+  oprname => '<', oprleft => 'oid8', oprright => 'oid8', oprresult => 'bool',
+  oprcom => '>(oid8,oid8)', oprnegate => '>=(oid8,oid8)', oprcode => 'oid8lt',
+  oprrest => 'scalarltsel', oprjoin => 'scalarltjoinsel' },
+{ oid => '8265', descr => 'greater than',
+  oprname => '>', oprleft => 'oid8', oprright => 'oid8', oprresult => 'bool',
+  oprcom => '<(oid8,oid8)', oprnegate => '<=(oid8,oid8)', oprcode => 'oid8gt',
+  oprrest => 'scalargtsel', oprjoin => 'scalargtjoinsel' },
+{ oid => '8266', descr => 'less than or equal',
+  oprname => '<=', oprleft => 'oid8', oprright => 'oid8', oprresult => 'bool',
+  oprcom => '>=(oid8,oid8)', oprnegate => '>(oid8,oid8)', oprcode => 'oid8le',
+  oprrest => 'scalarlesel', oprjoin => 'scalarlejoinsel' },
+{ oid => '8267', descr => 'greater than or equal',
+  oprname => '>=', oprleft => 'oid8', oprright => 'oid8', oprresult => 'bool',
+  oprcom => '<=(oid8,oid8)', oprnegate => '<(oid8,oid8)', oprcode => 'oid8ge',
+  oprrest => 'scalargesel', oprjoin => 'scalargejoinsel' },
+
 ]
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index f7dcb96b43ce..54472ce97dcd 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -116,6 +116,10 @@
   opfmethod => 'hash', opfname => 'xid8_ops' },
 { oid => '5067',
   opfmethod => 'btree', opfname => 'xid8_ops' },
+{ oid => '8278',
+  opfmethod => 'hash', opfname => 'oid8_ops' },
+{ oid => '8279',
+  opfmethod => 'btree', opfname => 'oid8_ops' },
 { oid => '2226',
   opfmethod => 'hash', opfname => 'cid_ops' },
 { oid => '2227',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 03e82d28c876..67fbb085024c 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1046,6 +1046,15 @@
 { oid => '6405', descr => 'skip support',
   proname => 'btoidskipsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btoidskipsupport' },
+{ oid => '8282', descr => 'less-equal-greater',
+  proname => 'btoid8cmp', proleakproof => 't', prorettype => 'int4',
+  proargtypes => 'oid8 oid8', prosrc => 'btoid8cmp' },
+{ oid => '8283', descr => 'sort support',
+  proname => 'btoid8sortsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoid8sortsupport' },
+{ oid => '8284', descr => 'skip support',
+  proname => 'btoid8skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoid8skipsupport' },
 { oid => '404', descr => 'less-equal-greater',
   proname => 'btoidvectorcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'oidvector oidvector', prosrc => 'btoidvectorcmp' },
@@ -12588,4 +12597,59 @@
   proargnames => '{pid,io_id,io_generation,state,operation,off,length,target,handle_data_len,raw_result,result,target_desc,f_sync,f_localmem,f_buffered}',
   prosrc => 'pg_get_aios' },
 
+# oid8 related functions
+{ oid => '8255', descr => 'convert oid to oid8',
+  proname => 'oid8', prorettype => 'oid8', proargtypes => 'oid',
+  prosrc => 'oidtooid8' },
+{ oid => '8257', descr => 'I/O',
+  proname => 'oid8in', prorettype => 'oid8', proargtypes => 'cstring',
+  prosrc => 'oid8in' },
+{ oid => '8258', descr => 'I/O',
+  proname => 'oid8out', prorettype => 'cstring', proargtypes => 'oid8',
+  prosrc => 'oid8out' },
+{ oid => '8259', descr => 'I/O',
+  proname => 'oid8recv', prorettype => 'oid8', proargtypes => 'internal',
+  prosrc => 'oid8recv' },
+{ oid => '8260', descr => 'I/O',
+  proname => 'oid8send', prorettype => 'bytea', proargtypes => 'oid8',
+  prosrc => 'oid8send' },
+# Comparators
+{ oid => '8268',
+  proname => 'oid8eq', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'oid8 oid8', prosrc => 'oid8eq' },
+{ oid => '8269',
+  proname => 'oid8ne', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'oid8 oid8', prosrc => 'oid8ne' },
+{ oid => '8270',
+  proname => 'oid8lt', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'oid8 oid8', prosrc => 'oid8lt' },
+{ oid => '8271',
+  proname => 'oid8le', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'oid8 oid8', prosrc => 'oid8le' },
+{ oid => '8272',
+  proname => 'oid8gt', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'oid8 oid8', prosrc => 'oid8gt' },
+{ oid => '8273',
+  proname => 'oid8ge', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'oid8 oid8', prosrc => 'oid8ge' },
+# Aggregates
+{ oid => '8274', descr => 'larger of two',
+  proname => 'oid8larger', prorettype => 'oid8', proargtypes => 'oid8 oid8',
+  prosrc => 'oid8larger' },
+{ oid => '8275', descr => 'smaller of two',
+  proname => 'oid8smaller', prorettype => 'oid8', proargtypes => 'oid8 oid8',
+  prosrc => 'oid8smaller' },
+{ oid => '8276', descr => 'maximum value of all oid8 input values',
+  proname => 'max', prokind => 'a', proisstrict => 'f', prorettype => 'oid8',
+  proargtypes => 'oid8', prosrc => 'aggregate_dummy' },
+{ oid => '8277', descr => 'minimum value of all oid8 input values',
+  proname => 'min', prokind => 'a', proisstrict => 'f', prorettype => 'oid8',
+  proargtypes => 'oid8', prosrc => 'aggregate_dummy' },
+{ oid => '8280', descr => 'hash',
+  proname => 'hashoid8', prorettype => 'int4', proargtypes => 'oid8',
+  prosrc => 'hashoid8' },
+{ oid => '8281', descr => 'hash',
+  proname => 'hashoid8extended', prorettype => 'int8',
+  proargtypes => 'oid8 int8', prosrc => 'hashoid8extended' },
+
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index cb730aeac864..704f2890cb28 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -700,4 +700,9 @@
   typreceive => 'brin_minmax_multi_summary_recv',
   typsend => 'brin_minmax_multi_summary_send', typalign => 'i',
   typstorage => 'x', typcollation => 'default' },
+{ oid => '8256', array_type_oid => '8261',
+  descr => 'object identifier(oid8), 8 bytes',
+  typname => 'oid8', typlen => '8', typbyval => 't',
+  typcategory => 'N', typinput => 'oid8in', typoutput => 'oid8out',
+  typreceive => 'oid8recv', typsend => 'oid8send', typalign => 'd' },
 ]
diff --git a/src/include/fmgr.h b/src/include/fmgr.h
index 74fe3ea05758..c127d2f87585 100644
--- a/src/include/fmgr.h
+++ b/src/include/fmgr.h
@@ -273,6 +273,7 @@ extern struct varlena *pg_detoast_datum_packed(struct varlena *datum);
 #define PG_GETARG_CHAR(n)	 DatumGetChar(PG_GETARG_DATUM(n))
 #define PG_GETARG_BOOL(n)	 DatumGetBool(PG_GETARG_DATUM(n))
 #define PG_GETARG_OID(n)	 DatumGetObjectId(PG_GETARG_DATUM(n))
+#define PG_GETARG_OID8(n)	 DatumGetObjectId8(PG_GETARG_DATUM(n))
 #define PG_GETARG_POINTER(n) DatumGetPointer(PG_GETARG_DATUM(n))
 #define PG_GETARG_CSTRING(n) DatumGetCString(PG_GETARG_DATUM(n))
 #define PG_GETARG_NAME(n)	 DatumGetName(PG_GETARG_DATUM(n))
@@ -358,6 +359,7 @@ extern struct varlena *pg_detoast_datum_packed(struct varlena *datum);
 #define PG_RETURN_CHAR(x)	 return CharGetDatum(x)
 #define PG_RETURN_BOOL(x)	 return BoolGetDatum(x)
 #define PG_RETURN_OID(x)	 return ObjectIdGetDatum(x)
+#define PG_RETURN_OID8(x)	 return ObjectId8GetDatum(x)
 #define PG_RETURN_POINTER(x) return PointerGetDatum(x)
 #define PG_RETURN_CSTRING(x) return CStringGetDatum(x)
 #define PG_RETURN_NAME(x)	 return NameGetDatum(x)
diff --git a/src/include/postgres.h b/src/include/postgres.h
index 357cbd6fd961..a5a0e3b7cbfa 100644
--- a/src/include/postgres.h
+++ b/src/include/postgres.h
@@ -264,6 +264,26 @@ ObjectIdGetDatum(Oid X)
 	return (Datum) X;
 }
 
+/*
+ * DatumGetObjectId8
+ *		Returns 8-byte object identifier value of a datum.
+ */
+static inline Oid8
+DatumGetObjectId8(Datum X)
+{
+	return (Oid8) X;
+}
+
+/*
+ * ObjectId8GetDatum
+ *		Returns datum representation for an 8-byte object identifier
+ */
+static inline Datum
+ObjectId8GetDatum(Oid8 X)
+{
+	return (Datum) X;
+}
+
 /*
  * DatumGetTransactionId
  *		Returns transaction identifier value of a datum.
diff --git a/src/include/postgres_ext.h b/src/include/postgres_ext.h
index bf45c50dcf31..c80b195bf235 100644
--- a/src/include/postgres_ext.h
+++ b/src/include/postgres_ext.h
@@ -41,7 +41,6 @@ typedef unsigned int Oid;
 #define atooid(x) ((Oid) strtoul((x), NULL, 10))
 /* the above needs <stdlib.h> */
 
-
 /*
  * Identifiers of error message fields.  Kept here to keep common
  * between frontend and backend, and also to export them to libpq
diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c
index 188c27b4925f..3f59ba3f1ad0 100644
--- a/src/backend/access/nbtree/nbtcompare.c
+++ b/src/backend/access/nbtree/nbtcompare.c
@@ -498,6 +498,88 @@ btoidskipsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+Datum
+btoid8cmp(PG_FUNCTION_ARGS)
+{
+	Oid8		a = PG_GETARG_OID8(0);
+	Oid8		b = PG_GETARG_OID8(1);
+
+	if (a > b)
+		PG_RETURN_INT32(A_GREATER_THAN_B);
+	else if (a == b)
+		PG_RETURN_INT32(0);
+	else
+		PG_RETURN_INT32(A_LESS_THAN_B);
+}
+
+static int
+btoid8fastcmp(Datum x, Datum y, SortSupport ssup)
+{
+	Oid8		a = DatumGetObjectId8(x);
+	Oid8		b = DatumGetObjectId8(y);
+
+	if (a > b)
+		return A_GREATER_THAN_B;
+	else if (a == b)
+		return 0;
+	else
+		return A_LESS_THAN_B;
+}
+
+Datum
+btoid8sortsupport(PG_FUNCTION_ARGS)
+{
+	SortSupport ssup = (SortSupport) PG_GETARG_POINTER(0);
+
+	ssup->comparator = btoid8fastcmp;
+	PG_RETURN_VOID();
+}
+
+static Datum
+oid8_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Oid8		oexisting = DatumGetObjectId8(existing);
+
+	if (oexisting == InvalidOid8)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return ObjectId8GetDatum(oexisting - 1);
+}
+
+static Datum
+oid8_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Oid8		oexisting = DatumGetObjectId8(existing);
+
+	if (oexisting == OID8_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return ObjectId8GetDatum(oexisting + 1);
+}
+
+Datum
+btoid8skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = oid8_decrement;
+	sksup->increment = oid8_increment;
+	sksup->low_elem = ObjectId8GetDatum(InvalidOid8);
+	sksup->high_elem = ObjectId8GetDatum(OID8_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btoidvectorcmp(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/bootstrap/bootstrap.c b/src/backend/bootstrap/bootstrap.c
index fc8638c1b61b..48e6966e6b48 100644
--- a/src/backend/bootstrap/bootstrap.c
+++ b/src/backend/bootstrap/bootstrap.c
@@ -115,6 +115,8 @@ static const struct typinfo TypInfo[] = {
 	F_TEXTIN, F_TEXTOUT},
 	{"oid", OIDOID, 0, 4, true, TYPALIGN_INT, TYPSTORAGE_PLAIN, InvalidOid,
 	F_OIDIN, F_OIDOUT},
+	{"oid8", OID8OID, 0, 8, true, TYPALIGN_DOUBLE, TYPSTORAGE_PLAIN, InvalidOid,
+	F_OID8IN, F_OID8OUT},
 	{"tid", TIDOID, 0, 6, false, TYPALIGN_SHORT, TYPSTORAGE_PLAIN, InvalidOid,
 	F_TIDIN, F_TIDOUT},
 	{"xid", XIDOID, 0, 4, true, TYPALIGN_INT, TYPSTORAGE_PLAIN, InvalidOid,
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index cc68ac545a5f..2b1bfcea516c 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -77,6 +77,7 @@ OBJS = \
 	numeric.o \
 	numutils.o \
 	oid.o \
+	oid8.o \
 	oracle_compat.o \
 	orderedsetaggs.o \
 	partitionfuncs.o \
diff --git a/src/backend/utils/adt/int8.c b/src/backend/utils/adt/int8.c
index bdea490202a6..9f7466e47b79 100644
--- a/src/backend/utils/adt/int8.c
+++ b/src/backend/utils/adt/int8.c
@@ -1323,6 +1323,14 @@ oidtoi8(PG_FUNCTION_ARGS)
 	PG_RETURN_INT64((int64) arg);
 }
 
+Datum
+oidtooid8(PG_FUNCTION_ARGS)
+{
+	Oid			arg = PG_GETARG_OID(0);
+
+	PG_RETURN_OID8((Oid8) arg);
+}
+
 /*
  * non-persistent numeric series generator
  */
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index 12fa0c209127..bd798b3e1236 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -73,6 +73,7 @@ backend_sources += files(
   'network_spgist.c',
   'numutils.c',
   'oid.c',
+  'oid8.c',
   'oracle_compat.c',
   'orderedsetaggs.c',
   'partitionfuncs.c',
diff --git a/src/backend/utils/adt/oid8.c b/src/backend/utils/adt/oid8.c
new file mode 100644
index 000000000000..6e9ffd96303f
--- /dev/null
+++ b/src/backend/utils/adt/oid8.c
@@ -0,0 +1,171 @@
+/*-------------------------------------------------------------------------
+ *
+ * oid8.c
+ *	  Functions for the built-in type Oid8
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/oid8.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include <ctype.h>
+#include <limits.h>
+
+#include "catalog/pg_type.h"
+#include "libpq/pqformat.h"
+#include "utils/builtins.h"
+
+#define MAXOID8LEN 20
+
+/*****************************************************************************
+ *	 USER I/O ROUTINES														 *
+ *****************************************************************************/
+
+Datum
+oid8in(PG_FUNCTION_ARGS)
+{
+	char	   *s = PG_GETARG_CSTRING(0);
+	Oid8		result;
+
+	result = uint64in_subr(s, NULL, "oid8", fcinfo->context);
+	PG_RETURN_OID8(result);
+}
+
+Datum
+oid8out(PG_FUNCTION_ARGS)
+{
+	Oid8		val = PG_GETARG_OID8(0);
+	char		buf[MAXOID8LEN + 1];
+	char	   *result;
+	int			len;
+
+	len = pg_ulltoa_n(val, buf) + 1;
+	buf[len - 1] = '\0';
+
+	/*
+	 * Since the length is already known, we do a manual palloc() and memcpy()
+	 * to avoid the strlen() call that would otherwise be done in pstrdup().
+	 */
+	result = palloc(len);
+	memcpy(result, buf, len);
+	PG_RETURN_CSTRING(result);
+}
+
+/*
+ *		oid8recv			- converts external binary format to oid8
+ */
+Datum
+oid8recv(PG_FUNCTION_ARGS)
+{
+	StringInfo	buf = (StringInfo) PG_GETARG_POINTER(0);
+
+	PG_RETURN_OID8(pq_getmsgint64(buf));
+}
+
+/*
+ *		oid8send			- converts oid8 to binary format
+ */
+Datum
+oid8send(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	StringInfoData buf;
+
+	pq_begintypsend(&buf);
+	pq_sendint64(&buf, arg1);
+	PG_RETURN_BYTEA_P(pq_endtypsend(&buf));
+}
+
+/*****************************************************************************
+ *	 PUBLIC ROUTINES														 *
+ *****************************************************************************/
+
+Datum
+oid8eq(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	Oid8		arg2 = PG_GETARG_OID8(1);
+
+	PG_RETURN_BOOL(arg1 == arg2);
+}
+
+Datum
+oid8ne(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	Oid8		arg2 = PG_GETARG_OID8(1);
+
+	PG_RETURN_BOOL(arg1 != arg2);
+}
+
+Datum
+oid8lt(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	Oid8		arg2 = PG_GETARG_OID8(1);
+
+	PG_RETURN_BOOL(arg1 < arg2);
+}
+
+Datum
+oid8le(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	Oid8		arg2 = PG_GETARG_OID8(1);
+
+	PG_RETURN_BOOL(arg1 <= arg2);
+}
+
+Datum
+oid8ge(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	Oid8		arg2 = PG_GETARG_OID8(1);
+
+	PG_RETURN_BOOL(arg1 >= arg2);
+}
+
+Datum
+oid8gt(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	Oid8		arg2 = PG_GETARG_OID8(1);
+
+	PG_RETURN_BOOL(arg1 > arg2);
+}
+
+Datum
+hashoid8(PG_FUNCTION_ARGS)
+{
+	return hashint8(fcinfo);
+}
+
+Datum
+hashoid8extended(PG_FUNCTION_ARGS)
+{
+	return hashint8extended(fcinfo);
+}
+
+Datum
+oid8larger(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	Oid8		arg2 = PG_GETARG_OID8(1);
+
+	PG_RETURN_OID8((arg1 > arg2) ? arg1 : arg2);
+}
+
+Datum
+oid8smaller(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	Oid8		arg2 = PG_GETARG_OID8(1);
+
+	PG_RETURN_OID8((arg1 < arg2) ? arg1 : arg2);
+}
diff --git a/src/fe_utils/print.c b/src/fe_utils/print.c
index 4af0f32f2fc0..221624707892 100644
--- a/src/fe_utils/print.c
+++ b/src/fe_utils/print.c
@@ -3624,6 +3624,7 @@ column_type_alignment(Oid ftype)
 		case FLOAT8OID:
 		case NUMERICOID:
 		case OIDOID:
+		case OID8OID:
 		case XIDOID:
 		case XID8OID:
 		case CIDOID:
diff --git a/src/test/regress/expected/oid8.out b/src/test/regress/expected/oid8.out
new file mode 100644
index 000000000000..80529214ca53
--- /dev/null
+++ b/src/test/regress/expected/oid8.out
@@ -0,0 +1,196 @@
+--
+-- OID8
+--
+CREATE TABLE OID8_TBL(f1 oid8);
+INSERT INTO OID8_TBL(f1) VALUES ('1234');
+INSERT INTO OID8_TBL(f1) VALUES ('1235');
+INSERT INTO OID8_TBL(f1) VALUES ('987');
+INSERT INTO OID8_TBL(f1) VALUES ('-1040');
+INSERT INTO OID8_TBL(f1) VALUES ('99999999');
+INSERT INTO OID8_TBL(f1) VALUES ('5     ');
+INSERT INTO OID8_TBL(f1) VALUES ('   10  ');
+-- leading/trailing hard tab is also allowed
+INSERT INTO OID8_TBL(f1) VALUES ('	  15 	  ');
+-- bad inputs
+INSERT INTO OID8_TBL(f1) VALUES ('');
+ERROR:  invalid input syntax for type oid8: ""
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('');
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES ('    ');
+ERROR:  invalid input syntax for type oid8: "    "
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('    ');
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES ('asdfasd');
+ERROR:  invalid input syntax for type oid8: "asdfasd"
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('asdfasd');
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES ('99asdfasd');
+ERROR:  invalid input syntax for type oid8: "99asdfasd"
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('99asdfasd');
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES ('5    d');
+ERROR:  invalid input syntax for type oid8: "5    d"
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('5    d');
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES ('    5d');
+ERROR:  invalid input syntax for type oid8: "    5d"
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('    5d');
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES ('5    5');
+ERROR:  invalid input syntax for type oid8: "5    5"
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('5    5');
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES (' - 500');
+ERROR:  invalid input syntax for type oid8: " - 500"
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES (' - 500');
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES ('3908203590239580293850293850329485');
+ERROR:  value "3908203590239580293850293850329485" is out of range for type oid8
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('39082035902395802938502938...
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES ('-1204982019841029840928340329840934');
+ERROR:  value "-1204982019841029840928340329840934" is out of range for type oid8
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('-1204982019841029840928340...
+                                         ^
+SELECT * FROM OID8_TBL;
+          f1          
+----------------------
+                 1234
+                 1235
+                  987
+ 18446744073709550576
+             99999999
+                    5
+                   10
+                   15
+(8 rows)
+
+-- Also try it with non-error-throwing API
+SELECT pg_input_is_valid('1234', 'oid8');
+ pg_input_is_valid 
+-------------------
+ t
+(1 row)
+
+SELECT pg_input_is_valid('01XYZ', 'oid8');
+ pg_input_is_valid 
+-------------------
+ f
+(1 row)
+
+SELECT * FROM pg_input_error_info('01XYZ', 'oid8');
+                   message                   | detail | hint | sql_error_code 
+---------------------------------------------+--------+------+----------------
+ invalid input syntax for type oid8: "01XYZ" |        |      | 22P02
+(1 row)
+
+SELECT pg_input_is_valid('3908203590239580293850293850329485', 'oid8');
+ pg_input_is_valid 
+-------------------
+ f
+(1 row)
+
+SELECT * FROM pg_input_error_info('-1204982019841029840928340329840934', 'oid8');
+                                  message                                  | detail | hint | sql_error_code 
+---------------------------------------------------------------------------+--------+------+----------------
+ value "-1204982019841029840928340329840934" is out of range for type oid8 |        |      | 22003
+(1 row)
+
+-- Operators
+SELECT o.* FROM OID8_TBL o WHERE o.f1 = 1234;
+  f1  
+------
+ 1234
+(1 row)
+
+SELECT o.* FROM OID8_TBL o WHERE o.f1 <> '1234';
+          f1          
+----------------------
+                 1235
+                  987
+ 18446744073709550576
+             99999999
+                    5
+                   10
+                   15
+(7 rows)
+
+SELECT o.* FROM OID8_TBL o WHERE o.f1 <= '1234';
+  f1  
+------
+ 1234
+  987
+    5
+   10
+   15
+(5 rows)
+
+SELECT o.* FROM OID8_TBL o WHERE o.f1 < '1234';
+ f1  
+-----
+ 987
+   5
+  10
+  15
+(4 rows)
+
+SELECT o.* FROM OID8_TBL o WHERE o.f1 >= '1234';
+          f1          
+----------------------
+                 1234
+                 1235
+ 18446744073709550576
+             99999999
+(4 rows)
+
+SELECT o.* FROM OID8_TBL o WHERE o.f1 > '1234';
+          f1          
+----------------------
+                 1235
+ 18446744073709550576
+             99999999
+(3 rows)
+
+-- Casts
+SELECT 1::int2::oid8;
+ oid8 
+------
+    1
+(1 row)
+
+SELECT 1::int4::oid8;
+ oid8 
+------
+    1
+(1 row)
+
+SELECT 1::int8::oid8;
+ oid8 
+------
+    1
+(1 row)
+
+SELECT 1::oid8::int8;
+ int8 
+------
+    1
+(1 row)
+
+SELECT 1::oid::oid8; -- ok
+ oid8 
+------
+    1
+(1 row)
+
+SELECT 1::oid8::oid; -- not ok
+ERROR:  cannot cast type oid8 to oid
+LINE 1: SELECT 1::oid8::oid;
+                      ^
+-- Aggregates
+SELECT min(f1), max(f1) FROM OID8_TBL;
+ min |         max          
+-----+----------------------
+   5 | 18446744073709550576
+(1 row)
+
+DROP TABLE OID8_TBL;
diff --git a/src/test/regress/expected/oid8.sql b/src/test/regress/expected/oid8.sql
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 20bf9ea9cdf7..1b2a1641029d 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -880,6 +880,13 @@ bytea(integer)
 bytea(bigint)
 bytea_larger(bytea,bytea)
 bytea_smaller(bytea,bytea)
+oid8eq(oid8,oid8)
+oid8ne(oid8,oid8)
+oid8lt(oid8,oid8)
+oid8le(oid8,oid8)
+oid8gt(oid8,oid8)
+oid8ge(oid8,oid8)
+btoid8cmp(oid8,oid8)
 -- Check that functions without argument are not marked as leakproof.
 SELECT p1.oid::regprocedure
 FROM pg_proc p1 JOIN pg_namespace pn
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 943e56506bf1..9ddcacec6bf4 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -702,6 +702,7 @@ CREATE TABLE tab_core_types AS SELECT
   'abc'::refcursor,
   '1 2'::int2vector,
   '1 2'::oidvector,
+  '1234'::oid8,
   format('%I=UC/%I', USER, USER)::aclitem AS aclitem,
   'a fat cat sat on a mat and ate a fat rat'::tsvector,
   'fat & rat'::tsquery,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index fbffc67ae601..56e129ce4aa0 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -28,7 +28,7 @@ test: strings md5 numerology point lseg line box path polygon circle date time t
 # geometry depends on point, lseg, line, box, path, polygon, circle
 # horology depends on date, time, timetz, timestamp, timestamptz, interval
 # ----------
-test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import oid8
 
 # ----------
 # Load huge amounts of data
diff --git a/src/test/regress/sql/oid8.sql b/src/test/regress/sql/oid8.sql
new file mode 100644
index 000000000000..c4f2ae6a2e57
--- /dev/null
+++ b/src/test/regress/sql/oid8.sql
@@ -0,0 +1,57 @@
+--
+-- OID8
+--
+
+CREATE TABLE OID8_TBL(f1 oid8);
+
+INSERT INTO OID8_TBL(f1) VALUES ('1234');
+INSERT INTO OID8_TBL(f1) VALUES ('1235');
+INSERT INTO OID8_TBL(f1) VALUES ('987');
+INSERT INTO OID8_TBL(f1) VALUES ('-1040');
+INSERT INTO OID8_TBL(f1) VALUES ('99999999');
+INSERT INTO OID8_TBL(f1) VALUES ('5     ');
+INSERT INTO OID8_TBL(f1) VALUES ('   10  ');
+-- leading/trailing hard tab is also allowed
+INSERT INTO OID8_TBL(f1) VALUES ('	  15 	  ');
+
+-- bad inputs
+INSERT INTO OID8_TBL(f1) VALUES ('');
+INSERT INTO OID8_TBL(f1) VALUES ('    ');
+INSERT INTO OID8_TBL(f1) VALUES ('asdfasd');
+INSERT INTO OID8_TBL(f1) VALUES ('99asdfasd');
+INSERT INTO OID8_TBL(f1) VALUES ('5    d');
+INSERT INTO OID8_TBL(f1) VALUES ('    5d');
+INSERT INTO OID8_TBL(f1) VALUES ('5    5');
+INSERT INTO OID8_TBL(f1) VALUES (' - 500');
+INSERT INTO OID8_TBL(f1) VALUES ('3908203590239580293850293850329485');
+INSERT INTO OID8_TBL(f1) VALUES ('-1204982019841029840928340329840934');
+
+SELECT * FROM OID8_TBL;
+
+-- Also try it with non-error-throwing API
+SELECT pg_input_is_valid('1234', 'oid8');
+SELECT pg_input_is_valid('01XYZ', 'oid8');
+SELECT * FROM pg_input_error_info('01XYZ', 'oid8');
+SELECT pg_input_is_valid('3908203590239580293850293850329485', 'oid8');
+SELECT * FROM pg_input_error_info('-1204982019841029840928340329840934', 'oid8');
+
+-- Operators
+SELECT o.* FROM OID8_TBL o WHERE o.f1 = 1234;
+SELECT o.* FROM OID8_TBL o WHERE o.f1 <> '1234';
+SELECT o.* FROM OID8_TBL o WHERE o.f1 <= '1234';
+SELECT o.* FROM OID8_TBL o WHERE o.f1 < '1234';
+SELECT o.* FROM OID8_TBL o WHERE o.f1 >= '1234';
+SELECT o.* FROM OID8_TBL o WHERE o.f1 > '1234';
+
+-- Casts
+SELECT 1::int2::oid8;
+SELECT 1::int4::oid8;
+SELECT 1::int8::oid8;
+SELECT 1::oid8::int8;
+SELECT 1::oid::oid8; -- ok
+SELECT 1::oid8::oid; -- not ok
+
+-- Aggregates
+SELECT min(f1), max(f1) FROM OID8_TBL;
+
+DROP TABLE OID8_TBL;
diff --git a/src/test/regress/sql/type_sanity.sql b/src/test/regress/sql/type_sanity.sql
index df795759bb4c..c2496823d90e 100644
--- a/src/test/regress/sql/type_sanity.sql
+++ b/src/test/regress/sql/type_sanity.sql
@@ -530,6 +530,7 @@ CREATE TABLE tab_core_types AS SELECT
   'abc'::refcursor,
   '1 2'::int2vector,
   '1 2'::oidvector,
+  '1234'::oid8,
   format('%I=UC/%I', USER, USER)::aclitem AS aclitem,
   'a fat cat sat on a mat and ate a fat rat'::tsvector,
   'fat & rat'::tsquery,
diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml
index b81d89e26080..66c6aa7f349a 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -4723,6 +4723,10 @@ INSERT INTO mytable VALUES(-1);  -- fails
     <primary>oid</primary>
    </indexterm>
 
+   <indexterm zone="datatype-oid">
+    <primary>oid8</primary>
+   </indexterm>
+
    <indexterm zone="datatype-oid">
     <primary>regclass</primary>
    </indexterm>
@@ -4805,6 +4809,13 @@ INSERT INTO mytable VALUES(-1);  -- fails
     individual tables.
    </para>
 
+   <para>
+    In some contexts, a 64-bit variant <type>oid8</type> is used.
+    It is implemented as an unsigned eight-byte integer. Unlike its
+    <type>oid</type> counterpart, it can ensure uniqueness in large
+    individual tables.
+   </para>
+
    <para>
     The <type>oid</type> type itself has few operations beyond comparison.
     It can be cast to integer, however, and then manipulated using the
diff --git a/doc/src/sgml/func/func-aggregate.sgml b/doc/src/sgml/func/func-aggregate.sgml
index f50b692516b6..a5396048adf3 100644
--- a/doc/src/sgml/func/func-aggregate.sgml
+++ b/doc/src/sgml/func/func-aggregate.sgml
@@ -508,8 +508,8 @@
         Computes the maximum of the non-null input
         values.  Available for any numeric, string, date/time, or enum type,
         as well as <type>bytea</type>, <type>inet</type>, <type>interval</type>,
-        <type>money</type>, <type>oid</type>, <type>pg_lsn</type>,
-        <type>tid</type>, <type>xid8</type>,
+        <type>money</type>, <type>oid</type>, <type>oid8</type>,
+        <type>pg_lsn</type>, <type>tid</type>, <type>xid8</type>,
         and also arrays and composite types containing sortable data types.
        </para></entry>
        <entry>Yes</entry>
@@ -527,8 +527,8 @@
         Computes the minimum of the non-null input
         values.  Available for any numeric, string, date/time, or enum type,
         as well as <type>bytea</type>, <type>inet</type>, <type>interval</type>,
-        <type>money</type>, <type>oid</type>, <type>pg_lsn</type>,
-        <type>tid</type>, <type>xid8</type>,
+        <type>money</type>, <type>oid</type>, <type>oid8</type>,
+        <type>pg_lsn</type>, <type>tid</type>, <type>xid8</type>,
         and also arrays and composite types containing sortable data types.
        </para></entry>
        <entry>Yes</entry>
-- 
2.51.0

v6-0002-Refactor-some-TOAST-value-ID-code-to-use-Oid8-ins.patchtext/x-diff; charset=us-asciiDownload
From d98e0211975eaca2ca83d40a18c73248377b3878 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 13 Aug 2025 17:26:36 +0900
Subject: [PATCH v6 02/15] Refactor some TOAST value ID code to use Oid8
 instead of Oid

This change is a mechanical switch to change most of the code paths that
assume TOAST value IDs to be Oids to become Oid8, easing an upcoming
change to allow larger TOAST values, at 8 bytes.

The areas touched are related to table AM, amcheck and logical
decoding's reorder buffer.  A good chunk of the changes involve
switching printf() markers from %u to OID8_FORMAT.
---
 src/include/access/heaptoast.h                |  2 +-
 src/include/access/tableam.h                  |  4 +-
 src/backend/access/common/toast_internals.c   |  8 +--
 src/backend/access/heap/heaptoast.c           | 12 ++--
 .../replication/logical/reorderbuffer.c       | 14 +++--
 contrib/amcheck/verify_heapam.c               | 56 +++++++++++--------
 6 files changed, 53 insertions(+), 43 deletions(-)

diff --git a/src/include/access/heaptoast.h b/src/include/access/heaptoast.h
index 6385a27caf83..fdc8d00d7099 100644
--- a/src/include/access/heaptoast.h
+++ b/src/include/access/heaptoast.h
@@ -142,7 +142,7 @@ extern HeapTuple toast_build_flattened_tuple(TupleDesc tupleDesc,
  *	Fetch a slice from a toast value stored in a heap table.
  * ----------
  */
-extern void heap_fetch_toast_slice(Relation toastrel, Oid valueid,
+extern void heap_fetch_toast_slice(Relation toastrel, Oid8 valueid,
 								   int32 attrsize, int32 sliceoffset,
 								   int32 slicelength, struct varlena *result);
 
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index b2ce35e2a340..40cc2218e61c 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -745,7 +745,7 @@ typedef struct TableAmRoutine
 	 * table implemented by this AM.  See table_relation_fetch_toast_slice()
 	 * for more details.
 	 */
-	void		(*relation_fetch_toast_slice) (Relation toastrel, Oid valueid,
+	void		(*relation_fetch_toast_slice) (Relation toastrel, Oid8 valueid,
 											   int32 attrsize,
 											   int32 sliceoffset,
 											   int32 slicelength,
@@ -1880,7 +1880,7 @@ table_relation_toast_am(Relation rel)
  * stored.
  */
 static inline void
-table_relation_fetch_toast_slice(Relation toastrel, Oid valueid,
+table_relation_fetch_toast_slice(Relation toastrel, Oid8 valueid,
 								 int32 attrsize, int32 sliceoffset,
 								 int32 slicelength, struct varlena *result)
 {
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 81dbd67c7258..420263691cc3 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -26,8 +26,8 @@
 #include "utils/rel.h"
 #include "utils/snapmgr.h"
 
-static bool toastrel_valueid_exists(Relation toastrel, Oid valueid);
-static bool toastid_valueid_exists(Oid toastrelid, Oid valueid);
+static bool toastrel_valueid_exists(Relation toastrel, Oid8 valueid);
+static bool toastid_valueid_exists(Oid toastrelid, Oid8 valueid);
 
 /* ----------
  * toast_compress_datum -
@@ -449,7 +449,7 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
  * ----------
  */
 static bool
-toastrel_valueid_exists(Relation toastrel, Oid valueid)
+toastrel_valueid_exists(Relation toastrel, Oid8 valueid)
 {
 	bool		result = false;
 	ScanKeyData toastkey;
@@ -497,7 +497,7 @@ toastrel_valueid_exists(Relation toastrel, Oid valueid)
  * ----------
  */
 static bool
-toastid_valueid_exists(Oid toastrelid, Oid valueid)
+toastid_valueid_exists(Oid toastrelid, Oid8 valueid)
 {
 	bool		result;
 	Relation	toastrel;
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index cb1e57030f64..d4b600de3aca 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -623,7 +623,7 @@ toast_build_flattened_tuple(TupleDesc tupleDesc,
  * result is the varlena into which the results should be written.
  */
 void
-heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
+heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 					   int32 sliceoffset, int32 slicelength,
 					   struct varlena *result)
 {
@@ -725,7 +725,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
 		else
 		{
 			/* should never happen */
-			elog(ERROR, "found toasted toast chunk for toast value %u in %s",
+			elog(ERROR, "found toasted toast chunk for toast value " OID8_FORMAT " in %s",
 				 valueid, RelationGetRelationName(toastrel));
 			chunksize = 0;		/* keep compiler quiet */
 			chunkdata = NULL;
@@ -737,13 +737,13 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
 		if (curchunk != expectedchunk)
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
-					 errmsg_internal("unexpected chunk number %d (expected %d) for toast value %u in %s",
+					 errmsg_internal("unexpected chunk number %d (expected %d) for toast value " OID8_FORMAT " in %s",
 									 curchunk, expectedchunk, valueid,
 									 RelationGetRelationName(toastrel))));
 		if (curchunk > endchunk)
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
-					 errmsg_internal("unexpected chunk number %d (out of range %d..%d) for toast value %u in %s",
+					 errmsg_internal("unexpected chunk number %d (out of range %d..%d) for toast value " OID8_FORMAT " in %s",
 									 curchunk,
 									 startchunk, endchunk, valueid,
 									 RelationGetRelationName(toastrel))));
@@ -752,7 +752,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
 		if (chunksize != expected_size)
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
-					 errmsg_internal("unexpected chunk size %d (expected %d) in chunk %d of %d for toast value %u in %s",
+					 errmsg_internal("unexpected chunk size %d (expected %d) in chunk %d of %d for toast value " OID8_FORMAT " in %s",
 									 chunksize, expected_size,
 									 curchunk, totalchunks, valueid,
 									 RelationGetRelationName(toastrel))));
@@ -781,7 +781,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
 	if (expectedchunk != (endchunk + 1))
 		ereport(ERROR,
 				(errcode(ERRCODE_DATA_CORRUPTED),
-				 errmsg_internal("missing chunk number %d for toast value %u in %s",
+				 errmsg_internal("missing chunk number %d for toast value " OID8_FORMAT " in %s",
 								 expectedchunk, valueid,
 								 RelationGetRelationName(toastrel))));
 
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 4736f993c374..a1cf30a8f2eb 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -176,7 +176,7 @@ typedef struct ReorderBufferIterTXNState
 /* toast datastructures */
 typedef struct ReorderBufferToastEnt
 {
-	Oid			chunk_id;		/* toast_table.chunk_id */
+	Oid8		chunk_id;		/* toast_table.chunk_id */
 	int32		last_chunk_seq; /* toast_table.chunk_seq of the last chunk we
 								 * have seen */
 	Size		num_chunks;		/* number of chunks we've already seen */
@@ -4959,7 +4959,7 @@ ReorderBufferToastInitHash(ReorderBuffer *rb, ReorderBufferTXN *txn)
 
 	Assert(txn->toast_hash == NULL);
 
-	hash_ctl.keysize = sizeof(Oid);
+	hash_ctl.keysize = sizeof(Oid8);
 	hash_ctl.entrysize = sizeof(ReorderBufferToastEnt);
 	hash_ctl.hcxt = rb->context;
 	txn->toast_hash = hash_create("ReorderBufferToastHash", 5, &hash_ctl,
@@ -4983,7 +4983,7 @@ ReorderBufferToastAppendChunk(ReorderBuffer *rb, ReorderBufferTXN *txn,
 	bool		isnull;
 	Pointer		chunk;
 	TupleDesc	desc = RelationGetDescr(relation);
-	Oid			chunk_id;
+	Oid8		chunk_id;
 	int32		chunk_seq;
 
 	if (txn->toast_hash == NULL)
@@ -5010,11 +5010,11 @@ ReorderBufferToastAppendChunk(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		dlist_init(&ent->chunks);
 
 		if (chunk_seq != 0)
-			elog(ERROR, "got sequence entry %d for toast chunk %u instead of seq 0",
+			elog(ERROR, "got sequence entry %d for toast chunk " OID8_FORMAT " instead of seq 0",
 				 chunk_seq, chunk_id);
 	}
 	else if (found && chunk_seq != ent->last_chunk_seq + 1)
-		elog(ERROR, "got sequence entry %d for toast chunk %u instead of seq %d",
+		elog(ERROR, "got sequence entry %d for toast chunk " OID8_FORMAT " instead of seq %d",
 			 chunk_seq, chunk_id, ent->last_chunk_seq + 1);
 
 	chunk = DatumGetPointer(fastgetattr(newtup, 3, desc, &isnull));
@@ -5123,6 +5123,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		struct varlena *reconstructed;
 		dlist_iter	it;
 		Size		data_done = 0;
+		Oid8		toast_valueid;
 
 		/* system columns aren't toasted */
 		if (attr->attnum < 0)
@@ -5147,13 +5148,14 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 			continue;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, varlena);
+		toast_valueid = toast_pointer.va_valueid;
 
 		/*
 		 * Check whether the toast tuple changed, replace if so.
 		 */
 		ent = (ReorderBufferToastEnt *)
 			hash_search(txn->toast_hash,
-						&toast_pointer.va_valueid,
+						&toast_valueid,
 						HASH_FIND,
 						NULL);
 		if (ent == NULL)
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 4963e9245cb5..eb353c40249e 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1561,6 +1561,9 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	bool		isnull;
 	int32		chunksize;
 	int32		expected_size;
+	Oid8		toast_valueid;
+
+	toast_valueid = ta->toast_pointer.va_valueid;
 
 	/* Sanity-check the sequence number. */
 	chunk_seq = DatumGetInt32(fastgetattr(toasttup, 2,
@@ -1568,16 +1571,16 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	if (isnull)
 	{
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u has toast chunk with null sequence number",
-										 ta->toast_pointer.va_valueid));
+								psprintf("toast value " OID8_FORMAT " has toast chunk with null sequence number",
+										 toast_valueid));
 		return;
 	}
 	if (chunk_seq != *expected_chunk_seq)
 	{
 		/* Either the TOAST index is corrupt, or we don't have all chunks. */
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u index scan returned chunk %d when expecting chunk %d",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value " OID8_FORMAT " index scan returned chunk %d when expecting chunk %d",
+										 toast_valueid,
 										 chunk_seq, *expected_chunk_seq));
 	}
 	*expected_chunk_seq = chunk_seq + 1;
@@ -1588,8 +1591,8 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	if (isnull)
 	{
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u chunk %d has null data",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value " OID8_FORMAT " chunk %d has null data",
+										 toast_valueid,
 										 chunk_seq));
 		return;
 	}
@@ -1608,8 +1611,8 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 		uint32		header = ((varattrib_4b *) chunk)->va_4byte.va_header;
 
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u chunk %d has invalid varlena header %0x",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value " OID8_FORMAT " chunk %d has invalid varlena header %0x",
+										 toast_valueid,
 										 chunk_seq, header));
 		return;
 	}
@@ -1620,8 +1623,8 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	if (chunk_seq > last_chunk_seq)
 	{
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u chunk %d follows last expected chunk %d",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value " OID8_FORMAT " chunk %d follows last expected chunk %d",
+										 toast_valueid,
 										 chunk_seq, last_chunk_seq));
 		return;
 	}
@@ -1631,8 +1634,8 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 
 	if (chunksize != expected_size)
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u chunk %d has size %u, but expected size %u",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value " OID8_FORMAT " chunk %d has size %u, but expected size %u",
+										 toast_valueid,
 										 chunk_seq, chunksize, expected_size));
 }
 
@@ -1663,6 +1666,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	struct varlena *attr;
 	char	   *tp;				/* pointer to the tuple data */
 	uint16		infomask;
+	Oid8		toast_pointer_valueid;
 	CompactAttribute *thisatt;
 	struct varatt_external toast_pointer;
 
@@ -1771,12 +1775,13 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	 * Must copy attr into toast_pointer for alignment considerations
 	 */
 	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_pointer_valueid = toast_pointer.va_valueid;
 
 	/* Toasted attributes too large to be untoasted should never be stored */
 	if (toast_pointer.va_rawsize > VARLENA_SIZE_LIMIT)
 		report_corruption(ctx,
-						  psprintf("toast value %u rawsize %d exceeds limit %d",
-								   toast_pointer.va_valueid,
+						  psprintf("toast value " OID8_FORMAT " rawsize %d exceeds limit %d",
+								   toast_pointer_valueid,
 								   toast_pointer.va_rawsize,
 								   VARLENA_SIZE_LIMIT));
 
@@ -1803,16 +1808,16 @@ check_tuple_attribute(HeapCheckContext *ctx)
 		}
 		if (!valid)
 			report_corruption(ctx,
-							  psprintf("toast value %u has invalid compression method id %d",
-									   toast_pointer.va_valueid, cmid));
+							  psprintf("toast value " OID8_FORMAT " has invalid compression method id %d",
+									   toast_pointer_valueid, cmid));
 	}
 
 	/* The tuple header better claim to contain toasted values */
 	if (!(infomask & HEAP_HASEXTERNAL))
 	{
 		report_corruption(ctx,
-						  psprintf("toast value %u is external but tuple header flag HEAP_HASEXTERNAL not set",
-								   toast_pointer.va_valueid));
+						  psprintf("toast value " OID8_FORMAT " is external but tuple header flag HEAP_HASEXTERNAL not set",
+								   toast_pointer_valueid));
 		return true;
 	}
 
@@ -1820,8 +1825,8 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	if (!ctx->rel->rd_rel->reltoastrelid)
 	{
 		report_corruption(ctx,
-						  psprintf("toast value %u is external but relation has no toast relation",
-								   toast_pointer.va_valueid));
+						  psprintf("toast value " OID8_FORMAT " is external but relation has no toast relation",
+								   toast_pointer_valueid));
 		return true;
 	}
 
@@ -1866,6 +1871,7 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	uint32		extsize;
 	int32		expected_chunk_seq = 0;
 	int32		last_chunk_seq;
+	Oid8		toast_valueid;
 
 	extsize = VARATT_EXTERNAL_GET_EXTSIZE(ta->toast_pointer);
 	last_chunk_seq = (extsize - 1) / TOAST_MAX_CHUNK_SIZE;
@@ -1896,14 +1902,16 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	}
 	systable_endscan_ordered(toastscan);
 
+	toast_valueid = ta->toast_pointer.va_valueid;
+
 	if (!found_toasttup)
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u not found in toast table",
-										 ta->toast_pointer.va_valueid));
+								psprintf("toast value " OID8_FORMAT " not found in toast table",
+										 toast_valueid));
 	else if (expected_chunk_seq <= last_chunk_seq)
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u was expected to end at chunk %d, but ended while expecting chunk %d",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value " OID8_FORMAT " was expected to end at chunk %d, but ended while expecting chunk %d",
+										 toast_valueid,
 										 last_chunk_seq, expected_chunk_seq));
 }
 
-- 
2.51.0

v6-0003-Minimize-footprint-of-TOAST_MAX_CHUNK_SIZE-in-hea.patchtext/x-diff; charset=us-asciiDownload
From 60be4365d68947e5f9b617219ab95a9e1782edcc Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 13 Aug 2025 17:40:13 +0900
Subject: [PATCH v6 03/15] Minimize footprint of TOAST_MAX_CHUNK_SIZE in heap
 and amcheck

This eases a follow-up change to support 8-byte TOAST value IDs, as the
maximum chunk size allowed for a single chunk of TOASTed data depends on
the size of the value ID.
---
 src/backend/access/heap/heaptoast.c | 20 ++++++++++++--------
 contrib/amcheck/verify_heapam.c     | 13 +++++++++----
 2 files changed, 21 insertions(+), 12 deletions(-)

diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index d4b600de3aca..a3933e48c8c8 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -634,11 +634,12 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 	SysScanDesc toastscan;
 	HeapTuple	ttup;
 	int32		expectedchunk;
-	int32		totalchunks = ((attrsize - 1) / TOAST_MAX_CHUNK_SIZE) + 1;
+	int32		totalchunks;
 	int			startchunk;
 	int			endchunk;
 	int			num_indexes;
 	int			validIndex;
+	int32		max_chunk_size;
 
 	/* Look for the valid index of toast relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -646,8 +647,11 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 									&toastidxs,
 									&num_indexes);
 
-	startchunk = sliceoffset / TOAST_MAX_CHUNK_SIZE;
-	endchunk = (sliceoffset + slicelength - 1) / TOAST_MAX_CHUNK_SIZE;
+	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+
+	totalchunks = ((attrsize - 1) / max_chunk_size) + 1;
+	startchunk = sliceoffset / max_chunk_size;
+	endchunk = (sliceoffset + slicelength - 1) / max_chunk_size;
 	Assert(endchunk <= totalchunks);
 
 	/* Set up a scan key to fetch from the index. */
@@ -747,8 +751,8 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 									 curchunk,
 									 startchunk, endchunk, valueid,
 									 RelationGetRelationName(toastrel))));
-		expected_size = curchunk < totalchunks - 1 ? TOAST_MAX_CHUNK_SIZE
-			: attrsize - ((totalchunks - 1) * TOAST_MAX_CHUNK_SIZE);
+		expected_size = curchunk < totalchunks - 1 ? max_chunk_size
+			: attrsize - ((totalchunks - 1) * max_chunk_size);
 		if (chunksize != expected_size)
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
@@ -763,12 +767,12 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 		chcpystrt = 0;
 		chcpyend = chunksize - 1;
 		if (curchunk == startchunk)
-			chcpystrt = sliceoffset % TOAST_MAX_CHUNK_SIZE;
+			chcpystrt = sliceoffset % max_chunk_size;
 		if (curchunk == endchunk)
-			chcpyend = (sliceoffset + slicelength - 1) % TOAST_MAX_CHUNK_SIZE;
+			chcpyend = (sliceoffset + slicelength - 1) % max_chunk_size;
 
 		memcpy(VARDATA(result) +
-			   (curchunk * TOAST_MAX_CHUNK_SIZE - sliceoffset) + chcpystrt,
+			   (curchunk * max_chunk_size - sliceoffset) + chcpystrt,
 			   chunkdata + chcpystrt,
 			   (chcpyend - chcpystrt) + 1);
 
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index eb353c40249e..164ced37583a 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1556,15 +1556,19 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 				  uint32 extsize)
 {
 	int32		chunk_seq;
-	int32		last_chunk_seq = (extsize - 1) / TOAST_MAX_CHUNK_SIZE;
+	int32		last_chunk_seq;
 	Pointer		chunk;
 	bool		isnull;
 	int32		chunksize;
 	int32		expected_size;
 	Oid8		toast_valueid;
+	int32		max_chunk_size;
 
 	toast_valueid = ta->toast_pointer.va_valueid;
 
+	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	last_chunk_seq = (extsize - 1) / max_chunk_size;
+
 	/* Sanity-check the sequence number. */
 	chunk_seq = DatumGetInt32(fastgetattr(toasttup, 2,
 										  ctx->toast_rel->rd_att, &isnull));
@@ -1629,8 +1633,8 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 		return;
 	}
 
-	expected_size = chunk_seq < last_chunk_seq ? TOAST_MAX_CHUNK_SIZE
-		: extsize - (last_chunk_seq * TOAST_MAX_CHUNK_SIZE);
+	expected_size = chunk_seq < last_chunk_seq ? max_chunk_size
+		: extsize - (last_chunk_seq * max_chunk_size);
 
 	if (chunksize != expected_size)
 		report_toast_corruption(ctx, ta,
@@ -1872,9 +1876,10 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	int32		expected_chunk_seq = 0;
 	int32		last_chunk_seq;
 	Oid8		toast_valueid;
+	int32		max_chunk_size = TOAST_MAX_CHUNK_SIZE;
 
 	extsize = VARATT_EXTERNAL_GET_EXTSIZE(ta->toast_pointer);
-	last_chunk_seq = (extsize - 1) / TOAST_MAX_CHUNK_SIZE;
+	last_chunk_seq = (extsize - 1) / max_chunk_size;
 
 	/*
 	 * Setup a scan key to find chunks in toast table with matching va_valueid
-- 
2.51.0

v6-0004-Renames-around-varatt_external-varatt_external_oi.patchtext/x-diff; charset=us-asciiDownload
From 37648c096017884eb12b48973e20855182f89be5 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 13 Aug 2025 18:28:10 +0900
Subject: [PATCH v6 04/15] Renames around varatt_external->varatt_external_oid

This impacts a few things:
- VARTAG_ONDISK -> VARTAG_ONDISK_OID
- TOAST_POINTER_SIZE -> TOAST_OID_POINTER_SIZE
- TOAST_MAX_CHUNK_SIZE -> TOAST_OID_MAX_CHUNK_SIZE

The "struct" around varatt_external is cleaned up in most places, while
on it.

This rename is in preparation of a follow-up commit that aims at adding
support for multiple types of external on-disk TOAST pointers, where the
OID type is only one subset of them.
---
 src/include/access/detoast.h                  |  4 +--
 src/include/access/heaptoast.h                |  6 ++--
 src/include/varatt.h                          | 34 +++++++++++--------
 src/backend/access/common/detoast.c           | 10 +++---
 src/backend/access/common/toast_compression.c |  2 +-
 src/backend/access/common/toast_internals.c   | 14 ++++----
 src/backend/access/heap/heaptoast.c           |  2 +-
 src/backend/access/table/toast_helper.c       |  4 +--
 src/backend/access/transam/xlog.c             |  8 ++---
 .../replication/logical/reorderbuffer.c       |  2 +-
 src/backend/utils/adt/varlena.c               |  2 +-
 src/bin/pg_resetwal/pg_resetwal.c             |  2 +-
 doc/src/sgml/func/func-info.sgml              |  2 +-
 doc/src/sgml/storage.sgml                     |  2 +-
 contrib/amcheck/verify_heapam.c               | 10 +++---
 15 files changed, 54 insertions(+), 50 deletions(-)

diff --git a/src/include/access/detoast.h b/src/include/access/detoast.h
index e603a2276c38..6435597b1127 100644
--- a/src/include/access/detoast.h
+++ b/src/include/access/detoast.h
@@ -14,7 +14,7 @@
 
 /*
  * Macro to fetch the possibly-unaligned contents of an EXTERNAL datum
- * into a local "struct varatt_external" toast pointer.  This should be
+ * into a local "varatt_external_oid" toast pointer.  This should be
  * just a memcpy, but some versions of gcc seem to produce broken code
  * that assumes the datum contents are aligned.  Introducing an explicit
  * intermediate "varattrib_1b_e *" variable seems to fix it.
@@ -28,7 +28,7 @@ do { \
 } while (0)
 
 /* Size of an EXTERNAL datum that contains a standard TOAST pointer */
-#define TOAST_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external))
+#define TOAST_OID_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_oid))
 
 /* Size of an EXTERNAL datum that contains an indirection pointer */
 #define INDIRECT_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_indirect))
diff --git a/src/include/access/heaptoast.h b/src/include/access/heaptoast.h
index fdc8d00d7099..59c82b2cb1a3 100644
--- a/src/include/access/heaptoast.h
+++ b/src/include/access/heaptoast.h
@@ -69,19 +69,19 @@
 
 /*
  * When we store an oversize datum externally, we divide it into chunks
- * containing at most TOAST_MAX_CHUNK_SIZE data bytes.  This number *must*
+ * containing at most TOAST_OID_MAX_CHUNK_SIZE data bytes.  This number *must*
  * be small enough that the completed toast-table tuple (including the
  * ID and sequence fields and all overhead) will fit on a page.
  * The coding here sets the size on the theory that we want to fit
  * EXTERN_TUPLES_PER_PAGE tuples of maximum size onto a page.
  *
- * NB: Changing TOAST_MAX_CHUNK_SIZE requires an initdb.
+ * NB: Changing TOAST_OID_MAX_CHUNK_SIZE requires an initdb.
  */
 #define EXTERN_TUPLES_PER_PAGE	4	/* tweak only this */
 
 #define EXTERN_TUPLE_MAX_SIZE	MaximumBytesPerTuple(EXTERN_TUPLES_PER_PAGE)
 
-#define TOAST_MAX_CHUNK_SIZE	\
+#define TOAST_OID_MAX_CHUNK_SIZE	\
 	(EXTERN_TUPLE_MAX_SIZE -							\
 	 MAXALIGN(SizeofHeapTupleHeader) -					\
 	 sizeof(Oid) -										\
diff --git a/src/include/varatt.h b/src/include/varatt.h
index aeeabf9145b5..c873a59bb1c9 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -16,7 +16,7 @@
 #define VARATT_H
 
 /*
- * struct varatt_external is a traditional "TOAST pointer", that is, the
+ * varatt_external_oid is a traditional "TOAST pointer", that is, the
  * information needed to fetch a Datum stored out-of-line in a TOAST table.
  * The data is compressed if and only if the external size stored in
  * va_extinfo is less than va_rawsize - VARHDRSZ.
@@ -29,14 +29,14 @@
  * you can look at these fields!  (The reason we use memcmp is to avoid
  * having to do that just to detect equality of two TOAST pointers...)
  */
-typedef struct varatt_external
+typedef struct varatt_external_oid
 {
 	int32		va_rawsize;		/* Original data size (includes header) */
 	uint32		va_extinfo;		/* External saved size (without header) and
 								 * compression method */
 	Oid			va_valueid;		/* Unique ID of value within TOAST table */
 	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
-}			varatt_external;
+}			varatt_external_oid;
 
 /*
  * These macros define the "saved size" portion of va_extinfo.  Its remaining
@@ -51,7 +51,7 @@ typedef struct varatt_external
  * The creator of such a Datum is entirely responsible that the referenced
  * storage survives for as long as referencing pointer Datums can exist.
  *
- * Note that just as for struct varatt_external, this struct is stored
+ * Note that just as for varatt_external_oid, this struct is stored
  * unaligned within any containing tuple.
  */
 typedef struct varatt_indirect
@@ -66,7 +66,7 @@ typedef struct varatt_indirect
  * storage.  APIs for this, in particular the definition of struct
  * ExpandedObjectHeader, are in src/include/utils/expandeddatum.h.
  *
- * Note that just as for struct varatt_external, this struct is stored
+ * Note that just as for varatt_external_oid, this struct is stored
  * unaligned within any containing tuple.
  */
 typedef struct ExpandedObjectHeader ExpandedObjectHeader;
@@ -78,15 +78,16 @@ typedef struct varatt_expanded
 
 /*
  * Type tag for the various sorts of "TOAST pointer" datums.  The peculiar
- * value for VARTAG_ONDISK comes from a requirement for on-disk compatibility
- * with a previous notion that the tag field was the pointer datum's length.
+ * value for VARTAG_ONDISK_OID comes from a requirement for on-disk
+ * compatibility with a previous notion that the tag field was the pointer
+ * datum's length.
  */
 typedef enum vartag_external
 {
 	VARTAG_INDIRECT = 1,
 	VARTAG_EXPANDED_RO = 2,
 	VARTAG_EXPANDED_RW = 3,
-	VARTAG_ONDISK = 18
+	VARTAG_ONDISK_OID = 18
 } vartag_external;
 
 /* Is a TOAST pointer either type of expanded-object pointer? */
@@ -105,8 +106,8 @@ VARTAG_SIZE(vartag_external tag)
 		return sizeof(varatt_indirect);
 	else if (VARTAG_IS_EXPANDED(tag))
 		return sizeof(varatt_expanded);
-	else if (tag == VARTAG_ONDISK)
-		return sizeof(varatt_external);
+	else if (tag == VARTAG_ONDISK_OID)
+		return sizeof(varatt_external_oid);
 	else
 	{
 		Assert(false);
@@ -360,7 +361,7 @@ VARATT_IS_EXTERNAL(const void *PTR)
 static inline bool
 VARATT_IS_EXTERNAL_ONDISK(const void *PTR)
 {
-	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK;
+	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID;
 }
 
 /* Is varlena datum an indirect pointer? */
@@ -502,15 +503,18 @@ VARDATA_COMPRESSED_GET_COMPRESS_METHOD(const void *PTR)
 	return ((varattrib_4b *) PTR)->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS;
 }
 
-/* Same for external Datums; but note argument is a struct varatt_external */
+/*
+ * Same for external Datums; but note argument is a struct
+ * varatt_external_oid.
+ */
 static inline Size
-VARATT_EXTERNAL_GET_EXTSIZE(struct varatt_external toast_pointer)
+VARATT_EXTERNAL_GET_EXTSIZE(varatt_external_oid toast_pointer)
 {
 	return toast_pointer.va_extinfo & VARLENA_EXTSIZE_MASK;
 }
 
 static inline uint32
-VARATT_EXTERNAL_GET_COMPRESS_METHOD(struct varatt_external toast_pointer)
+VARATT_EXTERNAL_GET_COMPRESS_METHOD(varatt_external_oid toast_pointer)
 {
 	return toast_pointer.va_extinfo >> VARLENA_EXTSIZE_BITS;
 }
@@ -533,7 +537,7 @@ VARATT_EXTERNAL_GET_COMPRESS_METHOD(struct varatt_external toast_pointer)
  * actually saves space, so we expect either equality or less-than.
  */
 static inline bool
-VARATT_EXTERNAL_IS_COMPRESSED(struct varatt_external toast_pointer)
+VARATT_EXTERNAL_IS_COMPRESSED(varatt_external_oid toast_pointer)
 {
 	return VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) <
 		(Size) (toast_pointer.va_rawsize - VARHDRSZ);
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 626517877422..c187c32d96dd 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -225,7 +225,7 @@ detoast_attr_slice(struct varlena *attr,
 
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
-		struct varatt_external toast_pointer;
+		varatt_external_oid toast_pointer;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 
@@ -344,7 +344,7 @@ toast_fetch_datum(struct varlena *attr)
 {
 	Relation	toastrel;
 	struct varlena *result;
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 	int32		attrsize;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
@@ -398,7 +398,7 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 {
 	Relation	toastrel;
 	struct varlena *result;
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 	int32		attrsize;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
@@ -550,7 +550,7 @@ toast_raw_datum_size(Datum value)
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
 		/* va_rawsize is the size of the original datum -- including header */
-		struct varatt_external toast_pointer;
+		varatt_external_oid toast_pointer;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 		result = toast_pointer.va_rawsize;
@@ -610,7 +610,7 @@ toast_datum_size(Datum value)
 		 * compressed or not.  We do not count the size of the toast pointer
 		 * ... should we?
 		 */
-		struct varatt_external toast_pointer;
+		varatt_external_oid toast_pointer;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 		result = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c
index 926f1e4008ab..08f572f31eed 100644
--- a/src/backend/access/common/toast_compression.c
+++ b/src/backend/access/common/toast_compression.c
@@ -262,7 +262,7 @@ toast_get_compression_id(struct varlena *attr)
 	 */
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
-		struct varatt_external toast_pointer;
+		varatt_external_oid toast_pointer;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 420263691cc3..770dbb5a6104 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -124,7 +124,7 @@ toast_save_datum(Relation rel, Datum value,
 	TupleDesc	toasttupDesc;
 	CommandId	mycid = GetCurrentCommandId(true);
 	struct varlena *result;
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 	int32		chunk_seq = 0;
 	char	   *data_p;
 	int32		data_todo;
@@ -225,7 +225,7 @@ toast_save_datum(Relation rel, Datum value,
 		toast_pointer.va_valueid = InvalidOid;
 		if (oldexternal != NULL)
 		{
-			struct varatt_external old_toast_pointer;
+			varatt_external_oid old_toast_pointer;
 
 			Assert(VARATT_IS_EXTERNAL_ONDISK(oldexternal));
 			/* Must copy to access aligned fields */
@@ -289,7 +289,7 @@ toast_save_datum(Relation rel, Datum value,
 		{
 			struct varlena hdr;
 			/* this is to make the union big enough for a chunk: */
-			char		data[TOAST_MAX_CHUNK_SIZE + VARHDRSZ];
+			char		data[TOAST_OID_MAX_CHUNK_SIZE + VARHDRSZ];
 			/* ensure union is aligned well enough: */
 			int32		align_it;
 		}			chunk_data;
@@ -300,7 +300,7 @@ toast_save_datum(Relation rel, Datum value,
 		/*
 		 * Calculate the size of this chunk
 		 */
-		chunk_size = Min(TOAST_MAX_CHUNK_SIZE, data_todo);
+		chunk_size = Min(TOAST_OID_MAX_CHUNK_SIZE, data_todo);
 
 		/*
 		 * Build a tuple and store it
@@ -361,8 +361,8 @@ toast_save_datum(Relation rel, Datum value,
 	/*
 	 * Create the TOAST pointer value that we'll return
 	 */
-	result = (struct varlena *) palloc(TOAST_POINTER_SIZE);
-	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK);
+	result = (struct varlena *) palloc(TOAST_OID_POINTER_SIZE);
+	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_OID);
 	memcpy(VARDATA_EXTERNAL(result), &toast_pointer, sizeof(toast_pointer));
 
 	return PointerGetDatum(result);
@@ -378,7 +378,7 @@ void
 toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 {
 	struct varlena *attr = (struct varlena *) DatumGetPointer(value);
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 	Relation	toastrel;
 	Relation   *toastidxs;
 	ScanKeyData toastkey;
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index a3933e48c8c8..ddde7fcf79a4 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -647,7 +647,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 									&toastidxs,
 									&num_indexes);
 
-	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
 
 	totalchunks = ((attrsize - 1) / max_chunk_size) + 1;
 	startchunk = sliceoffset / max_chunk_size;
diff --git a/src/backend/access/table/toast_helper.c b/src/backend/access/table/toast_helper.c
index 11f97d65367d..0c58c6c32565 100644
--- a/src/backend/access/table/toast_helper.c
+++ b/src/backend/access/table/toast_helper.c
@@ -171,7 +171,7 @@ toast_tuple_init(ToastTupleContext *ttc)
  * The column must have attstorage EXTERNAL or EXTENDED if check_main is
  * false, and must have attstorage MAIN if check_main is true.
  *
- * The column must have a minimum size of MAXALIGN(TOAST_POINTER_SIZE);
+ * The column must have a minimum size of MAXALIGN(TOAST_OID_POINTER_SIZE);
  * if not, no benefit is to be expected by compressing it.
  *
  * The return value is the index of the biggest suitable column, or
@@ -184,7 +184,7 @@ toast_tuple_find_biggest_attribute(ToastTupleContext *ttc,
 	TupleDesc	tupleDesc = ttc->ttc_rel->rd_att;
 	int			numAttrs = tupleDesc->natts;
 	int			biggest_attno = -1;
-	int32		biggest_size = MAXALIGN(TOAST_POINTER_SIZE);
+	int32		biggest_size = MAXALIGN(TOAST_OID_POINTER_SIZE);
 	int32		skip_colflags = TOASTCOL_IGNORE;
 	int			i;
 
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 0baf0ac6160a..b521c24c6c73 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4254,7 +4254,7 @@ WriteControlFile(void)
 	ControlFile->nameDataLen = NAMEDATALEN;
 	ControlFile->indexMaxKeys = INDEX_MAX_KEYS;
 
-	ControlFile->toast_max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	ControlFile->toast_max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
 	ControlFile->loblksize = LOBLKSIZE;
 
 	ControlFile->float8ByVal = true;	/* vestigial */
@@ -4497,15 +4497,15 @@ ReadControlFile(void)
 						   "INDEX_MAX_KEYS", ControlFile->indexMaxKeys,
 						   "INDEX_MAX_KEYS", INDEX_MAX_KEYS),
 				 errhint("It looks like you need to recompile or initdb.")));
-	if (ControlFile->toast_max_chunk_size != TOAST_MAX_CHUNK_SIZE)
+	if (ControlFile->toast_max_chunk_size != TOAST_OID_MAX_CHUNK_SIZE)
 		ereport(FATAL,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("database files are incompatible with server"),
 		/* translator: %s is a variable name and %d is its value */
 				 errdetail("The database cluster was initialized with %s %d,"
 						   " but the server was compiled with %s %d.",
-						   "TOAST_MAX_CHUNK_SIZE", ControlFile->toast_max_chunk_size,
-						   "TOAST_MAX_CHUNK_SIZE", (int) TOAST_MAX_CHUNK_SIZE),
+						   "TOAST_OID_MAX_CHUNK_SIZE", ControlFile->toast_max_chunk_size,
+						   "TOAST_OID_MAX_CHUNK_SIZE", (int) TOAST_OID_MAX_CHUNK_SIZE),
 				 errhint("It looks like you need to recompile or initdb.")));
 	if (ControlFile->loblksize != LOBLKSIZE)
 		ereport(FATAL,
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index a1cf30a8f2eb..d61347d11d45 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -5117,7 +5117,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		struct varlena *varlena;
 
 		/* va_rawsize is the size of the original datum -- including header */
-		struct varatt_external toast_pointer;
+		varatt_external_oid toast_pointer;
 		struct varatt_indirect redirect_pointer;
 		struct varlena *new_datum = NULL;
 		struct varlena *reconstructed;
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 2c398cd9e5cb..4aff647fccfd 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -4211,7 +4211,7 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 {
 	int			typlen;
 	struct varlena *attr;
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 
 	/* On first call, get the input type's typlen, and save at *fn_extra */
 	if (fcinfo->flinfo->fn_extra == NULL)
diff --git a/src/bin/pg_resetwal/pg_resetwal.c b/src/bin/pg_resetwal/pg_resetwal.c
index 7a4e4eb95706..638b41c922ba 100644
--- a/src/bin/pg_resetwal/pg_resetwal.c
+++ b/src/bin/pg_resetwal/pg_resetwal.c
@@ -717,7 +717,7 @@ GuessControlValues(void)
 	ControlFile.xlog_seg_size = DEFAULT_XLOG_SEG_SIZE;
 	ControlFile.nameDataLen = NAMEDATALEN;
 	ControlFile.indexMaxKeys = INDEX_MAX_KEYS;
-	ControlFile.toast_max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	ControlFile.toast_max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
 	ControlFile.loblksize = LOBLKSIZE;
 	ControlFile.float8ByVal = true; /* vestigial */
 
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index c393832d94c6..ba6b592cdb3f 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3533,7 +3533,7 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
       </row>
 
       <row>
-       <entry><structfield>max_toast_chunk_size</structfield></entry>
+       <entry><structfield>max_toast_oid_chunk_size</structfield></entry>
        <entry><type>integer</type></entry>
       </row>
 
diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml
index 02ddfda834a2..67600fd974d7 100644
--- a/doc/src/sgml/storage.sgml
+++ b/doc/src/sgml/storage.sgml
@@ -417,7 +417,7 @@ described in more detail below.
 
 <para>
 Out-of-line values are divided (after compression if used) into chunks of at
-most <symbol>TOAST_MAX_CHUNK_SIZE</symbol> bytes (by default this value is chosen
+most <symbol>TOAST_OID_MAX_CHUNK_SIZE</symbol> bytes (by default this value is chosen
 so that four chunk rows will fit on a page, making it about 2000 bytes).
 Each chunk is stored as a separate row in the <acronym>TOAST</acronym> table
 belonging to the owning table.  Every
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 164ced37583a..7ec6cef118fb 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -73,7 +73,7 @@ typedef enum SkipPages
  */
 typedef struct ToastedAttribute
 {
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 	BlockNumber blkno;			/* block in main table */
 	OffsetNumber offnum;		/* offset in main table */
 	AttrNumber	attnum;			/* attribute in main table */
@@ -1566,7 +1566,7 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 
 	toast_valueid = ta->toast_pointer.va_valueid;
 
-	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
 	last_chunk_seq = (extsize - 1) / max_chunk_size;
 
 	/* Sanity-check the sequence number. */
@@ -1672,7 +1672,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	uint16		infomask;
 	Oid8		toast_pointer_valueid;
 	CompactAttribute *thisatt;
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 
 	infomask = ctx->tuphdr->t_infomask;
 	thisatt = TupleDescCompactAttr(RelationGetDescr(ctx->rel), ctx->attnum);
@@ -1731,7 +1731,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	{
 		uint8		va_tag = VARTAG_EXTERNAL(tp + ctx->offset);
 
-		if (va_tag != VARTAG_ONDISK)
+		if (va_tag != VARTAG_ONDISK_OID)
 		{
 			report_corruption(ctx,
 							  psprintf("toasted attribute has unexpected TOAST tag %u",
@@ -1876,7 +1876,7 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	int32		expected_chunk_seq = 0;
 	int32		last_chunk_seq;
 	Oid8		toast_valueid;
-	int32		max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	int32		max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
 
 	extsize = VARATT_EXTERNAL_GET_EXTSIZE(ta->toast_pointer);
 	last_chunk_seq = (extsize - 1) / max_chunk_size;
-- 
2.51.0

v6-0005-Refactor-external-TOAST-pointer-code-for-better-p.patchtext/x-diff; charset=us-asciiDownload
From 18d5c662fca71d439d84f5e7d89d09c797bc1f47 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 13 Aug 2025 19:15:43 +0900
Subject: [PATCH v6 05/15] Refactor external TOAST pointer code for better
 pluggability

This commit introduces a new interface for external TOAST pointers,
which is able to make a translation of the varlena pointers stored on
disk to/from an new in-memory structure called toast_external.  The
types of varatt_external supported on disk need to be registered into a
new subsystem in a new file, called toast_external.[c|h], then define a
set of callbacks to allow the toasting and detoasting code to use it.

A follow-up change will rely on this refactoring to introduce new
vartag_external values with an associated varatt_external_* that is
able, which would be used in int8 TOAST tables.
---
 src/include/access/detoast.h                  |  12 +-
 src/include/access/heaptoast.h                |   3 +
 src/include/access/toast_external.h           | 176 ++++++++++++++++
 src/include/access/toast_helper.h             |   1 +
 src/include/varatt.h                          |  16 +-
 src/backend/access/common/Makefile            |   1 +
 src/backend/access/common/detoast.c           |  57 +++---
 src/backend/access/common/meson.build         |   1 +
 src/backend/access/common/toast_compression.c |  10 +-
 src/backend/access/common/toast_external.c    | 191 ++++++++++++++++++
 src/backend/access/common/toast_internals.c   |  84 +++++---
 src/backend/access/heap/heaptoast.c           |  20 +-
 src/backend/access/table/toast_helper.c       |  12 +-
 src/backend/access/transam/xlog.c             |   8 +-
 .../replication/logical/reorderbuffer.c       |  13 +-
 src/backend/utils/adt/varlena.c               |   7 +-
 src/bin/pg_resetwal/pg_resetwal.c             |   2 +-
 contrib/amcheck/verify_heapam.c               |  35 ++--
 src/tools/pgindent/typedefs.list              |   2 +
 19 files changed, 540 insertions(+), 111 deletions(-)
 create mode 100644 src/include/access/toast_external.h
 create mode 100644 src/backend/access/common/toast_external.c

diff --git a/src/include/access/detoast.h b/src/include/access/detoast.h
index 6435597b1127..b3ebad8b7cf9 100644
--- a/src/include/access/detoast.h
+++ b/src/include/access/detoast.h
@@ -14,10 +14,11 @@
 
 /*
  * Macro to fetch the possibly-unaligned contents of an EXTERNAL datum
- * into a local "varatt_external_oid" toast pointer.  This should be
- * just a memcpy, but some versions of gcc seem to produce broken code
- * that assumes the datum contents are aligned.  Introducing an explicit
- * intermediate "varattrib_1b_e *" variable seems to fix it.
+ * into a local "varatt_external_*" toast pointer, as supported
+ * in toast_external.c and varatt.h.  This should be just a memcpy, but
+ * some versions of gcc seem to produce broken code that assumes the datum
+ * contents are aligned.  Introducing an explicit intermediate
+ * "varattrib_1b_e *" variable seems to fix it.
  */
 #define VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr) \
 do { \
@@ -27,9 +28,6 @@ do { \
 	memcpy(&(toast_pointer), VARDATA_EXTERNAL(attre), sizeof(toast_pointer)); \
 } while (0)
 
-/* Size of an EXTERNAL datum that contains a standard TOAST pointer */
-#define TOAST_OID_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_oid))
-
 /* Size of an EXTERNAL datum that contains an indirection pointer */
 #define INDIRECT_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_indirect))
 
diff --git a/src/include/access/heaptoast.h b/src/include/access/heaptoast.h
index 59c82b2cb1a3..afa3d8ca95f7 100644
--- a/src/include/access/heaptoast.h
+++ b/src/include/access/heaptoast.h
@@ -88,6 +88,9 @@
 	 sizeof(int32) -									\
 	 VARHDRSZ)
 
+/* Maximum size of chunk possible */
+#define TOAST_MAX_CHUNK_SIZE	TOAST_OID_MAX_CHUNK_SIZE
+
 /* ----------
  * heap_toast_insert_or_update -
  *
diff --git a/src/include/access/toast_external.h b/src/include/access/toast_external.h
new file mode 100644
index 000000000000..6450343eab25
--- /dev/null
+++ b/src/include/access/toast_external.h
@@ -0,0 +1,176 @@
+/*-------------------------------------------------------------------------
+ *
+ * toast_external.h
+ *	  Support for on-disk external TOAST pointers
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1995, Regents of the University of California
+ *
+ * src/include/access/toast_external.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef TOAST_EXTERNAL_H
+#define TOAST_EXTERNAL_H
+
+#include "access/toast_compression.h"
+#include "varatt.h"
+
+/*
+ * Intermediate in-memory structure used when creating on-disk
+ * varatt_external_* or when deserializing varlena contents.
+ */
+typedef struct toast_external_data
+{
+	/* Original data size (includes header) */
+	int32		rawsize;
+
+	/* External saved size (without header) */
+	uint32		extsize;
+
+	/*
+	 * Compression method.
+	 *
+	 * If not compressed, set to TOAST_INVALID_COMPRESSION_ID.
+	 */
+	ToastCompressionId compression_method;
+
+	/* Relation OID of TOAST table containing the value */
+	Oid			toastrelid;
+
+	/*
+	 * Unique ID of value within TOAST table.  This could be an OID or an Oid8
+	 * value.  This field is large enough to be able to store any of these.
+	 */
+	Oid8		valueid;
+} toast_external_data;
+
+/*
+ * Metadata for external TOAST pointer kinds, separated based on their
+ * vartag_external.
+ */
+typedef struct toast_external_info
+{
+	/*
+	 * Maximum chunk of data authorized for this type of external TOAST
+	 * pointer, when dividing an entry by chunks.  Sized depending on the size
+	 * of its varatt_external_* structure.
+	 */
+	int32		maximum_chunk_size;
+
+	/*
+	 * Size of an external TOAST pointer of this type, typically
+	 * (VARHDRSZ_EXTERNAL + sizeof(varatt_external_struct)).
+	 */
+	int32		toast_pointer_size;
+
+	/*
+	 * Map an input varlena to a toast_external_data, for consumption in the
+	 * backend code.  "data" is an input/output result.
+	 */
+	void		(*to_external_data) (struct varlena *attr,
+									 toast_external_data *data);
+
+	/*
+	 * Create a varlena that will be used on-disk for the given TOAST type,
+	 * based on the given input data.
+	 *
+	 * The result is the varlena created, for on-disk insertion.
+	 */
+	struct varlena *(*create_external_data) (toast_external_data data);
+
+} toast_external_info;
+
+/* Retrieve a toast_external_info from a vartag */
+extern const toast_external_info *toast_external_get_info(uint8 tag);
+
+/* Retrieve toast_pointer_size using a TOAST attribute type */
+extern int32 toast_external_info_get_pointer_size(uint8 tag);
+
+/* Retrieve the vartag to assign to a TOAST typle */
+extern uint8 toast_external_assign_vartag(Oid toastrelid, Oid8 value);
+
+/*
+ * Testing whether an externally-stored value is compressed now requires
+ * comparing size stored in extsize (the actual length of the external data)
+ * to rawsize (the original uncompressed datum's size).  The latter includes
+ * VARHDRSZ overhead, the former doesn't.  We never use compression unless it
+ * actually saves space, so we expect either equality or less-than.
+ */
+static inline bool
+TOAST_EXTERNAL_IS_COMPRESSED(toast_external_data data)
+{
+	return data.extsize < (data.rawsize - VARHDRSZ);
+}
+
+/* Full data structure */
+static inline void
+toast_external_info_get_data(struct varlena *attr, toast_external_data *data)
+{
+	uint8		tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+
+	info->to_external_data(attr, data);
+}
+
+/*
+ * Helper routines to recover specific fields in toast_external_data.  Most
+ * code paths doing work with on-disk external TOAST pointers care about
+ * these.
+ */
+
+/* Detoasted "raw" size */
+static inline Size
+toast_external_info_get_rawsize(struct varlena *attr)
+{
+	uint8		tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+	toast_external_data data;
+
+	info->to_external_data(attr, &data);
+
+	return data.rawsize;
+}
+
+/* External saved size */
+static inline Size
+toast_external_info_get_extsize(struct varlena *attr)
+{
+	uint8		tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+	toast_external_data data;
+
+	info->to_external_data(attr, &data);
+
+	return data.extsize;
+}
+
+/* Compression method ID */
+static inline ToastCompressionId
+toast_external_info_get_compression_method(struct varlena *attr)
+{
+	uint8		tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+	toast_external_data data;
+
+	info->to_external_data(attr, &data);
+
+	return data.compression_method;
+}
+
+/* Value ID */
+static inline Oid8
+toast_external_info_get_valueid(struct varlena *attr)
+{
+	uint8		tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+	toast_external_data data;
+
+	info->to_external_data(attr, &data);
+
+	return data.valueid;
+}
+
+#endif							/* TOAST_EXTERNAL_H */
diff --git a/src/include/access/toast_helper.h b/src/include/access/toast_helper.h
index e6ab8afffb67..6bc912809f34 100644
--- a/src/include/access/toast_helper.h
+++ b/src/include/access/toast_helper.h
@@ -47,6 +47,7 @@ typedef struct
 	 * should be NULL in the case of an insert.
 	 */
 	Relation	ttc_rel;		/* the relation that contains the tuple */
+	int32		ttc_toast_pointer_size; /* size of external TOAST pointer */
 	Datum	   *ttc_values;		/* values from the tuple columns */
 	bool	   *ttc_isnull;		/* null flags for the tuple columns */
 	Datum	   *ttc_oldvalues;	/* values from previous tuple */
diff --git a/src/include/varatt.h b/src/include/varatt.h
index c873a59bb1c9..790d9f844c91 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -21,6 +21,9 @@
  * The data is compressed if and only if the external size stored in
  * va_extinfo is less than va_rawsize - VARHDRSZ.
  *
+ * The value ID is an OID, used for TOAST relations with OID as attribute
+ * for chunk_id.
+ *
  * This struct must not contain any padding, because we sometimes compare
  * these pointers using memcmp.
  *
@@ -51,7 +54,7 @@ typedef struct varatt_external_oid
  * The creator of such a Datum is entirely responsible that the referenced
  * storage survives for as long as referencing pointer Datums can exist.
  *
- * Note that just as for varatt_external_oid, this struct is stored
+ * Note that just as for varatt_external_*, this struct is stored
  * unaligned within any containing tuple.
  */
 typedef struct varatt_indirect
@@ -66,7 +69,7 @@ typedef struct varatt_indirect
  * storage.  APIs for this, in particular the definition of struct
  * ExpandedObjectHeader, are in src/include/utils/expandeddatum.h.
  *
- * Note that just as for varatt_external_oid, this struct is stored
+ * Note that just as for varatt_external_*, this struct is stored
  * unaligned within any containing tuple.
  */
 typedef struct ExpandedObjectHeader ExpandedObjectHeader;
@@ -357,11 +360,18 @@ VARATT_IS_EXTERNAL(const void *PTR)
 	return VARATT_IS_1B_E(PTR);
 }
 
+/* Is varlena datum a pointer to on-disk toasted data with OID value? */
+static inline bool
+VARATT_IS_EXTERNAL_ONDISK_OID(const void *PTR)
+{
+	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID;
+}
+
 /* Is varlena datum a pointer to on-disk toasted data? */
 static inline bool
 VARATT_IS_EXTERNAL_ONDISK(const void *PTR)
 {
-	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID;
+	return VARATT_IS_EXTERNAL_ONDISK_OID(PTR);
 }
 
 /* Is varlena datum an indirect pointer? */
diff --git a/src/backend/access/common/Makefile b/src/backend/access/common/Makefile
index e78de312659e..1ef86a245886 100644
--- a/src/backend/access/common/Makefile
+++ b/src/backend/access/common/Makefile
@@ -27,6 +27,7 @@ OBJS = \
 	syncscan.o \
 	tidstore.o \
 	toast_compression.o \
+	toast_external.o \
 	toast_internals.o \
 	tupconvert.o \
 	tupdesc.o
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index c187c32d96dd..8531c27439e4 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -16,6 +16,7 @@
 #include "access/detoast.h"
 #include "access/table.h"
 #include "access/tableam.h"
+#include "access/toast_external.h"
 #include "access/toast_internals.h"
 #include "common/int.h"
 #include "common/pg_lzcompress.h"
@@ -225,12 +226,12 @@ detoast_attr_slice(struct varlena *attr,
 
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
-		varatt_external_oid toast_pointer;
+		toast_external_data toast_pointer;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		toast_external_info_get_data(attr, &toast_pointer);
 
 		/* fast path for non-compressed external datums */
-		if (!VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+		if (!TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 			return toast_fetch_datum_slice(attr, sliceoffset, slicelength);
 
 		/*
@@ -240,7 +241,7 @@ detoast_attr_slice(struct varlena *attr,
 		 */
 		if (slicelimit >= 0)
 		{
-			int32		max_size = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+			int32		max_size = toast_pointer.extsize;
 
 			/*
 			 * Determine maximum amount of compressed data needed for a prefix
@@ -251,8 +252,7 @@ detoast_attr_slice(struct varlena *attr,
 			 * determine how much compressed data we need to be sure of being
 			 * able to decompress the required slice.
 			 */
-			if (VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) ==
-				TOAST_PGLZ_COMPRESSION_ID)
+			if (toast_pointer.compression_method == TOAST_PGLZ_COMPRESSION_ID)
 				max_size = pglz_maximum_compressed_size(slicelimit, max_size);
 
 			/*
@@ -344,20 +344,21 @@ toast_fetch_datum(struct varlena *attr)
 {
 	Relation	toastrel;
 	struct varlena *result;
-	varatt_external_oid toast_pointer;
+	toast_external_data toast_pointer;
 	int32		attrsize;
+	Oid8		valueid;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		elog(ERROR, "toast_fetch_datum shouldn't be called for non-ondisk datums");
 
 	/* Must copy to access aligned fields */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_external_info_get_data(attr, &toast_pointer);
 
-	attrsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+	attrsize = toast_pointer.extsize;
 
 	result = (struct varlena *) palloc(attrsize + VARHDRSZ);
 
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+	if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 		SET_VARSIZE_COMPRESSED(result, attrsize + VARHDRSZ);
 	else
 		SET_VARSIZE(result, attrsize + VARHDRSZ);
@@ -365,14 +366,15 @@ toast_fetch_datum(struct varlena *attr)
 	if (attrsize == 0)
 		return result;			/* Probably shouldn't happen, but just in
 								 * case. */
+	valueid = toast_pointer.valueid;
 
 	/*
 	 * Open the toast relation and its indexes
 	 */
-	toastrel = table_open(toast_pointer.va_toastrelid, AccessShareLock);
+	toastrel = table_open(toast_pointer.toastrelid, AccessShareLock);
 
 	/* Fetch all chunks */
-	table_relation_fetch_toast_slice(toastrel, toast_pointer.va_valueid,
+	table_relation_fetch_toast_slice(toastrel, valueid,
 									 attrsize, 0, attrsize, result);
 
 	/* Close toast table */
@@ -398,23 +400,26 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 {
 	Relation	toastrel;
 	struct varlena *result;
-	varatt_external_oid toast_pointer;
+	toast_external_data toast_pointer;
 	int32		attrsize;
+	Oid8		valueid;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		elog(ERROR, "toast_fetch_datum_slice shouldn't be called for non-ondisk datums");
 
 	/* Must copy to access aligned fields */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_external_info_get_data(attr, &toast_pointer);
+
+	valueid = toast_pointer.valueid;
 
 	/*
 	 * It's nonsense to fetch slices of a compressed datum unless when it's a
 	 * prefix -- this isn't lo_* we can't return a compressed datum which is
 	 * meaningful to toast later.
 	 */
-	Assert(!VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer) || 0 == sliceoffset);
+	Assert(!TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer) || 0 == sliceoffset);
 
-	attrsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+	attrsize = toast_pointer.extsize;
 
 	if (sliceoffset >= attrsize)
 	{
@@ -427,7 +432,7 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 	 * space required by va_tcinfo, which is stored at the beginning as an
 	 * int32 value.
 	 */
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer) && slicelength > 0)
+	if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer) && slicelength > 0)
 		slicelength = slicelength + sizeof(int32);
 
 	/*
@@ -440,7 +445,7 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 
 	result = (struct varlena *) palloc(slicelength + VARHDRSZ);
 
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+	if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 		SET_VARSIZE_COMPRESSED(result, slicelength + VARHDRSZ);
 	else
 		SET_VARSIZE(result, slicelength + VARHDRSZ);
@@ -449,10 +454,11 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 		return result;			/* Can save a lot of work at this point! */
 
 	/* Open the toast relation */
-	toastrel = table_open(toast_pointer.va_toastrelid, AccessShareLock);
+	toastrel = table_open(toast_pointer.toastrelid, AccessShareLock);
 
 	/* Fetch all chunks */
-	table_relation_fetch_toast_slice(toastrel, toast_pointer.va_valueid,
+	table_relation_fetch_toast_slice(toastrel,
+									 valueid,
 									 attrsize, sliceoffset, slicelength,
 									 result);
 
@@ -549,11 +555,7 @@ toast_raw_datum_size(Datum value)
 
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
-		/* va_rawsize is the size of the original datum -- including header */
-		varatt_external_oid toast_pointer;
-
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-		result = toast_pointer.va_rawsize;
+		result = toast_external_info_get_rawsize(attr);
 	}
 	else if (VARATT_IS_EXTERNAL_INDIRECT(attr))
 	{
@@ -610,10 +612,7 @@ toast_datum_size(Datum value)
 		 * compressed or not.  We do not count the size of the toast pointer
 		 * ... should we?
 		 */
-		varatt_external_oid toast_pointer;
-
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-		result = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+		result = toast_external_info_get_extsize(attr);
 	}
 	else if (VARATT_IS_EXTERNAL_INDIRECT(attr))
 	{
diff --git a/src/backend/access/common/meson.build b/src/backend/access/common/meson.build
index e3cdbe7a22e1..c20f2e88921e 100644
--- a/src/backend/access/common/meson.build
+++ b/src/backend/access/common/meson.build
@@ -15,6 +15,7 @@ backend_sources += files(
   'syncscan.c',
   'tidstore.c',
   'toast_compression.c',
+  'toast_external.c',
   'toast_internals.c',
   'tupconvert.c',
   'tupdesc.c',
diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c
index 08f572f31eed..94606a58c8fb 100644
--- a/src/backend/access/common/toast_compression.c
+++ b/src/backend/access/common/toast_compression.c
@@ -19,6 +19,7 @@
 
 #include "access/detoast.h"
 #include "access/toast_compression.h"
+#include "access/toast_external.h"
 #include "common/pg_lzcompress.h"
 #include "varatt.h"
 
@@ -261,14 +262,7 @@ toast_get_compression_id(struct varlena *attr)
 	 * toast compression header.
 	 */
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
-	{
-		varatt_external_oid toast_pointer;
-
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-
-		if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
-			cmid = VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer);
-	}
+		cmid = toast_external_info_get_compression_method(attr);
 	else if (VARATT_IS_COMPRESSED(attr))
 		cmid = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(attr);
 
diff --git a/src/backend/access/common/toast_external.c b/src/backend/access/common/toast_external.c
new file mode 100644
index 000000000000..5c8679a0f485
--- /dev/null
+++ b/src/backend/access/common/toast_external.c
@@ -0,0 +1,191 @@
+/*-------------------------------------------------------------------------
+ *
+ * toast_external.c
+ *	  Functions for the support of external on-disk TOAST pointers.
+ *
+ * This includes all the types of external on-disk TOAST pointers supported
+ * by the backend, based on the callbacks and data defined in
+ * external_toast.h.
+ *
+ * Copyright (c) 2025, PostgreSQL Global Development Group
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/common/toast_external.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/detoast.h"
+#include "access/heaptoast.h"
+#include "access/toast_external.h"
+
+/* Callbacks for VARTAG_ONDISK_OID */
+static void ondisk_oid_to_external_data(struct varlena *attr,
+										toast_external_data *data);
+static struct varlena *ondisk_oid_create_external_data(toast_external_data data);
+
+/*
+ * Size of an EXTERNAL datum that contains a standard TOAST pointer (OID
+ * value).
+ */
+#define TOAST_OID_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_oid))
+
+/*
+ * For now there are only two types, all defined in this file.  For now this
+ * is the maximum value of vartag_external, which is a historical choice.
+ */
+#define TOAST_EXTERNAL_INFO_SIZE	(VARTAG_ONDISK_OID + 1)
+
+/*
+ * The different kinds of on-disk external TOAST pointers, divided by
+ * vartag_external.
+ *
+ * See comments for struct toast_external_info about the details of the
+ * individual fields.
+ */
+static const toast_external_info toast_external_infos[TOAST_EXTERNAL_INFO_SIZE] = {
+	[VARTAG_ONDISK_OID] = {
+		.toast_pointer_size = TOAST_OID_POINTER_SIZE,
+		.maximum_chunk_size = TOAST_OID_MAX_CHUNK_SIZE,
+		.to_external_data = ondisk_oid_to_external_data,
+		.create_external_data = ondisk_oid_create_external_data,
+	},
+};
+
+/*
+ * toast_external_get_info
+ *
+ * Get toast_external_info of the defined vartag_external, central set of
+ * callbacks, based on a "tag", which is a vartag_external value for an
+ * on-disk external varlena.
+ */
+const toast_external_info *
+toast_external_get_info(uint8 tag)
+{
+	const toast_external_info *res = &toast_external_infos[tag];
+
+	/* sanity check, as it could be possible that corrupted data is read */
+	if (res == NULL)
+		elog(ERROR, "incorrect value %u for toast_external_info", tag);
+	return res;
+}
+
+/*
+ * toast_external_info_get_pointer_size
+ *
+ * Get external TOAST pointer size based on the attribute type of a TOAST
+ * value.  "tag" is a vartag_external value.
+ */
+int32
+toast_external_info_get_pointer_size(uint8 tag)
+{
+	return toast_external_infos[tag].toast_pointer_size;
+}
+
+/*
+ * toast_external_assign_vartag
+ *
+ * Assign the vartag_external of a TOAST tuple, based on the TOAST relation
+ * it uses and its value.
+ *
+ * An invalid value can be given by the caller of this routine, in which
+ * case a default vartag should be provided based on only the toast relation
+ * used.
+ */
+uint8
+toast_external_assign_vartag(Oid toastrelid, Oid8 valueid)
+{
+	/*
+	 * If dealing with a code path where a TOAST relation may not be assigned,
+	 * like heap_toast_insert_or_update(), just use the legacy
+	 * vartag_external.
+	 */
+	if (!OidIsValid(toastrelid))
+		return VARTAG_ONDISK_OID;
+
+	/*
+	 * Currently there is only one type of vartag_external supported: 4-byte
+	 * value with OID for the chunk_id type.
+	 *
+	 * Note: This routine will be extended to be able to use multiple
+	 * vartag_external within a single TOAST relation type, that may change
+	 * depending on the value used.
+	 */
+	return VARTAG_ONDISK_OID;
+}
+
+/*
+ * Helper routines able to translate the various varatt_external_* from/to
+ * the in-memory representation toast_external_data used in the backend.
+ */
+
+/* Callbacks for VARTAG_ONDISK_OID */
+
+/*
+ * ondisk_oid_to_external_data
+ *
+ * Translate a varlena to its toast_external_data representation, to be used
+ * by the backend code.
+ */
+static void
+ondisk_oid_to_external_data(struct varlena *attr, toast_external_data *data)
+{
+	varatt_external_oid external;
+
+	VARATT_EXTERNAL_GET_POINTER(external, attr);
+	data->rawsize = external.va_rawsize;
+
+	/*
+	 * External size and compression methods are stored in the same field,
+	 * extract.
+	 */
+	if (VARATT_EXTERNAL_IS_COMPRESSED(external))
+	{
+		data->extsize = VARATT_EXTERNAL_GET_EXTSIZE(external);
+		data->compression_method = VARATT_EXTERNAL_GET_COMPRESS_METHOD(external);
+	}
+	else
+	{
+		data->extsize = external.va_extinfo;
+		data->compression_method = TOAST_INVALID_COMPRESSION_ID;
+	}
+
+	data->valueid = (Oid8) external.va_valueid;
+	data->toastrelid = external.va_toastrelid;
+}
+
+/*
+ * ondisk_oid_create_external_data
+ *
+ * Create a new varlena based on the input toast_external_data, to be used
+ * when saving a new TOAST value.
+ */
+static struct varlena *
+ondisk_oid_create_external_data(toast_external_data data)
+{
+	struct varlena *result = NULL;
+	varatt_external_oid external;
+
+	external.va_rawsize = data.rawsize;
+
+	if (data.compression_method != TOAST_INVALID_COMPRESSION_ID)
+	{
+		/* Set size and compression method, in a single field. */
+		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(external,
+													 data.extsize,
+													 data.compression_method);
+	}
+	else
+		external.va_extinfo = data.extsize;
+
+	external.va_toastrelid = data.toastrelid;
+	external.va_valueid = (Oid) data.valueid;
+
+	result = (struct varlena *) palloc(TOAST_OID_POINTER_SIZE);
+	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_OID);
+	memcpy(VARDATA_EXTERNAL(result), &external, sizeof(external));
+
+	return result;
+}
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 770dbb5a6104..a68869f58517 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -18,6 +18,7 @@
 #include "access/heapam.h"
 #include "access/heaptoast.h"
 #include "access/table.h"
+#include "access/toast_external.h"
 #include "access/toast_internals.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -124,13 +125,15 @@ toast_save_datum(Relation rel, Datum value,
 	TupleDesc	toasttupDesc;
 	CommandId	mycid = GetCurrentCommandId(true);
 	struct varlena *result;
-	varatt_external_oid toast_pointer;
+	toast_external_data toast_pointer;
 	int32		chunk_seq = 0;
 	char	   *data_p;
 	int32		data_todo;
 	Pointer		dval = DatumGetPointer(value);
 	int			num_indexes;
 	int			validIndex;
+	const toast_external_info *info;
+	uint8		tag = VARTAG_INDIRECT;	/* init value does not matter */
 
 	Assert(!VARATT_IS_EXTERNAL(dval));
 
@@ -162,28 +165,41 @@ toast_save_datum(Relation rel, Datum value,
 	{
 		data_p = VARDATA_SHORT(dval);
 		data_todo = VARSIZE_SHORT(dval) - VARHDRSZ_SHORT;
-		toast_pointer.va_rawsize = data_todo + VARHDRSZ;	/* as if not short */
-		toast_pointer.va_extinfo = data_todo;
+		toast_pointer.rawsize = data_todo + VARHDRSZ;	/* as if not short */
+		toast_pointer.extsize = data_todo;
+
+		/*
+		 * TOAST_INVALID_COMPRESSION_ID means that the varlena is not
+		 * compressed, see toast_get_compression_id().
+		 */
+		toast_pointer.compression_method = TOAST_INVALID_COMPRESSION_ID;
 	}
 	else if (VARATT_IS_COMPRESSED(dval))
 	{
 		data_p = VARDATA(dval);
 		data_todo = VARSIZE(dval) - VARHDRSZ;
 		/* rawsize in a compressed datum is just the size of the payload */
-		toast_pointer.va_rawsize = VARDATA_COMPRESSED_GET_EXTSIZE(dval) + VARHDRSZ;
+		toast_pointer.rawsize = VARDATA_COMPRESSED_GET_EXTSIZE(dval) + VARHDRSZ;
 
 		/* set external size and compression method */
-		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, data_todo,
-													 VARDATA_COMPRESSED_GET_COMPRESS_METHOD(dval));
+		toast_pointer.extsize = data_todo;
+		toast_pointer.compression_method = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(dval);
+
 		/* Assert that the numbers look like it's compressed */
-		Assert(VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer));
+		Assert(TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer));
 	}
 	else
 	{
 		data_p = VARDATA(dval);
 		data_todo = VARSIZE(dval) - VARHDRSZ;
-		toast_pointer.va_rawsize = VARSIZE(dval);
-		toast_pointer.va_extinfo = data_todo;
+		toast_pointer.rawsize = VARSIZE(dval);
+		toast_pointer.extsize = data_todo;
+
+		/*
+		 * TOAST_INVALID_COMPRESSION_ID means that the varlena is not
+		 * compressed, see toast_get_compression_id().
+		 */
+		toast_pointer.compression_method = TOAST_INVALID_COMPRESSION_ID;
 	}
 
 	/*
@@ -195,9 +211,9 @@ toast_save_datum(Relation rel, Datum value,
 	 * if we have to substitute such an OID.
 	 */
 	if (OidIsValid(rel->rd_toastoid))
-		toast_pointer.va_toastrelid = rel->rd_toastoid;
+		toast_pointer.toastrelid = rel->rd_toastoid;
 	else
-		toast_pointer.va_toastrelid = RelationGetRelid(toastrel);
+		toast_pointer.toastrelid = RelationGetRelid(toastrel);
 
 	/*
 	 * Choose an OID to use as the value ID for this toast value.
@@ -214,7 +230,7 @@ toast_save_datum(Relation rel, Datum value,
 	if (!OidIsValid(rel->rd_toastoid))
 	{
 		/* normal case: just choose an unused OID */
-		toast_pointer.va_valueid =
+		toast_pointer.valueid =
 			GetNewOidWithIndex(toastrel,
 							   RelationGetRelid(toastidxs[validIndex]),
 							   (AttrNumber) 1);
@@ -222,18 +238,18 @@ toast_save_datum(Relation rel, Datum value,
 	else
 	{
 		/* rewrite case: check to see if value was in old toast table */
-		toast_pointer.va_valueid = InvalidOid;
+		toast_pointer.valueid = InvalidOid8;
 		if (oldexternal != NULL)
 		{
-			varatt_external_oid old_toast_pointer;
+			toast_external_data old_toast_pointer;
 
 			Assert(VARATT_IS_EXTERNAL_ONDISK(oldexternal));
-			/* Must copy to access aligned fields */
-			VARATT_EXTERNAL_GET_POINTER(old_toast_pointer, oldexternal);
-			if (old_toast_pointer.va_toastrelid == rel->rd_toastoid)
+			toast_external_info_get_data(oldexternal, &old_toast_pointer);
+
+			if (old_toast_pointer.toastrelid == rel->rd_toastoid)
 			{
 				/* This value came from the old toast table; reuse its OID */
-				toast_pointer.va_valueid = old_toast_pointer.va_valueid;
+				toast_pointer.valueid = old_toast_pointer.valueid;
 
 				/*
 				 * There is a corner case here: the table rewrite might have
@@ -253,14 +269,14 @@ toast_save_datum(Relation rel, Datum value,
 				 * be reclaimed by VACUUM.
 				 */
 				if (toastrel_valueid_exists(toastrel,
-											toast_pointer.va_valueid))
+											toast_pointer.valueid))
 				{
 					/* Match, so short-circuit the data storage loop below */
 					data_todo = 0;
 				}
 			}
 		}
-		if (toast_pointer.va_valueid == InvalidOid)
+		if (toast_pointer.valueid == InvalidOid8)
 		{
 			/*
 			 * new value; must choose an OID that doesn't conflict in either
@@ -268,15 +284,23 @@ toast_save_datum(Relation rel, Datum value,
 			 */
 			do
 			{
-				toast_pointer.va_valueid =
+				toast_pointer.valueid =
 					GetNewOidWithIndex(toastrel,
 									   RelationGetRelid(toastidxs[validIndex]),
 									   (AttrNumber) 1);
 			} while (toastid_valueid_exists(rel->rd_toastoid,
-											toast_pointer.va_valueid));
+											toast_pointer.valueid));
 		}
 	}
 
+	/*
+	 * Retrieve the vartag that can be assigned for the new TOAST tuple. This
+	 * depends on the type of TOAST table and its assigned value.
+	 */
+	tag = toast_external_assign_vartag(toast_pointer.toastrelid,
+									   toast_pointer.valueid);
+	info = toast_external_get_info(tag);
+
 	/*
 	 * Split up the item into chunks
 	 */
@@ -300,12 +324,12 @@ toast_save_datum(Relation rel, Datum value,
 		/*
 		 * Calculate the size of this chunk
 		 */
-		chunk_size = Min(TOAST_OID_MAX_CHUNK_SIZE, data_todo);
+		chunk_size = Min(info->maximum_chunk_size, data_todo);
 
 		/*
 		 * Build a tuple and store it
 		 */
-		t_values[0] = ObjectIdGetDatum(toast_pointer.va_valueid);
+		t_values[0] = ObjectIdGetDatum(toast_pointer.valueid);
 		t_values[1] = Int32GetDatum(chunk_seq++);
 		SET_VARSIZE(&chunk_data, chunk_size + VARHDRSZ);
 		memcpy(VARDATA(&chunk_data), data_p, chunk_size);
@@ -361,9 +385,7 @@ toast_save_datum(Relation rel, Datum value,
 	/*
 	 * Create the TOAST pointer value that we'll return
 	 */
-	result = (struct varlena *) palloc(TOAST_OID_POINTER_SIZE);
-	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_OID);
-	memcpy(VARDATA_EXTERNAL(result), &toast_pointer, sizeof(toast_pointer));
+	result = info->create_external_data(toast_pointer);
 
 	return PointerGetDatum(result);
 }
@@ -378,7 +400,7 @@ void
 toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 {
 	struct varlena *attr = (struct varlena *) DatumGetPointer(value);
-	varatt_external_oid toast_pointer;
+	toast_external_data toast_pointer;
 	Relation	toastrel;
 	Relation   *toastidxs;
 	ScanKeyData toastkey;
@@ -391,12 +413,12 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 		return;
 
 	/* Must copy to access aligned fields */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_external_info_get_data(attr, &toast_pointer);
 
 	/*
 	 * Open the toast relation and its indexes
 	 */
-	toastrel = table_open(toast_pointer.va_toastrelid, RowExclusiveLock);
+	toastrel = table_open(toast_pointer.toastrelid, RowExclusiveLock);
 
 	/* Fetch valid relation used for process */
 	validIndex = toast_open_indexes(toastrel,
@@ -410,7 +432,7 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	ScanKeyInit(&toastkey,
 				(AttrNumber) 1,
 				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(toast_pointer.va_valueid));
+				ObjectIdGetDatum(toast_pointer.valueid));
 
 	/*
 	 * Find all the chunks.  (We don't actually care whether we see them in
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index ddde7fcf79a4..230f2a6f35eb 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -28,6 +28,7 @@
 #include "access/genam.h"
 #include "access/heapam.h"
 #include "access/heaptoast.h"
+#include "access/toast_external.h"
 #include "access/toast_helper.h"
 #include "access/toast_internals.h"
 #include "utils/fmgroids.h"
@@ -109,6 +110,7 @@ heap_toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup,
 	Datum		toast_oldvalues[MaxHeapAttributeNumber];
 	ToastAttrInfo toast_attr[MaxHeapAttributeNumber];
 	ToastTupleContext ttc;
+	uint8		tag;
 
 	/*
 	 * Ignore the INSERT_SPECULATIVE option. Speculative insertions/super
@@ -140,6 +142,16 @@ heap_toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup,
 	 * Prepare for toasting
 	 * ----------
 	 */
+
+	/*
+	 * Retrieve the toast pointer size based on the type of external TOAST
+	 * pointer assumed to be used.
+	 */
+
+	/* The default value is invalid, to work as a default. */
+	tag = toast_external_assign_vartag(rel->rd_rel->reltoastrelid, InvalidOid8);
+	ttc.ttc_toast_pointer_size = toast_external_info_get_pointer_size(tag);
+
 	ttc.ttc_rel = rel;
 	ttc.ttc_values = toast_values;
 	ttc.ttc_isnull = toast_isnull;
@@ -640,6 +652,8 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 	int			num_indexes;
 	int			validIndex;
 	int32		max_chunk_size;
+	const toast_external_info *info;
+	uint8		tag = VARTAG_INDIRECT;	/* init value does not matter */
 
 	/* Look for the valid index of toast relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -647,7 +661,11 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 									&toastidxs,
 									&num_indexes);
 
-	max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
+	/* Grab the information for toast_external_data */
+	tag = toast_external_assign_vartag(RelationGetRelid(toastrel), valueid);
+	info = toast_external_get_info(tag);
+
+	max_chunk_size = info->maximum_chunk_size;
 
 	totalchunks = ((attrsize - 1) / max_chunk_size) + 1;
 	startchunk = sliceoffset / max_chunk_size;
diff --git a/src/backend/access/table/toast_helper.c b/src/backend/access/table/toast_helper.c
index 0c58c6c32565..76a7cfe6174e 100644
--- a/src/backend/access/table/toast_helper.c
+++ b/src/backend/access/table/toast_helper.c
@@ -171,8 +171,10 @@ toast_tuple_init(ToastTupleContext *ttc)
  * The column must have attstorage EXTERNAL or EXTENDED if check_main is
  * false, and must have attstorage MAIN if check_main is true.
  *
- * The column must have a minimum size of MAXALIGN(TOAST_OID_POINTER_SIZE);
- * if not, no benefit is to be expected by compressing it.
+ * The column must have a minimum size of MAXALIGN(tcc_toast_pointer_size);
+ * if not, no benefit is to be expected by compressing it.  The TOAST
+ * pointer size is given by the caller, depending on the type of TOAST
+ * table we are dealing with.
  *
  * The return value is the index of the biggest suitable column, or
  * -1 if there is none.
@@ -184,10 +186,14 @@ toast_tuple_find_biggest_attribute(ToastTupleContext *ttc,
 	TupleDesc	tupleDesc = ttc->ttc_rel->rd_att;
 	int			numAttrs = tupleDesc->natts;
 	int			biggest_attno = -1;
-	int32		biggest_size = MAXALIGN(TOAST_OID_POINTER_SIZE);
+	int32		biggest_size = 0;
 	int32		skip_colflags = TOASTCOL_IGNORE;
 	int			i;
 
+	/* Define the lower-bound */
+	biggest_size = MAXALIGN(ttc->ttc_toast_pointer_size);
+	Assert(biggest_size != 0);
+
 	if (for_compression)
 		skip_colflags |= TOASTCOL_INCOMPRESSIBLE;
 
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index b521c24c6c73..0baf0ac6160a 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4254,7 +4254,7 @@ WriteControlFile(void)
 	ControlFile->nameDataLen = NAMEDATALEN;
 	ControlFile->indexMaxKeys = INDEX_MAX_KEYS;
 
-	ControlFile->toast_max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
+	ControlFile->toast_max_chunk_size = TOAST_MAX_CHUNK_SIZE;
 	ControlFile->loblksize = LOBLKSIZE;
 
 	ControlFile->float8ByVal = true;	/* vestigial */
@@ -4497,15 +4497,15 @@ ReadControlFile(void)
 						   "INDEX_MAX_KEYS", ControlFile->indexMaxKeys,
 						   "INDEX_MAX_KEYS", INDEX_MAX_KEYS),
 				 errhint("It looks like you need to recompile or initdb.")));
-	if (ControlFile->toast_max_chunk_size != TOAST_OID_MAX_CHUNK_SIZE)
+	if (ControlFile->toast_max_chunk_size != TOAST_MAX_CHUNK_SIZE)
 		ereport(FATAL,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("database files are incompatible with server"),
 		/* translator: %s is a variable name and %d is its value */
 				 errdetail("The database cluster was initialized with %s %d,"
 						   " but the server was compiled with %s %d.",
-						   "TOAST_OID_MAX_CHUNK_SIZE", ControlFile->toast_max_chunk_size,
-						   "TOAST_OID_MAX_CHUNK_SIZE", (int) TOAST_OID_MAX_CHUNK_SIZE),
+						   "TOAST_MAX_CHUNK_SIZE", ControlFile->toast_max_chunk_size,
+						   "TOAST_MAX_CHUNK_SIZE", (int) TOAST_MAX_CHUNK_SIZE),
 				 errhint("It looks like you need to recompile or initdb.")));
 	if (ControlFile->loblksize != LOBLKSIZE)
 		ereport(FATAL,
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index d61347d11d45..2db447f58ad5 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -92,6 +92,7 @@
 #include "access/detoast.h"
 #include "access/heapam.h"
 #include "access/rewriteheap.h"
+#include "access/toast_external.h"
 #include "access/transam.h"
 #include "access/xact.h"
 #include "access/xlog_internal.h"
@@ -5117,7 +5118,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		struct varlena *varlena;
 
 		/* va_rawsize is the size of the original datum -- including header */
-		varatt_external_oid toast_pointer;
+		toast_external_data toast_pointer;
 		struct varatt_indirect redirect_pointer;
 		struct varlena *new_datum = NULL;
 		struct varlena *reconstructed;
@@ -5147,8 +5148,8 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		if (!VARATT_IS_EXTERNAL(varlena))
 			continue;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, varlena);
-		toast_valueid = toast_pointer.va_valueid;
+		toast_external_info_get_data(varlena, &toast_pointer);
+		toast_valueid = toast_pointer.valueid;
 
 		/*
 		 * Check whether the toast tuple changed, replace if so.
@@ -5166,7 +5167,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 
 		free[natt] = true;
 
-		reconstructed = palloc0(toast_pointer.va_rawsize);
+		reconstructed = palloc0(toast_pointer.rawsize);
 
 		ent->reconstructed = reconstructed;
 
@@ -5191,10 +5192,10 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 				   VARSIZE(chunk) - VARHDRSZ);
 			data_done += VARSIZE(chunk) - VARHDRSZ;
 		}
-		Assert(data_done == VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer));
+		Assert(data_done == toast_pointer.extsize);
 
 		/* make sure its marked as compressed or not */
-		if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+		if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 			SET_VARSIZE_COMPRESSED(reconstructed, data_done + VARHDRSZ);
 		else
 			SET_VARSIZE(reconstructed, data_done + VARHDRSZ);
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 4aff647fccfd..78b3e65b2396 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -19,6 +19,7 @@
 
 #include "access/detoast.h"
 #include "access/toast_compression.h"
+#include "access/toast_external.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_type.h"
 #include "common/hashfn.h"
@@ -4211,7 +4212,7 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 {
 	int			typlen;
 	struct varlena *attr;
-	varatt_external_oid toast_pointer;
+	Oid8		toast_valueid;
 
 	/* On first call, get the input type's typlen, and save at *fn_extra */
 	if (fcinfo->flinfo->fn_extra == NULL)
@@ -4238,9 +4239,9 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		PG_RETURN_NULL();
 
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_valueid = toast_external_info_get_valueid(attr);
 
-	PG_RETURN_OID(toast_pointer.va_valueid);
+	PG_RETURN_OID(toast_valueid);
 }
 
 /*
diff --git a/src/bin/pg_resetwal/pg_resetwal.c b/src/bin/pg_resetwal/pg_resetwal.c
index 638b41c922ba..7a4e4eb95706 100644
--- a/src/bin/pg_resetwal/pg_resetwal.c
+++ b/src/bin/pg_resetwal/pg_resetwal.c
@@ -717,7 +717,7 @@ GuessControlValues(void)
 	ControlFile.xlog_seg_size = DEFAULT_XLOG_SEG_SIZE;
 	ControlFile.nameDataLen = NAMEDATALEN;
 	ControlFile.indexMaxKeys = INDEX_MAX_KEYS;
-	ControlFile.toast_max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
+	ControlFile.toast_max_chunk_size = TOAST_MAX_CHUNK_SIZE;
 	ControlFile.loblksize = LOBLKSIZE;
 	ControlFile.float8ByVal = true; /* vestigial */
 
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 7ec6cef118fb..9cf3c081bf01 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -16,6 +16,7 @@
 #include "access/multixact.h"
 #include "access/relation.h"
 #include "access/table.h"
+#include "access/toast_external.h"
 #include "access/toast_internals.h"
 #include "access/visibilitymap.h"
 #include "access/xact.h"
@@ -73,7 +74,8 @@ typedef enum SkipPages
  */
 typedef struct ToastedAttribute
 {
-	varatt_external_oid toast_pointer;
+	toast_external_data toast_pointer;
+	const toast_external_info *info;
 	BlockNumber blkno;			/* block in main table */
 	OffsetNumber offnum;		/* offset in main table */
 	AttrNumber	attnum;			/* attribute in main table */
@@ -1564,9 +1566,9 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	Oid8		toast_valueid;
 	int32		max_chunk_size;
 
-	toast_valueid = ta->toast_pointer.va_valueid;
+	toast_valueid = ta->toast_pointer.valueid;
 
-	max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
+	max_chunk_size = ta->info->maximum_chunk_size;
 	last_chunk_seq = (extsize - 1) / max_chunk_size;
 
 	/* Sanity-check the sequence number. */
@@ -1672,7 +1674,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	uint16		infomask;
 	Oid8		toast_pointer_valueid;
 	CompactAttribute *thisatt;
-	varatt_external_oid toast_pointer;
+	toast_external_data toast_pointer;
 
 	infomask = ctx->tuphdr->t_infomask;
 	thisatt = TupleDescCompactAttr(RelationGetDescr(ctx->rel), ctx->attnum);
@@ -1778,24 +1780,24 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	/*
 	 * Must copy attr into toast_pointer for alignment considerations
 	 */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-	toast_pointer_valueid = toast_pointer.va_valueid;
+	toast_external_info_get_data(attr, &toast_pointer);
+	toast_pointer_valueid = toast_pointer.valueid;
 
 	/* Toasted attributes too large to be untoasted should never be stored */
-	if (toast_pointer.va_rawsize > VARLENA_SIZE_LIMIT)
+	if (toast_pointer.rawsize > VARLENA_SIZE_LIMIT)
 		report_corruption(ctx,
 						  psprintf("toast value " OID8_FORMAT " rawsize %d exceeds limit %d",
 								   toast_pointer_valueid,
-								   toast_pointer.va_rawsize,
+								   toast_pointer.rawsize,
 								   VARLENA_SIZE_LIMIT));
 
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+	if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 	{
 		ToastCompressionId cmid;
 		bool		valid = false;
 
 		/* Compressed attributes should have a valid compression method */
-		cmid = TOAST_COMPRESS_METHOD(&toast_pointer);
+		cmid = toast_pointer.compression_method;
 		switch (cmid)
 		{
 				/* List of all valid compression method IDs */
@@ -1849,7 +1851,8 @@ check_tuple_attribute(HeapCheckContext *ctx)
 
 		ta = (ToastedAttribute *) palloc0(sizeof(ToastedAttribute));
 
-		VARATT_EXTERNAL_GET_POINTER(ta->toast_pointer, attr);
+		toast_external_info_get_data(attr, &ta->toast_pointer);
+		ta->info = toast_external_get_info(VARTAG_EXTERNAL(attr));
 		ta->blkno = ctx->blkno;
 		ta->offnum = ctx->offnum;
 		ta->attnum = ctx->attnum;
@@ -1876,9 +1879,11 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	int32		expected_chunk_seq = 0;
 	int32		last_chunk_seq;
 	Oid8		toast_valueid;
-	int32		max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
+	int32		max_chunk_size;
 
-	extsize = VARATT_EXTERNAL_GET_EXTSIZE(ta->toast_pointer);
+	extsize = ta->toast_pointer.extsize;
+
+	max_chunk_size = ta->info->maximum_chunk_size;
 	last_chunk_seq = (extsize - 1) / max_chunk_size;
 
 	/*
@@ -1887,7 +1892,7 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	ScanKeyInit(&toastkey,
 				(AttrNumber) 1,
 				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(ta->toast_pointer.va_valueid));
+				ObjectIdGetDatum(ta->toast_pointer.valueid));
 
 	/*
 	 * Check if any chunks for this toasted object exist in the toast table,
@@ -1907,7 +1912,7 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	}
 	systable_endscan_ordered(toastscan);
 
-	toast_valueid = ta->toast_pointer.va_valueid;
+	toast_valueid = ta->toast_pointer.valueid;
 
 	if (!found_toasttup)
 		report_toast_corruption(ctx, ta,
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index e90af5b2ad36..500fb21fe55a 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -4137,6 +4137,8 @@ timeout_params
 timerCA
 tlist_vinfo
 toast_compress_header
+toast_external_data
+toast_external_info
 tokenize_error_callback_arg
 transferMode
 transfer_thread_arg
-- 
2.51.0

v6-0006-Move-static-inline-routines-of-varatt_external_oi.patchtext/x-diff; charset=us-asciiDownload
From 6569e45133ba2b460cdc359e42a78e7aad29460f Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 8 Aug 2025 15:40:04 +0900
Subject: [PATCH v6 06/15] Move static inline routines of varatt_external_oid
 to toast_external.c

This isolates most of the knowledge of varatt_external_oid into the
local area where it is manipulated through the toast_external transition
type, with the backend code not requiring it.  Extension code should not
need it either, as toast_external should be the layer to use when
looking at external on-dist TOAST varlenas.
---
 src/include/varatt.h                       | 31 -----------------
 src/backend/access/common/toast_external.c | 40 ++++++++++++++++++++--
 2 files changed, 37 insertions(+), 34 deletions(-)

diff --git a/src/include/varatt.h b/src/include/varatt.h
index 790d9f844c91..035c0f95e5b6 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -513,22 +513,6 @@ VARDATA_COMPRESSED_GET_COMPRESS_METHOD(const void *PTR)
 	return ((varattrib_4b *) PTR)->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS;
 }
 
-/*
- * Same for external Datums; but note argument is a struct
- * varatt_external_oid.
- */
-static inline Size
-VARATT_EXTERNAL_GET_EXTSIZE(varatt_external_oid toast_pointer)
-{
-	return toast_pointer.va_extinfo & VARLENA_EXTSIZE_MASK;
-}
-
-static inline uint32
-VARATT_EXTERNAL_GET_COMPRESS_METHOD(varatt_external_oid toast_pointer)
-{
-	return toast_pointer.va_extinfo >> VARLENA_EXTSIZE_BITS;
-}
-
 /* Set size and compress method of an externally-stored varlena datum */
 /* This has to remain a macro; beware multiple evaluations! */
 #define VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, len, cm) \
@@ -538,19 +522,4 @@ VARATT_EXTERNAL_GET_COMPRESS_METHOD(varatt_external_oid toast_pointer)
 		((toast_pointer).va_extinfo = \
 			(len) | ((uint32) (cm) << VARLENA_EXTSIZE_BITS)); \
 	} while (0)
-
-/*
- * Testing whether an externally-stored value is compressed now requires
- * comparing size stored in va_extinfo (the actual length of the external data)
- * to rawsize (the original uncompressed datum's size).  The latter includes
- * VARHDRSZ overhead, the former doesn't.  We never use compression unless it
- * actually saves space, so we expect either equality or less-than.
- */
-static inline bool
-VARATT_EXTERNAL_IS_COMPRESSED(varatt_external_oid toast_pointer)
-{
-	return VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) <
-		(Size) (toast_pointer.va_rawsize - VARHDRSZ);
-}
-
 #endif
diff --git a/src/backend/access/common/toast_external.c b/src/backend/access/common/toast_external.c
index 5c8679a0f485..d7bf2f7c69b2 100644
--- a/src/backend/access/common/toast_external.c
+++ b/src/backend/access/common/toast_external.c
@@ -26,6 +26,40 @@ static void ondisk_oid_to_external_data(struct varlena *attr,
 										toast_external_data *data);
 static struct varlena *ondisk_oid_create_external_data(toast_external_data data);
 
+/*
+ * Decompressed size of an on-disk varlena; but note argument is a struct
+ * varatt_external_oid.
+ */
+static inline Size
+varatt_external_oid_get_extsize(varatt_external_oid toast_pointer)
+{
+	return toast_pointer.va_extinfo & VARLENA_EXTSIZE_MASK;
+}
+
+/*
+ * Compression method of an on-disk varlena; but note argument is a struct
+ *  varatt_external_oid.
+ */
+static inline uint32
+varatt_external_oid_get_compress_method(varatt_external_oid toast_pointer)
+{
+	return toast_pointer.va_extinfo >> VARLENA_EXTSIZE_BITS;
+}
+
+/*
+ * Testing whether an externally-stored TOAST value is compressed now requires
+ * comparing size stored in va_extinfo (the actual length of the external data)
+ * to rawsize (the original uncompressed datum's size).  The latter includes
+ * VARHDRSZ overhead, the former doesn't.  We never use compression unless it
+ * actually saves space, so we expect either equality or less-than.
+ */
+static inline bool
+varatt_external_oid_is_compressed(varatt_external_oid toast_pointer)
+{
+	return varatt_external_oid_get_extsize(toast_pointer) <
+		(Size) (toast_pointer.va_rawsize - VARHDRSZ);
+}
+
 /*
  * Size of an EXTERNAL datum that contains a standard TOAST pointer (OID
  * value).
@@ -141,10 +175,10 @@ ondisk_oid_to_external_data(struct varlena *attr, toast_external_data *data)
 	 * External size and compression methods are stored in the same field,
 	 * extract.
 	 */
-	if (VARATT_EXTERNAL_IS_COMPRESSED(external))
+	if (varatt_external_oid_is_compressed(external))
 	{
-		data->extsize = VARATT_EXTERNAL_GET_EXTSIZE(external);
-		data->compression_method = VARATT_EXTERNAL_GET_COMPRESS_METHOD(external);
+		data->extsize = varatt_external_oid_get_extsize(external);
+		data->compression_method = varatt_external_oid_get_compress_method(external);
 	}
 	else
 	{
-- 
2.51.0

v6-0007-Split-VARATT_EXTERNAL_GET_POINTER-for-indirect-an.patchtext/x-diff; charset=us-asciiDownload
From a115be1e1ee95662b18da71ed1bff15091e844dc Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 13 Aug 2025 19:38:50 +0900
Subject: [PATCH v6 07/15] Split VARATT_EXTERNAL_GET_POINTER for indirect and
 OID TOAST pointers

VARATT_EXTERNAL_GET_POINTER() is renamed to
VARATT_INDIRECT_GET_POINTER() with the external on-disk TOAST pointers
for OID values being now located within toast_external.c, splitting both
concepts completely.
---
 src/include/access/detoast.h               | 16 ++++++++--------
 src/backend/access/common/detoast.c        | 10 +++++-----
 src/backend/access/common/toast_external.c | 21 ++++++++++++++++++++-
 src/backend/utils/adt/expandeddatum.c      |  2 +-
 4 files changed, 34 insertions(+), 15 deletions(-)

diff --git a/src/include/access/detoast.h b/src/include/access/detoast.h
index b3ebad8b7cf9..31e9786848ef 100644
--- a/src/include/access/detoast.h
+++ b/src/include/access/detoast.h
@@ -13,17 +13,17 @@
 #define DETOAST_H
 
 /*
- * Macro to fetch the possibly-unaligned contents of an EXTERNAL datum
- * into a local "varatt_external_*" toast pointer, as supported
- * in toast_external.c and varatt.h.  This should be just a memcpy, but
- * some versions of gcc seem to produce broken code that assumes the datum
- * contents are aligned.  Introducing an explicit intermediate
- * "varattrib_1b_e *" variable seems to fix it.
+ * Macro to fetch the possibly-unaligned contents of an indirect datum
+ * into a local "varatt_indirect" toast pointer, as supported
+ * in varatt.h.  This should be just a memcpy, but some versions of gcc
+ * seem to produce broken code that assumes the datum contents are aligned.
+ * Introducing an explicit intermediate "varattrib_1b_e *" variable seems
+ * to fix it.
  */
-#define VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr) \
+#define VARATT_INDIRECT_GET_POINTER(toast_pointer, attr) \
 do { \
 	varattrib_1b_e *attre = (varattrib_1b_e *) (attr); \
-	Assert(VARATT_IS_EXTERNAL(attre)); \
+	Assert(VARATT_IS_EXTERNAL_INDIRECT(attre)); \
 	Assert(VARSIZE_EXTERNAL(attre) == sizeof(toast_pointer) + VARHDRSZ_EXTERNAL); \
 	memcpy(&(toast_pointer), VARDATA_EXTERNAL(attre), sizeof(toast_pointer)); \
 } while (0)
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 8531c27439e4..b645988667f0 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -61,7 +61,7 @@ detoast_external_attr(struct varlena *attr)
 		 */
 		struct varatt_indirect redirect;
 
-		VARATT_EXTERNAL_GET_POINTER(redirect, attr);
+		VARATT_INDIRECT_GET_POINTER(redirect, attr);
 		attr = (struct varlena *) redirect.pointer;
 
 		/* nested indirect Datums aren't allowed */
@@ -138,7 +138,7 @@ detoast_attr(struct varlena *attr)
 		 */
 		struct varatt_indirect redirect;
 
-		VARATT_EXTERNAL_GET_POINTER(redirect, attr);
+		VARATT_INDIRECT_GET_POINTER(redirect, attr);
 		attr = (struct varlena *) redirect.pointer;
 
 		/* nested indirect Datums aren't allowed */
@@ -268,7 +268,7 @@ detoast_attr_slice(struct varlena *attr,
 	{
 		struct varatt_indirect redirect;
 
-		VARATT_EXTERNAL_GET_POINTER(redirect, attr);
+		VARATT_INDIRECT_GET_POINTER(redirect, attr);
 
 		/* nested indirect Datums aren't allowed */
 		Assert(!VARATT_IS_EXTERNAL_INDIRECT(redirect.pointer));
@@ -561,7 +561,7 @@ toast_raw_datum_size(Datum value)
 	{
 		struct varatt_indirect toast_pointer;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		VARATT_INDIRECT_GET_POINTER(toast_pointer, attr);
 
 		/* nested indirect Datums aren't allowed */
 		Assert(!VARATT_IS_EXTERNAL_INDIRECT(toast_pointer.pointer));
@@ -618,7 +618,7 @@ toast_datum_size(Datum value)
 	{
 		struct varatt_indirect toast_pointer;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		VARATT_INDIRECT_GET_POINTER(toast_pointer, attr);
 
 		/* nested indirect Datums aren't allowed */
 		Assert(!VARATT_IS_EXTERNAL_INDIRECT(attr));
diff --git a/src/backend/access/common/toast_external.c b/src/backend/access/common/toast_external.c
index d7bf2f7c69b2..8f58195051cf 100644
--- a/src/backend/access/common/toast_external.c
+++ b/src/backend/access/common/toast_external.c
@@ -26,6 +26,25 @@ static void ondisk_oid_to_external_data(struct varlena *attr,
 										toast_external_data *data);
 static struct varlena *ondisk_oid_create_external_data(toast_external_data data);
 
+/*
+ * Fetch the possibly-unaligned contents of an on-disk external TOAST with
+ * OID values into a local "varatt_external_oid" pointer.
+ *
+ * This should be just a memcpy, but some versions of gcc seem to produce
+ * broken code that assumes the datum contents are aligned.  Introducing
+ * an explicit intermediate "varattrib_1b_e *" variable seems to fix it.
+ */
+static inline void
+varatt_external_oid_get_pointer(varatt_external_oid *toast_pointer,
+								struct varlena *attr)
+{
+	varattrib_1b_e *attre = (varattrib_1b_e *) attr;
+
+	Assert(VARATT_IS_EXTERNAL_ONDISK_OID(attre));
+	Assert(VARSIZE_EXTERNAL(attre) == sizeof(varatt_external_oid) + VARHDRSZ_EXTERNAL);
+	memcpy(toast_pointer, VARDATA_EXTERNAL(attre), sizeof(varatt_external_oid));
+}
+
 /*
  * Decompressed size of an on-disk varlena; but note argument is a struct
  * varatt_external_oid.
@@ -168,7 +187,7 @@ ondisk_oid_to_external_data(struct varlena *attr, toast_external_data *data)
 {
 	varatt_external_oid external;
 
-	VARATT_EXTERNAL_GET_POINTER(external, attr);
+	varatt_external_oid_get_pointer(&external, attr);
 	data->rawsize = external.va_rawsize;
 
 	/*
diff --git a/src/backend/utils/adt/expandeddatum.c b/src/backend/utils/adt/expandeddatum.c
index 6b4b8eaf005c..4c04671d23ed 100644
--- a/src/backend/utils/adt/expandeddatum.c
+++ b/src/backend/utils/adt/expandeddatum.c
@@ -23,7 +23,7 @@
  * Given a Datum that is an expanded-object reference, extract the pointer.
  *
  * This is a bit tedious since the pointer may not be properly aligned;
- * compare VARATT_EXTERNAL_GET_POINTER().
+ * compare VARATT_INDIRECT_GET_POINTER().
  */
 ExpandedObjectHeader *
 DatumGetEOHP(Datum d)
-- 
2.51.0

v6-0008-Switch-pg_column_toast_chunk_id-return-value-from.patchtext/x-diff; charset=us-asciiDownload
From 9d8279649701b2ad9c377a99eb18d0d324353161 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 13 Aug 2025 19:55:39 +0900
Subject: [PATCH v6 08/15] Switch pg_column_toast_chunk_id() return value from
 oid to oid8

This is required for a follow-up patch that will add support for 8-byte
TOAST values, with this function being changed so as it is able to
support the largest TOAST value type available.

XXX: Bump catalog version.
---
 src/include/catalog/pg_proc.dat              | 2 +-
 src/backend/utils/adt/varlena.c              | 2 +-
 src/test/regress/expected/misc_functions.out | 2 +-
 src/test/regress/sql/misc_functions.sql      | 2 +-
 doc/src/sgml/func/func-admin.sgml            | 2 +-
 5 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 67fbb085024c..34d338782bc8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7756,7 +7756,7 @@
   proargtypes => 'any', prosrc => 'pg_column_compression' },
 { oid => '6316', descr => 'chunk ID of on-disk TOASTed value',
   proname => 'pg_column_toast_chunk_id', provolatile => 's',
-  prorettype => 'oid', proargtypes => 'any',
+  prorettype => 'oid8', proargtypes => 'any',
   prosrc => 'pg_column_toast_chunk_id' },
 { oid => '2322',
   descr => 'total disk space usage for the specified tablespace',
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 78b3e65b2396..a176a4fab0e5 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -4241,7 +4241,7 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 
 	toast_valueid = toast_external_info_get_valueid(attr);
 
-	PG_RETURN_OID(toast_valueid);
+	PG_RETURN_OID8(toast_valueid);
 }
 
 /*
diff --git a/src/test/regress/expected/misc_functions.out b/src/test/regress/expected/misc_functions.out
index c3b2b9d86034..9fa9e3466761 100644
--- a/src/test/regress/expected/misc_functions.out
+++ b/src/test/regress/expected/misc_functions.out
@@ -881,7 +881,7 @@ SELECT t.relname AS toastrel FROM pg_class c
   WHERE c.relname = 'test_chunk_id'
 \gset
 SELECT pg_column_toast_chunk_id(a) IS NULL,
-  pg_column_toast_chunk_id(b) IN (SELECT chunk_id FROM pg_toast.:toastrel)
+  pg_column_toast_chunk_id(b) IN (SELECT chunk_id::oid8 FROM pg_toast.:toastrel)
   FROM test_chunk_id;
  ?column? | ?column? 
 ----------+----------
diff --git a/src/test/regress/sql/misc_functions.sql b/src/test/regress/sql/misc_functions.sql
index 23792c4132a1..5fb79f315e37 100644
--- a/src/test/regress/sql/misc_functions.sql
+++ b/src/test/regress/sql/misc_functions.sql
@@ -395,7 +395,7 @@ SELECT t.relname AS toastrel FROM pg_class c
   WHERE c.relname = 'test_chunk_id'
 \gset
 SELECT pg_column_toast_chunk_id(a) IS NULL,
-  pg_column_toast_chunk_id(b) IN (SELECT chunk_id FROM pg_toast.:toastrel)
+  pg_column_toast_chunk_id(b) IN (SELECT chunk_id::oid8 FROM pg_toast.:toastrel)
   FROM test_chunk_id;
 DROP TABLE test_chunk_id;
 DROP FUNCTION explain_mask_costs(text, bool, bool, bool, bool);
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 57ff333159f0..6c0bf33ad03a 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1571,7 +1571,7 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
          <primary>pg_column_toast_chunk_id</primary>
         </indexterm>
         <function>pg_column_toast_chunk_id</function> ( <type>"any"</type> )
-        <returnvalue>oid</returnvalue>
+        <returnvalue>oid8</returnvalue>
        </para>
        <para>
         Shows the <structfield>chunk_id</structfield> of an on-disk
-- 
2.51.0

v6-0009-Add-catcache-support-for-OID8OID.patchtext/x-diff; charset=us-asciiDownload
From 57ee6b2cebb6bf7f928a15b71ee79ec65238481c Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 13 Aug 2025 20:00:30 +0900
Subject: [PATCH v6 09/15] Add catcache support for OID8OID

This is required to be able to do catalog cache lookups of oid8 fields
for toast values of the same type.
---
 src/backend/utils/cache/catcache.c | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index e2cd3feaf81d..fee2e67c7830 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -240,6 +240,18 @@ int4hashfast(Datum datum)
 	return murmurhash32((int32) DatumGetInt32(datum));
 }
 
+static bool
+oid8eqfast(Datum a, Datum b)
+{
+	return DatumGetObjectId8(a) == DatumGetObjectId8(b);
+}
+
+static uint32
+oid8hashfast(Datum datum)
+{
+	return murmurhash64(DatumGetObjectId8(datum));
+}
+
 static bool
 texteqfast(Datum a, Datum b)
 {
@@ -300,6 +312,11 @@ GetCCHashEqFuncs(Oid keytype, CCHashFN *hashfunc, RegProcedure *eqfunc, CCFastEq
 			*fasteqfunc = int4eqfast;
 			*eqfunc = F_INT4EQ;
 			break;
+		case OID8OID:
+			*hashfunc = oid8hashfast;
+			*fasteqfunc = oid8eqfast;
+			*eqfunc = F_OID8EQ;
+			break;
 		case TEXTOID:
 			*hashfunc = texthashfast;
 			*fasteqfunc = texteqfast;
-- 
2.51.0

v6-0010-Add-support-for-TOAST-chunk_id-type-in-binary-upg.patchtext/x-diff; charset=us-asciiDownload
From 4f545358aac0e79e6fe8b1090f76b0b828b0fcd9 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 10:57:59 +0900
Subject: [PATCH v6 10/15] Add support for TOAST chunk_id type in binary
 upgrades

This commit adds a new function, which would set the type of a chunk_id
attribute for a TOAST table across upgrades.  This piece currently works
only with chunk_id = OIDOID, but it is required in a follow-up patch
where support for chunk_id = OID8OID is supported on top of the existing
one.
---
 src/include/catalog/binary_upgrade.h          |  1 +
 src/include/catalog/pg_proc.dat               |  4 ++++
 src/backend/catalog/heap.c                    |  1 +
 src/backend/catalog/toasting.c                | 20 ++++++++++++++++++-
 src/backend/utils/adt/pg_upgrade_support.c    | 11 ++++++++++
 src/bin/pg_dump/pg_dump.c                     | 10 +++++++++-
 .../expected/spgist_name_ops.out              |  6 ++++--
 7 files changed, 49 insertions(+), 4 deletions(-)

diff --git a/src/include/catalog/binary_upgrade.h b/src/include/catalog/binary_upgrade.h
index 6fcc59edebd8..3deb0423d795 100644
--- a/src/include/catalog/binary_upgrade.h
+++ b/src/include/catalog/binary_upgrade.h
@@ -29,6 +29,7 @@ extern PGDLLIMPORT Oid binary_upgrade_next_index_pg_class_oid;
 extern PGDLLIMPORT RelFileNumber binary_upgrade_next_index_pg_class_relfilenumber;
 extern PGDLLIMPORT Oid binary_upgrade_next_toast_pg_class_oid;
 extern PGDLLIMPORT RelFileNumber binary_upgrade_next_toast_pg_class_relfilenumber;
+extern PGDLLIMPORT Oid binary_upgrade_next_toast_chunk_id_typoid;
 
 extern PGDLLIMPORT Oid binary_upgrade_next_pg_enum_oid;
 extern PGDLLIMPORT Oid binary_upgrade_next_pg_authid_oid;
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 34d338782bc8..bc63d789fd1b 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11771,6 +11771,10 @@
   proname => 'binary_upgrade_set_next_toast_pg_class_oid', provolatile => 'v',
   proparallel => 'r', prorettype => 'void', proargtypes => 'oid',
   prosrc => 'binary_upgrade_set_next_toast_pg_class_oid' },
+{ oid => '8219', descr => 'for use by pg_upgrade',
+  proname => 'binary_upgrade_set_next_toast_chunk_id_typoid', provolatile => 'v',
+  proparallel => 'r', prorettype => 'void', proargtypes => 'oid',
+  prosrc => 'binary_upgrade_set_next_toast_chunk_id_typoid' },
 { oid => '3589', descr => 'for use by pg_upgrade',
   proname => 'binary_upgrade_set_next_pg_enum_oid', provolatile => 'v',
   proparallel => 'r', prorettype => 'void', proargtypes => 'oid',
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index fd6537567ea2..e5cc98937055 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -80,6 +80,7 @@
 /* Potentially set by pg_upgrade_support functions */
 Oid			binary_upgrade_next_heap_pg_class_oid = InvalidOid;
 Oid			binary_upgrade_next_toast_pg_class_oid = InvalidOid;
+Oid			binary_upgrade_next_toast_chunk_id_typoid = InvalidOid;
 RelFileNumber binary_upgrade_next_heap_pg_class_relfilenumber = InvalidRelFileNumber;
 RelFileNumber binary_upgrade_next_toast_pg_class_relfilenumber = InvalidRelFileNumber;
 
diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c
index 874a8fc89adb..f1d76d8acd51 100644
--- a/src/backend/catalog/toasting.c
+++ b/src/backend/catalog/toasting.c
@@ -145,6 +145,7 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	int16		coloptions[2];
 	ObjectAddress baseobject,
 				toastobject;
+	Oid			toast_chunkid_typid = OIDOID;
 
 	/*
 	 * Is it already toasted?
@@ -183,6 +184,23 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 		 */
 		if (!OidIsValid(binary_upgrade_next_toast_pg_class_oid))
 			return false;
+
+		/*
+		 * The attribute type for chunk_id should have been set when requesting
+		 * a TOAST table creation.
+		 */
+		if (!OidIsValid(binary_upgrade_next_toast_chunk_id_typoid))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("toast chunk_id type not set while in binary upgrade mode")));
+		if (binary_upgrade_next_toast_chunk_id_typoid != OIDOID)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("cannot support toast chunk_id type %u in binary upgrade mode",
+							binary_upgrade_next_toast_chunk_id_typoid)));
+
+		toast_chunkid_typid = binary_upgrade_next_toast_chunk_id_typoid;
+		binary_upgrade_next_toast_chunk_id_typoid = InvalidOid;
 	}
 
 	/*
@@ -204,7 +222,7 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	tupdesc = CreateTemplateTupleDesc(3);
 	TupleDescInitEntry(tupdesc, (AttrNumber) 1,
 					   "chunk_id",
-					   OIDOID,
+					   toast_chunkid_typid,
 					   -1, 0);
 	TupleDescInitEntry(tupdesc, (AttrNumber) 2,
 					   "chunk_seq",
diff --git a/src/backend/utils/adt/pg_upgrade_support.c b/src/backend/utils/adt/pg_upgrade_support.c
index a4f8b4faa90d..200ffcdbab44 100644
--- a/src/backend/utils/adt/pg_upgrade_support.c
+++ b/src/backend/utils/adt/pg_upgrade_support.c
@@ -149,6 +149,17 @@ binary_upgrade_set_next_toast_pg_class_oid(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+Datum
+binary_upgrade_set_next_toast_chunk_id_typoid(PG_FUNCTION_ARGS)
+{
+	Oid			typoid = PG_GETARG_OID(0);
+
+	CHECK_IS_BINARY_UPGRADE;
+	binary_upgrade_next_toast_chunk_id_typoid = typoid;
+
+	PG_RETURN_VOID();
+}
+
 Datum
 binary_upgrade_set_next_toast_relfilenode(PG_FUNCTION_ARGS)
 {
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index b4c45ad803e9..eead7b6bca91 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -103,6 +103,7 @@ typedef struct
 	RelFileNumber relfilenumber;	/* object filenode */
 	Oid			toast_oid;		/* toast table OID */
 	RelFileNumber toast_relfilenumber;	/* toast table filenode */
+	Oid			toast_chunk_id_typoid;	/* type of chunk_id attribute */
 	Oid			toast_index_oid;	/* toast table index OID */
 	RelFileNumber toast_index_relfilenumber;	/* toast table index filenode */
 } BinaryUpgradeClassOidItem;
@@ -5799,7 +5800,10 @@ collectBinaryUpgradeClassOids(Archive *fout)
 	const char *query;
 
 	query = "SELECT c.oid, c.relkind, c.relfilenode, c.reltoastrelid, "
-		"ct.relfilenode, i.indexrelid, cti.relfilenode "
+		"ct.relfilenode, i.indexrelid, cti.relfilenode, "
+		"(SELECT a.atttypid FROM pg_attribute AS a "
+		"  WHERE a.attrelid = c.reltoastrelid AND attname = 'chunk_id'::text) "
+		"  AS toastchunktypid "
 		"FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_index i "
 		"ON (c.reltoastrelid = i.indrelid AND i.indisvalid) "
 		"LEFT JOIN pg_catalog.pg_class ct ON (c.reltoastrelid = ct.oid) "
@@ -5821,6 +5825,7 @@ collectBinaryUpgradeClassOids(Archive *fout)
 		binaryUpgradeClassOids[i].toast_relfilenumber = atooid(PQgetvalue(res, i, 4));
 		binaryUpgradeClassOids[i].toast_index_oid = atooid(PQgetvalue(res, i, 5));
 		binaryUpgradeClassOids[i].toast_index_relfilenumber = atooid(PQgetvalue(res, i, 6));
+		binaryUpgradeClassOids[i].toast_chunk_id_typoid = atooid(PQgetvalue(res, i, 7));
 	}
 
 	PQclear(res);
@@ -5885,6 +5890,9 @@ binary_upgrade_set_pg_class_oids(Archive *fout,
 			appendPQExpBuffer(upgrade_buffer,
 							  "SELECT pg_catalog.binary_upgrade_set_next_toast_relfilenode('%u'::pg_catalog.oid);\n",
 							  entry->toast_relfilenumber);
+			appendPQExpBuffer(upgrade_buffer,
+							  "SELECT pg_catalog.binary_upgrade_set_next_toast_chunk_id_typoid('%u'::pg_catalog.oid);\n",
+							  entry->toast_chunk_id_typoid);
 
 			/* every toast table has an index */
 			appendPQExpBuffer(upgrade_buffer,
diff --git a/src/test/modules/spgist_name_ops/expected/spgist_name_ops.out b/src/test/modules/spgist_name_ops/expected/spgist_name_ops.out
index 1ee65ede2430..35e59d0cd83c 100644
--- a/src/test/modules/spgist_name_ops/expected/spgist_name_ops.out
+++ b/src/test/modules/spgist_name_ops/expected/spgist_name_ops.out
@@ -61,9 +61,10 @@ select * from t
  binary_upgrade_set_next_pg_enum_oid                  |    | binary_upgrade_set_next_pg_enum_oid
  binary_upgrade_set_next_pg_tablespace_oid            |    | binary_upgrade_set_next_pg_tablespace_oid
  binary_upgrade_set_next_pg_type_oid                  |    | binary_upgrade_set_next_pg_type_oid
+ binary_upgrade_set_next_toast_chunk_id_typoid        |    | binary_upgrade_set_next_toast_chunk_id_typoid
  binary_upgrade_set_next_toast_pg_class_oid           |  1 | binary_upgrade_set_next_toast_pg_class_oid
  binary_upgrade_set_next_toast_relfilenode            |    | binary_upgrade_set_next_toast_relfilenode
-(13 rows)
+(14 rows)
 
 -- Verify clean failure when INCLUDE'd columns result in overlength tuple
 -- The error message details are platform-dependent, so show only SQLSTATE
@@ -110,9 +111,10 @@ select * from t
  binary_upgrade_set_next_pg_enum_oid                  |    | binary_upgrade_set_next_pg_enum_oid
  binary_upgrade_set_next_pg_tablespace_oid            |    | binary_upgrade_set_next_pg_tablespace_oid
  binary_upgrade_set_next_pg_type_oid                  |    | binary_upgrade_set_next_pg_type_oid
+ binary_upgrade_set_next_toast_chunk_id_typoid        |    | binary_upgrade_set_next_toast_chunk_id_typoid
  binary_upgrade_set_next_toast_pg_class_oid           |  1 | binary_upgrade_set_next_toast_pg_class_oid
  binary_upgrade_set_next_toast_relfilenode            |    | binary_upgrade_set_next_toast_relfilenode
-(13 rows)
+(14 rows)
 
 \set VERBOSITY sqlstate
 insert into t values(repeat('xyzzy', 12), 42, repeat('xyzzy', 4000));
-- 
2.51.0

v6-0011-Enlarge-OID-generation-to-8-bytes.patchtext/x-diff; charset=us-asciiDownload
From 704484b94e6e3ae44ce89799e8b64bb2cf8f2a07 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 12:15:15 +0900
Subject: [PATCH v6 11/15] Enlarge OID generation to 8 bytes

This adds a new routine called GetNewObjectId8() in varsup.c, which is
able to retrieve a 64b OID.  GetNewObjectId() is kept compatible with
its origin, where we still check that the lower 32 bits of the counter
do not wraparound, handling the FirstNormalObjectId case.

pg_resetwal -o/--next-oid is updated to be able to handle 8-byte OIDs.
---
 src/include/access/transam.h              |  3 +-
 src/include/access/xlog.h                 |  2 +-
 src/include/catalog/pg_control.h          |  2 +-
 src/backend/access/rmgrdesc/xlogdesc.c    |  8 +--
 src/backend/access/transam/varsup.c       | 62 ++++++++++++++++-------
 src/backend/access/transam/xlog.c         |  8 +--
 src/backend/access/transam/xlogrecovery.c |  2 +-
 src/bin/pg_controldata/pg_controldata.c   |  2 +-
 src/bin/pg_resetwal/pg_resetwal.c         | 10 ++--
 doc/src/sgml/ref/pg_resetwal.sgml         |  6 +--
 10 files changed, 66 insertions(+), 39 deletions(-)

diff --git a/src/include/access/transam.h b/src/include/access/transam.h
index 7d82cd2eb562..2ef3000bdb1f 100644
--- a/src/include/access/transam.h
+++ b/src/include/access/transam.h
@@ -211,7 +211,7 @@ typedef struct TransamVariablesData
 	/*
 	 * These fields are protected by OidGenLock.
 	 */
-	Oid			nextOid;		/* next OID to assign */
+	Oid8		nextOid;		/* next OID (8 bytes) to assign */
 	uint32		oidCount;		/* OIDs available before must do XLOG work */
 
 	/*
@@ -293,6 +293,7 @@ extern void SetTransactionIdLimit(TransactionId oldest_datfrozenxid,
 extern void AdvanceOldestClogXid(TransactionId oldest_datfrozenxid);
 extern bool ForceTransactionIdLimitUpdate(void);
 extern Oid	GetNewObjectId(void);
+extern Oid8 GetNewObjectId8(void);
 extern void StopGeneratingPinnedObjectIds(void);
 
 #ifdef USE_ASSERT_CHECKING
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index d12798be3d80..21d915ae5802 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -243,7 +243,7 @@ extern void ShutdownXLOG(int code, Datum arg);
 extern bool CreateCheckPoint(int flags);
 extern bool CreateRestartPoint(int flags);
 extern WALAvailability GetWALAvailability(XLogRecPtr targetLSN);
-extern void XLogPutNextOid(Oid nextOid);
+extern void XLogPutNextOid(Oid8 nextOid);
 extern XLogRecPtr XLogRestorePoint(const char *rpName);
 extern void UpdateFullPageWrites(void);
 extern void GetFullPageWriteInfo(XLogRecPtr *RedoRecPtr_p, bool *doPageWrites_p);
diff --git a/src/include/catalog/pg_control.h b/src/include/catalog/pg_control.h
index 63e834a6ce47..c85c84bbfdbb 100644
--- a/src/include/catalog/pg_control.h
+++ b/src/include/catalog/pg_control.h
@@ -42,7 +42,7 @@ typedef struct CheckPoint
 	bool		fullPageWrites; /* current full_page_writes */
 	int			wal_level;		/* current wal_level */
 	FullTransactionId nextXid;	/* next free transaction ID */
-	Oid			nextOid;		/* next free OID */
+	Oid8		nextOid;		/* next free OID */
 	MultiXactId nextMulti;		/* next free MultiXactId */
 	MultiXactOffset nextMultiOffset;	/* next free MultiXact offset */
 	TransactionId oldestXid;	/* cluster-wide minimum datfrozenxid */
diff --git a/src/backend/access/rmgrdesc/xlogdesc.c b/src/backend/access/rmgrdesc/xlogdesc.c
index cd6c2a2f650a..23920d32092a 100644
--- a/src/backend/access/rmgrdesc/xlogdesc.c
+++ b/src/backend/access/rmgrdesc/xlogdesc.c
@@ -66,7 +66,7 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
 		CheckPoint *checkpoint = (CheckPoint *) rec;
 
 		appendStringInfo(buf, "redo %X/%08X; "
-						 "tli %u; prev tli %u; fpw %s; wal_level %s; xid %u:%u; oid %u; multi %u; offset %u; "
+						 "tli %u; prev tli %u; fpw %s; wal_level %s; xid %u:%u; oid " OID8_FORMAT "; multi %u; offset %u; "
 						 "oldest xid %u in DB %u; oldest multi %u in DB %u; "
 						 "oldest/newest commit timestamp xid: %u/%u; "
 						 "oldest running xid %u; %s",
@@ -91,10 +91,10 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
 	}
 	else if (info == XLOG_NEXTOID)
 	{
-		Oid			nextOid;
+		Oid8		nextOid;
 
-		memcpy(&nextOid, rec, sizeof(Oid));
-		appendStringInfo(buf, "%u", nextOid);
+		memcpy(&nextOid, rec, sizeof(Oid8));
+		appendStringInfo(buf, OID8_FORMAT, nextOid);
 	}
 	else if (info == XLOG_RESTORE_POINT)
 	{
diff --git a/src/backend/access/transam/varsup.c b/src/backend/access/transam/varsup.c
index f8c4dada7c93..662d1dcaed16 100644
--- a/src/backend/access/transam/varsup.c
+++ b/src/backend/access/transam/varsup.c
@@ -542,31 +542,51 @@ ForceTransactionIdLimitUpdate(void)
 
 
 /*
- * GetNewObjectId -- allocate a new OID
+ * GetNewObjectId -- allocate a new OID (4 bytes)
  *
- * OIDs are generated by a cluster-wide counter.  Since they are only 32 bits
- * wide, counter wraparound will occur eventually, and therefore it is unwise
- * to assume they are unique unless precautions are taken to make them so.
- * Hence, this routine should generally not be used directly.  The only direct
- * callers should be GetNewOidWithIndex() and GetNewRelFileNumber() in
- * catalog/catalog.c.
+ * OIDs are generated by a cluster-wide counter.  The callers of this routine
+ * expect a 32 bit-wide counter, and counter wraparound will occur eventually,
+ * and therefore it is unwise to assume they are unique unless precautions are
+ * taken to make them so.  This routine should generally not be used directly.
+ * The only direct callers should be GetNewOidWithIndex() and
+ * GetNewRelFileNumber() in catalog/catalog.c.
  */
 Oid
 GetNewObjectId(void)
 {
-	Oid			result;
+	return (Oid) GetNewObjectId8();
+}
+
+/*
+ * GetNewObjectId8 -- allocate a new OID (8 bytes)
+ *
+ * This routine can be called directly if the consumer of the OID allocated
+ * stores the counter in an 8-byte space, where wraparound does not matter.
+ * We still need to care about the wraparound case in the low 32 bits of the
+ * space allocated, GetNewObjectId() expecting OIDs to never be allocated
+ * up to FirstNormalObjectId.
+ */
+Oid8
+GetNewObjectId8(void)
+{
+	Oid8		result;
+	Oid			nextoid_lo;
+	uint32		nextoid_hi;
 
 	/* safety check, we should never get this far in a HS standby */
 	if (RecoveryInProgress())
 		elog(ERROR, "cannot assign OIDs during recovery");
 
 	LWLockAcquire(OidGenLock, LW_EXCLUSIVE);
+	nextoid_lo = (Oid) TransamVariables->nextOid;
+	nextoid_hi = (uint32) (TransamVariables->nextOid >> 32);
 
 	/*
-	 * Check for wraparound of the OID counter.  We *must* not return 0
-	 * (InvalidOid), and in normal operation we mustn't return anything below
-	 * FirstNormalObjectId since that range is reserved for initdb (see
-	 * IsCatalogRelationOid()).  Note we are relying on unsigned comparison.
+	 * Check for wraparound of the OID counter in its lower 4 bytes.  We
+	 * *must* not return 0 (InvalidOid), and in normal operation we
+	 * mustn't return anything below FirstNormalObjectId since that range
+	 * is reserved for initdb (see IsCatalogRelationOid()).  Note we are
+	 * relying on unsigned comparison.
 	 *
 	 * During initdb, we start the OID generator at FirstGenbkiObjectId, so we
 	 * only wrap if before that point when in bootstrap or standalone mode.
@@ -576,26 +596,32 @@ GetNewObjectId(void)
 	 * available for automatic assignment during initdb, while ensuring they
 	 * will never conflict with user-assigned OIDs.
 	 */
-	if (TransamVariables->nextOid < ((Oid) FirstNormalObjectId))
+	if (nextoid_lo < ((Oid) FirstNormalObjectId))
 	{
 		if (IsPostmasterEnvironment)
 		{
 			/* wraparound, or first post-initdb assignment, in normal mode */
-			TransamVariables->nextOid = FirstNormalObjectId;
+			nextoid_lo = FirstNormalObjectId;
 			TransamVariables->oidCount = 0;
 		}
 		else
 		{
 			/* we may be bootstrapping, so don't enforce the full range */
-			if (TransamVariables->nextOid < ((Oid) FirstGenbkiObjectId))
+			if (nextoid_lo < ((Oid) FirstGenbkiObjectId))
 			{
 				/* wraparound in standalone mode (unlikely but possible) */
-				TransamVariables->nextOid = FirstNormalObjectId;
+				nextoid_lo = FirstNormalObjectId;
 				TransamVariables->oidCount = 0;
 			}
 		}
 	}
 
+	/*
+	 * Set next OID in its 8-byte space, skipping the first post-init
+	 * assignment.
+	 */
+	TransamVariables->nextOid = ((Oid8) nextoid_hi) << 32 | nextoid_lo;
+
 	/* If we run out of logged for use oids then we must log more */
 	if (TransamVariables->oidCount == 0)
 	{
@@ -620,7 +646,7 @@ GetNewObjectId(void)
  * to the specified value.
  */
 static void
-SetNextObjectId(Oid nextOid)
+SetNextObjectId(Oid8 nextOid)
 {
 	/* Safety check, this is only allowable during initdb */
 	if (IsPostmasterEnvironment)
@@ -630,7 +656,7 @@ SetNextObjectId(Oid nextOid)
 	LWLockAcquire(OidGenLock, LW_EXCLUSIVE);
 
 	if (TransamVariables->nextOid > nextOid)
-		elog(ERROR, "too late to advance OID counter to %u, it is now %u",
+		elog(ERROR, "too late to advance OID counter to " OID8_FORMAT ", it is now " OID8_FORMAT,
 			 nextOid, TransamVariables->nextOid);
 
 	TransamVariables->nextOid = nextOid;
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 0baf0ac6160a..adfaea15b103 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -8052,10 +8052,10 @@ KeepLogSeg(XLogRecPtr recptr, XLogSegNo *logSegNo)
  * Write a NEXTOID log record
  */
 void
-XLogPutNextOid(Oid nextOid)
+XLogPutNextOid(Oid8 nextOid)
 {
 	XLogBeginInsert();
-	XLogRegisterData(&nextOid, sizeof(Oid));
+	XLogRegisterData(&nextOid, sizeof(Oid8));
 	(void) XLogInsert(RM_XLOG_ID, XLOG_NEXTOID);
 
 	/*
@@ -8278,7 +8278,7 @@ xlog_redo(XLogReaderState *record)
 
 	if (info == XLOG_NEXTOID)
 	{
-		Oid			nextOid;
+		Oid8		nextOid;
 
 		/*
 		 * We used to try to take the maximum of TransamVariables->nextOid and
@@ -8287,7 +8287,7 @@ xlog_redo(XLogReaderState *record)
 		 * anyway, better to just believe the record exactly.  We still take
 		 * OidGenLock while setting the variable, just in case.
 		 */
-		memcpy(&nextOid, XLogRecGetData(record), sizeof(Oid));
+		memcpy(&nextOid, XLogRecGetData(record), sizeof(Oid8));
 		LWLockAcquire(OidGenLock, LW_EXCLUSIVE);
 		TransamVariables->nextOid = nextOid;
 		TransamVariables->oidCount = 0;
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index 346319338a0e..f19f6511c778 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -880,7 +880,7 @@ InitWalRecovery(ControlFileData *ControlFile, bool *wasShutdown_ptr,
 							LSN_FORMAT_ARGS(checkPoint.redo),
 							wasShutdown ? "true" : "false"));
 	ereport(DEBUG1,
-			(errmsg_internal("next transaction ID: " UINT64_FORMAT "; next OID: %u",
+			(errmsg_internal("next transaction ID: " UINT64_FORMAT "; next OID: " OID8_FORMAT,
 							 U64FromFullTransactionId(checkPoint.nextXid),
 							 checkPoint.nextOid)));
 	ereport(DEBUG1,
diff --git a/src/bin/pg_controldata/pg_controldata.c b/src/bin/pg_controldata/pg_controldata.c
index 10de058ce91f..992111d3a1d2 100644
--- a/src/bin/pg_controldata/pg_controldata.c
+++ b/src/bin/pg_controldata/pg_controldata.c
@@ -260,7 +260,7 @@ main(int argc, char *argv[])
 	printf(_("Latest checkpoint's NextXID:          %u:%u\n"),
 		   EpochFromFullTransactionId(ControlFile->checkPointCopy.nextXid),
 		   XidFromFullTransactionId(ControlFile->checkPointCopy.nextXid));
-	printf(_("Latest checkpoint's NextOID:          %u\n"),
+	printf(_("Latest checkpoint's NextOID:          " OID8_FORMAT "\n"),
 		   ControlFile->checkPointCopy.nextOid);
 	printf(_("Latest checkpoint's NextMultiXactId:  %u\n"),
 		   ControlFile->checkPointCopy.nextMulti);
diff --git a/src/bin/pg_resetwal/pg_resetwal.c b/src/bin/pg_resetwal/pg_resetwal.c
index 7a4e4eb95706..c1039a8a4d16 100644
--- a/src/bin/pg_resetwal/pg_resetwal.c
+++ b/src/bin/pg_resetwal/pg_resetwal.c
@@ -68,7 +68,7 @@ static TransactionId set_oldest_xid = 0;
 static TransactionId set_xid = 0;
 static TransactionId set_oldest_commit_ts_xid = 0;
 static TransactionId set_newest_commit_ts_xid = 0;
-static Oid	set_oid = 0;
+static Oid8 set_oid = 0;
 static MultiXactId set_mxid = 0;
 static MultiXactOffset set_mxoff = (MultiXactOffset) -1;
 static TimeLineID minXlogTli = 0;
@@ -225,7 +225,7 @@ main(int argc, char *argv[])
 
 			case 'o':
 				errno = 0;
-				set_oid = strtoul(optarg, &endptr, 0);
+				set_oid = strtou64(optarg, &endptr, 0);
 				if (endptr == optarg || *endptr != '\0' || errno != 0)
 				{
 					pg_log_error("invalid argument for option %s", "-o");
@@ -755,7 +755,7 @@ PrintControlValues(bool guessed)
 	printf(_("Latest checkpoint's NextXID:          %u:%u\n"),
 		   EpochFromFullTransactionId(ControlFile.checkPointCopy.nextXid),
 		   XidFromFullTransactionId(ControlFile.checkPointCopy.nextXid));
-	printf(_("Latest checkpoint's NextOID:          %u\n"),
+	printf(_("Latest checkpoint's NextOID:          " OID8_FORMAT "\n"),
 		   ControlFile.checkPointCopy.nextOid);
 	printf(_("Latest checkpoint's NextMultiXactId:  %u\n"),
 		   ControlFile.checkPointCopy.nextMulti);
@@ -839,7 +839,7 @@ PrintNewControlValues(void)
 
 	if (set_oid != 0)
 	{
-		printf(_("NextOID:                              %u\n"),
+		printf(_("NextOID:                              " OID8_FORMAT "\n"),
 			   ControlFile.checkPointCopy.nextOid);
 	}
 
@@ -1208,7 +1208,7 @@ usage(void)
 	printf(_("  -e, --epoch=XIDEPOCH             set next transaction ID epoch\n"));
 	printf(_("  -l, --next-wal-file=WALFILE      set minimum starting location for new WAL\n"));
 	printf(_("  -m, --multixact-ids=MXID,MXID    set next and oldest multitransaction ID\n"));
-	printf(_("  -o, --next-oid=OID               set next OID\n"));
+	printf(_("  -o, --next-oid=OID8              set next OID (8 bytes)\n"));
 	printf(_("  -O, --multixact-offset=OFFSET    set next multitransaction offset\n"));
 	printf(_("  -u, --oldest-transaction-id=XID  set oldest transaction ID\n"));
 	printf(_("  -x, --next-transaction-id=XID    set next transaction ID\n"));
diff --git a/doc/src/sgml/ref/pg_resetwal.sgml b/doc/src/sgml/ref/pg_resetwal.sgml
index 2c019c2aac6e..b03251cedbbe 100644
--- a/doc/src/sgml/ref/pg_resetwal.sgml
+++ b/doc/src/sgml/ref/pg_resetwal.sgml
@@ -279,11 +279,11 @@ PostgreSQL documentation
    </varlistentry>
 
    <varlistentry>
-    <term><option>-o <replaceable class="parameter">oid</replaceable></option></term>
-    <term><option>--next-oid=<replaceable class="parameter">oid</replaceable></option></term>
+    <term><option>-o <replaceable class="parameter">oid8</replaceable></option></term>
+    <term><option>--next-oid=<replaceable class="parameter">oid8</replaceable></option></term>
     <listitem>
      <para>
-      Manually set the next OID.
+      Manually set the next OID (8 bytes).
      </para>
 
      <para>
-- 
2.51.0

v6-0012-Add-relation-option-toast_value_type.patchtext/x-diff; charset=us-asciiDownload
From 206ec29d2d2f665a8d067f0be85a6a0cb5861278 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 12:54:58 +0900
Subject: [PATCH v6 12/15] Add relation option toast_value_type

This relation option gives the possibility to define the attribute type
that can be used for chunk_id in a TOAST table when it is created
initially.  This parameter has no effect if a TOAST table exists, even
after it is modified later on, even on rewrites.

This can be set only to "oid" currently, and will be expanded with a
second mode later.

Note: perhaps it would make sense to introduce that only when support
for 8-byte OID values are added to TOAST, the split is here to ease
review.
---
 src/include/utils/rel.h                | 17 +++++++++++++++++
 src/backend/access/common/reloptions.c | 21 +++++++++++++++++++++
 src/backend/catalog/toasting.c         |  6 ++++++
 src/bin/psql/tab-complete.in.c         |  1 +
 doc/src/sgml/ref/create_table.sgml     | 18 ++++++++++++++++++
 5 files changed, 63 insertions(+)

diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index b552359915f1..b846bd42103e 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -337,11 +337,20 @@ typedef enum StdRdOptIndexCleanup
 	STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON,
 } StdRdOptIndexCleanup;
 
+/* StdRdOptions->toast_value_type values */
+typedef enum StdRdOptToastValueType
+{
+	STDRD_OPTION_TOAST_VALUE_TYPE_INVALID = 0,
+	STDRD_OPTION_TOAST_VALUE_TYPE_OID,
+} StdRdOptToastValueType;
+
 typedef struct StdRdOptions
 {
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	int			fillfactor;		/* page fill factor in percent (0..100) */
 	int			toast_tuple_target; /* target for tuple toasting */
+	StdRdOptToastValueType	toast_value_type;	/* type assigned to chunk_id
+												 * at toast table creation */
 	AutoVacOpts autovacuum;		/* autovacuum-related options */
 	bool		user_catalog_table; /* use as an additional catalog relation */
 	int			parallel_workers;	/* max number of parallel workers */
@@ -367,6 +376,14 @@ typedef struct StdRdOptions
 	((relation)->rd_options ? \
 	 ((StdRdOptions *) (relation)->rd_options)->toast_tuple_target : (defaulttarg))
 
+/*
+ * RelationGetToastValueType
+ *		Returns the relation's toast_value_type.  Note multiple eval of argument!
+ */
+#define RelationGetToastValueType(relation, defaulttarg) \
+	((relation)->rd_options ? \
+	 ((StdRdOptions *) (relation)->rd_options)->toast_value_type : defaulttarg)
+
 /*
  * RelationGetFillFactor
  *		Returns the relation's fillfactor.  Note multiple eval of argument!
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 0af3fea68fa4..b0447d9e39bb 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -516,6 +516,14 @@ static relopt_enum_elt_def viewCheckOptValues[] =
 	{(const char *) NULL}		/* list terminator */
 };
 
+/* values from StdRdOptToastValueType */
+static relopt_enum_elt_def StdRdOptToastValueTypes[] =
+{
+	/* no value for INVALID */
+	{"oid", STDRD_OPTION_TOAST_VALUE_TYPE_OID},
+	{(const char *) NULL}		/* list terminator */
+};
+
 static relopt_enum enumRelOpts[] =
 {
 	{
@@ -529,6 +537,17 @@ static relopt_enum enumRelOpts[] =
 		STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO,
 		gettext_noop("Valid values are \"on\", \"off\", and \"auto\".")
 	},
+	{
+		{
+			"toast_value_type",
+			"Controls the attribute type of chunk_id at toast table creation",
+			RELOPT_KIND_HEAP,
+			ShareUpdateExclusiveLock
+		},
+		StdRdOptToastValueTypes,
+		STDRD_OPTION_TOAST_VALUE_TYPE_OID,
+		gettext_noop("Valid values are \"oid\".")
+	},
 	{
 		{
 			"buffering",
@@ -1898,6 +1917,8 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
 		offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_min_duration)},
 		{"toast_tuple_target", RELOPT_TYPE_INT,
 		offsetof(StdRdOptions, toast_tuple_target)},
+		{"toast_value_type", RELOPT_TYPE_ENUM,
+		offsetof(StdRdOptions, toast_value_type)},
 		{"autovacuum_vacuum_cost_delay", RELOPT_TYPE_REAL,
 		offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_delay)},
 		{"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c
index f1d76d8acd51..545983b5be9d 100644
--- a/src/backend/catalog/toasting.c
+++ b/src/backend/catalog/toasting.c
@@ -158,9 +158,15 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	 */
 	if (!IsBinaryUpgrade)
 	{
+		StdRdOptToastValueType value_type;
+
 		/* Normal mode, normal check */
 		if (!needs_toast_table(rel))
 			return false;
+
+		value_type = RelationGetToastValueType(rel, STDRD_OPTION_TOAST_VALUE_TYPE_OID);
+		if (value_type == STDRD_OPTION_TOAST_VALUE_TYPE_OID)
+			toast_chunkid_typid = OIDOID;
 	}
 	else
 	{
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 6b20a4404b21..bd93938d3eb2 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -1431,6 +1431,7 @@ static const char *const table_storage_parameters[] = {
 	"toast.vacuum_max_eager_freeze_failure_rate",
 	"toast.vacuum_truncate",
 	"toast_tuple_target",
+	"toast_value_type",
 	"user_catalog_table",
 	"vacuum_index_cleanup",
 	"vacuum_max_eager_freeze_failure_rate",
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index dc000e913c14..84ad78afa3de 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1632,6 +1632,24 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     </listitem>
    </varlistentry>
 
+   <varlistentry id="reloption-toast-value-type" xreflabel="toast_value_type">
+    <term><literal>toast_value_type</literal> (<type>enum</type>)
+    <indexterm>
+     <primary><varname>toast_value_type</varname> storage parameter</primary>
+    </indexterm>
+    </term>
+    <listitem>
+     <para>
+      The toast_value_type specifies the attribute type of
+      <literal>chunk_id</literal> used when initially creating  a toast
+      relation for this table.
+      By default this parameter is <literal>oid</literal>, to assign
+      <type>oid</type> as attribute type to <literal>chunk_id</literal>.
+      This parameter cannot be set for TOAST tables.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="reloption-parallel-workers" xreflabel="parallel_workers">
     <term><literal>parallel_workers</literal> (<type>integer</type>)
      <indexterm>
-- 
2.51.0

v6-0013-Add-support-for-oid8-TOAST-values.patchtext/x-diff; charset=us-asciiDownload
From 77a3ac4fbc0e8519d268bdba08e0dd74f08dc1fa Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 17:06:10 +0900
Subject: [PATCH v6 13/15] Add support for oid8 TOAST values

This commit adds the possibility to define TOAST tables with oid8 as
value ID, based on the reloption toast_value_type.  All the external
TOAST pointers still rely on varatt_external and a single vartag, with
all the values inserted in the bigint TOAST tables fed from the existing
OID value generator.  This will be changed in an upcoming patch that
adds more vartag_external types and its associated structures, with the
code being able to use a different external TOAST pointer depending on
the attribute type of chunk_id in TOAST relations.

All the changes done here are mechanical, with all the TOAST code able
to do chunk ID lookups based on the two types now supported.

XXX: Catalog version bump required.
---
 src/include/catalog/pg_opclass.dat          |  3 +-
 src/include/utils/rel.h                     |  1 +
 src/backend/access/common/reloptions.c      |  1 +
 src/backend/access/common/toast_internals.c | 94 +++++++++++++++------
 src/backend/access/heap/heaptoast.c         | 20 ++++-
 src/backend/catalog/toasting.c              | 24 +++++-
 doc/src/sgml/ref/create_table.sgml          |  2 +
 doc/src/sgml/storage.sgml                   |  7 +-
 contrib/amcheck/verify_heapam.c             | 19 ++++-
 9 files changed, 131 insertions(+), 40 deletions(-)

diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index c0de88fabc49..b8f2bc2d69c4 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -179,7 +179,8 @@
   opcintype => 'xid8' },
 { opcmethod => 'hash', opcname => 'oid8_ops', opcfamily => 'hash/oid8_ops',
   opcintype => 'oid8' },
-{ opcmethod => 'btree', opcname => 'oid8_ops', opcfamily => 'btree/oid8_ops',
+{ oid => '8285', oid_symbol => 'OID8_BTREE_OPS_OID',
+  opcmethod => 'btree', opcname => 'oid8_ops', opcfamily => 'btree/oid8_ops',
   opcintype => 'oid8' },
 { opcmethod => 'hash', opcname => 'cid_ops', opcfamily => 'hash/cid_ops',
   opcintype => 'cid' },
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index b846bd42103e..52646d43ebdc 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -342,6 +342,7 @@ typedef enum StdRdOptToastValueType
 {
 	STDRD_OPTION_TOAST_VALUE_TYPE_INVALID = 0,
 	STDRD_OPTION_TOAST_VALUE_TYPE_OID,
+	STDRD_OPTION_TOAST_VALUE_TYPE_OID8,
 } StdRdOptToastValueType;
 
 typedef struct StdRdOptions
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index b0447d9e39bb..f05eaacfa006 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -521,6 +521,7 @@ static relopt_enum_elt_def StdRdOptToastValueTypes[] =
 {
 	/* no value for INVALID */
 	{"oid", STDRD_OPTION_TOAST_VALUE_TYPE_OID},
+	{"oid8", STDRD_OPTION_TOAST_VALUE_TYPE_OID8},
 	{(const char *) NULL}		/* list terminator */
 };
 
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index a68869f58517..b54db5c5745a 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -26,6 +26,7 @@
 #include "utils/fmgroids.h"
 #include "utils/rel.h"
 #include "utils/snapmgr.h"
+#include "utils/lsyscache.h"
 
 static bool toastrel_valueid_exists(Relation toastrel, Oid8 valueid);
 static bool toastid_valueid_exists(Oid toastrelid, Oid8 valueid);
@@ -134,8 +135,10 @@ toast_save_datum(Relation rel, Datum value,
 	int			validIndex;
 	const toast_external_info *info;
 	uint8		tag = VARTAG_INDIRECT;	/* init value does not matter */
+	Oid			toast_typid = get_atttype(rel->rd_rel->reltoastrelid, 1);
 
 	Assert(!VARATT_IS_EXTERNAL(dval));
+	Assert(OidIsValid(toast_typid));
 
 	/*
 	 * Open the toast relation and its indexes.  We can use the index to check
@@ -216,24 +219,32 @@ toast_save_datum(Relation rel, Datum value,
 		toast_pointer.toastrelid = RelationGetRelid(toastrel);
 
 	/*
-	 * Choose an OID to use as the value ID for this toast value.
+	 * Choose a new value to use as the value ID for this toast value, be it
+	 * for OID or int8-based TOAST relations.
 	 *
-	 * Normally we just choose an unused OID within the toast table.  But
+	 * Normally we just choose an unused value within the toast table.  But
 	 * during table-rewriting operations where we are preserving an existing
-	 * toast table OID, we want to preserve toast value OIDs too.  So, if
+	 * toast table OID, we want to preserve toast value IDs too.  So, if
 	 * rd_toastoid is set and we had a prior external value from that same
 	 * toast table, re-use its value ID.  If we didn't have a prior external
 	 * value (which is a corner case, but possible if the table's attstorage
 	 * options have been changed), we have to pick a value ID that doesn't
-	 * conflict with either new or existing toast value OIDs.
+	 * conflict with either new or existing toast value IDs.  If the TOAST
+	 * table uses 8-byte value IDs, we should not really care much about
+	 * that.
 	 */
 	if (!OidIsValid(rel->rd_toastoid))
 	{
 		/* normal case: just choose an unused OID */
-		toast_pointer.valueid =
-			GetNewOidWithIndex(toastrel,
-							   RelationGetRelid(toastidxs[validIndex]),
-							   (AttrNumber) 1);
+		if (toast_typid == OIDOID)
+			toast_pointer.valueid =
+				GetNewOidWithIndex(toastrel,
+								   RelationGetRelid(toastidxs[validIndex]),
+								   (AttrNumber) 1);
+		else if (toast_typid == OID8OID)
+			toast_pointer.valueid = GetNewObjectId8();
+		else
+			Assert(false);
 	}
 	else
 	{
@@ -279,17 +290,22 @@ toast_save_datum(Relation rel, Datum value,
 		if (toast_pointer.valueid == InvalidOid8)
 		{
 			/*
-			 * new value; must choose an OID that doesn't conflict in either
-			 * old or new toast table
+			 * new value; must choose a value that doesn't conflict in either
+			 * old or new toast table.
 			 */
-			do
+			if (toast_typid == OIDOID)
 			{
-				toast_pointer.valueid =
-					GetNewOidWithIndex(toastrel,
-									   RelationGetRelid(toastidxs[validIndex]),
-									   (AttrNumber) 1);
-			} while (toastid_valueid_exists(rel->rd_toastoid,
-											toast_pointer.valueid));
+				do
+				{
+					toast_pointer.valueid =
+						GetNewOidWithIndex(toastrel,
+										   RelationGetRelid(toastidxs[validIndex]),
+										   (AttrNumber) 1);
+				} while (toastid_valueid_exists(rel->rd_toastoid,
+												toast_pointer.valueid));
+			}
+			else if (toast_typid == OID8OID)
+				toast_pointer.valueid = GetNewObjectId8();
 		}
 	}
 
@@ -329,7 +345,10 @@ toast_save_datum(Relation rel, Datum value,
 		/*
 		 * Build a tuple and store it
 		 */
-		t_values[0] = ObjectIdGetDatum(toast_pointer.valueid);
+		if (toast_typid == OIDOID)
+			t_values[0] = ObjectIdGetDatum(toast_pointer.valueid);
+		else if (toast_typid == OID8OID)
+			t_values[0] = ObjectId8GetDatum(toast_pointer.valueid);
 		t_values[1] = Int32GetDatum(chunk_seq++);
 		SET_VARSIZE(&chunk_data, chunk_size + VARHDRSZ);
 		memcpy(VARDATA(&chunk_data), data_p, chunk_size);
@@ -408,6 +427,7 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	HeapTuple	toasttup;
 	int			num_indexes;
 	int			validIndex;
+	Oid			toast_typid;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		return;
@@ -419,6 +439,8 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	 * Open the toast relation and its indexes
 	 */
 	toastrel = table_open(toast_pointer.toastrelid, RowExclusiveLock);
+	toast_typid = TupleDescAttr(toastrel->rd_att, 0)->atttypid;
+	Assert(toast_typid == OIDOID || toast_typid == OID8OID);
 
 	/* Fetch valid relation used for process */
 	validIndex = toast_open_indexes(toastrel,
@@ -429,10 +451,18 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	/*
 	 * Setup a scan key to find chunks with matching va_valueid
 	 */
-	ScanKeyInit(&toastkey,
-				(AttrNumber) 1,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(toast_pointer.valueid));
+	if (toast_typid == OIDOID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(toast_pointer.valueid));
+	else if (toast_typid == OID8OID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OID8EQ,
+					ObjectId8GetDatum(toast_pointer.valueid));
+	else
+		Assert(false);
 
 	/*
 	 * Find all the chunks.  (We don't actually care whether we see them in
@@ -479,6 +509,7 @@ toastrel_valueid_exists(Relation toastrel, Oid8 valueid)
 	int			num_indexes;
 	int			validIndex;
 	Relation   *toastidxs;
+	Oid			toast_typid;
 
 	/* Fetch a valid index relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -486,13 +517,24 @@ toastrel_valueid_exists(Relation toastrel, Oid8 valueid)
 									&toastidxs,
 									&num_indexes);
 
+	toast_typid = TupleDescAttr(toastrel->rd_att, 0)->atttypid;
+	Assert(toast_typid == OIDOID || toast_typid == OID8OID);
+
 	/*
 	 * Setup a scan key to find chunks with matching va_valueid
 	 */
-	ScanKeyInit(&toastkey,
-				(AttrNumber) 1,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(valueid));
+	if (toast_typid == OIDOID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(valueid));
+	else if (toast_typid == OID8OID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OID8EQ,
+					ObjectId8GetDatum(valueid));
+	else
+		Assert(false);
 
 	/*
 	 * Is there any such chunk?
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index 230f2a6f35eb..50e9bf9047f9 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -654,6 +654,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 	int32		max_chunk_size;
 	const toast_external_info *info;
 	uint8		tag = VARTAG_INDIRECT;	/* init value does not matter */
+	Oid			toast_typid;
 
 	/* Look for the valid index of toast relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -667,16 +668,27 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 
 	max_chunk_size = info->maximum_chunk_size;
 
+	toast_typid = TupleDescAttr(toastrel->rd_att, 0)->atttypid;
+	Assert(toast_typid == OIDOID || toast_typid == OID8OID);
+
 	totalchunks = ((attrsize - 1) / max_chunk_size) + 1;
 	startchunk = sliceoffset / max_chunk_size;
 	endchunk = (sliceoffset + slicelength - 1) / max_chunk_size;
 	Assert(endchunk <= totalchunks);
 
 	/* Set up a scan key to fetch from the index. */
-	ScanKeyInit(&toastkey[0],
-				(AttrNumber) 1,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(valueid));
+	if (toast_typid == OIDOID)
+		ScanKeyInit(&toastkey[0],
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(valueid));
+	else if (toast_typid == OID8OID)
+		ScanKeyInit(&toastkey[0],
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OID8EQ,
+					ObjectId8GetDatum(valueid));
+	else
+		Assert(false);
 
 	/*
 	 * No additional condition if fetching all chunks. Otherwise, use an
diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c
index 545983b5be9d..2288311b22a4 100644
--- a/src/backend/catalog/toasting.c
+++ b/src/backend/catalog/toasting.c
@@ -31,6 +31,7 @@
 #include "nodes/makefuncs.h"
 #include "utils/fmgroids.h"
 #include "utils/rel.h"
+#include "utils/lsyscache.h"
 #include "utils/syscache.h"
 
 static void CheckAndCreateToastTable(Oid relOid, Datum reloptions,
@@ -167,6 +168,8 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 		value_type = RelationGetToastValueType(rel, STDRD_OPTION_TOAST_VALUE_TYPE_OID);
 		if (value_type == STDRD_OPTION_TOAST_VALUE_TYPE_OID)
 			toast_chunkid_typid = OIDOID;
+		else if (value_type == STDRD_OPTION_TOAST_VALUE_TYPE_OID8)
+			toast_chunkid_typid = OID8OID;
 	}
 	else
 	{
@@ -199,7 +202,8 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("toast chunk_id type not set while in binary upgrade mode")));
-		if (binary_upgrade_next_toast_chunk_id_typoid != OIDOID)
+		if (binary_upgrade_next_toast_chunk_id_typoid != OIDOID &&
+			binary_upgrade_next_toast_chunk_id_typoid != OID8OID)
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("cannot support toast chunk_id type %u in binary upgrade mode",
@@ -224,6 +228,19 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	snprintf(toast_idxname, sizeof(toast_idxname),
 			 "pg_toast_%u_index", relOid);
 
+	/*
+	 * Special case here.  If OIDOldToast is defined, we need to rely on the
+	 * existing table for the job because we do not want to create an
+	 * inconsistent relation that would conflict with the parent and break
+	 * the world.
+	 */
+	if (OidIsValid(OIDOldToast))
+	{
+		toast_chunkid_typid = get_atttype(OIDOldToast, 1);
+		if (!OidIsValid(toast_chunkid_typid))
+			elog(ERROR, "cache lookup failed for relation %u", OIDOldToast);
+	}
+
 	/* this is pretty painful...  need a tuple descriptor */
 	tupdesc = CreateTemplateTupleDesc(3);
 	TupleDescInitEntry(tupdesc, (AttrNumber) 1,
@@ -336,7 +353,10 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	collationIds[0] = InvalidOid;
 	collationIds[1] = InvalidOid;
 
-	opclassIds[0] = OID_BTREE_OPS_OID;
+	if (toast_chunkid_typid == OIDOID)
+		opclassIds[0] = OID_BTREE_OPS_OID;
+	else if (toast_chunkid_typid == OID8OID)
+		opclassIds[0] = OID8_BTREE_OPS_OID;
 	opclassIds[1] = INT4_BTREE_OPS_OID;
 
 	coloptions[0] = 0;
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 84ad78afa3de..35dd317917e3 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1645,6 +1645,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       relation for this table.
       By default this parameter is <literal>oid</literal>, to assign
       <type>oid</type> as attribute type to <literal>chunk_id</literal>.
+      This parameter can be set to <type>oid8</type> to use <type>oid8</type>
+      as attribute type for <literal>chunk_id</literal>.
       This parameter cannot be set for TOAST tables.
      </para>
     </listitem>
diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml
index 67600fd974d7..afddf663fec5 100644
--- a/doc/src/sgml/storage.sgml
+++ b/doc/src/sgml/storage.sgml
@@ -421,14 +421,15 @@ most <symbol>TOAST_OID_MAX_CHUNK_SIZE</symbol> bytes (by default this value is c
 so that four chunk rows will fit on a page, making it about 2000 bytes).
 Each chunk is stored as a separate row in the <acronym>TOAST</acronym> table
 belonging to the owning table.  Every
-<acronym>TOAST</acronym> table has the columns <structfield>chunk_id</structfield> (an OID
-identifying the particular <acronym>TOAST</acronym>ed value),
+<acronym>TOAST</acronym> table has the columns
+<structfield>chunk_id</structfield> (an OID or an 8-byte integer identifying
+the particular <acronym>TOAST</acronym>ed value),
 <structfield>chunk_seq</structfield> (a sequence number for the chunk within its value),
 and <structfield>chunk_data</structfield> (the actual data of the chunk).  A unique index
 on <structfield>chunk_id</structfield> and <structfield>chunk_seq</structfield> provides fast
 retrieval of the values.  A pointer datum representing an out-of-line on-disk
 <acronym>TOAST</acronym>ed value therefore needs to store the OID of the
-<acronym>TOAST</acronym> table in which to look and the OID of the specific value
+<acronym>TOAST</acronym> table in which to look and the specific value
 (its <structfield>chunk_id</structfield>).  For convenience, pointer datums also store the
 logical datum size (original uncompressed data length), physical stored size
 (different if compression was applied), and the compression method used, if
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 9cf3c081bf01..143e6baa35cf 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1880,6 +1880,9 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	int32		last_chunk_seq;
 	Oid8		toast_valueid;
 	int32		max_chunk_size;
+	Oid			toast_typid;
+
+	toast_typid = TupleDescAttr(ctx->toast_rel->rd_att, 0)->atttypid;
 
 	extsize = ta->toast_pointer.extsize;
 
@@ -1889,10 +1892,18 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	/*
 	 * Setup a scan key to find chunks in toast table with matching va_valueid
 	 */
-	ScanKeyInit(&toastkey,
-				(AttrNumber) 1,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(ta->toast_pointer.valueid));
+	if (toast_typid == OIDOID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(ta->toast_pointer.valueid));
+	else if (toast_typid == OID8OID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OID8EQ,
+					ObjectId8GetDatum(ta->toast_pointer.valueid));
+	else
+		Assert(false);
 
 	/*
 	 * Check if any chunks for this toasted object exist in the toast table,
-- 
2.51.0

v6-0014-Add-tests-for-TOAST-relations-with-bigint-as-valu.patchtext/x-diff; charset=us-asciiDownload
From fbcdf979e7786178aa2a9629711034b113b02207 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 13:43:19 +0900
Subject: [PATCH v6 14/15] Add tests for TOAST relations with bigint as value
 type

This adds coverage for relations created with default_toast_type =
'int8', for external TOAST pointers both compressed and uncompressed.
---
 src/test/regress/expected/strings.out     | 231 ++++++++++++++++++----
 src/test/regress/expected/type_sanity.out |   6 +-
 src/test/regress/sql/strings.sql          | 134 +++++++++----
 src/test/regress/sql/type_sanity.sql      |   6 +-
 4 files changed, 296 insertions(+), 81 deletions(-)

diff --git a/src/test/regress/expected/strings.out b/src/test/regress/expected/strings.out
index 2d6cb02ad608..3a4bc9e65354 100644
--- a/src/test/regress/expected/strings.out
+++ b/src/test/regress/expected/strings.out
@@ -1954,21 +1954,37 @@ SELECT text 'text' || varchar ' and varchar' AS "Concat text to varchar";
 (1 row)
 
 --
--- test substr with toasted text values
+-- Test substr with toasted text values, for all types of TOAST relations
+-- supported.
 --
-CREATE TABLE toasttest(f1 text);
-insert into toasttest values(repeat('1234567890',10000));
-insert into toasttest values(repeat('1234567890',10000));
+CREATE TABLE toasttest_oid(f1 text) with (toast_value_type = 'oid');
+CREATE TABLE toasttest_oid8(f1 text) with (toast_value_type = 'oid8');
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid8 values(repeat('1234567890',10000));
+insert into toasttest_oid8 values(repeat('1234567890',10000));
 --
 -- Ensure that some values are uncompressed, to test the faster substring
 -- operation used in that case
 --
-alter table toasttest alter column f1 set storage external;
-insert into toasttest values(repeat('1234567890',10000));
-insert into toasttest values(repeat('1234567890',10000));
+alter table toasttest_oid alter column f1 set storage external;
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+alter table toasttest_oid8 alter column f1 set storage external;
+insert into toasttest_oid8 values(repeat('1234567890',10000));
+insert into toasttest_oid8 values(repeat('1234567890',10000));
 -- If the starting position is zero or less, then return from the start of the string
 -- adjusting the length to be consistent with the "negative start" per SQL.
-SELECT substr(f1, -1, 5) from toasttest;
+SELECT substr(f1, -1, 5) from toasttest_oid;
+ substr 
+--------
+ 123
+ 123
+ 123
+ 123
+(4 rows)
+
+SELECT substr(f1, -1, 5) from toasttest_oid8;
  substr 
 --------
  123
@@ -1978,11 +1994,22 @@ SELECT substr(f1, -1, 5) from toasttest;
 (4 rows)
 
 -- If the length is less than zero, an ERROR is thrown.
-SELECT substr(f1, 5, -1) from toasttest;
+SELECT substr(f1, 5, -1) from toasttest_oid;
+ERROR:  negative substring length not allowed
+SELECT substr(f1, 5, -1) from toasttest_oid8;
 ERROR:  negative substring length not allowed
 -- If no third argument (length) is provided, the length to the end of the
 -- string is assumed.
-SELECT substr(f1, 99995) from toasttest;
+SELECT substr(f1, 99995) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995) from toasttest_oid8;
  substr 
 --------
  567890
@@ -1993,7 +2020,7 @@ SELECT substr(f1, 99995) from toasttest;
 
 -- If start plus length is > string length, the result is truncated to
 -- string length
-SELECT substr(f1, 99995, 10) from toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
  substr 
 --------
  567890
@@ -2002,50 +2029,105 @@ SELECT substr(f1, 99995, 10) from toasttest;
  567890
 (4 rows)
 
-TRUNCATE TABLE toasttest;
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
+SELECT substr(f1, 99995, 10) from toasttest_oid8;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+-- TRUNCATE cases for TOAST relations with OID values.
+TRUNCATE TABLE toasttest_oid;
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
 -- expect >0 blocks
 SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
-  FROM pg_class where relname = 'toasttest';
+  FROM pg_class where relname = 'toasttest_oid';
  is_empty 
 ----------
  f
 (1 row)
 
-TRUNCATE TABLE toasttest;
-ALTER TABLE toasttest set (toast_tuple_target = 4080);
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
+TRUNCATE TABLE toasttest_oid;
+ALTER TABLE toasttest_oid set (toast_tuple_target = 4080);
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
 -- expect 0 blocks
 SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
-  FROM pg_class where relname = 'toasttest';
+  FROM pg_class where relname = 'toasttest_oid';
  is_empty 
 ----------
  t
 (1 row)
 
-DROP TABLE toasttest;
+DROP TABLE toasttest_oid;
+-- TRUNCATE cases for TOAST relation with int8 values.
+TRUNCATE TABLE toasttest_oid8;
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+-- expect >0 blocks
+SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
+  FROM pg_class where relname = 'toasttest_oid8';
+ is_empty 
+----------
+ f
+(1 row)
+
+TRUNCATE TABLE toasttest_oid8;
+ALTER TABLE toasttest_oid8 set (toast_tuple_target = 4080);
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+-- expect 0 blocks
+SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
+  FROM pg_class where relname = 'toasttest_oid8';
+ is_empty 
+----------
+ t
+(1 row)
+
+DROP TABLE toasttest_oid8;
 --
--- test substr with toasted bytea values
+-- test substr with toasted bytea values, for all types of TOAST relations
+-- supported. Do not drop these two relations, for pg_upgrade.
 --
-CREATE TABLE toasttest(f1 bytea);
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
+CREATE TABLE toasttest_oid(f1 bytea) WITH (toast_value_type = 'oid');
+CREATE TABLE toasttest_oid8(f1 bytea) WITH (toast_value_type = 'oid8');
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
 --
 -- Ensure that some values are uncompressed, to test the faster substring
 -- operation used in that case
 --
-alter table toasttest alter column f1 set storage external;
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
+alter table toasttest_oid alter column f1 set storage external;
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+alter table toasttest_oid8 alter column f1 set storage external;
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
 -- If the starting position is zero or less, then return from the start of the string
 -- adjusting the length to be consistent with the "negative start" per SQL.
-SELECT substr(f1, -1, 5) from toasttest;
+SELECT substr(f1, -1, 5) from toasttest_oid;
+ substr 
+--------
+ 123
+ 123
+ 123
+ 123
+(4 rows)
+
+SELECT substr(f1, -1, 5) from toasttest_oid8;
  substr 
 --------
  123
@@ -2055,11 +2137,22 @@ SELECT substr(f1, -1, 5) from toasttest;
 (4 rows)
 
 -- If the length is less than zero, an ERROR is thrown.
-SELECT substr(f1, 5, -1) from toasttest;
+SELECT substr(f1, 5, -1) from toasttest_oid;
+ERROR:  negative substring length not allowed
+SELECT substr(f1, 5, -1) from toasttest_oid8;
 ERROR:  negative substring length not allowed
 -- If no third argument (length) is provided, the length to the end of the
 -- string is assumed.
-SELECT substr(f1, 99995) from toasttest;
+SELECT substr(f1, 99995) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995) from toasttest_oid8;
  substr 
 --------
  567890
@@ -2070,7 +2163,72 @@ SELECT substr(f1, 99995) from toasttest;
 
 -- If start plus length is > string length, the result is truncated to
 -- string length
-SELECT substr(f1, 99995, 10) from toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995, 10) from toasttest_oid8;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+-- A relation rewrite leaves the TOAST value attributes unchanged.
+VACUUM FULL toasttest_oid;
+VACUUM FULL toasttest_oid8;
+SELECT c1.relname, a.atttypid::regtype
+  FROM pg_attribute AS a,
+       pg_class AS c1,
+       pg_class AS c2
+  WHERE
+       c1.relname IN ('toasttest_oid', 'toasttest_oid8') AND
+       c1.reltoastrelid = c2.oid AND
+       a.attrelid = c2.oid AND
+       a.attname = 'chunk_id'
+  ORDER BY c1.relname COLLATE "C";
+    relname     | atttypid 
+----------------+----------
+ toasttest_oid  | oid
+ toasttest_oid8 | oid8
+(2 rows)
+
+-- Check that data slices are still accessible.
+SELECT substr(f1, 99995) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995) from toasttest_oid8;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995, 10) from toasttest_oid8;
  substr 
 --------
  567890
@@ -2079,7 +2237,6 @@ SELECT substr(f1, 99995, 10) from toasttest;
  567890
 (4 rows)
 
-DROP TABLE toasttest;
 -- test internally compressing datums
 -- this tests compressing a datum to a very small size which exercises a
 -- corner case in packed-varlena handling: even though small, the compressed
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 9ddcacec6bf4..88faa57772c3 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -578,15 +578,15 @@ WHERE c1.relnatts != (SELECT count(*) FROM pg_attribute AS a1
 (0 rows)
 
 -- Cross-check against pg_type entry
--- NOTE: we allow attstorage to be 'plain' even when typstorage is not;
--- this is mainly for toast tables.
+-- NOTE: we allow attstorage to be 'plain' or 'external' even when typstorage
+-- is not; this is mainly for toast tables.
 SELECT a1.attrelid, a1.attname, t1.oid, t1.typname
 FROM pg_attribute AS a1, pg_type AS t1
 WHERE a1.atttypid = t1.oid AND
     (a1.attlen != t1.typlen OR
      a1.attalign != t1.typalign OR
      a1.attbyval != t1.typbyval OR
-     (a1.attstorage != t1.typstorage AND a1.attstorage != 'p'));
+     (a1.attstorage != t1.typstorage AND a1.attstorage NOT IN ('e', 'p')));
  attrelid | attname | oid | typname 
 ----------+---------+-----+---------
 (0 rows)
diff --git a/src/test/regress/sql/strings.sql b/src/test/regress/sql/strings.sql
index 5ed421d62059..d03ee6e17997 100644
--- a/src/test/regress/sql/strings.sql
+++ b/src/test/regress/sql/strings.sql
@@ -556,89 +556,147 @@ SELECT text 'text' || char(20) ' and characters' AS "Concat text to char";
 SELECT text 'text' || varchar ' and varchar' AS "Concat text to varchar";
 
 --
--- test substr with toasted text values
+-- Test substr with toasted text values, for all types of TOAST relations
+-- supported.
 --
-CREATE TABLE toasttest(f1 text);
+CREATE TABLE toasttest_oid(f1 text) with (toast_value_type = 'oid');
+CREATE TABLE toasttest_oid8(f1 text) with (toast_value_type = 'oid8');
 
-insert into toasttest values(repeat('1234567890',10000));
-insert into toasttest values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid8 values(repeat('1234567890',10000));
+insert into toasttest_oid8 values(repeat('1234567890',10000));
 
 --
 -- Ensure that some values are uncompressed, to test the faster substring
 -- operation used in that case
 --
-alter table toasttest alter column f1 set storage external;
-insert into toasttest values(repeat('1234567890',10000));
-insert into toasttest values(repeat('1234567890',10000));
+alter table toasttest_oid alter column f1 set storage external;
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+alter table toasttest_oid8 alter column f1 set storage external;
+insert into toasttest_oid8 values(repeat('1234567890',10000));
+insert into toasttest_oid8 values(repeat('1234567890',10000));
 
 -- If the starting position is zero or less, then return from the start of the string
 -- adjusting the length to be consistent with the "negative start" per SQL.
-SELECT substr(f1, -1, 5) from toasttest;
+SELECT substr(f1, -1, 5) from toasttest_oid;
+SELECT substr(f1, -1, 5) from toasttest_oid8;
 
 -- If the length is less than zero, an ERROR is thrown.
-SELECT substr(f1, 5, -1) from toasttest;
+SELECT substr(f1, 5, -1) from toasttest_oid;
+SELECT substr(f1, 5, -1) from toasttest_oid8;
 
 -- If no third argument (length) is provided, the length to the end of the
 -- string is assumed.
-SELECT substr(f1, 99995) from toasttest;
+SELECT substr(f1, 99995) from toasttest_oid;
+SELECT substr(f1, 99995) from toasttest_oid8;
 
 -- If start plus length is > string length, the result is truncated to
 -- string length
-SELECT substr(f1, 99995, 10) from toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+SELECT substr(f1, 99995, 10) from toasttest_oid8;
 
-TRUNCATE TABLE toasttest;
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
+-- TRUNCATE cases for TOAST relations with OID values.
+TRUNCATE TABLE toasttest_oid;
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
 -- expect >0 blocks
 SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
-  FROM pg_class where relname = 'toasttest';
-
-TRUNCATE TABLE toasttest;
-ALTER TABLE toasttest set (toast_tuple_target = 4080);
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
+  FROM pg_class where relname = 'toasttest_oid';
+TRUNCATE TABLE toasttest_oid;
+ALTER TABLE toasttest_oid set (toast_tuple_target = 4080);
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
 -- expect 0 blocks
 SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
-  FROM pg_class where relname = 'toasttest';
+  FROM pg_class where relname = 'toasttest_oid';
+DROP TABLE toasttest_oid;
 
-DROP TABLE toasttest;
+-- TRUNCATE cases for TOAST relation with int8 values.
+TRUNCATE TABLE toasttest_oid8;
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+-- expect >0 blocks
+SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
+  FROM pg_class where relname = 'toasttest_oid8';
+TRUNCATE TABLE toasttest_oid8;
+ALTER TABLE toasttest_oid8 set (toast_tuple_target = 4080);
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+-- expect 0 blocks
+SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
+  FROM pg_class where relname = 'toasttest_oid8';
+DROP TABLE toasttest_oid8;
 
 --
--- test substr with toasted bytea values
+-- test substr with toasted bytea values, for all types of TOAST relations
+-- supported. Do not drop these two relations, for pg_upgrade.
 --
-CREATE TABLE toasttest(f1 bytea);
+CREATE TABLE toasttest_oid(f1 bytea) WITH (toast_value_type = 'oid');
+CREATE TABLE toasttest_oid8(f1 bytea) WITH (toast_value_type = 'oid8');
 
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
 
 --
 -- Ensure that some values are uncompressed, to test the faster substring
 -- operation used in that case
 --
-alter table toasttest alter column f1 set storage external;
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
+alter table toasttest_oid alter column f1 set storage external;
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+alter table toasttest_oid8 alter column f1 set storage external;
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
 
 -- If the starting position is zero or less, then return from the start of the string
 -- adjusting the length to be consistent with the "negative start" per SQL.
-SELECT substr(f1, -1, 5) from toasttest;
+SELECT substr(f1, -1, 5) from toasttest_oid;
+SELECT substr(f1, -1, 5) from toasttest_oid8;
 
 -- If the length is less than zero, an ERROR is thrown.
-SELECT substr(f1, 5, -1) from toasttest;
+SELECT substr(f1, 5, -1) from toasttest_oid;
+SELECT substr(f1, 5, -1) from toasttest_oid8;
 
 -- If no third argument (length) is provided, the length to the end of the
 -- string is assumed.
-SELECT substr(f1, 99995) from toasttest;
+SELECT substr(f1, 99995) from toasttest_oid;
+SELECT substr(f1, 99995) from toasttest_oid8;
 
 -- If start plus length is > string length, the result is truncated to
 -- string length
-SELECT substr(f1, 99995, 10) from toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+SELECT substr(f1, 99995, 10) from toasttest_oid8;
 
-DROP TABLE toasttest;
+-- A relation rewrite leaves the TOAST value attributes unchanged.
+VACUUM FULL toasttest_oid;
+VACUUM FULL toasttest_oid8;
+SELECT c1.relname, a.atttypid::regtype
+  FROM pg_attribute AS a,
+       pg_class AS c1,
+       pg_class AS c2
+  WHERE
+       c1.relname IN ('toasttest_oid', 'toasttest_oid8') AND
+       c1.reltoastrelid = c2.oid AND
+       a.attrelid = c2.oid AND
+       a.attname = 'chunk_id'
+  ORDER BY c1.relname COLLATE "C";
+-- Check that data slices are still accessible.
+SELECT substr(f1, 99995) from toasttest_oid;
+SELECT substr(f1, 99995) from toasttest_oid8;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+SELECT substr(f1, 99995, 10) from toasttest_oid8;
 
 -- test internally compressing datums
 
diff --git a/src/test/regress/sql/type_sanity.sql b/src/test/regress/sql/type_sanity.sql
index c2496823d90e..a0d2e8bcf00b 100644
--- a/src/test/regress/sql/type_sanity.sql
+++ b/src/test/regress/sql/type_sanity.sql
@@ -420,8 +420,8 @@ WHERE c1.relnatts != (SELECT count(*) FROM pg_attribute AS a1
                       WHERE a1.attrelid = c1.oid AND a1.attnum > 0);
 
 -- Cross-check against pg_type entry
--- NOTE: we allow attstorage to be 'plain' even when typstorage is not;
--- this is mainly for toast tables.
+-- NOTE: we allow attstorage to be 'plain' or 'external' even when typstorage
+-- is not; this is mainly for toast tables.
 
 SELECT a1.attrelid, a1.attname, t1.oid, t1.typname
 FROM pg_attribute AS a1, pg_type AS t1
@@ -429,7 +429,7 @@ WHERE a1.atttypid = t1.oid AND
     (a1.attlen != t1.typlen OR
      a1.attalign != t1.typalign OR
      a1.attbyval != t1.typbyval OR
-     (a1.attstorage != t1.typstorage AND a1.attstorage != 'p'));
+     (a1.attstorage != t1.typstorage AND a1.attstorage NOT IN ('e', 'p')));
 
 -- Look for IsCatalogTextUniqueIndexOid() omissions.
 
-- 
2.51.0

v6-0015-Add-new-vartag_external-for-8-byte-TOAST-values.patchtext/x-diff; charset=us-asciiDownload
From 05fef162492e5bbdc2e16d24834b661e564ce6ca Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 14:10:36 +0900
Subject: [PATCH v6 15/15] Add new vartag_external for 8-byte TOAST values

This is a new type of external TOAST pointer, able to be fed 8-byte
TOAST values.  It uses a dedicated vartag_external, which is used when
a TOAST table uses bigint for its chunk_id.

The relevant callbacks are added to toast_external.c.
---
 src/include/access/heaptoast.h                |   8 +-
 src/include/varatt.h                          |  34 +++-
 src/backend/access/common/toast_external.c    | 145 ++++++++++++++++--
 src/backend/access/heap/heaptoast.c           |   1 +
 .../replication/logical/reorderbuffer.c       |  10 +-
 doc/src/sgml/storage.sgml                     |   6 +-
 contrib/amcheck/verify_heapam.c               |   2 +-
 7 files changed, 189 insertions(+), 17 deletions(-)

diff --git a/src/include/access/heaptoast.h b/src/include/access/heaptoast.h
index afa3d8ca95f7..e944d5f8420c 100644
--- a/src/include/access/heaptoast.h
+++ b/src/include/access/heaptoast.h
@@ -81,6 +81,12 @@
 
 #define EXTERN_TUPLE_MAX_SIZE	MaximumBytesPerTuple(EXTERN_TUPLES_PER_PAGE)
 
+#define TOAST_OID8_MAX_CHUNK_SIZE	\
+	(EXTERN_TUPLE_MAX_SIZE -							\
+	 MAXALIGN(SizeofHeapTupleHeader) -					\
+	 (sizeof(uint32) * 2) -								\
+	 sizeof(int32) -									\
+	 VARHDRSZ)
 #define TOAST_OID_MAX_CHUNK_SIZE	\
 	(EXTERN_TUPLE_MAX_SIZE -							\
 	 MAXALIGN(SizeofHeapTupleHeader) -					\
@@ -89,7 +95,7 @@
 	 VARHDRSZ)
 
 /* Maximum size of chunk possible */
-#define TOAST_MAX_CHUNK_SIZE	TOAST_OID_MAX_CHUNK_SIZE
+#define TOAST_MAX_CHUNK_SIZE	Max(TOAST_OID_MAX_CHUNK_SIZE, TOAST_OID8_MAX_CHUNK_SIZE)
 
 /* ----------
  * heap_toast_insert_or_update -
diff --git a/src/include/varatt.h b/src/include/varatt.h
index 035c0f95e5b6..de38d1cd1ce1 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -41,6 +41,27 @@ typedef struct varatt_external_oid
 	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
 }			varatt_external_oid;
 
+/*
+ * struct varatt_external_oid8 is a "larger" version of "TOAST pointer",
+ * that uses an 8-byte integer as value.
+ *
+ * This follows the same properties as varatt_external_oid, except that
+ * this is used in TOAST relations with oid8 as attribute for chunk_id.
+ */
+typedef struct varatt_external_oid8
+{
+	int32		va_rawsize;		/* Original data size (includes header) */
+	uint32		va_extinfo;		/* External saved size (without header) and
+								 * compression method */
+	/*
+	 * Unique ID of value within TOAST table, as two uint32 for alignment
+	 * and padding.
+	 */
+	uint32		va_valueid_lo;
+	uint32		va_valueid_hi;
+	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
+}			varatt_external_oid8;
+
 /*
  * These macros define the "saved size" portion of va_extinfo.  Its remaining
  * two high-order bits identify the compression method.
@@ -90,6 +111,7 @@ typedef enum vartag_external
 	VARTAG_INDIRECT = 1,
 	VARTAG_EXPANDED_RO = 2,
 	VARTAG_EXPANDED_RW = 3,
+	VARTAG_ONDISK_OID8 = 4,
 	VARTAG_ONDISK_OID = 18
 } vartag_external;
 
@@ -111,6 +133,8 @@ VARTAG_SIZE(vartag_external tag)
 		return sizeof(varatt_expanded);
 	else if (tag == VARTAG_ONDISK_OID)
 		return sizeof(varatt_external_oid);
+	else if (tag == VARTAG_ONDISK_OID8)
+		return sizeof(varatt_external_oid8);
 	else
 	{
 		Assert(false);
@@ -367,11 +391,19 @@ VARATT_IS_EXTERNAL_ONDISK_OID(const void *PTR)
 	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID;
 }
 
+/* Is varlena datum a pointer to on-disk toasted data with OID8 value? */
+static inline bool
+VARATT_IS_EXTERNAL_ONDISK_OID8(const void *PTR)
+{
+	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID8;
+}
+
 /* Is varlena datum a pointer to on-disk toasted data? */
 static inline bool
 VARATT_IS_EXTERNAL_ONDISK(const void *PTR)
 {
-	return VARATT_IS_EXTERNAL_ONDISK_OID(PTR);
+	return VARATT_IS_EXTERNAL_ONDISK_OID(PTR) ||
+		VARATT_IS_EXTERNAL_ONDISK_OID8(PTR);
 }
 
 /* Is varlena datum an indirect pointer? */
diff --git a/src/backend/access/common/toast_external.c b/src/backend/access/common/toast_external.c
index 8f58195051cf..f0f718085e8d 100644
--- a/src/backend/access/common/toast_external.c
+++ b/src/backend/access/common/toast_external.c
@@ -18,8 +18,19 @@
 #include "postgres.h"
 
 #include "access/detoast.h"
+#include "access/genam.h"
 #include "access/heaptoast.h"
 #include "access/toast_external.h"
+#include "catalog/catalog.h"
+#include "miscadmin.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+
+
+/* Callbacks for VARTAG_ONDISK_OID8 */
+static void ondisk_oid8_to_external_data(struct varlena *attr,
+										 toast_external_data *data);
+static struct varlena *ondisk_oid8_create_external_data(toast_external_data data);
 
 /* Callbacks for VARTAG_ONDISK_OID */
 static void ondisk_oid_to_external_data(struct varlena *attr,
@@ -28,7 +39,7 @@ static struct varlena *ondisk_oid_create_external_data(toast_external_data data)
 
 /*
  * Fetch the possibly-unaligned contents of an on-disk external TOAST with
- * OID values into a local "varatt_external_oid" pointer.
+ * OID or OID8 values into a local "varatt_external_*" pointer.
  *
  * This should be just a memcpy, but some versions of gcc seem to produce
  * broken code that assumes the datum contents are aligned.  Introducing
@@ -45,9 +56,20 @@ varatt_external_oid_get_pointer(varatt_external_oid *toast_pointer,
 	memcpy(toast_pointer, VARDATA_EXTERNAL(attre), sizeof(varatt_external_oid));
 }
 
+static inline void
+varatt_external_oid8_get_pointer(varatt_external_oid8 *toast_pointer,
+								 struct varlena *attr)
+{
+	varattrib_1b_e *attre = (varattrib_1b_e *) attr;
+
+	Assert(VARATT_IS_EXTERNAL_ONDISK_OID8(attre));
+	Assert(VARSIZE_EXTERNAL(attre) == sizeof(varatt_external_oid8) + VARHDRSZ_EXTERNAL);
+	memcpy(toast_pointer, VARDATA_EXTERNAL(attre), sizeof(varatt_external_oid8));
+}
+
 /*
  * Decompressed size of an on-disk varlena; but note argument is a struct
- * varatt_external_oid.
+ * varatt_external_oid or varatt_external_oid8.
  */
 static inline Size
 varatt_external_oid_get_extsize(varatt_external_oid toast_pointer)
@@ -55,9 +77,15 @@ varatt_external_oid_get_extsize(varatt_external_oid toast_pointer)
 	return toast_pointer.va_extinfo & VARLENA_EXTSIZE_MASK;
 }
 
+static inline Size
+varatt_external_oid8_get_extsize(varatt_external_oid8 toast_pointer)
+{
+	return toast_pointer.va_extinfo & VARLENA_EXTSIZE_MASK;
+}
+
 /*
  * Compression method of an on-disk varlena; but note argument is a struct
- *  varatt_external_oid.
+ *  varatt_external_oid or varatt_external_oid8.
  */
 static inline uint32
 varatt_external_oid_get_compress_method(varatt_external_oid toast_pointer)
@@ -65,6 +93,12 @@ varatt_external_oid_get_compress_method(varatt_external_oid toast_pointer)
 	return toast_pointer.va_extinfo >> VARLENA_EXTSIZE_BITS;
 }
 
+static inline uint32
+varatt_external_oid8_get_compress_method(varatt_external_oid8 toast_pointer)
+{
+	return toast_pointer.va_extinfo >> VARLENA_EXTSIZE_BITS;
+}
+
 /*
  * Testing whether an externally-stored TOAST value is compressed now requires
  * comparing size stored in va_extinfo (the actual length of the external data)
@@ -79,6 +113,19 @@ varatt_external_oid_is_compressed(varatt_external_oid toast_pointer)
 		(Size) (toast_pointer.va_rawsize - VARHDRSZ);
 }
 
+static inline bool
+varatt_external_oid8_is_compressed(varatt_external_oid8 toast_pointer)
+{
+	return varatt_external_oid8_get_extsize(toast_pointer) <
+		(Size) (toast_pointer.va_rawsize - VARHDRSZ);
+}
+
+/*
+ * Size of an EXTERNAL datum that contains a standard TOAST pointer
+ * (oid8 value).
+ */
+#define TOAST_POINTER_OID8_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_oid8))
+
 /*
  * Size of an EXTERNAL datum that contains a standard TOAST pointer (OID
  * value).
@@ -99,6 +146,12 @@ varatt_external_oid_is_compressed(varatt_external_oid toast_pointer)
  * individual fields.
  */
 static const toast_external_info toast_external_infos[TOAST_EXTERNAL_INFO_SIZE] = {
+	[VARTAG_ONDISK_OID8] = {
+		.toast_pointer_size = TOAST_POINTER_OID8_SIZE,
+		.maximum_chunk_size = TOAST_OID_MAX_CHUNK_SIZE,
+		.to_external_data = ondisk_oid8_to_external_data,
+		.create_external_data = ondisk_oid8_create_external_data,
+	},
 	[VARTAG_ONDISK_OID] = {
 		.toast_pointer_size = TOAST_OID_POINTER_SIZE,
 		.maximum_chunk_size = TOAST_OID_MAX_CHUNK_SIZE,
@@ -150,22 +203,33 @@ toast_external_info_get_pointer_size(uint8 tag)
 uint8
 toast_external_assign_vartag(Oid toastrelid, Oid8 valueid)
 {
+	Oid		toast_typid;
+
 	/*
-	 * If dealing with a code path where a TOAST relation may not be assigned,
-	 * like heap_toast_insert_or_update(), just use the legacy
-	 * vartag_external.
+	 * If dealing with a code path where a TOAST relation may not be assigned
+	 * like heap_toast_insert_or_update(), just use the default with an OID
+	 * type.
+	 *
+	 * In bootstrap mode, we should not do any kind of syscache lookups,
+	 * so also rely on OID.
 	 */
-	if (!OidIsValid(toastrelid))
+	if (!OidIsValid(toastrelid) || IsBootstrapProcessingMode())
 		return VARTAG_ONDISK_OID;
 
 	/*
-	 * Currently there is only one type of vartag_external supported: 4-byte
-	 * value with OID for the chunk_id type.
+	 * Two types of vartag_external are currently supported: OID and OID8,
+	 * which depend on the type assigned to "chunk_id" for the TOAST table.
 	 *
-	 * Note: This routine will be extended to be able to use multiple
-	 * vartag_external within a single TOAST relation type, that may change
-	 * depending on the value used.
+	 * XXX: Should we assign from the start an OID vartag if dealing with
+	 * a TOAST relation with OID8 as value if the value assigned is less
+	 * than UINT_MAX?  This just takes the "safe" approach of assigning
+	 * the larger vartag in all cases, but this can be made cheaper
+	 * depending on the OID consumption.
 	 */
+	toast_typid = get_atttype(toastrelid, 1);
+	if (toast_typid == OID8OID)
+		return VARTAG_ONDISK_OID8;
+
 	return VARTAG_ONDISK_OID;
 }
 
@@ -174,6 +238,63 @@ toast_external_assign_vartag(Oid toastrelid, Oid8 valueid)
  * the in-memory representation toast_external_data used in the backend.
  */
 
+/* Callbacks for VARTAG_ONDISK_OID8 */
+static void
+ondisk_oid8_to_external_data(struct varlena *attr, toast_external_data *data)
+{
+	varatt_external_oid8	external;
+
+	varatt_external_oid8_get_pointer(&external, attr);
+	data->rawsize = external.va_rawsize;
+
+	/* External size and compression methods are stored in the same field */
+	if (varatt_external_oid8_is_compressed(external))
+	{
+		data->extsize = varatt_external_oid8_get_extsize(external);
+		data->compression_method = varatt_external_oid8_get_compress_method(external);
+	}
+	else
+	{
+		data->extsize = external.va_extinfo;
+		data->compression_method = TOAST_INVALID_COMPRESSION_ID;
+	}
+
+	data->valueid = (((uint64) external.va_valueid_hi) << 32) |
+		external.va_valueid_lo;
+	data->toastrelid = external.va_toastrelid;
+
+}
+
+static struct varlena *
+ondisk_oid8_create_external_data(toast_external_data data)
+{
+	struct varlena *result = NULL;
+	varatt_external_oid8 external;
+
+	external.va_rawsize = data.rawsize;
+
+	if (data.compression_method != TOAST_INVALID_COMPRESSION_ID)
+	{
+		/* Set size and compression method, in a single field. */
+		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(external,
+													 data.extsize,
+													 data.compression_method);
+	}
+	else
+		external.va_extinfo = data.extsize;
+
+	external.va_toastrelid = data.toastrelid;
+	external.va_valueid_hi = (((uint64) data.valueid) >> 32);
+	external.va_valueid_lo = (uint32) data.valueid;
+
+	result = (struct varlena *) palloc(TOAST_POINTER_OID8_SIZE);
+	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_OID8);
+	memcpy(VARDATA_EXTERNAL(result), &external, sizeof(external));
+
+	return result;
+}
+
+
 /* Callbacks for VARTAG_ONDISK_OID */
 
 /*
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index 50e9bf9047f9..cba6e14ea805 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -32,6 +32,7 @@
 #include "access/toast_helper.h"
 #include "access/toast_internals.h"
 #include "utils/fmgroids.h"
+#include "utils/syscache.h"
 
 
 /* ----------
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 2db447f58ad5..32c512854f20 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -4986,14 +4986,22 @@ ReorderBufferToastAppendChunk(ReorderBuffer *rb, ReorderBufferTXN *txn,
 	TupleDesc	desc = RelationGetDescr(relation);
 	Oid8		chunk_id;
 	int32		chunk_seq;
+	Oid			toast_typid;
 
 	if (txn->toast_hash == NULL)
 		ReorderBufferToastInitHash(rb, txn);
+	toast_typid = TupleDescAttr(desc, 0)->atttypid;
 
 	Assert(IsToastRelation(relation));
 
 	newtup = change->data.tp.newtuple;
-	chunk_id = DatumGetObjectId(fastgetattr(newtup, 1, desc, &isnull));
+	/* This depends on the type of TOAST value dealt with. */
+	if (toast_typid == OIDOID)
+		chunk_id = DatumGetObjectId(fastgetattr(newtup, 1, desc, &isnull));
+	else if (toast_typid == INT8OID)
+		chunk_id = DatumGetUInt64(fastgetattr(newtup, 1, desc, &isnull));
+	else
+		Assert(false);
 	Assert(!isnull);
 	chunk_seq = DatumGetInt32(fastgetattr(newtup, 2, desc, &isnull));
 	Assert(!isnull);
diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml
index afddf663fec5..dbec30d48b4a 100644
--- a/doc/src/sgml/storage.sgml
+++ b/doc/src/sgml/storage.sgml
@@ -417,7 +417,11 @@ described in more detail below.
 
 <para>
 Out-of-line values are divided (after compression if used) into chunks of at
-most <symbol>TOAST_OID_MAX_CHUNK_SIZE</symbol> bytes (by default this value is chosen
+most <symbol>TOAST_OID_MAX_CHUNK_SIZE</symbol> bytes if the
+<acronym>TOAST</acronym> relation uses the <literal>oid</literal> type for
+<literal>chunk_id</literal>, or <symbol>TOAST_OID8_MAX_CHUNK_SIZE</symbol>
+bytes if the <acronym>TOAST</acronym> relation uses the <literal>oid8</literal>
+type for <literal>chunk_id</literal> (by default these values are chosen
 so that four chunk rows will fit on a page, making it about 2000 bytes).
 Each chunk is stored as a separate row in the <acronym>TOAST</acronym> table
 belonging to the owning table.  Every
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 143e6baa35cf..8cea9ad31bcd 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1733,7 +1733,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	{
 		uint8		va_tag = VARTAG_EXTERNAL(tp + ctx->offset);
 
-		if (va_tag != VARTAG_ONDISK_OID)
+		if (va_tag != VARTAG_ONDISK_OID && va_tag != VARTAG_ONDISK_OID8)
 		{
 			report_corruption(ctx,
 							  psprintf("toasted attribute has unexpected TOAST tag %u",
-- 
2.51.0

#48Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#47)
15 attachment(s)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

On Tue, Sep 16, 2025 at 02:14:43PM +0900, Michael Paquier wrote:

On Thu, Aug 14, 2025 at 02:49:06PM +0900, Michael Paquier wrote:

I have dropped the amcheck test patch for now, which was fun but it's
not really necessary for the "basics". I have done also more tests,
playing for example with pg_resetwal, installcheck and pg_upgrade
scenarios. I am wondering if it would be worth doing a pg_resetwal in
the node doing an installcheck on the instance to be upgraded, bumping
its next OID to be much larger than 4 billion, actually..

Four patches had conflicts with 748caa9dcb68, so rebased as v6.

There were a few conflicts, so here is a rebased v7, moving the patch
to the next CF. I have been sitting on this patch for six weeks for
the moment.

Tom, you are registered as a reviewer of the patch. The point of
contention of the patch, where I see there is no consensus yet, is if
my approach of using a redirection for the external TOAST pointers
with a new layer to facilitate the addition of more vartags (aka the
64b value vartag proposed here, concept that could also apply to
compression methods later on) is acceptable. Moving to a different
approach, like the "brutal" one I am naming upthread where the
redirection layer is replaced by changes in all the code paths that
need to be touched, would be of course cheaper at runtime as there
would be no more redirection, but the maintenance would be a nightmare
the more vartags we add, and I have some plans for more of these.
Doing the switch would be a few hours work, so that would not be a big
deal, I guess. The important part is an agreement about the approach,
IMO.

Please note that the latest patch set also uses a new reloption to
control if 8 byte TOAST values are set (not a GUC), binary upgrades
are handled with binary_upgrade.h functions, and the 8-byte OID value
uses a dedicated data type, as reviewed upthread.

One thing that shows up in the last patch of the set is mentioned in
an XXX comment in toast_external_assign_vartag(), if it would be
better to use a 4-byte vartag even if we have a 8-byte value in the
TOAST table to make the TOAST pointers shorter when a value is less
than 4 billions. Not sure how much to do about this one, and there's
little point in doing this change without the earlier infrastructure
patches if the approach taken is thought as OK, as well.
--
Michael

Attachments:

v7-0001-Implement-oid8-data-type.patchtext/x-diff; charset=us-asciiDownload
From 03c67d859688fe10d26164a4e3519df33a747e87 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 11:03:17 +0900
Subject: [PATCH v7 01/15] Implement oid8 data type

This new identifier type will be used for 8-byte TOAST values, and can
be useful for other purposes, not yet defined as of writing this patch.
The following operators are added for this data type:
- Casts with integer types and OID.
- btree and hash operators
- min/max functions.
- Tests and documentation.

XXX: Requires catversion bump.
---
 src/include/c.h                           |  11 +-
 src/include/catalog/pg_aggregate.dat      |   6 +
 src/include/catalog/pg_amop.dat           |  23 +++
 src/include/catalog/pg_amproc.dat         |  12 ++
 src/include/catalog/pg_cast.dat           |  14 ++
 src/include/catalog/pg_opclass.dat        |   4 +
 src/include/catalog/pg_operator.dat       |  26 +++
 src/include/catalog/pg_opfamily.dat       |   4 +
 src/include/catalog/pg_proc.dat           |  64 +++++++
 src/include/catalog/pg_type.dat           |   5 +
 src/include/fmgr.h                        |   2 +
 src/include/postgres.h                    |  20 +++
 src/backend/access/nbtree/nbtcompare.c    |  82 +++++++++
 src/backend/bootstrap/bootstrap.c         |   2 +
 src/backend/utils/adt/Makefile            |   1 +
 src/backend/utils/adt/int8.c              |   8 +
 src/backend/utils/adt/meson.build         |   1 +
 src/backend/utils/adt/oid8.c              | 171 +++++++++++++++++++
 src/fe_utils/print.c                      |   1 +
 src/test/regress/expected/oid8.out        | 196 ++++++++++++++++++++++
 src/test/regress/expected/oid8.sql        |   0
 src/test/regress/expected/opr_sanity.out  |   7 +
 src/test/regress/expected/type_sanity.out |   1 +
 src/test/regress/parallel_schedule        |   2 +-
 src/test/regress/sql/oid8.sql             |  57 +++++++
 src/test/regress/sql/type_sanity.sql      |   1 +
 doc/src/sgml/datatype.sgml                |  11 ++
 doc/src/sgml/func/func-aggregate.sgml     |   8 +-
 28 files changed, 734 insertions(+), 6 deletions(-)
 create mode 100644 src/backend/utils/adt/oid8.c
 create mode 100644 src/test/regress/expected/oid8.out
 create mode 100644 src/test/regress/expected/oid8.sql
 create mode 100644 src/test/regress/sql/oid8.sql

diff --git a/src/include/c.h b/src/include/c.h
index 7fe083c3afb8..049f75005b43 100644
--- a/src/include/c.h
+++ b/src/include/c.h
@@ -556,6 +556,7 @@ typedef uint32 bits32;			/* >= 32 bits */
 /* snprintf format strings to use for 64-bit integers */
 #define INT64_FORMAT "%" PRId64
 #define UINT64_FORMAT "%" PRIu64
+#define OID8_FORMAT "%" PRIu64
 
 /*
  * 128-bit signed and unsigned integers
@@ -642,7 +643,7 @@ typedef double float8;
 #define FLOAT8PASSBYVAL true
 
 /*
- * Oid, RegProcedure, TransactionId, SubTransactionId, MultiXactId,
+ * Oid, Oid8, RegProcedure, TransactionId, SubTransactionId, MultiXactId,
  * CommandId
  */
 
@@ -674,6 +675,12 @@ typedef uint32 CommandId;
 #define FirstCommandId	((CommandId) 0)
 #define InvalidCommandId	(~(CommandId)0)
 
+/* 8-byte Object ID */
+typedef uint64 Oid8;
+
+#define InvalidOid8		((Oid8) 0)
+#define OID8_MAX	UINT64_MAX
+#define atooid8(x) ((Oid8) strtou64((x), NULL, 10))
 
 /* ----------------
  *		Variable-length datatypes all share the 'struct varlena' header.
@@ -774,6 +781,8 @@ typedef NameData *Name;
 
 #define OidIsValid(objectId)  ((bool) ((objectId) != InvalidOid))
 
+#define Oid8IsValid(objectId)  ((bool) ((objectId) != InvalidOid8))
+
 #define RegProcedureIsValid(p)	OidIsValid(p)
 
 
diff --git a/src/include/catalog/pg_aggregate.dat b/src/include/catalog/pg_aggregate.dat
index d6aa1f6ec478..75acf4ef96cd 100644
--- a/src/include/catalog/pg_aggregate.dat
+++ b/src/include/catalog/pg_aggregate.dat
@@ -104,6 +104,9 @@
 { aggfnoid => 'max(oid)', aggtransfn => 'oidlarger',
   aggcombinefn => 'oidlarger', aggsortop => '>(oid,oid)',
   aggtranstype => 'oid' },
+{ aggfnoid => 'max(oid8)', aggtransfn => 'oid8larger',
+  aggcombinefn => 'oid8larger', aggsortop => '>(oid8,oid8)',
+  aggtranstype => 'oid8' },
 { aggfnoid => 'max(float4)', aggtransfn => 'float4larger',
   aggcombinefn => 'float4larger', aggsortop => '>(float4,float4)',
   aggtranstype => 'float4' },
@@ -178,6 +181,9 @@
 { aggfnoid => 'min(oid)', aggtransfn => 'oidsmaller',
   aggcombinefn => 'oidsmaller', aggsortop => '<(oid,oid)',
   aggtranstype => 'oid' },
+{ aggfnoid => 'min(oid8)', aggtransfn => 'oid8smaller',
+  aggcombinefn => 'oid8smaller', aggsortop => '<(oid8,oid8)',
+  aggtranstype => 'oid8' },
 { aggfnoid => 'min(float4)', aggtransfn => 'float4smaller',
   aggcombinefn => 'float4smaller', aggsortop => '<(float4,float4)',
   aggtranstype => 'float4' },
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 2a693cfc31c6..2c3004d53611 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -180,6 +180,24 @@
 { amopfamily => 'btree/oid_ops', amoplefttype => 'oid', amoprighttype => 'oid',
   amopstrategy => '5', amopopr => '>(oid,oid)', amopmethod => 'btree' },
 
+# btree oid8_ops
+
+{ amopfamily => 'btree/oid8_ops', amoplefttype => 'oid8',
+  amoprighttype => 'oid8', amopstrategy => '1', amopopr => '<(oid8,oid8)',
+  amopmethod => 'btree' },
+{ amopfamily => 'btree/oid8_ops', amoplefttype => 'oid8',
+  amoprighttype => 'oid8', amopstrategy => '2', amopopr => '<=(oid8,oid8)',
+  amopmethod => 'btree' },
+{ amopfamily => 'btree/oid8_ops', amoplefttype => 'oid8',
+  amoprighttype => 'oid8', amopstrategy => '3', amopopr => '=(oid8,oid8)',
+  amopmethod => 'btree' },
+{ amopfamily => 'btree/oid8_ops', amoplefttype => 'oid8',
+  amoprighttype => 'oid8', amopstrategy => '4', amopopr => '>=(oid8,oid8)',
+  amopmethod => 'btree' },
+{ amopfamily => 'btree/oid8_ops', amoplefttype => 'oid8',
+  amoprighttype => 'oid8', amopstrategy => '5', amopopr => '>(oid8,oid8)',
+  amopmethod => 'btree' },
+
 # btree xid8_ops
 
 { amopfamily => 'btree/xid8_ops', amoplefttype => 'xid8',
@@ -974,6 +992,11 @@
 { amopfamily => 'hash/oid_ops', amoplefttype => 'oid', amoprighttype => 'oid',
   amopstrategy => '1', amopopr => '=(oid,oid)', amopmethod => 'hash' },
 
+# oid8_ops
+{ amopfamily => 'hash/oid8_ops', amoplefttype => 'oid8',
+  amoprighttype => 'oid8', amopstrategy => '1', amopopr => '=(oid8,oid8)',
+  amopmethod => 'hash' },
+
 # oidvector_ops
 { amopfamily => 'hash/oidvector_ops', amoplefttype => 'oidvector',
   amoprighttype => 'oidvector', amopstrategy => '1',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index e3477500baa7..d3719b3610c4 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -213,6 +213,14 @@
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' },
+{ amprocfamily => 'btree/oid8_ops', amproclefttype => 'oid8',
+  amprocrighttype => 'oid8', amprocnum => '1', amproc => 'btoid8cmp' },
+{ amprocfamily => 'btree/oid8_ops', amproclefttype => 'oid8',
+  amprocrighttype => 'oid8', amprocnum => '2', amproc => 'btoid8sortsupport' },
+{ amprocfamily => 'btree/oid8_ops', amproclefttype => 'oid8',
+  amprocrighttype => 'oid8', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/oid8_ops', amproclefttype => 'oid8',
+  amprocrighttype => 'oid8', amprocnum => '6', amproc => 'btoid8skipsupport' },
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
@@ -432,6 +440,10 @@
   amprocrighttype => 'xid8', amprocnum => '1', amproc => 'hashxid8' },
 { amprocfamily => 'hash/xid8_ops', amproclefttype => 'xid8',
   amprocrighttype => 'xid8', amprocnum => '2', amproc => 'hashxid8extended' },
+{ amprocfamily => 'hash/oid8_ops', amproclefttype => 'oid8',
+  amprocrighttype => 'oid8', amprocnum => '1', amproc => 'hashoid8' },
+{ amprocfamily => 'hash/oid8_ops', amproclefttype => 'oid8',
+  amprocrighttype => 'oid8', amprocnum => '2', amproc => 'hashoid8extended' },
 { amprocfamily => 'hash/cid_ops', amproclefttype => 'cid',
   amprocrighttype => 'cid', amprocnum => '1', amproc => 'hashcid' },
 { amprocfamily => 'hash/cid_ops', amproclefttype => 'cid',
diff --git a/src/include/catalog/pg_cast.dat b/src/include/catalog/pg_cast.dat
index fbfd669587f0..695f6b2a5e73 100644
--- a/src/include/catalog/pg_cast.dat
+++ b/src/include/catalog/pg_cast.dat
@@ -296,6 +296,20 @@
 { castsource => 'regdatabase', casttarget => 'int4', castfunc => '0',
   castcontext => 'a', castmethod => 'b' },
 
+# OID8 category: allow implicit conversion from any integral type (including
+# int8), as well as assignment coercion to int8.
+{ castsource => 'int8', casttarget => 'oid8', castfunc => '0',
+  castcontext => 'i', castmethod => 'b' },
+{ castsource => 'int2', casttarget => 'oid8', castfunc => 'int8(int2)',
+  castcontext => 'i', castmethod => 'f' },
+{ castsource => 'int4', casttarget => 'oid8', castfunc => 'int8(int4)',
+  castcontext => 'i', castmethod => 'f' },
+{ castsource => 'oid8', casttarget => 'int8', castfunc => '0',
+  castcontext => 'a', castmethod => 'b' },
+# Assignment coercion from oid to oid8.
+{ castsource => 'oid', casttarget => 'oid8', castfunc => 'oid8(oid)',
+  castcontext => 'a', castmethod => 'f' },
+
 # String category
 { castsource => 'text', casttarget => 'bpchar', castfunc => '0',
   castcontext => 'i', castmethod => 'b' },
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 4a9624802aa5..c0de88fabc49 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -177,6 +177,10 @@
   opcintype => 'xid8' },
 { opcmethod => 'btree', opcname => 'xid8_ops', opcfamily => 'btree/xid8_ops',
   opcintype => 'xid8' },
+{ opcmethod => 'hash', opcname => 'oid8_ops', opcfamily => 'hash/oid8_ops',
+  opcintype => 'oid8' },
+{ opcmethod => 'btree', opcname => 'oid8_ops', opcfamily => 'btree/oid8_ops',
+  opcintype => 'oid8' },
 { opcmethod => 'hash', opcname => 'cid_ops', opcfamily => 'hash/cid_ops',
   opcintype => 'cid' },
 { opcmethod => 'hash', opcname => 'tid_ops', opcfamily => 'hash/tid_ops',
diff --git a/src/include/catalog/pg_operator.dat b/src/include/catalog/pg_operator.dat
index 6d9dc1528d6e..87a7255490a7 100644
--- a/src/include/catalog/pg_operator.dat
+++ b/src/include/catalog/pg_operator.dat
@@ -3460,4 +3460,30 @@
   oprcode => 'multirange_after_multirange', oprrest => 'multirangesel',
   oprjoin => 'scalargtjoinsel' },
 
+{ oid => '8262', descr => 'equal',
+  oprname => '=', oprcanmerge => 't', oprcanhash => 't', oprleft => 'oid8',
+  oprright => 'oid8', oprresult => 'bool', oprcom => '=(oid8,oid8)',
+  oprnegate => '<>(oid8,oid8)', oprcode => 'oid8eq', oprrest => 'eqsel',
+  oprjoin => 'eqjoinsel' },
+{ oid => '8263', descr => 'not equal',
+  oprname => '<>', oprleft => 'oid8', oprright => 'oid8', oprresult => 'bool',
+  oprcom => '<>(oid8,oid8)', oprnegate => '=(oid8,oid8)', oprcode => 'oid8ne',
+  oprrest => 'neqsel', oprjoin => 'neqjoinsel' },
+{ oid => '8264', descr => 'less than',
+  oprname => '<', oprleft => 'oid8', oprright => 'oid8', oprresult => 'bool',
+  oprcom => '>(oid8,oid8)', oprnegate => '>=(oid8,oid8)', oprcode => 'oid8lt',
+  oprrest => 'scalarltsel', oprjoin => 'scalarltjoinsel' },
+{ oid => '8265', descr => 'greater than',
+  oprname => '>', oprleft => 'oid8', oprright => 'oid8', oprresult => 'bool',
+  oprcom => '<(oid8,oid8)', oprnegate => '<=(oid8,oid8)', oprcode => 'oid8gt',
+  oprrest => 'scalargtsel', oprjoin => 'scalargtjoinsel' },
+{ oid => '8266', descr => 'less than or equal',
+  oprname => '<=', oprleft => 'oid8', oprright => 'oid8', oprresult => 'bool',
+  oprcom => '>=(oid8,oid8)', oprnegate => '>(oid8,oid8)', oprcode => 'oid8le',
+  oprrest => 'scalarlesel', oprjoin => 'scalarlejoinsel' },
+{ oid => '8267', descr => 'greater than or equal',
+  oprname => '>=', oprleft => 'oid8', oprright => 'oid8', oprresult => 'bool',
+  oprcom => '<=(oid8,oid8)', oprnegate => '<(oid8,oid8)', oprcode => 'oid8ge',
+  oprrest => 'scalargesel', oprjoin => 'scalargejoinsel' },
+
 ]
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index f7dcb96b43ce..54472ce97dcd 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -116,6 +116,10 @@
   opfmethod => 'hash', opfname => 'xid8_ops' },
 { oid => '5067',
   opfmethod => 'btree', opfname => 'xid8_ops' },
+{ oid => '8278',
+  opfmethod => 'hash', opfname => 'oid8_ops' },
+{ oid => '8279',
+  opfmethod => 'btree', opfname => 'oid8_ops' },
 { oid => '2226',
   opfmethod => 'hash', opfname => 'cid_ops' },
 { oid => '2227',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 01eba3b5a190..acca3f95b4e7 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1046,6 +1046,15 @@
 { oid => '6405', descr => 'skip support',
   proname => 'btoidskipsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btoidskipsupport' },
+{ oid => '8282', descr => 'less-equal-greater',
+  proname => 'btoid8cmp', proleakproof => 't', prorettype => 'int4',
+  proargtypes => 'oid8 oid8', prosrc => 'btoid8cmp' },
+{ oid => '8283', descr => 'sort support',
+  proname => 'btoid8sortsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoid8sortsupport' },
+{ oid => '8284', descr => 'skip support',
+  proname => 'btoid8skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoid8skipsupport' },
 { oid => '404', descr => 'less-equal-greater',
   proname => 'btoidvectorcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'oidvector oidvector', prosrc => 'btoidvectorcmp' },
@@ -12588,4 +12597,59 @@
   proargnames => '{pid,io_id,io_generation,state,operation,off,length,target,handle_data_len,raw_result,result,target_desc,f_sync,f_localmem,f_buffered}',
   prosrc => 'pg_get_aios' },
 
+# oid8 related functions
+{ oid => '8255', descr => 'convert oid to oid8',
+  proname => 'oid8', prorettype => 'oid8', proargtypes => 'oid',
+  prosrc => 'oidtooid8' },
+{ oid => '8257', descr => 'I/O',
+  proname => 'oid8in', prorettype => 'oid8', proargtypes => 'cstring',
+  prosrc => 'oid8in' },
+{ oid => '8258', descr => 'I/O',
+  proname => 'oid8out', prorettype => 'cstring', proargtypes => 'oid8',
+  prosrc => 'oid8out' },
+{ oid => '8259', descr => 'I/O',
+  proname => 'oid8recv', prorettype => 'oid8', proargtypes => 'internal',
+  prosrc => 'oid8recv' },
+{ oid => '8260', descr => 'I/O',
+  proname => 'oid8send', prorettype => 'bytea', proargtypes => 'oid8',
+  prosrc => 'oid8send' },
+# Comparators
+{ oid => '8268',
+  proname => 'oid8eq', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'oid8 oid8', prosrc => 'oid8eq' },
+{ oid => '8269',
+  proname => 'oid8ne', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'oid8 oid8', prosrc => 'oid8ne' },
+{ oid => '8270',
+  proname => 'oid8lt', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'oid8 oid8', prosrc => 'oid8lt' },
+{ oid => '8271',
+  proname => 'oid8le', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'oid8 oid8', prosrc => 'oid8le' },
+{ oid => '8272',
+  proname => 'oid8gt', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'oid8 oid8', prosrc => 'oid8gt' },
+{ oid => '8273',
+  proname => 'oid8ge', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'oid8 oid8', prosrc => 'oid8ge' },
+# Aggregates
+{ oid => '8274', descr => 'larger of two',
+  proname => 'oid8larger', prorettype => 'oid8', proargtypes => 'oid8 oid8',
+  prosrc => 'oid8larger' },
+{ oid => '8275', descr => 'smaller of two',
+  proname => 'oid8smaller', prorettype => 'oid8', proargtypes => 'oid8 oid8',
+  prosrc => 'oid8smaller' },
+{ oid => '8276', descr => 'maximum value of all oid8 input values',
+  proname => 'max', prokind => 'a', proisstrict => 'f', prorettype => 'oid8',
+  proargtypes => 'oid8', prosrc => 'aggregate_dummy' },
+{ oid => '8277', descr => 'minimum value of all oid8 input values',
+  proname => 'min', prokind => 'a', proisstrict => 'f', prorettype => 'oid8',
+  proargtypes => 'oid8', prosrc => 'aggregate_dummy' },
+{ oid => '8280', descr => 'hash',
+  proname => 'hashoid8', prorettype => 'int4', proargtypes => 'oid8',
+  prosrc => 'hashoid8' },
+{ oid => '8281', descr => 'hash',
+  proname => 'hashoid8extended', prorettype => 'int8',
+  proargtypes => 'oid8 int8', prosrc => 'hashoid8extended' },
+
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index cb730aeac864..704f2890cb28 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -700,4 +700,9 @@
   typreceive => 'brin_minmax_multi_summary_recv',
   typsend => 'brin_minmax_multi_summary_send', typalign => 'i',
   typstorage => 'x', typcollation => 'default' },
+{ oid => '8256', array_type_oid => '8261',
+  descr => 'object identifier(oid8), 8 bytes',
+  typname => 'oid8', typlen => '8', typbyval => 't',
+  typcategory => 'N', typinput => 'oid8in', typoutput => 'oid8out',
+  typreceive => 'oid8recv', typsend => 'oid8send', typalign => 'd' },
 ]
diff --git a/src/include/fmgr.h b/src/include/fmgr.h
index 74fe3ea05758..c127d2f87585 100644
--- a/src/include/fmgr.h
+++ b/src/include/fmgr.h
@@ -273,6 +273,7 @@ extern struct varlena *pg_detoast_datum_packed(struct varlena *datum);
 #define PG_GETARG_CHAR(n)	 DatumGetChar(PG_GETARG_DATUM(n))
 #define PG_GETARG_BOOL(n)	 DatumGetBool(PG_GETARG_DATUM(n))
 #define PG_GETARG_OID(n)	 DatumGetObjectId(PG_GETARG_DATUM(n))
+#define PG_GETARG_OID8(n)	 DatumGetObjectId8(PG_GETARG_DATUM(n))
 #define PG_GETARG_POINTER(n) DatumGetPointer(PG_GETARG_DATUM(n))
 #define PG_GETARG_CSTRING(n) DatumGetCString(PG_GETARG_DATUM(n))
 #define PG_GETARG_NAME(n)	 DatumGetName(PG_GETARG_DATUM(n))
@@ -358,6 +359,7 @@ extern struct varlena *pg_detoast_datum_packed(struct varlena *datum);
 #define PG_RETURN_CHAR(x)	 return CharGetDatum(x)
 #define PG_RETURN_BOOL(x)	 return BoolGetDatum(x)
 #define PG_RETURN_OID(x)	 return ObjectIdGetDatum(x)
+#define PG_RETURN_OID8(x)	 return ObjectId8GetDatum(x)
 #define PG_RETURN_POINTER(x) return PointerGetDatum(x)
 #define PG_RETURN_CSTRING(x) return CStringGetDatum(x)
 #define PG_RETURN_NAME(x)	 return NameGetDatum(x)
diff --git a/src/include/postgres.h b/src/include/postgres.h
index 357cbd6fd961..a5a0e3b7cbfa 100644
--- a/src/include/postgres.h
+++ b/src/include/postgres.h
@@ -264,6 +264,26 @@ ObjectIdGetDatum(Oid X)
 	return (Datum) X;
 }
 
+/*
+ * DatumGetObjectId8
+ *		Returns 8-byte object identifier value of a datum.
+ */
+static inline Oid8
+DatumGetObjectId8(Datum X)
+{
+	return (Oid8) X;
+}
+
+/*
+ * ObjectId8GetDatum
+ *		Returns datum representation for an 8-byte object identifier
+ */
+static inline Datum
+ObjectId8GetDatum(Oid8 X)
+{
+	return (Datum) X;
+}
+
 /*
  * DatumGetTransactionId
  *		Returns transaction identifier value of a datum.
diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c
index 188c27b4925f..3f59ba3f1ad0 100644
--- a/src/backend/access/nbtree/nbtcompare.c
+++ b/src/backend/access/nbtree/nbtcompare.c
@@ -498,6 +498,88 @@ btoidskipsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+Datum
+btoid8cmp(PG_FUNCTION_ARGS)
+{
+	Oid8		a = PG_GETARG_OID8(0);
+	Oid8		b = PG_GETARG_OID8(1);
+
+	if (a > b)
+		PG_RETURN_INT32(A_GREATER_THAN_B);
+	else if (a == b)
+		PG_RETURN_INT32(0);
+	else
+		PG_RETURN_INT32(A_LESS_THAN_B);
+}
+
+static int
+btoid8fastcmp(Datum x, Datum y, SortSupport ssup)
+{
+	Oid8		a = DatumGetObjectId8(x);
+	Oid8		b = DatumGetObjectId8(y);
+
+	if (a > b)
+		return A_GREATER_THAN_B;
+	else if (a == b)
+		return 0;
+	else
+		return A_LESS_THAN_B;
+}
+
+Datum
+btoid8sortsupport(PG_FUNCTION_ARGS)
+{
+	SortSupport ssup = (SortSupport) PG_GETARG_POINTER(0);
+
+	ssup->comparator = btoid8fastcmp;
+	PG_RETURN_VOID();
+}
+
+static Datum
+oid8_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Oid8		oexisting = DatumGetObjectId8(existing);
+
+	if (oexisting == InvalidOid8)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return ObjectId8GetDatum(oexisting - 1);
+}
+
+static Datum
+oid8_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Oid8		oexisting = DatumGetObjectId8(existing);
+
+	if (oexisting == OID8_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return ObjectId8GetDatum(oexisting + 1);
+}
+
+Datum
+btoid8skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = oid8_decrement;
+	sksup->increment = oid8_increment;
+	sksup->low_elem = ObjectId8GetDatum(InvalidOid8);
+	sksup->high_elem = ObjectId8GetDatum(OID8_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btoidvectorcmp(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/bootstrap/bootstrap.c b/src/backend/bootstrap/bootstrap.c
index fc8638c1b61b..48e6966e6b48 100644
--- a/src/backend/bootstrap/bootstrap.c
+++ b/src/backend/bootstrap/bootstrap.c
@@ -115,6 +115,8 @@ static const struct typinfo TypInfo[] = {
 	F_TEXTIN, F_TEXTOUT},
 	{"oid", OIDOID, 0, 4, true, TYPALIGN_INT, TYPSTORAGE_PLAIN, InvalidOid,
 	F_OIDIN, F_OIDOUT},
+	{"oid8", OID8OID, 0, 8, true, TYPALIGN_DOUBLE, TYPSTORAGE_PLAIN, InvalidOid,
+	F_OID8IN, F_OID8OUT},
 	{"tid", TIDOID, 0, 6, false, TYPALIGN_SHORT, TYPSTORAGE_PLAIN, InvalidOid,
 	F_TIDIN, F_TIDOUT},
 	{"xid", XIDOID, 0, 4, true, TYPALIGN_INT, TYPSTORAGE_PLAIN, InvalidOid,
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index cc68ac545a5f..2b1bfcea516c 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -77,6 +77,7 @@ OBJS = \
 	numeric.o \
 	numutils.o \
 	oid.o \
+	oid8.o \
 	oracle_compat.o \
 	orderedsetaggs.o \
 	partitionfuncs.o \
diff --git a/src/backend/utils/adt/int8.c b/src/backend/utils/adt/int8.c
index bdea490202a6..9f7466e47b79 100644
--- a/src/backend/utils/adt/int8.c
+++ b/src/backend/utils/adt/int8.c
@@ -1323,6 +1323,14 @@ oidtoi8(PG_FUNCTION_ARGS)
 	PG_RETURN_INT64((int64) arg);
 }
 
+Datum
+oidtooid8(PG_FUNCTION_ARGS)
+{
+	Oid			arg = PG_GETARG_OID(0);
+
+	PG_RETURN_OID8((Oid8) arg);
+}
+
 /*
  * non-persistent numeric series generator
  */
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index 12fa0c209127..bd798b3e1236 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -73,6 +73,7 @@ backend_sources += files(
   'network_spgist.c',
   'numutils.c',
   'oid.c',
+  'oid8.c',
   'oracle_compat.c',
   'orderedsetaggs.c',
   'partitionfuncs.c',
diff --git a/src/backend/utils/adt/oid8.c b/src/backend/utils/adt/oid8.c
new file mode 100644
index 000000000000..6e9ffd96303f
--- /dev/null
+++ b/src/backend/utils/adt/oid8.c
@@ -0,0 +1,171 @@
+/*-------------------------------------------------------------------------
+ *
+ * oid8.c
+ *	  Functions for the built-in type Oid8
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/oid8.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include <ctype.h>
+#include <limits.h>
+
+#include "catalog/pg_type.h"
+#include "libpq/pqformat.h"
+#include "utils/builtins.h"
+
+#define MAXOID8LEN 20
+
+/*****************************************************************************
+ *	 USER I/O ROUTINES														 *
+ *****************************************************************************/
+
+Datum
+oid8in(PG_FUNCTION_ARGS)
+{
+	char	   *s = PG_GETARG_CSTRING(0);
+	Oid8		result;
+
+	result = uint64in_subr(s, NULL, "oid8", fcinfo->context);
+	PG_RETURN_OID8(result);
+}
+
+Datum
+oid8out(PG_FUNCTION_ARGS)
+{
+	Oid8		val = PG_GETARG_OID8(0);
+	char		buf[MAXOID8LEN + 1];
+	char	   *result;
+	int			len;
+
+	len = pg_ulltoa_n(val, buf) + 1;
+	buf[len - 1] = '\0';
+
+	/*
+	 * Since the length is already known, we do a manual palloc() and memcpy()
+	 * to avoid the strlen() call that would otherwise be done in pstrdup().
+	 */
+	result = palloc(len);
+	memcpy(result, buf, len);
+	PG_RETURN_CSTRING(result);
+}
+
+/*
+ *		oid8recv			- converts external binary format to oid8
+ */
+Datum
+oid8recv(PG_FUNCTION_ARGS)
+{
+	StringInfo	buf = (StringInfo) PG_GETARG_POINTER(0);
+
+	PG_RETURN_OID8(pq_getmsgint64(buf));
+}
+
+/*
+ *		oid8send			- converts oid8 to binary format
+ */
+Datum
+oid8send(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	StringInfoData buf;
+
+	pq_begintypsend(&buf);
+	pq_sendint64(&buf, arg1);
+	PG_RETURN_BYTEA_P(pq_endtypsend(&buf));
+}
+
+/*****************************************************************************
+ *	 PUBLIC ROUTINES														 *
+ *****************************************************************************/
+
+Datum
+oid8eq(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	Oid8		arg2 = PG_GETARG_OID8(1);
+
+	PG_RETURN_BOOL(arg1 == arg2);
+}
+
+Datum
+oid8ne(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	Oid8		arg2 = PG_GETARG_OID8(1);
+
+	PG_RETURN_BOOL(arg1 != arg2);
+}
+
+Datum
+oid8lt(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	Oid8		arg2 = PG_GETARG_OID8(1);
+
+	PG_RETURN_BOOL(arg1 < arg2);
+}
+
+Datum
+oid8le(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	Oid8		arg2 = PG_GETARG_OID8(1);
+
+	PG_RETURN_BOOL(arg1 <= arg2);
+}
+
+Datum
+oid8ge(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	Oid8		arg2 = PG_GETARG_OID8(1);
+
+	PG_RETURN_BOOL(arg1 >= arg2);
+}
+
+Datum
+oid8gt(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	Oid8		arg2 = PG_GETARG_OID8(1);
+
+	PG_RETURN_BOOL(arg1 > arg2);
+}
+
+Datum
+hashoid8(PG_FUNCTION_ARGS)
+{
+	return hashint8(fcinfo);
+}
+
+Datum
+hashoid8extended(PG_FUNCTION_ARGS)
+{
+	return hashint8extended(fcinfo);
+}
+
+Datum
+oid8larger(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	Oid8		arg2 = PG_GETARG_OID8(1);
+
+	PG_RETURN_OID8((arg1 > arg2) ? arg1 : arg2);
+}
+
+Datum
+oid8smaller(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	Oid8		arg2 = PG_GETARG_OID8(1);
+
+	PG_RETURN_OID8((arg1 < arg2) ? arg1 : arg2);
+}
diff --git a/src/fe_utils/print.c b/src/fe_utils/print.c
index 4af0f32f2fc0..221624707892 100644
--- a/src/fe_utils/print.c
+++ b/src/fe_utils/print.c
@@ -3624,6 +3624,7 @@ column_type_alignment(Oid ftype)
 		case FLOAT8OID:
 		case NUMERICOID:
 		case OIDOID:
+		case OID8OID:
 		case XIDOID:
 		case XID8OID:
 		case CIDOID:
diff --git a/src/test/regress/expected/oid8.out b/src/test/regress/expected/oid8.out
new file mode 100644
index 000000000000..80529214ca53
--- /dev/null
+++ b/src/test/regress/expected/oid8.out
@@ -0,0 +1,196 @@
+--
+-- OID8
+--
+CREATE TABLE OID8_TBL(f1 oid8);
+INSERT INTO OID8_TBL(f1) VALUES ('1234');
+INSERT INTO OID8_TBL(f1) VALUES ('1235');
+INSERT INTO OID8_TBL(f1) VALUES ('987');
+INSERT INTO OID8_TBL(f1) VALUES ('-1040');
+INSERT INTO OID8_TBL(f1) VALUES ('99999999');
+INSERT INTO OID8_TBL(f1) VALUES ('5     ');
+INSERT INTO OID8_TBL(f1) VALUES ('   10  ');
+-- leading/trailing hard tab is also allowed
+INSERT INTO OID8_TBL(f1) VALUES ('	  15 	  ');
+-- bad inputs
+INSERT INTO OID8_TBL(f1) VALUES ('');
+ERROR:  invalid input syntax for type oid8: ""
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('');
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES ('    ');
+ERROR:  invalid input syntax for type oid8: "    "
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('    ');
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES ('asdfasd');
+ERROR:  invalid input syntax for type oid8: "asdfasd"
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('asdfasd');
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES ('99asdfasd');
+ERROR:  invalid input syntax for type oid8: "99asdfasd"
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('99asdfasd');
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES ('5    d');
+ERROR:  invalid input syntax for type oid8: "5    d"
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('5    d');
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES ('    5d');
+ERROR:  invalid input syntax for type oid8: "    5d"
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('    5d');
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES ('5    5');
+ERROR:  invalid input syntax for type oid8: "5    5"
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('5    5');
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES (' - 500');
+ERROR:  invalid input syntax for type oid8: " - 500"
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES (' - 500');
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES ('3908203590239580293850293850329485');
+ERROR:  value "3908203590239580293850293850329485" is out of range for type oid8
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('39082035902395802938502938...
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES ('-1204982019841029840928340329840934');
+ERROR:  value "-1204982019841029840928340329840934" is out of range for type oid8
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('-1204982019841029840928340...
+                                         ^
+SELECT * FROM OID8_TBL;
+          f1          
+----------------------
+                 1234
+                 1235
+                  987
+ 18446744073709550576
+             99999999
+                    5
+                   10
+                   15
+(8 rows)
+
+-- Also try it with non-error-throwing API
+SELECT pg_input_is_valid('1234', 'oid8');
+ pg_input_is_valid 
+-------------------
+ t
+(1 row)
+
+SELECT pg_input_is_valid('01XYZ', 'oid8');
+ pg_input_is_valid 
+-------------------
+ f
+(1 row)
+
+SELECT * FROM pg_input_error_info('01XYZ', 'oid8');
+                   message                   | detail | hint | sql_error_code 
+---------------------------------------------+--------+------+----------------
+ invalid input syntax for type oid8: "01XYZ" |        |      | 22P02
+(1 row)
+
+SELECT pg_input_is_valid('3908203590239580293850293850329485', 'oid8');
+ pg_input_is_valid 
+-------------------
+ f
+(1 row)
+
+SELECT * FROM pg_input_error_info('-1204982019841029840928340329840934', 'oid8');
+                                  message                                  | detail | hint | sql_error_code 
+---------------------------------------------------------------------------+--------+------+----------------
+ value "-1204982019841029840928340329840934" is out of range for type oid8 |        |      | 22003
+(1 row)
+
+-- Operators
+SELECT o.* FROM OID8_TBL o WHERE o.f1 = 1234;
+  f1  
+------
+ 1234
+(1 row)
+
+SELECT o.* FROM OID8_TBL o WHERE o.f1 <> '1234';
+          f1          
+----------------------
+                 1235
+                  987
+ 18446744073709550576
+             99999999
+                    5
+                   10
+                   15
+(7 rows)
+
+SELECT o.* FROM OID8_TBL o WHERE o.f1 <= '1234';
+  f1  
+------
+ 1234
+  987
+    5
+   10
+   15
+(5 rows)
+
+SELECT o.* FROM OID8_TBL o WHERE o.f1 < '1234';
+ f1  
+-----
+ 987
+   5
+  10
+  15
+(4 rows)
+
+SELECT o.* FROM OID8_TBL o WHERE o.f1 >= '1234';
+          f1          
+----------------------
+                 1234
+                 1235
+ 18446744073709550576
+             99999999
+(4 rows)
+
+SELECT o.* FROM OID8_TBL o WHERE o.f1 > '1234';
+          f1          
+----------------------
+                 1235
+ 18446744073709550576
+             99999999
+(3 rows)
+
+-- Casts
+SELECT 1::int2::oid8;
+ oid8 
+------
+    1
+(1 row)
+
+SELECT 1::int4::oid8;
+ oid8 
+------
+    1
+(1 row)
+
+SELECT 1::int8::oid8;
+ oid8 
+------
+    1
+(1 row)
+
+SELECT 1::oid8::int8;
+ int8 
+------
+    1
+(1 row)
+
+SELECT 1::oid::oid8; -- ok
+ oid8 
+------
+    1
+(1 row)
+
+SELECT 1::oid8::oid; -- not ok
+ERROR:  cannot cast type oid8 to oid
+LINE 1: SELECT 1::oid8::oid;
+                      ^
+-- Aggregates
+SELECT min(f1), max(f1) FROM OID8_TBL;
+ min |         max          
+-----+----------------------
+   5 | 18446744073709550576
+(1 row)
+
+DROP TABLE OID8_TBL;
diff --git a/src/test/regress/expected/oid8.sql b/src/test/regress/expected/oid8.sql
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 20bf9ea9cdf7..1b2a1641029d 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -880,6 +880,13 @@ bytea(integer)
 bytea(bigint)
 bytea_larger(bytea,bytea)
 bytea_smaller(bytea,bytea)
+oid8eq(oid8,oid8)
+oid8ne(oid8,oid8)
+oid8lt(oid8,oid8)
+oid8le(oid8,oid8)
+oid8gt(oid8,oid8)
+oid8ge(oid8,oid8)
+btoid8cmp(oid8,oid8)
 -- Check that functions without argument are not marked as leakproof.
 SELECT p1.oid::regprocedure
 FROM pg_proc p1 JOIN pg_namespace pn
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 943e56506bf1..9ddcacec6bf4 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -702,6 +702,7 @@ CREATE TABLE tab_core_types AS SELECT
   'abc'::refcursor,
   '1 2'::int2vector,
   '1 2'::oidvector,
+  '1234'::oid8,
   format('%I=UC/%I', USER, USER)::aclitem AS aclitem,
   'a fat cat sat on a mat and ate a fat rat'::tsvector,
   'fat & rat'::tsquery,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index fbffc67ae601..56e129ce4aa0 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -28,7 +28,7 @@ test: strings md5 numerology point lseg line box path polygon circle date time t
 # geometry depends on point, lseg, line, box, path, polygon, circle
 # horology depends on date, time, timetz, timestamp, timestamptz, interval
 # ----------
-test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import oid8
 
 # ----------
 # Load huge amounts of data
diff --git a/src/test/regress/sql/oid8.sql b/src/test/regress/sql/oid8.sql
new file mode 100644
index 000000000000..c4f2ae6a2e57
--- /dev/null
+++ b/src/test/regress/sql/oid8.sql
@@ -0,0 +1,57 @@
+--
+-- OID8
+--
+
+CREATE TABLE OID8_TBL(f1 oid8);
+
+INSERT INTO OID8_TBL(f1) VALUES ('1234');
+INSERT INTO OID8_TBL(f1) VALUES ('1235');
+INSERT INTO OID8_TBL(f1) VALUES ('987');
+INSERT INTO OID8_TBL(f1) VALUES ('-1040');
+INSERT INTO OID8_TBL(f1) VALUES ('99999999');
+INSERT INTO OID8_TBL(f1) VALUES ('5     ');
+INSERT INTO OID8_TBL(f1) VALUES ('   10  ');
+-- leading/trailing hard tab is also allowed
+INSERT INTO OID8_TBL(f1) VALUES ('	  15 	  ');
+
+-- bad inputs
+INSERT INTO OID8_TBL(f1) VALUES ('');
+INSERT INTO OID8_TBL(f1) VALUES ('    ');
+INSERT INTO OID8_TBL(f1) VALUES ('asdfasd');
+INSERT INTO OID8_TBL(f1) VALUES ('99asdfasd');
+INSERT INTO OID8_TBL(f1) VALUES ('5    d');
+INSERT INTO OID8_TBL(f1) VALUES ('    5d');
+INSERT INTO OID8_TBL(f1) VALUES ('5    5');
+INSERT INTO OID8_TBL(f1) VALUES (' - 500');
+INSERT INTO OID8_TBL(f1) VALUES ('3908203590239580293850293850329485');
+INSERT INTO OID8_TBL(f1) VALUES ('-1204982019841029840928340329840934');
+
+SELECT * FROM OID8_TBL;
+
+-- Also try it with non-error-throwing API
+SELECT pg_input_is_valid('1234', 'oid8');
+SELECT pg_input_is_valid('01XYZ', 'oid8');
+SELECT * FROM pg_input_error_info('01XYZ', 'oid8');
+SELECT pg_input_is_valid('3908203590239580293850293850329485', 'oid8');
+SELECT * FROM pg_input_error_info('-1204982019841029840928340329840934', 'oid8');
+
+-- Operators
+SELECT o.* FROM OID8_TBL o WHERE o.f1 = 1234;
+SELECT o.* FROM OID8_TBL o WHERE o.f1 <> '1234';
+SELECT o.* FROM OID8_TBL o WHERE o.f1 <= '1234';
+SELECT o.* FROM OID8_TBL o WHERE o.f1 < '1234';
+SELECT o.* FROM OID8_TBL o WHERE o.f1 >= '1234';
+SELECT o.* FROM OID8_TBL o WHERE o.f1 > '1234';
+
+-- Casts
+SELECT 1::int2::oid8;
+SELECT 1::int4::oid8;
+SELECT 1::int8::oid8;
+SELECT 1::oid8::int8;
+SELECT 1::oid::oid8; -- ok
+SELECT 1::oid8::oid; -- not ok
+
+-- Aggregates
+SELECT min(f1), max(f1) FROM OID8_TBL;
+
+DROP TABLE OID8_TBL;
diff --git a/src/test/regress/sql/type_sanity.sql b/src/test/regress/sql/type_sanity.sql
index df795759bb4c..c2496823d90e 100644
--- a/src/test/regress/sql/type_sanity.sql
+++ b/src/test/regress/sql/type_sanity.sql
@@ -530,6 +530,7 @@ CREATE TABLE tab_core_types AS SELECT
   'abc'::refcursor,
   '1 2'::int2vector,
   '1 2'::oidvector,
+  '1234'::oid8,
   format('%I=UC/%I', USER, USER)::aclitem AS aclitem,
   'a fat cat sat on a mat and ate a fat rat'::tsvector,
   'fat & rat'::tsquery,
diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml
index b81d89e26080..66c6aa7f349a 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -4723,6 +4723,10 @@ INSERT INTO mytable VALUES(-1);  -- fails
     <primary>oid</primary>
    </indexterm>
 
+   <indexterm zone="datatype-oid">
+    <primary>oid8</primary>
+   </indexterm>
+
    <indexterm zone="datatype-oid">
     <primary>regclass</primary>
    </indexterm>
@@ -4805,6 +4809,13 @@ INSERT INTO mytable VALUES(-1);  -- fails
     individual tables.
    </para>
 
+   <para>
+    In some contexts, a 64-bit variant <type>oid8</type> is used.
+    It is implemented as an unsigned eight-byte integer. Unlike its
+    <type>oid</type> counterpart, it can ensure uniqueness in large
+    individual tables.
+   </para>
+
    <para>
     The <type>oid</type> type itself has few operations beyond comparison.
     It can be cast to integer, however, and then manipulated using the
diff --git a/doc/src/sgml/func/func-aggregate.sgml b/doc/src/sgml/func/func-aggregate.sgml
index f50b692516b6..a5396048adf3 100644
--- a/doc/src/sgml/func/func-aggregate.sgml
+++ b/doc/src/sgml/func/func-aggregate.sgml
@@ -508,8 +508,8 @@
         Computes the maximum of the non-null input
         values.  Available for any numeric, string, date/time, or enum type,
         as well as <type>bytea</type>, <type>inet</type>, <type>interval</type>,
-        <type>money</type>, <type>oid</type>, <type>pg_lsn</type>,
-        <type>tid</type>, <type>xid8</type>,
+        <type>money</type>, <type>oid</type>, <type>oid8</type>,
+        <type>pg_lsn</type>, <type>tid</type>, <type>xid8</type>,
         and also arrays and composite types containing sortable data types.
        </para></entry>
        <entry>Yes</entry>
@@ -527,8 +527,8 @@
         Computes the minimum of the non-null input
         values.  Available for any numeric, string, date/time, or enum type,
         as well as <type>bytea</type>, <type>inet</type>, <type>interval</type>,
-        <type>money</type>, <type>oid</type>, <type>pg_lsn</type>,
-        <type>tid</type>, <type>xid8</type>,
+        <type>money</type>, <type>oid</type>, <type>oid8</type>,
+        <type>pg_lsn</type>, <type>tid</type>, <type>xid8</type>,
         and also arrays and composite types containing sortable data types.
        </para></entry>
        <entry>Yes</entry>
-- 
2.51.0

v7-0002-Refactor-some-TOAST-value-ID-code-to-use-Oid8-ins.patchtext/x-diff; charset=us-asciiDownload
From 4472b74f2bfd359ab97b3b8a826c3a73dfab19d4 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 13 Aug 2025 17:26:36 +0900
Subject: [PATCH v7 02/15] Refactor some TOAST value ID code to use Oid8
 instead of Oid

This change is a mechanical switch to change most of the code paths that
assume TOAST value IDs to be Oids to become Oid8, easing an upcoming
change to allow larger TOAST values, at 8 bytes.

The areas touched are related to table AM, amcheck and logical
decoding's reorder buffer.  A good chunk of the changes involve
switching printf() markers from %u to OID8_FORMAT.
---
 src/include/access/heaptoast.h                |  2 +-
 src/include/access/tableam.h                  |  4 +-
 src/backend/access/common/toast_internals.c   |  8 +--
 src/backend/access/heap/heaptoast.c           | 12 ++--
 .../replication/logical/reorderbuffer.c       | 14 +++--
 contrib/amcheck/verify_heapam.c               | 56 +++++++++++--------
 6 files changed, 53 insertions(+), 43 deletions(-)

diff --git a/src/include/access/heaptoast.h b/src/include/access/heaptoast.h
index 6385a27caf83..fdc8d00d7099 100644
--- a/src/include/access/heaptoast.h
+++ b/src/include/access/heaptoast.h
@@ -142,7 +142,7 @@ extern HeapTuple toast_build_flattened_tuple(TupleDesc tupleDesc,
  *	Fetch a slice from a toast value stored in a heap table.
  * ----------
  */
-extern void heap_fetch_toast_slice(Relation toastrel, Oid valueid,
+extern void heap_fetch_toast_slice(Relation toastrel, Oid8 valueid,
 								   int32 attrsize, int32 sliceoffset,
 								   int32 slicelength, struct varlena *result);
 
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index e16bf0256928..068861a6f1c1 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -746,7 +746,7 @@ typedef struct TableAmRoutine
 	 * table implemented by this AM.  See table_relation_fetch_toast_slice()
 	 * for more details.
 	 */
-	void		(*relation_fetch_toast_slice) (Relation toastrel, Oid valueid,
+	void		(*relation_fetch_toast_slice) (Relation toastrel, Oid8 valueid,
 											   int32 attrsize,
 											   int32 sliceoffset,
 											   int32 slicelength,
@@ -1882,7 +1882,7 @@ table_relation_toast_am(Relation rel)
  * stored.
  */
 static inline void
-table_relation_fetch_toast_slice(Relation toastrel, Oid valueid,
+table_relation_fetch_toast_slice(Relation toastrel, Oid8 valueid,
 								 int32 attrsize, int32 sliceoffset,
 								 int32 slicelength, struct varlena *result)
 {
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 81dbd67c7258..420263691cc3 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -26,8 +26,8 @@
 #include "utils/rel.h"
 #include "utils/snapmgr.h"
 
-static bool toastrel_valueid_exists(Relation toastrel, Oid valueid);
-static bool toastid_valueid_exists(Oid toastrelid, Oid valueid);
+static bool toastrel_valueid_exists(Relation toastrel, Oid8 valueid);
+static bool toastid_valueid_exists(Oid toastrelid, Oid8 valueid);
 
 /* ----------
  * toast_compress_datum -
@@ -449,7 +449,7 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
  * ----------
  */
 static bool
-toastrel_valueid_exists(Relation toastrel, Oid valueid)
+toastrel_valueid_exists(Relation toastrel, Oid8 valueid)
 {
 	bool		result = false;
 	ScanKeyData toastkey;
@@ -497,7 +497,7 @@ toastrel_valueid_exists(Relation toastrel, Oid valueid)
  * ----------
  */
 static bool
-toastid_valueid_exists(Oid toastrelid, Oid valueid)
+toastid_valueid_exists(Oid toastrelid, Oid8 valueid)
 {
 	bool		result;
 	Relation	toastrel;
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index cb1e57030f64..d4b600de3aca 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -623,7 +623,7 @@ toast_build_flattened_tuple(TupleDesc tupleDesc,
  * result is the varlena into which the results should be written.
  */
 void
-heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
+heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 					   int32 sliceoffset, int32 slicelength,
 					   struct varlena *result)
 {
@@ -725,7 +725,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
 		else
 		{
 			/* should never happen */
-			elog(ERROR, "found toasted toast chunk for toast value %u in %s",
+			elog(ERROR, "found toasted toast chunk for toast value " OID8_FORMAT " in %s",
 				 valueid, RelationGetRelationName(toastrel));
 			chunksize = 0;		/* keep compiler quiet */
 			chunkdata = NULL;
@@ -737,13 +737,13 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
 		if (curchunk != expectedchunk)
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
-					 errmsg_internal("unexpected chunk number %d (expected %d) for toast value %u in %s",
+					 errmsg_internal("unexpected chunk number %d (expected %d) for toast value " OID8_FORMAT " in %s",
 									 curchunk, expectedchunk, valueid,
 									 RelationGetRelationName(toastrel))));
 		if (curchunk > endchunk)
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
-					 errmsg_internal("unexpected chunk number %d (out of range %d..%d) for toast value %u in %s",
+					 errmsg_internal("unexpected chunk number %d (out of range %d..%d) for toast value " OID8_FORMAT " in %s",
 									 curchunk,
 									 startchunk, endchunk, valueid,
 									 RelationGetRelationName(toastrel))));
@@ -752,7 +752,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
 		if (chunksize != expected_size)
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
-					 errmsg_internal("unexpected chunk size %d (expected %d) in chunk %d of %d for toast value %u in %s",
+					 errmsg_internal("unexpected chunk size %d (expected %d) in chunk %d of %d for toast value " OID8_FORMAT " in %s",
 									 chunksize, expected_size,
 									 curchunk, totalchunks, valueid,
 									 RelationGetRelationName(toastrel))));
@@ -781,7 +781,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
 	if (expectedchunk != (endchunk + 1))
 		ereport(ERROR,
 				(errcode(ERRCODE_DATA_CORRUPTED),
-				 errmsg_internal("missing chunk number %d for toast value %u in %s",
+				 errmsg_internal("missing chunk number %d for toast value " OID8_FORMAT " in %s",
 								 expectedchunk, valueid,
 								 RelationGetRelationName(toastrel))));
 
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 4736f993c374..a1cf30a8f2eb 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -176,7 +176,7 @@ typedef struct ReorderBufferIterTXNState
 /* toast datastructures */
 typedef struct ReorderBufferToastEnt
 {
-	Oid			chunk_id;		/* toast_table.chunk_id */
+	Oid8		chunk_id;		/* toast_table.chunk_id */
 	int32		last_chunk_seq; /* toast_table.chunk_seq of the last chunk we
 								 * have seen */
 	Size		num_chunks;		/* number of chunks we've already seen */
@@ -4959,7 +4959,7 @@ ReorderBufferToastInitHash(ReorderBuffer *rb, ReorderBufferTXN *txn)
 
 	Assert(txn->toast_hash == NULL);
 
-	hash_ctl.keysize = sizeof(Oid);
+	hash_ctl.keysize = sizeof(Oid8);
 	hash_ctl.entrysize = sizeof(ReorderBufferToastEnt);
 	hash_ctl.hcxt = rb->context;
 	txn->toast_hash = hash_create("ReorderBufferToastHash", 5, &hash_ctl,
@@ -4983,7 +4983,7 @@ ReorderBufferToastAppendChunk(ReorderBuffer *rb, ReorderBufferTXN *txn,
 	bool		isnull;
 	Pointer		chunk;
 	TupleDesc	desc = RelationGetDescr(relation);
-	Oid			chunk_id;
+	Oid8		chunk_id;
 	int32		chunk_seq;
 
 	if (txn->toast_hash == NULL)
@@ -5010,11 +5010,11 @@ ReorderBufferToastAppendChunk(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		dlist_init(&ent->chunks);
 
 		if (chunk_seq != 0)
-			elog(ERROR, "got sequence entry %d for toast chunk %u instead of seq 0",
+			elog(ERROR, "got sequence entry %d for toast chunk " OID8_FORMAT " instead of seq 0",
 				 chunk_seq, chunk_id);
 	}
 	else if (found && chunk_seq != ent->last_chunk_seq + 1)
-		elog(ERROR, "got sequence entry %d for toast chunk %u instead of seq %d",
+		elog(ERROR, "got sequence entry %d for toast chunk " OID8_FORMAT " instead of seq %d",
 			 chunk_seq, chunk_id, ent->last_chunk_seq + 1);
 
 	chunk = DatumGetPointer(fastgetattr(newtup, 3, desc, &isnull));
@@ -5123,6 +5123,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		struct varlena *reconstructed;
 		dlist_iter	it;
 		Size		data_done = 0;
+		Oid8		toast_valueid;
 
 		/* system columns aren't toasted */
 		if (attr->attnum < 0)
@@ -5147,13 +5148,14 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 			continue;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, varlena);
+		toast_valueid = toast_pointer.va_valueid;
 
 		/*
 		 * Check whether the toast tuple changed, replace if so.
 		 */
 		ent = (ReorderBufferToastEnt *)
 			hash_search(txn->toast_hash,
-						&toast_pointer.va_valueid,
+						&toast_valueid,
 						HASH_FIND,
 						NULL);
 		if (ent == NULL)
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 4963e9245cb5..eb353c40249e 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1561,6 +1561,9 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	bool		isnull;
 	int32		chunksize;
 	int32		expected_size;
+	Oid8		toast_valueid;
+
+	toast_valueid = ta->toast_pointer.va_valueid;
 
 	/* Sanity-check the sequence number. */
 	chunk_seq = DatumGetInt32(fastgetattr(toasttup, 2,
@@ -1568,16 +1571,16 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	if (isnull)
 	{
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u has toast chunk with null sequence number",
-										 ta->toast_pointer.va_valueid));
+								psprintf("toast value " OID8_FORMAT " has toast chunk with null sequence number",
+										 toast_valueid));
 		return;
 	}
 	if (chunk_seq != *expected_chunk_seq)
 	{
 		/* Either the TOAST index is corrupt, or we don't have all chunks. */
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u index scan returned chunk %d when expecting chunk %d",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value " OID8_FORMAT " index scan returned chunk %d when expecting chunk %d",
+										 toast_valueid,
 										 chunk_seq, *expected_chunk_seq));
 	}
 	*expected_chunk_seq = chunk_seq + 1;
@@ -1588,8 +1591,8 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	if (isnull)
 	{
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u chunk %d has null data",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value " OID8_FORMAT " chunk %d has null data",
+										 toast_valueid,
 										 chunk_seq));
 		return;
 	}
@@ -1608,8 +1611,8 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 		uint32		header = ((varattrib_4b *) chunk)->va_4byte.va_header;
 
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u chunk %d has invalid varlena header %0x",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value " OID8_FORMAT " chunk %d has invalid varlena header %0x",
+										 toast_valueid,
 										 chunk_seq, header));
 		return;
 	}
@@ -1620,8 +1623,8 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	if (chunk_seq > last_chunk_seq)
 	{
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u chunk %d follows last expected chunk %d",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value " OID8_FORMAT " chunk %d follows last expected chunk %d",
+										 toast_valueid,
 										 chunk_seq, last_chunk_seq));
 		return;
 	}
@@ -1631,8 +1634,8 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 
 	if (chunksize != expected_size)
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u chunk %d has size %u, but expected size %u",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value " OID8_FORMAT " chunk %d has size %u, but expected size %u",
+										 toast_valueid,
 										 chunk_seq, chunksize, expected_size));
 }
 
@@ -1663,6 +1666,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	struct varlena *attr;
 	char	   *tp;				/* pointer to the tuple data */
 	uint16		infomask;
+	Oid8		toast_pointer_valueid;
 	CompactAttribute *thisatt;
 	struct varatt_external toast_pointer;
 
@@ -1771,12 +1775,13 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	 * Must copy attr into toast_pointer for alignment considerations
 	 */
 	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_pointer_valueid = toast_pointer.va_valueid;
 
 	/* Toasted attributes too large to be untoasted should never be stored */
 	if (toast_pointer.va_rawsize > VARLENA_SIZE_LIMIT)
 		report_corruption(ctx,
-						  psprintf("toast value %u rawsize %d exceeds limit %d",
-								   toast_pointer.va_valueid,
+						  psprintf("toast value " OID8_FORMAT " rawsize %d exceeds limit %d",
+								   toast_pointer_valueid,
 								   toast_pointer.va_rawsize,
 								   VARLENA_SIZE_LIMIT));
 
@@ -1803,16 +1808,16 @@ check_tuple_attribute(HeapCheckContext *ctx)
 		}
 		if (!valid)
 			report_corruption(ctx,
-							  psprintf("toast value %u has invalid compression method id %d",
-									   toast_pointer.va_valueid, cmid));
+							  psprintf("toast value " OID8_FORMAT " has invalid compression method id %d",
+									   toast_pointer_valueid, cmid));
 	}
 
 	/* The tuple header better claim to contain toasted values */
 	if (!(infomask & HEAP_HASEXTERNAL))
 	{
 		report_corruption(ctx,
-						  psprintf("toast value %u is external but tuple header flag HEAP_HASEXTERNAL not set",
-								   toast_pointer.va_valueid));
+						  psprintf("toast value " OID8_FORMAT " is external but tuple header flag HEAP_HASEXTERNAL not set",
+								   toast_pointer_valueid));
 		return true;
 	}
 
@@ -1820,8 +1825,8 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	if (!ctx->rel->rd_rel->reltoastrelid)
 	{
 		report_corruption(ctx,
-						  psprintf("toast value %u is external but relation has no toast relation",
-								   toast_pointer.va_valueid));
+						  psprintf("toast value " OID8_FORMAT " is external but relation has no toast relation",
+								   toast_pointer_valueid));
 		return true;
 	}
 
@@ -1866,6 +1871,7 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	uint32		extsize;
 	int32		expected_chunk_seq = 0;
 	int32		last_chunk_seq;
+	Oid8		toast_valueid;
 
 	extsize = VARATT_EXTERNAL_GET_EXTSIZE(ta->toast_pointer);
 	last_chunk_seq = (extsize - 1) / TOAST_MAX_CHUNK_SIZE;
@@ -1896,14 +1902,16 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	}
 	systable_endscan_ordered(toastscan);
 
+	toast_valueid = ta->toast_pointer.va_valueid;
+
 	if (!found_toasttup)
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u not found in toast table",
-										 ta->toast_pointer.va_valueid));
+								psprintf("toast value " OID8_FORMAT " not found in toast table",
+										 toast_valueid));
 	else if (expected_chunk_seq <= last_chunk_seq)
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u was expected to end at chunk %d, but ended while expecting chunk %d",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value " OID8_FORMAT " was expected to end at chunk %d, but ended while expecting chunk %d",
+										 toast_valueid,
 										 last_chunk_seq, expected_chunk_seq));
 }
 
-- 
2.51.0

v7-0003-Minimize-footprint-of-TOAST_MAX_CHUNK_SIZE-in-hea.patchtext/x-diff; charset=us-asciiDownload
From b2d421d9e74de49ff73b229d93cd6de7e66751e8 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 13 Aug 2025 17:40:13 +0900
Subject: [PATCH v7 03/15] Minimize footprint of TOAST_MAX_CHUNK_SIZE in heap
 and amcheck

This eases a follow-up change to support 8-byte TOAST value IDs, as the
maximum chunk size allowed for a single chunk of TOASTed data depends on
the size of the value ID.
---
 src/backend/access/heap/heaptoast.c | 20 ++++++++++++--------
 contrib/amcheck/verify_heapam.c     | 13 +++++++++----
 2 files changed, 21 insertions(+), 12 deletions(-)

diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index d4b600de3aca..a3933e48c8c8 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -634,11 +634,12 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 	SysScanDesc toastscan;
 	HeapTuple	ttup;
 	int32		expectedchunk;
-	int32		totalchunks = ((attrsize - 1) / TOAST_MAX_CHUNK_SIZE) + 1;
+	int32		totalchunks;
 	int			startchunk;
 	int			endchunk;
 	int			num_indexes;
 	int			validIndex;
+	int32		max_chunk_size;
 
 	/* Look for the valid index of toast relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -646,8 +647,11 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 									&toastidxs,
 									&num_indexes);
 
-	startchunk = sliceoffset / TOAST_MAX_CHUNK_SIZE;
-	endchunk = (sliceoffset + slicelength - 1) / TOAST_MAX_CHUNK_SIZE;
+	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+
+	totalchunks = ((attrsize - 1) / max_chunk_size) + 1;
+	startchunk = sliceoffset / max_chunk_size;
+	endchunk = (sliceoffset + slicelength - 1) / max_chunk_size;
 	Assert(endchunk <= totalchunks);
 
 	/* Set up a scan key to fetch from the index. */
@@ -747,8 +751,8 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 									 curchunk,
 									 startchunk, endchunk, valueid,
 									 RelationGetRelationName(toastrel))));
-		expected_size = curchunk < totalchunks - 1 ? TOAST_MAX_CHUNK_SIZE
-			: attrsize - ((totalchunks - 1) * TOAST_MAX_CHUNK_SIZE);
+		expected_size = curchunk < totalchunks - 1 ? max_chunk_size
+			: attrsize - ((totalchunks - 1) * max_chunk_size);
 		if (chunksize != expected_size)
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
@@ -763,12 +767,12 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 		chcpystrt = 0;
 		chcpyend = chunksize - 1;
 		if (curchunk == startchunk)
-			chcpystrt = sliceoffset % TOAST_MAX_CHUNK_SIZE;
+			chcpystrt = sliceoffset % max_chunk_size;
 		if (curchunk == endchunk)
-			chcpyend = (sliceoffset + slicelength - 1) % TOAST_MAX_CHUNK_SIZE;
+			chcpyend = (sliceoffset + slicelength - 1) % max_chunk_size;
 
 		memcpy(VARDATA(result) +
-			   (curchunk * TOAST_MAX_CHUNK_SIZE - sliceoffset) + chcpystrt,
+			   (curchunk * max_chunk_size - sliceoffset) + chcpystrt,
 			   chunkdata + chcpystrt,
 			   (chcpyend - chcpystrt) + 1);
 
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index eb353c40249e..164ced37583a 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1556,15 +1556,19 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 				  uint32 extsize)
 {
 	int32		chunk_seq;
-	int32		last_chunk_seq = (extsize - 1) / TOAST_MAX_CHUNK_SIZE;
+	int32		last_chunk_seq;
 	Pointer		chunk;
 	bool		isnull;
 	int32		chunksize;
 	int32		expected_size;
 	Oid8		toast_valueid;
+	int32		max_chunk_size;
 
 	toast_valueid = ta->toast_pointer.va_valueid;
 
+	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	last_chunk_seq = (extsize - 1) / max_chunk_size;
+
 	/* Sanity-check the sequence number. */
 	chunk_seq = DatumGetInt32(fastgetattr(toasttup, 2,
 										  ctx->toast_rel->rd_att, &isnull));
@@ -1629,8 +1633,8 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 		return;
 	}
 
-	expected_size = chunk_seq < last_chunk_seq ? TOAST_MAX_CHUNK_SIZE
-		: extsize - (last_chunk_seq * TOAST_MAX_CHUNK_SIZE);
+	expected_size = chunk_seq < last_chunk_seq ? max_chunk_size
+		: extsize - (last_chunk_seq * max_chunk_size);
 
 	if (chunksize != expected_size)
 		report_toast_corruption(ctx, ta,
@@ -1872,9 +1876,10 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	int32		expected_chunk_seq = 0;
 	int32		last_chunk_seq;
 	Oid8		toast_valueid;
+	int32		max_chunk_size = TOAST_MAX_CHUNK_SIZE;
 
 	extsize = VARATT_EXTERNAL_GET_EXTSIZE(ta->toast_pointer);
-	last_chunk_seq = (extsize - 1) / TOAST_MAX_CHUNK_SIZE;
+	last_chunk_seq = (extsize - 1) / max_chunk_size;
 
 	/*
 	 * Setup a scan key to find chunks in toast table with matching va_valueid
-- 
2.51.0

v7-0004-Renames-around-varatt_external-varatt_external_oi.patchtext/x-diff; charset=us-asciiDownload
From ef9221b9a04290eb1cea57f9acc1aea8095d9eab Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 13 Aug 2025 18:28:10 +0900
Subject: [PATCH v7 04/15] Renames around varatt_external->varatt_external_oid

This impacts a few things:
- VARTAG_ONDISK -> VARTAG_ONDISK_OID
- TOAST_POINTER_SIZE -> TOAST_OID_POINTER_SIZE
- TOAST_MAX_CHUNK_SIZE -> TOAST_OID_MAX_CHUNK_SIZE

The "struct" around varatt_external is cleaned up in most places, while
on it.

This rename is in preparation of a follow-up commit that aims at adding
support for multiple types of external on-disk TOAST pointers, where the
OID type is only one subset of them.
---
 src/include/access/detoast.h                  |  4 +--
 src/include/access/heaptoast.h                |  6 ++--
 src/include/varatt.h                          | 34 +++++++++++--------
 src/backend/access/common/detoast.c           | 10 +++---
 src/backend/access/common/toast_compression.c |  2 +-
 src/backend/access/common/toast_internals.c   | 14 ++++----
 src/backend/access/heap/heaptoast.c           |  2 +-
 src/backend/access/table/toast_helper.c       |  4 +--
 src/backend/access/transam/xlog.c             |  8 ++---
 .../replication/logical/reorderbuffer.c       |  2 +-
 src/backend/utils/adt/varlena.c               |  2 +-
 src/bin/pg_resetwal/pg_resetwal.c             |  2 +-
 doc/src/sgml/func/func-info.sgml              |  2 +-
 doc/src/sgml/storage.sgml                     |  2 +-
 contrib/amcheck/verify_heapam.c               | 10 +++---
 15 files changed, 54 insertions(+), 50 deletions(-)

diff --git a/src/include/access/detoast.h b/src/include/access/detoast.h
index e603a2276c38..6435597b1127 100644
--- a/src/include/access/detoast.h
+++ b/src/include/access/detoast.h
@@ -14,7 +14,7 @@
 
 /*
  * Macro to fetch the possibly-unaligned contents of an EXTERNAL datum
- * into a local "struct varatt_external" toast pointer.  This should be
+ * into a local "varatt_external_oid" toast pointer.  This should be
  * just a memcpy, but some versions of gcc seem to produce broken code
  * that assumes the datum contents are aligned.  Introducing an explicit
  * intermediate "varattrib_1b_e *" variable seems to fix it.
@@ -28,7 +28,7 @@ do { \
 } while (0)
 
 /* Size of an EXTERNAL datum that contains a standard TOAST pointer */
-#define TOAST_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external))
+#define TOAST_OID_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_oid))
 
 /* Size of an EXTERNAL datum that contains an indirection pointer */
 #define INDIRECT_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_indirect))
diff --git a/src/include/access/heaptoast.h b/src/include/access/heaptoast.h
index fdc8d00d7099..59c82b2cb1a3 100644
--- a/src/include/access/heaptoast.h
+++ b/src/include/access/heaptoast.h
@@ -69,19 +69,19 @@
 
 /*
  * When we store an oversize datum externally, we divide it into chunks
- * containing at most TOAST_MAX_CHUNK_SIZE data bytes.  This number *must*
+ * containing at most TOAST_OID_MAX_CHUNK_SIZE data bytes.  This number *must*
  * be small enough that the completed toast-table tuple (including the
  * ID and sequence fields and all overhead) will fit on a page.
  * The coding here sets the size on the theory that we want to fit
  * EXTERN_TUPLES_PER_PAGE tuples of maximum size onto a page.
  *
- * NB: Changing TOAST_MAX_CHUNK_SIZE requires an initdb.
+ * NB: Changing TOAST_OID_MAX_CHUNK_SIZE requires an initdb.
  */
 #define EXTERN_TUPLES_PER_PAGE	4	/* tweak only this */
 
 #define EXTERN_TUPLE_MAX_SIZE	MaximumBytesPerTuple(EXTERN_TUPLES_PER_PAGE)
 
-#define TOAST_MAX_CHUNK_SIZE	\
+#define TOAST_OID_MAX_CHUNK_SIZE	\
 	(EXTERN_TUPLE_MAX_SIZE -							\
 	 MAXALIGN(SizeofHeapTupleHeader) -					\
 	 sizeof(Oid) -										\
diff --git a/src/include/varatt.h b/src/include/varatt.h
index aeeabf9145b5..c873a59bb1c9 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -16,7 +16,7 @@
 #define VARATT_H
 
 /*
- * struct varatt_external is a traditional "TOAST pointer", that is, the
+ * varatt_external_oid is a traditional "TOAST pointer", that is, the
  * information needed to fetch a Datum stored out-of-line in a TOAST table.
  * The data is compressed if and only if the external size stored in
  * va_extinfo is less than va_rawsize - VARHDRSZ.
@@ -29,14 +29,14 @@
  * you can look at these fields!  (The reason we use memcmp is to avoid
  * having to do that just to detect equality of two TOAST pointers...)
  */
-typedef struct varatt_external
+typedef struct varatt_external_oid
 {
 	int32		va_rawsize;		/* Original data size (includes header) */
 	uint32		va_extinfo;		/* External saved size (without header) and
 								 * compression method */
 	Oid			va_valueid;		/* Unique ID of value within TOAST table */
 	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
-}			varatt_external;
+}			varatt_external_oid;
 
 /*
  * These macros define the "saved size" portion of va_extinfo.  Its remaining
@@ -51,7 +51,7 @@ typedef struct varatt_external
  * The creator of such a Datum is entirely responsible that the referenced
  * storage survives for as long as referencing pointer Datums can exist.
  *
- * Note that just as for struct varatt_external, this struct is stored
+ * Note that just as for varatt_external_oid, this struct is stored
  * unaligned within any containing tuple.
  */
 typedef struct varatt_indirect
@@ -66,7 +66,7 @@ typedef struct varatt_indirect
  * storage.  APIs for this, in particular the definition of struct
  * ExpandedObjectHeader, are in src/include/utils/expandeddatum.h.
  *
- * Note that just as for struct varatt_external, this struct is stored
+ * Note that just as for varatt_external_oid, this struct is stored
  * unaligned within any containing tuple.
  */
 typedef struct ExpandedObjectHeader ExpandedObjectHeader;
@@ -78,15 +78,16 @@ typedef struct varatt_expanded
 
 /*
  * Type tag for the various sorts of "TOAST pointer" datums.  The peculiar
- * value for VARTAG_ONDISK comes from a requirement for on-disk compatibility
- * with a previous notion that the tag field was the pointer datum's length.
+ * value for VARTAG_ONDISK_OID comes from a requirement for on-disk
+ * compatibility with a previous notion that the tag field was the pointer
+ * datum's length.
  */
 typedef enum vartag_external
 {
 	VARTAG_INDIRECT = 1,
 	VARTAG_EXPANDED_RO = 2,
 	VARTAG_EXPANDED_RW = 3,
-	VARTAG_ONDISK = 18
+	VARTAG_ONDISK_OID = 18
 } vartag_external;
 
 /* Is a TOAST pointer either type of expanded-object pointer? */
@@ -105,8 +106,8 @@ VARTAG_SIZE(vartag_external tag)
 		return sizeof(varatt_indirect);
 	else if (VARTAG_IS_EXPANDED(tag))
 		return sizeof(varatt_expanded);
-	else if (tag == VARTAG_ONDISK)
-		return sizeof(varatt_external);
+	else if (tag == VARTAG_ONDISK_OID)
+		return sizeof(varatt_external_oid);
 	else
 	{
 		Assert(false);
@@ -360,7 +361,7 @@ VARATT_IS_EXTERNAL(const void *PTR)
 static inline bool
 VARATT_IS_EXTERNAL_ONDISK(const void *PTR)
 {
-	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK;
+	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID;
 }
 
 /* Is varlena datum an indirect pointer? */
@@ -502,15 +503,18 @@ VARDATA_COMPRESSED_GET_COMPRESS_METHOD(const void *PTR)
 	return ((varattrib_4b *) PTR)->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS;
 }
 
-/* Same for external Datums; but note argument is a struct varatt_external */
+/*
+ * Same for external Datums; but note argument is a struct
+ * varatt_external_oid.
+ */
 static inline Size
-VARATT_EXTERNAL_GET_EXTSIZE(struct varatt_external toast_pointer)
+VARATT_EXTERNAL_GET_EXTSIZE(varatt_external_oid toast_pointer)
 {
 	return toast_pointer.va_extinfo & VARLENA_EXTSIZE_MASK;
 }
 
 static inline uint32
-VARATT_EXTERNAL_GET_COMPRESS_METHOD(struct varatt_external toast_pointer)
+VARATT_EXTERNAL_GET_COMPRESS_METHOD(varatt_external_oid toast_pointer)
 {
 	return toast_pointer.va_extinfo >> VARLENA_EXTSIZE_BITS;
 }
@@ -533,7 +537,7 @@ VARATT_EXTERNAL_GET_COMPRESS_METHOD(struct varatt_external toast_pointer)
  * actually saves space, so we expect either equality or less-than.
  */
 static inline bool
-VARATT_EXTERNAL_IS_COMPRESSED(struct varatt_external toast_pointer)
+VARATT_EXTERNAL_IS_COMPRESSED(varatt_external_oid toast_pointer)
 {
 	return VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) <
 		(Size) (toast_pointer.va_rawsize - VARHDRSZ);
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 626517877422..c187c32d96dd 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -225,7 +225,7 @@ detoast_attr_slice(struct varlena *attr,
 
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
-		struct varatt_external toast_pointer;
+		varatt_external_oid toast_pointer;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 
@@ -344,7 +344,7 @@ toast_fetch_datum(struct varlena *attr)
 {
 	Relation	toastrel;
 	struct varlena *result;
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 	int32		attrsize;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
@@ -398,7 +398,7 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 {
 	Relation	toastrel;
 	struct varlena *result;
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 	int32		attrsize;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
@@ -550,7 +550,7 @@ toast_raw_datum_size(Datum value)
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
 		/* va_rawsize is the size of the original datum -- including header */
-		struct varatt_external toast_pointer;
+		varatt_external_oid toast_pointer;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 		result = toast_pointer.va_rawsize;
@@ -610,7 +610,7 @@ toast_datum_size(Datum value)
 		 * compressed or not.  We do not count the size of the toast pointer
 		 * ... should we?
 		 */
-		struct varatt_external toast_pointer;
+		varatt_external_oid toast_pointer;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 		result = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c
index 926f1e4008ab..08f572f31eed 100644
--- a/src/backend/access/common/toast_compression.c
+++ b/src/backend/access/common/toast_compression.c
@@ -262,7 +262,7 @@ toast_get_compression_id(struct varlena *attr)
 	 */
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
-		struct varatt_external toast_pointer;
+		varatt_external_oid toast_pointer;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 420263691cc3..770dbb5a6104 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -124,7 +124,7 @@ toast_save_datum(Relation rel, Datum value,
 	TupleDesc	toasttupDesc;
 	CommandId	mycid = GetCurrentCommandId(true);
 	struct varlena *result;
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 	int32		chunk_seq = 0;
 	char	   *data_p;
 	int32		data_todo;
@@ -225,7 +225,7 @@ toast_save_datum(Relation rel, Datum value,
 		toast_pointer.va_valueid = InvalidOid;
 		if (oldexternal != NULL)
 		{
-			struct varatt_external old_toast_pointer;
+			varatt_external_oid old_toast_pointer;
 
 			Assert(VARATT_IS_EXTERNAL_ONDISK(oldexternal));
 			/* Must copy to access aligned fields */
@@ -289,7 +289,7 @@ toast_save_datum(Relation rel, Datum value,
 		{
 			struct varlena hdr;
 			/* this is to make the union big enough for a chunk: */
-			char		data[TOAST_MAX_CHUNK_SIZE + VARHDRSZ];
+			char		data[TOAST_OID_MAX_CHUNK_SIZE + VARHDRSZ];
 			/* ensure union is aligned well enough: */
 			int32		align_it;
 		}			chunk_data;
@@ -300,7 +300,7 @@ toast_save_datum(Relation rel, Datum value,
 		/*
 		 * Calculate the size of this chunk
 		 */
-		chunk_size = Min(TOAST_MAX_CHUNK_SIZE, data_todo);
+		chunk_size = Min(TOAST_OID_MAX_CHUNK_SIZE, data_todo);
 
 		/*
 		 * Build a tuple and store it
@@ -361,8 +361,8 @@ toast_save_datum(Relation rel, Datum value,
 	/*
 	 * Create the TOAST pointer value that we'll return
 	 */
-	result = (struct varlena *) palloc(TOAST_POINTER_SIZE);
-	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK);
+	result = (struct varlena *) palloc(TOAST_OID_POINTER_SIZE);
+	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_OID);
 	memcpy(VARDATA_EXTERNAL(result), &toast_pointer, sizeof(toast_pointer));
 
 	return PointerGetDatum(result);
@@ -378,7 +378,7 @@ void
 toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 {
 	struct varlena *attr = (struct varlena *) DatumGetPointer(value);
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 	Relation	toastrel;
 	Relation   *toastidxs;
 	ScanKeyData toastkey;
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index a3933e48c8c8..ddde7fcf79a4 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -647,7 +647,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 									&toastidxs,
 									&num_indexes);
 
-	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
 
 	totalchunks = ((attrsize - 1) / max_chunk_size) + 1;
 	startchunk = sliceoffset / max_chunk_size;
diff --git a/src/backend/access/table/toast_helper.c b/src/backend/access/table/toast_helper.c
index 11f97d65367d..0c58c6c32565 100644
--- a/src/backend/access/table/toast_helper.c
+++ b/src/backend/access/table/toast_helper.c
@@ -171,7 +171,7 @@ toast_tuple_init(ToastTupleContext *ttc)
  * The column must have attstorage EXTERNAL or EXTENDED if check_main is
  * false, and must have attstorage MAIN if check_main is true.
  *
- * The column must have a minimum size of MAXALIGN(TOAST_POINTER_SIZE);
+ * The column must have a minimum size of MAXALIGN(TOAST_OID_POINTER_SIZE);
  * if not, no benefit is to be expected by compressing it.
  *
  * The return value is the index of the biggest suitable column, or
@@ -184,7 +184,7 @@ toast_tuple_find_biggest_attribute(ToastTupleContext *ttc,
 	TupleDesc	tupleDesc = ttc->ttc_rel->rd_att;
 	int			numAttrs = tupleDesc->natts;
 	int			biggest_attno = -1;
-	int32		biggest_size = MAXALIGN(TOAST_POINTER_SIZE);
+	int32		biggest_size = MAXALIGN(TOAST_OID_POINTER_SIZE);
 	int32		skip_colflags = TOASTCOL_IGNORE;
 	int			i;
 
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index eceab3412558..79b0c8b40449 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4274,7 +4274,7 @@ WriteControlFile(void)
 	ControlFile->nameDataLen = NAMEDATALEN;
 	ControlFile->indexMaxKeys = INDEX_MAX_KEYS;
 
-	ControlFile->toast_max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	ControlFile->toast_max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
 	ControlFile->loblksize = LOBLKSIZE;
 
 	ControlFile->float8ByVal = true;	/* vestigial */
@@ -4517,15 +4517,15 @@ ReadControlFile(void)
 						   "INDEX_MAX_KEYS", ControlFile->indexMaxKeys,
 						   "INDEX_MAX_KEYS", INDEX_MAX_KEYS),
 				 errhint("It looks like you need to recompile or initdb.")));
-	if (ControlFile->toast_max_chunk_size != TOAST_MAX_CHUNK_SIZE)
+	if (ControlFile->toast_max_chunk_size != TOAST_OID_MAX_CHUNK_SIZE)
 		ereport(FATAL,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("database files are incompatible with server"),
 		/* translator: %s is a variable name and %d is its value */
 				 errdetail("The database cluster was initialized with %s %d,"
 						   " but the server was compiled with %s %d.",
-						   "TOAST_MAX_CHUNK_SIZE", ControlFile->toast_max_chunk_size,
-						   "TOAST_MAX_CHUNK_SIZE", (int) TOAST_MAX_CHUNK_SIZE),
+						   "TOAST_OID_MAX_CHUNK_SIZE", ControlFile->toast_max_chunk_size,
+						   "TOAST_OID_MAX_CHUNK_SIZE", (int) TOAST_OID_MAX_CHUNK_SIZE),
 				 errhint("It looks like you need to recompile or initdb.")));
 	if (ControlFile->loblksize != LOBLKSIZE)
 		ereport(FATAL,
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index a1cf30a8f2eb..d61347d11d45 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -5117,7 +5117,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		struct varlena *varlena;
 
 		/* va_rawsize is the size of the original datum -- including header */
-		struct varatt_external toast_pointer;
+		varatt_external_oid toast_pointer;
 		struct varatt_indirect redirect_pointer;
 		struct varlena *new_datum = NULL;
 		struct varlena *reconstructed;
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 2c398cd9e5cb..4aff647fccfd 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -4211,7 +4211,7 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 {
 	int			typlen;
 	struct varlena *attr;
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 
 	/* On first call, get the input type's typlen, and save at *fn_extra */
 	if (fcinfo->flinfo->fn_extra == NULL)
diff --git a/src/bin/pg_resetwal/pg_resetwal.c b/src/bin/pg_resetwal/pg_resetwal.c
index 7a4e4eb95706..638b41c922ba 100644
--- a/src/bin/pg_resetwal/pg_resetwal.c
+++ b/src/bin/pg_resetwal/pg_resetwal.c
@@ -717,7 +717,7 @@ GuessControlValues(void)
 	ControlFile.xlog_seg_size = DEFAULT_XLOG_SEG_SIZE;
 	ControlFile.nameDataLen = NAMEDATALEN;
 	ControlFile.indexMaxKeys = INDEX_MAX_KEYS;
-	ControlFile.toast_max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	ControlFile.toast_max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
 	ControlFile.loblksize = LOBLKSIZE;
 	ControlFile.float8ByVal = true; /* vestigial */
 
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index c393832d94c6..ba6b592cdb3f 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3533,7 +3533,7 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
       </row>
 
       <row>
-       <entry><structfield>max_toast_chunk_size</structfield></entry>
+       <entry><structfield>max_toast_oid_chunk_size</structfield></entry>
        <entry><type>integer</type></entry>
       </row>
 
diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml
index 02ddfda834a2..67600fd974d7 100644
--- a/doc/src/sgml/storage.sgml
+++ b/doc/src/sgml/storage.sgml
@@ -417,7 +417,7 @@ described in more detail below.
 
 <para>
 Out-of-line values are divided (after compression if used) into chunks of at
-most <symbol>TOAST_MAX_CHUNK_SIZE</symbol> bytes (by default this value is chosen
+most <symbol>TOAST_OID_MAX_CHUNK_SIZE</symbol> bytes (by default this value is chosen
 so that four chunk rows will fit on a page, making it about 2000 bytes).
 Each chunk is stored as a separate row in the <acronym>TOAST</acronym> table
 belonging to the owning table.  Every
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 164ced37583a..7ec6cef118fb 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -73,7 +73,7 @@ typedef enum SkipPages
  */
 typedef struct ToastedAttribute
 {
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 	BlockNumber blkno;			/* block in main table */
 	OffsetNumber offnum;		/* offset in main table */
 	AttrNumber	attnum;			/* attribute in main table */
@@ -1566,7 +1566,7 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 
 	toast_valueid = ta->toast_pointer.va_valueid;
 
-	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
 	last_chunk_seq = (extsize - 1) / max_chunk_size;
 
 	/* Sanity-check the sequence number. */
@@ -1672,7 +1672,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	uint16		infomask;
 	Oid8		toast_pointer_valueid;
 	CompactAttribute *thisatt;
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 
 	infomask = ctx->tuphdr->t_infomask;
 	thisatt = TupleDescCompactAttr(RelationGetDescr(ctx->rel), ctx->attnum);
@@ -1731,7 +1731,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	{
 		uint8		va_tag = VARTAG_EXTERNAL(tp + ctx->offset);
 
-		if (va_tag != VARTAG_ONDISK)
+		if (va_tag != VARTAG_ONDISK_OID)
 		{
 			report_corruption(ctx,
 							  psprintf("toasted attribute has unexpected TOAST tag %u",
@@ -1876,7 +1876,7 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	int32		expected_chunk_seq = 0;
 	int32		last_chunk_seq;
 	Oid8		toast_valueid;
-	int32		max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	int32		max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
 
 	extsize = VARATT_EXTERNAL_GET_EXTSIZE(ta->toast_pointer);
 	last_chunk_seq = (extsize - 1) / max_chunk_size;
-- 
2.51.0

v7-0005-Refactor-external-TOAST-pointer-code-for-better-p.patchtext/x-diff; charset=us-asciiDownload
From efeb94a5db38a8461bd762664ad255fbb68306d6 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 30 Sep 2025 15:09:13 +0900
Subject: [PATCH v7 05/15] Refactor external TOAST pointer code for better
 pluggability

This commit introduces a new interface for external TOAST pointers,
which is able to make a translation of the varlena pointers stored on
disk to/from an new in-memory structure called toast_external.  The
types of varatt_external supported on disk need to be registered into a
new subsystem in a new file, called toast_external.[c|h], then define a
set of callbacks to allow the toasting and detoasting code to use it.

A follow-up change will rely on this refactoring to introduce new
vartag_external values with an associated varatt_external_* that is
able, which would be used in int8 TOAST tables.
---
 src/include/access/detoast.h                  |  12 +-
 src/include/access/heaptoast.h                |   3 +
 src/include/access/toast_external.h           | 176 ++++++++++++++++
 src/include/access/toast_helper.h             |   1 +
 src/include/varatt.h                          |  16 +-
 src/backend/access/common/Makefile            |   1 +
 src/backend/access/common/detoast.c           |  57 +++--
 src/backend/access/common/meson.build         |   1 +
 src/backend/access/common/toast_compression.c |  10 +-
 src/backend/access/common/toast_external.c    | 196 ++++++++++++++++++
 src/backend/access/common/toast_internals.c   |  84 +++++---
 src/backend/access/heap/heaptoast.c           |  20 +-
 src/backend/access/table/toast_helper.c       |  12 +-
 src/backend/access/transam/xlog.c             |   8 +-
 .../replication/logical/reorderbuffer.c       |  13 +-
 src/backend/utils/adt/varlena.c               |   7 +-
 src/bin/pg_resetwal/pg_resetwal.c             |   2 +-
 contrib/amcheck/verify_heapam.c               |  35 ++--
 src/tools/pgindent/typedefs.list              |   2 +
 19 files changed, 545 insertions(+), 111 deletions(-)
 create mode 100644 src/include/access/toast_external.h
 create mode 100644 src/backend/access/common/toast_external.c

diff --git a/src/include/access/detoast.h b/src/include/access/detoast.h
index 6435597b1127..2f71fbd95f88 100644
--- a/src/include/access/detoast.h
+++ b/src/include/access/detoast.h
@@ -14,10 +14,11 @@
 
 /*
  * Macro to fetch the possibly-unaligned contents of an EXTERNAL datum
- * into a local "varatt_external_oid" toast pointer.  This should be
- * just a memcpy, but some versions of gcc seem to produce broken code
- * that assumes the datum contents are aligned.  Introducing an explicit
- * intermediate "varattrib_1b_e *" variable seems to fix it.
+ * into a local "varatt_external_*" toast pointer, as supported
+ * in toast_external.h and varatt.h.  This should be just a memcpy, but
+ * some versions of gcc seem to produce broken code that assumes the datum
+ * contents are aligned.  Introducing an explicit intermediate
+ * "varattrib_1b_e *" variable seems to fix it.
  */
 #define VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr) \
 do { \
@@ -27,9 +28,6 @@ do { \
 	memcpy(&(toast_pointer), VARDATA_EXTERNAL(attre), sizeof(toast_pointer)); \
 } while (0)
 
-/* Size of an EXTERNAL datum that contains a standard TOAST pointer */
-#define TOAST_OID_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_oid))
-
 /* Size of an EXTERNAL datum that contains an indirection pointer */
 #define INDIRECT_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_indirect))
 
diff --git a/src/include/access/heaptoast.h b/src/include/access/heaptoast.h
index 59c82b2cb1a3..afa3d8ca95f7 100644
--- a/src/include/access/heaptoast.h
+++ b/src/include/access/heaptoast.h
@@ -88,6 +88,9 @@
 	 sizeof(int32) -									\
 	 VARHDRSZ)
 
+/* Maximum size of chunk possible */
+#define TOAST_MAX_CHUNK_SIZE	TOAST_OID_MAX_CHUNK_SIZE
+
 /* ----------
  * heap_toast_insert_or_update -
  *
diff --git a/src/include/access/toast_external.h b/src/include/access/toast_external.h
new file mode 100644
index 000000000000..6450343eab25
--- /dev/null
+++ b/src/include/access/toast_external.h
@@ -0,0 +1,176 @@
+/*-------------------------------------------------------------------------
+ *
+ * toast_external.h
+ *	  Support for on-disk external TOAST pointers
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1995, Regents of the University of California
+ *
+ * src/include/access/toast_external.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef TOAST_EXTERNAL_H
+#define TOAST_EXTERNAL_H
+
+#include "access/toast_compression.h"
+#include "varatt.h"
+
+/*
+ * Intermediate in-memory structure used when creating on-disk
+ * varatt_external_* or when deserializing varlena contents.
+ */
+typedef struct toast_external_data
+{
+	/* Original data size (includes header) */
+	int32		rawsize;
+
+	/* External saved size (without header) */
+	uint32		extsize;
+
+	/*
+	 * Compression method.
+	 *
+	 * If not compressed, set to TOAST_INVALID_COMPRESSION_ID.
+	 */
+	ToastCompressionId compression_method;
+
+	/* Relation OID of TOAST table containing the value */
+	Oid			toastrelid;
+
+	/*
+	 * Unique ID of value within TOAST table.  This could be an OID or an Oid8
+	 * value.  This field is large enough to be able to store any of these.
+	 */
+	Oid8		valueid;
+} toast_external_data;
+
+/*
+ * Metadata for external TOAST pointer kinds, separated based on their
+ * vartag_external.
+ */
+typedef struct toast_external_info
+{
+	/*
+	 * Maximum chunk of data authorized for this type of external TOAST
+	 * pointer, when dividing an entry by chunks.  Sized depending on the size
+	 * of its varatt_external_* structure.
+	 */
+	int32		maximum_chunk_size;
+
+	/*
+	 * Size of an external TOAST pointer of this type, typically
+	 * (VARHDRSZ_EXTERNAL + sizeof(varatt_external_struct)).
+	 */
+	int32		toast_pointer_size;
+
+	/*
+	 * Map an input varlena to a toast_external_data, for consumption in the
+	 * backend code.  "data" is an input/output result.
+	 */
+	void		(*to_external_data) (struct varlena *attr,
+									 toast_external_data *data);
+
+	/*
+	 * Create a varlena that will be used on-disk for the given TOAST type,
+	 * based on the given input data.
+	 *
+	 * The result is the varlena created, for on-disk insertion.
+	 */
+	struct varlena *(*create_external_data) (toast_external_data data);
+
+} toast_external_info;
+
+/* Retrieve a toast_external_info from a vartag */
+extern const toast_external_info *toast_external_get_info(uint8 tag);
+
+/* Retrieve toast_pointer_size using a TOAST attribute type */
+extern int32 toast_external_info_get_pointer_size(uint8 tag);
+
+/* Retrieve the vartag to assign to a TOAST typle */
+extern uint8 toast_external_assign_vartag(Oid toastrelid, Oid8 value);
+
+/*
+ * Testing whether an externally-stored value is compressed now requires
+ * comparing size stored in extsize (the actual length of the external data)
+ * to rawsize (the original uncompressed datum's size).  The latter includes
+ * VARHDRSZ overhead, the former doesn't.  We never use compression unless it
+ * actually saves space, so we expect either equality or less-than.
+ */
+static inline bool
+TOAST_EXTERNAL_IS_COMPRESSED(toast_external_data data)
+{
+	return data.extsize < (data.rawsize - VARHDRSZ);
+}
+
+/* Full data structure */
+static inline void
+toast_external_info_get_data(struct varlena *attr, toast_external_data *data)
+{
+	uint8		tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+
+	info->to_external_data(attr, data);
+}
+
+/*
+ * Helper routines to recover specific fields in toast_external_data.  Most
+ * code paths doing work with on-disk external TOAST pointers care about
+ * these.
+ */
+
+/* Detoasted "raw" size */
+static inline Size
+toast_external_info_get_rawsize(struct varlena *attr)
+{
+	uint8		tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+	toast_external_data data;
+
+	info->to_external_data(attr, &data);
+
+	return data.rawsize;
+}
+
+/* External saved size */
+static inline Size
+toast_external_info_get_extsize(struct varlena *attr)
+{
+	uint8		tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+	toast_external_data data;
+
+	info->to_external_data(attr, &data);
+
+	return data.extsize;
+}
+
+/* Compression method ID */
+static inline ToastCompressionId
+toast_external_info_get_compression_method(struct varlena *attr)
+{
+	uint8		tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+	toast_external_data data;
+
+	info->to_external_data(attr, &data);
+
+	return data.compression_method;
+}
+
+/* Value ID */
+static inline Oid8
+toast_external_info_get_valueid(struct varlena *attr)
+{
+	uint8		tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+	toast_external_data data;
+
+	info->to_external_data(attr, &data);
+
+	return data.valueid;
+}
+
+#endif							/* TOAST_EXTERNAL_H */
diff --git a/src/include/access/toast_helper.h b/src/include/access/toast_helper.h
index e6ab8afffb67..6bc912809f34 100644
--- a/src/include/access/toast_helper.h
+++ b/src/include/access/toast_helper.h
@@ -47,6 +47,7 @@ typedef struct
 	 * should be NULL in the case of an insert.
 	 */
 	Relation	ttc_rel;		/* the relation that contains the tuple */
+	int32		ttc_toast_pointer_size; /* size of external TOAST pointer */
 	Datum	   *ttc_values;		/* values from the tuple columns */
 	bool	   *ttc_isnull;		/* null flags for the tuple columns */
 	Datum	   *ttc_oldvalues;	/* values from previous tuple */
diff --git a/src/include/varatt.h b/src/include/varatt.h
index c873a59bb1c9..790d9f844c91 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -21,6 +21,9 @@
  * The data is compressed if and only if the external size stored in
  * va_extinfo is less than va_rawsize - VARHDRSZ.
  *
+ * The value ID is an OID, used for TOAST relations with OID as attribute
+ * for chunk_id.
+ *
  * This struct must not contain any padding, because we sometimes compare
  * these pointers using memcmp.
  *
@@ -51,7 +54,7 @@ typedef struct varatt_external_oid
  * The creator of such a Datum is entirely responsible that the referenced
  * storage survives for as long as referencing pointer Datums can exist.
  *
- * Note that just as for varatt_external_oid, this struct is stored
+ * Note that just as for varatt_external_*, this struct is stored
  * unaligned within any containing tuple.
  */
 typedef struct varatt_indirect
@@ -66,7 +69,7 @@ typedef struct varatt_indirect
  * storage.  APIs for this, in particular the definition of struct
  * ExpandedObjectHeader, are in src/include/utils/expandeddatum.h.
  *
- * Note that just as for varatt_external_oid, this struct is stored
+ * Note that just as for varatt_external_*, this struct is stored
  * unaligned within any containing tuple.
  */
 typedef struct ExpandedObjectHeader ExpandedObjectHeader;
@@ -357,11 +360,18 @@ VARATT_IS_EXTERNAL(const void *PTR)
 	return VARATT_IS_1B_E(PTR);
 }
 
+/* Is varlena datum a pointer to on-disk toasted data with OID value? */
+static inline bool
+VARATT_IS_EXTERNAL_ONDISK_OID(const void *PTR)
+{
+	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID;
+}
+
 /* Is varlena datum a pointer to on-disk toasted data? */
 static inline bool
 VARATT_IS_EXTERNAL_ONDISK(const void *PTR)
 {
-	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID;
+	return VARATT_IS_EXTERNAL_ONDISK_OID(PTR);
 }
 
 /* Is varlena datum an indirect pointer? */
diff --git a/src/backend/access/common/Makefile b/src/backend/access/common/Makefile
index e78de312659e..1ef86a245886 100644
--- a/src/backend/access/common/Makefile
+++ b/src/backend/access/common/Makefile
@@ -27,6 +27,7 @@ OBJS = \
 	syncscan.o \
 	tidstore.o \
 	toast_compression.o \
+	toast_external.o \
 	toast_internals.o \
 	tupconvert.o \
 	tupdesc.o
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index c187c32d96dd..8531c27439e4 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -16,6 +16,7 @@
 #include "access/detoast.h"
 #include "access/table.h"
 #include "access/tableam.h"
+#include "access/toast_external.h"
 #include "access/toast_internals.h"
 #include "common/int.h"
 #include "common/pg_lzcompress.h"
@@ -225,12 +226,12 @@ detoast_attr_slice(struct varlena *attr,
 
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
-		varatt_external_oid toast_pointer;
+		toast_external_data toast_pointer;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		toast_external_info_get_data(attr, &toast_pointer);
 
 		/* fast path for non-compressed external datums */
-		if (!VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+		if (!TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 			return toast_fetch_datum_slice(attr, sliceoffset, slicelength);
 
 		/*
@@ -240,7 +241,7 @@ detoast_attr_slice(struct varlena *attr,
 		 */
 		if (slicelimit >= 0)
 		{
-			int32		max_size = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+			int32		max_size = toast_pointer.extsize;
 
 			/*
 			 * Determine maximum amount of compressed data needed for a prefix
@@ -251,8 +252,7 @@ detoast_attr_slice(struct varlena *attr,
 			 * determine how much compressed data we need to be sure of being
 			 * able to decompress the required slice.
 			 */
-			if (VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) ==
-				TOAST_PGLZ_COMPRESSION_ID)
+			if (toast_pointer.compression_method == TOAST_PGLZ_COMPRESSION_ID)
 				max_size = pglz_maximum_compressed_size(slicelimit, max_size);
 
 			/*
@@ -344,20 +344,21 @@ toast_fetch_datum(struct varlena *attr)
 {
 	Relation	toastrel;
 	struct varlena *result;
-	varatt_external_oid toast_pointer;
+	toast_external_data toast_pointer;
 	int32		attrsize;
+	Oid8		valueid;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		elog(ERROR, "toast_fetch_datum shouldn't be called for non-ondisk datums");
 
 	/* Must copy to access aligned fields */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_external_info_get_data(attr, &toast_pointer);
 
-	attrsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+	attrsize = toast_pointer.extsize;
 
 	result = (struct varlena *) palloc(attrsize + VARHDRSZ);
 
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+	if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 		SET_VARSIZE_COMPRESSED(result, attrsize + VARHDRSZ);
 	else
 		SET_VARSIZE(result, attrsize + VARHDRSZ);
@@ -365,14 +366,15 @@ toast_fetch_datum(struct varlena *attr)
 	if (attrsize == 0)
 		return result;			/* Probably shouldn't happen, but just in
 								 * case. */
+	valueid = toast_pointer.valueid;
 
 	/*
 	 * Open the toast relation and its indexes
 	 */
-	toastrel = table_open(toast_pointer.va_toastrelid, AccessShareLock);
+	toastrel = table_open(toast_pointer.toastrelid, AccessShareLock);
 
 	/* Fetch all chunks */
-	table_relation_fetch_toast_slice(toastrel, toast_pointer.va_valueid,
+	table_relation_fetch_toast_slice(toastrel, valueid,
 									 attrsize, 0, attrsize, result);
 
 	/* Close toast table */
@@ -398,23 +400,26 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 {
 	Relation	toastrel;
 	struct varlena *result;
-	varatt_external_oid toast_pointer;
+	toast_external_data toast_pointer;
 	int32		attrsize;
+	Oid8		valueid;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		elog(ERROR, "toast_fetch_datum_slice shouldn't be called for non-ondisk datums");
 
 	/* Must copy to access aligned fields */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_external_info_get_data(attr, &toast_pointer);
+
+	valueid = toast_pointer.valueid;
 
 	/*
 	 * It's nonsense to fetch slices of a compressed datum unless when it's a
 	 * prefix -- this isn't lo_* we can't return a compressed datum which is
 	 * meaningful to toast later.
 	 */
-	Assert(!VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer) || 0 == sliceoffset);
+	Assert(!TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer) || 0 == sliceoffset);
 
-	attrsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+	attrsize = toast_pointer.extsize;
 
 	if (sliceoffset >= attrsize)
 	{
@@ -427,7 +432,7 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 	 * space required by va_tcinfo, which is stored at the beginning as an
 	 * int32 value.
 	 */
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer) && slicelength > 0)
+	if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer) && slicelength > 0)
 		slicelength = slicelength + sizeof(int32);
 
 	/*
@@ -440,7 +445,7 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 
 	result = (struct varlena *) palloc(slicelength + VARHDRSZ);
 
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+	if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 		SET_VARSIZE_COMPRESSED(result, slicelength + VARHDRSZ);
 	else
 		SET_VARSIZE(result, slicelength + VARHDRSZ);
@@ -449,10 +454,11 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 		return result;			/* Can save a lot of work at this point! */
 
 	/* Open the toast relation */
-	toastrel = table_open(toast_pointer.va_toastrelid, AccessShareLock);
+	toastrel = table_open(toast_pointer.toastrelid, AccessShareLock);
 
 	/* Fetch all chunks */
-	table_relation_fetch_toast_slice(toastrel, toast_pointer.va_valueid,
+	table_relation_fetch_toast_slice(toastrel,
+									 valueid,
 									 attrsize, sliceoffset, slicelength,
 									 result);
 
@@ -549,11 +555,7 @@ toast_raw_datum_size(Datum value)
 
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
-		/* va_rawsize is the size of the original datum -- including header */
-		varatt_external_oid toast_pointer;
-
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-		result = toast_pointer.va_rawsize;
+		result = toast_external_info_get_rawsize(attr);
 	}
 	else if (VARATT_IS_EXTERNAL_INDIRECT(attr))
 	{
@@ -610,10 +612,7 @@ toast_datum_size(Datum value)
 		 * compressed or not.  We do not count the size of the toast pointer
 		 * ... should we?
 		 */
-		varatt_external_oid toast_pointer;
-
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-		result = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+		result = toast_external_info_get_extsize(attr);
 	}
 	else if (VARATT_IS_EXTERNAL_INDIRECT(attr))
 	{
diff --git a/src/backend/access/common/meson.build b/src/backend/access/common/meson.build
index e3cdbe7a22e1..c20f2e88921e 100644
--- a/src/backend/access/common/meson.build
+++ b/src/backend/access/common/meson.build
@@ -15,6 +15,7 @@ backend_sources += files(
   'syncscan.c',
   'tidstore.c',
   'toast_compression.c',
+  'toast_external.c',
   'toast_internals.c',
   'tupconvert.c',
   'tupdesc.c',
diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c
index 08f572f31eed..94606a58c8fb 100644
--- a/src/backend/access/common/toast_compression.c
+++ b/src/backend/access/common/toast_compression.c
@@ -19,6 +19,7 @@
 
 #include "access/detoast.h"
 #include "access/toast_compression.h"
+#include "access/toast_external.h"
 #include "common/pg_lzcompress.h"
 #include "varatt.h"
 
@@ -261,14 +262,7 @@ toast_get_compression_id(struct varlena *attr)
 	 * toast compression header.
 	 */
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
-	{
-		varatt_external_oid toast_pointer;
-
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-
-		if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
-			cmid = VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer);
-	}
+		cmid = toast_external_info_get_compression_method(attr);
 	else if (VARATT_IS_COMPRESSED(attr))
 		cmid = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(attr);
 
diff --git a/src/backend/access/common/toast_external.c b/src/backend/access/common/toast_external.c
new file mode 100644
index 000000000000..2154152b8bfb
--- /dev/null
+++ b/src/backend/access/common/toast_external.c
@@ -0,0 +1,196 @@
+/*-------------------------------------------------------------------------
+ *
+ * toast_external.c
+ *	  Functions for the support of external on-disk TOAST pointers.
+ *
+ * This includes all the types of external on-disk TOAST pointers supported
+ * by the backend, based on the callbacks and data defined in
+ * toast_external.h.
+ *
+ * Copyright (c) 2025, PostgreSQL Global Development Group
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/common/toast_external.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/detoast.h"
+#include "access/heaptoast.h"
+#include "access/toast_external.h"
+
+/* Callbacks for VARTAG_ONDISK_OID */
+static void ondisk_oid_to_external_data(struct varlena *attr,
+										toast_external_data *data);
+static struct varlena *ondisk_oid_create_external_data(toast_external_data data);
+
+/*
+ * Size of an EXTERNAL datum that contains a standard TOAST pointer (OID
+ * value).
+ */
+#define TOAST_OID_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_oid))
+
+/*
+ * For now there are only two types, all defined in this file.  For now this
+ * is the maximum value of vartag_external, which is a historical choice.
+ */
+#define TOAST_EXTERNAL_INFO_SIZE	(VARTAG_ONDISK_OID + 1)
+
+/*
+ * The different kinds of on-disk external TOAST pointers, divided by
+ * vartag_external.
+ *
+ * See comments for struct toast_external_info about the details of the
+ * individual fields.
+ */
+static const toast_external_info toast_external_infos[TOAST_EXTERNAL_INFO_SIZE] = {
+	[VARTAG_ONDISK_OID] = {
+		.toast_pointer_size = TOAST_OID_POINTER_SIZE,
+		.maximum_chunk_size = TOAST_OID_MAX_CHUNK_SIZE,
+		.to_external_data = ondisk_oid_to_external_data,
+		.create_external_data = ondisk_oid_create_external_data,
+	},
+};
+
+/*
+ * toast_external_get_info
+ *
+ * Get toast_external_info of the defined vartag_external, central set of
+ * callbacks, based on a "tag", which is a vartag_external value for an
+ * on-disk external varlena.
+ */
+const toast_external_info *
+toast_external_get_info(uint8 tag)
+{
+	const toast_external_info *res = &toast_external_infos[tag];
+
+	/* check tag for invalid range */
+	if (tag >= TOAST_EXTERNAL_INFO_SIZE)
+		elog(ERROR, "incorrect value %u for toast_external_info", tag);
+
+	/* sanity check with tag in valid range */
+	res = &toast_external_infos[tag];
+	if (res == NULL)
+		elog(ERROR, "incorrect value %u for toast_external_info", tag);
+	return res;
+}
+
+/*
+ * toast_external_info_get_pointer_size
+ *
+ * Get external TOAST pointer size based on the attribute type of a TOAST
+ * value.  "tag" is a vartag_external value.
+ */
+int32
+toast_external_info_get_pointer_size(uint8 tag)
+{
+	return toast_external_infos[tag].toast_pointer_size;
+}
+
+/*
+ * toast_external_assign_vartag
+ *
+ * Assign the vartag_external of a TOAST tuple, based on the TOAST relation
+ * it uses and its value.
+ *
+ * An invalid value can be given by the caller of this routine, in which
+ * case a default vartag should be provided based on only the toast relation
+ * used.
+ */
+uint8
+toast_external_assign_vartag(Oid toastrelid, Oid8 valueid)
+{
+	/*
+	 * If dealing with a code path where a TOAST relation may not be assigned,
+	 * like heap_toast_insert_or_update(), just use the legacy
+	 * vartag_external.
+	 */
+	if (!OidIsValid(toastrelid))
+		return VARTAG_ONDISK_OID;
+
+	/*
+	 * Currently there is only one type of vartag_external supported: 4-byte
+	 * value with OID for the chunk_id type.
+	 *
+	 * Note: This routine will be extended to be able to use multiple
+	 * vartag_external within a single TOAST relation type, that may change
+	 * depending on the value used.
+	 */
+	return VARTAG_ONDISK_OID;
+}
+
+/*
+ * Helper routines able to translate the various varatt_external_* from/to
+ * the in-memory representation toast_external_data used in the backend.
+ */
+
+/* Callbacks for VARTAG_ONDISK_OID */
+
+/*
+ * ondisk_oid_to_external_data
+ *
+ * Translate a varlena to its toast_external_data representation, to be used
+ * by the backend code.
+ */
+static void
+ondisk_oid_to_external_data(struct varlena *attr, toast_external_data *data)
+{
+	varatt_external_oid external;
+
+	VARATT_EXTERNAL_GET_POINTER(external, attr);
+	data->rawsize = external.va_rawsize;
+
+	/*
+	 * External size and compression methods are stored in the same field,
+	 * extract.
+	 */
+	if (VARATT_EXTERNAL_IS_COMPRESSED(external))
+	{
+		data->extsize = VARATT_EXTERNAL_GET_EXTSIZE(external);
+		data->compression_method = VARATT_EXTERNAL_GET_COMPRESS_METHOD(external);
+	}
+	else
+	{
+		data->extsize = external.va_extinfo;
+		data->compression_method = TOAST_INVALID_COMPRESSION_ID;
+	}
+
+	data->valueid = (Oid8) external.va_valueid;
+	data->toastrelid = external.va_toastrelid;
+}
+
+/*
+ * ondisk_oid_create_external_data
+ *
+ * Create a new varlena based on the input toast_external_data, to be used
+ * when saving a new TOAST value.
+ */
+static struct varlena *
+ondisk_oid_create_external_data(toast_external_data data)
+{
+	struct varlena *result = NULL;
+	varatt_external_oid external;
+
+	external.va_rawsize = data.rawsize;
+
+	if (data.compression_method != TOAST_INVALID_COMPRESSION_ID)
+	{
+		/* Set size and compression method, in a single field. */
+		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(external,
+													 data.extsize,
+													 data.compression_method);
+	}
+	else
+		external.va_extinfo = data.extsize;
+
+	external.va_toastrelid = data.toastrelid;
+	external.va_valueid = (Oid) data.valueid;
+
+	result = (struct varlena *) palloc(TOAST_OID_POINTER_SIZE);
+	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_OID);
+	memcpy(VARDATA_EXTERNAL(result), &external, sizeof(external));
+
+	return result;
+}
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 770dbb5a6104..a68869f58517 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -18,6 +18,7 @@
 #include "access/heapam.h"
 #include "access/heaptoast.h"
 #include "access/table.h"
+#include "access/toast_external.h"
 #include "access/toast_internals.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -124,13 +125,15 @@ toast_save_datum(Relation rel, Datum value,
 	TupleDesc	toasttupDesc;
 	CommandId	mycid = GetCurrentCommandId(true);
 	struct varlena *result;
-	varatt_external_oid toast_pointer;
+	toast_external_data toast_pointer;
 	int32		chunk_seq = 0;
 	char	   *data_p;
 	int32		data_todo;
 	Pointer		dval = DatumGetPointer(value);
 	int			num_indexes;
 	int			validIndex;
+	const toast_external_info *info;
+	uint8		tag = VARTAG_INDIRECT;	/* init value does not matter */
 
 	Assert(!VARATT_IS_EXTERNAL(dval));
 
@@ -162,28 +165,41 @@ toast_save_datum(Relation rel, Datum value,
 	{
 		data_p = VARDATA_SHORT(dval);
 		data_todo = VARSIZE_SHORT(dval) - VARHDRSZ_SHORT;
-		toast_pointer.va_rawsize = data_todo + VARHDRSZ;	/* as if not short */
-		toast_pointer.va_extinfo = data_todo;
+		toast_pointer.rawsize = data_todo + VARHDRSZ;	/* as if not short */
+		toast_pointer.extsize = data_todo;
+
+		/*
+		 * TOAST_INVALID_COMPRESSION_ID means that the varlena is not
+		 * compressed, see toast_get_compression_id().
+		 */
+		toast_pointer.compression_method = TOAST_INVALID_COMPRESSION_ID;
 	}
 	else if (VARATT_IS_COMPRESSED(dval))
 	{
 		data_p = VARDATA(dval);
 		data_todo = VARSIZE(dval) - VARHDRSZ;
 		/* rawsize in a compressed datum is just the size of the payload */
-		toast_pointer.va_rawsize = VARDATA_COMPRESSED_GET_EXTSIZE(dval) + VARHDRSZ;
+		toast_pointer.rawsize = VARDATA_COMPRESSED_GET_EXTSIZE(dval) + VARHDRSZ;
 
 		/* set external size and compression method */
-		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, data_todo,
-													 VARDATA_COMPRESSED_GET_COMPRESS_METHOD(dval));
+		toast_pointer.extsize = data_todo;
+		toast_pointer.compression_method = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(dval);
+
 		/* Assert that the numbers look like it's compressed */
-		Assert(VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer));
+		Assert(TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer));
 	}
 	else
 	{
 		data_p = VARDATA(dval);
 		data_todo = VARSIZE(dval) - VARHDRSZ;
-		toast_pointer.va_rawsize = VARSIZE(dval);
-		toast_pointer.va_extinfo = data_todo;
+		toast_pointer.rawsize = VARSIZE(dval);
+		toast_pointer.extsize = data_todo;
+
+		/*
+		 * TOAST_INVALID_COMPRESSION_ID means that the varlena is not
+		 * compressed, see toast_get_compression_id().
+		 */
+		toast_pointer.compression_method = TOAST_INVALID_COMPRESSION_ID;
 	}
 
 	/*
@@ -195,9 +211,9 @@ toast_save_datum(Relation rel, Datum value,
 	 * if we have to substitute such an OID.
 	 */
 	if (OidIsValid(rel->rd_toastoid))
-		toast_pointer.va_toastrelid = rel->rd_toastoid;
+		toast_pointer.toastrelid = rel->rd_toastoid;
 	else
-		toast_pointer.va_toastrelid = RelationGetRelid(toastrel);
+		toast_pointer.toastrelid = RelationGetRelid(toastrel);
 
 	/*
 	 * Choose an OID to use as the value ID for this toast value.
@@ -214,7 +230,7 @@ toast_save_datum(Relation rel, Datum value,
 	if (!OidIsValid(rel->rd_toastoid))
 	{
 		/* normal case: just choose an unused OID */
-		toast_pointer.va_valueid =
+		toast_pointer.valueid =
 			GetNewOidWithIndex(toastrel,
 							   RelationGetRelid(toastidxs[validIndex]),
 							   (AttrNumber) 1);
@@ -222,18 +238,18 @@ toast_save_datum(Relation rel, Datum value,
 	else
 	{
 		/* rewrite case: check to see if value was in old toast table */
-		toast_pointer.va_valueid = InvalidOid;
+		toast_pointer.valueid = InvalidOid8;
 		if (oldexternal != NULL)
 		{
-			varatt_external_oid old_toast_pointer;
+			toast_external_data old_toast_pointer;
 
 			Assert(VARATT_IS_EXTERNAL_ONDISK(oldexternal));
-			/* Must copy to access aligned fields */
-			VARATT_EXTERNAL_GET_POINTER(old_toast_pointer, oldexternal);
-			if (old_toast_pointer.va_toastrelid == rel->rd_toastoid)
+			toast_external_info_get_data(oldexternal, &old_toast_pointer);
+
+			if (old_toast_pointer.toastrelid == rel->rd_toastoid)
 			{
 				/* This value came from the old toast table; reuse its OID */
-				toast_pointer.va_valueid = old_toast_pointer.va_valueid;
+				toast_pointer.valueid = old_toast_pointer.valueid;
 
 				/*
 				 * There is a corner case here: the table rewrite might have
@@ -253,14 +269,14 @@ toast_save_datum(Relation rel, Datum value,
 				 * be reclaimed by VACUUM.
 				 */
 				if (toastrel_valueid_exists(toastrel,
-											toast_pointer.va_valueid))
+											toast_pointer.valueid))
 				{
 					/* Match, so short-circuit the data storage loop below */
 					data_todo = 0;
 				}
 			}
 		}
-		if (toast_pointer.va_valueid == InvalidOid)
+		if (toast_pointer.valueid == InvalidOid8)
 		{
 			/*
 			 * new value; must choose an OID that doesn't conflict in either
@@ -268,15 +284,23 @@ toast_save_datum(Relation rel, Datum value,
 			 */
 			do
 			{
-				toast_pointer.va_valueid =
+				toast_pointer.valueid =
 					GetNewOidWithIndex(toastrel,
 									   RelationGetRelid(toastidxs[validIndex]),
 									   (AttrNumber) 1);
 			} while (toastid_valueid_exists(rel->rd_toastoid,
-											toast_pointer.va_valueid));
+											toast_pointer.valueid));
 		}
 	}
 
+	/*
+	 * Retrieve the vartag that can be assigned for the new TOAST tuple. This
+	 * depends on the type of TOAST table and its assigned value.
+	 */
+	tag = toast_external_assign_vartag(toast_pointer.toastrelid,
+									   toast_pointer.valueid);
+	info = toast_external_get_info(tag);
+
 	/*
 	 * Split up the item into chunks
 	 */
@@ -300,12 +324,12 @@ toast_save_datum(Relation rel, Datum value,
 		/*
 		 * Calculate the size of this chunk
 		 */
-		chunk_size = Min(TOAST_OID_MAX_CHUNK_SIZE, data_todo);
+		chunk_size = Min(info->maximum_chunk_size, data_todo);
 
 		/*
 		 * Build a tuple and store it
 		 */
-		t_values[0] = ObjectIdGetDatum(toast_pointer.va_valueid);
+		t_values[0] = ObjectIdGetDatum(toast_pointer.valueid);
 		t_values[1] = Int32GetDatum(chunk_seq++);
 		SET_VARSIZE(&chunk_data, chunk_size + VARHDRSZ);
 		memcpy(VARDATA(&chunk_data), data_p, chunk_size);
@@ -361,9 +385,7 @@ toast_save_datum(Relation rel, Datum value,
 	/*
 	 * Create the TOAST pointer value that we'll return
 	 */
-	result = (struct varlena *) palloc(TOAST_OID_POINTER_SIZE);
-	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_OID);
-	memcpy(VARDATA_EXTERNAL(result), &toast_pointer, sizeof(toast_pointer));
+	result = info->create_external_data(toast_pointer);
 
 	return PointerGetDatum(result);
 }
@@ -378,7 +400,7 @@ void
 toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 {
 	struct varlena *attr = (struct varlena *) DatumGetPointer(value);
-	varatt_external_oid toast_pointer;
+	toast_external_data toast_pointer;
 	Relation	toastrel;
 	Relation   *toastidxs;
 	ScanKeyData toastkey;
@@ -391,12 +413,12 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 		return;
 
 	/* Must copy to access aligned fields */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_external_info_get_data(attr, &toast_pointer);
 
 	/*
 	 * Open the toast relation and its indexes
 	 */
-	toastrel = table_open(toast_pointer.va_toastrelid, RowExclusiveLock);
+	toastrel = table_open(toast_pointer.toastrelid, RowExclusiveLock);
 
 	/* Fetch valid relation used for process */
 	validIndex = toast_open_indexes(toastrel,
@@ -410,7 +432,7 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	ScanKeyInit(&toastkey,
 				(AttrNumber) 1,
 				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(toast_pointer.va_valueid));
+				ObjectIdGetDatum(toast_pointer.valueid));
 
 	/*
 	 * Find all the chunks.  (We don't actually care whether we see them in
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index ddde7fcf79a4..230f2a6f35eb 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -28,6 +28,7 @@
 #include "access/genam.h"
 #include "access/heapam.h"
 #include "access/heaptoast.h"
+#include "access/toast_external.h"
 #include "access/toast_helper.h"
 #include "access/toast_internals.h"
 #include "utils/fmgroids.h"
@@ -109,6 +110,7 @@ heap_toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup,
 	Datum		toast_oldvalues[MaxHeapAttributeNumber];
 	ToastAttrInfo toast_attr[MaxHeapAttributeNumber];
 	ToastTupleContext ttc;
+	uint8		tag;
 
 	/*
 	 * Ignore the INSERT_SPECULATIVE option. Speculative insertions/super
@@ -140,6 +142,16 @@ heap_toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup,
 	 * Prepare for toasting
 	 * ----------
 	 */
+
+	/*
+	 * Retrieve the toast pointer size based on the type of external TOAST
+	 * pointer assumed to be used.
+	 */
+
+	/* The default value is invalid, to work as a default. */
+	tag = toast_external_assign_vartag(rel->rd_rel->reltoastrelid, InvalidOid8);
+	ttc.ttc_toast_pointer_size = toast_external_info_get_pointer_size(tag);
+
 	ttc.ttc_rel = rel;
 	ttc.ttc_values = toast_values;
 	ttc.ttc_isnull = toast_isnull;
@@ -640,6 +652,8 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 	int			num_indexes;
 	int			validIndex;
 	int32		max_chunk_size;
+	const toast_external_info *info;
+	uint8		tag = VARTAG_INDIRECT;	/* init value does not matter */
 
 	/* Look for the valid index of toast relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -647,7 +661,11 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 									&toastidxs,
 									&num_indexes);
 
-	max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
+	/* Grab the information for toast_external_data */
+	tag = toast_external_assign_vartag(RelationGetRelid(toastrel), valueid);
+	info = toast_external_get_info(tag);
+
+	max_chunk_size = info->maximum_chunk_size;
 
 	totalchunks = ((attrsize - 1) / max_chunk_size) + 1;
 	startchunk = sliceoffset / max_chunk_size;
diff --git a/src/backend/access/table/toast_helper.c b/src/backend/access/table/toast_helper.c
index 0c58c6c32565..76a7cfe6174e 100644
--- a/src/backend/access/table/toast_helper.c
+++ b/src/backend/access/table/toast_helper.c
@@ -171,8 +171,10 @@ toast_tuple_init(ToastTupleContext *ttc)
  * The column must have attstorage EXTERNAL or EXTENDED if check_main is
  * false, and must have attstorage MAIN if check_main is true.
  *
- * The column must have a minimum size of MAXALIGN(TOAST_OID_POINTER_SIZE);
- * if not, no benefit is to be expected by compressing it.
+ * The column must have a minimum size of MAXALIGN(tcc_toast_pointer_size);
+ * if not, no benefit is to be expected by compressing it.  The TOAST
+ * pointer size is given by the caller, depending on the type of TOAST
+ * table we are dealing with.
  *
  * The return value is the index of the biggest suitable column, or
  * -1 if there is none.
@@ -184,10 +186,14 @@ toast_tuple_find_biggest_attribute(ToastTupleContext *ttc,
 	TupleDesc	tupleDesc = ttc->ttc_rel->rd_att;
 	int			numAttrs = tupleDesc->natts;
 	int			biggest_attno = -1;
-	int32		biggest_size = MAXALIGN(TOAST_OID_POINTER_SIZE);
+	int32		biggest_size = 0;
 	int32		skip_colflags = TOASTCOL_IGNORE;
 	int			i;
 
+	/* Define the lower-bound */
+	biggest_size = MAXALIGN(ttc->ttc_toast_pointer_size);
+	Assert(biggest_size != 0);
+
 	if (for_compression)
 		skip_colflags |= TOASTCOL_INCOMPRESSIBLE;
 
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 79b0c8b40449..eceab3412558 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4274,7 +4274,7 @@ WriteControlFile(void)
 	ControlFile->nameDataLen = NAMEDATALEN;
 	ControlFile->indexMaxKeys = INDEX_MAX_KEYS;
 
-	ControlFile->toast_max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
+	ControlFile->toast_max_chunk_size = TOAST_MAX_CHUNK_SIZE;
 	ControlFile->loblksize = LOBLKSIZE;
 
 	ControlFile->float8ByVal = true;	/* vestigial */
@@ -4517,15 +4517,15 @@ ReadControlFile(void)
 						   "INDEX_MAX_KEYS", ControlFile->indexMaxKeys,
 						   "INDEX_MAX_KEYS", INDEX_MAX_KEYS),
 				 errhint("It looks like you need to recompile or initdb.")));
-	if (ControlFile->toast_max_chunk_size != TOAST_OID_MAX_CHUNK_SIZE)
+	if (ControlFile->toast_max_chunk_size != TOAST_MAX_CHUNK_SIZE)
 		ereport(FATAL,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("database files are incompatible with server"),
 		/* translator: %s is a variable name and %d is its value */
 				 errdetail("The database cluster was initialized with %s %d,"
 						   " but the server was compiled with %s %d.",
-						   "TOAST_OID_MAX_CHUNK_SIZE", ControlFile->toast_max_chunk_size,
-						   "TOAST_OID_MAX_CHUNK_SIZE", (int) TOAST_OID_MAX_CHUNK_SIZE),
+						   "TOAST_MAX_CHUNK_SIZE", ControlFile->toast_max_chunk_size,
+						   "TOAST_MAX_CHUNK_SIZE", (int) TOAST_MAX_CHUNK_SIZE),
 				 errhint("It looks like you need to recompile or initdb.")));
 	if (ControlFile->loblksize != LOBLKSIZE)
 		ereport(FATAL,
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index d61347d11d45..2db447f58ad5 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -92,6 +92,7 @@
 #include "access/detoast.h"
 #include "access/heapam.h"
 #include "access/rewriteheap.h"
+#include "access/toast_external.h"
 #include "access/transam.h"
 #include "access/xact.h"
 #include "access/xlog_internal.h"
@@ -5117,7 +5118,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		struct varlena *varlena;
 
 		/* va_rawsize is the size of the original datum -- including header */
-		varatt_external_oid toast_pointer;
+		toast_external_data toast_pointer;
 		struct varatt_indirect redirect_pointer;
 		struct varlena *new_datum = NULL;
 		struct varlena *reconstructed;
@@ -5147,8 +5148,8 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		if (!VARATT_IS_EXTERNAL(varlena))
 			continue;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, varlena);
-		toast_valueid = toast_pointer.va_valueid;
+		toast_external_info_get_data(varlena, &toast_pointer);
+		toast_valueid = toast_pointer.valueid;
 
 		/*
 		 * Check whether the toast tuple changed, replace if so.
@@ -5166,7 +5167,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 
 		free[natt] = true;
 
-		reconstructed = palloc0(toast_pointer.va_rawsize);
+		reconstructed = palloc0(toast_pointer.rawsize);
 
 		ent->reconstructed = reconstructed;
 
@@ -5191,10 +5192,10 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 				   VARSIZE(chunk) - VARHDRSZ);
 			data_done += VARSIZE(chunk) - VARHDRSZ;
 		}
-		Assert(data_done == VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer));
+		Assert(data_done == toast_pointer.extsize);
 
 		/* make sure its marked as compressed or not */
-		if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+		if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 			SET_VARSIZE_COMPRESSED(reconstructed, data_done + VARHDRSZ);
 		else
 			SET_VARSIZE(reconstructed, data_done + VARHDRSZ);
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 4aff647fccfd..78b3e65b2396 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -19,6 +19,7 @@
 
 #include "access/detoast.h"
 #include "access/toast_compression.h"
+#include "access/toast_external.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_type.h"
 #include "common/hashfn.h"
@@ -4211,7 +4212,7 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 {
 	int			typlen;
 	struct varlena *attr;
-	varatt_external_oid toast_pointer;
+	Oid8		toast_valueid;
 
 	/* On first call, get the input type's typlen, and save at *fn_extra */
 	if (fcinfo->flinfo->fn_extra == NULL)
@@ -4238,9 +4239,9 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		PG_RETURN_NULL();
 
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_valueid = toast_external_info_get_valueid(attr);
 
-	PG_RETURN_OID(toast_pointer.va_valueid);
+	PG_RETURN_OID(toast_valueid);
 }
 
 /*
diff --git a/src/bin/pg_resetwal/pg_resetwal.c b/src/bin/pg_resetwal/pg_resetwal.c
index 638b41c922ba..7a4e4eb95706 100644
--- a/src/bin/pg_resetwal/pg_resetwal.c
+++ b/src/bin/pg_resetwal/pg_resetwal.c
@@ -717,7 +717,7 @@ GuessControlValues(void)
 	ControlFile.xlog_seg_size = DEFAULT_XLOG_SEG_SIZE;
 	ControlFile.nameDataLen = NAMEDATALEN;
 	ControlFile.indexMaxKeys = INDEX_MAX_KEYS;
-	ControlFile.toast_max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
+	ControlFile.toast_max_chunk_size = TOAST_MAX_CHUNK_SIZE;
 	ControlFile.loblksize = LOBLKSIZE;
 	ControlFile.float8ByVal = true; /* vestigial */
 
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 7ec6cef118fb..9cf3c081bf01 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -16,6 +16,7 @@
 #include "access/multixact.h"
 #include "access/relation.h"
 #include "access/table.h"
+#include "access/toast_external.h"
 #include "access/toast_internals.h"
 #include "access/visibilitymap.h"
 #include "access/xact.h"
@@ -73,7 +74,8 @@ typedef enum SkipPages
  */
 typedef struct ToastedAttribute
 {
-	varatt_external_oid toast_pointer;
+	toast_external_data toast_pointer;
+	const toast_external_info *info;
 	BlockNumber blkno;			/* block in main table */
 	OffsetNumber offnum;		/* offset in main table */
 	AttrNumber	attnum;			/* attribute in main table */
@@ -1564,9 +1566,9 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	Oid8		toast_valueid;
 	int32		max_chunk_size;
 
-	toast_valueid = ta->toast_pointer.va_valueid;
+	toast_valueid = ta->toast_pointer.valueid;
 
-	max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
+	max_chunk_size = ta->info->maximum_chunk_size;
 	last_chunk_seq = (extsize - 1) / max_chunk_size;
 
 	/* Sanity-check the sequence number. */
@@ -1672,7 +1674,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	uint16		infomask;
 	Oid8		toast_pointer_valueid;
 	CompactAttribute *thisatt;
-	varatt_external_oid toast_pointer;
+	toast_external_data toast_pointer;
 
 	infomask = ctx->tuphdr->t_infomask;
 	thisatt = TupleDescCompactAttr(RelationGetDescr(ctx->rel), ctx->attnum);
@@ -1778,24 +1780,24 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	/*
 	 * Must copy attr into toast_pointer for alignment considerations
 	 */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-	toast_pointer_valueid = toast_pointer.va_valueid;
+	toast_external_info_get_data(attr, &toast_pointer);
+	toast_pointer_valueid = toast_pointer.valueid;
 
 	/* Toasted attributes too large to be untoasted should never be stored */
-	if (toast_pointer.va_rawsize > VARLENA_SIZE_LIMIT)
+	if (toast_pointer.rawsize > VARLENA_SIZE_LIMIT)
 		report_corruption(ctx,
 						  psprintf("toast value " OID8_FORMAT " rawsize %d exceeds limit %d",
 								   toast_pointer_valueid,
-								   toast_pointer.va_rawsize,
+								   toast_pointer.rawsize,
 								   VARLENA_SIZE_LIMIT));
 
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+	if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 	{
 		ToastCompressionId cmid;
 		bool		valid = false;
 
 		/* Compressed attributes should have a valid compression method */
-		cmid = TOAST_COMPRESS_METHOD(&toast_pointer);
+		cmid = toast_pointer.compression_method;
 		switch (cmid)
 		{
 				/* List of all valid compression method IDs */
@@ -1849,7 +1851,8 @@ check_tuple_attribute(HeapCheckContext *ctx)
 
 		ta = (ToastedAttribute *) palloc0(sizeof(ToastedAttribute));
 
-		VARATT_EXTERNAL_GET_POINTER(ta->toast_pointer, attr);
+		toast_external_info_get_data(attr, &ta->toast_pointer);
+		ta->info = toast_external_get_info(VARTAG_EXTERNAL(attr));
 		ta->blkno = ctx->blkno;
 		ta->offnum = ctx->offnum;
 		ta->attnum = ctx->attnum;
@@ -1876,9 +1879,11 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	int32		expected_chunk_seq = 0;
 	int32		last_chunk_seq;
 	Oid8		toast_valueid;
-	int32		max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
+	int32		max_chunk_size;
 
-	extsize = VARATT_EXTERNAL_GET_EXTSIZE(ta->toast_pointer);
+	extsize = ta->toast_pointer.extsize;
+
+	max_chunk_size = ta->info->maximum_chunk_size;
 	last_chunk_seq = (extsize - 1) / max_chunk_size;
 
 	/*
@@ -1887,7 +1892,7 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	ScanKeyInit(&toastkey,
 				(AttrNumber) 1,
 				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(ta->toast_pointer.va_valueid));
+				ObjectIdGetDatum(ta->toast_pointer.valueid));
 
 	/*
 	 * Check if any chunks for this toasted object exist in the toast table,
@@ -1907,7 +1912,7 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	}
 	systable_endscan_ordered(toastscan);
 
-	toast_valueid = ta->toast_pointer.va_valueid;
+	toast_valueid = ta->toast_pointer.valueid;
 
 	if (!found_toasttup)
 		report_toast_corruption(ctx, ta,
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 37f26f6c6b75..7288d7e10d14 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -4140,6 +4140,8 @@ timeout_params
 timerCA
 tlist_vinfo
 toast_compress_header
+toast_external_data
+toast_external_info
 tokenize_error_callback_arg
 transferMode
 transfer_thread_arg
-- 
2.51.0

v7-0006-Move-static-inline-routines-of-varatt_external_oi.patchtext/x-diff; charset=us-asciiDownload
From a6debb33f37ccb67ca1ad7a89938cbda1cb29d90 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 8 Aug 2025 15:40:04 +0900
Subject: [PATCH v7 06/15] Move static inline routines of varatt_external_oid
 to toast_external.c

This isolates most of the knowledge of varatt_external_oid into the
local area where it is manipulated through the toast_external transition
type, with the backend code not requiring it.  Extension code should not
need it either, as toast_external should be the layer to use when
looking at external on-dist TOAST varlenas.
---
 src/include/varatt.h                       | 31 -----------------
 src/backend/access/common/toast_external.c | 40 ++++++++++++++++++++--
 2 files changed, 37 insertions(+), 34 deletions(-)

diff --git a/src/include/varatt.h b/src/include/varatt.h
index 790d9f844c91..035c0f95e5b6 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -513,22 +513,6 @@ VARDATA_COMPRESSED_GET_COMPRESS_METHOD(const void *PTR)
 	return ((varattrib_4b *) PTR)->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS;
 }
 
-/*
- * Same for external Datums; but note argument is a struct
- * varatt_external_oid.
- */
-static inline Size
-VARATT_EXTERNAL_GET_EXTSIZE(varatt_external_oid toast_pointer)
-{
-	return toast_pointer.va_extinfo & VARLENA_EXTSIZE_MASK;
-}
-
-static inline uint32
-VARATT_EXTERNAL_GET_COMPRESS_METHOD(varatt_external_oid toast_pointer)
-{
-	return toast_pointer.va_extinfo >> VARLENA_EXTSIZE_BITS;
-}
-
 /* Set size and compress method of an externally-stored varlena datum */
 /* This has to remain a macro; beware multiple evaluations! */
 #define VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, len, cm) \
@@ -538,19 +522,4 @@ VARATT_EXTERNAL_GET_COMPRESS_METHOD(varatt_external_oid toast_pointer)
 		((toast_pointer).va_extinfo = \
 			(len) | ((uint32) (cm) << VARLENA_EXTSIZE_BITS)); \
 	} while (0)
-
-/*
- * Testing whether an externally-stored value is compressed now requires
- * comparing size stored in va_extinfo (the actual length of the external data)
- * to rawsize (the original uncompressed datum's size).  The latter includes
- * VARHDRSZ overhead, the former doesn't.  We never use compression unless it
- * actually saves space, so we expect either equality or less-than.
- */
-static inline bool
-VARATT_EXTERNAL_IS_COMPRESSED(varatt_external_oid toast_pointer)
-{
-	return VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) <
-		(Size) (toast_pointer.va_rawsize - VARHDRSZ);
-}
-
 #endif
diff --git a/src/backend/access/common/toast_external.c b/src/backend/access/common/toast_external.c
index 2154152b8bfb..4c500720e0d1 100644
--- a/src/backend/access/common/toast_external.c
+++ b/src/backend/access/common/toast_external.c
@@ -26,6 +26,40 @@ static void ondisk_oid_to_external_data(struct varlena *attr,
 										toast_external_data *data);
 static struct varlena *ondisk_oid_create_external_data(toast_external_data data);
 
+/*
+ * Decompressed size of an on-disk varlena; but note argument is a struct
+ * varatt_external_oid.
+ */
+static inline Size
+varatt_external_oid_get_extsize(varatt_external_oid toast_pointer)
+{
+	return toast_pointer.va_extinfo & VARLENA_EXTSIZE_MASK;
+}
+
+/*
+ * Compression method of an on-disk varlena; but note argument is a struct
+ *  varatt_external_oid.
+ */
+static inline uint32
+varatt_external_oid_get_compress_method(varatt_external_oid toast_pointer)
+{
+	return toast_pointer.va_extinfo >> VARLENA_EXTSIZE_BITS;
+}
+
+/*
+ * Testing whether an externally-stored TOAST value is compressed now requires
+ * comparing size stored in va_extinfo (the actual length of the external data)
+ * to rawsize (the original uncompressed datum's size).  The latter includes
+ * VARHDRSZ overhead, the former doesn't.  We never use compression unless it
+ * actually saves space, so we expect either equality or less-than.
+ */
+static inline bool
+varatt_external_oid_is_compressed(varatt_external_oid toast_pointer)
+{
+	return varatt_external_oid_get_extsize(toast_pointer) <
+		(Size) (toast_pointer.va_rawsize - VARHDRSZ);
+}
+
 /*
  * Size of an EXTERNAL datum that contains a standard TOAST pointer (OID
  * value).
@@ -146,10 +180,10 @@ ondisk_oid_to_external_data(struct varlena *attr, toast_external_data *data)
 	 * External size and compression methods are stored in the same field,
 	 * extract.
 	 */
-	if (VARATT_EXTERNAL_IS_COMPRESSED(external))
+	if (varatt_external_oid_is_compressed(external))
 	{
-		data->extsize = VARATT_EXTERNAL_GET_EXTSIZE(external);
-		data->compression_method = VARATT_EXTERNAL_GET_COMPRESS_METHOD(external);
+		data->extsize = varatt_external_oid_get_extsize(external);
+		data->compression_method = varatt_external_oid_get_compress_method(external);
 	}
 	else
 	{
-- 
2.51.0

v7-0007-Split-VARATT_EXTERNAL_GET_POINTER-for-indirect-an.patchtext/x-diff; charset=us-asciiDownload
From b6470b8fa4f09d1105f597c344a64981b4c47415 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 13 Aug 2025 19:38:50 +0900
Subject: [PATCH v7 07/15] Split VARATT_EXTERNAL_GET_POINTER for indirect and
 OID TOAST pointers

VARATT_EXTERNAL_GET_POINTER() is renamed to
VARATT_INDIRECT_GET_POINTER() with the external on-disk TOAST pointers
for OID values being now located within toast_external.c, splitting both
concepts completely.
---
 src/include/access/detoast.h               | 16 ++++++++--------
 src/backend/access/common/detoast.c        | 10 +++++-----
 src/backend/access/common/toast_external.c | 21 ++++++++++++++++++++-
 src/backend/utils/adt/expandeddatum.c      |  2 +-
 4 files changed, 34 insertions(+), 15 deletions(-)

diff --git a/src/include/access/detoast.h b/src/include/access/detoast.h
index 2f71fbd95f88..31e9786848ef 100644
--- a/src/include/access/detoast.h
+++ b/src/include/access/detoast.h
@@ -13,17 +13,17 @@
 #define DETOAST_H
 
 /*
- * Macro to fetch the possibly-unaligned contents of an EXTERNAL datum
- * into a local "varatt_external_*" toast pointer, as supported
- * in toast_external.h and varatt.h.  This should be just a memcpy, but
- * some versions of gcc seem to produce broken code that assumes the datum
- * contents are aligned.  Introducing an explicit intermediate
- * "varattrib_1b_e *" variable seems to fix it.
+ * Macro to fetch the possibly-unaligned contents of an indirect datum
+ * into a local "varatt_indirect" toast pointer, as supported
+ * in varatt.h.  This should be just a memcpy, but some versions of gcc
+ * seem to produce broken code that assumes the datum contents are aligned.
+ * Introducing an explicit intermediate "varattrib_1b_e *" variable seems
+ * to fix it.
  */
-#define VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr) \
+#define VARATT_INDIRECT_GET_POINTER(toast_pointer, attr) \
 do { \
 	varattrib_1b_e *attre = (varattrib_1b_e *) (attr); \
-	Assert(VARATT_IS_EXTERNAL(attre)); \
+	Assert(VARATT_IS_EXTERNAL_INDIRECT(attre)); \
 	Assert(VARSIZE_EXTERNAL(attre) == sizeof(toast_pointer) + VARHDRSZ_EXTERNAL); \
 	memcpy(&(toast_pointer), VARDATA_EXTERNAL(attre), sizeof(toast_pointer)); \
 } while (0)
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 8531c27439e4..b645988667f0 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -61,7 +61,7 @@ detoast_external_attr(struct varlena *attr)
 		 */
 		struct varatt_indirect redirect;
 
-		VARATT_EXTERNAL_GET_POINTER(redirect, attr);
+		VARATT_INDIRECT_GET_POINTER(redirect, attr);
 		attr = (struct varlena *) redirect.pointer;
 
 		/* nested indirect Datums aren't allowed */
@@ -138,7 +138,7 @@ detoast_attr(struct varlena *attr)
 		 */
 		struct varatt_indirect redirect;
 
-		VARATT_EXTERNAL_GET_POINTER(redirect, attr);
+		VARATT_INDIRECT_GET_POINTER(redirect, attr);
 		attr = (struct varlena *) redirect.pointer;
 
 		/* nested indirect Datums aren't allowed */
@@ -268,7 +268,7 @@ detoast_attr_slice(struct varlena *attr,
 	{
 		struct varatt_indirect redirect;
 
-		VARATT_EXTERNAL_GET_POINTER(redirect, attr);
+		VARATT_INDIRECT_GET_POINTER(redirect, attr);
 
 		/* nested indirect Datums aren't allowed */
 		Assert(!VARATT_IS_EXTERNAL_INDIRECT(redirect.pointer));
@@ -561,7 +561,7 @@ toast_raw_datum_size(Datum value)
 	{
 		struct varatt_indirect toast_pointer;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		VARATT_INDIRECT_GET_POINTER(toast_pointer, attr);
 
 		/* nested indirect Datums aren't allowed */
 		Assert(!VARATT_IS_EXTERNAL_INDIRECT(toast_pointer.pointer));
@@ -618,7 +618,7 @@ toast_datum_size(Datum value)
 	{
 		struct varatt_indirect toast_pointer;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		VARATT_INDIRECT_GET_POINTER(toast_pointer, attr);
 
 		/* nested indirect Datums aren't allowed */
 		Assert(!VARATT_IS_EXTERNAL_INDIRECT(attr));
diff --git a/src/backend/access/common/toast_external.c b/src/backend/access/common/toast_external.c
index 4c500720e0d1..e2f0a9dc1c50 100644
--- a/src/backend/access/common/toast_external.c
+++ b/src/backend/access/common/toast_external.c
@@ -26,6 +26,25 @@ static void ondisk_oid_to_external_data(struct varlena *attr,
 										toast_external_data *data);
 static struct varlena *ondisk_oid_create_external_data(toast_external_data data);
 
+/*
+ * Fetch the possibly-unaligned contents of an on-disk external TOAST with
+ * OID values into a local "varatt_external_oid" pointer.
+ *
+ * This should be just a memcpy, but some versions of gcc seem to produce
+ * broken code that assumes the datum contents are aligned.  Introducing
+ * an explicit intermediate "varattrib_1b_e *" variable seems to fix it.
+ */
+static inline void
+varatt_external_oid_get_pointer(varatt_external_oid *toast_pointer,
+								struct varlena *attr)
+{
+	varattrib_1b_e *attre = (varattrib_1b_e *) attr;
+
+	Assert(VARATT_IS_EXTERNAL_ONDISK_OID(attre));
+	Assert(VARSIZE_EXTERNAL(attre) == sizeof(varatt_external_oid) + VARHDRSZ_EXTERNAL);
+	memcpy(toast_pointer, VARDATA_EXTERNAL(attre), sizeof(varatt_external_oid));
+}
+
 /*
  * Decompressed size of an on-disk varlena; but note argument is a struct
  * varatt_external_oid.
@@ -173,7 +192,7 @@ ondisk_oid_to_external_data(struct varlena *attr, toast_external_data *data)
 {
 	varatt_external_oid external;
 
-	VARATT_EXTERNAL_GET_POINTER(external, attr);
+	varatt_external_oid_get_pointer(&external, attr);
 	data->rawsize = external.va_rawsize;
 
 	/*
diff --git a/src/backend/utils/adt/expandeddatum.c b/src/backend/utils/adt/expandeddatum.c
index 6b4b8eaf005c..4c04671d23ed 100644
--- a/src/backend/utils/adt/expandeddatum.c
+++ b/src/backend/utils/adt/expandeddatum.c
@@ -23,7 +23,7 @@
  * Given a Datum that is an expanded-object reference, extract the pointer.
  *
  * This is a bit tedious since the pointer may not be properly aligned;
- * compare VARATT_EXTERNAL_GET_POINTER().
+ * compare VARATT_INDIRECT_GET_POINTER().
  */
 ExpandedObjectHeader *
 DatumGetEOHP(Datum d)
-- 
2.51.0

v7-0008-Switch-pg_column_toast_chunk_id-return-value-from.patchtext/x-diff; charset=us-asciiDownload
From efa4c0affc9d319a2e04319d8a48235fc7eabe3c Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 13 Aug 2025 19:55:39 +0900
Subject: [PATCH v7 08/15] Switch pg_column_toast_chunk_id() return value from
 oid to oid8

This is required for a follow-up patch that will add support for 8-byte
TOAST values, with this function being changed so as it is able to
support the largest TOAST value type available.

XXX: Bump catalog version.
---
 src/include/catalog/pg_proc.dat              | 2 +-
 src/backend/utils/adt/varlena.c              | 2 +-
 src/test/regress/expected/misc_functions.out | 2 +-
 src/test/regress/sql/misc_functions.sql      | 2 +-
 doc/src/sgml/func/func-admin.sgml            | 2 +-
 5 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index acca3f95b4e7..0e9cf3455dab 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7756,7 +7756,7 @@
   proargtypes => 'any', prosrc => 'pg_column_compression' },
 { oid => '6316', descr => 'chunk ID of on-disk TOASTed value',
   proname => 'pg_column_toast_chunk_id', provolatile => 's',
-  prorettype => 'oid', proargtypes => 'any',
+  prorettype => 'oid8', proargtypes => 'any',
   prosrc => 'pg_column_toast_chunk_id' },
 { oid => '2322',
   descr => 'total disk space usage for the specified tablespace',
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 78b3e65b2396..a176a4fab0e5 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -4241,7 +4241,7 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 
 	toast_valueid = toast_external_info_get_valueid(attr);
 
-	PG_RETURN_OID(toast_valueid);
+	PG_RETURN_OID8(toast_valueid);
 }
 
 /*
diff --git a/src/test/regress/expected/misc_functions.out b/src/test/regress/expected/misc_functions.out
index 36164a99c832..27a0f763a3fd 100644
--- a/src/test/regress/expected/misc_functions.out
+++ b/src/test/regress/expected/misc_functions.out
@@ -881,7 +881,7 @@ SELECT t.relname AS toastrel FROM pg_class c
   WHERE c.relname = 'test_chunk_id'
 \gset
 SELECT pg_column_toast_chunk_id(a) IS NULL,
-  pg_column_toast_chunk_id(b) IN (SELECT chunk_id FROM pg_toast.:toastrel)
+  pg_column_toast_chunk_id(b) IN (SELECT chunk_id::oid8 FROM pg_toast.:toastrel)
   FROM test_chunk_id;
  ?column? | ?column? 
 ----------+----------
diff --git a/src/test/regress/sql/misc_functions.sql b/src/test/regress/sql/misc_functions.sql
index 23792c4132a1..5fb79f315e37 100644
--- a/src/test/regress/sql/misc_functions.sql
+++ b/src/test/regress/sql/misc_functions.sql
@@ -395,7 +395,7 @@ SELECT t.relname AS toastrel FROM pg_class c
   WHERE c.relname = 'test_chunk_id'
 \gset
 SELECT pg_column_toast_chunk_id(a) IS NULL,
-  pg_column_toast_chunk_id(b) IN (SELECT chunk_id FROM pg_toast.:toastrel)
+  pg_column_toast_chunk_id(b) IN (SELECT chunk_id::oid8 FROM pg_toast.:toastrel)
   FROM test_chunk_id;
 DROP TABLE test_chunk_id;
 DROP FUNCTION explain_mask_costs(text, bool, bool, bool, bool);
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 1b465bc8ba71..df570fc2c5c9 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1590,7 +1590,7 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
          <primary>pg_column_toast_chunk_id</primary>
         </indexterm>
         <function>pg_column_toast_chunk_id</function> ( <type>"any"</type> )
-        <returnvalue>oid</returnvalue>
+        <returnvalue>oid8</returnvalue>
        </para>
        <para>
         Shows the <structfield>chunk_id</structfield> of an on-disk
-- 
2.51.0

v7-0009-Add-catcache-support-for-OID8OID.patchtext/x-diff; charset=us-asciiDownload
From 651bf4cb7b964ef98b681c8883566db546f87ec3 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 13 Aug 2025 20:00:30 +0900
Subject: [PATCH v7 09/15] Add catcache support for OID8OID

This is required to be able to do catalog cache lookups of oid8 fields
for toast values of the same type.
---
 src/backend/utils/cache/catcache.c | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index fd1c7be8f53d..62251403789d 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -240,6 +240,18 @@ int4hashfast(Datum datum)
 	return murmurhash32((int32) DatumGetInt32(datum));
 }
 
+static bool
+oid8eqfast(Datum a, Datum b)
+{
+	return DatumGetObjectId8(a) == DatumGetObjectId8(b);
+}
+
+static uint32
+oid8hashfast(Datum datum)
+{
+	return murmurhash64(DatumGetObjectId8(datum));
+}
+
 static bool
 texteqfast(Datum a, Datum b)
 {
@@ -300,6 +312,11 @@ GetCCHashEqFuncs(Oid keytype, CCHashFN *hashfunc, RegProcedure *eqfunc, CCFastEq
 			*fasteqfunc = int4eqfast;
 			*eqfunc = F_INT4EQ;
 			break;
+		case OID8OID:
+			*hashfunc = oid8hashfast;
+			*fasteqfunc = oid8eqfast;
+			*eqfunc = F_OID8EQ;
+			break;
 		case TEXTOID:
 			*hashfunc = texthashfast;
 			*fasteqfunc = texteqfast;
-- 
2.51.0

v7-0010-Add-support-for-TOAST-chunk_id-type-in-binary-upg.patchtext/x-diff; charset=us-asciiDownload
From 5fd4dc6c2614ad77fe5ae81b0f02556eb33621cf Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 10:57:59 +0900
Subject: [PATCH v7 10/15] Add support for TOAST chunk_id type in binary
 upgrades

This commit adds a new function, which would set the type of a chunk_id
attribute for a TOAST table across upgrades.  This piece currently works
only with chunk_id = OIDOID, but it is required in a follow-up patch
where support for chunk_id = OID8OID is supported on top of the existing
one.
---
 src/include/catalog/binary_upgrade.h          |  1 +
 src/include/catalog/pg_proc.dat               |  4 ++++
 src/backend/catalog/heap.c                    |  1 +
 src/backend/catalog/toasting.c                | 20 ++++++++++++++++++-
 src/backend/utils/adt/pg_upgrade_support.c    | 11 ++++++++++
 src/bin/pg_dump/pg_dump.c                     | 10 +++++++++-
 .../expected/spgist_name_ops.out              |  6 ++++--
 7 files changed, 49 insertions(+), 4 deletions(-)

diff --git a/src/include/catalog/binary_upgrade.h b/src/include/catalog/binary_upgrade.h
index 6fcc59edebd8..3deb0423d795 100644
--- a/src/include/catalog/binary_upgrade.h
+++ b/src/include/catalog/binary_upgrade.h
@@ -29,6 +29,7 @@ extern PGDLLIMPORT Oid binary_upgrade_next_index_pg_class_oid;
 extern PGDLLIMPORT RelFileNumber binary_upgrade_next_index_pg_class_relfilenumber;
 extern PGDLLIMPORT Oid binary_upgrade_next_toast_pg_class_oid;
 extern PGDLLIMPORT RelFileNumber binary_upgrade_next_toast_pg_class_relfilenumber;
+extern PGDLLIMPORT Oid binary_upgrade_next_toast_chunk_id_typoid;
 
 extern PGDLLIMPORT Oid binary_upgrade_next_pg_enum_oid;
 extern PGDLLIMPORT Oid binary_upgrade_next_pg_authid_oid;
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0e9cf3455dab..39ec282645ba 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11771,6 +11771,10 @@
   proname => 'binary_upgrade_set_next_toast_pg_class_oid', provolatile => 'v',
   proparallel => 'r', prorettype => 'void', proargtypes => 'oid',
   prosrc => 'binary_upgrade_set_next_toast_pg_class_oid' },
+{ oid => '8219', descr => 'for use by pg_upgrade',
+  proname => 'binary_upgrade_set_next_toast_chunk_id_typoid', provolatile => 'v',
+  proparallel => 'r', prorettype => 'void', proargtypes => 'oid',
+  prosrc => 'binary_upgrade_set_next_toast_chunk_id_typoid' },
 { oid => '3589', descr => 'for use by pg_upgrade',
   proname => 'binary_upgrade_set_next_pg_enum_oid', provolatile => 'v',
   proparallel => 'r', prorettype => 'void', proargtypes => 'oid',
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index fd6537567ea2..e5cc98937055 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -80,6 +80,7 @@
 /* Potentially set by pg_upgrade_support functions */
 Oid			binary_upgrade_next_heap_pg_class_oid = InvalidOid;
 Oid			binary_upgrade_next_toast_pg_class_oid = InvalidOid;
+Oid			binary_upgrade_next_toast_chunk_id_typoid = InvalidOid;
 RelFileNumber binary_upgrade_next_heap_pg_class_relfilenumber = InvalidRelFileNumber;
 RelFileNumber binary_upgrade_next_toast_pg_class_relfilenumber = InvalidRelFileNumber;
 
diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c
index 874a8fc89adb..f1d76d8acd51 100644
--- a/src/backend/catalog/toasting.c
+++ b/src/backend/catalog/toasting.c
@@ -145,6 +145,7 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	int16		coloptions[2];
 	ObjectAddress baseobject,
 				toastobject;
+	Oid			toast_chunkid_typid = OIDOID;
 
 	/*
 	 * Is it already toasted?
@@ -183,6 +184,23 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 		 */
 		if (!OidIsValid(binary_upgrade_next_toast_pg_class_oid))
 			return false;
+
+		/*
+		 * The attribute type for chunk_id should have been set when requesting
+		 * a TOAST table creation.
+		 */
+		if (!OidIsValid(binary_upgrade_next_toast_chunk_id_typoid))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("toast chunk_id type not set while in binary upgrade mode")));
+		if (binary_upgrade_next_toast_chunk_id_typoid != OIDOID)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("cannot support toast chunk_id type %u in binary upgrade mode",
+							binary_upgrade_next_toast_chunk_id_typoid)));
+
+		toast_chunkid_typid = binary_upgrade_next_toast_chunk_id_typoid;
+		binary_upgrade_next_toast_chunk_id_typoid = InvalidOid;
 	}
 
 	/*
@@ -204,7 +222,7 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	tupdesc = CreateTemplateTupleDesc(3);
 	TupleDescInitEntry(tupdesc, (AttrNumber) 1,
 					   "chunk_id",
-					   OIDOID,
+					   toast_chunkid_typid,
 					   -1, 0);
 	TupleDescInitEntry(tupdesc, (AttrNumber) 2,
 					   "chunk_seq",
diff --git a/src/backend/utils/adt/pg_upgrade_support.c b/src/backend/utils/adt/pg_upgrade_support.c
index a4f8b4faa90d..200ffcdbab44 100644
--- a/src/backend/utils/adt/pg_upgrade_support.c
+++ b/src/backend/utils/adt/pg_upgrade_support.c
@@ -149,6 +149,17 @@ binary_upgrade_set_next_toast_pg_class_oid(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+Datum
+binary_upgrade_set_next_toast_chunk_id_typoid(PG_FUNCTION_ARGS)
+{
+	Oid			typoid = PG_GETARG_OID(0);
+
+	CHECK_IS_BINARY_UPGRADE;
+	binary_upgrade_next_toast_chunk_id_typoid = typoid;
+
+	PG_RETURN_VOID();
+}
+
 Datum
 binary_upgrade_set_next_toast_relfilenode(PG_FUNCTION_ARGS)
 {
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 9fc3671cb350..44fa0fa1b4ea 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -103,6 +103,7 @@ typedef struct
 	RelFileNumber relfilenumber;	/* object filenode */
 	Oid			toast_oid;		/* toast table OID */
 	RelFileNumber toast_relfilenumber;	/* toast table filenode */
+	Oid			toast_chunk_id_typoid;	/* type of chunk_id attribute */
 	Oid			toast_index_oid;	/* toast table index OID */
 	RelFileNumber toast_index_relfilenumber;	/* toast table index filenode */
 } BinaryUpgradeClassOidItem;
@@ -5799,7 +5800,10 @@ collectBinaryUpgradeClassOids(Archive *fout)
 	const char *query;
 
 	query = "SELECT c.oid, c.relkind, c.relfilenode, c.reltoastrelid, "
-		"ct.relfilenode, i.indexrelid, cti.relfilenode "
+		"ct.relfilenode, i.indexrelid, cti.relfilenode, "
+		"(SELECT a.atttypid FROM pg_attribute AS a "
+		"  WHERE a.attrelid = c.reltoastrelid AND attname = 'chunk_id'::text) "
+		"  AS toastchunktypid "
 		"FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_index i "
 		"ON (c.reltoastrelid = i.indrelid AND i.indisvalid) "
 		"LEFT JOIN pg_catalog.pg_class ct ON (c.reltoastrelid = ct.oid) "
@@ -5821,6 +5825,7 @@ collectBinaryUpgradeClassOids(Archive *fout)
 		binaryUpgradeClassOids[i].toast_relfilenumber = atooid(PQgetvalue(res, i, 4));
 		binaryUpgradeClassOids[i].toast_index_oid = atooid(PQgetvalue(res, i, 5));
 		binaryUpgradeClassOids[i].toast_index_relfilenumber = atooid(PQgetvalue(res, i, 6));
+		binaryUpgradeClassOids[i].toast_chunk_id_typoid = atooid(PQgetvalue(res, i, 7));
 	}
 
 	PQclear(res);
@@ -5885,6 +5890,9 @@ binary_upgrade_set_pg_class_oids(Archive *fout,
 			appendPQExpBuffer(upgrade_buffer,
 							  "SELECT pg_catalog.binary_upgrade_set_next_toast_relfilenode('%u'::pg_catalog.oid);\n",
 							  entry->toast_relfilenumber);
+			appendPQExpBuffer(upgrade_buffer,
+							  "SELECT pg_catalog.binary_upgrade_set_next_toast_chunk_id_typoid('%u'::pg_catalog.oid);\n",
+							  entry->toast_chunk_id_typoid);
 
 			/* every toast table has an index */
 			appendPQExpBuffer(upgrade_buffer,
diff --git a/src/test/modules/spgist_name_ops/expected/spgist_name_ops.out b/src/test/modules/spgist_name_ops/expected/spgist_name_ops.out
index 1ee65ede2430..35e59d0cd83c 100644
--- a/src/test/modules/spgist_name_ops/expected/spgist_name_ops.out
+++ b/src/test/modules/spgist_name_ops/expected/spgist_name_ops.out
@@ -61,9 +61,10 @@ select * from t
  binary_upgrade_set_next_pg_enum_oid                  |    | binary_upgrade_set_next_pg_enum_oid
  binary_upgrade_set_next_pg_tablespace_oid            |    | binary_upgrade_set_next_pg_tablespace_oid
  binary_upgrade_set_next_pg_type_oid                  |    | binary_upgrade_set_next_pg_type_oid
+ binary_upgrade_set_next_toast_chunk_id_typoid        |    | binary_upgrade_set_next_toast_chunk_id_typoid
  binary_upgrade_set_next_toast_pg_class_oid           |  1 | binary_upgrade_set_next_toast_pg_class_oid
  binary_upgrade_set_next_toast_relfilenode            |    | binary_upgrade_set_next_toast_relfilenode
-(13 rows)
+(14 rows)
 
 -- Verify clean failure when INCLUDE'd columns result in overlength tuple
 -- The error message details are platform-dependent, so show only SQLSTATE
@@ -110,9 +111,10 @@ select * from t
  binary_upgrade_set_next_pg_enum_oid                  |    | binary_upgrade_set_next_pg_enum_oid
  binary_upgrade_set_next_pg_tablespace_oid            |    | binary_upgrade_set_next_pg_tablespace_oid
  binary_upgrade_set_next_pg_type_oid                  |    | binary_upgrade_set_next_pg_type_oid
+ binary_upgrade_set_next_toast_chunk_id_typoid        |    | binary_upgrade_set_next_toast_chunk_id_typoid
  binary_upgrade_set_next_toast_pg_class_oid           |  1 | binary_upgrade_set_next_toast_pg_class_oid
  binary_upgrade_set_next_toast_relfilenode            |    | binary_upgrade_set_next_toast_relfilenode
-(13 rows)
+(14 rows)
 
 \set VERBOSITY sqlstate
 insert into t values(repeat('xyzzy', 12), 42, repeat('xyzzy', 4000));
-- 
2.51.0

v7-0011-Enlarge-OID-generation-to-8-bytes.patchtext/x-diff; charset=us-asciiDownload
From fa5e0d60c835715bbc0b8d663f0e114bdaf866ef Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 12:15:15 +0900
Subject: [PATCH v7 11/15] Enlarge OID generation to 8 bytes

This adds a new routine called GetNewObjectId8() in varsup.c, which is
able to retrieve a 64b OID.  GetNewObjectId() is kept compatible with
its origin, where we still check that the lower 32 bits of the counter
do not wraparound, handling the FirstNormalObjectId case.

pg_resetwal -o/--next-oid is updated to be able to handle 8-byte OIDs.
---
 src/include/access/transam.h              |  3 +-
 src/include/access/xlog.h                 |  2 +-
 src/include/catalog/pg_control.h          |  2 +-
 src/backend/access/rmgrdesc/xlogdesc.c    |  8 +--
 src/backend/access/transam/varsup.c       | 62 ++++++++++++++++-------
 src/backend/access/transam/xlog.c         |  8 +--
 src/backend/access/transam/xlogrecovery.c |  2 +-
 src/bin/pg_controldata/pg_controldata.c   |  2 +-
 src/bin/pg_resetwal/pg_resetwal.c         | 10 ++--
 doc/src/sgml/ref/pg_resetwal.sgml         |  6 +--
 10 files changed, 66 insertions(+), 39 deletions(-)

diff --git a/src/include/access/transam.h b/src/include/access/transam.h
index 7d82cd2eb562..2ef3000bdb1f 100644
--- a/src/include/access/transam.h
+++ b/src/include/access/transam.h
@@ -211,7 +211,7 @@ typedef struct TransamVariablesData
 	/*
 	 * These fields are protected by OidGenLock.
 	 */
-	Oid			nextOid;		/* next OID to assign */
+	Oid8		nextOid;		/* next OID (8 bytes) to assign */
 	uint32		oidCount;		/* OIDs available before must do XLOG work */
 
 	/*
@@ -293,6 +293,7 @@ extern void SetTransactionIdLimit(TransactionId oldest_datfrozenxid,
 extern void AdvanceOldestClogXid(TransactionId oldest_datfrozenxid);
 extern bool ForceTransactionIdLimitUpdate(void);
 extern Oid	GetNewObjectId(void);
+extern Oid8 GetNewObjectId8(void);
 extern void StopGeneratingPinnedObjectIds(void);
 
 #ifdef USE_ASSERT_CHECKING
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index d12798be3d80..21d915ae5802 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -243,7 +243,7 @@ extern void ShutdownXLOG(int code, Datum arg);
 extern bool CreateCheckPoint(int flags);
 extern bool CreateRestartPoint(int flags);
 extern WALAvailability GetWALAvailability(XLogRecPtr targetLSN);
-extern void XLogPutNextOid(Oid nextOid);
+extern void XLogPutNextOid(Oid8 nextOid);
 extern XLogRecPtr XLogRestorePoint(const char *rpName);
 extern void UpdateFullPageWrites(void);
 extern void GetFullPageWriteInfo(XLogRecPtr *RedoRecPtr_p, bool *doPageWrites_p);
diff --git a/src/include/catalog/pg_control.h b/src/include/catalog/pg_control.h
index 63e834a6ce47..c85c84bbfdbb 100644
--- a/src/include/catalog/pg_control.h
+++ b/src/include/catalog/pg_control.h
@@ -42,7 +42,7 @@ typedef struct CheckPoint
 	bool		fullPageWrites; /* current full_page_writes */
 	int			wal_level;		/* current wal_level */
 	FullTransactionId nextXid;	/* next free transaction ID */
-	Oid			nextOid;		/* next free OID */
+	Oid8		nextOid;		/* next free OID */
 	MultiXactId nextMulti;		/* next free MultiXactId */
 	MultiXactOffset nextMultiOffset;	/* next free MultiXact offset */
 	TransactionId oldestXid;	/* cluster-wide minimum datfrozenxid */
diff --git a/src/backend/access/rmgrdesc/xlogdesc.c b/src/backend/access/rmgrdesc/xlogdesc.c
index cd6c2a2f650a..23920d32092a 100644
--- a/src/backend/access/rmgrdesc/xlogdesc.c
+++ b/src/backend/access/rmgrdesc/xlogdesc.c
@@ -66,7 +66,7 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
 		CheckPoint *checkpoint = (CheckPoint *) rec;
 
 		appendStringInfo(buf, "redo %X/%08X; "
-						 "tli %u; prev tli %u; fpw %s; wal_level %s; xid %u:%u; oid %u; multi %u; offset %u; "
+						 "tli %u; prev tli %u; fpw %s; wal_level %s; xid %u:%u; oid " OID8_FORMAT "; multi %u; offset %u; "
 						 "oldest xid %u in DB %u; oldest multi %u in DB %u; "
 						 "oldest/newest commit timestamp xid: %u/%u; "
 						 "oldest running xid %u; %s",
@@ -91,10 +91,10 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
 	}
 	else if (info == XLOG_NEXTOID)
 	{
-		Oid			nextOid;
+		Oid8		nextOid;
 
-		memcpy(&nextOid, rec, sizeof(Oid));
-		appendStringInfo(buf, "%u", nextOid);
+		memcpy(&nextOid, rec, sizeof(Oid8));
+		appendStringInfo(buf, OID8_FORMAT, nextOid);
 	}
 	else if (info == XLOG_RESTORE_POINT)
 	{
diff --git a/src/backend/access/transam/varsup.c b/src/backend/access/transam/varsup.c
index f8c4dada7c93..662d1dcaed16 100644
--- a/src/backend/access/transam/varsup.c
+++ b/src/backend/access/transam/varsup.c
@@ -542,31 +542,51 @@ ForceTransactionIdLimitUpdate(void)
 
 
 /*
- * GetNewObjectId -- allocate a new OID
+ * GetNewObjectId -- allocate a new OID (4 bytes)
  *
- * OIDs are generated by a cluster-wide counter.  Since they are only 32 bits
- * wide, counter wraparound will occur eventually, and therefore it is unwise
- * to assume they are unique unless precautions are taken to make them so.
- * Hence, this routine should generally not be used directly.  The only direct
- * callers should be GetNewOidWithIndex() and GetNewRelFileNumber() in
- * catalog/catalog.c.
+ * OIDs are generated by a cluster-wide counter.  The callers of this routine
+ * expect a 32 bit-wide counter, and counter wraparound will occur eventually,
+ * and therefore it is unwise to assume they are unique unless precautions are
+ * taken to make them so.  This routine should generally not be used directly.
+ * The only direct callers should be GetNewOidWithIndex() and
+ * GetNewRelFileNumber() in catalog/catalog.c.
  */
 Oid
 GetNewObjectId(void)
 {
-	Oid			result;
+	return (Oid) GetNewObjectId8();
+}
+
+/*
+ * GetNewObjectId8 -- allocate a new OID (8 bytes)
+ *
+ * This routine can be called directly if the consumer of the OID allocated
+ * stores the counter in an 8-byte space, where wraparound does not matter.
+ * We still need to care about the wraparound case in the low 32 bits of the
+ * space allocated, GetNewObjectId() expecting OIDs to never be allocated
+ * up to FirstNormalObjectId.
+ */
+Oid8
+GetNewObjectId8(void)
+{
+	Oid8		result;
+	Oid			nextoid_lo;
+	uint32		nextoid_hi;
 
 	/* safety check, we should never get this far in a HS standby */
 	if (RecoveryInProgress())
 		elog(ERROR, "cannot assign OIDs during recovery");
 
 	LWLockAcquire(OidGenLock, LW_EXCLUSIVE);
+	nextoid_lo = (Oid) TransamVariables->nextOid;
+	nextoid_hi = (uint32) (TransamVariables->nextOid >> 32);
 
 	/*
-	 * Check for wraparound of the OID counter.  We *must* not return 0
-	 * (InvalidOid), and in normal operation we mustn't return anything below
-	 * FirstNormalObjectId since that range is reserved for initdb (see
-	 * IsCatalogRelationOid()).  Note we are relying on unsigned comparison.
+	 * Check for wraparound of the OID counter in its lower 4 bytes.  We
+	 * *must* not return 0 (InvalidOid), and in normal operation we
+	 * mustn't return anything below FirstNormalObjectId since that range
+	 * is reserved for initdb (see IsCatalogRelationOid()).  Note we are
+	 * relying on unsigned comparison.
 	 *
 	 * During initdb, we start the OID generator at FirstGenbkiObjectId, so we
 	 * only wrap if before that point when in bootstrap or standalone mode.
@@ -576,26 +596,32 @@ GetNewObjectId(void)
 	 * available for automatic assignment during initdb, while ensuring they
 	 * will never conflict with user-assigned OIDs.
 	 */
-	if (TransamVariables->nextOid < ((Oid) FirstNormalObjectId))
+	if (nextoid_lo < ((Oid) FirstNormalObjectId))
 	{
 		if (IsPostmasterEnvironment)
 		{
 			/* wraparound, or first post-initdb assignment, in normal mode */
-			TransamVariables->nextOid = FirstNormalObjectId;
+			nextoid_lo = FirstNormalObjectId;
 			TransamVariables->oidCount = 0;
 		}
 		else
 		{
 			/* we may be bootstrapping, so don't enforce the full range */
-			if (TransamVariables->nextOid < ((Oid) FirstGenbkiObjectId))
+			if (nextoid_lo < ((Oid) FirstGenbkiObjectId))
 			{
 				/* wraparound in standalone mode (unlikely but possible) */
-				TransamVariables->nextOid = FirstNormalObjectId;
+				nextoid_lo = FirstNormalObjectId;
 				TransamVariables->oidCount = 0;
 			}
 		}
 	}
 
+	/*
+	 * Set next OID in its 8-byte space, skipping the first post-init
+	 * assignment.
+	 */
+	TransamVariables->nextOid = ((Oid8) nextoid_hi) << 32 | nextoid_lo;
+
 	/* If we run out of logged for use oids then we must log more */
 	if (TransamVariables->oidCount == 0)
 	{
@@ -620,7 +646,7 @@ GetNewObjectId(void)
  * to the specified value.
  */
 static void
-SetNextObjectId(Oid nextOid)
+SetNextObjectId(Oid8 nextOid)
 {
 	/* Safety check, this is only allowable during initdb */
 	if (IsPostmasterEnvironment)
@@ -630,7 +656,7 @@ SetNextObjectId(Oid nextOid)
 	LWLockAcquire(OidGenLock, LW_EXCLUSIVE);
 
 	if (TransamVariables->nextOid > nextOid)
-		elog(ERROR, "too late to advance OID counter to %u, it is now %u",
+		elog(ERROR, "too late to advance OID counter to " OID8_FORMAT ", it is now " OID8_FORMAT,
 			 nextOid, TransamVariables->nextOid);
 
 	TransamVariables->nextOid = nextOid;
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index eceab3412558..580b3190eb58 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -8070,10 +8070,10 @@ KeepLogSeg(XLogRecPtr recptr, XLogSegNo *logSegNo)
  * Write a NEXTOID log record
  */
 void
-XLogPutNextOid(Oid nextOid)
+XLogPutNextOid(Oid8 nextOid)
 {
 	XLogBeginInsert();
-	XLogRegisterData(&nextOid, sizeof(Oid));
+	XLogRegisterData(&nextOid, sizeof(Oid8));
 	(void) XLogInsert(RM_XLOG_ID, XLOG_NEXTOID);
 
 	/*
@@ -8296,7 +8296,7 @@ xlog_redo(XLogReaderState *record)
 
 	if (info == XLOG_NEXTOID)
 	{
-		Oid			nextOid;
+		Oid8		nextOid;
 
 		/*
 		 * We used to try to take the maximum of TransamVariables->nextOid and
@@ -8305,7 +8305,7 @@ xlog_redo(XLogReaderState *record)
 		 * anyway, better to just believe the record exactly.  We still take
 		 * OidGenLock while setting the variable, just in case.
 		 */
-		memcpy(&nextOid, XLogRecGetData(record), sizeof(Oid));
+		memcpy(&nextOid, XLogRecGetData(record), sizeof(Oid8));
 		LWLockAcquire(OidGenLock, LW_EXCLUSIVE);
 		TransamVariables->nextOid = nextOid;
 		TransamVariables->oidCount = 0;
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index 52ff4d119e6f..18a486d8ec9c 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -881,7 +881,7 @@ InitWalRecovery(ControlFileData *ControlFile, bool *wasShutdown_ptr,
 							LSN_FORMAT_ARGS(checkPoint.redo),
 							wasShutdown ? "true" : "false"));
 	ereport(DEBUG1,
-			(errmsg_internal("next transaction ID: " UINT64_FORMAT "; next OID: %u",
+			(errmsg_internal("next transaction ID: " UINT64_FORMAT "; next OID: " OID8_FORMAT,
 							 U64FromFullTransactionId(checkPoint.nextXid),
 							 checkPoint.nextOid)));
 	ereport(DEBUG1,
diff --git a/src/bin/pg_controldata/pg_controldata.c b/src/bin/pg_controldata/pg_controldata.c
index 10de058ce91f..992111d3a1d2 100644
--- a/src/bin/pg_controldata/pg_controldata.c
+++ b/src/bin/pg_controldata/pg_controldata.c
@@ -260,7 +260,7 @@ main(int argc, char *argv[])
 	printf(_("Latest checkpoint's NextXID:          %u:%u\n"),
 		   EpochFromFullTransactionId(ControlFile->checkPointCopy.nextXid),
 		   XidFromFullTransactionId(ControlFile->checkPointCopy.nextXid));
-	printf(_("Latest checkpoint's NextOID:          %u\n"),
+	printf(_("Latest checkpoint's NextOID:          " OID8_FORMAT "\n"),
 		   ControlFile->checkPointCopy.nextOid);
 	printf(_("Latest checkpoint's NextMultiXactId:  %u\n"),
 		   ControlFile->checkPointCopy.nextMulti);
diff --git a/src/bin/pg_resetwal/pg_resetwal.c b/src/bin/pg_resetwal/pg_resetwal.c
index 7a4e4eb95706..c1039a8a4d16 100644
--- a/src/bin/pg_resetwal/pg_resetwal.c
+++ b/src/bin/pg_resetwal/pg_resetwal.c
@@ -68,7 +68,7 @@ static TransactionId set_oldest_xid = 0;
 static TransactionId set_xid = 0;
 static TransactionId set_oldest_commit_ts_xid = 0;
 static TransactionId set_newest_commit_ts_xid = 0;
-static Oid	set_oid = 0;
+static Oid8 set_oid = 0;
 static MultiXactId set_mxid = 0;
 static MultiXactOffset set_mxoff = (MultiXactOffset) -1;
 static TimeLineID minXlogTli = 0;
@@ -225,7 +225,7 @@ main(int argc, char *argv[])
 
 			case 'o':
 				errno = 0;
-				set_oid = strtoul(optarg, &endptr, 0);
+				set_oid = strtou64(optarg, &endptr, 0);
 				if (endptr == optarg || *endptr != '\0' || errno != 0)
 				{
 					pg_log_error("invalid argument for option %s", "-o");
@@ -755,7 +755,7 @@ PrintControlValues(bool guessed)
 	printf(_("Latest checkpoint's NextXID:          %u:%u\n"),
 		   EpochFromFullTransactionId(ControlFile.checkPointCopy.nextXid),
 		   XidFromFullTransactionId(ControlFile.checkPointCopy.nextXid));
-	printf(_("Latest checkpoint's NextOID:          %u\n"),
+	printf(_("Latest checkpoint's NextOID:          " OID8_FORMAT "\n"),
 		   ControlFile.checkPointCopy.nextOid);
 	printf(_("Latest checkpoint's NextMultiXactId:  %u\n"),
 		   ControlFile.checkPointCopy.nextMulti);
@@ -839,7 +839,7 @@ PrintNewControlValues(void)
 
 	if (set_oid != 0)
 	{
-		printf(_("NextOID:                              %u\n"),
+		printf(_("NextOID:                              " OID8_FORMAT "\n"),
 			   ControlFile.checkPointCopy.nextOid);
 	}
 
@@ -1208,7 +1208,7 @@ usage(void)
 	printf(_("  -e, --epoch=XIDEPOCH             set next transaction ID epoch\n"));
 	printf(_("  -l, --next-wal-file=WALFILE      set minimum starting location for new WAL\n"));
 	printf(_("  -m, --multixact-ids=MXID,MXID    set next and oldest multitransaction ID\n"));
-	printf(_("  -o, --next-oid=OID               set next OID\n"));
+	printf(_("  -o, --next-oid=OID8              set next OID (8 bytes)\n"));
 	printf(_("  -O, --multixact-offset=OFFSET    set next multitransaction offset\n"));
 	printf(_("  -u, --oldest-transaction-id=XID  set oldest transaction ID\n"));
 	printf(_("  -x, --next-transaction-id=XID    set next transaction ID\n"));
diff --git a/doc/src/sgml/ref/pg_resetwal.sgml b/doc/src/sgml/ref/pg_resetwal.sgml
index 2c019c2aac6e..b03251cedbbe 100644
--- a/doc/src/sgml/ref/pg_resetwal.sgml
+++ b/doc/src/sgml/ref/pg_resetwal.sgml
@@ -279,11 +279,11 @@ PostgreSQL documentation
    </varlistentry>
 
    <varlistentry>
-    <term><option>-o <replaceable class="parameter">oid</replaceable></option></term>
-    <term><option>--next-oid=<replaceable class="parameter">oid</replaceable></option></term>
+    <term><option>-o <replaceable class="parameter">oid8</replaceable></option></term>
+    <term><option>--next-oid=<replaceable class="parameter">oid8</replaceable></option></term>
     <listitem>
      <para>
-      Manually set the next OID.
+      Manually set the next OID (8 bytes).
      </para>
 
      <para>
-- 
2.51.0

v7-0012-Add-relation-option-toast_value_type.patchtext/x-diff; charset=us-asciiDownload
From 01675525aa7ee59775cf1f43b71bba53cd8e7341 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 12:54:58 +0900
Subject: [PATCH v7 12/15] Add relation option toast_value_type

This relation option gives the possibility to define the attribute type
that can be used for chunk_id in a TOAST table when it is created
initially.  This parameter has no effect if a TOAST table exists, even
after it is modified later on, even on rewrites.

This can be set only to "oid" currently, and will be expanded with a
second mode later.

Note: perhaps it would make sense to introduce that only when support
for 8-byte OID values are added to TOAST, the split is here to ease
review.
---
 src/include/utils/rel.h                | 17 +++++++++++++++++
 src/backend/access/common/reloptions.c | 21 +++++++++++++++++++++
 src/backend/catalog/toasting.c         |  6 ++++++
 src/bin/psql/tab-complete.in.c         |  1 +
 doc/src/sgml/ref/create_table.sgml     | 18 ++++++++++++++++++
 5 files changed, 63 insertions(+)

diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 219904363737..ac82c9fee903 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -337,11 +337,20 @@ typedef enum StdRdOptIndexCleanup
 	STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON,
 } StdRdOptIndexCleanup;
 
+/* StdRdOptions->toast_value_type values */
+typedef enum StdRdOptToastValueType
+{
+	STDRD_OPTION_TOAST_VALUE_TYPE_INVALID = 0,
+	STDRD_OPTION_TOAST_VALUE_TYPE_OID,
+} StdRdOptToastValueType;
+
 typedef struct StdRdOptions
 {
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	int			fillfactor;		/* page fill factor in percent (0..100) */
 	int			toast_tuple_target; /* target for tuple toasting */
+	StdRdOptToastValueType	toast_value_type;	/* type assigned to chunk_id
+												 * at toast table creation */
 	AutoVacOpts autovacuum;		/* autovacuum-related options */
 	bool		user_catalog_table; /* use as an additional catalog relation */
 	int			parallel_workers;	/* max number of parallel workers */
@@ -367,6 +376,14 @@ typedef struct StdRdOptions
 	((relation)->rd_options ? \
 	 ((StdRdOptions *) (relation)->rd_options)->toast_tuple_target : (defaulttarg))
 
+/*
+ * RelationGetToastValueType
+ *		Returns the relation's toast_value_type.  Note multiple eval of argument!
+ */
+#define RelationGetToastValueType(relation, defaulttarg) \
+	((relation)->rd_options ? \
+	 ((StdRdOptions *) (relation)->rd_options)->toast_value_type : defaulttarg)
+
 /*
  * RelationGetFillFactor
  *		Returns the relation's fillfactor.  Note multiple eval of argument!
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 35150bf237bc..6524325a0645 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -516,6 +516,14 @@ static relopt_enum_elt_def viewCheckOptValues[] =
 	{(const char *) NULL}		/* list terminator */
 };
 
+/* values from StdRdOptToastValueType */
+static relopt_enum_elt_def StdRdOptToastValueTypes[] =
+{
+	/* no value for INVALID */
+	{"oid", STDRD_OPTION_TOAST_VALUE_TYPE_OID},
+	{(const char *) NULL}		/* list terminator */
+};
+
 static relopt_enum enumRelOpts[] =
 {
 	{
@@ -529,6 +537,17 @@ static relopt_enum enumRelOpts[] =
 		STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO,
 		gettext_noop("Valid values are \"on\", \"off\", and \"auto\".")
 	},
+	{
+		{
+			"toast_value_type",
+			"Controls the attribute type of chunk_id at toast table creation",
+			RELOPT_KIND_HEAP,
+			ShareUpdateExclusiveLock
+		},
+		StdRdOptToastValueTypes,
+		STDRD_OPTION_TOAST_VALUE_TYPE_OID,
+		gettext_noop("Valid values are \"oid\".")
+	},
 	{
 		{
 			"buffering",
@@ -1898,6 +1917,8 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
 		offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_min_duration)},
 		{"toast_tuple_target", RELOPT_TYPE_INT,
 		offsetof(StdRdOptions, toast_tuple_target)},
+		{"toast_value_type", RELOPT_TYPE_ENUM,
+		offsetof(StdRdOptions, toast_value_type)},
 		{"autovacuum_vacuum_cost_delay", RELOPT_TYPE_REAL,
 		offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_delay)},
 		{"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c
index f1d76d8acd51..545983b5be9d 100644
--- a/src/backend/catalog/toasting.c
+++ b/src/backend/catalog/toasting.c
@@ -158,9 +158,15 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	 */
 	if (!IsBinaryUpgrade)
 	{
+		StdRdOptToastValueType value_type;
+
 		/* Normal mode, normal check */
 		if (!needs_toast_table(rel))
 			return false;
+
+		value_type = RelationGetToastValueType(rel, STDRD_OPTION_TOAST_VALUE_TYPE_OID);
+		if (value_type == STDRD_OPTION_TOAST_VALUE_TYPE_OID)
+			toast_chunkid_typid = OIDOID;
 	}
 	else
 	{
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 6176741d20b1..3f5d7ff2d379 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -1441,6 +1441,7 @@ static const char *const table_storage_parameters[] = {
 	"toast.vacuum_max_eager_freeze_failure_rate",
 	"toast.vacuum_truncate",
 	"toast_tuple_target",
+	"toast_value_type",
 	"user_catalog_table",
 	"vacuum_index_cleanup",
 	"vacuum_max_eager_freeze_failure_rate",
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index dc000e913c14..84ad78afa3de 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1632,6 +1632,24 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     </listitem>
    </varlistentry>
 
+   <varlistentry id="reloption-toast-value-type" xreflabel="toast_value_type">
+    <term><literal>toast_value_type</literal> (<type>enum</type>)
+    <indexterm>
+     <primary><varname>toast_value_type</varname> storage parameter</primary>
+    </indexterm>
+    </term>
+    <listitem>
+     <para>
+      The toast_value_type specifies the attribute type of
+      <literal>chunk_id</literal> used when initially creating  a toast
+      relation for this table.
+      By default this parameter is <literal>oid</literal>, to assign
+      <type>oid</type> as attribute type to <literal>chunk_id</literal>.
+      This parameter cannot be set for TOAST tables.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="reloption-parallel-workers" xreflabel="parallel_workers">
     <term><literal>parallel_workers</literal> (<type>integer</type>)
      <indexterm>
-- 
2.51.0

v7-0013-Add-support-for-oid8-TOAST-values.patchtext/x-diff; charset=us-asciiDownload
From 7cb6c0d6f0a2592e016423f637724d58799f809c Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 17:06:10 +0900
Subject: [PATCH v7 13/15] Add support for oid8 TOAST values

This commit adds the possibility to define TOAST tables with oid8 as
value ID, based on the reloption toast_value_type.  All the external
TOAST pointers still rely on varatt_external and a single vartag, with
all the values inserted in the bigint TOAST tables fed from the existing
OID value generator.  This will be changed in an upcoming patch that
adds more vartag_external types and its associated structures, with the
code being able to use a different external TOAST pointer depending on
the attribute type of chunk_id in TOAST relations.

All the changes done here are mechanical, with all the TOAST code able
to do chunk ID lookups based on the two types now supported.

XXX: Catalog version bump required.
---
 src/include/catalog/pg_opclass.dat          |  3 +-
 src/include/utils/rel.h                     |  1 +
 src/backend/access/common/reloptions.c      |  1 +
 src/backend/access/common/toast_internals.c | 94 +++++++++++++++------
 src/backend/access/heap/heaptoast.c         | 20 ++++-
 src/backend/catalog/toasting.c              | 24 +++++-
 doc/src/sgml/ref/create_table.sgml          |  2 +
 doc/src/sgml/storage.sgml                   |  7 +-
 contrib/amcheck/verify_heapam.c             | 19 ++++-
 9 files changed, 131 insertions(+), 40 deletions(-)

diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index c0de88fabc49..b8f2bc2d69c4 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -179,7 +179,8 @@
   opcintype => 'xid8' },
 { opcmethod => 'hash', opcname => 'oid8_ops', opcfamily => 'hash/oid8_ops',
   opcintype => 'oid8' },
-{ opcmethod => 'btree', opcname => 'oid8_ops', opcfamily => 'btree/oid8_ops',
+{ oid => '8285', oid_symbol => 'OID8_BTREE_OPS_OID',
+  opcmethod => 'btree', opcname => 'oid8_ops', opcfamily => 'btree/oid8_ops',
   opcintype => 'oid8' },
 { opcmethod => 'hash', opcname => 'cid_ops', opcfamily => 'hash/cid_ops',
   opcintype => 'cid' },
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index ac82c9fee903..fd922f518946 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -342,6 +342,7 @@ typedef enum StdRdOptToastValueType
 {
 	STDRD_OPTION_TOAST_VALUE_TYPE_INVALID = 0,
 	STDRD_OPTION_TOAST_VALUE_TYPE_OID,
+	STDRD_OPTION_TOAST_VALUE_TYPE_OID8,
 } StdRdOptToastValueType;
 
 typedef struct StdRdOptions
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 6524325a0645..c0795331d2d5 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -521,6 +521,7 @@ static relopt_enum_elt_def StdRdOptToastValueTypes[] =
 {
 	/* no value for INVALID */
 	{"oid", STDRD_OPTION_TOAST_VALUE_TYPE_OID},
+	{"oid8", STDRD_OPTION_TOAST_VALUE_TYPE_OID8},
 	{(const char *) NULL}		/* list terminator */
 };
 
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index a68869f58517..b54db5c5745a 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -26,6 +26,7 @@
 #include "utils/fmgroids.h"
 #include "utils/rel.h"
 #include "utils/snapmgr.h"
+#include "utils/lsyscache.h"
 
 static bool toastrel_valueid_exists(Relation toastrel, Oid8 valueid);
 static bool toastid_valueid_exists(Oid toastrelid, Oid8 valueid);
@@ -134,8 +135,10 @@ toast_save_datum(Relation rel, Datum value,
 	int			validIndex;
 	const toast_external_info *info;
 	uint8		tag = VARTAG_INDIRECT;	/* init value does not matter */
+	Oid			toast_typid = get_atttype(rel->rd_rel->reltoastrelid, 1);
 
 	Assert(!VARATT_IS_EXTERNAL(dval));
+	Assert(OidIsValid(toast_typid));
 
 	/*
 	 * Open the toast relation and its indexes.  We can use the index to check
@@ -216,24 +219,32 @@ toast_save_datum(Relation rel, Datum value,
 		toast_pointer.toastrelid = RelationGetRelid(toastrel);
 
 	/*
-	 * Choose an OID to use as the value ID for this toast value.
+	 * Choose a new value to use as the value ID for this toast value, be it
+	 * for OID or int8-based TOAST relations.
 	 *
-	 * Normally we just choose an unused OID within the toast table.  But
+	 * Normally we just choose an unused value within the toast table.  But
 	 * during table-rewriting operations where we are preserving an existing
-	 * toast table OID, we want to preserve toast value OIDs too.  So, if
+	 * toast table OID, we want to preserve toast value IDs too.  So, if
 	 * rd_toastoid is set and we had a prior external value from that same
 	 * toast table, re-use its value ID.  If we didn't have a prior external
 	 * value (which is a corner case, but possible if the table's attstorage
 	 * options have been changed), we have to pick a value ID that doesn't
-	 * conflict with either new or existing toast value OIDs.
+	 * conflict with either new or existing toast value IDs.  If the TOAST
+	 * table uses 8-byte value IDs, we should not really care much about
+	 * that.
 	 */
 	if (!OidIsValid(rel->rd_toastoid))
 	{
 		/* normal case: just choose an unused OID */
-		toast_pointer.valueid =
-			GetNewOidWithIndex(toastrel,
-							   RelationGetRelid(toastidxs[validIndex]),
-							   (AttrNumber) 1);
+		if (toast_typid == OIDOID)
+			toast_pointer.valueid =
+				GetNewOidWithIndex(toastrel,
+								   RelationGetRelid(toastidxs[validIndex]),
+								   (AttrNumber) 1);
+		else if (toast_typid == OID8OID)
+			toast_pointer.valueid = GetNewObjectId8();
+		else
+			Assert(false);
 	}
 	else
 	{
@@ -279,17 +290,22 @@ toast_save_datum(Relation rel, Datum value,
 		if (toast_pointer.valueid == InvalidOid8)
 		{
 			/*
-			 * new value; must choose an OID that doesn't conflict in either
-			 * old or new toast table
+			 * new value; must choose a value that doesn't conflict in either
+			 * old or new toast table.
 			 */
-			do
+			if (toast_typid == OIDOID)
 			{
-				toast_pointer.valueid =
-					GetNewOidWithIndex(toastrel,
-									   RelationGetRelid(toastidxs[validIndex]),
-									   (AttrNumber) 1);
-			} while (toastid_valueid_exists(rel->rd_toastoid,
-											toast_pointer.valueid));
+				do
+				{
+					toast_pointer.valueid =
+						GetNewOidWithIndex(toastrel,
+										   RelationGetRelid(toastidxs[validIndex]),
+										   (AttrNumber) 1);
+				} while (toastid_valueid_exists(rel->rd_toastoid,
+												toast_pointer.valueid));
+			}
+			else if (toast_typid == OID8OID)
+				toast_pointer.valueid = GetNewObjectId8();
 		}
 	}
 
@@ -329,7 +345,10 @@ toast_save_datum(Relation rel, Datum value,
 		/*
 		 * Build a tuple and store it
 		 */
-		t_values[0] = ObjectIdGetDatum(toast_pointer.valueid);
+		if (toast_typid == OIDOID)
+			t_values[0] = ObjectIdGetDatum(toast_pointer.valueid);
+		else if (toast_typid == OID8OID)
+			t_values[0] = ObjectId8GetDatum(toast_pointer.valueid);
 		t_values[1] = Int32GetDatum(chunk_seq++);
 		SET_VARSIZE(&chunk_data, chunk_size + VARHDRSZ);
 		memcpy(VARDATA(&chunk_data), data_p, chunk_size);
@@ -408,6 +427,7 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	HeapTuple	toasttup;
 	int			num_indexes;
 	int			validIndex;
+	Oid			toast_typid;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		return;
@@ -419,6 +439,8 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	 * Open the toast relation and its indexes
 	 */
 	toastrel = table_open(toast_pointer.toastrelid, RowExclusiveLock);
+	toast_typid = TupleDescAttr(toastrel->rd_att, 0)->atttypid;
+	Assert(toast_typid == OIDOID || toast_typid == OID8OID);
 
 	/* Fetch valid relation used for process */
 	validIndex = toast_open_indexes(toastrel,
@@ -429,10 +451,18 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	/*
 	 * Setup a scan key to find chunks with matching va_valueid
 	 */
-	ScanKeyInit(&toastkey,
-				(AttrNumber) 1,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(toast_pointer.valueid));
+	if (toast_typid == OIDOID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(toast_pointer.valueid));
+	else if (toast_typid == OID8OID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OID8EQ,
+					ObjectId8GetDatum(toast_pointer.valueid));
+	else
+		Assert(false);
 
 	/*
 	 * Find all the chunks.  (We don't actually care whether we see them in
@@ -479,6 +509,7 @@ toastrel_valueid_exists(Relation toastrel, Oid8 valueid)
 	int			num_indexes;
 	int			validIndex;
 	Relation   *toastidxs;
+	Oid			toast_typid;
 
 	/* Fetch a valid index relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -486,13 +517,24 @@ toastrel_valueid_exists(Relation toastrel, Oid8 valueid)
 									&toastidxs,
 									&num_indexes);
 
+	toast_typid = TupleDescAttr(toastrel->rd_att, 0)->atttypid;
+	Assert(toast_typid == OIDOID || toast_typid == OID8OID);
+
 	/*
 	 * Setup a scan key to find chunks with matching va_valueid
 	 */
-	ScanKeyInit(&toastkey,
-				(AttrNumber) 1,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(valueid));
+	if (toast_typid == OIDOID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(valueid));
+	else if (toast_typid == OID8OID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OID8EQ,
+					ObjectId8GetDatum(valueid));
+	else
+		Assert(false);
 
 	/*
 	 * Is there any such chunk?
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index 230f2a6f35eb..50e9bf9047f9 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -654,6 +654,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 	int32		max_chunk_size;
 	const toast_external_info *info;
 	uint8		tag = VARTAG_INDIRECT;	/* init value does not matter */
+	Oid			toast_typid;
 
 	/* Look for the valid index of toast relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -667,16 +668,27 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 
 	max_chunk_size = info->maximum_chunk_size;
 
+	toast_typid = TupleDescAttr(toastrel->rd_att, 0)->atttypid;
+	Assert(toast_typid == OIDOID || toast_typid == OID8OID);
+
 	totalchunks = ((attrsize - 1) / max_chunk_size) + 1;
 	startchunk = sliceoffset / max_chunk_size;
 	endchunk = (sliceoffset + slicelength - 1) / max_chunk_size;
 	Assert(endchunk <= totalchunks);
 
 	/* Set up a scan key to fetch from the index. */
-	ScanKeyInit(&toastkey[0],
-				(AttrNumber) 1,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(valueid));
+	if (toast_typid == OIDOID)
+		ScanKeyInit(&toastkey[0],
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(valueid));
+	else if (toast_typid == OID8OID)
+		ScanKeyInit(&toastkey[0],
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OID8EQ,
+					ObjectId8GetDatum(valueid));
+	else
+		Assert(false);
 
 	/*
 	 * No additional condition if fetching all chunks. Otherwise, use an
diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c
index 545983b5be9d..2288311b22a4 100644
--- a/src/backend/catalog/toasting.c
+++ b/src/backend/catalog/toasting.c
@@ -31,6 +31,7 @@
 #include "nodes/makefuncs.h"
 #include "utils/fmgroids.h"
 #include "utils/rel.h"
+#include "utils/lsyscache.h"
 #include "utils/syscache.h"
 
 static void CheckAndCreateToastTable(Oid relOid, Datum reloptions,
@@ -167,6 +168,8 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 		value_type = RelationGetToastValueType(rel, STDRD_OPTION_TOAST_VALUE_TYPE_OID);
 		if (value_type == STDRD_OPTION_TOAST_VALUE_TYPE_OID)
 			toast_chunkid_typid = OIDOID;
+		else if (value_type == STDRD_OPTION_TOAST_VALUE_TYPE_OID8)
+			toast_chunkid_typid = OID8OID;
 	}
 	else
 	{
@@ -199,7 +202,8 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("toast chunk_id type not set while in binary upgrade mode")));
-		if (binary_upgrade_next_toast_chunk_id_typoid != OIDOID)
+		if (binary_upgrade_next_toast_chunk_id_typoid != OIDOID &&
+			binary_upgrade_next_toast_chunk_id_typoid != OID8OID)
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("cannot support toast chunk_id type %u in binary upgrade mode",
@@ -224,6 +228,19 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	snprintf(toast_idxname, sizeof(toast_idxname),
 			 "pg_toast_%u_index", relOid);
 
+	/*
+	 * Special case here.  If OIDOldToast is defined, we need to rely on the
+	 * existing table for the job because we do not want to create an
+	 * inconsistent relation that would conflict with the parent and break
+	 * the world.
+	 */
+	if (OidIsValid(OIDOldToast))
+	{
+		toast_chunkid_typid = get_atttype(OIDOldToast, 1);
+		if (!OidIsValid(toast_chunkid_typid))
+			elog(ERROR, "cache lookup failed for relation %u", OIDOldToast);
+	}
+
 	/* this is pretty painful...  need a tuple descriptor */
 	tupdesc = CreateTemplateTupleDesc(3);
 	TupleDescInitEntry(tupdesc, (AttrNumber) 1,
@@ -336,7 +353,10 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	collationIds[0] = InvalidOid;
 	collationIds[1] = InvalidOid;
 
-	opclassIds[0] = OID_BTREE_OPS_OID;
+	if (toast_chunkid_typid == OIDOID)
+		opclassIds[0] = OID_BTREE_OPS_OID;
+	else if (toast_chunkid_typid == OID8OID)
+		opclassIds[0] = OID8_BTREE_OPS_OID;
 	opclassIds[1] = INT4_BTREE_OPS_OID;
 
 	coloptions[0] = 0;
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 84ad78afa3de..35dd317917e3 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1645,6 +1645,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       relation for this table.
       By default this parameter is <literal>oid</literal>, to assign
       <type>oid</type> as attribute type to <literal>chunk_id</literal>.
+      This parameter can be set to <type>oid8</type> to use <type>oid8</type>
+      as attribute type for <literal>chunk_id</literal>.
       This parameter cannot be set for TOAST tables.
      </para>
     </listitem>
diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml
index 67600fd974d7..afddf663fec5 100644
--- a/doc/src/sgml/storage.sgml
+++ b/doc/src/sgml/storage.sgml
@@ -421,14 +421,15 @@ most <symbol>TOAST_OID_MAX_CHUNK_SIZE</symbol> bytes (by default this value is c
 so that four chunk rows will fit on a page, making it about 2000 bytes).
 Each chunk is stored as a separate row in the <acronym>TOAST</acronym> table
 belonging to the owning table.  Every
-<acronym>TOAST</acronym> table has the columns <structfield>chunk_id</structfield> (an OID
-identifying the particular <acronym>TOAST</acronym>ed value),
+<acronym>TOAST</acronym> table has the columns
+<structfield>chunk_id</structfield> (an OID or an 8-byte integer identifying
+the particular <acronym>TOAST</acronym>ed value),
 <structfield>chunk_seq</structfield> (a sequence number for the chunk within its value),
 and <structfield>chunk_data</structfield> (the actual data of the chunk).  A unique index
 on <structfield>chunk_id</structfield> and <structfield>chunk_seq</structfield> provides fast
 retrieval of the values.  A pointer datum representing an out-of-line on-disk
 <acronym>TOAST</acronym>ed value therefore needs to store the OID of the
-<acronym>TOAST</acronym> table in which to look and the OID of the specific value
+<acronym>TOAST</acronym> table in which to look and the specific value
 (its <structfield>chunk_id</structfield>).  For convenience, pointer datums also store the
 logical datum size (original uncompressed data length), physical stored size
 (different if compression was applied), and the compression method used, if
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 9cf3c081bf01..143e6baa35cf 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1880,6 +1880,9 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	int32		last_chunk_seq;
 	Oid8		toast_valueid;
 	int32		max_chunk_size;
+	Oid			toast_typid;
+
+	toast_typid = TupleDescAttr(ctx->toast_rel->rd_att, 0)->atttypid;
 
 	extsize = ta->toast_pointer.extsize;
 
@@ -1889,10 +1892,18 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	/*
 	 * Setup a scan key to find chunks in toast table with matching va_valueid
 	 */
-	ScanKeyInit(&toastkey,
-				(AttrNumber) 1,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(ta->toast_pointer.valueid));
+	if (toast_typid == OIDOID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(ta->toast_pointer.valueid));
+	else if (toast_typid == OID8OID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OID8EQ,
+					ObjectId8GetDatum(ta->toast_pointer.valueid));
+	else
+		Assert(false);
 
 	/*
 	 * Check if any chunks for this toasted object exist in the toast table,
-- 
2.51.0

v7-0014-Add-tests-for-TOAST-relations-with-bigint-as-valu.patchtext/x-diff; charset=us-asciiDownload
From d2e24004836896c56341b21caeec6c8614f69785 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 13:43:19 +0900
Subject: [PATCH v7 14/15] Add tests for TOAST relations with bigint as value
 type

This adds coverage for relations created with default_toast_type =
'int8', for external TOAST pointers both compressed and uncompressed.
---
 src/test/regress/expected/strings.out     | 231 ++++++++++++++++++----
 src/test/regress/expected/type_sanity.out |   6 +-
 src/test/regress/sql/strings.sql          | 134 +++++++++----
 src/test/regress/sql/type_sanity.sql      |   6 +-
 4 files changed, 296 insertions(+), 81 deletions(-)

diff --git a/src/test/regress/expected/strings.out b/src/test/regress/expected/strings.out
index 691e475bce37..4921056bc0cb 100644
--- a/src/test/regress/expected/strings.out
+++ b/src/test/regress/expected/strings.out
@@ -1954,21 +1954,37 @@ SELECT text 'text' || varchar ' and varchar' AS "Concat text to varchar";
 (1 row)
 
 --
--- test substr with toasted text values
+-- Test substr with toasted text values, for all types of TOAST relations
+-- supported.
 --
-CREATE TABLE toasttest(f1 text);
-insert into toasttest values(repeat('1234567890',10000));
-insert into toasttest values(repeat('1234567890',10000));
+CREATE TABLE toasttest_oid(f1 text) with (toast_value_type = 'oid');
+CREATE TABLE toasttest_oid8(f1 text) with (toast_value_type = 'oid8');
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid8 values(repeat('1234567890',10000));
+insert into toasttest_oid8 values(repeat('1234567890',10000));
 --
 -- Ensure that some values are uncompressed, to test the faster substring
 -- operation used in that case
 --
-alter table toasttest alter column f1 set storage external;
-insert into toasttest values(repeat('1234567890',10000));
-insert into toasttest values(repeat('1234567890',10000));
+alter table toasttest_oid alter column f1 set storage external;
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+alter table toasttest_oid8 alter column f1 set storage external;
+insert into toasttest_oid8 values(repeat('1234567890',10000));
+insert into toasttest_oid8 values(repeat('1234567890',10000));
 -- If the starting position is zero or less, then return from the start of the string
 -- adjusting the length to be consistent with the "negative start" per SQL.
-SELECT substr(f1, -1, 5) from toasttest;
+SELECT substr(f1, -1, 5) from toasttest_oid;
+ substr 
+--------
+ 123
+ 123
+ 123
+ 123
+(4 rows)
+
+SELECT substr(f1, -1, 5) from toasttest_oid8;
  substr 
 --------
  123
@@ -1978,11 +1994,22 @@ SELECT substr(f1, -1, 5) from toasttest;
 (4 rows)
 
 -- If the length is less than zero, an ERROR is thrown.
-SELECT substr(f1, 5, -1) from toasttest;
+SELECT substr(f1, 5, -1) from toasttest_oid;
+ERROR:  negative substring length not allowed
+SELECT substr(f1, 5, -1) from toasttest_oid8;
 ERROR:  negative substring length not allowed
 -- If no third argument (length) is provided, the length to the end of the
 -- string is assumed.
-SELECT substr(f1, 99995) from toasttest;
+SELECT substr(f1, 99995) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995) from toasttest_oid8;
  substr 
 --------
  567890
@@ -1993,7 +2020,7 @@ SELECT substr(f1, 99995) from toasttest;
 
 -- If start plus length is > string length, the result is truncated to
 -- string length
-SELECT substr(f1, 99995, 10) from toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
  substr 
 --------
  567890
@@ -2002,50 +2029,105 @@ SELECT substr(f1, 99995, 10) from toasttest;
  567890
 (4 rows)
 
-TRUNCATE TABLE toasttest;
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
+SELECT substr(f1, 99995, 10) from toasttest_oid8;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+-- TRUNCATE cases for TOAST relations with OID values.
+TRUNCATE TABLE toasttest_oid;
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
 -- expect >0 blocks
 SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
-  FROM pg_class where relname = 'toasttest';
+  FROM pg_class where relname = 'toasttest_oid';
  is_empty 
 ----------
  f
 (1 row)
 
-TRUNCATE TABLE toasttest;
-ALTER TABLE toasttest set (toast_tuple_target = 4080);
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
+TRUNCATE TABLE toasttest_oid;
+ALTER TABLE toasttest_oid set (toast_tuple_target = 4080);
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
 -- expect 0 blocks
 SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
-  FROM pg_class where relname = 'toasttest';
+  FROM pg_class where relname = 'toasttest_oid';
  is_empty 
 ----------
  t
 (1 row)
 
-DROP TABLE toasttest;
+DROP TABLE toasttest_oid;
+-- TRUNCATE cases for TOAST relation with int8 values.
+TRUNCATE TABLE toasttest_oid8;
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+-- expect >0 blocks
+SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
+  FROM pg_class where relname = 'toasttest_oid8';
+ is_empty 
+----------
+ f
+(1 row)
+
+TRUNCATE TABLE toasttest_oid8;
+ALTER TABLE toasttest_oid8 set (toast_tuple_target = 4080);
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+-- expect 0 blocks
+SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
+  FROM pg_class where relname = 'toasttest_oid8';
+ is_empty 
+----------
+ t
+(1 row)
+
+DROP TABLE toasttest_oid8;
 --
--- test substr with toasted bytea values
+-- test substr with toasted bytea values, for all types of TOAST relations
+-- supported. Do not drop these two relations, for pg_upgrade.
 --
-CREATE TABLE toasttest(f1 bytea);
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
+CREATE TABLE toasttest_oid(f1 bytea) WITH (toast_value_type = 'oid');
+CREATE TABLE toasttest_oid8(f1 bytea) WITH (toast_value_type = 'oid8');
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
 --
 -- Ensure that some values are uncompressed, to test the faster substring
 -- operation used in that case
 --
-alter table toasttest alter column f1 set storage external;
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
+alter table toasttest_oid alter column f1 set storage external;
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+alter table toasttest_oid8 alter column f1 set storage external;
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
 -- If the starting position is zero or less, then return from the start of the string
 -- adjusting the length to be consistent with the "negative start" per SQL.
-SELECT substr(f1, -1, 5) from toasttest;
+SELECT substr(f1, -1, 5) from toasttest_oid;
+ substr 
+--------
+ 123
+ 123
+ 123
+ 123
+(4 rows)
+
+SELECT substr(f1, -1, 5) from toasttest_oid8;
  substr 
 --------
  123
@@ -2055,11 +2137,22 @@ SELECT substr(f1, -1, 5) from toasttest;
 (4 rows)
 
 -- If the length is less than zero, an ERROR is thrown.
-SELECT substr(f1, 5, -1) from toasttest;
+SELECT substr(f1, 5, -1) from toasttest_oid;
+ERROR:  negative substring length not allowed
+SELECT substr(f1, 5, -1) from toasttest_oid8;
 ERROR:  negative substring length not allowed
 -- If no third argument (length) is provided, the length to the end of the
 -- string is assumed.
-SELECT substr(f1, 99995) from toasttest;
+SELECT substr(f1, 99995) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995) from toasttest_oid8;
  substr 
 --------
  567890
@@ -2070,7 +2163,72 @@ SELECT substr(f1, 99995) from toasttest;
 
 -- If start plus length is > string length, the result is truncated to
 -- string length
-SELECT substr(f1, 99995, 10) from toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995, 10) from toasttest_oid8;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+-- A relation rewrite leaves the TOAST value attributes unchanged.
+VACUUM FULL toasttest_oid;
+VACUUM FULL toasttest_oid8;
+SELECT c1.relname, a.atttypid::regtype
+  FROM pg_attribute AS a,
+       pg_class AS c1,
+       pg_class AS c2
+  WHERE
+       c1.relname IN ('toasttest_oid', 'toasttest_oid8') AND
+       c1.reltoastrelid = c2.oid AND
+       a.attrelid = c2.oid AND
+       a.attname = 'chunk_id'
+  ORDER BY c1.relname COLLATE "C";
+    relname     | atttypid 
+----------------+----------
+ toasttest_oid  | oid
+ toasttest_oid8 | oid8
+(2 rows)
+
+-- Check that data slices are still accessible.
+SELECT substr(f1, 99995) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995) from toasttest_oid8;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995, 10) from toasttest_oid8;
  substr 
 --------
  567890
@@ -2079,7 +2237,6 @@ SELECT substr(f1, 99995, 10) from toasttest;
  567890
 (4 rows)
 
-DROP TABLE toasttest;
 -- test internally compressing datums
 -- this tests compressing a datum to a very small size which exercises a
 -- corner case in packed-varlena handling: even though small, the compressed
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 9ddcacec6bf4..88faa57772c3 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -578,15 +578,15 @@ WHERE c1.relnatts != (SELECT count(*) FROM pg_attribute AS a1
 (0 rows)
 
 -- Cross-check against pg_type entry
--- NOTE: we allow attstorage to be 'plain' even when typstorage is not;
--- this is mainly for toast tables.
+-- NOTE: we allow attstorage to be 'plain' or 'external' even when typstorage
+-- is not; this is mainly for toast tables.
 SELECT a1.attrelid, a1.attname, t1.oid, t1.typname
 FROM pg_attribute AS a1, pg_type AS t1
 WHERE a1.atttypid = t1.oid AND
     (a1.attlen != t1.typlen OR
      a1.attalign != t1.typalign OR
      a1.attbyval != t1.typbyval OR
-     (a1.attstorage != t1.typstorage AND a1.attstorage != 'p'));
+     (a1.attstorage != t1.typstorage AND a1.attstorage NOT IN ('e', 'p')));
  attrelid | attname | oid | typname 
 ----------+---------+-----+---------
 (0 rows)
diff --git a/src/test/regress/sql/strings.sql b/src/test/regress/sql/strings.sql
index c05f34136990..f992451561f1 100644
--- a/src/test/regress/sql/strings.sql
+++ b/src/test/regress/sql/strings.sql
@@ -556,89 +556,147 @@ SELECT text 'text' || char(20) ' and characters' AS "Concat text to char";
 SELECT text 'text' || varchar ' and varchar' AS "Concat text to varchar";
 
 --
--- test substr with toasted text values
+-- Test substr with toasted text values, for all types of TOAST relations
+-- supported.
 --
-CREATE TABLE toasttest(f1 text);
+CREATE TABLE toasttest_oid(f1 text) with (toast_value_type = 'oid');
+CREATE TABLE toasttest_oid8(f1 text) with (toast_value_type = 'oid8');
 
-insert into toasttest values(repeat('1234567890',10000));
-insert into toasttest values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid8 values(repeat('1234567890',10000));
+insert into toasttest_oid8 values(repeat('1234567890',10000));
 
 --
 -- Ensure that some values are uncompressed, to test the faster substring
 -- operation used in that case
 --
-alter table toasttest alter column f1 set storage external;
-insert into toasttest values(repeat('1234567890',10000));
-insert into toasttest values(repeat('1234567890',10000));
+alter table toasttest_oid alter column f1 set storage external;
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+alter table toasttest_oid8 alter column f1 set storage external;
+insert into toasttest_oid8 values(repeat('1234567890',10000));
+insert into toasttest_oid8 values(repeat('1234567890',10000));
 
 -- If the starting position is zero or less, then return from the start of the string
 -- adjusting the length to be consistent with the "negative start" per SQL.
-SELECT substr(f1, -1, 5) from toasttest;
+SELECT substr(f1, -1, 5) from toasttest_oid;
+SELECT substr(f1, -1, 5) from toasttest_oid8;
 
 -- If the length is less than zero, an ERROR is thrown.
-SELECT substr(f1, 5, -1) from toasttest;
+SELECT substr(f1, 5, -1) from toasttest_oid;
+SELECT substr(f1, 5, -1) from toasttest_oid8;
 
 -- If no third argument (length) is provided, the length to the end of the
 -- string is assumed.
-SELECT substr(f1, 99995) from toasttest;
+SELECT substr(f1, 99995) from toasttest_oid;
+SELECT substr(f1, 99995) from toasttest_oid8;
 
 -- If start plus length is > string length, the result is truncated to
 -- string length
-SELECT substr(f1, 99995, 10) from toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+SELECT substr(f1, 99995, 10) from toasttest_oid8;
 
-TRUNCATE TABLE toasttest;
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
+-- TRUNCATE cases for TOAST relations with OID values.
+TRUNCATE TABLE toasttest_oid;
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
 -- expect >0 blocks
 SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
-  FROM pg_class where relname = 'toasttest';
-
-TRUNCATE TABLE toasttest;
-ALTER TABLE toasttest set (toast_tuple_target = 4080);
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
+  FROM pg_class where relname = 'toasttest_oid';
+TRUNCATE TABLE toasttest_oid;
+ALTER TABLE toasttest_oid set (toast_tuple_target = 4080);
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
 -- expect 0 blocks
 SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
-  FROM pg_class where relname = 'toasttest';
+  FROM pg_class where relname = 'toasttest_oid';
+DROP TABLE toasttest_oid;
 
-DROP TABLE toasttest;
+-- TRUNCATE cases for TOAST relation with int8 values.
+TRUNCATE TABLE toasttest_oid8;
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+-- expect >0 blocks
+SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
+  FROM pg_class where relname = 'toasttest_oid8';
+TRUNCATE TABLE toasttest_oid8;
+ALTER TABLE toasttest_oid8 set (toast_tuple_target = 4080);
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+-- expect 0 blocks
+SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
+  FROM pg_class where relname = 'toasttest_oid8';
+DROP TABLE toasttest_oid8;
 
 --
--- test substr with toasted bytea values
+-- test substr with toasted bytea values, for all types of TOAST relations
+-- supported. Do not drop these two relations, for pg_upgrade.
 --
-CREATE TABLE toasttest(f1 bytea);
+CREATE TABLE toasttest_oid(f1 bytea) WITH (toast_value_type = 'oid');
+CREATE TABLE toasttest_oid8(f1 bytea) WITH (toast_value_type = 'oid8');
 
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
 
 --
 -- Ensure that some values are uncompressed, to test the faster substring
 -- operation used in that case
 --
-alter table toasttest alter column f1 set storage external;
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
+alter table toasttest_oid alter column f1 set storage external;
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+alter table toasttest_oid8 alter column f1 set storage external;
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
 
 -- If the starting position is zero or less, then return from the start of the string
 -- adjusting the length to be consistent with the "negative start" per SQL.
-SELECT substr(f1, -1, 5) from toasttest;
+SELECT substr(f1, -1, 5) from toasttest_oid;
+SELECT substr(f1, -1, 5) from toasttest_oid8;
 
 -- If the length is less than zero, an ERROR is thrown.
-SELECT substr(f1, 5, -1) from toasttest;
+SELECT substr(f1, 5, -1) from toasttest_oid;
+SELECT substr(f1, 5, -1) from toasttest_oid8;
 
 -- If no third argument (length) is provided, the length to the end of the
 -- string is assumed.
-SELECT substr(f1, 99995) from toasttest;
+SELECT substr(f1, 99995) from toasttest_oid;
+SELECT substr(f1, 99995) from toasttest_oid8;
 
 -- If start plus length is > string length, the result is truncated to
 -- string length
-SELECT substr(f1, 99995, 10) from toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+SELECT substr(f1, 99995, 10) from toasttest_oid8;
 
-DROP TABLE toasttest;
+-- A relation rewrite leaves the TOAST value attributes unchanged.
+VACUUM FULL toasttest_oid;
+VACUUM FULL toasttest_oid8;
+SELECT c1.relname, a.atttypid::regtype
+  FROM pg_attribute AS a,
+       pg_class AS c1,
+       pg_class AS c2
+  WHERE
+       c1.relname IN ('toasttest_oid', 'toasttest_oid8') AND
+       c1.reltoastrelid = c2.oid AND
+       a.attrelid = c2.oid AND
+       a.attname = 'chunk_id'
+  ORDER BY c1.relname COLLATE "C";
+-- Check that data slices are still accessible.
+SELECT substr(f1, 99995) from toasttest_oid;
+SELECT substr(f1, 99995) from toasttest_oid8;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+SELECT substr(f1, 99995, 10) from toasttest_oid8;
 
 -- test internally compressing datums
 
diff --git a/src/test/regress/sql/type_sanity.sql b/src/test/regress/sql/type_sanity.sql
index c2496823d90e..a0d2e8bcf00b 100644
--- a/src/test/regress/sql/type_sanity.sql
+++ b/src/test/regress/sql/type_sanity.sql
@@ -420,8 +420,8 @@ WHERE c1.relnatts != (SELECT count(*) FROM pg_attribute AS a1
                       WHERE a1.attrelid = c1.oid AND a1.attnum > 0);
 
 -- Cross-check against pg_type entry
--- NOTE: we allow attstorage to be 'plain' even when typstorage is not;
--- this is mainly for toast tables.
+-- NOTE: we allow attstorage to be 'plain' or 'external' even when typstorage
+-- is not; this is mainly for toast tables.
 
 SELECT a1.attrelid, a1.attname, t1.oid, t1.typname
 FROM pg_attribute AS a1, pg_type AS t1
@@ -429,7 +429,7 @@ WHERE a1.atttypid = t1.oid AND
     (a1.attlen != t1.typlen OR
      a1.attalign != t1.typalign OR
      a1.attbyval != t1.typbyval OR
-     (a1.attstorage != t1.typstorage AND a1.attstorage != 'p'));
+     (a1.attstorage != t1.typstorage AND a1.attstorage NOT IN ('e', 'p')));
 
 -- Look for IsCatalogTextUniqueIndexOid() omissions.
 
-- 
2.51.0

v7-0015-Add-new-vartag_external-for-8-byte-TOAST-values.patchtext/x-diff; charset=us-asciiDownload
From 51a19845c14a09b645d53fc492660c3a3a610409 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 14:10:36 +0900
Subject: [PATCH v7 15/15] Add new vartag_external for 8-byte TOAST values

This is a new type of external TOAST pointer, able to be fed 8-byte
TOAST values.  It uses a dedicated vartag_external, which is used when
a TOAST table uses bigint for its chunk_id.

The relevant callbacks are added to toast_external.c.
---
 src/include/access/heaptoast.h                |   8 +-
 src/include/varatt.h                          |  34 +++-
 src/backend/access/common/toast_external.c    | 145 ++++++++++++++++--
 src/backend/access/heap/heaptoast.c           |   1 +
 .../replication/logical/reorderbuffer.c       |  10 +-
 doc/src/sgml/storage.sgml                     |   6 +-
 contrib/amcheck/verify_heapam.c               |   2 +-
 7 files changed, 189 insertions(+), 17 deletions(-)

diff --git a/src/include/access/heaptoast.h b/src/include/access/heaptoast.h
index afa3d8ca95f7..e944d5f8420c 100644
--- a/src/include/access/heaptoast.h
+++ b/src/include/access/heaptoast.h
@@ -81,6 +81,12 @@
 
 #define EXTERN_TUPLE_MAX_SIZE	MaximumBytesPerTuple(EXTERN_TUPLES_PER_PAGE)
 
+#define TOAST_OID8_MAX_CHUNK_SIZE	\
+	(EXTERN_TUPLE_MAX_SIZE -							\
+	 MAXALIGN(SizeofHeapTupleHeader) -					\
+	 (sizeof(uint32) * 2) -								\
+	 sizeof(int32) -									\
+	 VARHDRSZ)
 #define TOAST_OID_MAX_CHUNK_SIZE	\
 	(EXTERN_TUPLE_MAX_SIZE -							\
 	 MAXALIGN(SizeofHeapTupleHeader) -					\
@@ -89,7 +95,7 @@
 	 VARHDRSZ)
 
 /* Maximum size of chunk possible */
-#define TOAST_MAX_CHUNK_SIZE	TOAST_OID_MAX_CHUNK_SIZE
+#define TOAST_MAX_CHUNK_SIZE	Max(TOAST_OID_MAX_CHUNK_SIZE, TOAST_OID8_MAX_CHUNK_SIZE)
 
 /* ----------
  * heap_toast_insert_or_update -
diff --git a/src/include/varatt.h b/src/include/varatt.h
index 035c0f95e5b6..de38d1cd1ce1 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -41,6 +41,27 @@ typedef struct varatt_external_oid
 	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
 }			varatt_external_oid;
 
+/*
+ * struct varatt_external_oid8 is a "larger" version of "TOAST pointer",
+ * that uses an 8-byte integer as value.
+ *
+ * This follows the same properties as varatt_external_oid, except that
+ * this is used in TOAST relations with oid8 as attribute for chunk_id.
+ */
+typedef struct varatt_external_oid8
+{
+	int32		va_rawsize;		/* Original data size (includes header) */
+	uint32		va_extinfo;		/* External saved size (without header) and
+								 * compression method */
+	/*
+	 * Unique ID of value within TOAST table, as two uint32 for alignment
+	 * and padding.
+	 */
+	uint32		va_valueid_lo;
+	uint32		va_valueid_hi;
+	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
+}			varatt_external_oid8;
+
 /*
  * These macros define the "saved size" portion of va_extinfo.  Its remaining
  * two high-order bits identify the compression method.
@@ -90,6 +111,7 @@ typedef enum vartag_external
 	VARTAG_INDIRECT = 1,
 	VARTAG_EXPANDED_RO = 2,
 	VARTAG_EXPANDED_RW = 3,
+	VARTAG_ONDISK_OID8 = 4,
 	VARTAG_ONDISK_OID = 18
 } vartag_external;
 
@@ -111,6 +133,8 @@ VARTAG_SIZE(vartag_external tag)
 		return sizeof(varatt_expanded);
 	else if (tag == VARTAG_ONDISK_OID)
 		return sizeof(varatt_external_oid);
+	else if (tag == VARTAG_ONDISK_OID8)
+		return sizeof(varatt_external_oid8);
 	else
 	{
 		Assert(false);
@@ -367,11 +391,19 @@ VARATT_IS_EXTERNAL_ONDISK_OID(const void *PTR)
 	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID;
 }
 
+/* Is varlena datum a pointer to on-disk toasted data with OID8 value? */
+static inline bool
+VARATT_IS_EXTERNAL_ONDISK_OID8(const void *PTR)
+{
+	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID8;
+}
+
 /* Is varlena datum a pointer to on-disk toasted data? */
 static inline bool
 VARATT_IS_EXTERNAL_ONDISK(const void *PTR)
 {
-	return VARATT_IS_EXTERNAL_ONDISK_OID(PTR);
+	return VARATT_IS_EXTERNAL_ONDISK_OID(PTR) ||
+		VARATT_IS_EXTERNAL_ONDISK_OID8(PTR);
 }
 
 /* Is varlena datum an indirect pointer? */
diff --git a/src/backend/access/common/toast_external.c b/src/backend/access/common/toast_external.c
index e2f0a9dc1c50..431258b2be96 100644
--- a/src/backend/access/common/toast_external.c
+++ b/src/backend/access/common/toast_external.c
@@ -18,8 +18,19 @@
 #include "postgres.h"
 
 #include "access/detoast.h"
+#include "access/genam.h"
 #include "access/heaptoast.h"
 #include "access/toast_external.h"
+#include "catalog/catalog.h"
+#include "miscadmin.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+
+
+/* Callbacks for VARTAG_ONDISK_OID8 */
+static void ondisk_oid8_to_external_data(struct varlena *attr,
+										 toast_external_data *data);
+static struct varlena *ondisk_oid8_create_external_data(toast_external_data data);
 
 /* Callbacks for VARTAG_ONDISK_OID */
 static void ondisk_oid_to_external_data(struct varlena *attr,
@@ -28,7 +39,7 @@ static struct varlena *ondisk_oid_create_external_data(toast_external_data data)
 
 /*
  * Fetch the possibly-unaligned contents of an on-disk external TOAST with
- * OID values into a local "varatt_external_oid" pointer.
+ * OID or OID8 values into a local "varatt_external_*" pointer.
  *
  * This should be just a memcpy, but some versions of gcc seem to produce
  * broken code that assumes the datum contents are aligned.  Introducing
@@ -45,9 +56,20 @@ varatt_external_oid_get_pointer(varatt_external_oid *toast_pointer,
 	memcpy(toast_pointer, VARDATA_EXTERNAL(attre), sizeof(varatt_external_oid));
 }
 
+static inline void
+varatt_external_oid8_get_pointer(varatt_external_oid8 *toast_pointer,
+								 struct varlena *attr)
+{
+	varattrib_1b_e *attre = (varattrib_1b_e *) attr;
+
+	Assert(VARATT_IS_EXTERNAL_ONDISK_OID8(attre));
+	Assert(VARSIZE_EXTERNAL(attre) == sizeof(varatt_external_oid8) + VARHDRSZ_EXTERNAL);
+	memcpy(toast_pointer, VARDATA_EXTERNAL(attre), sizeof(varatt_external_oid8));
+}
+
 /*
  * Decompressed size of an on-disk varlena; but note argument is a struct
- * varatt_external_oid.
+ * varatt_external_oid or varatt_external_oid8.
  */
 static inline Size
 varatt_external_oid_get_extsize(varatt_external_oid toast_pointer)
@@ -55,9 +77,15 @@ varatt_external_oid_get_extsize(varatt_external_oid toast_pointer)
 	return toast_pointer.va_extinfo & VARLENA_EXTSIZE_MASK;
 }
 
+static inline Size
+varatt_external_oid8_get_extsize(varatt_external_oid8 toast_pointer)
+{
+	return toast_pointer.va_extinfo & VARLENA_EXTSIZE_MASK;
+}
+
 /*
  * Compression method of an on-disk varlena; but note argument is a struct
- *  varatt_external_oid.
+ *  varatt_external_oid or varatt_external_oid8.
  */
 static inline uint32
 varatt_external_oid_get_compress_method(varatt_external_oid toast_pointer)
@@ -65,6 +93,12 @@ varatt_external_oid_get_compress_method(varatt_external_oid toast_pointer)
 	return toast_pointer.va_extinfo >> VARLENA_EXTSIZE_BITS;
 }
 
+static inline uint32
+varatt_external_oid8_get_compress_method(varatt_external_oid8 toast_pointer)
+{
+	return toast_pointer.va_extinfo >> VARLENA_EXTSIZE_BITS;
+}
+
 /*
  * Testing whether an externally-stored TOAST value is compressed now requires
  * comparing size stored in va_extinfo (the actual length of the external data)
@@ -79,6 +113,19 @@ varatt_external_oid_is_compressed(varatt_external_oid toast_pointer)
 		(Size) (toast_pointer.va_rawsize - VARHDRSZ);
 }
 
+static inline bool
+varatt_external_oid8_is_compressed(varatt_external_oid8 toast_pointer)
+{
+	return varatt_external_oid8_get_extsize(toast_pointer) <
+		(Size) (toast_pointer.va_rawsize - VARHDRSZ);
+}
+
+/*
+ * Size of an EXTERNAL datum that contains a standard TOAST pointer
+ * (oid8 value).
+ */
+#define TOAST_POINTER_OID8_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_oid8))
+
 /*
  * Size of an EXTERNAL datum that contains a standard TOAST pointer (OID
  * value).
@@ -99,6 +146,12 @@ varatt_external_oid_is_compressed(varatt_external_oid toast_pointer)
  * individual fields.
  */
 static const toast_external_info toast_external_infos[TOAST_EXTERNAL_INFO_SIZE] = {
+	[VARTAG_ONDISK_OID8] = {
+		.toast_pointer_size = TOAST_POINTER_OID8_SIZE,
+		.maximum_chunk_size = TOAST_OID_MAX_CHUNK_SIZE,
+		.to_external_data = ondisk_oid8_to_external_data,
+		.create_external_data = ondisk_oid8_create_external_data,
+	},
 	[VARTAG_ONDISK_OID] = {
 		.toast_pointer_size = TOAST_OID_POINTER_SIZE,
 		.maximum_chunk_size = TOAST_OID_MAX_CHUNK_SIZE,
@@ -155,22 +208,33 @@ toast_external_info_get_pointer_size(uint8 tag)
 uint8
 toast_external_assign_vartag(Oid toastrelid, Oid8 valueid)
 {
+	Oid		toast_typid;
+
 	/*
-	 * If dealing with a code path where a TOAST relation may not be assigned,
-	 * like heap_toast_insert_or_update(), just use the legacy
-	 * vartag_external.
+	 * If dealing with a code path where a TOAST relation may not be assigned
+	 * like heap_toast_insert_or_update(), just use the default with an OID
+	 * type.
+	 *
+	 * In bootstrap mode, we should not do any kind of syscache lookups,
+	 * so also rely on OID.
 	 */
-	if (!OidIsValid(toastrelid))
+	if (!OidIsValid(toastrelid) || IsBootstrapProcessingMode())
 		return VARTAG_ONDISK_OID;
 
 	/*
-	 * Currently there is only one type of vartag_external supported: 4-byte
-	 * value with OID for the chunk_id type.
+	 * Two types of vartag_external are currently supported: OID and OID8,
+	 * which depend on the type assigned to "chunk_id" for the TOAST table.
 	 *
-	 * Note: This routine will be extended to be able to use multiple
-	 * vartag_external within a single TOAST relation type, that may change
-	 * depending on the value used.
+	 * XXX: Should we assign from the start an OID vartag if dealing with
+	 * a TOAST relation with OID8 as value if the value assigned is less
+	 * than UINT_MAX?  This just takes the "safe" approach of assigning
+	 * the larger vartag in all cases, but this can be made cheaper
+	 * depending on the OID consumption.
 	 */
+	toast_typid = get_atttype(toastrelid, 1);
+	if (toast_typid == OID8OID)
+		return VARTAG_ONDISK_OID8;
+
 	return VARTAG_ONDISK_OID;
 }
 
@@ -179,6 +243,63 @@ toast_external_assign_vartag(Oid toastrelid, Oid8 valueid)
  * the in-memory representation toast_external_data used in the backend.
  */
 
+/* Callbacks for VARTAG_ONDISK_OID8 */
+static void
+ondisk_oid8_to_external_data(struct varlena *attr, toast_external_data *data)
+{
+	varatt_external_oid8	external;
+
+	varatt_external_oid8_get_pointer(&external, attr);
+	data->rawsize = external.va_rawsize;
+
+	/* External size and compression methods are stored in the same field */
+	if (varatt_external_oid8_is_compressed(external))
+	{
+		data->extsize = varatt_external_oid8_get_extsize(external);
+		data->compression_method = varatt_external_oid8_get_compress_method(external);
+	}
+	else
+	{
+		data->extsize = external.va_extinfo;
+		data->compression_method = TOAST_INVALID_COMPRESSION_ID;
+	}
+
+	data->valueid = (((uint64) external.va_valueid_hi) << 32) |
+		external.va_valueid_lo;
+	data->toastrelid = external.va_toastrelid;
+
+}
+
+static struct varlena *
+ondisk_oid8_create_external_data(toast_external_data data)
+{
+	struct varlena *result = NULL;
+	varatt_external_oid8 external;
+
+	external.va_rawsize = data.rawsize;
+
+	if (data.compression_method != TOAST_INVALID_COMPRESSION_ID)
+	{
+		/* Set size and compression method, in a single field. */
+		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(external,
+													 data.extsize,
+													 data.compression_method);
+	}
+	else
+		external.va_extinfo = data.extsize;
+
+	external.va_toastrelid = data.toastrelid;
+	external.va_valueid_hi = (((uint64) data.valueid) >> 32);
+	external.va_valueid_lo = (uint32) data.valueid;
+
+	result = (struct varlena *) palloc(TOAST_POINTER_OID8_SIZE);
+	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_OID8);
+	memcpy(VARDATA_EXTERNAL(result), &external, sizeof(external));
+
+	return result;
+}
+
+
 /* Callbacks for VARTAG_ONDISK_OID */
 
 /*
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index 50e9bf9047f9..cba6e14ea805 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -32,6 +32,7 @@
 #include "access/toast_helper.h"
 #include "access/toast_internals.h"
 #include "utils/fmgroids.h"
+#include "utils/syscache.h"
 
 
 /* ----------
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 2db447f58ad5..32c512854f20 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -4986,14 +4986,22 @@ ReorderBufferToastAppendChunk(ReorderBuffer *rb, ReorderBufferTXN *txn,
 	TupleDesc	desc = RelationGetDescr(relation);
 	Oid8		chunk_id;
 	int32		chunk_seq;
+	Oid			toast_typid;
 
 	if (txn->toast_hash == NULL)
 		ReorderBufferToastInitHash(rb, txn);
+	toast_typid = TupleDescAttr(desc, 0)->atttypid;
 
 	Assert(IsToastRelation(relation));
 
 	newtup = change->data.tp.newtuple;
-	chunk_id = DatumGetObjectId(fastgetattr(newtup, 1, desc, &isnull));
+	/* This depends on the type of TOAST value dealt with. */
+	if (toast_typid == OIDOID)
+		chunk_id = DatumGetObjectId(fastgetattr(newtup, 1, desc, &isnull));
+	else if (toast_typid == INT8OID)
+		chunk_id = DatumGetUInt64(fastgetattr(newtup, 1, desc, &isnull));
+	else
+		Assert(false);
 	Assert(!isnull);
 	chunk_seq = DatumGetInt32(fastgetattr(newtup, 2, desc, &isnull));
 	Assert(!isnull);
diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml
index afddf663fec5..dbec30d48b4a 100644
--- a/doc/src/sgml/storage.sgml
+++ b/doc/src/sgml/storage.sgml
@@ -417,7 +417,11 @@ described in more detail below.
 
 <para>
 Out-of-line values are divided (after compression if used) into chunks of at
-most <symbol>TOAST_OID_MAX_CHUNK_SIZE</symbol> bytes (by default this value is chosen
+most <symbol>TOAST_OID_MAX_CHUNK_SIZE</symbol> bytes if the
+<acronym>TOAST</acronym> relation uses the <literal>oid</literal> type for
+<literal>chunk_id</literal>, or <symbol>TOAST_OID8_MAX_CHUNK_SIZE</symbol>
+bytes if the <acronym>TOAST</acronym> relation uses the <literal>oid8</literal>
+type for <literal>chunk_id</literal> (by default these values are chosen
 so that four chunk rows will fit on a page, making it about 2000 bytes).
 Each chunk is stored as a separate row in the <acronym>TOAST</acronym> table
 belonging to the owning table.  Every
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 143e6baa35cf..8cea9ad31bcd 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1733,7 +1733,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	{
 		uint8		va_tag = VARTAG_EXTERNAL(tp + ctx->offset);
 
-		if (va_tag != VARTAG_ONDISK_OID)
+		if (va_tag != VARTAG_ONDISK_OID && va_tag != VARTAG_ONDISK_OID8)
 		{
 			report_corruption(ctx,
 							  psprintf("toasted attribute has unexpected TOAST tag %u",
-- 
2.51.0

#49Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#48)
15 attachment(s)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

On Tue, Sep 30, 2025 at 03:26:14PM +0900, Michael Paquier wrote:

There were a few conflicts, so here is a rebased v7, moving the patch
to the next CF. I have been sitting on this patch for six weeks for
the moment.

Attached is a rebased v8, fixing a couple of conflicts.

Tom, you are registered as a reviewer of the patch. The point of
contention of the patch, where I see there is no consensus yet, is if
my approach of using a redirection for the external TOAST pointers
with a new layer to facilitate the addition of more vartags (aka the
64b value vartag proposed here, concept that could also apply to
compression methods later on) is acceptable. Moving to a different
approach, like the "brutal" one I am naming upthread where the
redirection layer is replaced by changes in all the code paths that
need to be touched, would be of course cheaper at runtime as there
would be no more redirection, but the maintenance would be a nightmare
the more vartags we add, and I have some plans for more of these.
Doing the switch would be a few hours work, so that would not be a big
deal, I guess. The important part is an agreement about the approach,
IMO.

This point still got no reply. It would be nice to do something for
this release regarding this old issue, IMO..
--
Michael

Attachments:

v8-0001-Implement-oid8-data-type.patchtext/x-diff; charset=us-asciiDownload
From 73533e9c61a7df0763c91e918b881947d7a820cc Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 11:03:17 +0900
Subject: [PATCH v8 01/15] Implement oid8 data type

This new identifier type will be used for 8-byte TOAST values, and can
be useful for other purposes, not yet defined as of writing this patch.
The following operators are added for this data type:
- Casts with integer types and OID.
- btree and hash operators
- min/max functions.
- Tests and documentation.

XXX: Requires catversion bump.
---
 src/include/c.h                           |  11 +-
 src/include/catalog/pg_aggregate.dat      |   6 +
 src/include/catalog/pg_amop.dat           |  23 +++
 src/include/catalog/pg_amproc.dat         |  12 ++
 src/include/catalog/pg_cast.dat           |  14 ++
 src/include/catalog/pg_opclass.dat        |   4 +
 src/include/catalog/pg_operator.dat       |  26 +++
 src/include/catalog/pg_opfamily.dat       |   4 +
 src/include/catalog/pg_proc.dat           |  64 +++++++
 src/include/catalog/pg_type.dat           |   5 +
 src/include/fmgr.h                        |   2 +
 src/include/postgres.h                    |  20 +++
 src/backend/access/nbtree/nbtcompare.c    |  82 +++++++++
 src/backend/bootstrap/bootstrap.c         |   2 +
 src/backend/utils/adt/Makefile            |   1 +
 src/backend/utils/adt/int8.c              |   8 +
 src/backend/utils/adt/meson.build         |   1 +
 src/backend/utils/adt/oid8.c              | 171 +++++++++++++++++++
 src/fe_utils/print.c                      |   1 +
 src/test/regress/expected/oid8.out        | 196 ++++++++++++++++++++++
 src/test/regress/expected/oid8.sql        |   0
 src/test/regress/expected/opr_sanity.out  |   7 +
 src/test/regress/expected/type_sanity.out |   1 +
 src/test/regress/parallel_schedule        |   2 +-
 src/test/regress/sql/oid8.sql             |  57 +++++++
 src/test/regress/sql/type_sanity.sql      |   1 +
 doc/src/sgml/datatype.sgml                |  11 ++
 doc/src/sgml/func/func-aggregate.sgml     |   8 +-
 28 files changed, 734 insertions(+), 6 deletions(-)
 create mode 100644 src/backend/utils/adt/oid8.c
 create mode 100644 src/test/regress/expected/oid8.out
 create mode 100644 src/test/regress/expected/oid8.sql
 create mode 100644 src/test/regress/sql/oid8.sql

diff --git a/src/include/c.h b/src/include/c.h
index a40f0cf4642c..441d1e63e178 100644
--- a/src/include/c.h
+++ b/src/include/c.h
@@ -560,6 +560,7 @@ typedef uint32 bits32;			/* >= 32 bits */
 /* snprintf format strings to use for 64-bit integers */
 #define INT64_FORMAT "%" PRId64
 #define UINT64_FORMAT "%" PRIu64
+#define OID8_FORMAT "%" PRIu64
 
 /*
  * 128-bit signed and unsigned integers
@@ -646,7 +647,7 @@ typedef double float8;
 #define FLOAT8PASSBYVAL true
 
 /*
- * Oid, RegProcedure, TransactionId, SubTransactionId, MultiXactId,
+ * Oid, Oid8, RegProcedure, TransactionId, SubTransactionId, MultiXactId,
  * CommandId
  */
 
@@ -678,6 +679,12 @@ typedef uint32 CommandId;
 #define FirstCommandId	((CommandId) 0)
 #define InvalidCommandId	(~(CommandId)0)
 
+/* 8-byte Object ID */
+typedef uint64 Oid8;
+
+#define InvalidOid8		((Oid8) 0)
+#define OID8_MAX	UINT64_MAX
+#define atooid8(x) ((Oid8) strtou64((x), NULL, 10))
 
 /* ----------------
  *		Variable-length datatypes all share the 'struct varlena' header.
@@ -778,6 +785,8 @@ typedef NameData *Name;
 
 #define OidIsValid(objectId)  ((bool) ((objectId) != InvalidOid))
 
+#define Oid8IsValid(objectId)  ((bool) ((objectId) != InvalidOid8))
+
 #define RegProcedureIsValid(p)	OidIsValid(p)
 
 
diff --git a/src/include/catalog/pg_aggregate.dat b/src/include/catalog/pg_aggregate.dat
index 870769e8f14c..17c70d3033e8 100644
--- a/src/include/catalog/pg_aggregate.dat
+++ b/src/include/catalog/pg_aggregate.dat
@@ -104,6 +104,9 @@
 { aggfnoid => 'max(oid)', aggtransfn => 'oidlarger',
   aggcombinefn => 'oidlarger', aggsortop => '>(oid,oid)',
   aggtranstype => 'oid' },
+{ aggfnoid => 'max(oid8)', aggtransfn => 'oid8larger',
+  aggcombinefn => 'oid8larger', aggsortop => '>(oid8,oid8)',
+  aggtranstype => 'oid8' },
 { aggfnoid => 'max(float4)', aggtransfn => 'float4larger',
   aggcombinefn => 'float4larger', aggsortop => '>(float4,float4)',
   aggtranstype => 'float4' },
@@ -178,6 +181,9 @@
 { aggfnoid => 'min(oid)', aggtransfn => 'oidsmaller',
   aggcombinefn => 'oidsmaller', aggsortop => '<(oid,oid)',
   aggtranstype => 'oid' },
+{ aggfnoid => 'min(oid8)', aggtransfn => 'oid8smaller',
+  aggcombinefn => 'oid8smaller', aggsortop => '<(oid8,oid8)',
+  aggtranstype => 'oid8' },
 { aggfnoid => 'min(float4)', aggtransfn => 'float4smaller',
   aggcombinefn => 'float4smaller', aggsortop => '<(float4,float4)',
   aggtranstype => 'float4' },
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 2a693cfc31c6..2c3004d53611 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -180,6 +180,24 @@
 { amopfamily => 'btree/oid_ops', amoplefttype => 'oid', amoprighttype => 'oid',
   amopstrategy => '5', amopopr => '>(oid,oid)', amopmethod => 'btree' },
 
+# btree oid8_ops
+
+{ amopfamily => 'btree/oid8_ops', amoplefttype => 'oid8',
+  amoprighttype => 'oid8', amopstrategy => '1', amopopr => '<(oid8,oid8)',
+  amopmethod => 'btree' },
+{ amopfamily => 'btree/oid8_ops', amoplefttype => 'oid8',
+  amoprighttype => 'oid8', amopstrategy => '2', amopopr => '<=(oid8,oid8)',
+  amopmethod => 'btree' },
+{ amopfamily => 'btree/oid8_ops', amoplefttype => 'oid8',
+  amoprighttype => 'oid8', amopstrategy => '3', amopopr => '=(oid8,oid8)',
+  amopmethod => 'btree' },
+{ amopfamily => 'btree/oid8_ops', amoplefttype => 'oid8',
+  amoprighttype => 'oid8', amopstrategy => '4', amopopr => '>=(oid8,oid8)',
+  amopmethod => 'btree' },
+{ amopfamily => 'btree/oid8_ops', amoplefttype => 'oid8',
+  amoprighttype => 'oid8', amopstrategy => '5', amopopr => '>(oid8,oid8)',
+  amopmethod => 'btree' },
+
 # btree xid8_ops
 
 { amopfamily => 'btree/xid8_ops', amoplefttype => 'xid8',
@@ -974,6 +992,11 @@
 { amopfamily => 'hash/oid_ops', amoplefttype => 'oid', amoprighttype => 'oid',
   amopstrategy => '1', amopopr => '=(oid,oid)', amopmethod => 'hash' },
 
+# oid8_ops
+{ amopfamily => 'hash/oid8_ops', amoplefttype => 'oid8',
+  amoprighttype => 'oid8', amopstrategy => '1', amopopr => '=(oid8,oid8)',
+  amopmethod => 'hash' },
+
 # oidvector_ops
 { amopfamily => 'hash/oidvector_ops', amoplefttype => 'oidvector',
   amoprighttype => 'oidvector', amopstrategy => '1',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index e3477500baa7..d3719b3610c4 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -213,6 +213,14 @@
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' },
+{ amprocfamily => 'btree/oid8_ops', amproclefttype => 'oid8',
+  amprocrighttype => 'oid8', amprocnum => '1', amproc => 'btoid8cmp' },
+{ amprocfamily => 'btree/oid8_ops', amproclefttype => 'oid8',
+  amprocrighttype => 'oid8', amprocnum => '2', amproc => 'btoid8sortsupport' },
+{ amprocfamily => 'btree/oid8_ops', amproclefttype => 'oid8',
+  amprocrighttype => 'oid8', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/oid8_ops', amproclefttype => 'oid8',
+  amprocrighttype => 'oid8', amprocnum => '6', amproc => 'btoid8skipsupport' },
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
@@ -432,6 +440,10 @@
   amprocrighttype => 'xid8', amprocnum => '1', amproc => 'hashxid8' },
 { amprocfamily => 'hash/xid8_ops', amproclefttype => 'xid8',
   amprocrighttype => 'xid8', amprocnum => '2', amproc => 'hashxid8extended' },
+{ amprocfamily => 'hash/oid8_ops', amproclefttype => 'oid8',
+  amprocrighttype => 'oid8', amprocnum => '1', amproc => 'hashoid8' },
+{ amprocfamily => 'hash/oid8_ops', amproclefttype => 'oid8',
+  amprocrighttype => 'oid8', amprocnum => '2', amproc => 'hashoid8extended' },
 { amprocfamily => 'hash/cid_ops', amproclefttype => 'cid',
   amprocrighttype => 'cid', amprocnum => '1', amproc => 'hashcid' },
 { amprocfamily => 'hash/cid_ops', amproclefttype => 'cid',
diff --git a/src/include/catalog/pg_cast.dat b/src/include/catalog/pg_cast.dat
index fbfd669587f0..695f6b2a5e73 100644
--- a/src/include/catalog/pg_cast.dat
+++ b/src/include/catalog/pg_cast.dat
@@ -296,6 +296,20 @@
 { castsource => 'regdatabase', casttarget => 'int4', castfunc => '0',
   castcontext => 'a', castmethod => 'b' },
 
+# OID8 category: allow implicit conversion from any integral type (including
+# int8), as well as assignment coercion to int8.
+{ castsource => 'int8', casttarget => 'oid8', castfunc => '0',
+  castcontext => 'i', castmethod => 'b' },
+{ castsource => 'int2', casttarget => 'oid8', castfunc => 'int8(int2)',
+  castcontext => 'i', castmethod => 'f' },
+{ castsource => 'int4', casttarget => 'oid8', castfunc => 'int8(int4)',
+  castcontext => 'i', castmethod => 'f' },
+{ castsource => 'oid8', casttarget => 'int8', castfunc => '0',
+  castcontext => 'a', castmethod => 'b' },
+# Assignment coercion from oid to oid8.
+{ castsource => 'oid', casttarget => 'oid8', castfunc => 'oid8(oid)',
+  castcontext => 'a', castmethod => 'f' },
+
 # String category
 { castsource => 'text', casttarget => 'bpchar', castfunc => '0',
   castcontext => 'i', castmethod => 'b' },
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 4a9624802aa5..c0de88fabc49 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -177,6 +177,10 @@
   opcintype => 'xid8' },
 { opcmethod => 'btree', opcname => 'xid8_ops', opcfamily => 'btree/xid8_ops',
   opcintype => 'xid8' },
+{ opcmethod => 'hash', opcname => 'oid8_ops', opcfamily => 'hash/oid8_ops',
+  opcintype => 'oid8' },
+{ opcmethod => 'btree', opcname => 'oid8_ops', opcfamily => 'btree/oid8_ops',
+  opcintype => 'oid8' },
 { opcmethod => 'hash', opcname => 'cid_ops', opcfamily => 'hash/cid_ops',
   opcintype => 'cid' },
 { opcmethod => 'hash', opcname => 'tid_ops', opcfamily => 'hash/tid_ops',
diff --git a/src/include/catalog/pg_operator.dat b/src/include/catalog/pg_operator.dat
index 6d9dc1528d6e..87a7255490a7 100644
--- a/src/include/catalog/pg_operator.dat
+++ b/src/include/catalog/pg_operator.dat
@@ -3460,4 +3460,30 @@
   oprcode => 'multirange_after_multirange', oprrest => 'multirangesel',
   oprjoin => 'scalargtjoinsel' },
 
+{ oid => '8262', descr => 'equal',
+  oprname => '=', oprcanmerge => 't', oprcanhash => 't', oprleft => 'oid8',
+  oprright => 'oid8', oprresult => 'bool', oprcom => '=(oid8,oid8)',
+  oprnegate => '<>(oid8,oid8)', oprcode => 'oid8eq', oprrest => 'eqsel',
+  oprjoin => 'eqjoinsel' },
+{ oid => '8263', descr => 'not equal',
+  oprname => '<>', oprleft => 'oid8', oprright => 'oid8', oprresult => 'bool',
+  oprcom => '<>(oid8,oid8)', oprnegate => '=(oid8,oid8)', oprcode => 'oid8ne',
+  oprrest => 'neqsel', oprjoin => 'neqjoinsel' },
+{ oid => '8264', descr => 'less than',
+  oprname => '<', oprleft => 'oid8', oprright => 'oid8', oprresult => 'bool',
+  oprcom => '>(oid8,oid8)', oprnegate => '>=(oid8,oid8)', oprcode => 'oid8lt',
+  oprrest => 'scalarltsel', oprjoin => 'scalarltjoinsel' },
+{ oid => '8265', descr => 'greater than',
+  oprname => '>', oprleft => 'oid8', oprright => 'oid8', oprresult => 'bool',
+  oprcom => '<(oid8,oid8)', oprnegate => '<=(oid8,oid8)', oprcode => 'oid8gt',
+  oprrest => 'scalargtsel', oprjoin => 'scalargtjoinsel' },
+{ oid => '8266', descr => 'less than or equal',
+  oprname => '<=', oprleft => 'oid8', oprright => 'oid8', oprresult => 'bool',
+  oprcom => '>=(oid8,oid8)', oprnegate => '>(oid8,oid8)', oprcode => 'oid8le',
+  oprrest => 'scalarlesel', oprjoin => 'scalarlejoinsel' },
+{ oid => '8267', descr => 'greater than or equal',
+  oprname => '>=', oprleft => 'oid8', oprright => 'oid8', oprresult => 'bool',
+  oprcom => '<=(oid8,oid8)', oprnegate => '<(oid8,oid8)', oprcode => 'oid8ge',
+  oprrest => 'scalargesel', oprjoin => 'scalargejoinsel' },
+
 ]
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index f7dcb96b43ce..54472ce97dcd 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -116,6 +116,10 @@
   opfmethod => 'hash', opfname => 'xid8_ops' },
 { oid => '5067',
   opfmethod => 'btree', opfname => 'xid8_ops' },
+{ oid => '8278',
+  opfmethod => 'hash', opfname => 'oid8_ops' },
+{ oid => '8279',
+  opfmethod => 'btree', opfname => 'oid8_ops' },
 { oid => '2226',
   opfmethod => 'hash', opfname => 'cid_ops' },
 { oid => '2227',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 664319407008..6c392a57c7f3 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1046,6 +1046,15 @@
 { oid => '6405', descr => 'skip support',
   proname => 'btoidskipsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btoidskipsupport' },
+{ oid => '8282', descr => 'less-equal-greater',
+  proname => 'btoid8cmp', proleakproof => 't', prorettype => 'int4',
+  proargtypes => 'oid8 oid8', prosrc => 'btoid8cmp' },
+{ oid => '8283', descr => 'sort support',
+  proname => 'btoid8sortsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoid8sortsupport' },
+{ oid => '8284', descr => 'skip support',
+  proname => 'btoid8skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoid8skipsupport' },
 { oid => '404', descr => 'less-equal-greater',
   proname => 'btoidvectorcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'oidvector oidvector', prosrc => 'btoidvectorcmp' },
@@ -12612,4 +12621,59 @@
   proargnames => '{pid,io_id,io_generation,state,operation,off,length,target,handle_data_len,raw_result,result,target_desc,f_sync,f_localmem,f_buffered}',
   prosrc => 'pg_get_aios' },
 
+# oid8 related functions
+{ oid => '8255', descr => 'convert oid to oid8',
+  proname => 'oid8', prorettype => 'oid8', proargtypes => 'oid',
+  prosrc => 'oidtooid8' },
+{ oid => '8257', descr => 'I/O',
+  proname => 'oid8in', prorettype => 'oid8', proargtypes => 'cstring',
+  prosrc => 'oid8in' },
+{ oid => '8258', descr => 'I/O',
+  proname => 'oid8out', prorettype => 'cstring', proargtypes => 'oid8',
+  prosrc => 'oid8out' },
+{ oid => '8259', descr => 'I/O',
+  proname => 'oid8recv', prorettype => 'oid8', proargtypes => 'internal',
+  prosrc => 'oid8recv' },
+{ oid => '8260', descr => 'I/O',
+  proname => 'oid8send', prorettype => 'bytea', proargtypes => 'oid8',
+  prosrc => 'oid8send' },
+# Comparators
+{ oid => '8268',
+  proname => 'oid8eq', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'oid8 oid8', prosrc => 'oid8eq' },
+{ oid => '8269',
+  proname => 'oid8ne', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'oid8 oid8', prosrc => 'oid8ne' },
+{ oid => '8270',
+  proname => 'oid8lt', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'oid8 oid8', prosrc => 'oid8lt' },
+{ oid => '8271',
+  proname => 'oid8le', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'oid8 oid8', prosrc => 'oid8le' },
+{ oid => '8272',
+  proname => 'oid8gt', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'oid8 oid8', prosrc => 'oid8gt' },
+{ oid => '8273',
+  proname => 'oid8ge', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'oid8 oid8', prosrc => 'oid8ge' },
+# Aggregates
+{ oid => '8274', descr => 'larger of two',
+  proname => 'oid8larger', prorettype => 'oid8', proargtypes => 'oid8 oid8',
+  prosrc => 'oid8larger' },
+{ oid => '8275', descr => 'smaller of two',
+  proname => 'oid8smaller', prorettype => 'oid8', proargtypes => 'oid8 oid8',
+  prosrc => 'oid8smaller' },
+{ oid => '8276', descr => 'maximum value of all oid8 input values',
+  proname => 'max', prokind => 'a', proisstrict => 'f', prorettype => 'oid8',
+  proargtypes => 'oid8', prosrc => 'aggregate_dummy' },
+{ oid => '8277', descr => 'minimum value of all oid8 input values',
+  proname => 'min', prokind => 'a', proisstrict => 'f', prorettype => 'oid8',
+  proargtypes => 'oid8', prosrc => 'aggregate_dummy' },
+{ oid => '8280', descr => 'hash',
+  proname => 'hashoid8', prorettype => 'int4', proargtypes => 'oid8',
+  prosrc => 'hashoid8' },
+{ oid => '8281', descr => 'hash',
+  proname => 'hashoid8extended', prorettype => 'int8',
+  proargtypes => 'oid8 int8', prosrc => 'hashoid8extended' },
+
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index cb730aeac864..704f2890cb28 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -700,4 +700,9 @@
   typreceive => 'brin_minmax_multi_summary_recv',
   typsend => 'brin_minmax_multi_summary_send', typalign => 'i',
   typstorage => 'x', typcollation => 'default' },
+{ oid => '8256', array_type_oid => '8261',
+  descr => 'object identifier(oid8), 8 bytes',
+  typname => 'oid8', typlen => '8', typbyval => 't',
+  typcategory => 'N', typinput => 'oid8in', typoutput => 'oid8out',
+  typreceive => 'oid8recv', typsend => 'oid8send', typalign => 'd' },
 ]
diff --git a/src/include/fmgr.h b/src/include/fmgr.h
index 74fe3ea05758..c127d2f87585 100644
--- a/src/include/fmgr.h
+++ b/src/include/fmgr.h
@@ -273,6 +273,7 @@ extern struct varlena *pg_detoast_datum_packed(struct varlena *datum);
 #define PG_GETARG_CHAR(n)	 DatumGetChar(PG_GETARG_DATUM(n))
 #define PG_GETARG_BOOL(n)	 DatumGetBool(PG_GETARG_DATUM(n))
 #define PG_GETARG_OID(n)	 DatumGetObjectId(PG_GETARG_DATUM(n))
+#define PG_GETARG_OID8(n)	 DatumGetObjectId8(PG_GETARG_DATUM(n))
 #define PG_GETARG_POINTER(n) DatumGetPointer(PG_GETARG_DATUM(n))
 #define PG_GETARG_CSTRING(n) DatumGetCString(PG_GETARG_DATUM(n))
 #define PG_GETARG_NAME(n)	 DatumGetName(PG_GETARG_DATUM(n))
@@ -358,6 +359,7 @@ extern struct varlena *pg_detoast_datum_packed(struct varlena *datum);
 #define PG_RETURN_CHAR(x)	 return CharGetDatum(x)
 #define PG_RETURN_BOOL(x)	 return BoolGetDatum(x)
 #define PG_RETURN_OID(x)	 return ObjectIdGetDatum(x)
+#define PG_RETURN_OID8(x)	 return ObjectId8GetDatum(x)
 #define PG_RETURN_POINTER(x) return PointerGetDatum(x)
 #define PG_RETURN_CSTRING(x) return CStringGetDatum(x)
 #define PG_RETURN_NAME(x)	 return NameGetDatum(x)
diff --git a/src/include/postgres.h b/src/include/postgres.h
index 357cbd6fd961..a5a0e3b7cbfa 100644
--- a/src/include/postgres.h
+++ b/src/include/postgres.h
@@ -264,6 +264,26 @@ ObjectIdGetDatum(Oid X)
 	return (Datum) X;
 }
 
+/*
+ * DatumGetObjectId8
+ *		Returns 8-byte object identifier value of a datum.
+ */
+static inline Oid8
+DatumGetObjectId8(Datum X)
+{
+	return (Oid8) X;
+}
+
+/*
+ * ObjectId8GetDatum
+ *		Returns datum representation for an 8-byte object identifier
+ */
+static inline Datum
+ObjectId8GetDatum(Oid8 X)
+{
+	return (Datum) X;
+}
+
 /*
  * DatumGetTransactionId
  *		Returns transaction identifier value of a datum.
diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c
index 188c27b4925f..3f59ba3f1ad0 100644
--- a/src/backend/access/nbtree/nbtcompare.c
+++ b/src/backend/access/nbtree/nbtcompare.c
@@ -498,6 +498,88 @@ btoidskipsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+Datum
+btoid8cmp(PG_FUNCTION_ARGS)
+{
+	Oid8		a = PG_GETARG_OID8(0);
+	Oid8		b = PG_GETARG_OID8(1);
+
+	if (a > b)
+		PG_RETURN_INT32(A_GREATER_THAN_B);
+	else if (a == b)
+		PG_RETURN_INT32(0);
+	else
+		PG_RETURN_INT32(A_LESS_THAN_B);
+}
+
+static int
+btoid8fastcmp(Datum x, Datum y, SortSupport ssup)
+{
+	Oid8		a = DatumGetObjectId8(x);
+	Oid8		b = DatumGetObjectId8(y);
+
+	if (a > b)
+		return A_GREATER_THAN_B;
+	else if (a == b)
+		return 0;
+	else
+		return A_LESS_THAN_B;
+}
+
+Datum
+btoid8sortsupport(PG_FUNCTION_ARGS)
+{
+	SortSupport ssup = (SortSupport) PG_GETARG_POINTER(0);
+
+	ssup->comparator = btoid8fastcmp;
+	PG_RETURN_VOID();
+}
+
+static Datum
+oid8_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Oid8		oexisting = DatumGetObjectId8(existing);
+
+	if (oexisting == InvalidOid8)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return ObjectId8GetDatum(oexisting - 1);
+}
+
+static Datum
+oid8_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Oid8		oexisting = DatumGetObjectId8(existing);
+
+	if (oexisting == OID8_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return ObjectId8GetDatum(oexisting + 1);
+}
+
+Datum
+btoid8skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = oid8_decrement;
+	sksup->increment = oid8_increment;
+	sksup->low_elem = ObjectId8GetDatum(InvalidOid8);
+	sksup->high_elem = ObjectId8GetDatum(OID8_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btoidvectorcmp(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/bootstrap/bootstrap.c b/src/backend/bootstrap/bootstrap.c
index fc8638c1b61b..48e6966e6b48 100644
--- a/src/backend/bootstrap/bootstrap.c
+++ b/src/backend/bootstrap/bootstrap.c
@@ -115,6 +115,8 @@ static const struct typinfo TypInfo[] = {
 	F_TEXTIN, F_TEXTOUT},
 	{"oid", OIDOID, 0, 4, true, TYPALIGN_INT, TYPSTORAGE_PLAIN, InvalidOid,
 	F_OIDIN, F_OIDOUT},
+	{"oid8", OID8OID, 0, 8, true, TYPALIGN_DOUBLE, TYPSTORAGE_PLAIN, InvalidOid,
+	F_OID8IN, F_OID8OUT},
 	{"tid", TIDOID, 0, 6, false, TYPALIGN_SHORT, TYPSTORAGE_PLAIN, InvalidOid,
 	F_TIDIN, F_TIDOUT},
 	{"xid", XIDOID, 0, 4, true, TYPALIGN_INT, TYPSTORAGE_PLAIN, InvalidOid,
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index ba40ada11caf..a8fd680589f7 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -77,6 +77,7 @@ OBJS = \
 	numeric.o \
 	numutils.o \
 	oid.o \
+	oid8.o \
 	oracle_compat.o \
 	orderedsetaggs.o \
 	partitionfuncs.o \
diff --git a/src/backend/utils/adt/int8.c b/src/backend/utils/adt/int8.c
index bdea490202a6..9f7466e47b79 100644
--- a/src/backend/utils/adt/int8.c
+++ b/src/backend/utils/adt/int8.c
@@ -1323,6 +1323,14 @@ oidtoi8(PG_FUNCTION_ARGS)
 	PG_RETURN_INT64((int64) arg);
 }
 
+Datum
+oidtooid8(PG_FUNCTION_ARGS)
+{
+	Oid			arg = PG_GETARG_OID(0);
+
+	PG_RETURN_OID8((Oid8) arg);
+}
+
 /*
  * non-persistent numeric series generator
  */
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index 9c4c62d41da1..a90e1df035c3 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -73,6 +73,7 @@ backend_sources += files(
   'network_spgist.c',
   'numutils.c',
   'oid.c',
+  'oid8.c',
   'oracle_compat.c',
   'orderedsetaggs.c',
   'partitionfuncs.c',
diff --git a/src/backend/utils/adt/oid8.c b/src/backend/utils/adt/oid8.c
new file mode 100644
index 000000000000..6e9ffd96303f
--- /dev/null
+++ b/src/backend/utils/adt/oid8.c
@@ -0,0 +1,171 @@
+/*-------------------------------------------------------------------------
+ *
+ * oid8.c
+ *	  Functions for the built-in type Oid8
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/oid8.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include <ctype.h>
+#include <limits.h>
+
+#include "catalog/pg_type.h"
+#include "libpq/pqformat.h"
+#include "utils/builtins.h"
+
+#define MAXOID8LEN 20
+
+/*****************************************************************************
+ *	 USER I/O ROUTINES														 *
+ *****************************************************************************/
+
+Datum
+oid8in(PG_FUNCTION_ARGS)
+{
+	char	   *s = PG_GETARG_CSTRING(0);
+	Oid8		result;
+
+	result = uint64in_subr(s, NULL, "oid8", fcinfo->context);
+	PG_RETURN_OID8(result);
+}
+
+Datum
+oid8out(PG_FUNCTION_ARGS)
+{
+	Oid8		val = PG_GETARG_OID8(0);
+	char		buf[MAXOID8LEN + 1];
+	char	   *result;
+	int			len;
+
+	len = pg_ulltoa_n(val, buf) + 1;
+	buf[len - 1] = '\0';
+
+	/*
+	 * Since the length is already known, we do a manual palloc() and memcpy()
+	 * to avoid the strlen() call that would otherwise be done in pstrdup().
+	 */
+	result = palloc(len);
+	memcpy(result, buf, len);
+	PG_RETURN_CSTRING(result);
+}
+
+/*
+ *		oid8recv			- converts external binary format to oid8
+ */
+Datum
+oid8recv(PG_FUNCTION_ARGS)
+{
+	StringInfo	buf = (StringInfo) PG_GETARG_POINTER(0);
+
+	PG_RETURN_OID8(pq_getmsgint64(buf));
+}
+
+/*
+ *		oid8send			- converts oid8 to binary format
+ */
+Datum
+oid8send(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	StringInfoData buf;
+
+	pq_begintypsend(&buf);
+	pq_sendint64(&buf, arg1);
+	PG_RETURN_BYTEA_P(pq_endtypsend(&buf));
+}
+
+/*****************************************************************************
+ *	 PUBLIC ROUTINES														 *
+ *****************************************************************************/
+
+Datum
+oid8eq(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	Oid8		arg2 = PG_GETARG_OID8(1);
+
+	PG_RETURN_BOOL(arg1 == arg2);
+}
+
+Datum
+oid8ne(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	Oid8		arg2 = PG_GETARG_OID8(1);
+
+	PG_RETURN_BOOL(arg1 != arg2);
+}
+
+Datum
+oid8lt(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	Oid8		arg2 = PG_GETARG_OID8(1);
+
+	PG_RETURN_BOOL(arg1 < arg2);
+}
+
+Datum
+oid8le(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	Oid8		arg2 = PG_GETARG_OID8(1);
+
+	PG_RETURN_BOOL(arg1 <= arg2);
+}
+
+Datum
+oid8ge(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	Oid8		arg2 = PG_GETARG_OID8(1);
+
+	PG_RETURN_BOOL(arg1 >= arg2);
+}
+
+Datum
+oid8gt(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	Oid8		arg2 = PG_GETARG_OID8(1);
+
+	PG_RETURN_BOOL(arg1 > arg2);
+}
+
+Datum
+hashoid8(PG_FUNCTION_ARGS)
+{
+	return hashint8(fcinfo);
+}
+
+Datum
+hashoid8extended(PG_FUNCTION_ARGS)
+{
+	return hashint8extended(fcinfo);
+}
+
+Datum
+oid8larger(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	Oid8		arg2 = PG_GETARG_OID8(1);
+
+	PG_RETURN_OID8((arg1 > arg2) ? arg1 : arg2);
+}
+
+Datum
+oid8smaller(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	Oid8		arg2 = PG_GETARG_OID8(1);
+
+	PG_RETURN_OID8((arg1 < arg2) ? arg1 : arg2);
+}
diff --git a/src/fe_utils/print.c b/src/fe_utils/print.c
index 4d97ad2ddeb7..7450e95dc513 100644
--- a/src/fe_utils/print.c
+++ b/src/fe_utils/print.c
@@ -3821,6 +3821,7 @@ column_type_alignment(Oid ftype)
 		case FLOAT8OID:
 		case NUMERICOID:
 		case OIDOID:
+		case OID8OID:
 		case XIDOID:
 		case XID8OID:
 		case CIDOID:
diff --git a/src/test/regress/expected/oid8.out b/src/test/regress/expected/oid8.out
new file mode 100644
index 000000000000..80529214ca53
--- /dev/null
+++ b/src/test/regress/expected/oid8.out
@@ -0,0 +1,196 @@
+--
+-- OID8
+--
+CREATE TABLE OID8_TBL(f1 oid8);
+INSERT INTO OID8_TBL(f1) VALUES ('1234');
+INSERT INTO OID8_TBL(f1) VALUES ('1235');
+INSERT INTO OID8_TBL(f1) VALUES ('987');
+INSERT INTO OID8_TBL(f1) VALUES ('-1040');
+INSERT INTO OID8_TBL(f1) VALUES ('99999999');
+INSERT INTO OID8_TBL(f1) VALUES ('5     ');
+INSERT INTO OID8_TBL(f1) VALUES ('   10  ');
+-- leading/trailing hard tab is also allowed
+INSERT INTO OID8_TBL(f1) VALUES ('	  15 	  ');
+-- bad inputs
+INSERT INTO OID8_TBL(f1) VALUES ('');
+ERROR:  invalid input syntax for type oid8: ""
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('');
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES ('    ');
+ERROR:  invalid input syntax for type oid8: "    "
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('    ');
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES ('asdfasd');
+ERROR:  invalid input syntax for type oid8: "asdfasd"
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('asdfasd');
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES ('99asdfasd');
+ERROR:  invalid input syntax for type oid8: "99asdfasd"
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('99asdfasd');
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES ('5    d');
+ERROR:  invalid input syntax for type oid8: "5    d"
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('5    d');
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES ('    5d');
+ERROR:  invalid input syntax for type oid8: "    5d"
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('    5d');
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES ('5    5');
+ERROR:  invalid input syntax for type oid8: "5    5"
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('5    5');
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES (' - 500');
+ERROR:  invalid input syntax for type oid8: " - 500"
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES (' - 500');
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES ('3908203590239580293850293850329485');
+ERROR:  value "3908203590239580293850293850329485" is out of range for type oid8
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('39082035902395802938502938...
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES ('-1204982019841029840928340329840934');
+ERROR:  value "-1204982019841029840928340329840934" is out of range for type oid8
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('-1204982019841029840928340...
+                                         ^
+SELECT * FROM OID8_TBL;
+          f1          
+----------------------
+                 1234
+                 1235
+                  987
+ 18446744073709550576
+             99999999
+                    5
+                   10
+                   15
+(8 rows)
+
+-- Also try it with non-error-throwing API
+SELECT pg_input_is_valid('1234', 'oid8');
+ pg_input_is_valid 
+-------------------
+ t
+(1 row)
+
+SELECT pg_input_is_valid('01XYZ', 'oid8');
+ pg_input_is_valid 
+-------------------
+ f
+(1 row)
+
+SELECT * FROM pg_input_error_info('01XYZ', 'oid8');
+                   message                   | detail | hint | sql_error_code 
+---------------------------------------------+--------+------+----------------
+ invalid input syntax for type oid8: "01XYZ" |        |      | 22P02
+(1 row)
+
+SELECT pg_input_is_valid('3908203590239580293850293850329485', 'oid8');
+ pg_input_is_valid 
+-------------------
+ f
+(1 row)
+
+SELECT * FROM pg_input_error_info('-1204982019841029840928340329840934', 'oid8');
+                                  message                                  | detail | hint | sql_error_code 
+---------------------------------------------------------------------------+--------+------+----------------
+ value "-1204982019841029840928340329840934" is out of range for type oid8 |        |      | 22003
+(1 row)
+
+-- Operators
+SELECT o.* FROM OID8_TBL o WHERE o.f1 = 1234;
+  f1  
+------
+ 1234
+(1 row)
+
+SELECT o.* FROM OID8_TBL o WHERE o.f1 <> '1234';
+          f1          
+----------------------
+                 1235
+                  987
+ 18446744073709550576
+             99999999
+                    5
+                   10
+                   15
+(7 rows)
+
+SELECT o.* FROM OID8_TBL o WHERE o.f1 <= '1234';
+  f1  
+------
+ 1234
+  987
+    5
+   10
+   15
+(5 rows)
+
+SELECT o.* FROM OID8_TBL o WHERE o.f1 < '1234';
+ f1  
+-----
+ 987
+   5
+  10
+  15
+(4 rows)
+
+SELECT o.* FROM OID8_TBL o WHERE o.f1 >= '1234';
+          f1          
+----------------------
+                 1234
+                 1235
+ 18446744073709550576
+             99999999
+(4 rows)
+
+SELECT o.* FROM OID8_TBL o WHERE o.f1 > '1234';
+          f1          
+----------------------
+                 1235
+ 18446744073709550576
+             99999999
+(3 rows)
+
+-- Casts
+SELECT 1::int2::oid8;
+ oid8 
+------
+    1
+(1 row)
+
+SELECT 1::int4::oid8;
+ oid8 
+------
+    1
+(1 row)
+
+SELECT 1::int8::oid8;
+ oid8 
+------
+    1
+(1 row)
+
+SELECT 1::oid8::int8;
+ int8 
+------
+    1
+(1 row)
+
+SELECT 1::oid::oid8; -- ok
+ oid8 
+------
+    1
+(1 row)
+
+SELECT 1::oid8::oid; -- not ok
+ERROR:  cannot cast type oid8 to oid
+LINE 1: SELECT 1::oid8::oid;
+                      ^
+-- Aggregates
+SELECT min(f1), max(f1) FROM OID8_TBL;
+ min |         max          
+-----+----------------------
+   5 | 18446744073709550576
+(1 row)
+
+DROP TABLE OID8_TBL;
diff --git a/src/test/regress/expected/oid8.sql b/src/test/regress/expected/oid8.sql
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index a357e1d0c0e1..6ff4d7ee9014 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -880,6 +880,13 @@ bytea(integer)
 bytea(bigint)
 bytea_larger(bytea,bytea)
 bytea_smaller(bytea,bytea)
+oid8eq(oid8,oid8)
+oid8ne(oid8,oid8)
+oid8lt(oid8,oid8)
+oid8le(oid8,oid8)
+oid8gt(oid8,oid8)
+oid8ge(oid8,oid8)
+btoid8cmp(oid8,oid8)
 -- Check that functions without argument are not marked as leakproof.
 SELECT p1.oid::regprocedure
 FROM pg_proc p1 JOIN pg_namespace pn
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 943e56506bf1..9ddcacec6bf4 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -702,6 +702,7 @@ CREATE TABLE tab_core_types AS SELECT
   'abc'::refcursor,
   '1 2'::int2vector,
   '1 2'::oidvector,
+  '1234'::oid8,
   format('%I=UC/%I', USER, USER)::aclitem AS aclitem,
   'a fat cat sat on a mat and ate a fat rat'::tsvector,
   'fat & rat'::tsquery,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index cc6d799bceaf..713d220c2efb 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -28,7 +28,7 @@ test: strings md5 numerology point lseg line box path polygon circle date time t
 # geometry depends on point, lseg, line, box, path, polygon, circle
 # horology depends on date, time, timetz, timestamp, timestamptz, interval
 # ----------
-test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import pg_ndistinct pg_dependencies
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import pg_ndistinct pg_dependencies oid8
 
 # ----------
 # Load huge amounts of data
diff --git a/src/test/regress/sql/oid8.sql b/src/test/regress/sql/oid8.sql
new file mode 100644
index 000000000000..c4f2ae6a2e57
--- /dev/null
+++ b/src/test/regress/sql/oid8.sql
@@ -0,0 +1,57 @@
+--
+-- OID8
+--
+
+CREATE TABLE OID8_TBL(f1 oid8);
+
+INSERT INTO OID8_TBL(f1) VALUES ('1234');
+INSERT INTO OID8_TBL(f1) VALUES ('1235');
+INSERT INTO OID8_TBL(f1) VALUES ('987');
+INSERT INTO OID8_TBL(f1) VALUES ('-1040');
+INSERT INTO OID8_TBL(f1) VALUES ('99999999');
+INSERT INTO OID8_TBL(f1) VALUES ('5     ');
+INSERT INTO OID8_TBL(f1) VALUES ('   10  ');
+-- leading/trailing hard tab is also allowed
+INSERT INTO OID8_TBL(f1) VALUES ('	  15 	  ');
+
+-- bad inputs
+INSERT INTO OID8_TBL(f1) VALUES ('');
+INSERT INTO OID8_TBL(f1) VALUES ('    ');
+INSERT INTO OID8_TBL(f1) VALUES ('asdfasd');
+INSERT INTO OID8_TBL(f1) VALUES ('99asdfasd');
+INSERT INTO OID8_TBL(f1) VALUES ('5    d');
+INSERT INTO OID8_TBL(f1) VALUES ('    5d');
+INSERT INTO OID8_TBL(f1) VALUES ('5    5');
+INSERT INTO OID8_TBL(f1) VALUES (' - 500');
+INSERT INTO OID8_TBL(f1) VALUES ('3908203590239580293850293850329485');
+INSERT INTO OID8_TBL(f1) VALUES ('-1204982019841029840928340329840934');
+
+SELECT * FROM OID8_TBL;
+
+-- Also try it with non-error-throwing API
+SELECT pg_input_is_valid('1234', 'oid8');
+SELECT pg_input_is_valid('01XYZ', 'oid8');
+SELECT * FROM pg_input_error_info('01XYZ', 'oid8');
+SELECT pg_input_is_valid('3908203590239580293850293850329485', 'oid8');
+SELECT * FROM pg_input_error_info('-1204982019841029840928340329840934', 'oid8');
+
+-- Operators
+SELECT o.* FROM OID8_TBL o WHERE o.f1 = 1234;
+SELECT o.* FROM OID8_TBL o WHERE o.f1 <> '1234';
+SELECT o.* FROM OID8_TBL o WHERE o.f1 <= '1234';
+SELECT o.* FROM OID8_TBL o WHERE o.f1 < '1234';
+SELECT o.* FROM OID8_TBL o WHERE o.f1 >= '1234';
+SELECT o.* FROM OID8_TBL o WHERE o.f1 > '1234';
+
+-- Casts
+SELECT 1::int2::oid8;
+SELECT 1::int4::oid8;
+SELECT 1::int8::oid8;
+SELECT 1::oid8::int8;
+SELECT 1::oid::oid8; -- ok
+SELECT 1::oid8::oid; -- not ok
+
+-- Aggregates
+SELECT min(f1), max(f1) FROM OID8_TBL;
+
+DROP TABLE OID8_TBL;
diff --git a/src/test/regress/sql/type_sanity.sql b/src/test/regress/sql/type_sanity.sql
index df795759bb4c..c2496823d90e 100644
--- a/src/test/regress/sql/type_sanity.sql
+++ b/src/test/regress/sql/type_sanity.sql
@@ -530,6 +530,7 @@ CREATE TABLE tab_core_types AS SELECT
   'abc'::refcursor,
   '1 2'::int2vector,
   '1 2'::oidvector,
+  '1234'::oid8,
   format('%I=UC/%I', USER, USER)::aclitem AS aclitem,
   'a fat cat sat on a mat and ate a fat rat'::tsvector,
   'fat & rat'::tsquery,
diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml
index 1f2829e56a95..827ba0c23b85 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -4723,6 +4723,10 @@ INSERT INTO mytable VALUES(-1);  -- fails
     <primary>oid</primary>
    </indexterm>
 
+   <indexterm zone="datatype-oid">
+    <primary>oid8</primary>
+   </indexterm>
+
    <indexterm zone="datatype-oid">
     <primary>regclass</primary>
    </indexterm>
@@ -4805,6 +4809,13 @@ INSERT INTO mytable VALUES(-1);  -- fails
     individual tables.
    </para>
 
+   <para>
+    In some contexts, a 64-bit variant <type>oid8</type> is used.
+    It is implemented as an unsigned eight-byte integer. Unlike its
+    <type>oid</type> counterpart, it can ensure uniqueness in large
+    individual tables.
+   </para>
+
    <para>
     The <type>oid</type> type itself has few operations beyond comparison.
     It can be cast to integer, however, and then manipulated using the
diff --git a/doc/src/sgml/func/func-aggregate.sgml b/doc/src/sgml/func/func-aggregate.sgml
index f50b692516b6..a5396048adf3 100644
--- a/doc/src/sgml/func/func-aggregate.sgml
+++ b/doc/src/sgml/func/func-aggregate.sgml
@@ -508,8 +508,8 @@
         Computes the maximum of the non-null input
         values.  Available for any numeric, string, date/time, or enum type,
         as well as <type>bytea</type>, <type>inet</type>, <type>interval</type>,
-        <type>money</type>, <type>oid</type>, <type>pg_lsn</type>,
-        <type>tid</type>, <type>xid8</type>,
+        <type>money</type>, <type>oid</type>, <type>oid8</type>,
+        <type>pg_lsn</type>, <type>tid</type>, <type>xid8</type>,
         and also arrays and composite types containing sortable data types.
        </para></entry>
        <entry>Yes</entry>
@@ -527,8 +527,8 @@
         Computes the minimum of the non-null input
         values.  Available for any numeric, string, date/time, or enum type,
         as well as <type>bytea</type>, <type>inet</type>, <type>interval</type>,
-        <type>money</type>, <type>oid</type>, <type>pg_lsn</type>,
-        <type>tid</type>, <type>xid8</type>,
+        <type>money</type>, <type>oid</type>, <type>oid8</type>,
+        <type>pg_lsn</type>, <type>tid</type>, <type>xid8</type>,
         and also arrays and composite types containing sortable data types.
        </para></entry>
        <entry>Yes</entry>
-- 
2.51.0

v8-0002-Refactor-some-TOAST-value-ID-code-to-use-Oid8-ins.patchtext/x-diff; charset=us-asciiDownload
From 039336657fd16ff09d994f89d4587ad464789b88 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 13 Aug 2025 17:26:36 +0900
Subject: [PATCH v8 02/15] Refactor some TOAST value ID code to use Oid8
 instead of Oid

This change is a mechanical switch to change most of the code paths that
assume TOAST value IDs to be Oids to become Oid8, easing an upcoming
change to allow larger TOAST values, at 8 bytes.

The areas touched are related to table AM, amcheck and logical
decoding's reorder buffer.  A good chunk of the changes involve
switching printf() markers from %u to OID8_FORMAT.
---
 src/include/access/heaptoast.h                |  2 +-
 src/include/access/tableam.h                  |  4 +-
 src/backend/access/common/toast_internals.c   |  8 +--
 src/backend/access/heap/heaptoast.c           | 12 ++--
 .../replication/logical/reorderbuffer.c       | 14 +++--
 contrib/amcheck/verify_heapam.c               | 56 +++++++++++--------
 6 files changed, 53 insertions(+), 43 deletions(-)

diff --git a/src/include/access/heaptoast.h b/src/include/access/heaptoast.h
index 1c68f8107d6f..de6b3e2212a5 100644
--- a/src/include/access/heaptoast.h
+++ b/src/include/access/heaptoast.h
@@ -142,7 +142,7 @@ extern HeapTuple toast_build_flattened_tuple(TupleDesc tupleDesc,
  *	Fetch a slice from a toast value stored in a heap table.
  * ----------
  */
-extern void heap_fetch_toast_slice(Relation toastrel, Oid valueid,
+extern void heap_fetch_toast_slice(Relation toastrel, Oid8 valueid,
 								   int32 attrsize, int32 sliceoffset,
 								   int32 slicelength, struct varlena *result);
 
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index e16bf0256928..068861a6f1c1 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -746,7 +746,7 @@ typedef struct TableAmRoutine
 	 * table implemented by this AM.  See table_relation_fetch_toast_slice()
 	 * for more details.
 	 */
-	void		(*relation_fetch_toast_slice) (Relation toastrel, Oid valueid,
+	void		(*relation_fetch_toast_slice) (Relation toastrel, Oid8 valueid,
 											   int32 attrsize,
 											   int32 sliceoffset,
 											   int32 slicelength,
@@ -1882,7 +1882,7 @@ table_relation_toast_am(Relation rel)
  * stored.
  */
 static inline void
-table_relation_fetch_toast_slice(Relation toastrel, Oid valueid,
+table_relation_fetch_toast_slice(Relation toastrel, Oid8 valueid,
 								 int32 attrsize, int32 sliceoffset,
 								 int32 slicelength, struct varlena *result)
 {
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 63b848473f8a..4b279a3fd734 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -26,8 +26,8 @@
 #include "utils/rel.h"
 #include "utils/snapmgr.h"
 
-static bool toastrel_valueid_exists(Relation toastrel, Oid valueid);
-static bool toastid_valueid_exists(Oid toastrelid, Oid valueid);
+static bool toastrel_valueid_exists(Relation toastrel, Oid8 valueid);
+static bool toastid_valueid_exists(Oid toastrelid, Oid8 valueid);
 
 /* ----------
  * toast_compress_datum -
@@ -447,7 +447,7 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
  * ----------
  */
 static bool
-toastrel_valueid_exists(Relation toastrel, Oid valueid)
+toastrel_valueid_exists(Relation toastrel, Oid8 valueid)
 {
 	bool		result = false;
 	ScanKeyData toastkey;
@@ -495,7 +495,7 @@ toastrel_valueid_exists(Relation toastrel, Oid valueid)
  * ----------
  */
 static bool
-toastid_valueid_exists(Oid toastrelid, Oid valueid)
+toastid_valueid_exists(Oid toastrelid, Oid8 valueid)
 {
 	bool		result;
 	Relation	toastrel;
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index e148c9be4825..3818d7c5d895 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -623,7 +623,7 @@ toast_build_flattened_tuple(TupleDesc tupleDesc,
  * result is the varlena into which the results should be written.
  */
 void
-heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
+heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 					   int32 sliceoffset, int32 slicelength,
 					   struct varlena *result)
 {
@@ -725,7 +725,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
 		else
 		{
 			/* should never happen */
-			elog(ERROR, "found toasted toast chunk for toast value %u in %s",
+			elog(ERROR, "found toasted toast chunk for toast value " OID8_FORMAT " in %s",
 				 valueid, RelationGetRelationName(toastrel));
 			chunksize = 0;		/* keep compiler quiet */
 			chunkdata = NULL;
@@ -737,13 +737,13 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
 		if (curchunk != expectedchunk)
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
-					 errmsg_internal("unexpected chunk number %d (expected %d) for toast value %u in %s",
+					 errmsg_internal("unexpected chunk number %d (expected %d) for toast value " OID8_FORMAT " in %s",
 									 curchunk, expectedchunk, valueid,
 									 RelationGetRelationName(toastrel))));
 		if (curchunk > endchunk)
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
-					 errmsg_internal("unexpected chunk number %d (out of range %d..%d) for toast value %u in %s",
+					 errmsg_internal("unexpected chunk number %d (out of range %d..%d) for toast value " OID8_FORMAT " in %s",
 									 curchunk,
 									 startchunk, endchunk, valueid,
 									 RelationGetRelationName(toastrel))));
@@ -752,7 +752,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
 		if (chunksize != expected_size)
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
-					 errmsg_internal("unexpected chunk size %d (expected %d) in chunk %d of %d for toast value %u in %s",
+					 errmsg_internal("unexpected chunk size %d (expected %d) in chunk %d of %d for toast value " OID8_FORMAT " in %s",
 									 chunksize, expected_size,
 									 curchunk, totalchunks, valueid,
 									 RelationGetRelationName(toastrel))));
@@ -781,7 +781,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
 	if (expectedchunk != (endchunk + 1))
 		ereport(ERROR,
 				(errcode(ERRCODE_DATA_CORRUPTED),
-				 errmsg_internal("missing chunk number %d for toast value %u in %s",
+				 errmsg_internal("missing chunk number %d for toast value " OID8_FORMAT " in %s",
 								 expectedchunk, valueid,
 								 RelationGetRelationName(toastrel))));
 
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index eb6a84554b78..8af8ecc61d68 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -176,7 +176,7 @@ typedef struct ReorderBufferIterTXNState
 /* toast datastructures */
 typedef struct ReorderBufferToastEnt
 {
-	Oid			chunk_id;		/* toast_table.chunk_id */
+	Oid8		chunk_id;		/* toast_table.chunk_id */
 	int32		last_chunk_seq; /* toast_table.chunk_seq of the last chunk we
 								 * have seen */
 	Size		num_chunks;		/* number of chunks we've already seen */
@@ -4981,7 +4981,7 @@ ReorderBufferToastInitHash(ReorderBuffer *rb, ReorderBufferTXN *txn)
 
 	Assert(txn->toast_hash == NULL);
 
-	hash_ctl.keysize = sizeof(Oid);
+	hash_ctl.keysize = sizeof(Oid8);
 	hash_ctl.entrysize = sizeof(ReorderBufferToastEnt);
 	hash_ctl.hcxt = rb->context;
 	txn->toast_hash = hash_create("ReorderBufferToastHash", 5, &hash_ctl,
@@ -5005,7 +5005,7 @@ ReorderBufferToastAppendChunk(ReorderBuffer *rb, ReorderBufferTXN *txn,
 	bool		isnull;
 	Pointer		chunk;
 	TupleDesc	desc = RelationGetDescr(relation);
-	Oid			chunk_id;
+	Oid8		chunk_id;
 	int32		chunk_seq;
 
 	if (txn->toast_hash == NULL)
@@ -5032,11 +5032,11 @@ ReorderBufferToastAppendChunk(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		dlist_init(&ent->chunks);
 
 		if (chunk_seq != 0)
-			elog(ERROR, "got sequence entry %d for toast chunk %u instead of seq 0",
+			elog(ERROR, "got sequence entry %d for toast chunk " OID8_FORMAT " instead of seq 0",
 				 chunk_seq, chunk_id);
 	}
 	else if (found && chunk_seq != ent->last_chunk_seq + 1)
-		elog(ERROR, "got sequence entry %d for toast chunk %u instead of seq %d",
+		elog(ERROR, "got sequence entry %d for toast chunk " OID8_FORMAT " instead of seq %d",
 			 chunk_seq, chunk_id, ent->last_chunk_seq + 1);
 
 	chunk = DatumGetPointer(fastgetattr(newtup, 3, desc, &isnull));
@@ -5145,6 +5145,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		struct varlena *reconstructed;
 		dlist_iter	it;
 		Size		data_done = 0;
+		Oid8		toast_valueid;
 
 		if (attr->attisdropped)
 			continue;
@@ -5165,13 +5166,14 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 			continue;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, varlena);
+		toast_valueid = toast_pointer.va_valueid;
 
 		/*
 		 * Check whether the toast tuple changed, replace if so.
 		 */
 		ent = (ReorderBufferToastEnt *)
 			hash_search(txn->toast_hash,
-						&toast_pointer.va_valueid,
+						&toast_valueid,
 						HASH_FIND,
 						NULL);
 		if (ent == NULL)
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 4963e9245cb5..eb353c40249e 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1561,6 +1561,9 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	bool		isnull;
 	int32		chunksize;
 	int32		expected_size;
+	Oid8		toast_valueid;
+
+	toast_valueid = ta->toast_pointer.va_valueid;
 
 	/* Sanity-check the sequence number. */
 	chunk_seq = DatumGetInt32(fastgetattr(toasttup, 2,
@@ -1568,16 +1571,16 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	if (isnull)
 	{
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u has toast chunk with null sequence number",
-										 ta->toast_pointer.va_valueid));
+								psprintf("toast value " OID8_FORMAT " has toast chunk with null sequence number",
+										 toast_valueid));
 		return;
 	}
 	if (chunk_seq != *expected_chunk_seq)
 	{
 		/* Either the TOAST index is corrupt, or we don't have all chunks. */
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u index scan returned chunk %d when expecting chunk %d",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value " OID8_FORMAT " index scan returned chunk %d when expecting chunk %d",
+										 toast_valueid,
 										 chunk_seq, *expected_chunk_seq));
 	}
 	*expected_chunk_seq = chunk_seq + 1;
@@ -1588,8 +1591,8 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	if (isnull)
 	{
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u chunk %d has null data",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value " OID8_FORMAT " chunk %d has null data",
+										 toast_valueid,
 										 chunk_seq));
 		return;
 	}
@@ -1608,8 +1611,8 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 		uint32		header = ((varattrib_4b *) chunk)->va_4byte.va_header;
 
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u chunk %d has invalid varlena header %0x",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value " OID8_FORMAT " chunk %d has invalid varlena header %0x",
+										 toast_valueid,
 										 chunk_seq, header));
 		return;
 	}
@@ -1620,8 +1623,8 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	if (chunk_seq > last_chunk_seq)
 	{
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u chunk %d follows last expected chunk %d",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value " OID8_FORMAT " chunk %d follows last expected chunk %d",
+										 toast_valueid,
 										 chunk_seq, last_chunk_seq));
 		return;
 	}
@@ -1631,8 +1634,8 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 
 	if (chunksize != expected_size)
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u chunk %d has size %u, but expected size %u",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value " OID8_FORMAT " chunk %d has size %u, but expected size %u",
+										 toast_valueid,
 										 chunk_seq, chunksize, expected_size));
 }
 
@@ -1663,6 +1666,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	struct varlena *attr;
 	char	   *tp;				/* pointer to the tuple data */
 	uint16		infomask;
+	Oid8		toast_pointer_valueid;
 	CompactAttribute *thisatt;
 	struct varatt_external toast_pointer;
 
@@ -1771,12 +1775,13 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	 * Must copy attr into toast_pointer for alignment considerations
 	 */
 	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_pointer_valueid = toast_pointer.va_valueid;
 
 	/* Toasted attributes too large to be untoasted should never be stored */
 	if (toast_pointer.va_rawsize > VARLENA_SIZE_LIMIT)
 		report_corruption(ctx,
-						  psprintf("toast value %u rawsize %d exceeds limit %d",
-								   toast_pointer.va_valueid,
+						  psprintf("toast value " OID8_FORMAT " rawsize %d exceeds limit %d",
+								   toast_pointer_valueid,
 								   toast_pointer.va_rawsize,
 								   VARLENA_SIZE_LIMIT));
 
@@ -1803,16 +1808,16 @@ check_tuple_attribute(HeapCheckContext *ctx)
 		}
 		if (!valid)
 			report_corruption(ctx,
-							  psprintf("toast value %u has invalid compression method id %d",
-									   toast_pointer.va_valueid, cmid));
+							  psprintf("toast value " OID8_FORMAT " has invalid compression method id %d",
+									   toast_pointer_valueid, cmid));
 	}
 
 	/* The tuple header better claim to contain toasted values */
 	if (!(infomask & HEAP_HASEXTERNAL))
 	{
 		report_corruption(ctx,
-						  psprintf("toast value %u is external but tuple header flag HEAP_HASEXTERNAL not set",
-								   toast_pointer.va_valueid));
+						  psprintf("toast value " OID8_FORMAT " is external but tuple header flag HEAP_HASEXTERNAL not set",
+								   toast_pointer_valueid));
 		return true;
 	}
 
@@ -1820,8 +1825,8 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	if (!ctx->rel->rd_rel->reltoastrelid)
 	{
 		report_corruption(ctx,
-						  psprintf("toast value %u is external but relation has no toast relation",
-								   toast_pointer.va_valueid));
+						  psprintf("toast value " OID8_FORMAT " is external but relation has no toast relation",
+								   toast_pointer_valueid));
 		return true;
 	}
 
@@ -1866,6 +1871,7 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	uint32		extsize;
 	int32		expected_chunk_seq = 0;
 	int32		last_chunk_seq;
+	Oid8		toast_valueid;
 
 	extsize = VARATT_EXTERNAL_GET_EXTSIZE(ta->toast_pointer);
 	last_chunk_seq = (extsize - 1) / TOAST_MAX_CHUNK_SIZE;
@@ -1896,14 +1902,16 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	}
 	systable_endscan_ordered(toastscan);
 
+	toast_valueid = ta->toast_pointer.va_valueid;
+
 	if (!found_toasttup)
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u not found in toast table",
-										 ta->toast_pointer.va_valueid));
+								psprintf("toast value " OID8_FORMAT " not found in toast table",
+										 toast_valueid));
 	else if (expected_chunk_seq <= last_chunk_seq)
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u was expected to end at chunk %d, but ended while expecting chunk %d",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value " OID8_FORMAT " was expected to end at chunk %d, but ended while expecting chunk %d",
+										 toast_valueid,
 										 last_chunk_seq, expected_chunk_seq));
 }
 
-- 
2.51.0

v8-0003-Minimize-footprint-of-TOAST_MAX_CHUNK_SIZE-in-hea.patchtext/x-diff; charset=us-asciiDownload
From 325a91f15ef0f41ce6ae9ca1d8e452cf2dadb183 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 13 Aug 2025 17:40:13 +0900
Subject: [PATCH v8 03/15] Minimize footprint of TOAST_MAX_CHUNK_SIZE in heap
 and amcheck

This eases a follow-up change to support 8-byte TOAST value IDs, as the
maximum chunk size allowed for a single chunk of TOASTed data depends on
the size of the value ID.
---
 src/backend/access/heap/heaptoast.c | 20 ++++++++++++--------
 contrib/amcheck/verify_heapam.c     | 13 +++++++++----
 2 files changed, 21 insertions(+), 12 deletions(-)

diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index 3818d7c5d895..d186d620b0f8 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -634,11 +634,12 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 	SysScanDesc toastscan;
 	HeapTuple	ttup;
 	int32		expectedchunk;
-	int32		totalchunks = ((attrsize - 1) / TOAST_MAX_CHUNK_SIZE) + 1;
+	int32		totalchunks;
 	int			startchunk;
 	int			endchunk;
 	int			num_indexes;
 	int			validIndex;
+	int32		max_chunk_size;
 
 	/* Look for the valid index of toast relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -646,8 +647,11 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 									&toastidxs,
 									&num_indexes);
 
-	startchunk = sliceoffset / TOAST_MAX_CHUNK_SIZE;
-	endchunk = (sliceoffset + slicelength - 1) / TOAST_MAX_CHUNK_SIZE;
+	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+
+	totalchunks = ((attrsize - 1) / max_chunk_size) + 1;
+	startchunk = sliceoffset / max_chunk_size;
+	endchunk = (sliceoffset + slicelength - 1) / max_chunk_size;
 	Assert(endchunk <= totalchunks);
 
 	/* Set up a scan key to fetch from the index. */
@@ -747,8 +751,8 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 									 curchunk,
 									 startchunk, endchunk, valueid,
 									 RelationGetRelationName(toastrel))));
-		expected_size = curchunk < totalchunks - 1 ? TOAST_MAX_CHUNK_SIZE
-			: attrsize - ((totalchunks - 1) * TOAST_MAX_CHUNK_SIZE);
+		expected_size = curchunk < totalchunks - 1 ? max_chunk_size
+			: attrsize - ((totalchunks - 1) * max_chunk_size);
 		if (chunksize != expected_size)
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
@@ -763,12 +767,12 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 		chcpystrt = 0;
 		chcpyend = chunksize - 1;
 		if (curchunk == startchunk)
-			chcpystrt = sliceoffset % TOAST_MAX_CHUNK_SIZE;
+			chcpystrt = sliceoffset % max_chunk_size;
 		if (curchunk == endchunk)
-			chcpyend = (sliceoffset + slicelength - 1) % TOAST_MAX_CHUNK_SIZE;
+			chcpyend = (sliceoffset + slicelength - 1) % max_chunk_size;
 
 		memcpy(VARDATA(result) +
-			   (curchunk * TOAST_MAX_CHUNK_SIZE - sliceoffset) + chcpystrt,
+			   (curchunk * max_chunk_size - sliceoffset) + chcpystrt,
 			   chunkdata + chcpystrt,
 			   (chcpyend - chcpystrt) + 1);
 
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index eb353c40249e..164ced37583a 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1556,15 +1556,19 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 				  uint32 extsize)
 {
 	int32		chunk_seq;
-	int32		last_chunk_seq = (extsize - 1) / TOAST_MAX_CHUNK_SIZE;
+	int32		last_chunk_seq;
 	Pointer		chunk;
 	bool		isnull;
 	int32		chunksize;
 	int32		expected_size;
 	Oid8		toast_valueid;
+	int32		max_chunk_size;
 
 	toast_valueid = ta->toast_pointer.va_valueid;
 
+	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	last_chunk_seq = (extsize - 1) / max_chunk_size;
+
 	/* Sanity-check the sequence number. */
 	chunk_seq = DatumGetInt32(fastgetattr(toasttup, 2,
 										  ctx->toast_rel->rd_att, &isnull));
@@ -1629,8 +1633,8 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 		return;
 	}
 
-	expected_size = chunk_seq < last_chunk_seq ? TOAST_MAX_CHUNK_SIZE
-		: extsize - (last_chunk_seq * TOAST_MAX_CHUNK_SIZE);
+	expected_size = chunk_seq < last_chunk_seq ? max_chunk_size
+		: extsize - (last_chunk_seq * max_chunk_size);
 
 	if (chunksize != expected_size)
 		report_toast_corruption(ctx, ta,
@@ -1872,9 +1876,10 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	int32		expected_chunk_seq = 0;
 	int32		last_chunk_seq;
 	Oid8		toast_valueid;
+	int32		max_chunk_size = TOAST_MAX_CHUNK_SIZE;
 
 	extsize = VARATT_EXTERNAL_GET_EXTSIZE(ta->toast_pointer);
-	last_chunk_seq = (extsize - 1) / TOAST_MAX_CHUNK_SIZE;
+	last_chunk_seq = (extsize - 1) / max_chunk_size;
 
 	/*
 	 * Setup a scan key to find chunks in toast table with matching va_valueid
-- 
2.51.0

v8-0004-Renames-around-varatt_external-varatt_external_oi.patchtext/x-diff; charset=us-asciiDownload
From 614d8c52a0d6b328b1a720bbf79574b7a6b31575 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 13 Aug 2025 18:28:10 +0900
Subject: [PATCH v8 04/15] Renames around varatt_external->varatt_external_oid

This impacts a few things:
- VARTAG_ONDISK -> VARTAG_ONDISK_OID
- TOAST_POINTER_SIZE -> TOAST_OID_POINTER_SIZE
- TOAST_MAX_CHUNK_SIZE -> TOAST_OID_MAX_CHUNK_SIZE

The "struct" around varatt_external is cleaned up in most places, while
on it.

This rename is in preparation of a follow-up commit that aims at adding
support for multiple types of external on-disk TOAST pointers, where the
OID type is only one subset of them.
---
 src/include/access/detoast.h                  |  4 +--
 src/include/access/heaptoast.h                |  6 ++--
 src/include/varatt.h                          | 34 +++++++++++--------
 src/backend/access/common/detoast.c           | 10 +++---
 src/backend/access/common/toast_compression.c |  2 +-
 src/backend/access/common/toast_internals.c   | 14 ++++----
 src/backend/access/heap/heaptoast.c           |  2 +-
 src/backend/access/table/toast_helper.c       |  4 +--
 src/backend/access/transam/xlog.c             |  8 ++---
 .../replication/logical/reorderbuffer.c       |  2 +-
 src/backend/utils/adt/varlena.c               |  2 +-
 src/bin/pg_resetwal/pg_resetwal.c             |  2 +-
 doc/src/sgml/func/func-info.sgml              |  2 +-
 doc/src/sgml/storage.sgml                     |  2 +-
 contrib/amcheck/verify_heapam.c               | 10 +++---
 15 files changed, 54 insertions(+), 50 deletions(-)

diff --git a/src/include/access/detoast.h b/src/include/access/detoast.h
index e603a2276c38..6435597b1127 100644
--- a/src/include/access/detoast.h
+++ b/src/include/access/detoast.h
@@ -14,7 +14,7 @@
 
 /*
  * Macro to fetch the possibly-unaligned contents of an EXTERNAL datum
- * into a local "struct varatt_external" toast pointer.  This should be
+ * into a local "varatt_external_oid" toast pointer.  This should be
  * just a memcpy, but some versions of gcc seem to produce broken code
  * that assumes the datum contents are aligned.  Introducing an explicit
  * intermediate "varattrib_1b_e *" variable seems to fix it.
@@ -28,7 +28,7 @@ do { \
 } while (0)
 
 /* Size of an EXTERNAL datum that contains a standard TOAST pointer */
-#define TOAST_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external))
+#define TOAST_OID_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_oid))
 
 /* Size of an EXTERNAL datum that contains an indirection pointer */
 #define INDIRECT_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_indirect))
diff --git a/src/include/access/heaptoast.h b/src/include/access/heaptoast.h
index de6b3e2212a5..55a6a17b2c0b 100644
--- a/src/include/access/heaptoast.h
+++ b/src/include/access/heaptoast.h
@@ -69,19 +69,19 @@
 
 /*
  * When we store an oversize datum externally, we divide it into chunks
- * containing at most TOAST_MAX_CHUNK_SIZE data bytes.  This number *must*
+ * containing at most TOAST_OID_MAX_CHUNK_SIZE data bytes.  This number *must*
  * be small enough that the completed toast-table tuple (including the
  * ID and sequence fields and all overhead) will fit on a page.
  * The coding here sets the size on the theory that we want to fit
  * EXTERN_TUPLES_PER_PAGE tuples of maximum size onto a page.
  *
- * NB: Changing TOAST_MAX_CHUNK_SIZE requires an initdb.
+ * NB: Changing TOAST_OID_MAX_CHUNK_SIZE requires an initdb.
  */
 #define EXTERN_TUPLES_PER_PAGE	4	/* tweak only this */
 
 #define EXTERN_TUPLE_MAX_SIZE	MaximumBytesPerTuple(EXTERN_TUPLES_PER_PAGE)
 
-#define TOAST_MAX_CHUNK_SIZE	\
+#define TOAST_OID_MAX_CHUNK_SIZE	\
 	(EXTERN_TUPLE_MAX_SIZE -							\
 	 MAXALIGN(SizeofHeapTupleHeader) -					\
 	 sizeof(Oid) -										\
diff --git a/src/include/varatt.h b/src/include/varatt.h
index aeeabf9145b5..c873a59bb1c9 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -16,7 +16,7 @@
 #define VARATT_H
 
 /*
- * struct varatt_external is a traditional "TOAST pointer", that is, the
+ * varatt_external_oid is a traditional "TOAST pointer", that is, the
  * information needed to fetch a Datum stored out-of-line in a TOAST table.
  * The data is compressed if and only if the external size stored in
  * va_extinfo is less than va_rawsize - VARHDRSZ.
@@ -29,14 +29,14 @@
  * you can look at these fields!  (The reason we use memcmp is to avoid
  * having to do that just to detect equality of two TOAST pointers...)
  */
-typedef struct varatt_external
+typedef struct varatt_external_oid
 {
 	int32		va_rawsize;		/* Original data size (includes header) */
 	uint32		va_extinfo;		/* External saved size (without header) and
 								 * compression method */
 	Oid			va_valueid;		/* Unique ID of value within TOAST table */
 	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
-}			varatt_external;
+}			varatt_external_oid;
 
 /*
  * These macros define the "saved size" portion of va_extinfo.  Its remaining
@@ -51,7 +51,7 @@ typedef struct varatt_external
  * The creator of such a Datum is entirely responsible that the referenced
  * storage survives for as long as referencing pointer Datums can exist.
  *
- * Note that just as for struct varatt_external, this struct is stored
+ * Note that just as for varatt_external_oid, this struct is stored
  * unaligned within any containing tuple.
  */
 typedef struct varatt_indirect
@@ -66,7 +66,7 @@ typedef struct varatt_indirect
  * storage.  APIs for this, in particular the definition of struct
  * ExpandedObjectHeader, are in src/include/utils/expandeddatum.h.
  *
- * Note that just as for struct varatt_external, this struct is stored
+ * Note that just as for varatt_external_oid, this struct is stored
  * unaligned within any containing tuple.
  */
 typedef struct ExpandedObjectHeader ExpandedObjectHeader;
@@ -78,15 +78,16 @@ typedef struct varatt_expanded
 
 /*
  * Type tag for the various sorts of "TOAST pointer" datums.  The peculiar
- * value for VARTAG_ONDISK comes from a requirement for on-disk compatibility
- * with a previous notion that the tag field was the pointer datum's length.
+ * value for VARTAG_ONDISK_OID comes from a requirement for on-disk
+ * compatibility with a previous notion that the tag field was the pointer
+ * datum's length.
  */
 typedef enum vartag_external
 {
 	VARTAG_INDIRECT = 1,
 	VARTAG_EXPANDED_RO = 2,
 	VARTAG_EXPANDED_RW = 3,
-	VARTAG_ONDISK = 18
+	VARTAG_ONDISK_OID = 18
 } vartag_external;
 
 /* Is a TOAST pointer either type of expanded-object pointer? */
@@ -105,8 +106,8 @@ VARTAG_SIZE(vartag_external tag)
 		return sizeof(varatt_indirect);
 	else if (VARTAG_IS_EXPANDED(tag))
 		return sizeof(varatt_expanded);
-	else if (tag == VARTAG_ONDISK)
-		return sizeof(varatt_external);
+	else if (tag == VARTAG_ONDISK_OID)
+		return sizeof(varatt_external_oid);
 	else
 	{
 		Assert(false);
@@ -360,7 +361,7 @@ VARATT_IS_EXTERNAL(const void *PTR)
 static inline bool
 VARATT_IS_EXTERNAL_ONDISK(const void *PTR)
 {
-	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK;
+	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID;
 }
 
 /* Is varlena datum an indirect pointer? */
@@ -502,15 +503,18 @@ VARDATA_COMPRESSED_GET_COMPRESS_METHOD(const void *PTR)
 	return ((varattrib_4b *) PTR)->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS;
 }
 
-/* Same for external Datums; but note argument is a struct varatt_external */
+/*
+ * Same for external Datums; but note argument is a struct
+ * varatt_external_oid.
+ */
 static inline Size
-VARATT_EXTERNAL_GET_EXTSIZE(struct varatt_external toast_pointer)
+VARATT_EXTERNAL_GET_EXTSIZE(varatt_external_oid toast_pointer)
 {
 	return toast_pointer.va_extinfo & VARLENA_EXTSIZE_MASK;
 }
 
 static inline uint32
-VARATT_EXTERNAL_GET_COMPRESS_METHOD(struct varatt_external toast_pointer)
+VARATT_EXTERNAL_GET_COMPRESS_METHOD(varatt_external_oid toast_pointer)
 {
 	return toast_pointer.va_extinfo >> VARLENA_EXTSIZE_BITS;
 }
@@ -533,7 +537,7 @@ VARATT_EXTERNAL_GET_COMPRESS_METHOD(struct varatt_external toast_pointer)
  * actually saves space, so we expect either equality or less-than.
  */
 static inline bool
-VARATT_EXTERNAL_IS_COMPRESSED(struct varatt_external toast_pointer)
+VARATT_EXTERNAL_IS_COMPRESSED(varatt_external_oid toast_pointer)
 {
 	return VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) <
 		(Size) (toast_pointer.va_rawsize - VARHDRSZ);
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 626517877422..c187c32d96dd 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -225,7 +225,7 @@ detoast_attr_slice(struct varlena *attr,
 
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
-		struct varatt_external toast_pointer;
+		varatt_external_oid toast_pointer;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 
@@ -344,7 +344,7 @@ toast_fetch_datum(struct varlena *attr)
 {
 	Relation	toastrel;
 	struct varlena *result;
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 	int32		attrsize;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
@@ -398,7 +398,7 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 {
 	Relation	toastrel;
 	struct varlena *result;
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 	int32		attrsize;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
@@ -550,7 +550,7 @@ toast_raw_datum_size(Datum value)
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
 		/* va_rawsize is the size of the original datum -- including header */
-		struct varatt_external toast_pointer;
+		varatt_external_oid toast_pointer;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 		result = toast_pointer.va_rawsize;
@@ -610,7 +610,7 @@ toast_datum_size(Datum value)
 		 * compressed or not.  We do not count the size of the toast pointer
 		 * ... should we?
 		 */
-		struct varatt_external toast_pointer;
+		varatt_external_oid toast_pointer;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 		result = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c
index 926f1e4008ab..08f572f31eed 100644
--- a/src/backend/access/common/toast_compression.c
+++ b/src/backend/access/common/toast_compression.c
@@ -262,7 +262,7 @@ toast_get_compression_id(struct varlena *attr)
 	 */
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
-		struct varatt_external toast_pointer;
+		varatt_external_oid toast_pointer;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 4b279a3fd734..72eae4f7fbe6 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -124,7 +124,7 @@ toast_save_datum(Relation rel, Datum value,
 	TupleDesc	toasttupDesc;
 	CommandId	mycid = GetCurrentCommandId(true);
 	struct varlena *result;
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 	int32		chunk_seq = 0;
 	char	   *data_p;
 	int32		data_todo;
@@ -225,7 +225,7 @@ toast_save_datum(Relation rel, Datum value,
 		toast_pointer.va_valueid = InvalidOid;
 		if (oldexternal != NULL)
 		{
-			struct varatt_external old_toast_pointer;
+			varatt_external_oid old_toast_pointer;
 
 			Assert(VARATT_IS_EXTERNAL_ONDISK(oldexternal));
 			/* Must copy to access aligned fields */
@@ -289,7 +289,7 @@ toast_save_datum(Relation rel, Datum value,
 		{
 			alignas(int32) struct varlena hdr;
 			/* this is to make the union big enough for a chunk: */
-			char		data[TOAST_MAX_CHUNK_SIZE + VARHDRSZ];
+			char		data[TOAST_OID_MAX_CHUNK_SIZE + VARHDRSZ];
 		}			chunk_data;
 		int32		chunk_size;
 
@@ -298,7 +298,7 @@ toast_save_datum(Relation rel, Datum value,
 		/*
 		 * Calculate the size of this chunk
 		 */
-		chunk_size = Min(TOAST_MAX_CHUNK_SIZE, data_todo);
+		chunk_size = Min(TOAST_OID_MAX_CHUNK_SIZE, data_todo);
 
 		/*
 		 * Build a tuple and store it
@@ -359,8 +359,8 @@ toast_save_datum(Relation rel, Datum value,
 	/*
 	 * Create the TOAST pointer value that we'll return
 	 */
-	result = (struct varlena *) palloc(TOAST_POINTER_SIZE);
-	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK);
+	result = (struct varlena *) palloc(TOAST_OID_POINTER_SIZE);
+	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_OID);
 	memcpy(VARDATA_EXTERNAL(result), &toast_pointer, sizeof(toast_pointer));
 
 	return PointerGetDatum(result);
@@ -376,7 +376,7 @@ void
 toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 {
 	struct varlena *attr = (struct varlena *) DatumGetPointer(value);
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 	Relation	toastrel;
 	Relation   *toastidxs;
 	ScanKeyData toastkey;
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index d186d620b0f8..7f408ccd0bb6 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -647,7 +647,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 									&toastidxs,
 									&num_indexes);
 
-	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
 
 	totalchunks = ((attrsize - 1) / max_chunk_size) + 1;
 	startchunk = sliceoffset / max_chunk_size;
diff --git a/src/backend/access/table/toast_helper.c b/src/backend/access/table/toast_helper.c
index 11f97d65367d..0c58c6c32565 100644
--- a/src/backend/access/table/toast_helper.c
+++ b/src/backend/access/table/toast_helper.c
@@ -171,7 +171,7 @@ toast_tuple_init(ToastTupleContext *ttc)
  * The column must have attstorage EXTERNAL or EXTENDED if check_main is
  * false, and must have attstorage MAIN if check_main is true.
  *
- * The column must have a minimum size of MAXALIGN(TOAST_POINTER_SIZE);
+ * The column must have a minimum size of MAXALIGN(TOAST_OID_POINTER_SIZE);
  * if not, no benefit is to be expected by compressing it.
  *
  * The return value is the index of the biggest suitable column, or
@@ -184,7 +184,7 @@ toast_tuple_find_biggest_attribute(ToastTupleContext *ttc,
 	TupleDesc	tupleDesc = ttc->ttc_rel->rd_att;
 	int			numAttrs = tupleDesc->natts;
 	int			biggest_attno = -1;
-	int32		biggest_size = MAXALIGN(TOAST_POINTER_SIZE);
+	int32		biggest_size = MAXALIGN(TOAST_OID_POINTER_SIZE);
 	int32		skip_colflags = TOASTCOL_IGNORE;
 	int			i;
 
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 22d0a2e8c3a6..606b9bcc6109 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4278,7 +4278,7 @@ WriteControlFile(void)
 	ControlFile->nameDataLen = NAMEDATALEN;
 	ControlFile->indexMaxKeys = INDEX_MAX_KEYS;
 
-	ControlFile->toast_max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	ControlFile->toast_max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
 	ControlFile->loblksize = LOBLKSIZE;
 
 	ControlFile->float8ByVal = true;	/* vestigial */
@@ -4531,15 +4531,15 @@ ReadControlFile(void)
 						   "INDEX_MAX_KEYS", ControlFile->indexMaxKeys,
 						   "INDEX_MAX_KEYS", INDEX_MAX_KEYS),
 				 errhint("It looks like you need to recompile or initdb.")));
-	if (ControlFile->toast_max_chunk_size != TOAST_MAX_CHUNK_SIZE)
+	if (ControlFile->toast_max_chunk_size != TOAST_OID_MAX_CHUNK_SIZE)
 		ereport(FATAL,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("database files are incompatible with server"),
 		/* translator: %s is a variable name and %d is its value */
 				 errdetail("The database cluster was initialized with %s %d,"
 						   " but the server was compiled with %s %d.",
-						   "TOAST_MAX_CHUNK_SIZE", ControlFile->toast_max_chunk_size,
-						   "TOAST_MAX_CHUNK_SIZE", (int) TOAST_MAX_CHUNK_SIZE),
+						   "TOAST_OID_MAX_CHUNK_SIZE", ControlFile->toast_max_chunk_size,
+						   "TOAST_OID_MAX_CHUNK_SIZE", (int) TOAST_OID_MAX_CHUNK_SIZE),
 				 errhint("It looks like you need to recompile or initdb.")));
 	if (ControlFile->loblksize != LOBLKSIZE)
 		ereport(FATAL,
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 8af8ecc61d68..21a45b6a1324 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -5139,7 +5139,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		struct varlena *varlena;
 
 		/* va_rawsize is the size of the original datum -- including header */
-		struct varatt_external toast_pointer;
+		varatt_external_oid toast_pointer;
 		struct varatt_indirect redirect_pointer;
 		struct varlena *new_datum = NULL;
 		struct varlena *reconstructed;
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 3894457ab404..cf22296cd178 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -4211,7 +4211,7 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 {
 	int			typlen;
 	struct varlena *attr;
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 
 	/* On first call, get the input type's typlen, and save at *fn_extra */
 	if (fcinfo->flinfo->fn_extra == NULL)
diff --git a/src/bin/pg_resetwal/pg_resetwal.c b/src/bin/pg_resetwal/pg_resetwal.c
index 8d5d9805279a..6133105d0e33 100644
--- a/src/bin/pg_resetwal/pg_resetwal.c
+++ b/src/bin/pg_resetwal/pg_resetwal.c
@@ -707,7 +707,7 @@ GuessControlValues(void)
 	ControlFile.xlog_seg_size = DEFAULT_XLOG_SEG_SIZE;
 	ControlFile.nameDataLen = NAMEDATALEN;
 	ControlFile.indexMaxKeys = INDEX_MAX_KEYS;
-	ControlFile.toast_max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	ControlFile.toast_max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
 	ControlFile.loblksize = LOBLKSIZE;
 	ControlFile.float8ByVal = true; /* vestigial */
 
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index d4508114a48e..e51612f1fead 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3533,7 +3533,7 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
       </row>
 
       <row>
-       <entry><structfield>max_toast_chunk_size</structfield></entry>
+       <entry><structfield>max_toast_oid_chunk_size</structfield></entry>
        <entry><type>integer</type></entry>
       </row>
 
diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml
index 02ddfda834a2..67600fd974d7 100644
--- a/doc/src/sgml/storage.sgml
+++ b/doc/src/sgml/storage.sgml
@@ -417,7 +417,7 @@ described in more detail below.
 
 <para>
 Out-of-line values are divided (after compression if used) into chunks of at
-most <symbol>TOAST_MAX_CHUNK_SIZE</symbol> bytes (by default this value is chosen
+most <symbol>TOAST_OID_MAX_CHUNK_SIZE</symbol> bytes (by default this value is chosen
 so that four chunk rows will fit on a page, making it about 2000 bytes).
 Each chunk is stored as a separate row in the <acronym>TOAST</acronym> table
 belonging to the owning table.  Every
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 164ced37583a..7ec6cef118fb 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -73,7 +73,7 @@ typedef enum SkipPages
  */
 typedef struct ToastedAttribute
 {
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 	BlockNumber blkno;			/* block in main table */
 	OffsetNumber offnum;		/* offset in main table */
 	AttrNumber	attnum;			/* attribute in main table */
@@ -1566,7 +1566,7 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 
 	toast_valueid = ta->toast_pointer.va_valueid;
 
-	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
 	last_chunk_seq = (extsize - 1) / max_chunk_size;
 
 	/* Sanity-check the sequence number. */
@@ -1672,7 +1672,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	uint16		infomask;
 	Oid8		toast_pointer_valueid;
 	CompactAttribute *thisatt;
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 
 	infomask = ctx->tuphdr->t_infomask;
 	thisatt = TupleDescCompactAttr(RelationGetDescr(ctx->rel), ctx->attnum);
@@ -1731,7 +1731,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	{
 		uint8		va_tag = VARTAG_EXTERNAL(tp + ctx->offset);
 
-		if (va_tag != VARTAG_ONDISK)
+		if (va_tag != VARTAG_ONDISK_OID)
 		{
 			report_corruption(ctx,
 							  psprintf("toasted attribute has unexpected TOAST tag %u",
@@ -1876,7 +1876,7 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	int32		expected_chunk_seq = 0;
 	int32		last_chunk_seq;
 	Oid8		toast_valueid;
-	int32		max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	int32		max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
 
 	extsize = VARATT_EXTERNAL_GET_EXTSIZE(ta->toast_pointer);
 	last_chunk_seq = (extsize - 1) / max_chunk_size;
-- 
2.51.0

v8-0005-Refactor-external-TOAST-pointer-code-for-better-p.patchtext/x-diff; charset=us-asciiDownload
From 2d77debcd4bc4ffd0e985524a4801ee685bc017d Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 30 Sep 2025 15:09:13 +0900
Subject: [PATCH v8 05/15] Refactor external TOAST pointer code for better
 pluggability

This commit introduces a new interface for external TOAST pointers,
which is able to make a translation of the varlena pointers stored on
disk to/from an new in-memory structure called toast_external.  The
types of varatt_external supported on disk need to be registered into a
new subsystem in a new file, called toast_external.[c|h], then define a
set of callbacks to allow the toasting and detoasting code to use it.

A follow-up change will rely on this refactoring to introduce new
vartag_external values with an associated varatt_external_* that is
able, which would be used in int8 TOAST tables.
---
 src/include/access/detoast.h                  |  12 +-
 src/include/access/heaptoast.h                |   3 +
 src/include/access/toast_external.h           | 176 ++++++++++++++++
 src/include/access/toast_helper.h             |   1 +
 src/include/varatt.h                          |  16 +-
 src/backend/access/common/Makefile            |   1 +
 src/backend/access/common/detoast.c           |  57 +++--
 src/backend/access/common/meson.build         |   1 +
 src/backend/access/common/toast_compression.c |  10 +-
 src/backend/access/common/toast_external.c    | 196 ++++++++++++++++++
 src/backend/access/common/toast_internals.c   |  84 +++++---
 src/backend/access/heap/heaptoast.c           |  20 +-
 src/backend/access/table/toast_helper.c       |  12 +-
 src/backend/access/transam/xlog.c             |   8 +-
 .../replication/logical/reorderbuffer.c       |  13 +-
 src/backend/utils/adt/varlena.c               |   7 +-
 src/bin/pg_resetwal/pg_resetwal.c             |   2 +-
 contrib/amcheck/verify_heapam.c               |  35 ++--
 src/tools/pgindent/typedefs.list              |   2 +
 19 files changed, 545 insertions(+), 111 deletions(-)
 create mode 100644 src/include/access/toast_external.h
 create mode 100644 src/backend/access/common/toast_external.c

diff --git a/src/include/access/detoast.h b/src/include/access/detoast.h
index 6435597b1127..2f71fbd95f88 100644
--- a/src/include/access/detoast.h
+++ b/src/include/access/detoast.h
@@ -14,10 +14,11 @@
 
 /*
  * Macro to fetch the possibly-unaligned contents of an EXTERNAL datum
- * into a local "varatt_external_oid" toast pointer.  This should be
- * just a memcpy, but some versions of gcc seem to produce broken code
- * that assumes the datum contents are aligned.  Introducing an explicit
- * intermediate "varattrib_1b_e *" variable seems to fix it.
+ * into a local "varatt_external_*" toast pointer, as supported
+ * in toast_external.h and varatt.h.  This should be just a memcpy, but
+ * some versions of gcc seem to produce broken code that assumes the datum
+ * contents are aligned.  Introducing an explicit intermediate
+ * "varattrib_1b_e *" variable seems to fix it.
  */
 #define VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr) \
 do { \
@@ -27,9 +28,6 @@ do { \
 	memcpy(&(toast_pointer), VARDATA_EXTERNAL(attre), sizeof(toast_pointer)); \
 } while (0)
 
-/* Size of an EXTERNAL datum that contains a standard TOAST pointer */
-#define TOAST_OID_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_oid))
-
 /* Size of an EXTERNAL datum that contains an indirection pointer */
 #define INDIRECT_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_indirect))
 
diff --git a/src/include/access/heaptoast.h b/src/include/access/heaptoast.h
index 55a6a17b2c0b..12c9702af689 100644
--- a/src/include/access/heaptoast.h
+++ b/src/include/access/heaptoast.h
@@ -88,6 +88,9 @@
 	 sizeof(int32) -									\
 	 VARHDRSZ)
 
+/* Maximum size of chunk possible */
+#define TOAST_MAX_CHUNK_SIZE	TOAST_OID_MAX_CHUNK_SIZE
+
 /* ----------
  * heap_toast_insert_or_update -
  *
diff --git a/src/include/access/toast_external.h b/src/include/access/toast_external.h
new file mode 100644
index 000000000000..6450343eab25
--- /dev/null
+++ b/src/include/access/toast_external.h
@@ -0,0 +1,176 @@
+/*-------------------------------------------------------------------------
+ *
+ * toast_external.h
+ *	  Support for on-disk external TOAST pointers
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1995, Regents of the University of California
+ *
+ * src/include/access/toast_external.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef TOAST_EXTERNAL_H
+#define TOAST_EXTERNAL_H
+
+#include "access/toast_compression.h"
+#include "varatt.h"
+
+/*
+ * Intermediate in-memory structure used when creating on-disk
+ * varatt_external_* or when deserializing varlena contents.
+ */
+typedef struct toast_external_data
+{
+	/* Original data size (includes header) */
+	int32		rawsize;
+
+	/* External saved size (without header) */
+	uint32		extsize;
+
+	/*
+	 * Compression method.
+	 *
+	 * If not compressed, set to TOAST_INVALID_COMPRESSION_ID.
+	 */
+	ToastCompressionId compression_method;
+
+	/* Relation OID of TOAST table containing the value */
+	Oid			toastrelid;
+
+	/*
+	 * Unique ID of value within TOAST table.  This could be an OID or an Oid8
+	 * value.  This field is large enough to be able to store any of these.
+	 */
+	Oid8		valueid;
+} toast_external_data;
+
+/*
+ * Metadata for external TOAST pointer kinds, separated based on their
+ * vartag_external.
+ */
+typedef struct toast_external_info
+{
+	/*
+	 * Maximum chunk of data authorized for this type of external TOAST
+	 * pointer, when dividing an entry by chunks.  Sized depending on the size
+	 * of its varatt_external_* structure.
+	 */
+	int32		maximum_chunk_size;
+
+	/*
+	 * Size of an external TOAST pointer of this type, typically
+	 * (VARHDRSZ_EXTERNAL + sizeof(varatt_external_struct)).
+	 */
+	int32		toast_pointer_size;
+
+	/*
+	 * Map an input varlena to a toast_external_data, for consumption in the
+	 * backend code.  "data" is an input/output result.
+	 */
+	void		(*to_external_data) (struct varlena *attr,
+									 toast_external_data *data);
+
+	/*
+	 * Create a varlena that will be used on-disk for the given TOAST type,
+	 * based on the given input data.
+	 *
+	 * The result is the varlena created, for on-disk insertion.
+	 */
+	struct varlena *(*create_external_data) (toast_external_data data);
+
+} toast_external_info;
+
+/* Retrieve a toast_external_info from a vartag */
+extern const toast_external_info *toast_external_get_info(uint8 tag);
+
+/* Retrieve toast_pointer_size using a TOAST attribute type */
+extern int32 toast_external_info_get_pointer_size(uint8 tag);
+
+/* Retrieve the vartag to assign to a TOAST typle */
+extern uint8 toast_external_assign_vartag(Oid toastrelid, Oid8 value);
+
+/*
+ * Testing whether an externally-stored value is compressed now requires
+ * comparing size stored in extsize (the actual length of the external data)
+ * to rawsize (the original uncompressed datum's size).  The latter includes
+ * VARHDRSZ overhead, the former doesn't.  We never use compression unless it
+ * actually saves space, so we expect either equality or less-than.
+ */
+static inline bool
+TOAST_EXTERNAL_IS_COMPRESSED(toast_external_data data)
+{
+	return data.extsize < (data.rawsize - VARHDRSZ);
+}
+
+/* Full data structure */
+static inline void
+toast_external_info_get_data(struct varlena *attr, toast_external_data *data)
+{
+	uint8		tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+
+	info->to_external_data(attr, data);
+}
+
+/*
+ * Helper routines to recover specific fields in toast_external_data.  Most
+ * code paths doing work with on-disk external TOAST pointers care about
+ * these.
+ */
+
+/* Detoasted "raw" size */
+static inline Size
+toast_external_info_get_rawsize(struct varlena *attr)
+{
+	uint8		tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+	toast_external_data data;
+
+	info->to_external_data(attr, &data);
+
+	return data.rawsize;
+}
+
+/* External saved size */
+static inline Size
+toast_external_info_get_extsize(struct varlena *attr)
+{
+	uint8		tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+	toast_external_data data;
+
+	info->to_external_data(attr, &data);
+
+	return data.extsize;
+}
+
+/* Compression method ID */
+static inline ToastCompressionId
+toast_external_info_get_compression_method(struct varlena *attr)
+{
+	uint8		tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+	toast_external_data data;
+
+	info->to_external_data(attr, &data);
+
+	return data.compression_method;
+}
+
+/* Value ID */
+static inline Oid8
+toast_external_info_get_valueid(struct varlena *attr)
+{
+	uint8		tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+	toast_external_data data;
+
+	info->to_external_data(attr, &data);
+
+	return data.valueid;
+}
+
+#endif							/* TOAST_EXTERNAL_H */
diff --git a/src/include/access/toast_helper.h b/src/include/access/toast_helper.h
index e6ab8afffb67..6bc912809f34 100644
--- a/src/include/access/toast_helper.h
+++ b/src/include/access/toast_helper.h
@@ -47,6 +47,7 @@ typedef struct
 	 * should be NULL in the case of an insert.
 	 */
 	Relation	ttc_rel;		/* the relation that contains the tuple */
+	int32		ttc_toast_pointer_size; /* size of external TOAST pointer */
 	Datum	   *ttc_values;		/* values from the tuple columns */
 	bool	   *ttc_isnull;		/* null flags for the tuple columns */
 	Datum	   *ttc_oldvalues;	/* values from previous tuple */
diff --git a/src/include/varatt.h b/src/include/varatt.h
index c873a59bb1c9..790d9f844c91 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -21,6 +21,9 @@
  * The data is compressed if and only if the external size stored in
  * va_extinfo is less than va_rawsize - VARHDRSZ.
  *
+ * The value ID is an OID, used for TOAST relations with OID as attribute
+ * for chunk_id.
+ *
  * This struct must not contain any padding, because we sometimes compare
  * these pointers using memcmp.
  *
@@ -51,7 +54,7 @@ typedef struct varatt_external_oid
  * The creator of such a Datum is entirely responsible that the referenced
  * storage survives for as long as referencing pointer Datums can exist.
  *
- * Note that just as for varatt_external_oid, this struct is stored
+ * Note that just as for varatt_external_*, this struct is stored
  * unaligned within any containing tuple.
  */
 typedef struct varatt_indirect
@@ -66,7 +69,7 @@ typedef struct varatt_indirect
  * storage.  APIs for this, in particular the definition of struct
  * ExpandedObjectHeader, are in src/include/utils/expandeddatum.h.
  *
- * Note that just as for varatt_external_oid, this struct is stored
+ * Note that just as for varatt_external_*, this struct is stored
  * unaligned within any containing tuple.
  */
 typedef struct ExpandedObjectHeader ExpandedObjectHeader;
@@ -357,11 +360,18 @@ VARATT_IS_EXTERNAL(const void *PTR)
 	return VARATT_IS_1B_E(PTR);
 }
 
+/* Is varlena datum a pointer to on-disk toasted data with OID value? */
+static inline bool
+VARATT_IS_EXTERNAL_ONDISK_OID(const void *PTR)
+{
+	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID;
+}
+
 /* Is varlena datum a pointer to on-disk toasted data? */
 static inline bool
 VARATT_IS_EXTERNAL_ONDISK(const void *PTR)
 {
-	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID;
+	return VARATT_IS_EXTERNAL_ONDISK_OID(PTR);
 }
 
 /* Is varlena datum an indirect pointer? */
diff --git a/src/backend/access/common/Makefile b/src/backend/access/common/Makefile
index e78de312659e..1ef86a245886 100644
--- a/src/backend/access/common/Makefile
+++ b/src/backend/access/common/Makefile
@@ -27,6 +27,7 @@ OBJS = \
 	syncscan.o \
 	tidstore.o \
 	toast_compression.o \
+	toast_external.o \
 	toast_internals.o \
 	tupconvert.o \
 	tupdesc.o
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index c187c32d96dd..8531c27439e4 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -16,6 +16,7 @@
 #include "access/detoast.h"
 #include "access/table.h"
 #include "access/tableam.h"
+#include "access/toast_external.h"
 #include "access/toast_internals.h"
 #include "common/int.h"
 #include "common/pg_lzcompress.h"
@@ -225,12 +226,12 @@ detoast_attr_slice(struct varlena *attr,
 
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
-		varatt_external_oid toast_pointer;
+		toast_external_data toast_pointer;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		toast_external_info_get_data(attr, &toast_pointer);
 
 		/* fast path for non-compressed external datums */
-		if (!VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+		if (!TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 			return toast_fetch_datum_slice(attr, sliceoffset, slicelength);
 
 		/*
@@ -240,7 +241,7 @@ detoast_attr_slice(struct varlena *attr,
 		 */
 		if (slicelimit >= 0)
 		{
-			int32		max_size = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+			int32		max_size = toast_pointer.extsize;
 
 			/*
 			 * Determine maximum amount of compressed data needed for a prefix
@@ -251,8 +252,7 @@ detoast_attr_slice(struct varlena *attr,
 			 * determine how much compressed data we need to be sure of being
 			 * able to decompress the required slice.
 			 */
-			if (VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) ==
-				TOAST_PGLZ_COMPRESSION_ID)
+			if (toast_pointer.compression_method == TOAST_PGLZ_COMPRESSION_ID)
 				max_size = pglz_maximum_compressed_size(slicelimit, max_size);
 
 			/*
@@ -344,20 +344,21 @@ toast_fetch_datum(struct varlena *attr)
 {
 	Relation	toastrel;
 	struct varlena *result;
-	varatt_external_oid toast_pointer;
+	toast_external_data toast_pointer;
 	int32		attrsize;
+	Oid8		valueid;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		elog(ERROR, "toast_fetch_datum shouldn't be called for non-ondisk datums");
 
 	/* Must copy to access aligned fields */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_external_info_get_data(attr, &toast_pointer);
 
-	attrsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+	attrsize = toast_pointer.extsize;
 
 	result = (struct varlena *) palloc(attrsize + VARHDRSZ);
 
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+	if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 		SET_VARSIZE_COMPRESSED(result, attrsize + VARHDRSZ);
 	else
 		SET_VARSIZE(result, attrsize + VARHDRSZ);
@@ -365,14 +366,15 @@ toast_fetch_datum(struct varlena *attr)
 	if (attrsize == 0)
 		return result;			/* Probably shouldn't happen, but just in
 								 * case. */
+	valueid = toast_pointer.valueid;
 
 	/*
 	 * Open the toast relation and its indexes
 	 */
-	toastrel = table_open(toast_pointer.va_toastrelid, AccessShareLock);
+	toastrel = table_open(toast_pointer.toastrelid, AccessShareLock);
 
 	/* Fetch all chunks */
-	table_relation_fetch_toast_slice(toastrel, toast_pointer.va_valueid,
+	table_relation_fetch_toast_slice(toastrel, valueid,
 									 attrsize, 0, attrsize, result);
 
 	/* Close toast table */
@@ -398,23 +400,26 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 {
 	Relation	toastrel;
 	struct varlena *result;
-	varatt_external_oid toast_pointer;
+	toast_external_data toast_pointer;
 	int32		attrsize;
+	Oid8		valueid;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		elog(ERROR, "toast_fetch_datum_slice shouldn't be called for non-ondisk datums");
 
 	/* Must copy to access aligned fields */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_external_info_get_data(attr, &toast_pointer);
+
+	valueid = toast_pointer.valueid;
 
 	/*
 	 * It's nonsense to fetch slices of a compressed datum unless when it's a
 	 * prefix -- this isn't lo_* we can't return a compressed datum which is
 	 * meaningful to toast later.
 	 */
-	Assert(!VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer) || 0 == sliceoffset);
+	Assert(!TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer) || 0 == sliceoffset);
 
-	attrsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+	attrsize = toast_pointer.extsize;
 
 	if (sliceoffset >= attrsize)
 	{
@@ -427,7 +432,7 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 	 * space required by va_tcinfo, which is stored at the beginning as an
 	 * int32 value.
 	 */
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer) && slicelength > 0)
+	if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer) && slicelength > 0)
 		slicelength = slicelength + sizeof(int32);
 
 	/*
@@ -440,7 +445,7 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 
 	result = (struct varlena *) palloc(slicelength + VARHDRSZ);
 
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+	if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 		SET_VARSIZE_COMPRESSED(result, slicelength + VARHDRSZ);
 	else
 		SET_VARSIZE(result, slicelength + VARHDRSZ);
@@ -449,10 +454,11 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 		return result;			/* Can save a lot of work at this point! */
 
 	/* Open the toast relation */
-	toastrel = table_open(toast_pointer.va_toastrelid, AccessShareLock);
+	toastrel = table_open(toast_pointer.toastrelid, AccessShareLock);
 
 	/* Fetch all chunks */
-	table_relation_fetch_toast_slice(toastrel, toast_pointer.va_valueid,
+	table_relation_fetch_toast_slice(toastrel,
+									 valueid,
 									 attrsize, sliceoffset, slicelength,
 									 result);
 
@@ -549,11 +555,7 @@ toast_raw_datum_size(Datum value)
 
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
-		/* va_rawsize is the size of the original datum -- including header */
-		varatt_external_oid toast_pointer;
-
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-		result = toast_pointer.va_rawsize;
+		result = toast_external_info_get_rawsize(attr);
 	}
 	else if (VARATT_IS_EXTERNAL_INDIRECT(attr))
 	{
@@ -610,10 +612,7 @@ toast_datum_size(Datum value)
 		 * compressed or not.  We do not count the size of the toast pointer
 		 * ... should we?
 		 */
-		varatt_external_oid toast_pointer;
-
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-		result = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+		result = toast_external_info_get_extsize(attr);
 	}
 	else if (VARATT_IS_EXTERNAL_INDIRECT(attr))
 	{
diff --git a/src/backend/access/common/meson.build b/src/backend/access/common/meson.build
index e3cdbe7a22e1..c20f2e88921e 100644
--- a/src/backend/access/common/meson.build
+++ b/src/backend/access/common/meson.build
@@ -15,6 +15,7 @@ backend_sources += files(
   'syncscan.c',
   'tidstore.c',
   'toast_compression.c',
+  'toast_external.c',
   'toast_internals.c',
   'tupconvert.c',
   'tupdesc.c',
diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c
index 08f572f31eed..94606a58c8fb 100644
--- a/src/backend/access/common/toast_compression.c
+++ b/src/backend/access/common/toast_compression.c
@@ -19,6 +19,7 @@
 
 #include "access/detoast.h"
 #include "access/toast_compression.h"
+#include "access/toast_external.h"
 #include "common/pg_lzcompress.h"
 #include "varatt.h"
 
@@ -261,14 +262,7 @@ toast_get_compression_id(struct varlena *attr)
 	 * toast compression header.
 	 */
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
-	{
-		varatt_external_oid toast_pointer;
-
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-
-		if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
-			cmid = VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer);
-	}
+		cmid = toast_external_info_get_compression_method(attr);
 	else if (VARATT_IS_COMPRESSED(attr))
 		cmid = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(attr);
 
diff --git a/src/backend/access/common/toast_external.c b/src/backend/access/common/toast_external.c
new file mode 100644
index 000000000000..2154152b8bfb
--- /dev/null
+++ b/src/backend/access/common/toast_external.c
@@ -0,0 +1,196 @@
+/*-------------------------------------------------------------------------
+ *
+ * toast_external.c
+ *	  Functions for the support of external on-disk TOAST pointers.
+ *
+ * This includes all the types of external on-disk TOAST pointers supported
+ * by the backend, based on the callbacks and data defined in
+ * toast_external.h.
+ *
+ * Copyright (c) 2025, PostgreSQL Global Development Group
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/common/toast_external.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/detoast.h"
+#include "access/heaptoast.h"
+#include "access/toast_external.h"
+
+/* Callbacks for VARTAG_ONDISK_OID */
+static void ondisk_oid_to_external_data(struct varlena *attr,
+										toast_external_data *data);
+static struct varlena *ondisk_oid_create_external_data(toast_external_data data);
+
+/*
+ * Size of an EXTERNAL datum that contains a standard TOAST pointer (OID
+ * value).
+ */
+#define TOAST_OID_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_oid))
+
+/*
+ * For now there are only two types, all defined in this file.  For now this
+ * is the maximum value of vartag_external, which is a historical choice.
+ */
+#define TOAST_EXTERNAL_INFO_SIZE	(VARTAG_ONDISK_OID + 1)
+
+/*
+ * The different kinds of on-disk external TOAST pointers, divided by
+ * vartag_external.
+ *
+ * See comments for struct toast_external_info about the details of the
+ * individual fields.
+ */
+static const toast_external_info toast_external_infos[TOAST_EXTERNAL_INFO_SIZE] = {
+	[VARTAG_ONDISK_OID] = {
+		.toast_pointer_size = TOAST_OID_POINTER_SIZE,
+		.maximum_chunk_size = TOAST_OID_MAX_CHUNK_SIZE,
+		.to_external_data = ondisk_oid_to_external_data,
+		.create_external_data = ondisk_oid_create_external_data,
+	},
+};
+
+/*
+ * toast_external_get_info
+ *
+ * Get toast_external_info of the defined vartag_external, central set of
+ * callbacks, based on a "tag", which is a vartag_external value for an
+ * on-disk external varlena.
+ */
+const toast_external_info *
+toast_external_get_info(uint8 tag)
+{
+	const toast_external_info *res = &toast_external_infos[tag];
+
+	/* check tag for invalid range */
+	if (tag >= TOAST_EXTERNAL_INFO_SIZE)
+		elog(ERROR, "incorrect value %u for toast_external_info", tag);
+
+	/* sanity check with tag in valid range */
+	res = &toast_external_infos[tag];
+	if (res == NULL)
+		elog(ERROR, "incorrect value %u for toast_external_info", tag);
+	return res;
+}
+
+/*
+ * toast_external_info_get_pointer_size
+ *
+ * Get external TOAST pointer size based on the attribute type of a TOAST
+ * value.  "tag" is a vartag_external value.
+ */
+int32
+toast_external_info_get_pointer_size(uint8 tag)
+{
+	return toast_external_infos[tag].toast_pointer_size;
+}
+
+/*
+ * toast_external_assign_vartag
+ *
+ * Assign the vartag_external of a TOAST tuple, based on the TOAST relation
+ * it uses and its value.
+ *
+ * An invalid value can be given by the caller of this routine, in which
+ * case a default vartag should be provided based on only the toast relation
+ * used.
+ */
+uint8
+toast_external_assign_vartag(Oid toastrelid, Oid8 valueid)
+{
+	/*
+	 * If dealing with a code path where a TOAST relation may not be assigned,
+	 * like heap_toast_insert_or_update(), just use the legacy
+	 * vartag_external.
+	 */
+	if (!OidIsValid(toastrelid))
+		return VARTAG_ONDISK_OID;
+
+	/*
+	 * Currently there is only one type of vartag_external supported: 4-byte
+	 * value with OID for the chunk_id type.
+	 *
+	 * Note: This routine will be extended to be able to use multiple
+	 * vartag_external within a single TOAST relation type, that may change
+	 * depending on the value used.
+	 */
+	return VARTAG_ONDISK_OID;
+}
+
+/*
+ * Helper routines able to translate the various varatt_external_* from/to
+ * the in-memory representation toast_external_data used in the backend.
+ */
+
+/* Callbacks for VARTAG_ONDISK_OID */
+
+/*
+ * ondisk_oid_to_external_data
+ *
+ * Translate a varlena to its toast_external_data representation, to be used
+ * by the backend code.
+ */
+static void
+ondisk_oid_to_external_data(struct varlena *attr, toast_external_data *data)
+{
+	varatt_external_oid external;
+
+	VARATT_EXTERNAL_GET_POINTER(external, attr);
+	data->rawsize = external.va_rawsize;
+
+	/*
+	 * External size and compression methods are stored in the same field,
+	 * extract.
+	 */
+	if (VARATT_EXTERNAL_IS_COMPRESSED(external))
+	{
+		data->extsize = VARATT_EXTERNAL_GET_EXTSIZE(external);
+		data->compression_method = VARATT_EXTERNAL_GET_COMPRESS_METHOD(external);
+	}
+	else
+	{
+		data->extsize = external.va_extinfo;
+		data->compression_method = TOAST_INVALID_COMPRESSION_ID;
+	}
+
+	data->valueid = (Oid8) external.va_valueid;
+	data->toastrelid = external.va_toastrelid;
+}
+
+/*
+ * ondisk_oid_create_external_data
+ *
+ * Create a new varlena based on the input toast_external_data, to be used
+ * when saving a new TOAST value.
+ */
+static struct varlena *
+ondisk_oid_create_external_data(toast_external_data data)
+{
+	struct varlena *result = NULL;
+	varatt_external_oid external;
+
+	external.va_rawsize = data.rawsize;
+
+	if (data.compression_method != TOAST_INVALID_COMPRESSION_ID)
+	{
+		/* Set size and compression method, in a single field. */
+		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(external,
+													 data.extsize,
+													 data.compression_method);
+	}
+	else
+		external.va_extinfo = data.extsize;
+
+	external.va_toastrelid = data.toastrelid;
+	external.va_valueid = (Oid) data.valueid;
+
+	result = (struct varlena *) palloc(TOAST_OID_POINTER_SIZE);
+	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_OID);
+	memcpy(VARDATA_EXTERNAL(result), &external, sizeof(external));
+
+	return result;
+}
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 72eae4f7fbe6..3f597d5b545c 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -18,6 +18,7 @@
 #include "access/heapam.h"
 #include "access/heaptoast.h"
 #include "access/table.h"
+#include "access/toast_external.h"
 #include "access/toast_internals.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -124,13 +125,15 @@ toast_save_datum(Relation rel, Datum value,
 	TupleDesc	toasttupDesc;
 	CommandId	mycid = GetCurrentCommandId(true);
 	struct varlena *result;
-	varatt_external_oid toast_pointer;
+	toast_external_data toast_pointer;
 	int32		chunk_seq = 0;
 	char	   *data_p;
 	int32		data_todo;
 	Pointer		dval = DatumGetPointer(value);
 	int			num_indexes;
 	int			validIndex;
+	const toast_external_info *info;
+	uint8		tag = VARTAG_INDIRECT;	/* init value does not matter */
 
 	Assert(!VARATT_IS_EXTERNAL(dval));
 
@@ -162,28 +165,41 @@ toast_save_datum(Relation rel, Datum value,
 	{
 		data_p = VARDATA_SHORT(dval);
 		data_todo = VARSIZE_SHORT(dval) - VARHDRSZ_SHORT;
-		toast_pointer.va_rawsize = data_todo + VARHDRSZ;	/* as if not short */
-		toast_pointer.va_extinfo = data_todo;
+		toast_pointer.rawsize = data_todo + VARHDRSZ;	/* as if not short */
+		toast_pointer.extsize = data_todo;
+
+		/*
+		 * TOAST_INVALID_COMPRESSION_ID means that the varlena is not
+		 * compressed, see toast_get_compression_id().
+		 */
+		toast_pointer.compression_method = TOAST_INVALID_COMPRESSION_ID;
 	}
 	else if (VARATT_IS_COMPRESSED(dval))
 	{
 		data_p = VARDATA(dval);
 		data_todo = VARSIZE(dval) - VARHDRSZ;
 		/* rawsize in a compressed datum is just the size of the payload */
-		toast_pointer.va_rawsize = VARDATA_COMPRESSED_GET_EXTSIZE(dval) + VARHDRSZ;
+		toast_pointer.rawsize = VARDATA_COMPRESSED_GET_EXTSIZE(dval) + VARHDRSZ;
 
 		/* set external size and compression method */
-		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, data_todo,
-													 VARDATA_COMPRESSED_GET_COMPRESS_METHOD(dval));
+		toast_pointer.extsize = data_todo;
+		toast_pointer.compression_method = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(dval);
+
 		/* Assert that the numbers look like it's compressed */
-		Assert(VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer));
+		Assert(TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer));
 	}
 	else
 	{
 		data_p = VARDATA(dval);
 		data_todo = VARSIZE(dval) - VARHDRSZ;
-		toast_pointer.va_rawsize = VARSIZE(dval);
-		toast_pointer.va_extinfo = data_todo;
+		toast_pointer.rawsize = VARSIZE(dval);
+		toast_pointer.extsize = data_todo;
+
+		/*
+		 * TOAST_INVALID_COMPRESSION_ID means that the varlena is not
+		 * compressed, see toast_get_compression_id().
+		 */
+		toast_pointer.compression_method = TOAST_INVALID_COMPRESSION_ID;
 	}
 
 	/*
@@ -195,9 +211,9 @@ toast_save_datum(Relation rel, Datum value,
 	 * if we have to substitute such an OID.
 	 */
 	if (OidIsValid(rel->rd_toastoid))
-		toast_pointer.va_toastrelid = rel->rd_toastoid;
+		toast_pointer.toastrelid = rel->rd_toastoid;
 	else
-		toast_pointer.va_toastrelid = RelationGetRelid(toastrel);
+		toast_pointer.toastrelid = RelationGetRelid(toastrel);
 
 	/*
 	 * Choose an OID to use as the value ID for this toast value.
@@ -214,7 +230,7 @@ toast_save_datum(Relation rel, Datum value,
 	if (!OidIsValid(rel->rd_toastoid))
 	{
 		/* normal case: just choose an unused OID */
-		toast_pointer.va_valueid =
+		toast_pointer.valueid =
 			GetNewOidWithIndex(toastrel,
 							   RelationGetRelid(toastidxs[validIndex]),
 							   (AttrNumber) 1);
@@ -222,18 +238,18 @@ toast_save_datum(Relation rel, Datum value,
 	else
 	{
 		/* rewrite case: check to see if value was in old toast table */
-		toast_pointer.va_valueid = InvalidOid;
+		toast_pointer.valueid = InvalidOid8;
 		if (oldexternal != NULL)
 		{
-			varatt_external_oid old_toast_pointer;
+			toast_external_data old_toast_pointer;
 
 			Assert(VARATT_IS_EXTERNAL_ONDISK(oldexternal));
-			/* Must copy to access aligned fields */
-			VARATT_EXTERNAL_GET_POINTER(old_toast_pointer, oldexternal);
-			if (old_toast_pointer.va_toastrelid == rel->rd_toastoid)
+			toast_external_info_get_data(oldexternal, &old_toast_pointer);
+
+			if (old_toast_pointer.toastrelid == rel->rd_toastoid)
 			{
 				/* This value came from the old toast table; reuse its OID */
-				toast_pointer.va_valueid = old_toast_pointer.va_valueid;
+				toast_pointer.valueid = old_toast_pointer.valueid;
 
 				/*
 				 * There is a corner case here: the table rewrite might have
@@ -253,14 +269,14 @@ toast_save_datum(Relation rel, Datum value,
 				 * be reclaimed by VACUUM.
 				 */
 				if (toastrel_valueid_exists(toastrel,
-											toast_pointer.va_valueid))
+											toast_pointer.valueid))
 				{
 					/* Match, so short-circuit the data storage loop below */
 					data_todo = 0;
 				}
 			}
 		}
-		if (toast_pointer.va_valueid == InvalidOid)
+		if (toast_pointer.valueid == InvalidOid8)
 		{
 			/*
 			 * new value; must choose an OID that doesn't conflict in either
@@ -268,15 +284,23 @@ toast_save_datum(Relation rel, Datum value,
 			 */
 			do
 			{
-				toast_pointer.va_valueid =
+				toast_pointer.valueid =
 					GetNewOidWithIndex(toastrel,
 									   RelationGetRelid(toastidxs[validIndex]),
 									   (AttrNumber) 1);
 			} while (toastid_valueid_exists(rel->rd_toastoid,
-											toast_pointer.va_valueid));
+											toast_pointer.valueid));
 		}
 	}
 
+	/*
+	 * Retrieve the vartag that can be assigned for the new TOAST tuple. This
+	 * depends on the type of TOAST table and its assigned value.
+	 */
+	tag = toast_external_assign_vartag(toast_pointer.toastrelid,
+									   toast_pointer.valueid);
+	info = toast_external_get_info(tag);
+
 	/*
 	 * Split up the item into chunks
 	 */
@@ -298,12 +322,12 @@ toast_save_datum(Relation rel, Datum value,
 		/*
 		 * Calculate the size of this chunk
 		 */
-		chunk_size = Min(TOAST_OID_MAX_CHUNK_SIZE, data_todo);
+		chunk_size = Min(info->maximum_chunk_size, data_todo);
 
 		/*
 		 * Build a tuple and store it
 		 */
-		t_values[0] = ObjectIdGetDatum(toast_pointer.va_valueid);
+		t_values[0] = ObjectIdGetDatum(toast_pointer.valueid);
 		t_values[1] = Int32GetDatum(chunk_seq++);
 		SET_VARSIZE(&chunk_data, chunk_size + VARHDRSZ);
 		memcpy(VARDATA(&chunk_data), data_p, chunk_size);
@@ -359,9 +383,7 @@ toast_save_datum(Relation rel, Datum value,
 	/*
 	 * Create the TOAST pointer value that we'll return
 	 */
-	result = (struct varlena *) palloc(TOAST_OID_POINTER_SIZE);
-	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_OID);
-	memcpy(VARDATA_EXTERNAL(result), &toast_pointer, sizeof(toast_pointer));
+	result = info->create_external_data(toast_pointer);
 
 	return PointerGetDatum(result);
 }
@@ -376,7 +398,7 @@ void
 toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 {
 	struct varlena *attr = (struct varlena *) DatumGetPointer(value);
-	varatt_external_oid toast_pointer;
+	toast_external_data toast_pointer;
 	Relation	toastrel;
 	Relation   *toastidxs;
 	ScanKeyData toastkey;
@@ -389,12 +411,12 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 		return;
 
 	/* Must copy to access aligned fields */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_external_info_get_data(attr, &toast_pointer);
 
 	/*
 	 * Open the toast relation and its indexes
 	 */
-	toastrel = table_open(toast_pointer.va_toastrelid, RowExclusiveLock);
+	toastrel = table_open(toast_pointer.toastrelid, RowExclusiveLock);
 
 	/* Fetch valid relation used for process */
 	validIndex = toast_open_indexes(toastrel,
@@ -408,7 +430,7 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	ScanKeyInit(&toastkey,
 				(AttrNumber) 1,
 				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(toast_pointer.va_valueid));
+				ObjectIdGetDatum(toast_pointer.valueid));
 
 	/*
 	 * Find all the chunks.  (We don't actually care whether we see them in
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index 7f408ccd0bb6..2242fc2f75bc 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -28,6 +28,7 @@
 #include "access/genam.h"
 #include "access/heapam.h"
 #include "access/heaptoast.h"
+#include "access/toast_external.h"
 #include "access/toast_helper.h"
 #include "access/toast_internals.h"
 #include "utils/fmgroids.h"
@@ -109,6 +110,7 @@ heap_toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup,
 	Datum		toast_oldvalues[MaxHeapAttributeNumber];
 	ToastAttrInfo toast_attr[MaxHeapAttributeNumber];
 	ToastTupleContext ttc;
+	uint8		tag;
 
 	/*
 	 * Ignore the INSERT_SPECULATIVE option. Speculative insertions/super
@@ -140,6 +142,16 @@ heap_toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup,
 	 * Prepare for toasting
 	 * ----------
 	 */
+
+	/*
+	 * Retrieve the toast pointer size based on the type of external TOAST
+	 * pointer assumed to be used.
+	 */
+
+	/* The default value is invalid, to work as a default. */
+	tag = toast_external_assign_vartag(rel->rd_rel->reltoastrelid, InvalidOid8);
+	ttc.ttc_toast_pointer_size = toast_external_info_get_pointer_size(tag);
+
 	ttc.ttc_rel = rel;
 	ttc.ttc_values = toast_values;
 	ttc.ttc_isnull = toast_isnull;
@@ -640,6 +652,8 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 	int			num_indexes;
 	int			validIndex;
 	int32		max_chunk_size;
+	const toast_external_info *info;
+	uint8		tag = VARTAG_INDIRECT;	/* init value does not matter */
 
 	/* Look for the valid index of toast relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -647,7 +661,11 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 									&toastidxs,
 									&num_indexes);
 
-	max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
+	/* Grab the information for toast_external_data */
+	tag = toast_external_assign_vartag(RelationGetRelid(toastrel), valueid);
+	info = toast_external_get_info(tag);
+
+	max_chunk_size = info->maximum_chunk_size;
 
 	totalchunks = ((attrsize - 1) / max_chunk_size) + 1;
 	startchunk = sliceoffset / max_chunk_size;
diff --git a/src/backend/access/table/toast_helper.c b/src/backend/access/table/toast_helper.c
index 0c58c6c32565..76a7cfe6174e 100644
--- a/src/backend/access/table/toast_helper.c
+++ b/src/backend/access/table/toast_helper.c
@@ -171,8 +171,10 @@ toast_tuple_init(ToastTupleContext *ttc)
  * The column must have attstorage EXTERNAL or EXTENDED if check_main is
  * false, and must have attstorage MAIN if check_main is true.
  *
- * The column must have a minimum size of MAXALIGN(TOAST_OID_POINTER_SIZE);
- * if not, no benefit is to be expected by compressing it.
+ * The column must have a minimum size of MAXALIGN(tcc_toast_pointer_size);
+ * if not, no benefit is to be expected by compressing it.  The TOAST
+ * pointer size is given by the caller, depending on the type of TOAST
+ * table we are dealing with.
  *
  * The return value is the index of the biggest suitable column, or
  * -1 if there is none.
@@ -184,10 +186,14 @@ toast_tuple_find_biggest_attribute(ToastTupleContext *ttc,
 	TupleDesc	tupleDesc = ttc->ttc_rel->rd_att;
 	int			numAttrs = tupleDesc->natts;
 	int			biggest_attno = -1;
-	int32		biggest_size = MAXALIGN(TOAST_OID_POINTER_SIZE);
+	int32		biggest_size = 0;
 	int32		skip_colflags = TOASTCOL_IGNORE;
 	int			i;
 
+	/* Define the lower-bound */
+	biggest_size = MAXALIGN(ttc->ttc_toast_pointer_size);
+	Assert(biggest_size != 0);
+
 	if (for_compression)
 		skip_colflags |= TOASTCOL_INCOMPRESSIBLE;
 
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 606b9bcc6109..22d0a2e8c3a6 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4278,7 +4278,7 @@ WriteControlFile(void)
 	ControlFile->nameDataLen = NAMEDATALEN;
 	ControlFile->indexMaxKeys = INDEX_MAX_KEYS;
 
-	ControlFile->toast_max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
+	ControlFile->toast_max_chunk_size = TOAST_MAX_CHUNK_SIZE;
 	ControlFile->loblksize = LOBLKSIZE;
 
 	ControlFile->float8ByVal = true;	/* vestigial */
@@ -4531,15 +4531,15 @@ ReadControlFile(void)
 						   "INDEX_MAX_KEYS", ControlFile->indexMaxKeys,
 						   "INDEX_MAX_KEYS", INDEX_MAX_KEYS),
 				 errhint("It looks like you need to recompile or initdb.")));
-	if (ControlFile->toast_max_chunk_size != TOAST_OID_MAX_CHUNK_SIZE)
+	if (ControlFile->toast_max_chunk_size != TOAST_MAX_CHUNK_SIZE)
 		ereport(FATAL,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("database files are incompatible with server"),
 		/* translator: %s is a variable name and %d is its value */
 				 errdetail("The database cluster was initialized with %s %d,"
 						   " but the server was compiled with %s %d.",
-						   "TOAST_OID_MAX_CHUNK_SIZE", ControlFile->toast_max_chunk_size,
-						   "TOAST_OID_MAX_CHUNK_SIZE", (int) TOAST_OID_MAX_CHUNK_SIZE),
+						   "TOAST_MAX_CHUNK_SIZE", ControlFile->toast_max_chunk_size,
+						   "TOAST_MAX_CHUNK_SIZE", (int) TOAST_MAX_CHUNK_SIZE),
 				 errhint("It looks like you need to recompile or initdb.")));
 	if (ControlFile->loblksize != LOBLKSIZE)
 		ereport(FATAL,
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 21a45b6a1324..24daeb93bc81 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -92,6 +92,7 @@
 #include "access/detoast.h"
 #include "access/heapam.h"
 #include "access/rewriteheap.h"
+#include "access/toast_external.h"
 #include "access/transam.h"
 #include "access/xact.h"
 #include "access/xlog_internal.h"
@@ -5139,7 +5140,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		struct varlena *varlena;
 
 		/* va_rawsize is the size of the original datum -- including header */
-		varatt_external_oid toast_pointer;
+		toast_external_data toast_pointer;
 		struct varatt_indirect redirect_pointer;
 		struct varlena *new_datum = NULL;
 		struct varlena *reconstructed;
@@ -5165,8 +5166,8 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		if (!VARATT_IS_EXTERNAL(varlena))
 			continue;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, varlena);
-		toast_valueid = toast_pointer.va_valueid;
+		toast_external_info_get_data(varlena, &toast_pointer);
+		toast_valueid = toast_pointer.valueid;
 
 		/*
 		 * Check whether the toast tuple changed, replace if so.
@@ -5184,7 +5185,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 
 		free[natt] = true;
 
-		reconstructed = palloc0(toast_pointer.va_rawsize);
+		reconstructed = palloc0(toast_pointer.rawsize);
 
 		ent->reconstructed = reconstructed;
 
@@ -5209,10 +5210,10 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 				   VARSIZE(chunk) - VARHDRSZ);
 			data_done += VARSIZE(chunk) - VARHDRSZ;
 		}
-		Assert(data_done == VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer));
+		Assert(data_done == toast_pointer.extsize);
 
 		/* make sure its marked as compressed or not */
-		if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+		if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 			SET_VARSIZE_COMPRESSED(reconstructed, data_done + VARHDRSZ);
 		else
 			SET_VARSIZE(reconstructed, data_done + VARHDRSZ);
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index cf22296cd178..15b14866fccf 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -19,6 +19,7 @@
 
 #include "access/detoast.h"
 #include "access/toast_compression.h"
+#include "access/toast_external.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_type.h"
 #include "common/hashfn.h"
@@ -4211,7 +4212,7 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 {
 	int			typlen;
 	struct varlena *attr;
-	varatt_external_oid toast_pointer;
+	Oid8		toast_valueid;
 
 	/* On first call, get the input type's typlen, and save at *fn_extra */
 	if (fcinfo->flinfo->fn_extra == NULL)
@@ -4238,9 +4239,9 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		PG_RETURN_NULL();
 
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_valueid = toast_external_info_get_valueid(attr);
 
-	PG_RETURN_OID(toast_pointer.va_valueid);
+	PG_RETURN_OID(toast_valueid);
 }
 
 /*
diff --git a/src/bin/pg_resetwal/pg_resetwal.c b/src/bin/pg_resetwal/pg_resetwal.c
index 6133105d0e33..8d5d9805279a 100644
--- a/src/bin/pg_resetwal/pg_resetwal.c
+++ b/src/bin/pg_resetwal/pg_resetwal.c
@@ -707,7 +707,7 @@ GuessControlValues(void)
 	ControlFile.xlog_seg_size = DEFAULT_XLOG_SEG_SIZE;
 	ControlFile.nameDataLen = NAMEDATALEN;
 	ControlFile.indexMaxKeys = INDEX_MAX_KEYS;
-	ControlFile.toast_max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
+	ControlFile.toast_max_chunk_size = TOAST_MAX_CHUNK_SIZE;
 	ControlFile.loblksize = LOBLKSIZE;
 	ControlFile.float8ByVal = true; /* vestigial */
 
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 7ec6cef118fb..9cf3c081bf01 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -16,6 +16,7 @@
 #include "access/multixact.h"
 #include "access/relation.h"
 #include "access/table.h"
+#include "access/toast_external.h"
 #include "access/toast_internals.h"
 #include "access/visibilitymap.h"
 #include "access/xact.h"
@@ -73,7 +74,8 @@ typedef enum SkipPages
  */
 typedef struct ToastedAttribute
 {
-	varatt_external_oid toast_pointer;
+	toast_external_data toast_pointer;
+	const toast_external_info *info;
 	BlockNumber blkno;			/* block in main table */
 	OffsetNumber offnum;		/* offset in main table */
 	AttrNumber	attnum;			/* attribute in main table */
@@ -1564,9 +1566,9 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	Oid8		toast_valueid;
 	int32		max_chunk_size;
 
-	toast_valueid = ta->toast_pointer.va_valueid;
+	toast_valueid = ta->toast_pointer.valueid;
 
-	max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
+	max_chunk_size = ta->info->maximum_chunk_size;
 	last_chunk_seq = (extsize - 1) / max_chunk_size;
 
 	/* Sanity-check the sequence number. */
@@ -1672,7 +1674,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	uint16		infomask;
 	Oid8		toast_pointer_valueid;
 	CompactAttribute *thisatt;
-	varatt_external_oid toast_pointer;
+	toast_external_data toast_pointer;
 
 	infomask = ctx->tuphdr->t_infomask;
 	thisatt = TupleDescCompactAttr(RelationGetDescr(ctx->rel), ctx->attnum);
@@ -1778,24 +1780,24 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	/*
 	 * Must copy attr into toast_pointer for alignment considerations
 	 */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-	toast_pointer_valueid = toast_pointer.va_valueid;
+	toast_external_info_get_data(attr, &toast_pointer);
+	toast_pointer_valueid = toast_pointer.valueid;
 
 	/* Toasted attributes too large to be untoasted should never be stored */
-	if (toast_pointer.va_rawsize > VARLENA_SIZE_LIMIT)
+	if (toast_pointer.rawsize > VARLENA_SIZE_LIMIT)
 		report_corruption(ctx,
 						  psprintf("toast value " OID8_FORMAT " rawsize %d exceeds limit %d",
 								   toast_pointer_valueid,
-								   toast_pointer.va_rawsize,
+								   toast_pointer.rawsize,
 								   VARLENA_SIZE_LIMIT));
 
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+	if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 	{
 		ToastCompressionId cmid;
 		bool		valid = false;
 
 		/* Compressed attributes should have a valid compression method */
-		cmid = TOAST_COMPRESS_METHOD(&toast_pointer);
+		cmid = toast_pointer.compression_method;
 		switch (cmid)
 		{
 				/* List of all valid compression method IDs */
@@ -1849,7 +1851,8 @@ check_tuple_attribute(HeapCheckContext *ctx)
 
 		ta = (ToastedAttribute *) palloc0(sizeof(ToastedAttribute));
 
-		VARATT_EXTERNAL_GET_POINTER(ta->toast_pointer, attr);
+		toast_external_info_get_data(attr, &ta->toast_pointer);
+		ta->info = toast_external_get_info(VARTAG_EXTERNAL(attr));
 		ta->blkno = ctx->blkno;
 		ta->offnum = ctx->offnum;
 		ta->attnum = ctx->attnum;
@@ -1876,9 +1879,11 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	int32		expected_chunk_seq = 0;
 	int32		last_chunk_seq;
 	Oid8		toast_valueid;
-	int32		max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
+	int32		max_chunk_size;
 
-	extsize = VARATT_EXTERNAL_GET_EXTSIZE(ta->toast_pointer);
+	extsize = ta->toast_pointer.extsize;
+
+	max_chunk_size = ta->info->maximum_chunk_size;
 	last_chunk_seq = (extsize - 1) / max_chunk_size;
 
 	/*
@@ -1887,7 +1892,7 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	ScanKeyInit(&toastkey,
 				(AttrNumber) 1,
 				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(ta->toast_pointer.va_valueid));
+				ObjectIdGetDatum(ta->toast_pointer.valueid));
 
 	/*
 	 * Check if any chunks for this toasted object exist in the toast table,
@@ -1907,7 +1912,7 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	}
 	systable_endscan_ordered(toastscan);
 
-	toast_valueid = ta->toast_pointer.va_valueid;
+	toast_valueid = ta->toast_pointer.valueid;
 
 	if (!found_toasttup)
 		report_toast_corruption(ctx, ta,
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index dfcd619bfee3..296e4e352434 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -4168,6 +4168,8 @@ timeout_params
 timerCA
 tlist_vinfo
 toast_compress_header
+toast_external_data
+toast_external_info
 tokenize_error_callback_arg
 transferMode
 transfer_thread_arg
-- 
2.51.0

v8-0006-Move-static-inline-routines-of-varatt_external_oi.patchtext/x-diff; charset=us-asciiDownload
From c671979a8f9387ec6a81d64823725c8b7f893577 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 8 Aug 2025 15:40:04 +0900
Subject: [PATCH v8 06/15] Move static inline routines of varatt_external_oid
 to toast_external.c

This isolates most of the knowledge of varatt_external_oid into the
local area where it is manipulated through the toast_external transition
type, with the backend code not requiring it.  Extension code should not
need it either, as toast_external should be the layer to use when
looking at external on-dist TOAST varlenas.
---
 src/include/varatt.h                       | 31 -----------------
 src/backend/access/common/toast_external.c | 40 ++++++++++++++++++++--
 2 files changed, 37 insertions(+), 34 deletions(-)

diff --git a/src/include/varatt.h b/src/include/varatt.h
index 790d9f844c91..035c0f95e5b6 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -513,22 +513,6 @@ VARDATA_COMPRESSED_GET_COMPRESS_METHOD(const void *PTR)
 	return ((varattrib_4b *) PTR)->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS;
 }
 
-/*
- * Same for external Datums; but note argument is a struct
- * varatt_external_oid.
- */
-static inline Size
-VARATT_EXTERNAL_GET_EXTSIZE(varatt_external_oid toast_pointer)
-{
-	return toast_pointer.va_extinfo & VARLENA_EXTSIZE_MASK;
-}
-
-static inline uint32
-VARATT_EXTERNAL_GET_COMPRESS_METHOD(varatt_external_oid toast_pointer)
-{
-	return toast_pointer.va_extinfo >> VARLENA_EXTSIZE_BITS;
-}
-
 /* Set size and compress method of an externally-stored varlena datum */
 /* This has to remain a macro; beware multiple evaluations! */
 #define VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, len, cm) \
@@ -538,19 +522,4 @@ VARATT_EXTERNAL_GET_COMPRESS_METHOD(varatt_external_oid toast_pointer)
 		((toast_pointer).va_extinfo = \
 			(len) | ((uint32) (cm) << VARLENA_EXTSIZE_BITS)); \
 	} while (0)
-
-/*
- * Testing whether an externally-stored value is compressed now requires
- * comparing size stored in va_extinfo (the actual length of the external data)
- * to rawsize (the original uncompressed datum's size).  The latter includes
- * VARHDRSZ overhead, the former doesn't.  We never use compression unless it
- * actually saves space, so we expect either equality or less-than.
- */
-static inline bool
-VARATT_EXTERNAL_IS_COMPRESSED(varatt_external_oid toast_pointer)
-{
-	return VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) <
-		(Size) (toast_pointer.va_rawsize - VARHDRSZ);
-}
-
 #endif
diff --git a/src/backend/access/common/toast_external.c b/src/backend/access/common/toast_external.c
index 2154152b8bfb..4c500720e0d1 100644
--- a/src/backend/access/common/toast_external.c
+++ b/src/backend/access/common/toast_external.c
@@ -26,6 +26,40 @@ static void ondisk_oid_to_external_data(struct varlena *attr,
 										toast_external_data *data);
 static struct varlena *ondisk_oid_create_external_data(toast_external_data data);
 
+/*
+ * Decompressed size of an on-disk varlena; but note argument is a struct
+ * varatt_external_oid.
+ */
+static inline Size
+varatt_external_oid_get_extsize(varatt_external_oid toast_pointer)
+{
+	return toast_pointer.va_extinfo & VARLENA_EXTSIZE_MASK;
+}
+
+/*
+ * Compression method of an on-disk varlena; but note argument is a struct
+ *  varatt_external_oid.
+ */
+static inline uint32
+varatt_external_oid_get_compress_method(varatt_external_oid toast_pointer)
+{
+	return toast_pointer.va_extinfo >> VARLENA_EXTSIZE_BITS;
+}
+
+/*
+ * Testing whether an externally-stored TOAST value is compressed now requires
+ * comparing size stored in va_extinfo (the actual length of the external data)
+ * to rawsize (the original uncompressed datum's size).  The latter includes
+ * VARHDRSZ overhead, the former doesn't.  We never use compression unless it
+ * actually saves space, so we expect either equality or less-than.
+ */
+static inline bool
+varatt_external_oid_is_compressed(varatt_external_oid toast_pointer)
+{
+	return varatt_external_oid_get_extsize(toast_pointer) <
+		(Size) (toast_pointer.va_rawsize - VARHDRSZ);
+}
+
 /*
  * Size of an EXTERNAL datum that contains a standard TOAST pointer (OID
  * value).
@@ -146,10 +180,10 @@ ondisk_oid_to_external_data(struct varlena *attr, toast_external_data *data)
 	 * External size and compression methods are stored in the same field,
 	 * extract.
 	 */
-	if (VARATT_EXTERNAL_IS_COMPRESSED(external))
+	if (varatt_external_oid_is_compressed(external))
 	{
-		data->extsize = VARATT_EXTERNAL_GET_EXTSIZE(external);
-		data->compression_method = VARATT_EXTERNAL_GET_COMPRESS_METHOD(external);
+		data->extsize = varatt_external_oid_get_extsize(external);
+		data->compression_method = varatt_external_oid_get_compress_method(external);
 	}
 	else
 	{
-- 
2.51.0

v8-0007-Split-VARATT_EXTERNAL_GET_POINTER-for-indirect-an.patchtext/x-diff; charset=us-asciiDownload
From 15454f7e0e1dfe64556460b6aa6d43721b473e79 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 13 Aug 2025 19:38:50 +0900
Subject: [PATCH v8 07/15] Split VARATT_EXTERNAL_GET_POINTER for indirect and
 OID TOAST pointers

VARATT_EXTERNAL_GET_POINTER() is renamed to
VARATT_INDIRECT_GET_POINTER() with the external on-disk TOAST pointers
for OID values being now located within toast_external.c, splitting both
concepts completely.
---
 src/include/access/detoast.h               | 16 ++++++++--------
 src/backend/access/common/detoast.c        | 10 +++++-----
 src/backend/access/common/toast_external.c | 21 ++++++++++++++++++++-
 src/backend/utils/adt/expandeddatum.c      |  2 +-
 4 files changed, 34 insertions(+), 15 deletions(-)

diff --git a/src/include/access/detoast.h b/src/include/access/detoast.h
index 2f71fbd95f88..31e9786848ef 100644
--- a/src/include/access/detoast.h
+++ b/src/include/access/detoast.h
@@ -13,17 +13,17 @@
 #define DETOAST_H
 
 /*
- * Macro to fetch the possibly-unaligned contents of an EXTERNAL datum
- * into a local "varatt_external_*" toast pointer, as supported
- * in toast_external.h and varatt.h.  This should be just a memcpy, but
- * some versions of gcc seem to produce broken code that assumes the datum
- * contents are aligned.  Introducing an explicit intermediate
- * "varattrib_1b_e *" variable seems to fix it.
+ * Macro to fetch the possibly-unaligned contents of an indirect datum
+ * into a local "varatt_indirect" toast pointer, as supported
+ * in varatt.h.  This should be just a memcpy, but some versions of gcc
+ * seem to produce broken code that assumes the datum contents are aligned.
+ * Introducing an explicit intermediate "varattrib_1b_e *" variable seems
+ * to fix it.
  */
-#define VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr) \
+#define VARATT_INDIRECT_GET_POINTER(toast_pointer, attr) \
 do { \
 	varattrib_1b_e *attre = (varattrib_1b_e *) (attr); \
-	Assert(VARATT_IS_EXTERNAL(attre)); \
+	Assert(VARATT_IS_EXTERNAL_INDIRECT(attre)); \
 	Assert(VARSIZE_EXTERNAL(attre) == sizeof(toast_pointer) + VARHDRSZ_EXTERNAL); \
 	memcpy(&(toast_pointer), VARDATA_EXTERNAL(attre), sizeof(toast_pointer)); \
 } while (0)
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 8531c27439e4..b645988667f0 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -61,7 +61,7 @@ detoast_external_attr(struct varlena *attr)
 		 */
 		struct varatt_indirect redirect;
 
-		VARATT_EXTERNAL_GET_POINTER(redirect, attr);
+		VARATT_INDIRECT_GET_POINTER(redirect, attr);
 		attr = (struct varlena *) redirect.pointer;
 
 		/* nested indirect Datums aren't allowed */
@@ -138,7 +138,7 @@ detoast_attr(struct varlena *attr)
 		 */
 		struct varatt_indirect redirect;
 
-		VARATT_EXTERNAL_GET_POINTER(redirect, attr);
+		VARATT_INDIRECT_GET_POINTER(redirect, attr);
 		attr = (struct varlena *) redirect.pointer;
 
 		/* nested indirect Datums aren't allowed */
@@ -268,7 +268,7 @@ detoast_attr_slice(struct varlena *attr,
 	{
 		struct varatt_indirect redirect;
 
-		VARATT_EXTERNAL_GET_POINTER(redirect, attr);
+		VARATT_INDIRECT_GET_POINTER(redirect, attr);
 
 		/* nested indirect Datums aren't allowed */
 		Assert(!VARATT_IS_EXTERNAL_INDIRECT(redirect.pointer));
@@ -561,7 +561,7 @@ toast_raw_datum_size(Datum value)
 	{
 		struct varatt_indirect toast_pointer;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		VARATT_INDIRECT_GET_POINTER(toast_pointer, attr);
 
 		/* nested indirect Datums aren't allowed */
 		Assert(!VARATT_IS_EXTERNAL_INDIRECT(toast_pointer.pointer));
@@ -618,7 +618,7 @@ toast_datum_size(Datum value)
 	{
 		struct varatt_indirect toast_pointer;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		VARATT_INDIRECT_GET_POINTER(toast_pointer, attr);
 
 		/* nested indirect Datums aren't allowed */
 		Assert(!VARATT_IS_EXTERNAL_INDIRECT(attr));
diff --git a/src/backend/access/common/toast_external.c b/src/backend/access/common/toast_external.c
index 4c500720e0d1..e2f0a9dc1c50 100644
--- a/src/backend/access/common/toast_external.c
+++ b/src/backend/access/common/toast_external.c
@@ -26,6 +26,25 @@ static void ondisk_oid_to_external_data(struct varlena *attr,
 										toast_external_data *data);
 static struct varlena *ondisk_oid_create_external_data(toast_external_data data);
 
+/*
+ * Fetch the possibly-unaligned contents of an on-disk external TOAST with
+ * OID values into a local "varatt_external_oid" pointer.
+ *
+ * This should be just a memcpy, but some versions of gcc seem to produce
+ * broken code that assumes the datum contents are aligned.  Introducing
+ * an explicit intermediate "varattrib_1b_e *" variable seems to fix it.
+ */
+static inline void
+varatt_external_oid_get_pointer(varatt_external_oid *toast_pointer,
+								struct varlena *attr)
+{
+	varattrib_1b_e *attre = (varattrib_1b_e *) attr;
+
+	Assert(VARATT_IS_EXTERNAL_ONDISK_OID(attre));
+	Assert(VARSIZE_EXTERNAL(attre) == sizeof(varatt_external_oid) + VARHDRSZ_EXTERNAL);
+	memcpy(toast_pointer, VARDATA_EXTERNAL(attre), sizeof(varatt_external_oid));
+}
+
 /*
  * Decompressed size of an on-disk varlena; but note argument is a struct
  * varatt_external_oid.
@@ -173,7 +192,7 @@ ondisk_oid_to_external_data(struct varlena *attr, toast_external_data *data)
 {
 	varatt_external_oid external;
 
-	VARATT_EXTERNAL_GET_POINTER(external, attr);
+	varatt_external_oid_get_pointer(&external, attr);
 	data->rawsize = external.va_rawsize;
 
 	/*
diff --git a/src/backend/utils/adt/expandeddatum.c b/src/backend/utils/adt/expandeddatum.c
index 6b4b8eaf005c..4c04671d23ed 100644
--- a/src/backend/utils/adt/expandeddatum.c
+++ b/src/backend/utils/adt/expandeddatum.c
@@ -23,7 +23,7 @@
  * Given a Datum that is an expanded-object reference, extract the pointer.
  *
  * This is a bit tedious since the pointer may not be properly aligned;
- * compare VARATT_EXTERNAL_GET_POINTER().
+ * compare VARATT_INDIRECT_GET_POINTER().
  */
 ExpandedObjectHeader *
 DatumGetEOHP(Datum d)
-- 
2.51.0

v8-0008-Switch-pg_column_toast_chunk_id-return-value-from.patchtext/x-diff; charset=us-asciiDownload
From 5a23c6860ff082322b8ef31d05b36da88ca9b726 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 13 Aug 2025 19:55:39 +0900
Subject: [PATCH v8 08/15] Switch pg_column_toast_chunk_id() return value from
 oid to oid8

This is required for a follow-up patch that will add support for 8-byte
TOAST values, with this function being changed so as it is able to
support the largest TOAST value type available.

XXX: Bump catalog version.
---
 src/include/catalog/pg_proc.dat              | 2 +-
 src/backend/utils/adt/varlena.c              | 2 +-
 src/test/regress/expected/misc_functions.out | 2 +-
 src/test/regress/sql/misc_functions.sql      | 2 +-
 doc/src/sgml/func/func-admin.sgml            | 2 +-
 5 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 6c392a57c7f3..ed7021382822 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7764,7 +7764,7 @@
   proargtypes => 'any', prosrc => 'pg_column_compression' },
 { oid => '6316', descr => 'chunk ID of on-disk TOASTed value',
   proname => 'pg_column_toast_chunk_id', provolatile => 's',
-  prorettype => 'oid', proargtypes => 'any',
+  prorettype => 'oid8', proargtypes => 'any',
   prosrc => 'pg_column_toast_chunk_id' },
 { oid => '2322',
   descr => 'total disk space usage for the specified tablespace',
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 15b14866fccf..4d86e2cf9b29 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -4241,7 +4241,7 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 
 	toast_valueid = toast_external_info_get_valueid(attr);
 
-	PG_RETURN_OID(toast_valueid);
+	PG_RETURN_OID8(toast_valueid);
 }
 
 /*
diff --git a/src/test/regress/expected/misc_functions.out b/src/test/regress/expected/misc_functions.out
index d7d965d884a1..1028934b9aba 100644
--- a/src/test/regress/expected/misc_functions.out
+++ b/src/test/regress/expected/misc_functions.out
@@ -962,7 +962,7 @@ SELECT t.relname AS toastrel FROM pg_class c
   WHERE c.relname = 'test_chunk_id'
 \gset
 SELECT pg_column_toast_chunk_id(a) IS NULL,
-  pg_column_toast_chunk_id(b) IN (SELECT chunk_id FROM pg_toast.:toastrel)
+  pg_column_toast_chunk_id(b) IN (SELECT chunk_id::oid8 FROM pg_toast.:toastrel)
   FROM test_chunk_id;
  ?column? | ?column? 
 ----------+----------
diff --git a/src/test/regress/sql/misc_functions.sql b/src/test/regress/sql/misc_functions.sql
index 0fc20fbb6b40..05ed8f517af0 100644
--- a/src/test/regress/sql/misc_functions.sql
+++ b/src/test/regress/sql/misc_functions.sql
@@ -440,7 +440,7 @@ SELECT t.relname AS toastrel FROM pg_class c
   WHERE c.relname = 'test_chunk_id'
 \gset
 SELECT pg_column_toast_chunk_id(a) IS NULL,
-  pg_column_toast_chunk_id(b) IN (SELECT chunk_id FROM pg_toast.:toastrel)
+  pg_column_toast_chunk_id(b) IN (SELECT chunk_id::oid8 FROM pg_toast.:toastrel)
   FROM test_chunk_id;
 DROP TABLE test_chunk_id;
 DROP FUNCTION explain_mask_costs(text, bool, bool, bool, bool);
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 1b465bc8ba71..df570fc2c5c9 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1590,7 +1590,7 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
          <primary>pg_column_toast_chunk_id</primary>
         </indexterm>
         <function>pg_column_toast_chunk_id</function> ( <type>"any"</type> )
-        <returnvalue>oid</returnvalue>
+        <returnvalue>oid8</returnvalue>
        </para>
        <para>
         Shows the <structfield>chunk_id</structfield> of an on-disk
-- 
2.51.0

v8-0009-Add-catcache-support-for-OID8OID.patchtext/x-diff; charset=us-asciiDownload
From 4a5e94ea0d554432e182ed5b79b26387e50c3c38 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 13 Aug 2025 20:00:30 +0900
Subject: [PATCH v8 09/15] Add catcache support for OID8OID

This is required to be able to do catalog cache lookups of oid8 fields
for toast values of the same type.
---
 src/backend/utils/cache/catcache.c | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index 02ae7d5a8318..37c98a4c3483 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -240,6 +240,18 @@ int4hashfast(Datum datum)
 	return murmurhash32((int32) DatumGetInt32(datum));
 }
 
+static bool
+oid8eqfast(Datum a, Datum b)
+{
+	return DatumGetObjectId8(a) == DatumGetObjectId8(b);
+}
+
+static uint32
+oid8hashfast(Datum datum)
+{
+	return murmurhash64(DatumGetObjectId8(datum));
+}
+
 static bool
 texteqfast(Datum a, Datum b)
 {
@@ -300,6 +312,11 @@ GetCCHashEqFuncs(Oid keytype, CCHashFN *hashfunc, RegProcedure *eqfunc, CCFastEq
 			*fasteqfunc = int4eqfast;
 			*eqfunc = F_INT4EQ;
 			break;
+		case OID8OID:
+			*hashfunc = oid8hashfast;
+			*fasteqfunc = oid8eqfast;
+			*eqfunc = F_OID8EQ;
+			break;
 		case TEXTOID:
 			*hashfunc = texthashfast;
 			*fasteqfunc = texteqfast;
-- 
2.51.0

v8-0010-Add-support-for-TOAST-chunk_id-type-in-binary-upg.patchtext/x-diff; charset=us-asciiDownload
From 656f8ecd7b128d90262364be67697520cfd0efe4 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 10:57:59 +0900
Subject: [PATCH v8 10/15] Add support for TOAST chunk_id type in binary
 upgrades

This commit adds a new function, which would set the type of a chunk_id
attribute for a TOAST table across upgrades.  This piece currently works
only with chunk_id = OIDOID, but it is required in a follow-up patch
where support for chunk_id = OID8OID is supported on top of the existing
one.
---
 src/include/catalog/binary_upgrade.h          |  1 +
 src/include/catalog/pg_proc.dat               |  4 ++++
 src/backend/catalog/heap.c                    |  1 +
 src/backend/catalog/toasting.c                | 20 ++++++++++++++++++-
 src/backend/utils/adt/pg_upgrade_support.c    | 11 ++++++++++
 src/bin/pg_dump/pg_dump.c                     | 10 +++++++++-
 .../expected/spgist_name_ops.out              |  6 ++++--
 7 files changed, 49 insertions(+), 4 deletions(-)

diff --git a/src/include/catalog/binary_upgrade.h b/src/include/catalog/binary_upgrade.h
index 6fcc59edebd8..3deb0423d795 100644
--- a/src/include/catalog/binary_upgrade.h
+++ b/src/include/catalog/binary_upgrade.h
@@ -29,6 +29,7 @@ extern PGDLLIMPORT Oid binary_upgrade_next_index_pg_class_oid;
 extern PGDLLIMPORT RelFileNumber binary_upgrade_next_index_pg_class_relfilenumber;
 extern PGDLLIMPORT Oid binary_upgrade_next_toast_pg_class_oid;
 extern PGDLLIMPORT RelFileNumber binary_upgrade_next_toast_pg_class_relfilenumber;
+extern PGDLLIMPORT Oid binary_upgrade_next_toast_chunk_id_typoid;
 
 extern PGDLLIMPORT Oid binary_upgrade_next_pg_enum_oid;
 extern PGDLLIMPORT Oid binary_upgrade_next_pg_authid_oid;
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index ed7021382822..298d5c9337a3 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11787,6 +11787,10 @@
   proname => 'binary_upgrade_set_next_toast_pg_class_oid', provolatile => 'v',
   proparallel => 'r', prorettype => 'void', proargtypes => 'oid',
   prosrc => 'binary_upgrade_set_next_toast_pg_class_oid' },
+{ oid => '8219', descr => 'for use by pg_upgrade',
+  proname => 'binary_upgrade_set_next_toast_chunk_id_typoid', provolatile => 'v',
+  proparallel => 'r', prorettype => 'void', proargtypes => 'oid',
+  prosrc => 'binary_upgrade_set_next_toast_chunk_id_typoid' },
 { oid => '3589', descr => 'for use by pg_upgrade',
   proname => 'binary_upgrade_set_next_pg_enum_oid', provolatile => 'v',
   proparallel => 'r', prorettype => 'void', proargtypes => 'oid',
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index fd6537567ea2..e5cc98937055 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -80,6 +80,7 @@
 /* Potentially set by pg_upgrade_support functions */
 Oid			binary_upgrade_next_heap_pg_class_oid = InvalidOid;
 Oid			binary_upgrade_next_toast_pg_class_oid = InvalidOid;
+Oid			binary_upgrade_next_toast_chunk_id_typoid = InvalidOid;
 RelFileNumber binary_upgrade_next_heap_pg_class_relfilenumber = InvalidRelFileNumber;
 RelFileNumber binary_upgrade_next_toast_pg_class_relfilenumber = InvalidRelFileNumber;
 
diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c
index 874a8fc89adb..f1d76d8acd51 100644
--- a/src/backend/catalog/toasting.c
+++ b/src/backend/catalog/toasting.c
@@ -145,6 +145,7 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	int16		coloptions[2];
 	ObjectAddress baseobject,
 				toastobject;
+	Oid			toast_chunkid_typid = OIDOID;
 
 	/*
 	 * Is it already toasted?
@@ -183,6 +184,23 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 		 */
 		if (!OidIsValid(binary_upgrade_next_toast_pg_class_oid))
 			return false;
+
+		/*
+		 * The attribute type for chunk_id should have been set when requesting
+		 * a TOAST table creation.
+		 */
+		if (!OidIsValid(binary_upgrade_next_toast_chunk_id_typoid))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("toast chunk_id type not set while in binary upgrade mode")));
+		if (binary_upgrade_next_toast_chunk_id_typoid != OIDOID)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("cannot support toast chunk_id type %u in binary upgrade mode",
+							binary_upgrade_next_toast_chunk_id_typoid)));
+
+		toast_chunkid_typid = binary_upgrade_next_toast_chunk_id_typoid;
+		binary_upgrade_next_toast_chunk_id_typoid = InvalidOid;
 	}
 
 	/*
@@ -204,7 +222,7 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	tupdesc = CreateTemplateTupleDesc(3);
 	TupleDescInitEntry(tupdesc, (AttrNumber) 1,
 					   "chunk_id",
-					   OIDOID,
+					   toast_chunkid_typid,
 					   -1, 0);
 	TupleDescInitEntry(tupdesc, (AttrNumber) 2,
 					   "chunk_seq",
diff --git a/src/backend/utils/adt/pg_upgrade_support.c b/src/backend/utils/adt/pg_upgrade_support.c
index a4f8b4faa90d..200ffcdbab44 100644
--- a/src/backend/utils/adt/pg_upgrade_support.c
+++ b/src/backend/utils/adt/pg_upgrade_support.c
@@ -149,6 +149,17 @@ binary_upgrade_set_next_toast_pg_class_oid(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+Datum
+binary_upgrade_set_next_toast_chunk_id_typoid(PG_FUNCTION_ARGS)
+{
+	Oid			typoid = PG_GETARG_OID(0);
+
+	CHECK_IS_BINARY_UPGRADE;
+	binary_upgrade_next_toast_chunk_id_typoid = typoid;
+
+	PG_RETURN_VOID();
+}
+
 Datum
 binary_upgrade_set_next_toast_relfilenode(PG_FUNCTION_ARGS)
 {
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index a00918bacb40..8acf91c6c05b 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -103,6 +103,7 @@ typedef struct
 	RelFileNumber relfilenumber;	/* object filenode */
 	Oid			toast_oid;		/* toast table OID */
 	RelFileNumber toast_relfilenumber;	/* toast table filenode */
+	Oid			toast_chunk_id_typoid;	/* type of chunk_id attribute */
 	Oid			toast_index_oid;	/* toast table index OID */
 	RelFileNumber toast_index_relfilenumber;	/* toast table index filenode */
 } BinaryUpgradeClassOidItem;
@@ -5812,7 +5813,10 @@ collectBinaryUpgradeClassOids(Archive *fout)
 	const char *query;
 
 	query = "SELECT c.oid, c.relkind, c.relfilenode, c.reltoastrelid, "
-		"ct.relfilenode, i.indexrelid, cti.relfilenode "
+		"ct.relfilenode, i.indexrelid, cti.relfilenode, "
+		"(SELECT a.atttypid FROM pg_attribute AS a "
+		"  WHERE a.attrelid = c.reltoastrelid AND attname = 'chunk_id'::text) "
+		"  AS toastchunktypid "
 		"FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_index i "
 		"ON (c.reltoastrelid = i.indrelid AND i.indisvalid) "
 		"LEFT JOIN pg_catalog.pg_class ct ON (c.reltoastrelid = ct.oid) "
@@ -5834,6 +5838,7 @@ collectBinaryUpgradeClassOids(Archive *fout)
 		binaryUpgradeClassOids[i].toast_relfilenumber = atooid(PQgetvalue(res, i, 4));
 		binaryUpgradeClassOids[i].toast_index_oid = atooid(PQgetvalue(res, i, 5));
 		binaryUpgradeClassOids[i].toast_index_relfilenumber = atooid(PQgetvalue(res, i, 6));
+		binaryUpgradeClassOids[i].toast_chunk_id_typoid = atooid(PQgetvalue(res, i, 7));
 	}
 
 	PQclear(res);
@@ -5898,6 +5903,9 @@ binary_upgrade_set_pg_class_oids(Archive *fout,
 			appendPQExpBuffer(upgrade_buffer,
 							  "SELECT pg_catalog.binary_upgrade_set_next_toast_relfilenode('%u'::pg_catalog.oid);\n",
 							  entry->toast_relfilenumber);
+			appendPQExpBuffer(upgrade_buffer,
+							  "SELECT pg_catalog.binary_upgrade_set_next_toast_chunk_id_typoid('%u'::pg_catalog.oid);\n",
+							  entry->toast_chunk_id_typoid);
 
 			/* every toast table has an index */
 			appendPQExpBuffer(upgrade_buffer,
diff --git a/src/test/modules/spgist_name_ops/expected/spgist_name_ops.out b/src/test/modules/spgist_name_ops/expected/spgist_name_ops.out
index 1ee65ede2430..35e59d0cd83c 100644
--- a/src/test/modules/spgist_name_ops/expected/spgist_name_ops.out
+++ b/src/test/modules/spgist_name_ops/expected/spgist_name_ops.out
@@ -61,9 +61,10 @@ select * from t
  binary_upgrade_set_next_pg_enum_oid                  |    | binary_upgrade_set_next_pg_enum_oid
  binary_upgrade_set_next_pg_tablespace_oid            |    | binary_upgrade_set_next_pg_tablespace_oid
  binary_upgrade_set_next_pg_type_oid                  |    | binary_upgrade_set_next_pg_type_oid
+ binary_upgrade_set_next_toast_chunk_id_typoid        |    | binary_upgrade_set_next_toast_chunk_id_typoid
  binary_upgrade_set_next_toast_pg_class_oid           |  1 | binary_upgrade_set_next_toast_pg_class_oid
  binary_upgrade_set_next_toast_relfilenode            |    | binary_upgrade_set_next_toast_relfilenode
-(13 rows)
+(14 rows)
 
 -- Verify clean failure when INCLUDE'd columns result in overlength tuple
 -- The error message details are platform-dependent, so show only SQLSTATE
@@ -110,9 +111,10 @@ select * from t
  binary_upgrade_set_next_pg_enum_oid                  |    | binary_upgrade_set_next_pg_enum_oid
  binary_upgrade_set_next_pg_tablespace_oid            |    | binary_upgrade_set_next_pg_tablespace_oid
  binary_upgrade_set_next_pg_type_oid                  |    | binary_upgrade_set_next_pg_type_oid
+ binary_upgrade_set_next_toast_chunk_id_typoid        |    | binary_upgrade_set_next_toast_chunk_id_typoid
  binary_upgrade_set_next_toast_pg_class_oid           |  1 | binary_upgrade_set_next_toast_pg_class_oid
  binary_upgrade_set_next_toast_relfilenode            |    | binary_upgrade_set_next_toast_relfilenode
-(13 rows)
+(14 rows)
 
 \set VERBOSITY sqlstate
 insert into t values(repeat('xyzzy', 12), 42, repeat('xyzzy', 4000));
-- 
2.51.0

v8-0011-Enlarge-OID-generation-to-8-bytes.patchtext/x-diff; charset=us-asciiDownload
From ed6b6feb62087e55125cba78014aa50543285017 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 12:15:15 +0900
Subject: [PATCH v8 11/15] Enlarge OID generation to 8 bytes

This adds a new routine called GetNewObjectId8() in varsup.c, which is
able to retrieve a 64b OID.  GetNewObjectId() is kept compatible with
its origin, where we still check that the lower 32 bits of the counter
do not wraparound, handling the FirstNormalObjectId case.

pg_resetwal -o/--next-oid is updated to be able to handle 8-byte OIDs.
---
 src/include/access/transam.h              |  3 +-
 src/include/access/xlog.h                 |  2 +-
 src/include/catalog/pg_control.h          |  2 +-
 src/backend/access/rmgrdesc/xlogdesc.c    |  8 +--
 src/backend/access/transam/varsup.c       | 62 ++++++++++++++++-------
 src/backend/access/transam/xlog.c         |  8 +--
 src/backend/access/transam/xlogrecovery.c |  2 +-
 src/bin/pg_controldata/pg_controldata.c   |  2 +-
 src/bin/pg_resetwal/pg_resetwal.c         | 10 ++--
 doc/src/sgml/ref/pg_resetwal.sgml         |  6 +--
 10 files changed, 66 insertions(+), 39 deletions(-)

diff --git a/src/include/access/transam.h b/src/include/access/transam.h
index c9e204182757..2cde38e28b3d 100644
--- a/src/include/access/transam.h
+++ b/src/include/access/transam.h
@@ -211,7 +211,7 @@ typedef struct TransamVariablesData
 	/*
 	 * These fields are protected by OidGenLock.
 	 */
-	Oid			nextOid;		/* next OID to assign */
+	Oid8		nextOid;		/* next OID (8 bytes) to assign */
 	uint32		oidCount;		/* OIDs available before must do XLOG work */
 
 	/*
@@ -355,6 +355,7 @@ extern void SetTransactionIdLimit(TransactionId oldest_datfrozenxid,
 extern void AdvanceOldestClogXid(TransactionId oldest_datfrozenxid);
 extern bool ForceTransactionIdLimitUpdate(void);
 extern Oid	GetNewObjectId(void);
+extern Oid8 GetNewObjectId8(void);
 extern void StopGeneratingPinnedObjectIds(void);
 
 #ifdef USE_ASSERT_CHECKING
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index 605280ed8fb6..9c8d79394625 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -244,7 +244,7 @@ extern void ShutdownXLOG(int code, Datum arg);
 extern bool CreateCheckPoint(int flags);
 extern bool CreateRestartPoint(int flags);
 extern WALAvailability GetWALAvailability(XLogRecPtr targetLSN);
-extern void XLogPutNextOid(Oid nextOid);
+extern void XLogPutNextOid(Oid8 nextOid);
 extern XLogRecPtr XLogRestorePoint(const char *rpName);
 extern void UpdateFullPageWrites(void);
 extern void GetFullPageWriteInfo(XLogRecPtr *RedoRecPtr_p, bool *doPageWrites_p);
diff --git a/src/include/catalog/pg_control.h b/src/include/catalog/pg_control.h
index 293e9e03f599..2bd747c4f829 100644
--- a/src/include/catalog/pg_control.h
+++ b/src/include/catalog/pg_control.h
@@ -42,7 +42,7 @@ typedef struct CheckPoint
 	bool		fullPageWrites; /* current full_page_writes */
 	int			wal_level;		/* current wal_level */
 	FullTransactionId nextXid;	/* next free transaction ID */
-	Oid			nextOid;		/* next free OID */
+	Oid8		nextOid;		/* next free OID */
 	MultiXactId nextMulti;		/* next free MultiXactId */
 	MultiXactOffset nextMultiOffset;	/* next free MultiXact offset */
 	TransactionId oldestXid;	/* cluster-wide minimum datfrozenxid */
diff --git a/src/backend/access/rmgrdesc/xlogdesc.c b/src/backend/access/rmgrdesc/xlogdesc.c
index cd6c2a2f650a..23920d32092a 100644
--- a/src/backend/access/rmgrdesc/xlogdesc.c
+++ b/src/backend/access/rmgrdesc/xlogdesc.c
@@ -66,7 +66,7 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
 		CheckPoint *checkpoint = (CheckPoint *) rec;
 
 		appendStringInfo(buf, "redo %X/%08X; "
-						 "tli %u; prev tli %u; fpw %s; wal_level %s; xid %u:%u; oid %u; multi %u; offset %u; "
+						 "tli %u; prev tli %u; fpw %s; wal_level %s; xid %u:%u; oid " OID8_FORMAT "; multi %u; offset %u; "
 						 "oldest xid %u in DB %u; oldest multi %u in DB %u; "
 						 "oldest/newest commit timestamp xid: %u/%u; "
 						 "oldest running xid %u; %s",
@@ -91,10 +91,10 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
 	}
 	else if (info == XLOG_NEXTOID)
 	{
-		Oid			nextOid;
+		Oid8		nextOid;
 
-		memcpy(&nextOid, rec, sizeof(Oid));
-		appendStringInfo(buf, "%u", nextOid);
+		memcpy(&nextOid, rec, sizeof(Oid8));
+		appendStringInfo(buf, OID8_FORMAT, nextOid);
 	}
 	else if (info == XLOG_RESTORE_POINT)
 	{
diff --git a/src/backend/access/transam/varsup.c b/src/backend/access/transam/varsup.c
index f8c4dada7c93..662d1dcaed16 100644
--- a/src/backend/access/transam/varsup.c
+++ b/src/backend/access/transam/varsup.c
@@ -542,31 +542,51 @@ ForceTransactionIdLimitUpdate(void)
 
 
 /*
- * GetNewObjectId -- allocate a new OID
+ * GetNewObjectId -- allocate a new OID (4 bytes)
  *
- * OIDs are generated by a cluster-wide counter.  Since they are only 32 bits
- * wide, counter wraparound will occur eventually, and therefore it is unwise
- * to assume they are unique unless precautions are taken to make them so.
- * Hence, this routine should generally not be used directly.  The only direct
- * callers should be GetNewOidWithIndex() and GetNewRelFileNumber() in
- * catalog/catalog.c.
+ * OIDs are generated by a cluster-wide counter.  The callers of this routine
+ * expect a 32 bit-wide counter, and counter wraparound will occur eventually,
+ * and therefore it is unwise to assume they are unique unless precautions are
+ * taken to make them so.  This routine should generally not be used directly.
+ * The only direct callers should be GetNewOidWithIndex() and
+ * GetNewRelFileNumber() in catalog/catalog.c.
  */
 Oid
 GetNewObjectId(void)
 {
-	Oid			result;
+	return (Oid) GetNewObjectId8();
+}
+
+/*
+ * GetNewObjectId8 -- allocate a new OID (8 bytes)
+ *
+ * This routine can be called directly if the consumer of the OID allocated
+ * stores the counter in an 8-byte space, where wraparound does not matter.
+ * We still need to care about the wraparound case in the low 32 bits of the
+ * space allocated, GetNewObjectId() expecting OIDs to never be allocated
+ * up to FirstNormalObjectId.
+ */
+Oid8
+GetNewObjectId8(void)
+{
+	Oid8		result;
+	Oid			nextoid_lo;
+	uint32		nextoid_hi;
 
 	/* safety check, we should never get this far in a HS standby */
 	if (RecoveryInProgress())
 		elog(ERROR, "cannot assign OIDs during recovery");
 
 	LWLockAcquire(OidGenLock, LW_EXCLUSIVE);
+	nextoid_lo = (Oid) TransamVariables->nextOid;
+	nextoid_hi = (uint32) (TransamVariables->nextOid >> 32);
 
 	/*
-	 * Check for wraparound of the OID counter.  We *must* not return 0
-	 * (InvalidOid), and in normal operation we mustn't return anything below
-	 * FirstNormalObjectId since that range is reserved for initdb (see
-	 * IsCatalogRelationOid()).  Note we are relying on unsigned comparison.
+	 * Check for wraparound of the OID counter in its lower 4 bytes.  We
+	 * *must* not return 0 (InvalidOid), and in normal operation we
+	 * mustn't return anything below FirstNormalObjectId since that range
+	 * is reserved for initdb (see IsCatalogRelationOid()).  Note we are
+	 * relying on unsigned comparison.
 	 *
 	 * During initdb, we start the OID generator at FirstGenbkiObjectId, so we
 	 * only wrap if before that point when in bootstrap or standalone mode.
@@ -576,26 +596,32 @@ GetNewObjectId(void)
 	 * available for automatic assignment during initdb, while ensuring they
 	 * will never conflict with user-assigned OIDs.
 	 */
-	if (TransamVariables->nextOid < ((Oid) FirstNormalObjectId))
+	if (nextoid_lo < ((Oid) FirstNormalObjectId))
 	{
 		if (IsPostmasterEnvironment)
 		{
 			/* wraparound, or first post-initdb assignment, in normal mode */
-			TransamVariables->nextOid = FirstNormalObjectId;
+			nextoid_lo = FirstNormalObjectId;
 			TransamVariables->oidCount = 0;
 		}
 		else
 		{
 			/* we may be bootstrapping, so don't enforce the full range */
-			if (TransamVariables->nextOid < ((Oid) FirstGenbkiObjectId))
+			if (nextoid_lo < ((Oid) FirstGenbkiObjectId))
 			{
 				/* wraparound in standalone mode (unlikely but possible) */
-				TransamVariables->nextOid = FirstNormalObjectId;
+				nextoid_lo = FirstNormalObjectId;
 				TransamVariables->oidCount = 0;
 			}
 		}
 	}
 
+	/*
+	 * Set next OID in its 8-byte space, skipping the first post-init
+	 * assignment.
+	 */
+	TransamVariables->nextOid = ((Oid8) nextoid_hi) << 32 | nextoid_lo;
+
 	/* If we run out of logged for use oids then we must log more */
 	if (TransamVariables->oidCount == 0)
 	{
@@ -620,7 +646,7 @@ GetNewObjectId(void)
  * to the specified value.
  */
 static void
-SetNextObjectId(Oid nextOid)
+SetNextObjectId(Oid8 nextOid)
 {
 	/* Safety check, this is only allowable during initdb */
 	if (IsPostmasterEnvironment)
@@ -630,7 +656,7 @@ SetNextObjectId(Oid nextOid)
 	LWLockAcquire(OidGenLock, LW_EXCLUSIVE);
 
 	if (TransamVariables->nextOid > nextOid)
-		elog(ERROR, "too late to advance OID counter to %u, it is now %u",
+		elog(ERROR, "too late to advance OID counter to " OID8_FORMAT ", it is now " OID8_FORMAT,
 			 nextOid, TransamVariables->nextOid);
 
 	TransamVariables->nextOid = nextOid;
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 22d0a2e8c3a6..21ab4be8eed9 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -8090,10 +8090,10 @@ KeepLogSeg(XLogRecPtr recptr, XLogSegNo *logSegNo)
  * Write a NEXTOID log record
  */
 void
-XLogPutNextOid(Oid nextOid)
+XLogPutNextOid(Oid8 nextOid)
 {
 	XLogBeginInsert();
-	XLogRegisterData(&nextOid, sizeof(Oid));
+	XLogRegisterData(&nextOid, sizeof(Oid8));
 	(void) XLogInsert(RM_XLOG_ID, XLOG_NEXTOID);
 
 	/*
@@ -8316,7 +8316,7 @@ xlog_redo(XLogReaderState *record)
 
 	if (info == XLOG_NEXTOID)
 	{
-		Oid			nextOid;
+		Oid8		nextOid;
 
 		/*
 		 * We used to try to take the maximum of TransamVariables->nextOid and
@@ -8325,7 +8325,7 @@ xlog_redo(XLogReaderState *record)
 		 * anyway, better to just believe the record exactly.  We still take
 		 * OidGenLock while setting the variable, just in case.
 		 */
-		memcpy(&nextOid, XLogRecGetData(record), sizeof(Oid));
+		memcpy(&nextOid, XLogRecGetData(record), sizeof(Oid8));
 		LWLockAcquire(OidGenLock, LW_EXCLUSIVE);
 		TransamVariables->nextOid = nextOid;
 		TransamVariables->oidCount = 0;
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index 21b8f179ba0d..39f81fda309a 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -882,7 +882,7 @@ InitWalRecovery(ControlFileData *ControlFile, bool *wasShutdown_ptr,
 							LSN_FORMAT_ARGS(checkPoint.redo),
 							wasShutdown ? "true" : "false"));
 	ereport(DEBUG1,
-			(errmsg_internal("next transaction ID: " UINT64_FORMAT "; next OID: %u",
+			(errmsg_internal("next transaction ID: " UINT64_FORMAT "; next OID: " OID8_FORMAT,
 							 U64FromFullTransactionId(checkPoint.nextXid),
 							 checkPoint.nextOid)));
 	ereport(DEBUG1,
diff --git a/src/bin/pg_controldata/pg_controldata.c b/src/bin/pg_controldata/pg_controldata.c
index 30ad46912e18..a6ea1bd5a210 100644
--- a/src/bin/pg_controldata/pg_controldata.c
+++ b/src/bin/pg_controldata/pg_controldata.c
@@ -267,7 +267,7 @@ main(int argc, char *argv[])
 	printf(_("Latest checkpoint's NextXID:          %u:%u\n"),
 		   EpochFromFullTransactionId(ControlFile->checkPointCopy.nextXid),
 		   XidFromFullTransactionId(ControlFile->checkPointCopy.nextXid));
-	printf(_("Latest checkpoint's NextOID:          %u\n"),
+	printf(_("Latest checkpoint's NextOID:          " OID8_FORMAT "\n"),
 		   ControlFile->checkPointCopy.nextOid);
 	printf(_("Latest checkpoint's NextMultiXactId:  %u\n"),
 		   ControlFile->checkPointCopy.nextMulti);
diff --git a/src/bin/pg_resetwal/pg_resetwal.c b/src/bin/pg_resetwal/pg_resetwal.c
index 8d5d9805279a..54aba7aa6dbc 100644
--- a/src/bin/pg_resetwal/pg_resetwal.c
+++ b/src/bin/pg_resetwal/pg_resetwal.c
@@ -69,7 +69,7 @@ static TransactionId set_oldest_xid = 0;
 static TransactionId set_xid = 0;
 static TransactionId set_oldest_commit_ts_xid = 0;
 static TransactionId set_newest_commit_ts_xid = 0;
-static Oid	set_oid = 0;
+static Oid8 set_oid = 0;
 static bool mxid_given = false;
 static MultiXactId set_mxid = 0;
 static bool mxoff_given = false;
@@ -229,7 +229,7 @@ main(int argc, char *argv[])
 
 			case 'o':
 				errno = 0;
-				set_oid = strtoul(optarg, &endptr, 0);
+				set_oid = strtou64(optarg, &endptr, 0);
 				if (endptr == optarg || *endptr != '\0' || errno != 0)
 				{
 					pg_log_error("invalid argument for option %s", "-o");
@@ -745,7 +745,7 @@ PrintControlValues(bool guessed)
 	printf(_("Latest checkpoint's NextXID:          %u:%u\n"),
 		   EpochFromFullTransactionId(ControlFile.checkPointCopy.nextXid),
 		   XidFromFullTransactionId(ControlFile.checkPointCopy.nextXid));
-	printf(_("Latest checkpoint's NextOID:          %u\n"),
+	printf(_("Latest checkpoint's NextOID:          " OID8_FORMAT "\n"),
 		   ControlFile.checkPointCopy.nextOid);
 	printf(_("Latest checkpoint's NextMultiXactId:  %u\n"),
 		   ControlFile.checkPointCopy.nextMulti);
@@ -831,7 +831,7 @@ PrintNewControlValues(void)
 
 	if (set_oid != 0)
 	{
-		printf(_("NextOID:                              %u\n"),
+		printf(_("NextOID:                              " OID8_FORMAT "\n"),
 			   ControlFile.checkPointCopy.nextOid);
 	}
 
@@ -1204,7 +1204,7 @@ usage(void)
 	printf(_("  -e, --epoch=XIDEPOCH             set next transaction ID epoch\n"));
 	printf(_("  -l, --next-wal-file=WALFILE      set minimum starting location for new WAL\n"));
 	printf(_("  -m, --multixact-ids=MXID,MXID    set next and oldest multitransaction ID\n"));
-	printf(_("  -o, --next-oid=OID               set next OID\n"));
+	printf(_("  -o, --next-oid=OID8              set next OID (8 bytes)\n"));
 	printf(_("  -O, --multixact-offset=OFFSET    set next multitransaction offset\n"));
 	printf(_("  -u, --oldest-transaction-id=XID  set oldest transaction ID\n"));
 	printf(_("  -x, --next-transaction-id=XID    set next transaction ID\n"));
diff --git a/doc/src/sgml/ref/pg_resetwal.sgml b/doc/src/sgml/ref/pg_resetwal.sgml
index 2c019c2aac6e..b03251cedbbe 100644
--- a/doc/src/sgml/ref/pg_resetwal.sgml
+++ b/doc/src/sgml/ref/pg_resetwal.sgml
@@ -279,11 +279,11 @@ PostgreSQL documentation
    </varlistentry>
 
    <varlistentry>
-    <term><option>-o <replaceable class="parameter">oid</replaceable></option></term>
-    <term><option>--next-oid=<replaceable class="parameter">oid</replaceable></option></term>
+    <term><option>-o <replaceable class="parameter">oid8</replaceable></option></term>
+    <term><option>--next-oid=<replaceable class="parameter">oid8</replaceable></option></term>
     <listitem>
      <para>
-      Manually set the next OID.
+      Manually set the next OID (8 bytes).
      </para>
 
      <para>
-- 
2.51.0

v8-0012-Add-relation-option-toast_value_type.patchtext/x-diff; charset=us-asciiDownload
From 0e5484f9ffae0dc35aa5c137cd06466012bbf59e Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 12:54:58 +0900
Subject: [PATCH v8 12/15] Add relation option toast_value_type

This relation option gives the possibility to define the attribute type
that can be used for chunk_id in a TOAST table when it is created
initially.  This parameter has no effect if a TOAST table exists, even
after it is modified later on, even on rewrites.

This can be set only to "oid" currently, and will be expanded with a
second mode later.

Note: perhaps it would make sense to introduce that only when support
for 8-byte OID values are added to TOAST, the split is here to ease
review.
---
 src/include/utils/rel.h                | 17 +++++++++++++++++
 src/backend/access/common/reloptions.c | 21 +++++++++++++++++++++
 src/backend/catalog/toasting.c         |  6 ++++++
 src/bin/psql/tab-complete.in.c         |  1 +
 doc/src/sgml/ref/create_table.sgml     | 18 ++++++++++++++++++
 5 files changed, 63 insertions(+)

diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 80286076a111..fcd59099535f 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -338,11 +338,20 @@ typedef enum StdRdOptIndexCleanup
 	STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON,
 } StdRdOptIndexCleanup;
 
+/* StdRdOptions->toast_value_type values */
+typedef enum StdRdOptToastValueType
+{
+	STDRD_OPTION_TOAST_VALUE_TYPE_INVALID = 0,
+	STDRD_OPTION_TOAST_VALUE_TYPE_OID,
+} StdRdOptToastValueType;
+
 typedef struct StdRdOptions
 {
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	int			fillfactor;		/* page fill factor in percent (0..100) */
 	int			toast_tuple_target; /* target for tuple toasting */
+	StdRdOptToastValueType	toast_value_type;	/* type assigned to chunk_id
+												 * at toast table creation */
 	AutoVacOpts autovacuum;		/* autovacuum-related options */
 	bool		user_catalog_table; /* use as an additional catalog relation */
 	int			parallel_workers;	/* max number of parallel workers */
@@ -368,6 +377,14 @@ typedef struct StdRdOptions
 	((relation)->rd_options ? \
 	 ((StdRdOptions *) (relation)->rd_options)->toast_tuple_target : (defaulttarg))
 
+/*
+ * RelationGetToastValueType
+ *		Returns the relation's toast_value_type.  Note multiple eval of argument!
+ */
+#define RelationGetToastValueType(relation, defaulttarg) \
+	((relation)->rd_options ? \
+	 ((StdRdOptions *) (relation)->rd_options)->toast_value_type : defaulttarg)
+
 /*
  * RelationGetFillFactor
  *		Returns the relation's fillfactor.  Note multiple eval of argument!
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 9e288dfecbfd..b7a400d8c3e5 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -525,6 +525,14 @@ static relopt_enum_elt_def viewCheckOptValues[] =
 	{(const char *) NULL}		/* list terminator */
 };
 
+/* values from StdRdOptToastValueType */
+static relopt_enum_elt_def StdRdOptToastValueTypes[] =
+{
+	/* no value for INVALID */
+	{"oid", STDRD_OPTION_TOAST_VALUE_TYPE_OID},
+	{(const char *) NULL}		/* list terminator */
+};
+
 static relopt_enum enumRelOpts[] =
 {
 	{
@@ -538,6 +546,17 @@ static relopt_enum enumRelOpts[] =
 		STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO,
 		gettext_noop("Valid values are \"on\", \"off\", and \"auto\".")
 	},
+	{
+		{
+			"toast_value_type",
+			"Controls the attribute type of chunk_id at toast table creation",
+			RELOPT_KIND_HEAP,
+			ShareUpdateExclusiveLock
+		},
+		StdRdOptToastValueTypes,
+		STDRD_OPTION_TOAST_VALUE_TYPE_OID,
+		gettext_noop("Valid values are \"oid\".")
+	},
 	{
 		{
 			"buffering",
@@ -1909,6 +1928,8 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
 		offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_analyze_min_duration)},
 		{"toast_tuple_target", RELOPT_TYPE_INT,
 		offsetof(StdRdOptions, toast_tuple_target)},
+		{"toast_value_type", RELOPT_TYPE_ENUM,
+		offsetof(StdRdOptions, toast_value_type)},
 		{"autovacuum_vacuum_cost_delay", RELOPT_TYPE_REAL,
 		offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_delay)},
 		{"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c
index f1d76d8acd51..545983b5be9d 100644
--- a/src/backend/catalog/toasting.c
+++ b/src/backend/catalog/toasting.c
@@ -158,9 +158,15 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	 */
 	if (!IsBinaryUpgrade)
 	{
+		StdRdOptToastValueType value_type;
+
 		/* Normal mode, normal check */
 		if (!needs_toast_table(rel))
 			return false;
+
+		value_type = RelationGetToastValueType(rel, STDRD_OPTION_TOAST_VALUE_TYPE_OID);
+		if (value_type == STDRD_OPTION_TOAST_VALUE_TYPE_OID)
+			toast_chunkid_typid = OIDOID;
 	}
 	else
 	{
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 20d7a65c614e..b636a431d392 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -1453,6 +1453,7 @@ static const char *const table_storage_parameters[] = {
 	"toast.vacuum_max_eager_freeze_failure_rate",
 	"toast.vacuum_truncate",
 	"toast_tuple_target",
+	"toast_value_type",
 	"user_catalog_table",
 	"vacuum_index_cleanup",
 	"vacuum_max_eager_freeze_failure_rate",
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 6557c5cffd88..428f09044110 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1632,6 +1632,24 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     </listitem>
    </varlistentry>
 
+   <varlistentry id="reloption-toast-value-type" xreflabel="toast_value_type">
+    <term><literal>toast_value_type</literal> (<type>enum</type>)
+    <indexterm>
+     <primary><varname>toast_value_type</varname> storage parameter</primary>
+    </indexterm>
+    </term>
+    <listitem>
+     <para>
+      The toast_value_type specifies the attribute type of
+      <literal>chunk_id</literal> used when initially creating  a toast
+      relation for this table.
+      By default this parameter is <literal>oid</literal>, to assign
+      <type>oid</type> as attribute type to <literal>chunk_id</literal>.
+      This parameter cannot be set for TOAST tables.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="reloption-parallel-workers" xreflabel="parallel_workers">
     <term><literal>parallel_workers</literal> (<type>integer</type>)
      <indexterm>
-- 
2.51.0

v8-0013-Add-support-for-oid8-TOAST-values.patchtext/x-diff; charset=us-asciiDownload
From 4a6d867714103bf84cd997911b95c31f7a469ce0 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 17:06:10 +0900
Subject: [PATCH v8 13/15] Add support for oid8 TOAST values

This commit adds the possibility to define TOAST tables with oid8 as
value ID, based on the reloption toast_value_type.  All the external
TOAST pointers still rely on varatt_external and a single vartag, with
all the values inserted in the bigint TOAST tables fed from the existing
OID value generator.  This will be changed in an upcoming patch that
adds more vartag_external types and its associated structures, with the
code being able to use a different external TOAST pointer depending on
the attribute type of chunk_id in TOAST relations.

All the changes done here are mechanical, with all the TOAST code able
to do chunk ID lookups based on the two types now supported.

XXX: Catalog version bump required.
---
 src/include/catalog/pg_opclass.dat          |  3 +-
 src/include/utils/rel.h                     |  1 +
 src/backend/access/common/reloptions.c      |  1 +
 src/backend/access/common/toast_internals.c | 94 +++++++++++++++------
 src/backend/access/heap/heaptoast.c         | 20 ++++-
 src/backend/catalog/toasting.c              | 24 +++++-
 doc/src/sgml/ref/create_table.sgml          |  2 +
 doc/src/sgml/storage.sgml                   |  7 +-
 contrib/amcheck/verify_heapam.c             | 19 ++++-
 9 files changed, 131 insertions(+), 40 deletions(-)

diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index c0de88fabc49..b8f2bc2d69c4 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -179,7 +179,8 @@
   opcintype => 'xid8' },
 { opcmethod => 'hash', opcname => 'oid8_ops', opcfamily => 'hash/oid8_ops',
   opcintype => 'oid8' },
-{ opcmethod => 'btree', opcname => 'oid8_ops', opcfamily => 'btree/oid8_ops',
+{ oid => '8285', oid_symbol => 'OID8_BTREE_OPS_OID',
+  opcmethod => 'btree', opcname => 'oid8_ops', opcfamily => 'btree/oid8_ops',
   opcintype => 'oid8' },
 { opcmethod => 'hash', opcname => 'cid_ops', opcfamily => 'hash/cid_ops',
   opcintype => 'cid' },
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index fcd59099535f..c04f7c1f87fb 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -343,6 +343,7 @@ typedef enum StdRdOptToastValueType
 {
 	STDRD_OPTION_TOAST_VALUE_TYPE_INVALID = 0,
 	STDRD_OPTION_TOAST_VALUE_TYPE_OID,
+	STDRD_OPTION_TOAST_VALUE_TYPE_OID8,
 } StdRdOptToastValueType;
 
 typedef struct StdRdOptions
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index b7a400d8c3e5..616872ddfb45 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -530,6 +530,7 @@ static relopt_enum_elt_def StdRdOptToastValueTypes[] =
 {
 	/* no value for INVALID */
 	{"oid", STDRD_OPTION_TOAST_VALUE_TYPE_OID},
+	{"oid8", STDRD_OPTION_TOAST_VALUE_TYPE_OID8},
 	{(const char *) NULL}		/* list terminator */
 };
 
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 3f597d5b545c..5c49ecbcec2e 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -26,6 +26,7 @@
 #include "utils/fmgroids.h"
 #include "utils/rel.h"
 #include "utils/snapmgr.h"
+#include "utils/lsyscache.h"
 
 static bool toastrel_valueid_exists(Relation toastrel, Oid8 valueid);
 static bool toastid_valueid_exists(Oid toastrelid, Oid8 valueid);
@@ -134,8 +135,10 @@ toast_save_datum(Relation rel, Datum value,
 	int			validIndex;
 	const toast_external_info *info;
 	uint8		tag = VARTAG_INDIRECT;	/* init value does not matter */
+	Oid			toast_typid = get_atttype(rel->rd_rel->reltoastrelid, 1);
 
 	Assert(!VARATT_IS_EXTERNAL(dval));
+	Assert(OidIsValid(toast_typid));
 
 	/*
 	 * Open the toast relation and its indexes.  We can use the index to check
@@ -216,24 +219,32 @@ toast_save_datum(Relation rel, Datum value,
 		toast_pointer.toastrelid = RelationGetRelid(toastrel);
 
 	/*
-	 * Choose an OID to use as the value ID for this toast value.
+	 * Choose a new value to use as the value ID for this toast value, be it
+	 * for OID or int8-based TOAST relations.
 	 *
-	 * Normally we just choose an unused OID within the toast table.  But
+	 * Normally we just choose an unused value within the toast table.  But
 	 * during table-rewriting operations where we are preserving an existing
-	 * toast table OID, we want to preserve toast value OIDs too.  So, if
+	 * toast table OID, we want to preserve toast value IDs too.  So, if
 	 * rd_toastoid is set and we had a prior external value from that same
 	 * toast table, re-use its value ID.  If we didn't have a prior external
 	 * value (which is a corner case, but possible if the table's attstorage
 	 * options have been changed), we have to pick a value ID that doesn't
-	 * conflict with either new or existing toast value OIDs.
+	 * conflict with either new or existing toast value IDs.  If the TOAST
+	 * table uses 8-byte value IDs, we should not really care much about
+	 * that.
 	 */
 	if (!OidIsValid(rel->rd_toastoid))
 	{
 		/* normal case: just choose an unused OID */
-		toast_pointer.valueid =
-			GetNewOidWithIndex(toastrel,
-							   RelationGetRelid(toastidxs[validIndex]),
-							   (AttrNumber) 1);
+		if (toast_typid == OIDOID)
+			toast_pointer.valueid =
+				GetNewOidWithIndex(toastrel,
+								   RelationGetRelid(toastidxs[validIndex]),
+								   (AttrNumber) 1);
+		else if (toast_typid == OID8OID)
+			toast_pointer.valueid = GetNewObjectId8();
+		else
+			Assert(false);
 	}
 	else
 	{
@@ -279,17 +290,22 @@ toast_save_datum(Relation rel, Datum value,
 		if (toast_pointer.valueid == InvalidOid8)
 		{
 			/*
-			 * new value; must choose an OID that doesn't conflict in either
-			 * old or new toast table
+			 * new value; must choose a value that doesn't conflict in either
+			 * old or new toast table.
 			 */
-			do
+			if (toast_typid == OIDOID)
 			{
-				toast_pointer.valueid =
-					GetNewOidWithIndex(toastrel,
-									   RelationGetRelid(toastidxs[validIndex]),
-									   (AttrNumber) 1);
-			} while (toastid_valueid_exists(rel->rd_toastoid,
-											toast_pointer.valueid));
+				do
+				{
+					toast_pointer.valueid =
+						GetNewOidWithIndex(toastrel,
+										   RelationGetRelid(toastidxs[validIndex]),
+										   (AttrNumber) 1);
+				} while (toastid_valueid_exists(rel->rd_toastoid,
+												toast_pointer.valueid));
+			}
+			else if (toast_typid == OID8OID)
+				toast_pointer.valueid = GetNewObjectId8();
 		}
 	}
 
@@ -327,7 +343,10 @@ toast_save_datum(Relation rel, Datum value,
 		/*
 		 * Build a tuple and store it
 		 */
-		t_values[0] = ObjectIdGetDatum(toast_pointer.valueid);
+		if (toast_typid == OIDOID)
+			t_values[0] = ObjectIdGetDatum(toast_pointer.valueid);
+		else if (toast_typid == OID8OID)
+			t_values[0] = ObjectId8GetDatum(toast_pointer.valueid);
 		t_values[1] = Int32GetDatum(chunk_seq++);
 		SET_VARSIZE(&chunk_data, chunk_size + VARHDRSZ);
 		memcpy(VARDATA(&chunk_data), data_p, chunk_size);
@@ -406,6 +425,7 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	HeapTuple	toasttup;
 	int			num_indexes;
 	int			validIndex;
+	Oid			toast_typid;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		return;
@@ -417,6 +437,8 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	 * Open the toast relation and its indexes
 	 */
 	toastrel = table_open(toast_pointer.toastrelid, RowExclusiveLock);
+	toast_typid = TupleDescAttr(toastrel->rd_att, 0)->atttypid;
+	Assert(toast_typid == OIDOID || toast_typid == OID8OID);
 
 	/* Fetch valid relation used for process */
 	validIndex = toast_open_indexes(toastrel,
@@ -427,10 +449,18 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	/*
 	 * Setup a scan key to find chunks with matching va_valueid
 	 */
-	ScanKeyInit(&toastkey,
-				(AttrNumber) 1,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(toast_pointer.valueid));
+	if (toast_typid == OIDOID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(toast_pointer.valueid));
+	else if (toast_typid == OID8OID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OID8EQ,
+					ObjectId8GetDatum(toast_pointer.valueid));
+	else
+		Assert(false);
 
 	/*
 	 * Find all the chunks.  (We don't actually care whether we see them in
@@ -477,6 +507,7 @@ toastrel_valueid_exists(Relation toastrel, Oid8 valueid)
 	int			num_indexes;
 	int			validIndex;
 	Relation   *toastidxs;
+	Oid			toast_typid;
 
 	/* Fetch a valid index relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -484,13 +515,24 @@ toastrel_valueid_exists(Relation toastrel, Oid8 valueid)
 									&toastidxs,
 									&num_indexes);
 
+	toast_typid = TupleDescAttr(toastrel->rd_att, 0)->atttypid;
+	Assert(toast_typid == OIDOID || toast_typid == OID8OID);
+
 	/*
 	 * Setup a scan key to find chunks with matching va_valueid
 	 */
-	ScanKeyInit(&toastkey,
-				(AttrNumber) 1,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(valueid));
+	if (toast_typid == OIDOID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(valueid));
+	else if (toast_typid == OID8OID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OID8EQ,
+					ObjectId8GetDatum(valueid));
+	else
+		Assert(false);
 
 	/*
 	 * Is there any such chunk?
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index 2242fc2f75bc..6434a424ddc3 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -654,6 +654,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 	int32		max_chunk_size;
 	const toast_external_info *info;
 	uint8		tag = VARTAG_INDIRECT;	/* init value does not matter */
+	Oid			toast_typid;
 
 	/* Look for the valid index of toast relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -667,16 +668,27 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 
 	max_chunk_size = info->maximum_chunk_size;
 
+	toast_typid = TupleDescAttr(toastrel->rd_att, 0)->atttypid;
+	Assert(toast_typid == OIDOID || toast_typid == OID8OID);
+
 	totalchunks = ((attrsize - 1) / max_chunk_size) + 1;
 	startchunk = sliceoffset / max_chunk_size;
 	endchunk = (sliceoffset + slicelength - 1) / max_chunk_size;
 	Assert(endchunk <= totalchunks);
 
 	/* Set up a scan key to fetch from the index. */
-	ScanKeyInit(&toastkey[0],
-				(AttrNumber) 1,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(valueid));
+	if (toast_typid == OIDOID)
+		ScanKeyInit(&toastkey[0],
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(valueid));
+	else if (toast_typid == OID8OID)
+		ScanKeyInit(&toastkey[0],
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OID8EQ,
+					ObjectId8GetDatum(valueid));
+	else
+		Assert(false);
 
 	/*
 	 * No additional condition if fetching all chunks. Otherwise, use an
diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c
index 545983b5be9d..2288311b22a4 100644
--- a/src/backend/catalog/toasting.c
+++ b/src/backend/catalog/toasting.c
@@ -31,6 +31,7 @@
 #include "nodes/makefuncs.h"
 #include "utils/fmgroids.h"
 #include "utils/rel.h"
+#include "utils/lsyscache.h"
 #include "utils/syscache.h"
 
 static void CheckAndCreateToastTable(Oid relOid, Datum reloptions,
@@ -167,6 +168,8 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 		value_type = RelationGetToastValueType(rel, STDRD_OPTION_TOAST_VALUE_TYPE_OID);
 		if (value_type == STDRD_OPTION_TOAST_VALUE_TYPE_OID)
 			toast_chunkid_typid = OIDOID;
+		else if (value_type == STDRD_OPTION_TOAST_VALUE_TYPE_OID8)
+			toast_chunkid_typid = OID8OID;
 	}
 	else
 	{
@@ -199,7 +202,8 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("toast chunk_id type not set while in binary upgrade mode")));
-		if (binary_upgrade_next_toast_chunk_id_typoid != OIDOID)
+		if (binary_upgrade_next_toast_chunk_id_typoid != OIDOID &&
+			binary_upgrade_next_toast_chunk_id_typoid != OID8OID)
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("cannot support toast chunk_id type %u in binary upgrade mode",
@@ -224,6 +228,19 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	snprintf(toast_idxname, sizeof(toast_idxname),
 			 "pg_toast_%u_index", relOid);
 
+	/*
+	 * Special case here.  If OIDOldToast is defined, we need to rely on the
+	 * existing table for the job because we do not want to create an
+	 * inconsistent relation that would conflict with the parent and break
+	 * the world.
+	 */
+	if (OidIsValid(OIDOldToast))
+	{
+		toast_chunkid_typid = get_atttype(OIDOldToast, 1);
+		if (!OidIsValid(toast_chunkid_typid))
+			elog(ERROR, "cache lookup failed for relation %u", OIDOldToast);
+	}
+
 	/* this is pretty painful...  need a tuple descriptor */
 	tupdesc = CreateTemplateTupleDesc(3);
 	TupleDescInitEntry(tupdesc, (AttrNumber) 1,
@@ -336,7 +353,10 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	collationIds[0] = InvalidOid;
 	collationIds[1] = InvalidOid;
 
-	opclassIds[0] = OID_BTREE_OPS_OID;
+	if (toast_chunkid_typid == OIDOID)
+		opclassIds[0] = OID_BTREE_OPS_OID;
+	else if (toast_chunkid_typid == OID8OID)
+		opclassIds[0] = OID8_BTREE_OPS_OID;
 	opclassIds[1] = INT4_BTREE_OPS_OID;
 
 	coloptions[0] = 0;
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 428f09044110..88541169fcf7 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1645,6 +1645,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       relation for this table.
       By default this parameter is <literal>oid</literal>, to assign
       <type>oid</type> as attribute type to <literal>chunk_id</literal>.
+      This parameter can be set to <type>oid8</type> to use <type>oid8</type>
+      as attribute type for <literal>chunk_id</literal>.
       This parameter cannot be set for TOAST tables.
      </para>
     </listitem>
diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml
index 67600fd974d7..afddf663fec5 100644
--- a/doc/src/sgml/storage.sgml
+++ b/doc/src/sgml/storage.sgml
@@ -421,14 +421,15 @@ most <symbol>TOAST_OID_MAX_CHUNK_SIZE</symbol> bytes (by default this value is c
 so that four chunk rows will fit on a page, making it about 2000 bytes).
 Each chunk is stored as a separate row in the <acronym>TOAST</acronym> table
 belonging to the owning table.  Every
-<acronym>TOAST</acronym> table has the columns <structfield>chunk_id</structfield> (an OID
-identifying the particular <acronym>TOAST</acronym>ed value),
+<acronym>TOAST</acronym> table has the columns
+<structfield>chunk_id</structfield> (an OID or an 8-byte integer identifying
+the particular <acronym>TOAST</acronym>ed value),
 <structfield>chunk_seq</structfield> (a sequence number for the chunk within its value),
 and <structfield>chunk_data</structfield> (the actual data of the chunk).  A unique index
 on <structfield>chunk_id</structfield> and <structfield>chunk_seq</structfield> provides fast
 retrieval of the values.  A pointer datum representing an out-of-line on-disk
 <acronym>TOAST</acronym>ed value therefore needs to store the OID of the
-<acronym>TOAST</acronym> table in which to look and the OID of the specific value
+<acronym>TOAST</acronym> table in which to look and the specific value
 (its <structfield>chunk_id</structfield>).  For convenience, pointer datums also store the
 logical datum size (original uncompressed data length), physical stored size
 (different if compression was applied), and the compression method used, if
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 9cf3c081bf01..143e6baa35cf 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1880,6 +1880,9 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	int32		last_chunk_seq;
 	Oid8		toast_valueid;
 	int32		max_chunk_size;
+	Oid			toast_typid;
+
+	toast_typid = TupleDescAttr(ctx->toast_rel->rd_att, 0)->atttypid;
 
 	extsize = ta->toast_pointer.extsize;
 
@@ -1889,10 +1892,18 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	/*
 	 * Setup a scan key to find chunks in toast table with matching va_valueid
 	 */
-	ScanKeyInit(&toastkey,
-				(AttrNumber) 1,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(ta->toast_pointer.valueid));
+	if (toast_typid == OIDOID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(ta->toast_pointer.valueid));
+	else if (toast_typid == OID8OID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OID8EQ,
+					ObjectId8GetDatum(ta->toast_pointer.valueid));
+	else
+		Assert(false);
 
 	/*
 	 * Check if any chunks for this toasted object exist in the toast table,
-- 
2.51.0

v8-0014-Add-tests-for-TOAST-relations-with-bigint-as-valu.patchtext/x-diff; charset=us-asciiDownload
From 4f17485a7fee84fc52a33d1425717555625d41b2 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 13:43:19 +0900
Subject: [PATCH v8 14/15] Add tests for TOAST relations with bigint as value
 type

This adds coverage for relations created with default_toast_type =
'int8', for external TOAST pointers both compressed and uncompressed.
---
 src/test/regress/expected/strings.out     | 231 ++++++++++++++++++----
 src/test/regress/expected/type_sanity.out |   6 +-
 src/test/regress/sql/strings.sql          | 134 +++++++++----
 src/test/regress/sql/type_sanity.sql      |   6 +-
 4 files changed, 296 insertions(+), 81 deletions(-)

diff --git a/src/test/regress/expected/strings.out b/src/test/regress/expected/strings.out
index 727304f60e74..ed1921b32280 100644
--- a/src/test/regress/expected/strings.out
+++ b/src/test/regress/expected/strings.out
@@ -2012,21 +2012,37 @@ SELECT text 'text' || varchar ' and varchar' AS "Concat text to varchar";
 (1 row)
 
 --
--- test substr with toasted text values
+-- Test substr with toasted text values, for all types of TOAST relations
+-- supported.
 --
-CREATE TABLE toasttest(f1 text);
-insert into toasttest values(repeat('1234567890',10000));
-insert into toasttest values(repeat('1234567890',10000));
+CREATE TABLE toasttest_oid(f1 text) with (toast_value_type = 'oid');
+CREATE TABLE toasttest_oid8(f1 text) with (toast_value_type = 'oid8');
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid8 values(repeat('1234567890',10000));
+insert into toasttest_oid8 values(repeat('1234567890',10000));
 --
 -- Ensure that some values are uncompressed, to test the faster substring
 -- operation used in that case
 --
-alter table toasttest alter column f1 set storage external;
-insert into toasttest values(repeat('1234567890',10000));
-insert into toasttest values(repeat('1234567890',10000));
+alter table toasttest_oid alter column f1 set storage external;
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+alter table toasttest_oid8 alter column f1 set storage external;
+insert into toasttest_oid8 values(repeat('1234567890',10000));
+insert into toasttest_oid8 values(repeat('1234567890',10000));
 -- If the starting position is zero or less, then return from the start of the string
 -- adjusting the length to be consistent with the "negative start" per SQL.
-SELECT substr(f1, -1, 5) from toasttest;
+SELECT substr(f1, -1, 5) from toasttest_oid;
+ substr 
+--------
+ 123
+ 123
+ 123
+ 123
+(4 rows)
+
+SELECT substr(f1, -1, 5) from toasttest_oid8;
  substr 
 --------
  123
@@ -2036,11 +2052,22 @@ SELECT substr(f1, -1, 5) from toasttest;
 (4 rows)
 
 -- If the length is less than zero, an ERROR is thrown.
-SELECT substr(f1, 5, -1) from toasttest;
+SELECT substr(f1, 5, -1) from toasttest_oid;
+ERROR:  negative substring length not allowed
+SELECT substr(f1, 5, -1) from toasttest_oid8;
 ERROR:  negative substring length not allowed
 -- If no third argument (length) is provided, the length to the end of the
 -- string is assumed.
-SELECT substr(f1, 99995) from toasttest;
+SELECT substr(f1, 99995) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995) from toasttest_oid8;
  substr 
 --------
  567890
@@ -2051,7 +2078,7 @@ SELECT substr(f1, 99995) from toasttest;
 
 -- If start plus length is > string length, the result is truncated to
 -- string length
-SELECT substr(f1, 99995, 10) from toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
  substr 
 --------
  567890
@@ -2060,50 +2087,105 @@ SELECT substr(f1, 99995, 10) from toasttest;
  567890
 (4 rows)
 
-TRUNCATE TABLE toasttest;
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
+SELECT substr(f1, 99995, 10) from toasttest_oid8;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+-- TRUNCATE cases for TOAST relations with OID values.
+TRUNCATE TABLE toasttest_oid;
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
 -- expect >0 blocks
 SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
-  FROM pg_class where relname = 'toasttest';
+  FROM pg_class where relname = 'toasttest_oid';
  is_empty 
 ----------
  f
 (1 row)
 
-TRUNCATE TABLE toasttest;
-ALTER TABLE toasttest set (toast_tuple_target = 4080);
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
+TRUNCATE TABLE toasttest_oid;
+ALTER TABLE toasttest_oid set (toast_tuple_target = 4080);
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
 -- expect 0 blocks
 SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
-  FROM pg_class where relname = 'toasttest';
+  FROM pg_class where relname = 'toasttest_oid';
  is_empty 
 ----------
  t
 (1 row)
 
-DROP TABLE toasttest;
+DROP TABLE toasttest_oid;
+-- TRUNCATE cases for TOAST relation with int8 values.
+TRUNCATE TABLE toasttest_oid8;
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+-- expect >0 blocks
+SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
+  FROM pg_class where relname = 'toasttest_oid8';
+ is_empty 
+----------
+ f
+(1 row)
+
+TRUNCATE TABLE toasttest_oid8;
+ALTER TABLE toasttest_oid8 set (toast_tuple_target = 4080);
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+-- expect 0 blocks
+SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
+  FROM pg_class where relname = 'toasttest_oid8';
+ is_empty 
+----------
+ t
+(1 row)
+
+DROP TABLE toasttest_oid8;
 --
--- test substr with toasted bytea values
+-- test substr with toasted bytea values, for all types of TOAST relations
+-- supported. Do not drop these two relations, for pg_upgrade.
 --
-CREATE TABLE toasttest(f1 bytea);
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
+CREATE TABLE toasttest_oid(f1 bytea) WITH (toast_value_type = 'oid');
+CREATE TABLE toasttest_oid8(f1 bytea) WITH (toast_value_type = 'oid8');
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
 --
 -- Ensure that some values are uncompressed, to test the faster substring
 -- operation used in that case
 --
-alter table toasttest alter column f1 set storage external;
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
+alter table toasttest_oid alter column f1 set storage external;
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+alter table toasttest_oid8 alter column f1 set storage external;
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
 -- If the starting position is zero or less, then return from the start of the string
 -- adjusting the length to be consistent with the "negative start" per SQL.
-SELECT substr(f1, -1, 5) from toasttest;
+SELECT substr(f1, -1, 5) from toasttest_oid;
+ substr 
+--------
+ 123
+ 123
+ 123
+ 123
+(4 rows)
+
+SELECT substr(f1, -1, 5) from toasttest_oid8;
  substr 
 --------
  123
@@ -2113,11 +2195,22 @@ SELECT substr(f1, -1, 5) from toasttest;
 (4 rows)
 
 -- If the length is less than zero, an ERROR is thrown.
-SELECT substr(f1, 5, -1) from toasttest;
+SELECT substr(f1, 5, -1) from toasttest_oid;
+ERROR:  negative substring length not allowed
+SELECT substr(f1, 5, -1) from toasttest_oid8;
 ERROR:  negative substring length not allowed
 -- If no third argument (length) is provided, the length to the end of the
 -- string is assumed.
-SELECT substr(f1, 99995) from toasttest;
+SELECT substr(f1, 99995) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995) from toasttest_oid8;
  substr 
 --------
  567890
@@ -2128,7 +2221,72 @@ SELECT substr(f1, 99995) from toasttest;
 
 -- If start plus length is > string length, the result is truncated to
 -- string length
-SELECT substr(f1, 99995, 10) from toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995, 10) from toasttest_oid8;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+-- A relation rewrite leaves the TOAST value attributes unchanged.
+VACUUM FULL toasttest_oid;
+VACUUM FULL toasttest_oid8;
+SELECT c1.relname, a.atttypid::regtype
+  FROM pg_attribute AS a,
+       pg_class AS c1,
+       pg_class AS c2
+  WHERE
+       c1.relname IN ('toasttest_oid', 'toasttest_oid8') AND
+       c1.reltoastrelid = c2.oid AND
+       a.attrelid = c2.oid AND
+       a.attname = 'chunk_id'
+  ORDER BY c1.relname COLLATE "C";
+    relname     | atttypid 
+----------------+----------
+ toasttest_oid  | oid
+ toasttest_oid8 | oid8
+(2 rows)
+
+-- Check that data slices are still accessible.
+SELECT substr(f1, 99995) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995) from toasttest_oid8;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995, 10) from toasttest_oid8;
  substr 
 --------
  567890
@@ -2137,7 +2295,6 @@ SELECT substr(f1, 99995, 10) from toasttest;
  567890
 (4 rows)
 
-DROP TABLE toasttest;
 -- test internally compressing datums
 -- this tests compressing a datum to a very small size which exercises a
 -- corner case in packed-varlena handling: even though small, the compressed
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 9ddcacec6bf4..88faa57772c3 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -578,15 +578,15 @@ WHERE c1.relnatts != (SELECT count(*) FROM pg_attribute AS a1
 (0 rows)
 
 -- Cross-check against pg_type entry
--- NOTE: we allow attstorage to be 'plain' even when typstorage is not;
--- this is mainly for toast tables.
+-- NOTE: we allow attstorage to be 'plain' or 'external' even when typstorage
+-- is not; this is mainly for toast tables.
 SELECT a1.attrelid, a1.attname, t1.oid, t1.typname
 FROM pg_attribute AS a1, pg_type AS t1
 WHERE a1.atttypid = t1.oid AND
     (a1.attlen != t1.typlen OR
      a1.attalign != t1.typalign OR
      a1.attbyval != t1.typbyval OR
-     (a1.attstorage != t1.typstorage AND a1.attstorage != 'p'));
+     (a1.attstorage != t1.typstorage AND a1.attstorage NOT IN ('e', 'p')));
  attrelid | attname | oid | typname 
 ----------+---------+-----+---------
 (0 rows)
diff --git a/src/test/regress/sql/strings.sql b/src/test/regress/sql/strings.sql
index 88aa4c2983ba..5c97f5e72eb5 100644
--- a/src/test/regress/sql/strings.sql
+++ b/src/test/regress/sql/strings.sql
@@ -572,89 +572,147 @@ SELECT text 'text' || char(20) ' and characters' AS "Concat text to char";
 SELECT text 'text' || varchar ' and varchar' AS "Concat text to varchar";
 
 --
--- test substr with toasted text values
+-- Test substr with toasted text values, for all types of TOAST relations
+-- supported.
 --
-CREATE TABLE toasttest(f1 text);
+CREATE TABLE toasttest_oid(f1 text) with (toast_value_type = 'oid');
+CREATE TABLE toasttest_oid8(f1 text) with (toast_value_type = 'oid8');
 
-insert into toasttest values(repeat('1234567890',10000));
-insert into toasttest values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid8 values(repeat('1234567890',10000));
+insert into toasttest_oid8 values(repeat('1234567890',10000));
 
 --
 -- Ensure that some values are uncompressed, to test the faster substring
 -- operation used in that case
 --
-alter table toasttest alter column f1 set storage external;
-insert into toasttest values(repeat('1234567890',10000));
-insert into toasttest values(repeat('1234567890',10000));
+alter table toasttest_oid alter column f1 set storage external;
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+alter table toasttest_oid8 alter column f1 set storage external;
+insert into toasttest_oid8 values(repeat('1234567890',10000));
+insert into toasttest_oid8 values(repeat('1234567890',10000));
 
 -- If the starting position is zero or less, then return from the start of the string
 -- adjusting the length to be consistent with the "negative start" per SQL.
-SELECT substr(f1, -1, 5) from toasttest;
+SELECT substr(f1, -1, 5) from toasttest_oid;
+SELECT substr(f1, -1, 5) from toasttest_oid8;
 
 -- If the length is less than zero, an ERROR is thrown.
-SELECT substr(f1, 5, -1) from toasttest;
+SELECT substr(f1, 5, -1) from toasttest_oid;
+SELECT substr(f1, 5, -1) from toasttest_oid8;
 
 -- If no third argument (length) is provided, the length to the end of the
 -- string is assumed.
-SELECT substr(f1, 99995) from toasttest;
+SELECT substr(f1, 99995) from toasttest_oid;
+SELECT substr(f1, 99995) from toasttest_oid8;
 
 -- If start plus length is > string length, the result is truncated to
 -- string length
-SELECT substr(f1, 99995, 10) from toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+SELECT substr(f1, 99995, 10) from toasttest_oid8;
 
-TRUNCATE TABLE toasttest;
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
+-- TRUNCATE cases for TOAST relations with OID values.
+TRUNCATE TABLE toasttest_oid;
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
 -- expect >0 blocks
 SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
-  FROM pg_class where relname = 'toasttest';
-
-TRUNCATE TABLE toasttest;
-ALTER TABLE toasttest set (toast_tuple_target = 4080);
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
+  FROM pg_class where relname = 'toasttest_oid';
+TRUNCATE TABLE toasttest_oid;
+ALTER TABLE toasttest_oid set (toast_tuple_target = 4080);
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
 -- expect 0 blocks
 SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
-  FROM pg_class where relname = 'toasttest';
+  FROM pg_class where relname = 'toasttest_oid';
+DROP TABLE toasttest_oid;
 
-DROP TABLE toasttest;
+-- TRUNCATE cases for TOAST relation with int8 values.
+TRUNCATE TABLE toasttest_oid8;
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+-- expect >0 blocks
+SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
+  FROM pg_class where relname = 'toasttest_oid8';
+TRUNCATE TABLE toasttest_oid8;
+ALTER TABLE toasttest_oid8 set (toast_tuple_target = 4080);
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+-- expect 0 blocks
+SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
+  FROM pg_class where relname = 'toasttest_oid8';
+DROP TABLE toasttest_oid8;
 
 --
--- test substr with toasted bytea values
+-- test substr with toasted bytea values, for all types of TOAST relations
+-- supported. Do not drop these two relations, for pg_upgrade.
 --
-CREATE TABLE toasttest(f1 bytea);
+CREATE TABLE toasttest_oid(f1 bytea) WITH (toast_value_type = 'oid');
+CREATE TABLE toasttest_oid8(f1 bytea) WITH (toast_value_type = 'oid8');
 
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
 
 --
 -- Ensure that some values are uncompressed, to test the faster substring
 -- operation used in that case
 --
-alter table toasttest alter column f1 set storage external;
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
+alter table toasttest_oid alter column f1 set storage external;
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+alter table toasttest_oid8 alter column f1 set storage external;
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
 
 -- If the starting position is zero or less, then return from the start of the string
 -- adjusting the length to be consistent with the "negative start" per SQL.
-SELECT substr(f1, -1, 5) from toasttest;
+SELECT substr(f1, -1, 5) from toasttest_oid;
+SELECT substr(f1, -1, 5) from toasttest_oid8;
 
 -- If the length is less than zero, an ERROR is thrown.
-SELECT substr(f1, 5, -1) from toasttest;
+SELECT substr(f1, 5, -1) from toasttest_oid;
+SELECT substr(f1, 5, -1) from toasttest_oid8;
 
 -- If no third argument (length) is provided, the length to the end of the
 -- string is assumed.
-SELECT substr(f1, 99995) from toasttest;
+SELECT substr(f1, 99995) from toasttest_oid;
+SELECT substr(f1, 99995) from toasttest_oid8;
 
 -- If start plus length is > string length, the result is truncated to
 -- string length
-SELECT substr(f1, 99995, 10) from toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+SELECT substr(f1, 99995, 10) from toasttest_oid8;
 
-DROP TABLE toasttest;
+-- A relation rewrite leaves the TOAST value attributes unchanged.
+VACUUM FULL toasttest_oid;
+VACUUM FULL toasttest_oid8;
+SELECT c1.relname, a.atttypid::regtype
+  FROM pg_attribute AS a,
+       pg_class AS c1,
+       pg_class AS c2
+  WHERE
+       c1.relname IN ('toasttest_oid', 'toasttest_oid8') AND
+       c1.reltoastrelid = c2.oid AND
+       a.attrelid = c2.oid AND
+       a.attname = 'chunk_id'
+  ORDER BY c1.relname COLLATE "C";
+-- Check that data slices are still accessible.
+SELECT substr(f1, 99995) from toasttest_oid;
+SELECT substr(f1, 99995) from toasttest_oid8;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+SELECT substr(f1, 99995, 10) from toasttest_oid8;
 
 -- test internally compressing datums
 
diff --git a/src/test/regress/sql/type_sanity.sql b/src/test/regress/sql/type_sanity.sql
index c2496823d90e..a0d2e8bcf00b 100644
--- a/src/test/regress/sql/type_sanity.sql
+++ b/src/test/regress/sql/type_sanity.sql
@@ -420,8 +420,8 @@ WHERE c1.relnatts != (SELECT count(*) FROM pg_attribute AS a1
                       WHERE a1.attrelid = c1.oid AND a1.attnum > 0);
 
 -- Cross-check against pg_type entry
--- NOTE: we allow attstorage to be 'plain' even when typstorage is not;
--- this is mainly for toast tables.
+-- NOTE: we allow attstorage to be 'plain' or 'external' even when typstorage
+-- is not; this is mainly for toast tables.
 
 SELECT a1.attrelid, a1.attname, t1.oid, t1.typname
 FROM pg_attribute AS a1, pg_type AS t1
@@ -429,7 +429,7 @@ WHERE a1.atttypid = t1.oid AND
     (a1.attlen != t1.typlen OR
      a1.attalign != t1.typalign OR
      a1.attbyval != t1.typbyval OR
-     (a1.attstorage != t1.typstorage AND a1.attstorage != 'p'));
+     (a1.attstorage != t1.typstorage AND a1.attstorage NOT IN ('e', 'p')));
 
 -- Look for IsCatalogTextUniqueIndexOid() omissions.
 
-- 
2.51.0

v8-0015-Add-new-vartag_external-for-8-byte-TOAST-values.patchtext/x-diff; charset=us-asciiDownload
From 394a06962f18798b2d9441f46e541b533089b65a Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 14:10:36 +0900
Subject: [PATCH v8 15/15] Add new vartag_external for 8-byte TOAST values

This is a new type of external TOAST pointer, able to be fed 8-byte
TOAST values.  It uses a dedicated vartag_external, which is used when
a TOAST table uses bigint for its chunk_id.

The relevant callbacks are added to toast_external.c.
---
 src/include/access/heaptoast.h                |   8 +-
 src/include/varatt.h                          |  34 +++-
 src/backend/access/common/toast_external.c    | 145 ++++++++++++++++--
 src/backend/access/heap/heaptoast.c           |   1 +
 .../replication/logical/reorderbuffer.c       |  10 +-
 doc/src/sgml/storage.sgml                     |   6 +-
 contrib/amcheck/verify_heapam.c               |   2 +-
 7 files changed, 189 insertions(+), 17 deletions(-)

diff --git a/src/include/access/heaptoast.h b/src/include/access/heaptoast.h
index 12c9702af689..0f65e076efe5 100644
--- a/src/include/access/heaptoast.h
+++ b/src/include/access/heaptoast.h
@@ -81,6 +81,12 @@
 
 #define EXTERN_TUPLE_MAX_SIZE	MaximumBytesPerTuple(EXTERN_TUPLES_PER_PAGE)
 
+#define TOAST_OID8_MAX_CHUNK_SIZE	\
+	(EXTERN_TUPLE_MAX_SIZE -							\
+	 MAXALIGN(SizeofHeapTupleHeader) -					\
+	 (sizeof(uint32) * 2) -								\
+	 sizeof(int32) -									\
+	 VARHDRSZ)
 #define TOAST_OID_MAX_CHUNK_SIZE	\
 	(EXTERN_TUPLE_MAX_SIZE -							\
 	 MAXALIGN(SizeofHeapTupleHeader) -					\
@@ -89,7 +95,7 @@
 	 VARHDRSZ)
 
 /* Maximum size of chunk possible */
-#define TOAST_MAX_CHUNK_SIZE	TOAST_OID_MAX_CHUNK_SIZE
+#define TOAST_MAX_CHUNK_SIZE	Max(TOAST_OID_MAX_CHUNK_SIZE, TOAST_OID8_MAX_CHUNK_SIZE)
 
 /* ----------
  * heap_toast_insert_or_update -
diff --git a/src/include/varatt.h b/src/include/varatt.h
index 035c0f95e5b6..de38d1cd1ce1 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -41,6 +41,27 @@ typedef struct varatt_external_oid
 	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
 }			varatt_external_oid;
 
+/*
+ * struct varatt_external_oid8 is a "larger" version of "TOAST pointer",
+ * that uses an 8-byte integer as value.
+ *
+ * This follows the same properties as varatt_external_oid, except that
+ * this is used in TOAST relations with oid8 as attribute for chunk_id.
+ */
+typedef struct varatt_external_oid8
+{
+	int32		va_rawsize;		/* Original data size (includes header) */
+	uint32		va_extinfo;		/* External saved size (without header) and
+								 * compression method */
+	/*
+	 * Unique ID of value within TOAST table, as two uint32 for alignment
+	 * and padding.
+	 */
+	uint32		va_valueid_lo;
+	uint32		va_valueid_hi;
+	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
+}			varatt_external_oid8;
+
 /*
  * These macros define the "saved size" portion of va_extinfo.  Its remaining
  * two high-order bits identify the compression method.
@@ -90,6 +111,7 @@ typedef enum vartag_external
 	VARTAG_INDIRECT = 1,
 	VARTAG_EXPANDED_RO = 2,
 	VARTAG_EXPANDED_RW = 3,
+	VARTAG_ONDISK_OID8 = 4,
 	VARTAG_ONDISK_OID = 18
 } vartag_external;
 
@@ -111,6 +133,8 @@ VARTAG_SIZE(vartag_external tag)
 		return sizeof(varatt_expanded);
 	else if (tag == VARTAG_ONDISK_OID)
 		return sizeof(varatt_external_oid);
+	else if (tag == VARTAG_ONDISK_OID8)
+		return sizeof(varatt_external_oid8);
 	else
 	{
 		Assert(false);
@@ -367,11 +391,19 @@ VARATT_IS_EXTERNAL_ONDISK_OID(const void *PTR)
 	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID;
 }
 
+/* Is varlena datum a pointer to on-disk toasted data with OID8 value? */
+static inline bool
+VARATT_IS_EXTERNAL_ONDISK_OID8(const void *PTR)
+{
+	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID8;
+}
+
 /* Is varlena datum a pointer to on-disk toasted data? */
 static inline bool
 VARATT_IS_EXTERNAL_ONDISK(const void *PTR)
 {
-	return VARATT_IS_EXTERNAL_ONDISK_OID(PTR);
+	return VARATT_IS_EXTERNAL_ONDISK_OID(PTR) ||
+		VARATT_IS_EXTERNAL_ONDISK_OID8(PTR);
 }
 
 /* Is varlena datum an indirect pointer? */
diff --git a/src/backend/access/common/toast_external.c b/src/backend/access/common/toast_external.c
index e2f0a9dc1c50..431258b2be96 100644
--- a/src/backend/access/common/toast_external.c
+++ b/src/backend/access/common/toast_external.c
@@ -18,8 +18,19 @@
 #include "postgres.h"
 
 #include "access/detoast.h"
+#include "access/genam.h"
 #include "access/heaptoast.h"
 #include "access/toast_external.h"
+#include "catalog/catalog.h"
+#include "miscadmin.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+
+
+/* Callbacks for VARTAG_ONDISK_OID8 */
+static void ondisk_oid8_to_external_data(struct varlena *attr,
+										 toast_external_data *data);
+static struct varlena *ondisk_oid8_create_external_data(toast_external_data data);
 
 /* Callbacks for VARTAG_ONDISK_OID */
 static void ondisk_oid_to_external_data(struct varlena *attr,
@@ -28,7 +39,7 @@ static struct varlena *ondisk_oid_create_external_data(toast_external_data data)
 
 /*
  * Fetch the possibly-unaligned contents of an on-disk external TOAST with
- * OID values into a local "varatt_external_oid" pointer.
+ * OID or OID8 values into a local "varatt_external_*" pointer.
  *
  * This should be just a memcpy, but some versions of gcc seem to produce
  * broken code that assumes the datum contents are aligned.  Introducing
@@ -45,9 +56,20 @@ varatt_external_oid_get_pointer(varatt_external_oid *toast_pointer,
 	memcpy(toast_pointer, VARDATA_EXTERNAL(attre), sizeof(varatt_external_oid));
 }
 
+static inline void
+varatt_external_oid8_get_pointer(varatt_external_oid8 *toast_pointer,
+								 struct varlena *attr)
+{
+	varattrib_1b_e *attre = (varattrib_1b_e *) attr;
+
+	Assert(VARATT_IS_EXTERNAL_ONDISK_OID8(attre));
+	Assert(VARSIZE_EXTERNAL(attre) == sizeof(varatt_external_oid8) + VARHDRSZ_EXTERNAL);
+	memcpy(toast_pointer, VARDATA_EXTERNAL(attre), sizeof(varatt_external_oid8));
+}
+
 /*
  * Decompressed size of an on-disk varlena; but note argument is a struct
- * varatt_external_oid.
+ * varatt_external_oid or varatt_external_oid8.
  */
 static inline Size
 varatt_external_oid_get_extsize(varatt_external_oid toast_pointer)
@@ -55,9 +77,15 @@ varatt_external_oid_get_extsize(varatt_external_oid toast_pointer)
 	return toast_pointer.va_extinfo & VARLENA_EXTSIZE_MASK;
 }
 
+static inline Size
+varatt_external_oid8_get_extsize(varatt_external_oid8 toast_pointer)
+{
+	return toast_pointer.va_extinfo & VARLENA_EXTSIZE_MASK;
+}
+
 /*
  * Compression method of an on-disk varlena; but note argument is a struct
- *  varatt_external_oid.
+ *  varatt_external_oid or varatt_external_oid8.
  */
 static inline uint32
 varatt_external_oid_get_compress_method(varatt_external_oid toast_pointer)
@@ -65,6 +93,12 @@ varatt_external_oid_get_compress_method(varatt_external_oid toast_pointer)
 	return toast_pointer.va_extinfo >> VARLENA_EXTSIZE_BITS;
 }
 
+static inline uint32
+varatt_external_oid8_get_compress_method(varatt_external_oid8 toast_pointer)
+{
+	return toast_pointer.va_extinfo >> VARLENA_EXTSIZE_BITS;
+}
+
 /*
  * Testing whether an externally-stored TOAST value is compressed now requires
  * comparing size stored in va_extinfo (the actual length of the external data)
@@ -79,6 +113,19 @@ varatt_external_oid_is_compressed(varatt_external_oid toast_pointer)
 		(Size) (toast_pointer.va_rawsize - VARHDRSZ);
 }
 
+static inline bool
+varatt_external_oid8_is_compressed(varatt_external_oid8 toast_pointer)
+{
+	return varatt_external_oid8_get_extsize(toast_pointer) <
+		(Size) (toast_pointer.va_rawsize - VARHDRSZ);
+}
+
+/*
+ * Size of an EXTERNAL datum that contains a standard TOAST pointer
+ * (oid8 value).
+ */
+#define TOAST_POINTER_OID8_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_oid8))
+
 /*
  * Size of an EXTERNAL datum that contains a standard TOAST pointer (OID
  * value).
@@ -99,6 +146,12 @@ varatt_external_oid_is_compressed(varatt_external_oid toast_pointer)
  * individual fields.
  */
 static const toast_external_info toast_external_infos[TOAST_EXTERNAL_INFO_SIZE] = {
+	[VARTAG_ONDISK_OID8] = {
+		.toast_pointer_size = TOAST_POINTER_OID8_SIZE,
+		.maximum_chunk_size = TOAST_OID_MAX_CHUNK_SIZE,
+		.to_external_data = ondisk_oid8_to_external_data,
+		.create_external_data = ondisk_oid8_create_external_data,
+	},
 	[VARTAG_ONDISK_OID] = {
 		.toast_pointer_size = TOAST_OID_POINTER_SIZE,
 		.maximum_chunk_size = TOAST_OID_MAX_CHUNK_SIZE,
@@ -155,22 +208,33 @@ toast_external_info_get_pointer_size(uint8 tag)
 uint8
 toast_external_assign_vartag(Oid toastrelid, Oid8 valueid)
 {
+	Oid		toast_typid;
+
 	/*
-	 * If dealing with a code path where a TOAST relation may not be assigned,
-	 * like heap_toast_insert_or_update(), just use the legacy
-	 * vartag_external.
+	 * If dealing with a code path where a TOAST relation may not be assigned
+	 * like heap_toast_insert_or_update(), just use the default with an OID
+	 * type.
+	 *
+	 * In bootstrap mode, we should not do any kind of syscache lookups,
+	 * so also rely on OID.
 	 */
-	if (!OidIsValid(toastrelid))
+	if (!OidIsValid(toastrelid) || IsBootstrapProcessingMode())
 		return VARTAG_ONDISK_OID;
 
 	/*
-	 * Currently there is only one type of vartag_external supported: 4-byte
-	 * value with OID for the chunk_id type.
+	 * Two types of vartag_external are currently supported: OID and OID8,
+	 * which depend on the type assigned to "chunk_id" for the TOAST table.
 	 *
-	 * Note: This routine will be extended to be able to use multiple
-	 * vartag_external within a single TOAST relation type, that may change
-	 * depending on the value used.
+	 * XXX: Should we assign from the start an OID vartag if dealing with
+	 * a TOAST relation with OID8 as value if the value assigned is less
+	 * than UINT_MAX?  This just takes the "safe" approach of assigning
+	 * the larger vartag in all cases, but this can be made cheaper
+	 * depending on the OID consumption.
 	 */
+	toast_typid = get_atttype(toastrelid, 1);
+	if (toast_typid == OID8OID)
+		return VARTAG_ONDISK_OID8;
+
 	return VARTAG_ONDISK_OID;
 }
 
@@ -179,6 +243,63 @@ toast_external_assign_vartag(Oid toastrelid, Oid8 valueid)
  * the in-memory representation toast_external_data used in the backend.
  */
 
+/* Callbacks for VARTAG_ONDISK_OID8 */
+static void
+ondisk_oid8_to_external_data(struct varlena *attr, toast_external_data *data)
+{
+	varatt_external_oid8	external;
+
+	varatt_external_oid8_get_pointer(&external, attr);
+	data->rawsize = external.va_rawsize;
+
+	/* External size and compression methods are stored in the same field */
+	if (varatt_external_oid8_is_compressed(external))
+	{
+		data->extsize = varatt_external_oid8_get_extsize(external);
+		data->compression_method = varatt_external_oid8_get_compress_method(external);
+	}
+	else
+	{
+		data->extsize = external.va_extinfo;
+		data->compression_method = TOAST_INVALID_COMPRESSION_ID;
+	}
+
+	data->valueid = (((uint64) external.va_valueid_hi) << 32) |
+		external.va_valueid_lo;
+	data->toastrelid = external.va_toastrelid;
+
+}
+
+static struct varlena *
+ondisk_oid8_create_external_data(toast_external_data data)
+{
+	struct varlena *result = NULL;
+	varatt_external_oid8 external;
+
+	external.va_rawsize = data.rawsize;
+
+	if (data.compression_method != TOAST_INVALID_COMPRESSION_ID)
+	{
+		/* Set size and compression method, in a single field. */
+		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(external,
+													 data.extsize,
+													 data.compression_method);
+	}
+	else
+		external.va_extinfo = data.extsize;
+
+	external.va_toastrelid = data.toastrelid;
+	external.va_valueid_hi = (((uint64) data.valueid) >> 32);
+	external.va_valueid_lo = (uint32) data.valueid;
+
+	result = (struct varlena *) palloc(TOAST_POINTER_OID8_SIZE);
+	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_OID8);
+	memcpy(VARDATA_EXTERNAL(result), &external, sizeof(external));
+
+	return result;
+}
+
+
 /* Callbacks for VARTAG_ONDISK_OID */
 
 /*
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index 6434a424ddc3..4997837335d6 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -32,6 +32,7 @@
 #include "access/toast_helper.h"
 #include "access/toast_internals.h"
 #include "utils/fmgroids.h"
+#include "utils/syscache.h"
 
 
 /* ----------
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 24daeb93bc81..1c9a163a94b1 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -5008,14 +5008,22 @@ ReorderBufferToastAppendChunk(ReorderBuffer *rb, ReorderBufferTXN *txn,
 	TupleDesc	desc = RelationGetDescr(relation);
 	Oid8		chunk_id;
 	int32		chunk_seq;
+	Oid			toast_typid;
 
 	if (txn->toast_hash == NULL)
 		ReorderBufferToastInitHash(rb, txn);
+	toast_typid = TupleDescAttr(desc, 0)->atttypid;
 
 	Assert(IsToastRelation(relation));
 
 	newtup = change->data.tp.newtuple;
-	chunk_id = DatumGetObjectId(fastgetattr(newtup, 1, desc, &isnull));
+	/* This depends on the type of TOAST value dealt with. */
+	if (toast_typid == OIDOID)
+		chunk_id = DatumGetObjectId(fastgetattr(newtup, 1, desc, &isnull));
+	else if (toast_typid == INT8OID)
+		chunk_id = DatumGetUInt64(fastgetattr(newtup, 1, desc, &isnull));
+	else
+		Assert(false);
 	Assert(!isnull);
 	chunk_seq = DatumGetInt32(fastgetattr(newtup, 2, desc, &isnull));
 	Assert(!isnull);
diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml
index afddf663fec5..dbec30d48b4a 100644
--- a/doc/src/sgml/storage.sgml
+++ b/doc/src/sgml/storage.sgml
@@ -417,7 +417,11 @@ described in more detail below.
 
 <para>
 Out-of-line values are divided (after compression if used) into chunks of at
-most <symbol>TOAST_OID_MAX_CHUNK_SIZE</symbol> bytes (by default this value is chosen
+most <symbol>TOAST_OID_MAX_CHUNK_SIZE</symbol> bytes if the
+<acronym>TOAST</acronym> relation uses the <literal>oid</literal> type for
+<literal>chunk_id</literal>, or <symbol>TOAST_OID8_MAX_CHUNK_SIZE</symbol>
+bytes if the <acronym>TOAST</acronym> relation uses the <literal>oid8</literal>
+type for <literal>chunk_id</literal> (by default these values are chosen
 so that four chunk rows will fit on a page, making it about 2000 bytes).
 Each chunk is stored as a separate row in the <acronym>TOAST</acronym> table
 belonging to the owning table.  Every
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 143e6baa35cf..8cea9ad31bcd 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1733,7 +1733,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	{
 		uint8		va_tag = VARTAG_EXTERNAL(tp + ctx->offset);
 
-		if (va_tag != VARTAG_ONDISK_OID)
+		if (va_tag != VARTAG_ONDISK_OID && va_tag != VARTAG_ONDISK_OID8)
 		{
 			report_corruption(ctx,
 							  psprintf("toasted attribute has unexpected TOAST tag %u",
-- 
2.51.0

#50Nikhil Kumar Veldanda
veldanda.nikhilkumar17@gmail.com
In reply to: Michael Paquier (#49)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

Hi Michael,

On Tue, Nov 25, 2025 at 8:54 PM Michael Paquier <michael@paquier.xyz> wrote:

Tom, you are registered as a reviewer of the patch. The point of
contention of the patch, where I see there is no consensus yet, is if
my approach of using a redirection for the external TOAST pointers
with a new layer to facilitate the addition of more vartags (aka the
64b value vartag proposed here, concept that could also apply to
compression methods later on) is acceptable. Moving to a different
approach, like the "brutal" one I am naming upthread where the
redirection layer is replaced by changes in all the code paths that
need to be touched, would be of course cheaper at runtime as there
would be no more redirection, but the maintenance would be a nightmare
the more vartags we add, and I have some plans for more of these.
Doing the switch would be a few hours work, so that would not be a big
deal, I guess. The important part is an agreement about the approach,
IMO.

This point still got no reply. It would be nice to do something for
this release regarding this old issue, IMO..
--

Thanks for the updated v8. I’m also looking forward to feedback on the
vartag approach. My work on adding ZSTD compression depends on this
design decision, so consensus here will help me proceed in the right
direction.

--
Nikhil Veldanda

#51Robert Haas
robertmhaas@gmail.com
In reply to: Michael Paquier (#49)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

On Tue, Nov 25, 2025 at 11:54 PM Michael Paquier <michael@paquier.xyz> wrote:

Tom, you are registered as a reviewer of the patch. The point of
contention of the patch, where I see there is no consensus yet, is if
my approach of using a redirection for the external TOAST pointers
with a new layer to facilitate the addition of more vartags (aka the
64b value vartag proposed here, concept that could also apply to
compression methods later on) is acceptable. Moving to a different
approach, like the "brutal" one I am naming upthread where the
redirection layer is replaced by changes in all the code paths that
need to be touched, would be of course cheaper at runtime as there
would be no more redirection, but the maintenance would be a nightmare
the more vartags we add, and I have some plans for more of these.
Doing the switch would be a few hours work, so that would not be a big
deal, I guess. The important part is an agreement about the approach,
IMO.

This point still got no reply. It would be nice to do something for
this release regarding this old issue, IMO..

I took a brief look at this today, looking at parts of v8-0005 and
v8-0015. Although I don't dislike the idea of an abstraction layer in
concept, it's unclear to me how much this particular abstraction layer
is really buying you. It's basically abstracting away two things: the
difference between the current varatt_external and
varatt_external_oid8, and the unbundling of compression_method from
extsize. That's not a lot. This thread has a bunch of ideas already on
other things that people want to do to the TOAST system or think you
should be doing to the TOAST system, and while I'm -1 on letting those
discussions hijack the thread, it's worth paying attention to whether
the abstraction layer makes any of them easier. As far as I can see,
it doesn't. I suspect, for example, that direct TID access to toast
values is a much worse idea than people here seem to be supposing, but
whether that's true or false, toast_external_data doesn't help anyone
who is trying to implement it. I don't think it even really helps with
zstd compression, even though that's going to need a
ToastCompressionId, so I kind of find myself wondering what the point
is.

One alternative idea that I had is just provide a way to turn a 4-byte
TOAST pointe into an 8-byte TOAST pointer. If you inserted that
surgically at certain places, maybe you could make 4-byte TOAST
pointers invisible to certain parts of the system. But I'm also not
sure that's any better than what you call the "brutal" approach, which
I'm actually not sure is that brutal. I mean, how bad is it if we just
deal with one more possibility at each place where we currently deal
with VARTAG_ONDISK? To be clear, I am not trying to say "absolutely
don't do this abstraction layer". However, imagine a future where we
have 10 vartags that can appear on disk: the current VARTAG_ONDISK and
9 others. If, when that time comes, your abstraction layer handles 5
or 6 or more of those, I'd call that a win. If the only two it ever
handles are OID and OID8, I'd say that's a loss: it's not really an
abstraction layer at all at that point. In that situation you'd rather
just admit that what you're really trying to do is smooth over the
distinction between OID and OID8 and keep any other goals (including
unbundling the compression ID, IMHO) separate.

I am somewhat bothered by the idea of just bluntly changing everything
over to 8-byte TOAST OIDs. Widening the TOAST table column doesn't
concern me; it eats up 4 bytes, but the tuples are ~2k so it's not
that much of a difference. Widening the TOAST pointer seems like a
bigger concern. Every single toasted value is getting 4 bytes wider. A
single tuple could potentially have a significant number of such
columns, and as a result, get significantly wider. We could still use
the 4-byte toast pointer format if the value ID happens to be small,
but if we just assign the value ID using a 4-byte counter, after the
first 4B allocations, it will never be small again. After that,
relations that never would have needed anything like 4B toast pointers
will still pay the cost for those that do, which seems quite sad.
Whether this patch should be responsible for doing something about
that sadness is unclear to me: a sequence-per-relation to allow
separate counter allocation for each relation would be great, but
bottlenecking this patch set behind such a large change might well be
the wrong idea.

I am also uncertain how much loss we're really talking about: if for
example a wide table contains 5 long text blogs per row (which seems
fairly high) then that's "only" 20 bytes/row, and probably those rows
don't fit into 8kB blocks that well anyway, so maybe the loss of
efficiency isn't much. Maybe the biggest concern is that, IIUC, some
tuples that currently can be stored on disk wouldn't be storable at
all any more with the wider pointers, because the row couldn't be
squeezed into the block no matter how much pressure the TOAST code
applies. If that's correct, I think there's a pretty good chance that
this patch will make a few people who are already desperately unhappy
with the TOAST system even more unhappy. There's nothing you can
really do if you're up against that hard limit. I'm not sure what, if
anything, we can or should do about that, but it seems like something
we ought to at least discuss.

--
Robert Haas
EDB: http://www.enterprisedb.com

#52Michael Paquier
michael@paquier.xyz
In reply to: Robert Haas (#51)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

On Mon, Dec 08, 2025 at 04:19:54PM -0500, Robert Haas wrote:

I took a brief look at this today, looking at parts of v8-0005 and
v8-0015.

Thanks for the input!

Although I don't dislike the idea of an abstraction layer in
concept, it's unclear to me how much this particular abstraction layer
is really buying you. It's basically abstracting away two things: the
difference between the current varatt_external and
varatt_external_oid8, and the unbundling of compression_method from
extsize. That's not a lot. This thread has a bunch of ideas already on
other things that people want to do to the TOAST system or think you
should be doing to the TOAST system, and while I'm -1 on letting those
discussions hijack the thread, it's worth paying attention to whether
the abstraction layer makes any of them easier. As far as I can see,
it doesn't. I suspect, for example, that direct TID access to toast
values is a much worse idea than people here seem to be supposing, but
whether that's true or false, toast_external_data doesn't help anyone
who is trying to implement it.
I don't think it even really helps with zstd compression, even
though that's going to need a ToastCompressionId, so I kind of
find myself wondering what the point is.

The idea for the support of zstd compression with this set of APIs
would be to allocate a new vartag_external, and perhaps just allow an
8-byte OID in this case to keep the code simple. It is true that I
have not attempted to rework the code behind the compression and the
decompression of the slices, but I could not directly see why we would
want to do that as this also relates with the in-memory representation
of compressed datums.

One alternative idea that I had is just provide a way to turn a 4-byte
TOAST pointe into an 8-byte TOAST pointer. If you inserted that
surgically at certain places, maybe you could make 4-byte TOAST
pointers invisible to certain parts of the system. But I'm also not
sure that's any better than what you call the "brutal" approach, which
I'm actually not sure is that brutal. I mean, how bad is it if we just
deal with one more possibility at each place where we currently deal
with VARTAG_ONDISK? To be clear, I am not trying to say "absolutely
don't do this abstraction layer".

To be honest, the brutal method is not that bad I think. For the
in-tree places, my patch touches all the areas that matter, so I have
identified what needs to be touched. Also please note that the
approach used in this patch is based on a remark that has been made in
the last few years (perhaps by you actually, I'd need to find the
reference?), where folks were worrying about the introduction of a new
vartag_external as something that could be a deadly trap if we don't
patch all the areas that need to handle with external on-disk datums.
The brutal method was actually the first thing that I have done, just
to notice that I was refactoring all these areas of the code the same
way, leading to the patch attached at the end as being a win if we add
more vartags in the future. Anyway, as far as things stand today on
this thread, I have not been able to get even a single +1 for this
design. I mean, that's fine, it's still what can be called a
consensus and that's how development happens.

However, imagine a future where we
have 10 vartags that can appear on disk: the current VARTAG_ONDISK and
9 others. If, when that time comes, your abstraction layer handles 5
or 6 or more of those, I'd call that a win. If the only two it ever
handles are OID and OID8, I'd say that's a loss: it's not really an
abstraction layer at all at that point. In that situation you'd rather
just admit that what you're really trying to do is smooth over the
distinction between OID and OID8 and keep any other goals (including
unbundling the compression ID, IMHO) separate.

I won't deny this argument. As far as things go for the backend core
engine, most of the bad things I hear back from users regarding TOAST
is the 4-byte OID limitation with TOAST values. The addition of zstd
comes second, but the 32 bit problem still primes because backends
just get suddenly stuck.

I am somewhat bothered by the idea of just bluntly changing everything
over to 8-byte TOAST OIDs.

I agree that forcing that everywhere is a bad idea and I am not
suggesting that, neither does the patch set enforce that. The first
design of the patch made an 8-byte enforcement possible by using a
GUC, with the default being 4 bytes. The first set of feedback I have
received in August was to use a reloption, making 8-byte OIDs an
option that one has to pass with CREATE TABLE, and to not use a GUC.
The latest versions of the patch use a reloption.

Widening the TOAST table column doesn't
concern me; it eats up 4 bytes, but the tuples are ~2k so it's not
that much of a difference. Widening the TOAST pointer seems like a
bigger concern.
Every single toasted value is getting 4 bytes wider. A
single tuple could potentially have a significant number of such
columns, and as a result, get significantly wider. We could still use
the 4-byte toast pointer format if the value ID happens to be small,
but if we just assign the value ID using a 4-byte counter, after the
first 4B allocations, it will never be small again. After that,
relations that never would have needed anything like 4B toast pointers
will still pay the cost for those that do, which seems quite sad.
Whether this patch should be responsible for doing something about
that sadness is unclear to me: a sequence-per-relation to allow
separate counter allocation for each relation would be great, but
bottlenecking this patch set behind such a large change might well be
the wrong idea.

As far as I am concerned about the user cases, the tables where the
4-byte limitation is reached usually concern a small subset of tables
in one or more schemas. The cost of a 8-byte OID also comes down to
how much the TOAST blobs are updated. If they are updated a lot,
we'll need a new value anyway. If the blobs are mostly static
content with other attributes updated a lot, the cost of 8-byte OIDs
gets much high. If I would be a user deadling with a set of tables
that have already 4 billions TOAST blobs of at least 2kB in more than
one relation, the extra 4 bytes when updating the other attributes are
not my main worry. :)

I am also uncertain how much loss we're really talking about: if for
example a wide table contains 5 long text blogs per row (which seems
fairly high) then that's "only" 20 bytes/row, and probably those rows
don't fit into 8kB blocks that well anyway, so maybe the loss of
efficiency isn't much. Maybe the biggest concern is that, IIUC, some
tuples that currently can be stored on disk wouldn't be storable at
all any more with the wider pointers, because the row couldn't be
squeezed into the block no matter how much pressure the TOAST code
applies. If that's correct, I think there's a pretty good chance that
this patch will make a few people who are already desperately unhappy
with the TOAST system even more unhappy. There's nothing you can
really do if you're up against that hard limit. I'm not sure what, if
anything, we can or should do about that, but it seems like something
we ought to at least discuss.

Yeah, perhaps. Again, for users that fight against the hard limit,
what I'm hearing is that for some users reworking a large table to be
rewritten as a partitioned one to bypass the limit is not acceptable
in some cases. With the recent planner changes that have improved the
planning time for many partitions, things are better, but it's
basically impossible to predict all the possible user behaviors that
there could be out there. I won't deny that it could be possible that
enlarging the toast values to 8-bytes for some relations leads to some
folks being unhappy because it makes the storage of the on-disk
external pointers less effective regarding to alignment.

At this point, I am mostly interested in a possible consensus about
what people would like to see, in the timeframe that remains for v19
and the timing is what it is because everybody is busy. What I am in
priority interested is giving a way for users to bypass the 4-byte
limit without reworking a schema. If this is opt-in, users have at
least a choice. A reloption has the disadvantage to not be something
that users are aware of by default. A rewrite of the TOAST table is
required anyway. If the consensus is this design is not good enough,
that's fine. If the consensus is to use the
"brutal-still-not-so-brutal" approach, that's fine. The only thing I
can do is commit my resources into making something happen for the
4-byte limitation, whatever the approach folks would be OK with.
--
Michael

#53Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#52)
15 attachment(s)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

On Thu, Dec 11, 2025 at 07:47:05AM +0900, Michael Paquier wrote:

On Mon, Dec 08, 2025 at 04:19:54PM -0500, Robert Haas wrote:

I took a brief look at this today, looking at parts of v8-0005 and
v8-0015.

Thanks for the input!

The CF bot was complaining that this patch set needed a rebase due to
the recent changes in pg_resetwal, so here we go. For now this is the
same stuff as the previous versions, with the same separation and
design.

I am also looking at what it would take to implement what the brutal
approach I have mentioned upthread. This requires a bit more
reorganization than what I had in mind initially. By putting first in
the patch set some of the parts that are kind of relevant with the two
designs, things seem to be a bit leaner. I need to spend a few more
hours on that beforebeing sure, though..

For now, rebase for a happy bot.
--
Michael

Attachments:

v9-0001-Implement-oid8-data-type.patchtext/x-diff; charset=us-asciiDownload
From 21c939931f22bd48a40ef8ad8f3aae055e962d9c Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 11:03:17 +0900
Subject: [PATCH v9 01/15] Implement oid8 data type

This new identifier type will be used for 8-byte TOAST values, and can
be useful for other purposes, not yet defined as of writing this patch.
The following operators are added for this data type:
- Casts with integer types and OID.
- btree and hash operators
- min/max functions.
- Tests and documentation.

XXX: Requires catversion bump.
---
 src/include/c.h                           |  11 +-
 src/include/catalog/pg_aggregate.dat      |   6 +
 src/include/catalog/pg_amop.dat           |  23 +++
 src/include/catalog/pg_amproc.dat         |  12 ++
 src/include/catalog/pg_cast.dat           |  14 ++
 src/include/catalog/pg_opclass.dat        |   4 +
 src/include/catalog/pg_operator.dat       |  26 +++
 src/include/catalog/pg_opfamily.dat       |   4 +
 src/include/catalog/pg_proc.dat           |  64 +++++++
 src/include/catalog/pg_type.dat           |   5 +
 src/include/fmgr.h                        |   2 +
 src/include/postgres.h                    |  20 +++
 src/backend/access/nbtree/nbtcompare.c    |  82 +++++++++
 src/backend/bootstrap/bootstrap.c         |   2 +
 src/backend/utils/adt/Makefile            |   1 +
 src/backend/utils/adt/int8.c              |   8 +
 src/backend/utils/adt/meson.build         |   1 +
 src/backend/utils/adt/oid8.c              | 171 +++++++++++++++++++
 src/fe_utils/print.c                      |   1 +
 src/test/regress/expected/oid8.out        | 196 ++++++++++++++++++++++
 src/test/regress/expected/oid8.sql        |   0
 src/test/regress/expected/opr_sanity.out  |   7 +
 src/test/regress/expected/type_sanity.out |   1 +
 src/test/regress/parallel_schedule        |   2 +-
 src/test/regress/sql/oid8.sql             |  57 +++++++
 src/test/regress/sql/type_sanity.sql      |   1 +
 doc/src/sgml/datatype.sgml                |  11 ++
 doc/src/sgml/func/func-aggregate.sgml     |   8 +-
 28 files changed, 734 insertions(+), 6 deletions(-)
 create mode 100644 src/backend/utils/adt/oid8.c
 create mode 100644 src/test/regress/expected/oid8.out
 create mode 100644 src/test/regress/expected/oid8.sql
 create mode 100644 src/test/regress/sql/oid8.sql

diff --git a/src/include/c.h b/src/include/c.h
index 811d6d0110c0..c7114e0ee217 100644
--- a/src/include/c.h
+++ b/src/include/c.h
@@ -569,6 +569,7 @@ typedef uint32 bits32;			/* >= 32 bits */
 /* snprintf format strings to use for 64-bit integers */
 #define INT64_FORMAT "%" PRId64
 #define UINT64_FORMAT "%" PRIu64
+#define OID8_FORMAT "%" PRIu64
 
 /*
  * 128-bit signed and unsigned integers
@@ -655,7 +656,7 @@ typedef double float8;
 #define FLOAT8PASSBYVAL true
 
 /*
- * Oid, RegProcedure, TransactionId, SubTransactionId, MultiXactId,
+ * Oid, Oid8, RegProcedure, TransactionId, SubTransactionId, MultiXactId,
  * CommandId
  */
 
@@ -687,6 +688,12 @@ typedef uint32 CommandId;
 #define FirstCommandId	((CommandId) 0)
 #define InvalidCommandId	(~(CommandId)0)
 
+/* 8-byte Object ID */
+typedef uint64 Oid8;
+
+#define InvalidOid8		((Oid8) 0)
+#define OID8_MAX	UINT64_MAX
+#define atooid8(x) ((Oid8) strtou64((x), NULL, 10))
 
 /* ----------------
  *		Variable-length datatypes all share the 'struct varlena' header.
@@ -787,6 +794,8 @@ typedef NameData *Name;
 
 #define OidIsValid(objectId)  ((bool) ((objectId) != InvalidOid))
 
+#define Oid8IsValid(objectId)  ((bool) ((objectId) != InvalidOid8))
+
 #define RegProcedureIsValid(p)	OidIsValid(p)
 
 
diff --git a/src/include/catalog/pg_aggregate.dat b/src/include/catalog/pg_aggregate.dat
index f22ccfbf49fe..ecf330f05e67 100644
--- a/src/include/catalog/pg_aggregate.dat
+++ b/src/include/catalog/pg_aggregate.dat
@@ -104,6 +104,9 @@
 { aggfnoid => 'max(oid)', aggtransfn => 'oidlarger',
   aggcombinefn => 'oidlarger', aggsortop => '>(oid,oid)',
   aggtranstype => 'oid' },
+{ aggfnoid => 'max(oid8)', aggtransfn => 'oid8larger',
+  aggcombinefn => 'oid8larger', aggsortop => '>(oid8,oid8)',
+  aggtranstype => 'oid8' },
 { aggfnoid => 'max(float4)', aggtransfn => 'float4larger',
   aggcombinefn => 'float4larger', aggsortop => '>(float4,float4)',
   aggtranstype => 'float4' },
@@ -178,6 +181,9 @@
 { aggfnoid => 'min(oid)', aggtransfn => 'oidsmaller',
   aggcombinefn => 'oidsmaller', aggsortop => '<(oid,oid)',
   aggtranstype => 'oid' },
+{ aggfnoid => 'min(oid8)', aggtransfn => 'oid8smaller',
+  aggcombinefn => 'oid8smaller', aggsortop => '<(oid8,oid8)',
+  aggtranstype => 'oid8' },
 { aggfnoid => 'min(float4)', aggtransfn => 'float4smaller',
   aggcombinefn => 'float4smaller', aggsortop => '<(float4,float4)',
   aggtranstype => 'float4' },
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 2a693cfc31c6..2c3004d53611 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -180,6 +180,24 @@
 { amopfamily => 'btree/oid_ops', amoplefttype => 'oid', amoprighttype => 'oid',
   amopstrategy => '5', amopopr => '>(oid,oid)', amopmethod => 'btree' },
 
+# btree oid8_ops
+
+{ amopfamily => 'btree/oid8_ops', amoplefttype => 'oid8',
+  amoprighttype => 'oid8', amopstrategy => '1', amopopr => '<(oid8,oid8)',
+  amopmethod => 'btree' },
+{ amopfamily => 'btree/oid8_ops', amoplefttype => 'oid8',
+  amoprighttype => 'oid8', amopstrategy => '2', amopopr => '<=(oid8,oid8)',
+  amopmethod => 'btree' },
+{ amopfamily => 'btree/oid8_ops', amoplefttype => 'oid8',
+  amoprighttype => 'oid8', amopstrategy => '3', amopopr => '=(oid8,oid8)',
+  amopmethod => 'btree' },
+{ amopfamily => 'btree/oid8_ops', amoplefttype => 'oid8',
+  amoprighttype => 'oid8', amopstrategy => '4', amopopr => '>=(oid8,oid8)',
+  amopmethod => 'btree' },
+{ amopfamily => 'btree/oid8_ops', amoplefttype => 'oid8',
+  amoprighttype => 'oid8', amopstrategy => '5', amopopr => '>(oid8,oid8)',
+  amopmethod => 'btree' },
+
 # btree xid8_ops
 
 { amopfamily => 'btree/xid8_ops', amoplefttype => 'xid8',
@@ -974,6 +992,11 @@
 { amopfamily => 'hash/oid_ops', amoplefttype => 'oid', amoprighttype => 'oid',
   amopstrategy => '1', amopopr => '=(oid,oid)', amopmethod => 'hash' },
 
+# oid8_ops
+{ amopfamily => 'hash/oid8_ops', amoplefttype => 'oid8',
+  amoprighttype => 'oid8', amopstrategy => '1', amopopr => '=(oid8,oid8)',
+  amopmethod => 'hash' },
+
 # oidvector_ops
 { amopfamily => 'hash/oidvector_ops', amoplefttype => 'oidvector',
   amoprighttype => 'oidvector', amopstrategy => '1',
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index e3477500baa7..d3719b3610c4 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -213,6 +213,14 @@
   amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid',
   amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' },
+{ amprocfamily => 'btree/oid8_ops', amproclefttype => 'oid8',
+  amprocrighttype => 'oid8', amprocnum => '1', amproc => 'btoid8cmp' },
+{ amprocfamily => 'btree/oid8_ops', amproclefttype => 'oid8',
+  amprocrighttype => 'oid8', amprocnum => '2', amproc => 'btoid8sortsupport' },
+{ amprocfamily => 'btree/oid8_ops', amproclefttype => 'oid8',
+  amprocrighttype => 'oid8', amprocnum => '4', amproc => 'btequalimage' },
+{ amprocfamily => 'btree/oid8_ops', amproclefttype => 'oid8',
+  amprocrighttype => 'oid8', amprocnum => '6', amproc => 'btoid8skipsupport' },
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
@@ -432,6 +440,10 @@
   amprocrighttype => 'xid8', amprocnum => '1', amproc => 'hashxid8' },
 { amprocfamily => 'hash/xid8_ops', amproclefttype => 'xid8',
   amprocrighttype => 'xid8', amprocnum => '2', amproc => 'hashxid8extended' },
+{ amprocfamily => 'hash/oid8_ops', amproclefttype => 'oid8',
+  amprocrighttype => 'oid8', amprocnum => '1', amproc => 'hashoid8' },
+{ amprocfamily => 'hash/oid8_ops', amproclefttype => 'oid8',
+  amprocrighttype => 'oid8', amprocnum => '2', amproc => 'hashoid8extended' },
 { amprocfamily => 'hash/cid_ops', amproclefttype => 'cid',
   amprocrighttype => 'cid', amprocnum => '1', amproc => 'hashcid' },
 { amprocfamily => 'hash/cid_ops', amproclefttype => 'cid',
diff --git a/src/include/catalog/pg_cast.dat b/src/include/catalog/pg_cast.dat
index fbfd669587f0..695f6b2a5e73 100644
--- a/src/include/catalog/pg_cast.dat
+++ b/src/include/catalog/pg_cast.dat
@@ -296,6 +296,20 @@
 { castsource => 'regdatabase', casttarget => 'int4', castfunc => '0',
   castcontext => 'a', castmethod => 'b' },
 
+# OID8 category: allow implicit conversion from any integral type (including
+# int8), as well as assignment coercion to int8.
+{ castsource => 'int8', casttarget => 'oid8', castfunc => '0',
+  castcontext => 'i', castmethod => 'b' },
+{ castsource => 'int2', casttarget => 'oid8', castfunc => 'int8(int2)',
+  castcontext => 'i', castmethod => 'f' },
+{ castsource => 'int4', casttarget => 'oid8', castfunc => 'int8(int4)',
+  castcontext => 'i', castmethod => 'f' },
+{ castsource => 'oid8', casttarget => 'int8', castfunc => '0',
+  castcontext => 'a', castmethod => 'b' },
+# Assignment coercion from oid to oid8.
+{ castsource => 'oid', casttarget => 'oid8', castfunc => 'oid8(oid)',
+  castcontext => 'a', castmethod => 'f' },
+
 # String category
 { castsource => 'text', casttarget => 'bpchar', castfunc => '0',
   castcontext => 'i', castmethod => 'b' },
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 4a9624802aa5..c0de88fabc49 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -177,6 +177,10 @@
   opcintype => 'xid8' },
 { opcmethod => 'btree', opcname => 'xid8_ops', opcfamily => 'btree/xid8_ops',
   opcintype => 'xid8' },
+{ opcmethod => 'hash', opcname => 'oid8_ops', opcfamily => 'hash/oid8_ops',
+  opcintype => 'oid8' },
+{ opcmethod => 'btree', opcname => 'oid8_ops', opcfamily => 'btree/oid8_ops',
+  opcintype => 'oid8' },
 { opcmethod => 'hash', opcname => 'cid_ops', opcfamily => 'hash/cid_ops',
   opcintype => 'cid' },
 { opcmethod => 'hash', opcname => 'tid_ops', opcfamily => 'hash/tid_ops',
diff --git a/src/include/catalog/pg_operator.dat b/src/include/catalog/pg_operator.dat
index 6d9dc1528d6e..87a7255490a7 100644
--- a/src/include/catalog/pg_operator.dat
+++ b/src/include/catalog/pg_operator.dat
@@ -3460,4 +3460,30 @@
   oprcode => 'multirange_after_multirange', oprrest => 'multirangesel',
   oprjoin => 'scalargtjoinsel' },
 
+{ oid => '8262', descr => 'equal',
+  oprname => '=', oprcanmerge => 't', oprcanhash => 't', oprleft => 'oid8',
+  oprright => 'oid8', oprresult => 'bool', oprcom => '=(oid8,oid8)',
+  oprnegate => '<>(oid8,oid8)', oprcode => 'oid8eq', oprrest => 'eqsel',
+  oprjoin => 'eqjoinsel' },
+{ oid => '8263', descr => 'not equal',
+  oprname => '<>', oprleft => 'oid8', oprright => 'oid8', oprresult => 'bool',
+  oprcom => '<>(oid8,oid8)', oprnegate => '=(oid8,oid8)', oprcode => 'oid8ne',
+  oprrest => 'neqsel', oprjoin => 'neqjoinsel' },
+{ oid => '8264', descr => 'less than',
+  oprname => '<', oprleft => 'oid8', oprright => 'oid8', oprresult => 'bool',
+  oprcom => '>(oid8,oid8)', oprnegate => '>=(oid8,oid8)', oprcode => 'oid8lt',
+  oprrest => 'scalarltsel', oprjoin => 'scalarltjoinsel' },
+{ oid => '8265', descr => 'greater than',
+  oprname => '>', oprleft => 'oid8', oprright => 'oid8', oprresult => 'bool',
+  oprcom => '<(oid8,oid8)', oprnegate => '<=(oid8,oid8)', oprcode => 'oid8gt',
+  oprrest => 'scalargtsel', oprjoin => 'scalargtjoinsel' },
+{ oid => '8266', descr => 'less than or equal',
+  oprname => '<=', oprleft => 'oid8', oprright => 'oid8', oprresult => 'bool',
+  oprcom => '>=(oid8,oid8)', oprnegate => '>(oid8,oid8)', oprcode => 'oid8le',
+  oprrest => 'scalarlesel', oprjoin => 'scalarlejoinsel' },
+{ oid => '8267', descr => 'greater than or equal',
+  oprname => '>=', oprleft => 'oid8', oprright => 'oid8', oprresult => 'bool',
+  oprcom => '<=(oid8,oid8)', oprnegate => '<(oid8,oid8)', oprcode => 'oid8ge',
+  oprrest => 'scalargesel', oprjoin => 'scalargejoinsel' },
+
 ]
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index f7dcb96b43ce..54472ce97dcd 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -116,6 +116,10 @@
   opfmethod => 'hash', opfname => 'xid8_ops' },
 { oid => '5067',
   opfmethod => 'btree', opfname => 'xid8_ops' },
+{ oid => '8278',
+  opfmethod => 'hash', opfname => 'oid8_ops' },
+{ oid => '8279',
+  opfmethod => 'btree', opfname => 'oid8_ops' },
 { oid => '2226',
   opfmethod => 'hash', opfname => 'cid_ops' },
 { oid => '2227',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fd9448ec7b98..af0042d221b8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1046,6 +1046,15 @@
 { oid => '6405', descr => 'skip support',
   proname => 'btoidskipsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'btoidskipsupport' },
+{ oid => '8282', descr => 'less-equal-greater',
+  proname => 'btoid8cmp', proleakproof => 't', prorettype => 'int4',
+  proargtypes => 'oid8 oid8', prosrc => 'btoid8cmp' },
+{ oid => '8283', descr => 'sort support',
+  proname => 'btoid8sortsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoid8sortsupport' },
+{ oid => '8284', descr => 'skip support',
+  proname => 'btoid8skipsupport', prorettype => 'void',
+  proargtypes => 'internal', prosrc => 'btoid8skipsupport' },
 { oid => '404', descr => 'less-equal-greater',
   proname => 'btoidvectorcmp', proleakproof => 't', prorettype => 'int4',
   proargtypes => 'oidvector oidvector', prosrc => 'btoidvectorcmp' },
@@ -12612,4 +12621,59 @@
   proargnames => '{pid,io_id,io_generation,state,operation,off,length,target,handle_data_len,raw_result,result,target_desc,f_sync,f_localmem,f_buffered}',
   prosrc => 'pg_get_aios' },
 
+# oid8 related functions
+{ oid => '8255', descr => 'convert oid to oid8',
+  proname => 'oid8', prorettype => 'oid8', proargtypes => 'oid',
+  prosrc => 'oidtooid8' },
+{ oid => '8257', descr => 'I/O',
+  proname => 'oid8in', prorettype => 'oid8', proargtypes => 'cstring',
+  prosrc => 'oid8in' },
+{ oid => '8258', descr => 'I/O',
+  proname => 'oid8out', prorettype => 'cstring', proargtypes => 'oid8',
+  prosrc => 'oid8out' },
+{ oid => '8259', descr => 'I/O',
+  proname => 'oid8recv', prorettype => 'oid8', proargtypes => 'internal',
+  prosrc => 'oid8recv' },
+{ oid => '8260', descr => 'I/O',
+  proname => 'oid8send', prorettype => 'bytea', proargtypes => 'oid8',
+  prosrc => 'oid8send' },
+# Comparators
+{ oid => '8268',
+  proname => 'oid8eq', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'oid8 oid8', prosrc => 'oid8eq' },
+{ oid => '8269',
+  proname => 'oid8ne', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'oid8 oid8', prosrc => 'oid8ne' },
+{ oid => '8270',
+  proname => 'oid8lt', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'oid8 oid8', prosrc => 'oid8lt' },
+{ oid => '8271',
+  proname => 'oid8le', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'oid8 oid8', prosrc => 'oid8le' },
+{ oid => '8272',
+  proname => 'oid8gt', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'oid8 oid8', prosrc => 'oid8gt' },
+{ oid => '8273',
+  proname => 'oid8ge', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'oid8 oid8', prosrc => 'oid8ge' },
+# Aggregates
+{ oid => '8274', descr => 'larger of two',
+  proname => 'oid8larger', prorettype => 'oid8', proargtypes => 'oid8 oid8',
+  prosrc => 'oid8larger' },
+{ oid => '8275', descr => 'smaller of two',
+  proname => 'oid8smaller', prorettype => 'oid8', proargtypes => 'oid8 oid8',
+  prosrc => 'oid8smaller' },
+{ oid => '8276', descr => 'maximum value of all oid8 input values',
+  proname => 'max', prokind => 'a', proisstrict => 'f', prorettype => 'oid8',
+  proargtypes => 'oid8', prosrc => 'aggregate_dummy' },
+{ oid => '8277', descr => 'minimum value of all oid8 input values',
+  proname => 'min', prokind => 'a', proisstrict => 'f', prorettype => 'oid8',
+  proargtypes => 'oid8', prosrc => 'aggregate_dummy' },
+{ oid => '8280', descr => 'hash',
+  proname => 'hashoid8', prorettype => 'int4', proargtypes => 'oid8',
+  prosrc => 'hashoid8' },
+{ oid => '8281', descr => 'hash',
+  proname => 'hashoid8extended', prorettype => 'int8',
+  proargtypes => 'oid8 int8', prosrc => 'hashoid8extended' },
+
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index cb730aeac864..704f2890cb28 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -700,4 +700,9 @@
   typreceive => 'brin_minmax_multi_summary_recv',
   typsend => 'brin_minmax_multi_summary_send', typalign => 'i',
   typstorage => 'x', typcollation => 'default' },
+{ oid => '8256', array_type_oid => '8261',
+  descr => 'object identifier(oid8), 8 bytes',
+  typname => 'oid8', typlen => '8', typbyval => 't',
+  typcategory => 'N', typinput => 'oid8in', typoutput => 'oid8out',
+  typreceive => 'oid8recv', typsend => 'oid8send', typalign => 'd' },
 ]
diff --git a/src/include/fmgr.h b/src/include/fmgr.h
index c0dbe85ed1c3..9224377cae19 100644
--- a/src/include/fmgr.h
+++ b/src/include/fmgr.h
@@ -273,6 +273,7 @@ extern struct varlena *pg_detoast_datum_packed(struct varlena *datum);
 #define PG_GETARG_CHAR(n)	 DatumGetChar(PG_GETARG_DATUM(n))
 #define PG_GETARG_BOOL(n)	 DatumGetBool(PG_GETARG_DATUM(n))
 #define PG_GETARG_OID(n)	 DatumGetObjectId(PG_GETARG_DATUM(n))
+#define PG_GETARG_OID8(n)	 DatumGetObjectId8(PG_GETARG_DATUM(n))
 #define PG_GETARG_POINTER(n) DatumGetPointer(PG_GETARG_DATUM(n))
 #define PG_GETARG_CSTRING(n) DatumGetCString(PG_GETARG_DATUM(n))
 #define PG_GETARG_NAME(n)	 DatumGetName(PG_GETARG_DATUM(n))
@@ -358,6 +359,7 @@ extern struct varlena *pg_detoast_datum_packed(struct varlena *datum);
 #define PG_RETURN_CHAR(x)	 return CharGetDatum(x)
 #define PG_RETURN_BOOL(x)	 return BoolGetDatum(x)
 #define PG_RETURN_OID(x)	 return ObjectIdGetDatum(x)
+#define PG_RETURN_OID8(x)	 return ObjectId8GetDatum(x)
 #define PG_RETURN_POINTER(x) return PointerGetDatum(x)
 #define PG_RETURN_CSTRING(x) return CStringGetDatum(x)
 #define PG_RETURN_NAME(x)	 return NameGetDatum(x)
diff --git a/src/include/postgres.h b/src/include/postgres.h
index 357cbd6fd961..a5a0e3b7cbfa 100644
--- a/src/include/postgres.h
+++ b/src/include/postgres.h
@@ -264,6 +264,26 @@ ObjectIdGetDatum(Oid X)
 	return (Datum) X;
 }
 
+/*
+ * DatumGetObjectId8
+ *		Returns 8-byte object identifier value of a datum.
+ */
+static inline Oid8
+DatumGetObjectId8(Datum X)
+{
+	return (Oid8) X;
+}
+
+/*
+ * ObjectId8GetDatum
+ *		Returns datum representation for an 8-byte object identifier
+ */
+static inline Datum
+ObjectId8GetDatum(Oid8 X)
+{
+	return (Datum) X;
+}
+
 /*
  * DatumGetTransactionId
  *		Returns transaction identifier value of a datum.
diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c
index 188c27b4925f..3f59ba3f1ad0 100644
--- a/src/backend/access/nbtree/nbtcompare.c
+++ b/src/backend/access/nbtree/nbtcompare.c
@@ -498,6 +498,88 @@ btoidskipsupport(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+Datum
+btoid8cmp(PG_FUNCTION_ARGS)
+{
+	Oid8		a = PG_GETARG_OID8(0);
+	Oid8		b = PG_GETARG_OID8(1);
+
+	if (a > b)
+		PG_RETURN_INT32(A_GREATER_THAN_B);
+	else if (a == b)
+		PG_RETURN_INT32(0);
+	else
+		PG_RETURN_INT32(A_LESS_THAN_B);
+}
+
+static int
+btoid8fastcmp(Datum x, Datum y, SortSupport ssup)
+{
+	Oid8		a = DatumGetObjectId8(x);
+	Oid8		b = DatumGetObjectId8(y);
+
+	if (a > b)
+		return A_GREATER_THAN_B;
+	else if (a == b)
+		return 0;
+	else
+		return A_LESS_THAN_B;
+}
+
+Datum
+btoid8sortsupport(PG_FUNCTION_ARGS)
+{
+	SortSupport ssup = (SortSupport) PG_GETARG_POINTER(0);
+
+	ssup->comparator = btoid8fastcmp;
+	PG_RETURN_VOID();
+}
+
+static Datum
+oid8_decrement(Relation rel, Datum existing, bool *underflow)
+{
+	Oid8		oexisting = DatumGetObjectId8(existing);
+
+	if (oexisting == InvalidOid8)
+	{
+		/* return value is undefined */
+		*underflow = true;
+		return (Datum) 0;
+	}
+
+	*underflow = false;
+	return ObjectId8GetDatum(oexisting - 1);
+}
+
+static Datum
+oid8_increment(Relation rel, Datum existing, bool *overflow)
+{
+	Oid8		oexisting = DatumGetObjectId8(existing);
+
+	if (oexisting == OID8_MAX)
+	{
+		/* return value is undefined */
+		*overflow = true;
+		return (Datum) 0;
+	}
+
+	*overflow = false;
+	return ObjectId8GetDatum(oexisting + 1);
+}
+
+Datum
+btoid8skipsupport(PG_FUNCTION_ARGS)
+{
+	SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0);
+
+	sksup->decrement = oid8_decrement;
+	sksup->increment = oid8_increment;
+	sksup->low_elem = ObjectId8GetDatum(InvalidOid8);
+	sksup->high_elem = ObjectId8GetDatum(OID8_MAX);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 btoidvectorcmp(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/bootstrap/bootstrap.c b/src/backend/bootstrap/bootstrap.c
index 4986b1ea7ed0..7b7570d2f75b 100644
--- a/src/backend/bootstrap/bootstrap.c
+++ b/src/backend/bootstrap/bootstrap.c
@@ -115,6 +115,8 @@ static const struct typinfo TypInfo[] = {
 	F_TEXTIN, F_TEXTOUT},
 	{"oid", OIDOID, 0, 4, true, TYPALIGN_INT, TYPSTORAGE_PLAIN, InvalidOid,
 	F_OIDIN, F_OIDOUT},
+	{"oid8", OID8OID, 0, 8, true, TYPALIGN_DOUBLE, TYPSTORAGE_PLAIN, InvalidOid,
+	F_OID8IN, F_OID8OUT},
 	{"tid", TIDOID, 0, 6, false, TYPALIGN_SHORT, TYPSTORAGE_PLAIN, InvalidOid,
 	F_TIDIN, F_TIDOUT},
 	{"xid", XIDOID, 0, 4, true, TYPALIGN_INT, TYPSTORAGE_PLAIN, InvalidOid,
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index ba40ada11caf..a8fd680589f7 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -77,6 +77,7 @@ OBJS = \
 	numeric.o \
 	numutils.o \
 	oid.o \
+	oid8.o \
 	oracle_compat.o \
 	orderedsetaggs.o \
 	partitionfuncs.o \
diff --git a/src/backend/utils/adt/int8.c b/src/backend/utils/adt/int8.c
index 678f971508be..22bc4dee83c0 100644
--- a/src/backend/utils/adt/int8.c
+++ b/src/backend/utils/adt/int8.c
@@ -1370,6 +1370,14 @@ oidtoi8(PG_FUNCTION_ARGS)
 	PG_RETURN_INT64((int64) arg);
 }
 
+Datum
+oidtooid8(PG_FUNCTION_ARGS)
+{
+	Oid			arg = PG_GETARG_OID(0);
+
+	PG_RETURN_OID8((Oid8) arg);
+}
+
 /*
  * non-persistent numeric series generator
  */
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index 9c4c62d41da1..a90e1df035c3 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -73,6 +73,7 @@ backend_sources += files(
   'network_spgist.c',
   'numutils.c',
   'oid.c',
+  'oid8.c',
   'oracle_compat.c',
   'orderedsetaggs.c',
   'partitionfuncs.c',
diff --git a/src/backend/utils/adt/oid8.c b/src/backend/utils/adt/oid8.c
new file mode 100644
index 000000000000..6e9ffd96303f
--- /dev/null
+++ b/src/backend/utils/adt/oid8.c
@@ -0,0 +1,171 @@
+/*-------------------------------------------------------------------------
+ *
+ * oid8.c
+ *	  Functions for the built-in type Oid8
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/oid8.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include <ctype.h>
+#include <limits.h>
+
+#include "catalog/pg_type.h"
+#include "libpq/pqformat.h"
+#include "utils/builtins.h"
+
+#define MAXOID8LEN 20
+
+/*****************************************************************************
+ *	 USER I/O ROUTINES														 *
+ *****************************************************************************/
+
+Datum
+oid8in(PG_FUNCTION_ARGS)
+{
+	char	   *s = PG_GETARG_CSTRING(0);
+	Oid8		result;
+
+	result = uint64in_subr(s, NULL, "oid8", fcinfo->context);
+	PG_RETURN_OID8(result);
+}
+
+Datum
+oid8out(PG_FUNCTION_ARGS)
+{
+	Oid8		val = PG_GETARG_OID8(0);
+	char		buf[MAXOID8LEN + 1];
+	char	   *result;
+	int			len;
+
+	len = pg_ulltoa_n(val, buf) + 1;
+	buf[len - 1] = '\0';
+
+	/*
+	 * Since the length is already known, we do a manual palloc() and memcpy()
+	 * to avoid the strlen() call that would otherwise be done in pstrdup().
+	 */
+	result = palloc(len);
+	memcpy(result, buf, len);
+	PG_RETURN_CSTRING(result);
+}
+
+/*
+ *		oid8recv			- converts external binary format to oid8
+ */
+Datum
+oid8recv(PG_FUNCTION_ARGS)
+{
+	StringInfo	buf = (StringInfo) PG_GETARG_POINTER(0);
+
+	PG_RETURN_OID8(pq_getmsgint64(buf));
+}
+
+/*
+ *		oid8send			- converts oid8 to binary format
+ */
+Datum
+oid8send(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	StringInfoData buf;
+
+	pq_begintypsend(&buf);
+	pq_sendint64(&buf, arg1);
+	PG_RETURN_BYTEA_P(pq_endtypsend(&buf));
+}
+
+/*****************************************************************************
+ *	 PUBLIC ROUTINES														 *
+ *****************************************************************************/
+
+Datum
+oid8eq(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	Oid8		arg2 = PG_GETARG_OID8(1);
+
+	PG_RETURN_BOOL(arg1 == arg2);
+}
+
+Datum
+oid8ne(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	Oid8		arg2 = PG_GETARG_OID8(1);
+
+	PG_RETURN_BOOL(arg1 != arg2);
+}
+
+Datum
+oid8lt(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	Oid8		arg2 = PG_GETARG_OID8(1);
+
+	PG_RETURN_BOOL(arg1 < arg2);
+}
+
+Datum
+oid8le(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	Oid8		arg2 = PG_GETARG_OID8(1);
+
+	PG_RETURN_BOOL(arg1 <= arg2);
+}
+
+Datum
+oid8ge(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	Oid8		arg2 = PG_GETARG_OID8(1);
+
+	PG_RETURN_BOOL(arg1 >= arg2);
+}
+
+Datum
+oid8gt(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	Oid8		arg2 = PG_GETARG_OID8(1);
+
+	PG_RETURN_BOOL(arg1 > arg2);
+}
+
+Datum
+hashoid8(PG_FUNCTION_ARGS)
+{
+	return hashint8(fcinfo);
+}
+
+Datum
+hashoid8extended(PG_FUNCTION_ARGS)
+{
+	return hashint8extended(fcinfo);
+}
+
+Datum
+oid8larger(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	Oid8		arg2 = PG_GETARG_OID8(1);
+
+	PG_RETURN_OID8((arg1 > arg2) ? arg1 : arg2);
+}
+
+Datum
+oid8smaller(PG_FUNCTION_ARGS)
+{
+	Oid8		arg1 = PG_GETARG_OID8(0);
+	Oid8		arg2 = PG_GETARG_OID8(1);
+
+	PG_RETURN_OID8((arg1 < arg2) ? arg1 : arg2);
+}
diff --git a/src/fe_utils/print.c b/src/fe_utils/print.c
index 4d97ad2ddeb7..7450e95dc513 100644
--- a/src/fe_utils/print.c
+++ b/src/fe_utils/print.c
@@ -3821,6 +3821,7 @@ column_type_alignment(Oid ftype)
 		case FLOAT8OID:
 		case NUMERICOID:
 		case OIDOID:
+		case OID8OID:
 		case XIDOID:
 		case XID8OID:
 		case CIDOID:
diff --git a/src/test/regress/expected/oid8.out b/src/test/regress/expected/oid8.out
new file mode 100644
index 000000000000..80529214ca53
--- /dev/null
+++ b/src/test/regress/expected/oid8.out
@@ -0,0 +1,196 @@
+--
+-- OID8
+--
+CREATE TABLE OID8_TBL(f1 oid8);
+INSERT INTO OID8_TBL(f1) VALUES ('1234');
+INSERT INTO OID8_TBL(f1) VALUES ('1235');
+INSERT INTO OID8_TBL(f1) VALUES ('987');
+INSERT INTO OID8_TBL(f1) VALUES ('-1040');
+INSERT INTO OID8_TBL(f1) VALUES ('99999999');
+INSERT INTO OID8_TBL(f1) VALUES ('5     ');
+INSERT INTO OID8_TBL(f1) VALUES ('   10  ');
+-- leading/trailing hard tab is also allowed
+INSERT INTO OID8_TBL(f1) VALUES ('	  15 	  ');
+-- bad inputs
+INSERT INTO OID8_TBL(f1) VALUES ('');
+ERROR:  invalid input syntax for type oid8: ""
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('');
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES ('    ');
+ERROR:  invalid input syntax for type oid8: "    "
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('    ');
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES ('asdfasd');
+ERROR:  invalid input syntax for type oid8: "asdfasd"
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('asdfasd');
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES ('99asdfasd');
+ERROR:  invalid input syntax for type oid8: "99asdfasd"
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('99asdfasd');
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES ('5    d');
+ERROR:  invalid input syntax for type oid8: "5    d"
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('5    d');
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES ('    5d');
+ERROR:  invalid input syntax for type oid8: "    5d"
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('    5d');
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES ('5    5');
+ERROR:  invalid input syntax for type oid8: "5    5"
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('5    5');
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES (' - 500');
+ERROR:  invalid input syntax for type oid8: " - 500"
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES (' - 500');
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES ('3908203590239580293850293850329485');
+ERROR:  value "3908203590239580293850293850329485" is out of range for type oid8
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('39082035902395802938502938...
+                                         ^
+INSERT INTO OID8_TBL(f1) VALUES ('-1204982019841029840928340329840934');
+ERROR:  value "-1204982019841029840928340329840934" is out of range for type oid8
+LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('-1204982019841029840928340...
+                                         ^
+SELECT * FROM OID8_TBL;
+          f1          
+----------------------
+                 1234
+                 1235
+                  987
+ 18446744073709550576
+             99999999
+                    5
+                   10
+                   15
+(8 rows)
+
+-- Also try it with non-error-throwing API
+SELECT pg_input_is_valid('1234', 'oid8');
+ pg_input_is_valid 
+-------------------
+ t
+(1 row)
+
+SELECT pg_input_is_valid('01XYZ', 'oid8');
+ pg_input_is_valid 
+-------------------
+ f
+(1 row)
+
+SELECT * FROM pg_input_error_info('01XYZ', 'oid8');
+                   message                   | detail | hint | sql_error_code 
+---------------------------------------------+--------+------+----------------
+ invalid input syntax for type oid8: "01XYZ" |        |      | 22P02
+(1 row)
+
+SELECT pg_input_is_valid('3908203590239580293850293850329485', 'oid8');
+ pg_input_is_valid 
+-------------------
+ f
+(1 row)
+
+SELECT * FROM pg_input_error_info('-1204982019841029840928340329840934', 'oid8');
+                                  message                                  | detail | hint | sql_error_code 
+---------------------------------------------------------------------------+--------+------+----------------
+ value "-1204982019841029840928340329840934" is out of range for type oid8 |        |      | 22003
+(1 row)
+
+-- Operators
+SELECT o.* FROM OID8_TBL o WHERE o.f1 = 1234;
+  f1  
+------
+ 1234
+(1 row)
+
+SELECT o.* FROM OID8_TBL o WHERE o.f1 <> '1234';
+          f1          
+----------------------
+                 1235
+                  987
+ 18446744073709550576
+             99999999
+                    5
+                   10
+                   15
+(7 rows)
+
+SELECT o.* FROM OID8_TBL o WHERE o.f1 <= '1234';
+  f1  
+------
+ 1234
+  987
+    5
+   10
+   15
+(5 rows)
+
+SELECT o.* FROM OID8_TBL o WHERE o.f1 < '1234';
+ f1  
+-----
+ 987
+   5
+  10
+  15
+(4 rows)
+
+SELECT o.* FROM OID8_TBL o WHERE o.f1 >= '1234';
+          f1          
+----------------------
+                 1234
+                 1235
+ 18446744073709550576
+             99999999
+(4 rows)
+
+SELECT o.* FROM OID8_TBL o WHERE o.f1 > '1234';
+          f1          
+----------------------
+                 1235
+ 18446744073709550576
+             99999999
+(3 rows)
+
+-- Casts
+SELECT 1::int2::oid8;
+ oid8 
+------
+    1
+(1 row)
+
+SELECT 1::int4::oid8;
+ oid8 
+------
+    1
+(1 row)
+
+SELECT 1::int8::oid8;
+ oid8 
+------
+    1
+(1 row)
+
+SELECT 1::oid8::int8;
+ int8 
+------
+    1
+(1 row)
+
+SELECT 1::oid::oid8; -- ok
+ oid8 
+------
+    1
+(1 row)
+
+SELECT 1::oid8::oid; -- not ok
+ERROR:  cannot cast type oid8 to oid
+LINE 1: SELECT 1::oid8::oid;
+                      ^
+-- Aggregates
+SELECT min(f1), max(f1) FROM OID8_TBL;
+ min |         max          
+-----+----------------------
+   5 | 18446744073709550576
+(1 row)
+
+DROP TABLE OID8_TBL;
diff --git a/src/test/regress/expected/oid8.sql b/src/test/regress/expected/oid8.sql
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index a357e1d0c0e1..6ff4d7ee9014 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -880,6 +880,13 @@ bytea(integer)
 bytea(bigint)
 bytea_larger(bytea,bytea)
 bytea_smaller(bytea,bytea)
+oid8eq(oid8,oid8)
+oid8ne(oid8,oid8)
+oid8lt(oid8,oid8)
+oid8le(oid8,oid8)
+oid8gt(oid8,oid8)
+oid8ge(oid8,oid8)
+btoid8cmp(oid8,oid8)
 -- Check that functions without argument are not marked as leakproof.
 SELECT p1.oid::regprocedure
 FROM pg_proc p1 JOIN pg_namespace pn
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 943e56506bf1..9ddcacec6bf4 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -702,6 +702,7 @@ CREATE TABLE tab_core_types AS SELECT
   'abc'::refcursor,
   '1 2'::int2vector,
   '1 2'::oidvector,
+  '1234'::oid8,
   format('%I=UC/%I', USER, USER)::aclitem AS aclitem,
   'a fat cat sat on a mat and ate a fat rat'::tsvector,
   'fat & rat'::tsquery,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 905f9bca9598..021d57f66bbd 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -28,7 +28,7 @@ test: strings md5 numerology point lseg line box path polygon circle date time t
 # geometry depends on point, lseg, line, box, path, polygon, circle
 # horology depends on date, time, timetz, timestamp, timestamptz, interval
 # ----------
-test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import pg_ndistinct pg_dependencies
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import pg_ndistinct pg_dependencies oid8
 
 # ----------
 # Load huge amounts of data
diff --git a/src/test/regress/sql/oid8.sql b/src/test/regress/sql/oid8.sql
new file mode 100644
index 000000000000..c4f2ae6a2e57
--- /dev/null
+++ b/src/test/regress/sql/oid8.sql
@@ -0,0 +1,57 @@
+--
+-- OID8
+--
+
+CREATE TABLE OID8_TBL(f1 oid8);
+
+INSERT INTO OID8_TBL(f1) VALUES ('1234');
+INSERT INTO OID8_TBL(f1) VALUES ('1235');
+INSERT INTO OID8_TBL(f1) VALUES ('987');
+INSERT INTO OID8_TBL(f1) VALUES ('-1040');
+INSERT INTO OID8_TBL(f1) VALUES ('99999999');
+INSERT INTO OID8_TBL(f1) VALUES ('5     ');
+INSERT INTO OID8_TBL(f1) VALUES ('   10  ');
+-- leading/trailing hard tab is also allowed
+INSERT INTO OID8_TBL(f1) VALUES ('	  15 	  ');
+
+-- bad inputs
+INSERT INTO OID8_TBL(f1) VALUES ('');
+INSERT INTO OID8_TBL(f1) VALUES ('    ');
+INSERT INTO OID8_TBL(f1) VALUES ('asdfasd');
+INSERT INTO OID8_TBL(f1) VALUES ('99asdfasd');
+INSERT INTO OID8_TBL(f1) VALUES ('5    d');
+INSERT INTO OID8_TBL(f1) VALUES ('    5d');
+INSERT INTO OID8_TBL(f1) VALUES ('5    5');
+INSERT INTO OID8_TBL(f1) VALUES (' - 500');
+INSERT INTO OID8_TBL(f1) VALUES ('3908203590239580293850293850329485');
+INSERT INTO OID8_TBL(f1) VALUES ('-1204982019841029840928340329840934');
+
+SELECT * FROM OID8_TBL;
+
+-- Also try it with non-error-throwing API
+SELECT pg_input_is_valid('1234', 'oid8');
+SELECT pg_input_is_valid('01XYZ', 'oid8');
+SELECT * FROM pg_input_error_info('01XYZ', 'oid8');
+SELECT pg_input_is_valid('3908203590239580293850293850329485', 'oid8');
+SELECT * FROM pg_input_error_info('-1204982019841029840928340329840934', 'oid8');
+
+-- Operators
+SELECT o.* FROM OID8_TBL o WHERE o.f1 = 1234;
+SELECT o.* FROM OID8_TBL o WHERE o.f1 <> '1234';
+SELECT o.* FROM OID8_TBL o WHERE o.f1 <= '1234';
+SELECT o.* FROM OID8_TBL o WHERE o.f1 < '1234';
+SELECT o.* FROM OID8_TBL o WHERE o.f1 >= '1234';
+SELECT o.* FROM OID8_TBL o WHERE o.f1 > '1234';
+
+-- Casts
+SELECT 1::int2::oid8;
+SELECT 1::int4::oid8;
+SELECT 1::int8::oid8;
+SELECT 1::oid8::int8;
+SELECT 1::oid::oid8; -- ok
+SELECT 1::oid8::oid; -- not ok
+
+-- Aggregates
+SELECT min(f1), max(f1) FROM OID8_TBL;
+
+DROP TABLE OID8_TBL;
diff --git a/src/test/regress/sql/type_sanity.sql b/src/test/regress/sql/type_sanity.sql
index df795759bb4c..c2496823d90e 100644
--- a/src/test/regress/sql/type_sanity.sql
+++ b/src/test/regress/sql/type_sanity.sql
@@ -530,6 +530,7 @@ CREATE TABLE tab_core_types AS SELECT
   'abc'::refcursor,
   '1 2'::int2vector,
   '1 2'::oidvector,
+  '1234'::oid8,
   format('%I=UC/%I', USER, USER)::aclitem AS aclitem,
   'a fat cat sat on a mat and ate a fat rat'::tsvector,
   'fat & rat'::tsquery,
diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml
index e5267a8e4be6..3cda7022e461 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -4724,6 +4724,10 @@ INSERT INTO mytable VALUES(-1);  -- fails
     <primary>oid</primary>
    </indexterm>
 
+   <indexterm zone="datatype-oid">
+    <primary>oid8</primary>
+   </indexterm>
+
    <indexterm zone="datatype-oid">
     <primary>regclass</primary>
    </indexterm>
@@ -4806,6 +4810,13 @@ INSERT INTO mytable VALUES(-1);  -- fails
     individual tables.
    </para>
 
+   <para>
+    In some contexts, a 64-bit variant <type>oid8</type> is used.
+    It is implemented as an unsigned eight-byte integer. Unlike its
+    <type>oid</type> counterpart, it can ensure uniqueness in large
+    individual tables.
+   </para>
+
    <para>
     The <type>oid</type> type itself has few operations beyond comparison.
     It can be cast to integer, however, and then manipulated using the
diff --git a/doc/src/sgml/func/func-aggregate.sgml b/doc/src/sgml/func/func-aggregate.sgml
index f50b692516b6..a5396048adf3 100644
--- a/doc/src/sgml/func/func-aggregate.sgml
+++ b/doc/src/sgml/func/func-aggregate.sgml
@@ -508,8 +508,8 @@
         Computes the maximum of the non-null input
         values.  Available for any numeric, string, date/time, or enum type,
         as well as <type>bytea</type>, <type>inet</type>, <type>interval</type>,
-        <type>money</type>, <type>oid</type>, <type>pg_lsn</type>,
-        <type>tid</type>, <type>xid8</type>,
+        <type>money</type>, <type>oid</type>, <type>oid8</type>,
+        <type>pg_lsn</type>, <type>tid</type>, <type>xid8</type>,
         and also arrays and composite types containing sortable data types.
        </para></entry>
        <entry>Yes</entry>
@@ -527,8 +527,8 @@
         Computes the minimum of the non-null input
         values.  Available for any numeric, string, date/time, or enum type,
         as well as <type>bytea</type>, <type>inet</type>, <type>interval</type>,
-        <type>money</type>, <type>oid</type>, <type>pg_lsn</type>,
-        <type>tid</type>, <type>xid8</type>,
+        <type>money</type>, <type>oid</type>, <type>oid8</type>,
+        <type>pg_lsn</type>, <type>tid</type>, <type>xid8</type>,
         and also arrays and composite types containing sortable data types.
        </para></entry>
        <entry>Yes</entry>
-- 
2.51.0

v9-0002-Refactor-some-TOAST-value-ID-code-to-use-Oid8-ins.patchtext/x-diff; charset=us-asciiDownload
From 1fa8fdc31c94f3462d224722ea5f914962c51da4 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 13 Aug 2025 17:26:36 +0900
Subject: [PATCH v9 02/15] Refactor some TOAST value ID code to use Oid8
 instead of Oid

This change is a mechanical switch to change most of the code paths that
assume TOAST value IDs to be Oids to become Oid8, easing an upcoming
change to allow larger TOAST values, at 8 bytes.

The areas touched are related to table AM, amcheck and logical
decoding's reorder buffer.  A good chunk of the changes involve
switching printf() markers from %u to OID8_FORMAT.
---
 src/include/access/heaptoast.h                |  2 +-
 src/include/access/tableam.h                  |  4 +-
 src/backend/access/common/toast_internals.c   |  8 +--
 src/backend/access/heap/heaptoast.c           | 12 ++--
 .../replication/logical/reorderbuffer.c       | 14 +++--
 contrib/amcheck/verify_heapam.c               | 56 +++++++++++--------
 6 files changed, 53 insertions(+), 43 deletions(-)

diff --git a/src/include/access/heaptoast.h b/src/include/access/heaptoast.h
index 1c68f8107d6f..de6b3e2212a5 100644
--- a/src/include/access/heaptoast.h
+++ b/src/include/access/heaptoast.h
@@ -142,7 +142,7 @@ extern HeapTuple toast_build_flattened_tuple(TupleDesc tupleDesc,
  *	Fetch a slice from a toast value stored in a heap table.
  * ----------
  */
-extern void heap_fetch_toast_slice(Relation toastrel, Oid valueid,
+extern void heap_fetch_toast_slice(Relation toastrel, Oid8 valueid,
 								   int32 attrsize, int32 sliceoffset,
 								   int32 slicelength, struct varlena *result);
 
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index 2fa790b6bf54..eb24a93e2902 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -746,7 +746,7 @@ typedef struct TableAmRoutine
 	 * table implemented by this AM.  See table_relation_fetch_toast_slice()
 	 * for more details.
 	 */
-	void		(*relation_fetch_toast_slice) (Relation toastrel, Oid valueid,
+	void		(*relation_fetch_toast_slice) (Relation toastrel, Oid8 valueid,
 											   int32 attrsize,
 											   int32 sliceoffset,
 											   int32 slicelength,
@@ -1892,7 +1892,7 @@ table_relation_toast_am(Relation rel)
  * stored.
  */
 static inline void
-table_relation_fetch_toast_slice(Relation toastrel, Oid valueid,
+table_relation_fetch_toast_slice(Relation toastrel, Oid8 valueid,
 								 int32 attrsize, int32 sliceoffset,
 								 int32 slicelength, struct varlena *result)
 {
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index d06af82de15d..f5a50c00c2e3 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -26,8 +26,8 @@
 #include "utils/rel.h"
 #include "utils/snapmgr.h"
 
-static bool toastrel_valueid_exists(Relation toastrel, Oid valueid);
-static bool toastid_valueid_exists(Oid toastrelid, Oid valueid);
+static bool toastrel_valueid_exists(Relation toastrel, Oid8 valueid);
+static bool toastid_valueid_exists(Oid toastrelid, Oid8 valueid);
 
 /* ----------
  * toast_compress_datum -
@@ -447,7 +447,7 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
  * ----------
  */
 static bool
-toastrel_valueid_exists(Relation toastrel, Oid valueid)
+toastrel_valueid_exists(Relation toastrel, Oid8 valueid)
 {
 	bool		result = false;
 	ScanKeyData toastkey;
@@ -495,7 +495,7 @@ toastrel_valueid_exists(Relation toastrel, Oid valueid)
  * ----------
  */
 static bool
-toastid_valueid_exists(Oid toastrelid, Oid valueid)
+toastid_valueid_exists(Oid toastrelid, Oid8 valueid)
 {
 	bool		result;
 	Relation	toastrel;
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index 60e765fbfce1..38b2480c33fa 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -623,7 +623,7 @@ toast_build_flattened_tuple(TupleDesc tupleDesc,
  * result is the varlena into which the results should be written.
  */
 void
-heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
+heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 					   int32 sliceoffset, int32 slicelength,
 					   struct varlena *result)
 {
@@ -725,7 +725,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
 		else
 		{
 			/* should never happen */
-			elog(ERROR, "found toasted toast chunk for toast value %u in %s",
+			elog(ERROR, "found toasted toast chunk for toast value " OID8_FORMAT " in %s",
 				 valueid, RelationGetRelationName(toastrel));
 			chunksize = 0;		/* keep compiler quiet */
 			chunkdata = NULL;
@@ -737,13 +737,13 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
 		if (curchunk != expectedchunk)
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
-					 errmsg_internal("unexpected chunk number %d (expected %d) for toast value %u in %s",
+					 errmsg_internal("unexpected chunk number %d (expected %d) for toast value " OID8_FORMAT " in %s",
 									 curchunk, expectedchunk, valueid,
 									 RelationGetRelationName(toastrel))));
 		if (curchunk > endchunk)
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
-					 errmsg_internal("unexpected chunk number %d (out of range %d..%d) for toast value %u in %s",
+					 errmsg_internal("unexpected chunk number %d (out of range %d..%d) for toast value " OID8_FORMAT " in %s",
 									 curchunk,
 									 startchunk, endchunk, valueid,
 									 RelationGetRelationName(toastrel))));
@@ -752,7 +752,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
 		if (chunksize != expected_size)
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
-					 errmsg_internal("unexpected chunk size %d (expected %d) in chunk %d of %d for toast value %u in %s",
+					 errmsg_internal("unexpected chunk size %d (expected %d) in chunk %d of %d for toast value " OID8_FORMAT " in %s",
 									 chunksize, expected_size,
 									 curchunk, totalchunks, valueid,
 									 RelationGetRelationName(toastrel))));
@@ -781,7 +781,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
 	if (expectedchunk != (endchunk + 1))
 		ereport(ERROR,
 				(errcode(ERRCODE_DATA_CORRUPTED),
-				 errmsg_internal("missing chunk number %d for toast value %u in %s",
+				 errmsg_internal("missing chunk number %d for toast value " OID8_FORMAT " in %s",
 								 expectedchunk, valueid,
 								 RelationGetRelationName(toastrel))));
 
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index f18c6fb52b57..4c59ca2453be 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -176,7 +176,7 @@ typedef struct ReorderBufferIterTXNState
 /* toast datastructures */
 typedef struct ReorderBufferToastEnt
 {
-	Oid			chunk_id;		/* toast_table.chunk_id */
+	Oid8		chunk_id;		/* toast_table.chunk_id */
 	int32		last_chunk_seq; /* toast_table.chunk_seq of the last chunk we
 								 * have seen */
 	Size		num_chunks;		/* number of chunks we've already seen */
@@ -4978,7 +4978,7 @@ ReorderBufferToastInitHash(ReorderBuffer *rb, ReorderBufferTXN *txn)
 
 	Assert(txn->toast_hash == NULL);
 
-	hash_ctl.keysize = sizeof(Oid);
+	hash_ctl.keysize = sizeof(Oid8);
 	hash_ctl.entrysize = sizeof(ReorderBufferToastEnt);
 	hash_ctl.hcxt = rb->context;
 	txn->toast_hash = hash_create("ReorderBufferToastHash", 5, &hash_ctl,
@@ -5002,7 +5002,7 @@ ReorderBufferToastAppendChunk(ReorderBuffer *rb, ReorderBufferTXN *txn,
 	bool		isnull;
 	Pointer		chunk;
 	TupleDesc	desc = RelationGetDescr(relation);
-	Oid			chunk_id;
+	Oid8		chunk_id;
 	int32		chunk_seq;
 
 	if (txn->toast_hash == NULL)
@@ -5029,11 +5029,11 @@ ReorderBufferToastAppendChunk(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		dlist_init(&ent->chunks);
 
 		if (chunk_seq != 0)
-			elog(ERROR, "got sequence entry %d for toast chunk %u instead of seq 0",
+			elog(ERROR, "got sequence entry %d for toast chunk " OID8_FORMAT " instead of seq 0",
 				 chunk_seq, chunk_id);
 	}
 	else if (found && chunk_seq != ent->last_chunk_seq + 1)
-		elog(ERROR, "got sequence entry %d for toast chunk %u instead of seq %d",
+		elog(ERROR, "got sequence entry %d for toast chunk " OID8_FORMAT " instead of seq %d",
 			 chunk_seq, chunk_id, ent->last_chunk_seq + 1);
 
 	chunk = DatumGetPointer(fastgetattr(newtup, 3, desc, &isnull));
@@ -5142,6 +5142,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		struct varlena *reconstructed;
 		dlist_iter	it;
 		Size		data_done = 0;
+		Oid8		toast_valueid;
 
 		if (attr->attisdropped)
 			continue;
@@ -5162,13 +5163,14 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 			continue;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, varlena);
+		toast_valueid = toast_pointer.va_valueid;
 
 		/*
 		 * Check whether the toast tuple changed, replace if so.
 		 */
 		ent = (ReorderBufferToastEnt *)
 			hash_search(txn->toast_hash,
-						&toast_pointer.va_valueid,
+						&toast_valueid,
 						HASH_FIND,
 						NULL);
 		if (ent == NULL)
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 130b35334639..99ff07f970da 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1561,6 +1561,9 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	bool		isnull;
 	int32		chunksize;
 	int32		expected_size;
+	Oid8		toast_valueid;
+
+	toast_valueid = ta->toast_pointer.va_valueid;
 
 	/* Sanity-check the sequence number. */
 	chunk_seq = DatumGetInt32(fastgetattr(toasttup, 2,
@@ -1568,16 +1571,16 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	if (isnull)
 	{
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u has toast chunk with null sequence number",
-										 ta->toast_pointer.va_valueid));
+								psprintf("toast value " OID8_FORMAT " has toast chunk with null sequence number",
+										 toast_valueid));
 		return;
 	}
 	if (chunk_seq != *expected_chunk_seq)
 	{
 		/* Either the TOAST index is corrupt, or we don't have all chunks. */
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u index scan returned chunk %d when expecting chunk %d",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value " OID8_FORMAT " index scan returned chunk %d when expecting chunk %d",
+										 toast_valueid,
 										 chunk_seq, *expected_chunk_seq));
 	}
 	*expected_chunk_seq = chunk_seq + 1;
@@ -1588,8 +1591,8 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	if (isnull)
 	{
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u chunk %d has null data",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value " OID8_FORMAT " chunk %d has null data",
+										 toast_valueid,
 										 chunk_seq));
 		return;
 	}
@@ -1608,8 +1611,8 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 		uint32		header = ((varattrib_4b *) chunk)->va_4byte.va_header;
 
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u chunk %d has invalid varlena header %0x",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value " OID8_FORMAT " chunk %d has invalid varlena header %0x",
+										 toast_valueid,
 										 chunk_seq, header));
 		return;
 	}
@@ -1620,8 +1623,8 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	if (chunk_seq > last_chunk_seq)
 	{
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u chunk %d follows last expected chunk %d",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value " OID8_FORMAT " chunk %d follows last expected chunk %d",
+										 toast_valueid,
 										 chunk_seq, last_chunk_seq));
 		return;
 	}
@@ -1631,8 +1634,8 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 
 	if (chunksize != expected_size)
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u chunk %d has size %u, but expected size %u",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value " OID8_FORMAT " chunk %d has size %u, but expected size %u",
+										 toast_valueid,
 										 chunk_seq, chunksize, expected_size));
 }
 
@@ -1663,6 +1666,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	struct varlena *attr;
 	char	   *tp;				/* pointer to the tuple data */
 	uint16		infomask;
+	Oid8		toast_pointer_valueid;
 	CompactAttribute *thisatt;
 	struct varatt_external toast_pointer;
 
@@ -1771,12 +1775,13 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	 * Must copy attr into toast_pointer for alignment considerations
 	 */
 	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_pointer_valueid = toast_pointer.va_valueid;
 
 	/* Toasted attributes too large to be untoasted should never be stored */
 	if (toast_pointer.va_rawsize > VARLENA_SIZE_LIMIT)
 		report_corruption(ctx,
-						  psprintf("toast value %u rawsize %d exceeds limit %d",
-								   toast_pointer.va_valueid,
+						  psprintf("toast value " OID8_FORMAT " rawsize %d exceeds limit %d",
+								   toast_pointer_valueid,
 								   toast_pointer.va_rawsize,
 								   VARLENA_SIZE_LIMIT));
 
@@ -1803,16 +1808,16 @@ check_tuple_attribute(HeapCheckContext *ctx)
 		}
 		if (!valid)
 			report_corruption(ctx,
-							  psprintf("toast value %u has invalid compression method id %d",
-									   toast_pointer.va_valueid, cmid));
+							  psprintf("toast value " OID8_FORMAT " has invalid compression method id %d",
+									   toast_pointer_valueid, cmid));
 	}
 
 	/* The tuple header better claim to contain toasted values */
 	if (!(infomask & HEAP_HASEXTERNAL))
 	{
 		report_corruption(ctx,
-						  psprintf("toast value %u is external but tuple header flag HEAP_HASEXTERNAL not set",
-								   toast_pointer.va_valueid));
+						  psprintf("toast value " OID8_FORMAT " is external but tuple header flag HEAP_HASEXTERNAL not set",
+								   toast_pointer_valueid));
 		return true;
 	}
 
@@ -1820,8 +1825,8 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	if (!ctx->rel->rd_rel->reltoastrelid)
 	{
 		report_corruption(ctx,
-						  psprintf("toast value %u is external but relation has no toast relation",
-								   toast_pointer.va_valueid));
+						  psprintf("toast value " OID8_FORMAT " is external but relation has no toast relation",
+								   toast_pointer_valueid));
 		return true;
 	}
 
@@ -1866,6 +1871,7 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	uint32		extsize;
 	int32		expected_chunk_seq = 0;
 	int32		last_chunk_seq;
+	Oid8		toast_valueid;
 
 	extsize = VARATT_EXTERNAL_GET_EXTSIZE(ta->toast_pointer);
 	last_chunk_seq = (extsize - 1) / TOAST_MAX_CHUNK_SIZE;
@@ -1896,14 +1902,16 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	}
 	systable_endscan_ordered(toastscan);
 
+	toast_valueid = ta->toast_pointer.va_valueid;
+
 	if (!found_toasttup)
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u not found in toast table",
-										 ta->toast_pointer.va_valueid));
+								psprintf("toast value " OID8_FORMAT " not found in toast table",
+										 toast_valueid));
 	else if (expected_chunk_seq <= last_chunk_seq)
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u was expected to end at chunk %d, but ended while expecting chunk %d",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value " OID8_FORMAT " was expected to end at chunk %d, but ended while expecting chunk %d",
+										 toast_valueid,
 										 last_chunk_seq, expected_chunk_seq));
 }
 
-- 
2.51.0

v9-0003-Minimize-footprint-of-TOAST_MAX_CHUNK_SIZE-in-hea.patchtext/x-diff; charset=us-asciiDownload
From 976e601b658f5181ead73ff2fbdcccf741b518c6 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 13 Aug 2025 17:40:13 +0900
Subject: [PATCH v9 03/15] Minimize footprint of TOAST_MAX_CHUNK_SIZE in heap
 and amcheck

This eases a follow-up change to support 8-byte TOAST value IDs, as the
maximum chunk size allowed for a single chunk of TOASTed data depends on
the size of the value ID.
---
 src/backend/access/heap/heaptoast.c | 20 ++++++++++++--------
 contrib/amcheck/verify_heapam.c     | 13 +++++++++----
 2 files changed, 21 insertions(+), 12 deletions(-)

diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index 38b2480c33fa..d134f0292a6f 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -634,11 +634,12 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 	SysScanDesc toastscan;
 	HeapTuple	ttup;
 	int32		expectedchunk;
-	int32		totalchunks = ((attrsize - 1) / TOAST_MAX_CHUNK_SIZE) + 1;
+	int32		totalchunks;
 	int			startchunk;
 	int			endchunk;
 	int			num_indexes;
 	int			validIndex;
+	int32		max_chunk_size;
 
 	/* Look for the valid index of toast relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -646,8 +647,11 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 									&toastidxs,
 									&num_indexes);
 
-	startchunk = sliceoffset / TOAST_MAX_CHUNK_SIZE;
-	endchunk = (sliceoffset + slicelength - 1) / TOAST_MAX_CHUNK_SIZE;
+	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+
+	totalchunks = ((attrsize - 1) / max_chunk_size) + 1;
+	startchunk = sliceoffset / max_chunk_size;
+	endchunk = (sliceoffset + slicelength - 1) / max_chunk_size;
 	Assert(endchunk <= totalchunks);
 
 	/* Set up a scan key to fetch from the index. */
@@ -747,8 +751,8 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 									 curchunk,
 									 startchunk, endchunk, valueid,
 									 RelationGetRelationName(toastrel))));
-		expected_size = curchunk < totalchunks - 1 ? TOAST_MAX_CHUNK_SIZE
-			: attrsize - ((totalchunks - 1) * TOAST_MAX_CHUNK_SIZE);
+		expected_size = curchunk < totalchunks - 1 ? max_chunk_size
+			: attrsize - ((totalchunks - 1) * max_chunk_size);
 		if (chunksize != expected_size)
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
@@ -763,12 +767,12 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 		chcpystrt = 0;
 		chcpyend = chunksize - 1;
 		if (curchunk == startchunk)
-			chcpystrt = sliceoffset % TOAST_MAX_CHUNK_SIZE;
+			chcpystrt = sliceoffset % max_chunk_size;
 		if (curchunk == endchunk)
-			chcpyend = (sliceoffset + slicelength - 1) % TOAST_MAX_CHUNK_SIZE;
+			chcpyend = (sliceoffset + slicelength - 1) % max_chunk_size;
 
 		memcpy(VARDATA(result) +
-			   (curchunk * TOAST_MAX_CHUNK_SIZE - sliceoffset) + chcpystrt,
+			   (curchunk * max_chunk_size - sliceoffset) + chcpystrt,
 			   chunkdata + chcpystrt,
 			   (chcpyend - chcpystrt) + 1);
 
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 99ff07f970da..7b4f83aab012 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1556,15 +1556,19 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 				  uint32 extsize)
 {
 	int32		chunk_seq;
-	int32		last_chunk_seq = (extsize - 1) / TOAST_MAX_CHUNK_SIZE;
+	int32		last_chunk_seq;
 	Pointer		chunk;
 	bool		isnull;
 	int32		chunksize;
 	int32		expected_size;
 	Oid8		toast_valueid;
+	int32		max_chunk_size;
 
 	toast_valueid = ta->toast_pointer.va_valueid;
 
+	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	last_chunk_seq = (extsize - 1) / max_chunk_size;
+
 	/* Sanity-check the sequence number. */
 	chunk_seq = DatumGetInt32(fastgetattr(toasttup, 2,
 										  ctx->toast_rel->rd_att, &isnull));
@@ -1629,8 +1633,8 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 		return;
 	}
 
-	expected_size = chunk_seq < last_chunk_seq ? TOAST_MAX_CHUNK_SIZE
-		: extsize - (last_chunk_seq * TOAST_MAX_CHUNK_SIZE);
+	expected_size = chunk_seq < last_chunk_seq ? max_chunk_size
+		: extsize - (last_chunk_seq * max_chunk_size);
 
 	if (chunksize != expected_size)
 		report_toast_corruption(ctx, ta,
@@ -1872,9 +1876,10 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	int32		expected_chunk_seq = 0;
 	int32		last_chunk_seq;
 	Oid8		toast_valueid;
+	int32		max_chunk_size = TOAST_MAX_CHUNK_SIZE;
 
 	extsize = VARATT_EXTERNAL_GET_EXTSIZE(ta->toast_pointer);
-	last_chunk_seq = (extsize - 1) / TOAST_MAX_CHUNK_SIZE;
+	last_chunk_seq = (extsize - 1) / max_chunk_size;
 
 	/*
 	 * Setup a scan key to find chunks in toast table with matching va_valueid
-- 
2.51.0

v9-0004-Renames-around-varatt_external-varatt_external_oi.patchtext/x-diff; charset=us-asciiDownload
From 2e1a74e9e793266bf5210964398d6a31b987d0ff Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 13 Aug 2025 18:28:10 +0900
Subject: [PATCH v9 04/15] Renames around varatt_external->varatt_external_oid

This impacts a few things:
- VARTAG_ONDISK -> VARTAG_ONDISK_OID
- TOAST_POINTER_SIZE -> TOAST_OID_POINTER_SIZE
- TOAST_MAX_CHUNK_SIZE -> TOAST_OID_MAX_CHUNK_SIZE

The "struct" around varatt_external is cleaned up in most places, while
on it.

This rename is in preparation of a follow-up commit that aims at adding
support for multiple types of external on-disk TOAST pointers, where the
OID type is only one subset of them.
---
 src/include/access/detoast.h                  |  4 +--
 src/include/access/heaptoast.h                |  6 ++--
 src/include/varatt.h                          | 34 +++++++++++--------
 src/backend/access/common/detoast.c           | 10 +++---
 src/backend/access/common/toast_compression.c |  2 +-
 src/backend/access/common/toast_internals.c   | 14 ++++----
 src/backend/access/heap/heaptoast.c           |  2 +-
 src/backend/access/table/toast_helper.c       |  4 +--
 src/backend/access/transam/xlog.c             |  8 ++---
 .../replication/logical/reorderbuffer.c       |  2 +-
 src/backend/utils/adt/varlena.c               |  2 +-
 src/bin/pg_resetwal/pg_resetwal.c             |  2 +-
 doc/src/sgml/func/func-info.sgml              |  2 +-
 doc/src/sgml/storage.sgml                     |  2 +-
 contrib/amcheck/verify_heapam.c               | 10 +++---
 15 files changed, 54 insertions(+), 50 deletions(-)

diff --git a/src/include/access/detoast.h b/src/include/access/detoast.h
index e603a2276c38..6435597b1127 100644
--- a/src/include/access/detoast.h
+++ b/src/include/access/detoast.h
@@ -14,7 +14,7 @@
 
 /*
  * Macro to fetch the possibly-unaligned contents of an EXTERNAL datum
- * into a local "struct varatt_external" toast pointer.  This should be
+ * into a local "varatt_external_oid" toast pointer.  This should be
  * just a memcpy, but some versions of gcc seem to produce broken code
  * that assumes the datum contents are aligned.  Introducing an explicit
  * intermediate "varattrib_1b_e *" variable seems to fix it.
@@ -28,7 +28,7 @@ do { \
 } while (0)
 
 /* Size of an EXTERNAL datum that contains a standard TOAST pointer */
-#define TOAST_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external))
+#define TOAST_OID_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_oid))
 
 /* Size of an EXTERNAL datum that contains an indirection pointer */
 #define INDIRECT_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_indirect))
diff --git a/src/include/access/heaptoast.h b/src/include/access/heaptoast.h
index de6b3e2212a5..55a6a17b2c0b 100644
--- a/src/include/access/heaptoast.h
+++ b/src/include/access/heaptoast.h
@@ -69,19 +69,19 @@
 
 /*
  * When we store an oversize datum externally, we divide it into chunks
- * containing at most TOAST_MAX_CHUNK_SIZE data bytes.  This number *must*
+ * containing at most TOAST_OID_MAX_CHUNK_SIZE data bytes.  This number *must*
  * be small enough that the completed toast-table tuple (including the
  * ID and sequence fields and all overhead) will fit on a page.
  * The coding here sets the size on the theory that we want to fit
  * EXTERN_TUPLES_PER_PAGE tuples of maximum size onto a page.
  *
- * NB: Changing TOAST_MAX_CHUNK_SIZE requires an initdb.
+ * NB: Changing TOAST_OID_MAX_CHUNK_SIZE requires an initdb.
  */
 #define EXTERN_TUPLES_PER_PAGE	4	/* tweak only this */
 
 #define EXTERN_TUPLE_MAX_SIZE	MaximumBytesPerTuple(EXTERN_TUPLES_PER_PAGE)
 
-#define TOAST_MAX_CHUNK_SIZE	\
+#define TOAST_OID_MAX_CHUNK_SIZE	\
 	(EXTERN_TUPLE_MAX_SIZE -							\
 	 MAXALIGN(SizeofHeapTupleHeader) -					\
 	 sizeof(Oid) -										\
diff --git a/src/include/varatt.h b/src/include/varatt.h
index aeeabf9145b5..c873a59bb1c9 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -16,7 +16,7 @@
 #define VARATT_H
 
 /*
- * struct varatt_external is a traditional "TOAST pointer", that is, the
+ * varatt_external_oid is a traditional "TOAST pointer", that is, the
  * information needed to fetch a Datum stored out-of-line in a TOAST table.
  * The data is compressed if and only if the external size stored in
  * va_extinfo is less than va_rawsize - VARHDRSZ.
@@ -29,14 +29,14 @@
  * you can look at these fields!  (The reason we use memcmp is to avoid
  * having to do that just to detect equality of two TOAST pointers...)
  */
-typedef struct varatt_external
+typedef struct varatt_external_oid
 {
 	int32		va_rawsize;		/* Original data size (includes header) */
 	uint32		va_extinfo;		/* External saved size (without header) and
 								 * compression method */
 	Oid			va_valueid;		/* Unique ID of value within TOAST table */
 	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
-}			varatt_external;
+}			varatt_external_oid;
 
 /*
  * These macros define the "saved size" portion of va_extinfo.  Its remaining
@@ -51,7 +51,7 @@ typedef struct varatt_external
  * The creator of such a Datum is entirely responsible that the referenced
  * storage survives for as long as referencing pointer Datums can exist.
  *
- * Note that just as for struct varatt_external, this struct is stored
+ * Note that just as for varatt_external_oid, this struct is stored
  * unaligned within any containing tuple.
  */
 typedef struct varatt_indirect
@@ -66,7 +66,7 @@ typedef struct varatt_indirect
  * storage.  APIs for this, in particular the definition of struct
  * ExpandedObjectHeader, are in src/include/utils/expandeddatum.h.
  *
- * Note that just as for struct varatt_external, this struct is stored
+ * Note that just as for varatt_external_oid, this struct is stored
  * unaligned within any containing tuple.
  */
 typedef struct ExpandedObjectHeader ExpandedObjectHeader;
@@ -78,15 +78,16 @@ typedef struct varatt_expanded
 
 /*
  * Type tag for the various sorts of "TOAST pointer" datums.  The peculiar
- * value for VARTAG_ONDISK comes from a requirement for on-disk compatibility
- * with a previous notion that the tag field was the pointer datum's length.
+ * value for VARTAG_ONDISK_OID comes from a requirement for on-disk
+ * compatibility with a previous notion that the tag field was the pointer
+ * datum's length.
  */
 typedef enum vartag_external
 {
 	VARTAG_INDIRECT = 1,
 	VARTAG_EXPANDED_RO = 2,
 	VARTAG_EXPANDED_RW = 3,
-	VARTAG_ONDISK = 18
+	VARTAG_ONDISK_OID = 18
 } vartag_external;
 
 /* Is a TOAST pointer either type of expanded-object pointer? */
@@ -105,8 +106,8 @@ VARTAG_SIZE(vartag_external tag)
 		return sizeof(varatt_indirect);
 	else if (VARTAG_IS_EXPANDED(tag))
 		return sizeof(varatt_expanded);
-	else if (tag == VARTAG_ONDISK)
-		return sizeof(varatt_external);
+	else if (tag == VARTAG_ONDISK_OID)
+		return sizeof(varatt_external_oid);
 	else
 	{
 		Assert(false);
@@ -360,7 +361,7 @@ VARATT_IS_EXTERNAL(const void *PTR)
 static inline bool
 VARATT_IS_EXTERNAL_ONDISK(const void *PTR)
 {
-	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK;
+	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID;
 }
 
 /* Is varlena datum an indirect pointer? */
@@ -502,15 +503,18 @@ VARDATA_COMPRESSED_GET_COMPRESS_METHOD(const void *PTR)
 	return ((varattrib_4b *) PTR)->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS;
 }
 
-/* Same for external Datums; but note argument is a struct varatt_external */
+/*
+ * Same for external Datums; but note argument is a struct
+ * varatt_external_oid.
+ */
 static inline Size
-VARATT_EXTERNAL_GET_EXTSIZE(struct varatt_external toast_pointer)
+VARATT_EXTERNAL_GET_EXTSIZE(varatt_external_oid toast_pointer)
 {
 	return toast_pointer.va_extinfo & VARLENA_EXTSIZE_MASK;
 }
 
 static inline uint32
-VARATT_EXTERNAL_GET_COMPRESS_METHOD(struct varatt_external toast_pointer)
+VARATT_EXTERNAL_GET_COMPRESS_METHOD(varatt_external_oid toast_pointer)
 {
 	return toast_pointer.va_extinfo >> VARLENA_EXTSIZE_BITS;
 }
@@ -533,7 +537,7 @@ VARATT_EXTERNAL_GET_COMPRESS_METHOD(struct varatt_external toast_pointer)
  * actually saves space, so we expect either equality or less-than.
  */
 static inline bool
-VARATT_EXTERNAL_IS_COMPRESSED(struct varatt_external toast_pointer)
+VARATT_EXTERNAL_IS_COMPRESSED(varatt_external_oid toast_pointer)
 {
 	return VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) <
 		(Size) (toast_pointer.va_rawsize - VARHDRSZ);
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 626517877422..c187c32d96dd 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -225,7 +225,7 @@ detoast_attr_slice(struct varlena *attr,
 
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
-		struct varatt_external toast_pointer;
+		varatt_external_oid toast_pointer;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 
@@ -344,7 +344,7 @@ toast_fetch_datum(struct varlena *attr)
 {
 	Relation	toastrel;
 	struct varlena *result;
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 	int32		attrsize;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
@@ -398,7 +398,7 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 {
 	Relation	toastrel;
 	struct varlena *result;
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 	int32		attrsize;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
@@ -550,7 +550,7 @@ toast_raw_datum_size(Datum value)
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
 		/* va_rawsize is the size of the original datum -- including header */
-		struct varatt_external toast_pointer;
+		varatt_external_oid toast_pointer;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 		result = toast_pointer.va_rawsize;
@@ -610,7 +610,7 @@ toast_datum_size(Datum value)
 		 * compressed or not.  We do not count the size of the toast pointer
 		 * ... should we?
 		 */
-		struct varatt_external toast_pointer;
+		varatt_external_oid toast_pointer;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 		result = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c
index 926f1e4008ab..08f572f31eed 100644
--- a/src/backend/access/common/toast_compression.c
+++ b/src/backend/access/common/toast_compression.c
@@ -262,7 +262,7 @@ toast_get_compression_id(struct varlena *attr)
 	 */
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
-		struct varatt_external toast_pointer;
+		varatt_external_oid toast_pointer;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index f5a50c00c2e3..32eaf37f79a8 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -124,7 +124,7 @@ toast_save_datum(Relation rel, Datum value,
 	TupleDesc	toasttupDesc;
 	CommandId	mycid = GetCurrentCommandId(true);
 	struct varlena *result;
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 	int32		chunk_seq = 0;
 	char	   *data_p;
 	int32		data_todo;
@@ -225,7 +225,7 @@ toast_save_datum(Relation rel, Datum value,
 		toast_pointer.va_valueid = InvalidOid;
 		if (oldexternal != NULL)
 		{
-			struct varatt_external old_toast_pointer;
+			varatt_external_oid old_toast_pointer;
 
 			Assert(VARATT_IS_EXTERNAL_ONDISK(oldexternal));
 			/* Must copy to access aligned fields */
@@ -289,7 +289,7 @@ toast_save_datum(Relation rel, Datum value,
 		{
 			alignas(int32) struct varlena hdr;
 			/* this is to make the union big enough for a chunk: */
-			char		data[TOAST_MAX_CHUNK_SIZE + VARHDRSZ];
+			char		data[TOAST_OID_MAX_CHUNK_SIZE + VARHDRSZ];
 		}			chunk_data;
 		int32		chunk_size;
 
@@ -298,7 +298,7 @@ toast_save_datum(Relation rel, Datum value,
 		/*
 		 * Calculate the size of this chunk
 		 */
-		chunk_size = Min(TOAST_MAX_CHUNK_SIZE, data_todo);
+		chunk_size = Min(TOAST_OID_MAX_CHUNK_SIZE, data_todo);
 
 		/*
 		 * Build a tuple and store it
@@ -359,8 +359,8 @@ toast_save_datum(Relation rel, Datum value,
 	/*
 	 * Create the TOAST pointer value that we'll return
 	 */
-	result = (struct varlena *) palloc(TOAST_POINTER_SIZE);
-	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK);
+	result = (struct varlena *) palloc(TOAST_OID_POINTER_SIZE);
+	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_OID);
 	memcpy(VARDATA_EXTERNAL(result), &toast_pointer, sizeof(toast_pointer));
 
 	return PointerGetDatum(result);
@@ -376,7 +376,7 @@ void
 toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 {
 	struct varlena *attr = (struct varlena *) DatumGetPointer(value);
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 	Relation	toastrel;
 	Relation   *toastidxs;
 	ScanKeyData toastkey;
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index d134f0292a6f..11b4fc6487d6 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -647,7 +647,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 									&toastidxs,
 									&num_indexes);
 
-	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
 
 	totalchunks = ((attrsize - 1) / max_chunk_size) + 1;
 	startchunk = sliceoffset / max_chunk_size;
diff --git a/src/backend/access/table/toast_helper.c b/src/backend/access/table/toast_helper.c
index 11f97d65367d..0c58c6c32565 100644
--- a/src/backend/access/table/toast_helper.c
+++ b/src/backend/access/table/toast_helper.c
@@ -171,7 +171,7 @@ toast_tuple_init(ToastTupleContext *ttc)
  * The column must have attstorage EXTERNAL or EXTENDED if check_main is
  * false, and must have attstorage MAIN if check_main is true.
  *
- * The column must have a minimum size of MAXALIGN(TOAST_POINTER_SIZE);
+ * The column must have a minimum size of MAXALIGN(TOAST_OID_POINTER_SIZE);
  * if not, no benefit is to be expected by compressing it.
  *
  * The return value is the index of the biggest suitable column, or
@@ -184,7 +184,7 @@ toast_tuple_find_biggest_attribute(ToastTupleContext *ttc,
 	TupleDesc	tupleDesc = ttc->ttc_rel->rd_att;
 	int			numAttrs = tupleDesc->natts;
 	int			biggest_attno = -1;
-	int32		biggest_size = MAXALIGN(TOAST_POINTER_SIZE);
+	int32		biggest_size = MAXALIGN(TOAST_OID_POINTER_SIZE);
 	int32		skip_colflags = TOASTCOL_IGNORE;
 	int			i;
 
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 430a38b1a216..017a0f196c2b 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4280,7 +4280,7 @@ WriteControlFile(void)
 	ControlFile->nameDataLen = NAMEDATALEN;
 	ControlFile->indexMaxKeys = INDEX_MAX_KEYS;
 
-	ControlFile->toast_max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	ControlFile->toast_max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
 	ControlFile->loblksize = LOBLKSIZE;
 
 	ControlFile->float8ByVal = true;	/* vestigial */
@@ -4533,15 +4533,15 @@ ReadControlFile(void)
 						   "INDEX_MAX_KEYS", ControlFile->indexMaxKeys,
 						   "INDEX_MAX_KEYS", INDEX_MAX_KEYS),
 				 errhint("It looks like you need to recompile or initdb.")));
-	if (ControlFile->toast_max_chunk_size != TOAST_MAX_CHUNK_SIZE)
+	if (ControlFile->toast_max_chunk_size != TOAST_OID_MAX_CHUNK_SIZE)
 		ereport(FATAL,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("database files are incompatible with server"),
 		/* translator: %s is a variable name and %d is its value */
 				 errdetail("The database cluster was initialized with %s %d,"
 						   " but the server was compiled with %s %d.",
-						   "TOAST_MAX_CHUNK_SIZE", ControlFile->toast_max_chunk_size,
-						   "TOAST_MAX_CHUNK_SIZE", (int) TOAST_MAX_CHUNK_SIZE),
+						   "TOAST_OID_MAX_CHUNK_SIZE", ControlFile->toast_max_chunk_size,
+						   "TOAST_OID_MAX_CHUNK_SIZE", (int) TOAST_OID_MAX_CHUNK_SIZE),
 				 errhint("It looks like you need to recompile or initdb.")));
 	if (ControlFile->loblksize != LOBLKSIZE)
 		ereport(FATAL,
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 4c59ca2453be..2c9840d4b9cd 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -5136,7 +5136,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		struct varlena *varlena;
 
 		/* va_rawsize is the size of the original datum -- including header */
-		struct varatt_external toast_pointer;
+		varatt_external_oid toast_pointer;
 		struct varatt_indirect redirect_pointer;
 		struct varlena *new_datum = NULL;
 		struct varlena *reconstructed;
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 8adeb8dadc66..dfcf888c7c43 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -4195,7 +4195,7 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 {
 	int			typlen;
 	struct varlena *attr;
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 
 	/* On first call, get the input type's typlen, and save at *fn_extra */
 	if (fcinfo->flinfo->fn_extra == NULL)
diff --git a/src/bin/pg_resetwal/pg_resetwal.c b/src/bin/pg_resetwal/pg_resetwal.c
index 9bfab8c307bc..deef3261f444 100644
--- a/src/bin/pg_resetwal/pg_resetwal.c
+++ b/src/bin/pg_resetwal/pg_resetwal.c
@@ -733,7 +733,7 @@ GuessControlValues(void)
 	ControlFile.xlog_seg_size = DEFAULT_XLOG_SEG_SIZE;
 	ControlFile.nameDataLen = NAMEDATALEN;
 	ControlFile.indexMaxKeys = INDEX_MAX_KEYS;
-	ControlFile.toast_max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	ControlFile.toast_max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
 	ControlFile.loblksize = LOBLKSIZE;
 	ControlFile.float8ByVal = true; /* vestigial */
 
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index d4508114a48e..e51612f1fead 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3533,7 +3533,7 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
       </row>
 
       <row>
-       <entry><structfield>max_toast_chunk_size</structfield></entry>
+       <entry><structfield>max_toast_oid_chunk_size</structfield></entry>
        <entry><type>integer</type></entry>
       </row>
 
diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml
index 02ddfda834a2..67600fd974d7 100644
--- a/doc/src/sgml/storage.sgml
+++ b/doc/src/sgml/storage.sgml
@@ -417,7 +417,7 @@ described in more detail below.
 
 <para>
 Out-of-line values are divided (after compression if used) into chunks of at
-most <symbol>TOAST_MAX_CHUNK_SIZE</symbol> bytes (by default this value is chosen
+most <symbol>TOAST_OID_MAX_CHUNK_SIZE</symbol> bytes (by default this value is chosen
 so that four chunk rows will fit on a page, making it about 2000 bytes).
 Each chunk is stored as a separate row in the <acronym>TOAST</acronym> table
 belonging to the owning table.  Every
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 7b4f83aab012..5f51e8d55153 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -73,7 +73,7 @@ typedef enum SkipPages
  */
 typedef struct ToastedAttribute
 {
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 	BlockNumber blkno;			/* block in main table */
 	OffsetNumber offnum;		/* offset in main table */
 	AttrNumber	attnum;			/* attribute in main table */
@@ -1566,7 +1566,7 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 
 	toast_valueid = ta->toast_pointer.va_valueid;
 
-	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
 	last_chunk_seq = (extsize - 1) / max_chunk_size;
 
 	/* Sanity-check the sequence number. */
@@ -1672,7 +1672,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	uint16		infomask;
 	Oid8		toast_pointer_valueid;
 	CompactAttribute *thisatt;
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 
 	infomask = ctx->tuphdr->t_infomask;
 	thisatt = TupleDescCompactAttr(RelationGetDescr(ctx->rel), ctx->attnum);
@@ -1731,7 +1731,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	{
 		uint8		va_tag = VARTAG_EXTERNAL(tp + ctx->offset);
 
-		if (va_tag != VARTAG_ONDISK)
+		if (va_tag != VARTAG_ONDISK_OID)
 		{
 			report_corruption(ctx,
 							  psprintf("toasted attribute has unexpected TOAST tag %u",
@@ -1876,7 +1876,7 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	int32		expected_chunk_seq = 0;
 	int32		last_chunk_seq;
 	Oid8		toast_valueid;
-	int32		max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	int32		max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
 
 	extsize = VARATT_EXTERNAL_GET_EXTSIZE(ta->toast_pointer);
 	last_chunk_seq = (extsize - 1) / max_chunk_size;
-- 
2.51.0

v9-0005-Refactor-external-TOAST-pointer-code-for-better-p.patchtext/x-diff; charset=us-asciiDownload
From 0c09d84250333a3771025c709d95a6b66c10f611 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 30 Sep 2025 15:09:13 +0900
Subject: [PATCH v9 05/15] Refactor external TOAST pointer code for better
 pluggability

This commit introduces a new interface for external TOAST pointers,
which is able to make a translation of the varlena pointers stored on
disk to/from an new in-memory structure called toast_external.  The
types of varatt_external supported on disk need to be registered into a
new subsystem in a new file, called toast_external.[c|h], then define a
set of callbacks to allow the toasting and detoasting code to use it.

A follow-up change will rely on this refactoring to introduce new
vartag_external values with an associated varatt_external_* that is
able, which would be used in int8 TOAST tables.
---
 src/include/access/detoast.h                  |  12 +-
 src/include/access/heaptoast.h                |   3 +
 src/include/access/toast_external.h           | 176 ++++++++++++++++
 src/include/access/toast_helper.h             |   1 +
 src/include/varatt.h                          |  16 +-
 src/backend/access/common/Makefile            |   1 +
 src/backend/access/common/detoast.c           |  57 +++--
 src/backend/access/common/meson.build         |   1 +
 src/backend/access/common/toast_compression.c |  10 +-
 src/backend/access/common/toast_external.c    | 196 ++++++++++++++++++
 src/backend/access/common/toast_internals.c   |  84 +++++---
 src/backend/access/heap/heaptoast.c           |  20 +-
 src/backend/access/table/toast_helper.c       |  12 +-
 src/backend/access/transam/xlog.c             |   8 +-
 .../replication/logical/reorderbuffer.c       |  13 +-
 src/backend/utils/adt/varlena.c               |   7 +-
 src/bin/pg_resetwal/pg_resetwal.c             |   2 +-
 contrib/amcheck/verify_heapam.c               |  35 ++--
 src/tools/pgindent/typedefs.list              |   2 +
 19 files changed, 545 insertions(+), 111 deletions(-)
 create mode 100644 src/include/access/toast_external.h
 create mode 100644 src/backend/access/common/toast_external.c

diff --git a/src/include/access/detoast.h b/src/include/access/detoast.h
index 6435597b1127..2f71fbd95f88 100644
--- a/src/include/access/detoast.h
+++ b/src/include/access/detoast.h
@@ -14,10 +14,11 @@
 
 /*
  * Macro to fetch the possibly-unaligned contents of an EXTERNAL datum
- * into a local "varatt_external_oid" toast pointer.  This should be
- * just a memcpy, but some versions of gcc seem to produce broken code
- * that assumes the datum contents are aligned.  Introducing an explicit
- * intermediate "varattrib_1b_e *" variable seems to fix it.
+ * into a local "varatt_external_*" toast pointer, as supported
+ * in toast_external.h and varatt.h.  This should be just a memcpy, but
+ * some versions of gcc seem to produce broken code that assumes the datum
+ * contents are aligned.  Introducing an explicit intermediate
+ * "varattrib_1b_e *" variable seems to fix it.
  */
 #define VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr) \
 do { \
@@ -27,9 +28,6 @@ do { \
 	memcpy(&(toast_pointer), VARDATA_EXTERNAL(attre), sizeof(toast_pointer)); \
 } while (0)
 
-/* Size of an EXTERNAL datum that contains a standard TOAST pointer */
-#define TOAST_OID_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_oid))
-
 /* Size of an EXTERNAL datum that contains an indirection pointer */
 #define INDIRECT_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_indirect))
 
diff --git a/src/include/access/heaptoast.h b/src/include/access/heaptoast.h
index 55a6a17b2c0b..12c9702af689 100644
--- a/src/include/access/heaptoast.h
+++ b/src/include/access/heaptoast.h
@@ -88,6 +88,9 @@
 	 sizeof(int32) -									\
 	 VARHDRSZ)
 
+/* Maximum size of chunk possible */
+#define TOAST_MAX_CHUNK_SIZE	TOAST_OID_MAX_CHUNK_SIZE
+
 /* ----------
  * heap_toast_insert_or_update -
  *
diff --git a/src/include/access/toast_external.h b/src/include/access/toast_external.h
new file mode 100644
index 000000000000..6450343eab25
--- /dev/null
+++ b/src/include/access/toast_external.h
@@ -0,0 +1,176 @@
+/*-------------------------------------------------------------------------
+ *
+ * toast_external.h
+ *	  Support for on-disk external TOAST pointers
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1995, Regents of the University of California
+ *
+ * src/include/access/toast_external.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef TOAST_EXTERNAL_H
+#define TOAST_EXTERNAL_H
+
+#include "access/toast_compression.h"
+#include "varatt.h"
+
+/*
+ * Intermediate in-memory structure used when creating on-disk
+ * varatt_external_* or when deserializing varlena contents.
+ */
+typedef struct toast_external_data
+{
+	/* Original data size (includes header) */
+	int32		rawsize;
+
+	/* External saved size (without header) */
+	uint32		extsize;
+
+	/*
+	 * Compression method.
+	 *
+	 * If not compressed, set to TOAST_INVALID_COMPRESSION_ID.
+	 */
+	ToastCompressionId compression_method;
+
+	/* Relation OID of TOAST table containing the value */
+	Oid			toastrelid;
+
+	/*
+	 * Unique ID of value within TOAST table.  This could be an OID or an Oid8
+	 * value.  This field is large enough to be able to store any of these.
+	 */
+	Oid8		valueid;
+} toast_external_data;
+
+/*
+ * Metadata for external TOAST pointer kinds, separated based on their
+ * vartag_external.
+ */
+typedef struct toast_external_info
+{
+	/*
+	 * Maximum chunk of data authorized for this type of external TOAST
+	 * pointer, when dividing an entry by chunks.  Sized depending on the size
+	 * of its varatt_external_* structure.
+	 */
+	int32		maximum_chunk_size;
+
+	/*
+	 * Size of an external TOAST pointer of this type, typically
+	 * (VARHDRSZ_EXTERNAL + sizeof(varatt_external_struct)).
+	 */
+	int32		toast_pointer_size;
+
+	/*
+	 * Map an input varlena to a toast_external_data, for consumption in the
+	 * backend code.  "data" is an input/output result.
+	 */
+	void		(*to_external_data) (struct varlena *attr,
+									 toast_external_data *data);
+
+	/*
+	 * Create a varlena that will be used on-disk for the given TOAST type,
+	 * based on the given input data.
+	 *
+	 * The result is the varlena created, for on-disk insertion.
+	 */
+	struct varlena *(*create_external_data) (toast_external_data data);
+
+} toast_external_info;
+
+/* Retrieve a toast_external_info from a vartag */
+extern const toast_external_info *toast_external_get_info(uint8 tag);
+
+/* Retrieve toast_pointer_size using a TOAST attribute type */
+extern int32 toast_external_info_get_pointer_size(uint8 tag);
+
+/* Retrieve the vartag to assign to a TOAST typle */
+extern uint8 toast_external_assign_vartag(Oid toastrelid, Oid8 value);
+
+/*
+ * Testing whether an externally-stored value is compressed now requires
+ * comparing size stored in extsize (the actual length of the external data)
+ * to rawsize (the original uncompressed datum's size).  The latter includes
+ * VARHDRSZ overhead, the former doesn't.  We never use compression unless it
+ * actually saves space, so we expect either equality or less-than.
+ */
+static inline bool
+TOAST_EXTERNAL_IS_COMPRESSED(toast_external_data data)
+{
+	return data.extsize < (data.rawsize - VARHDRSZ);
+}
+
+/* Full data structure */
+static inline void
+toast_external_info_get_data(struct varlena *attr, toast_external_data *data)
+{
+	uint8		tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+
+	info->to_external_data(attr, data);
+}
+
+/*
+ * Helper routines to recover specific fields in toast_external_data.  Most
+ * code paths doing work with on-disk external TOAST pointers care about
+ * these.
+ */
+
+/* Detoasted "raw" size */
+static inline Size
+toast_external_info_get_rawsize(struct varlena *attr)
+{
+	uint8		tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+	toast_external_data data;
+
+	info->to_external_data(attr, &data);
+
+	return data.rawsize;
+}
+
+/* External saved size */
+static inline Size
+toast_external_info_get_extsize(struct varlena *attr)
+{
+	uint8		tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+	toast_external_data data;
+
+	info->to_external_data(attr, &data);
+
+	return data.extsize;
+}
+
+/* Compression method ID */
+static inline ToastCompressionId
+toast_external_info_get_compression_method(struct varlena *attr)
+{
+	uint8		tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+	toast_external_data data;
+
+	info->to_external_data(attr, &data);
+
+	return data.compression_method;
+}
+
+/* Value ID */
+static inline Oid8
+toast_external_info_get_valueid(struct varlena *attr)
+{
+	uint8		tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+	toast_external_data data;
+
+	info->to_external_data(attr, &data);
+
+	return data.valueid;
+}
+
+#endif							/* TOAST_EXTERNAL_H */
diff --git a/src/include/access/toast_helper.h b/src/include/access/toast_helper.h
index e6ab8afffb67..6bc912809f34 100644
--- a/src/include/access/toast_helper.h
+++ b/src/include/access/toast_helper.h
@@ -47,6 +47,7 @@ typedef struct
 	 * should be NULL in the case of an insert.
 	 */
 	Relation	ttc_rel;		/* the relation that contains the tuple */
+	int32		ttc_toast_pointer_size; /* size of external TOAST pointer */
 	Datum	   *ttc_values;		/* values from the tuple columns */
 	bool	   *ttc_isnull;		/* null flags for the tuple columns */
 	Datum	   *ttc_oldvalues;	/* values from previous tuple */
diff --git a/src/include/varatt.h b/src/include/varatt.h
index c873a59bb1c9..790d9f844c91 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -21,6 +21,9 @@
  * The data is compressed if and only if the external size stored in
  * va_extinfo is less than va_rawsize - VARHDRSZ.
  *
+ * The value ID is an OID, used for TOAST relations with OID as attribute
+ * for chunk_id.
+ *
  * This struct must not contain any padding, because we sometimes compare
  * these pointers using memcmp.
  *
@@ -51,7 +54,7 @@ typedef struct varatt_external_oid
  * The creator of such a Datum is entirely responsible that the referenced
  * storage survives for as long as referencing pointer Datums can exist.
  *
- * Note that just as for varatt_external_oid, this struct is stored
+ * Note that just as for varatt_external_*, this struct is stored
  * unaligned within any containing tuple.
  */
 typedef struct varatt_indirect
@@ -66,7 +69,7 @@ typedef struct varatt_indirect
  * storage.  APIs for this, in particular the definition of struct
  * ExpandedObjectHeader, are in src/include/utils/expandeddatum.h.
  *
- * Note that just as for varatt_external_oid, this struct is stored
+ * Note that just as for varatt_external_*, this struct is stored
  * unaligned within any containing tuple.
  */
 typedef struct ExpandedObjectHeader ExpandedObjectHeader;
@@ -357,11 +360,18 @@ VARATT_IS_EXTERNAL(const void *PTR)
 	return VARATT_IS_1B_E(PTR);
 }
 
+/* Is varlena datum a pointer to on-disk toasted data with OID value? */
+static inline bool
+VARATT_IS_EXTERNAL_ONDISK_OID(const void *PTR)
+{
+	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID;
+}
+
 /* Is varlena datum a pointer to on-disk toasted data? */
 static inline bool
 VARATT_IS_EXTERNAL_ONDISK(const void *PTR)
 {
-	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID;
+	return VARATT_IS_EXTERNAL_ONDISK_OID(PTR);
 }
 
 /* Is varlena datum an indirect pointer? */
diff --git a/src/backend/access/common/Makefile b/src/backend/access/common/Makefile
index e78de312659e..1ef86a245886 100644
--- a/src/backend/access/common/Makefile
+++ b/src/backend/access/common/Makefile
@@ -27,6 +27,7 @@ OBJS = \
 	syncscan.o \
 	tidstore.o \
 	toast_compression.o \
+	toast_external.o \
 	toast_internals.o \
 	tupconvert.o \
 	tupdesc.o
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index c187c32d96dd..8531c27439e4 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -16,6 +16,7 @@
 #include "access/detoast.h"
 #include "access/table.h"
 #include "access/tableam.h"
+#include "access/toast_external.h"
 #include "access/toast_internals.h"
 #include "common/int.h"
 #include "common/pg_lzcompress.h"
@@ -225,12 +226,12 @@ detoast_attr_slice(struct varlena *attr,
 
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
-		varatt_external_oid toast_pointer;
+		toast_external_data toast_pointer;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		toast_external_info_get_data(attr, &toast_pointer);
 
 		/* fast path for non-compressed external datums */
-		if (!VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+		if (!TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 			return toast_fetch_datum_slice(attr, sliceoffset, slicelength);
 
 		/*
@@ -240,7 +241,7 @@ detoast_attr_slice(struct varlena *attr,
 		 */
 		if (slicelimit >= 0)
 		{
-			int32		max_size = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+			int32		max_size = toast_pointer.extsize;
 
 			/*
 			 * Determine maximum amount of compressed data needed for a prefix
@@ -251,8 +252,7 @@ detoast_attr_slice(struct varlena *attr,
 			 * determine how much compressed data we need to be sure of being
 			 * able to decompress the required slice.
 			 */
-			if (VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) ==
-				TOAST_PGLZ_COMPRESSION_ID)
+			if (toast_pointer.compression_method == TOAST_PGLZ_COMPRESSION_ID)
 				max_size = pglz_maximum_compressed_size(slicelimit, max_size);
 
 			/*
@@ -344,20 +344,21 @@ toast_fetch_datum(struct varlena *attr)
 {
 	Relation	toastrel;
 	struct varlena *result;
-	varatt_external_oid toast_pointer;
+	toast_external_data toast_pointer;
 	int32		attrsize;
+	Oid8		valueid;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		elog(ERROR, "toast_fetch_datum shouldn't be called for non-ondisk datums");
 
 	/* Must copy to access aligned fields */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_external_info_get_data(attr, &toast_pointer);
 
-	attrsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+	attrsize = toast_pointer.extsize;
 
 	result = (struct varlena *) palloc(attrsize + VARHDRSZ);
 
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+	if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 		SET_VARSIZE_COMPRESSED(result, attrsize + VARHDRSZ);
 	else
 		SET_VARSIZE(result, attrsize + VARHDRSZ);
@@ -365,14 +366,15 @@ toast_fetch_datum(struct varlena *attr)
 	if (attrsize == 0)
 		return result;			/* Probably shouldn't happen, but just in
 								 * case. */
+	valueid = toast_pointer.valueid;
 
 	/*
 	 * Open the toast relation and its indexes
 	 */
-	toastrel = table_open(toast_pointer.va_toastrelid, AccessShareLock);
+	toastrel = table_open(toast_pointer.toastrelid, AccessShareLock);
 
 	/* Fetch all chunks */
-	table_relation_fetch_toast_slice(toastrel, toast_pointer.va_valueid,
+	table_relation_fetch_toast_slice(toastrel, valueid,
 									 attrsize, 0, attrsize, result);
 
 	/* Close toast table */
@@ -398,23 +400,26 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 {
 	Relation	toastrel;
 	struct varlena *result;
-	varatt_external_oid toast_pointer;
+	toast_external_data toast_pointer;
 	int32		attrsize;
+	Oid8		valueid;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		elog(ERROR, "toast_fetch_datum_slice shouldn't be called for non-ondisk datums");
 
 	/* Must copy to access aligned fields */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_external_info_get_data(attr, &toast_pointer);
+
+	valueid = toast_pointer.valueid;
 
 	/*
 	 * It's nonsense to fetch slices of a compressed datum unless when it's a
 	 * prefix -- this isn't lo_* we can't return a compressed datum which is
 	 * meaningful to toast later.
 	 */
-	Assert(!VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer) || 0 == sliceoffset);
+	Assert(!TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer) || 0 == sliceoffset);
 
-	attrsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+	attrsize = toast_pointer.extsize;
 
 	if (sliceoffset >= attrsize)
 	{
@@ -427,7 +432,7 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 	 * space required by va_tcinfo, which is stored at the beginning as an
 	 * int32 value.
 	 */
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer) && slicelength > 0)
+	if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer) && slicelength > 0)
 		slicelength = slicelength + sizeof(int32);
 
 	/*
@@ -440,7 +445,7 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 
 	result = (struct varlena *) palloc(slicelength + VARHDRSZ);
 
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+	if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 		SET_VARSIZE_COMPRESSED(result, slicelength + VARHDRSZ);
 	else
 		SET_VARSIZE(result, slicelength + VARHDRSZ);
@@ -449,10 +454,11 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 		return result;			/* Can save a lot of work at this point! */
 
 	/* Open the toast relation */
-	toastrel = table_open(toast_pointer.va_toastrelid, AccessShareLock);
+	toastrel = table_open(toast_pointer.toastrelid, AccessShareLock);
 
 	/* Fetch all chunks */
-	table_relation_fetch_toast_slice(toastrel, toast_pointer.va_valueid,
+	table_relation_fetch_toast_slice(toastrel,
+									 valueid,
 									 attrsize, sliceoffset, slicelength,
 									 result);
 
@@ -549,11 +555,7 @@ toast_raw_datum_size(Datum value)
 
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
-		/* va_rawsize is the size of the original datum -- including header */
-		varatt_external_oid toast_pointer;
-
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-		result = toast_pointer.va_rawsize;
+		result = toast_external_info_get_rawsize(attr);
 	}
 	else if (VARATT_IS_EXTERNAL_INDIRECT(attr))
 	{
@@ -610,10 +612,7 @@ toast_datum_size(Datum value)
 		 * compressed or not.  We do not count the size of the toast pointer
 		 * ... should we?
 		 */
-		varatt_external_oid toast_pointer;
-
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-		result = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+		result = toast_external_info_get_extsize(attr);
 	}
 	else if (VARATT_IS_EXTERNAL_INDIRECT(attr))
 	{
diff --git a/src/backend/access/common/meson.build b/src/backend/access/common/meson.build
index e3cdbe7a22e1..c20f2e88921e 100644
--- a/src/backend/access/common/meson.build
+++ b/src/backend/access/common/meson.build
@@ -15,6 +15,7 @@ backend_sources += files(
   'syncscan.c',
   'tidstore.c',
   'toast_compression.c',
+  'toast_external.c',
   'toast_internals.c',
   'tupconvert.c',
   'tupdesc.c',
diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c
index 08f572f31eed..94606a58c8fb 100644
--- a/src/backend/access/common/toast_compression.c
+++ b/src/backend/access/common/toast_compression.c
@@ -19,6 +19,7 @@
 
 #include "access/detoast.h"
 #include "access/toast_compression.h"
+#include "access/toast_external.h"
 #include "common/pg_lzcompress.h"
 #include "varatt.h"
 
@@ -261,14 +262,7 @@ toast_get_compression_id(struct varlena *attr)
 	 * toast compression header.
 	 */
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
-	{
-		varatt_external_oid toast_pointer;
-
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-
-		if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
-			cmid = VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer);
-	}
+		cmid = toast_external_info_get_compression_method(attr);
 	else if (VARATT_IS_COMPRESSED(attr))
 		cmid = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(attr);
 
diff --git a/src/backend/access/common/toast_external.c b/src/backend/access/common/toast_external.c
new file mode 100644
index 000000000000..2154152b8bfb
--- /dev/null
+++ b/src/backend/access/common/toast_external.c
@@ -0,0 +1,196 @@
+/*-------------------------------------------------------------------------
+ *
+ * toast_external.c
+ *	  Functions for the support of external on-disk TOAST pointers.
+ *
+ * This includes all the types of external on-disk TOAST pointers supported
+ * by the backend, based on the callbacks and data defined in
+ * toast_external.h.
+ *
+ * Copyright (c) 2025, PostgreSQL Global Development Group
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/common/toast_external.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/detoast.h"
+#include "access/heaptoast.h"
+#include "access/toast_external.h"
+
+/* Callbacks for VARTAG_ONDISK_OID */
+static void ondisk_oid_to_external_data(struct varlena *attr,
+										toast_external_data *data);
+static struct varlena *ondisk_oid_create_external_data(toast_external_data data);
+
+/*
+ * Size of an EXTERNAL datum that contains a standard TOAST pointer (OID
+ * value).
+ */
+#define TOAST_OID_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_oid))
+
+/*
+ * For now there are only two types, all defined in this file.  For now this
+ * is the maximum value of vartag_external, which is a historical choice.
+ */
+#define TOAST_EXTERNAL_INFO_SIZE	(VARTAG_ONDISK_OID + 1)
+
+/*
+ * The different kinds of on-disk external TOAST pointers, divided by
+ * vartag_external.
+ *
+ * See comments for struct toast_external_info about the details of the
+ * individual fields.
+ */
+static const toast_external_info toast_external_infos[TOAST_EXTERNAL_INFO_SIZE] = {
+	[VARTAG_ONDISK_OID] = {
+		.toast_pointer_size = TOAST_OID_POINTER_SIZE,
+		.maximum_chunk_size = TOAST_OID_MAX_CHUNK_SIZE,
+		.to_external_data = ondisk_oid_to_external_data,
+		.create_external_data = ondisk_oid_create_external_data,
+	},
+};
+
+/*
+ * toast_external_get_info
+ *
+ * Get toast_external_info of the defined vartag_external, central set of
+ * callbacks, based on a "tag", which is a vartag_external value for an
+ * on-disk external varlena.
+ */
+const toast_external_info *
+toast_external_get_info(uint8 tag)
+{
+	const toast_external_info *res = &toast_external_infos[tag];
+
+	/* check tag for invalid range */
+	if (tag >= TOAST_EXTERNAL_INFO_SIZE)
+		elog(ERROR, "incorrect value %u for toast_external_info", tag);
+
+	/* sanity check with tag in valid range */
+	res = &toast_external_infos[tag];
+	if (res == NULL)
+		elog(ERROR, "incorrect value %u for toast_external_info", tag);
+	return res;
+}
+
+/*
+ * toast_external_info_get_pointer_size
+ *
+ * Get external TOAST pointer size based on the attribute type of a TOAST
+ * value.  "tag" is a vartag_external value.
+ */
+int32
+toast_external_info_get_pointer_size(uint8 tag)
+{
+	return toast_external_infos[tag].toast_pointer_size;
+}
+
+/*
+ * toast_external_assign_vartag
+ *
+ * Assign the vartag_external of a TOAST tuple, based on the TOAST relation
+ * it uses and its value.
+ *
+ * An invalid value can be given by the caller of this routine, in which
+ * case a default vartag should be provided based on only the toast relation
+ * used.
+ */
+uint8
+toast_external_assign_vartag(Oid toastrelid, Oid8 valueid)
+{
+	/*
+	 * If dealing with a code path where a TOAST relation may not be assigned,
+	 * like heap_toast_insert_or_update(), just use the legacy
+	 * vartag_external.
+	 */
+	if (!OidIsValid(toastrelid))
+		return VARTAG_ONDISK_OID;
+
+	/*
+	 * Currently there is only one type of vartag_external supported: 4-byte
+	 * value with OID for the chunk_id type.
+	 *
+	 * Note: This routine will be extended to be able to use multiple
+	 * vartag_external within a single TOAST relation type, that may change
+	 * depending on the value used.
+	 */
+	return VARTAG_ONDISK_OID;
+}
+
+/*
+ * Helper routines able to translate the various varatt_external_* from/to
+ * the in-memory representation toast_external_data used in the backend.
+ */
+
+/* Callbacks for VARTAG_ONDISK_OID */
+
+/*
+ * ondisk_oid_to_external_data
+ *
+ * Translate a varlena to its toast_external_data representation, to be used
+ * by the backend code.
+ */
+static void
+ondisk_oid_to_external_data(struct varlena *attr, toast_external_data *data)
+{
+	varatt_external_oid external;
+
+	VARATT_EXTERNAL_GET_POINTER(external, attr);
+	data->rawsize = external.va_rawsize;
+
+	/*
+	 * External size and compression methods are stored in the same field,
+	 * extract.
+	 */
+	if (VARATT_EXTERNAL_IS_COMPRESSED(external))
+	{
+		data->extsize = VARATT_EXTERNAL_GET_EXTSIZE(external);
+		data->compression_method = VARATT_EXTERNAL_GET_COMPRESS_METHOD(external);
+	}
+	else
+	{
+		data->extsize = external.va_extinfo;
+		data->compression_method = TOAST_INVALID_COMPRESSION_ID;
+	}
+
+	data->valueid = (Oid8) external.va_valueid;
+	data->toastrelid = external.va_toastrelid;
+}
+
+/*
+ * ondisk_oid_create_external_data
+ *
+ * Create a new varlena based on the input toast_external_data, to be used
+ * when saving a new TOAST value.
+ */
+static struct varlena *
+ondisk_oid_create_external_data(toast_external_data data)
+{
+	struct varlena *result = NULL;
+	varatt_external_oid external;
+
+	external.va_rawsize = data.rawsize;
+
+	if (data.compression_method != TOAST_INVALID_COMPRESSION_ID)
+	{
+		/* Set size and compression method, in a single field. */
+		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(external,
+													 data.extsize,
+													 data.compression_method);
+	}
+	else
+		external.va_extinfo = data.extsize;
+
+	external.va_toastrelid = data.toastrelid;
+	external.va_valueid = (Oid) data.valueid;
+
+	result = (struct varlena *) palloc(TOAST_OID_POINTER_SIZE);
+	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_OID);
+	memcpy(VARDATA_EXTERNAL(result), &external, sizeof(external));
+
+	return result;
+}
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 32eaf37f79a8..f614a49e1815 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -18,6 +18,7 @@
 #include "access/heapam.h"
 #include "access/heaptoast.h"
 #include "access/table.h"
+#include "access/toast_external.h"
 #include "access/toast_internals.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -124,13 +125,15 @@ toast_save_datum(Relation rel, Datum value,
 	TupleDesc	toasttupDesc;
 	CommandId	mycid = GetCurrentCommandId(true);
 	struct varlena *result;
-	varatt_external_oid toast_pointer;
+	toast_external_data toast_pointer;
 	int32		chunk_seq = 0;
 	char	   *data_p;
 	int32		data_todo;
 	Pointer		dval = DatumGetPointer(value);
 	int			num_indexes;
 	int			validIndex;
+	const toast_external_info *info;
+	uint8		tag = VARTAG_INDIRECT;	/* init value does not matter */
 
 	Assert(!VARATT_IS_EXTERNAL(dval));
 
@@ -162,28 +165,41 @@ toast_save_datum(Relation rel, Datum value,
 	{
 		data_p = VARDATA_SHORT(dval);
 		data_todo = VARSIZE_SHORT(dval) - VARHDRSZ_SHORT;
-		toast_pointer.va_rawsize = data_todo + VARHDRSZ;	/* as if not short */
-		toast_pointer.va_extinfo = data_todo;
+		toast_pointer.rawsize = data_todo + VARHDRSZ;	/* as if not short */
+		toast_pointer.extsize = data_todo;
+
+		/*
+		 * TOAST_INVALID_COMPRESSION_ID means that the varlena is not
+		 * compressed, see toast_get_compression_id().
+		 */
+		toast_pointer.compression_method = TOAST_INVALID_COMPRESSION_ID;
 	}
 	else if (VARATT_IS_COMPRESSED(dval))
 	{
 		data_p = VARDATA(dval);
 		data_todo = VARSIZE(dval) - VARHDRSZ;
 		/* rawsize in a compressed datum is just the size of the payload */
-		toast_pointer.va_rawsize = VARDATA_COMPRESSED_GET_EXTSIZE(dval) + VARHDRSZ;
+		toast_pointer.rawsize = VARDATA_COMPRESSED_GET_EXTSIZE(dval) + VARHDRSZ;
 
 		/* set external size and compression method */
-		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, data_todo,
-													 VARDATA_COMPRESSED_GET_COMPRESS_METHOD(dval));
+		toast_pointer.extsize = data_todo;
+		toast_pointer.compression_method = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(dval);
+
 		/* Assert that the numbers look like it's compressed */
-		Assert(VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer));
+		Assert(TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer));
 	}
 	else
 	{
 		data_p = VARDATA(dval);
 		data_todo = VARSIZE(dval) - VARHDRSZ;
-		toast_pointer.va_rawsize = VARSIZE(dval);
-		toast_pointer.va_extinfo = data_todo;
+		toast_pointer.rawsize = VARSIZE(dval);
+		toast_pointer.extsize = data_todo;
+
+		/*
+		 * TOAST_INVALID_COMPRESSION_ID means that the varlena is not
+		 * compressed, see toast_get_compression_id().
+		 */
+		toast_pointer.compression_method = TOAST_INVALID_COMPRESSION_ID;
 	}
 
 	/*
@@ -195,9 +211,9 @@ toast_save_datum(Relation rel, Datum value,
 	 * if we have to substitute such an OID.
 	 */
 	if (OidIsValid(rel->rd_toastoid))
-		toast_pointer.va_toastrelid = rel->rd_toastoid;
+		toast_pointer.toastrelid = rel->rd_toastoid;
 	else
-		toast_pointer.va_toastrelid = RelationGetRelid(toastrel);
+		toast_pointer.toastrelid = RelationGetRelid(toastrel);
 
 	/*
 	 * Choose an OID to use as the value ID for this toast value.
@@ -214,7 +230,7 @@ toast_save_datum(Relation rel, Datum value,
 	if (!OidIsValid(rel->rd_toastoid))
 	{
 		/* normal case: just choose an unused OID */
-		toast_pointer.va_valueid =
+		toast_pointer.valueid =
 			GetNewOidWithIndex(toastrel,
 							   RelationGetRelid(toastidxs[validIndex]),
 							   (AttrNumber) 1);
@@ -222,18 +238,18 @@ toast_save_datum(Relation rel, Datum value,
 	else
 	{
 		/* rewrite case: check to see if value was in old toast table */
-		toast_pointer.va_valueid = InvalidOid;
+		toast_pointer.valueid = InvalidOid8;
 		if (oldexternal != NULL)
 		{
-			varatt_external_oid old_toast_pointer;
+			toast_external_data old_toast_pointer;
 
 			Assert(VARATT_IS_EXTERNAL_ONDISK(oldexternal));
-			/* Must copy to access aligned fields */
-			VARATT_EXTERNAL_GET_POINTER(old_toast_pointer, oldexternal);
-			if (old_toast_pointer.va_toastrelid == rel->rd_toastoid)
+			toast_external_info_get_data(oldexternal, &old_toast_pointer);
+
+			if (old_toast_pointer.toastrelid == rel->rd_toastoid)
 			{
 				/* This value came from the old toast table; reuse its OID */
-				toast_pointer.va_valueid = old_toast_pointer.va_valueid;
+				toast_pointer.valueid = old_toast_pointer.valueid;
 
 				/*
 				 * There is a corner case here: the table rewrite might have
@@ -253,14 +269,14 @@ toast_save_datum(Relation rel, Datum value,
 				 * be reclaimed by VACUUM.
 				 */
 				if (toastrel_valueid_exists(toastrel,
-											toast_pointer.va_valueid))
+											toast_pointer.valueid))
 				{
 					/* Match, so short-circuit the data storage loop below */
 					data_todo = 0;
 				}
 			}
 		}
-		if (toast_pointer.va_valueid == InvalidOid)
+		if (toast_pointer.valueid == InvalidOid8)
 		{
 			/*
 			 * new value; must choose an OID that doesn't conflict in either
@@ -268,15 +284,23 @@ toast_save_datum(Relation rel, Datum value,
 			 */
 			do
 			{
-				toast_pointer.va_valueid =
+				toast_pointer.valueid =
 					GetNewOidWithIndex(toastrel,
 									   RelationGetRelid(toastidxs[validIndex]),
 									   (AttrNumber) 1);
 			} while (toastid_valueid_exists(rel->rd_toastoid,
-											toast_pointer.va_valueid));
+											toast_pointer.valueid));
 		}
 	}
 
+	/*
+	 * Retrieve the vartag that can be assigned for the new TOAST tuple. This
+	 * depends on the type of TOAST table and its assigned value.
+	 */
+	tag = toast_external_assign_vartag(toast_pointer.toastrelid,
+									   toast_pointer.valueid);
+	info = toast_external_get_info(tag);
+
 	/*
 	 * Split up the item into chunks
 	 */
@@ -298,12 +322,12 @@ toast_save_datum(Relation rel, Datum value,
 		/*
 		 * Calculate the size of this chunk
 		 */
-		chunk_size = Min(TOAST_OID_MAX_CHUNK_SIZE, data_todo);
+		chunk_size = Min(info->maximum_chunk_size, data_todo);
 
 		/*
 		 * Build a tuple and store it
 		 */
-		t_values[0] = ObjectIdGetDatum(toast_pointer.va_valueid);
+		t_values[0] = ObjectIdGetDatum(toast_pointer.valueid);
 		t_values[1] = Int32GetDatum(chunk_seq++);
 		SET_VARSIZE(&chunk_data, chunk_size + VARHDRSZ);
 		memcpy(VARDATA(&chunk_data), data_p, chunk_size);
@@ -359,9 +383,7 @@ toast_save_datum(Relation rel, Datum value,
 	/*
 	 * Create the TOAST pointer value that we'll return
 	 */
-	result = (struct varlena *) palloc(TOAST_OID_POINTER_SIZE);
-	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_OID);
-	memcpy(VARDATA_EXTERNAL(result), &toast_pointer, sizeof(toast_pointer));
+	result = info->create_external_data(toast_pointer);
 
 	return PointerGetDatum(result);
 }
@@ -376,7 +398,7 @@ void
 toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 {
 	struct varlena *attr = (struct varlena *) DatumGetPointer(value);
-	varatt_external_oid toast_pointer;
+	toast_external_data toast_pointer;
 	Relation	toastrel;
 	Relation   *toastidxs;
 	ScanKeyData toastkey;
@@ -389,12 +411,12 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 		return;
 
 	/* Must copy to access aligned fields */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_external_info_get_data(attr, &toast_pointer);
 
 	/*
 	 * Open the toast relation and its indexes
 	 */
-	toastrel = table_open(toast_pointer.va_toastrelid, RowExclusiveLock);
+	toastrel = table_open(toast_pointer.toastrelid, RowExclusiveLock);
 
 	/* Fetch valid relation used for process */
 	validIndex = toast_open_indexes(toastrel,
@@ -408,7 +430,7 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	ScanKeyInit(&toastkey,
 				(AttrNumber) 1,
 				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(toast_pointer.va_valueid));
+				ObjectIdGetDatum(toast_pointer.valueid));
 
 	/*
 	 * Find all the chunks.  (We don't actually care whether we see them in
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index 11b4fc6487d6..b3cd0ec0dbf4 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -28,6 +28,7 @@
 #include "access/genam.h"
 #include "access/heapam.h"
 #include "access/heaptoast.h"
+#include "access/toast_external.h"
 #include "access/toast_helper.h"
 #include "access/toast_internals.h"
 #include "utils/fmgroids.h"
@@ -109,6 +110,7 @@ heap_toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup,
 	Datum		toast_oldvalues[MaxHeapAttributeNumber];
 	ToastAttrInfo toast_attr[MaxHeapAttributeNumber];
 	ToastTupleContext ttc;
+	uint8		tag;
 
 	/*
 	 * Ignore the INSERT_SPECULATIVE option. Speculative insertions/super
@@ -140,6 +142,16 @@ heap_toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup,
 	 * Prepare for toasting
 	 * ----------
 	 */
+
+	/*
+	 * Retrieve the toast pointer size based on the type of external TOAST
+	 * pointer assumed to be used.
+	 */
+
+	/* The default value is invalid, to work as a default. */
+	tag = toast_external_assign_vartag(rel->rd_rel->reltoastrelid, InvalidOid8);
+	ttc.ttc_toast_pointer_size = toast_external_info_get_pointer_size(tag);
+
 	ttc.ttc_rel = rel;
 	ttc.ttc_values = toast_values;
 	ttc.ttc_isnull = toast_isnull;
@@ -640,6 +652,8 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 	int			num_indexes;
 	int			validIndex;
 	int32		max_chunk_size;
+	const toast_external_info *info;
+	uint8		tag = VARTAG_INDIRECT;	/* init value does not matter */
 
 	/* Look for the valid index of toast relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -647,7 +661,11 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 									&toastidxs,
 									&num_indexes);
 
-	max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
+	/* Grab the information for toast_external_data */
+	tag = toast_external_assign_vartag(RelationGetRelid(toastrel), valueid);
+	info = toast_external_get_info(tag);
+
+	max_chunk_size = info->maximum_chunk_size;
 
 	totalchunks = ((attrsize - 1) / max_chunk_size) + 1;
 	startchunk = sliceoffset / max_chunk_size;
diff --git a/src/backend/access/table/toast_helper.c b/src/backend/access/table/toast_helper.c
index 0c58c6c32565..76a7cfe6174e 100644
--- a/src/backend/access/table/toast_helper.c
+++ b/src/backend/access/table/toast_helper.c
@@ -171,8 +171,10 @@ toast_tuple_init(ToastTupleContext *ttc)
  * The column must have attstorage EXTERNAL or EXTENDED if check_main is
  * false, and must have attstorage MAIN if check_main is true.
  *
- * The column must have a minimum size of MAXALIGN(TOAST_OID_POINTER_SIZE);
- * if not, no benefit is to be expected by compressing it.
+ * The column must have a minimum size of MAXALIGN(tcc_toast_pointer_size);
+ * if not, no benefit is to be expected by compressing it.  The TOAST
+ * pointer size is given by the caller, depending on the type of TOAST
+ * table we are dealing with.
  *
  * The return value is the index of the biggest suitable column, or
  * -1 if there is none.
@@ -184,10 +186,14 @@ toast_tuple_find_biggest_attribute(ToastTupleContext *ttc,
 	TupleDesc	tupleDesc = ttc->ttc_rel->rd_att;
 	int			numAttrs = tupleDesc->natts;
 	int			biggest_attno = -1;
-	int32		biggest_size = MAXALIGN(TOAST_OID_POINTER_SIZE);
+	int32		biggest_size = 0;
 	int32		skip_colflags = TOASTCOL_IGNORE;
 	int			i;
 
+	/* Define the lower-bound */
+	biggest_size = MAXALIGN(ttc->ttc_toast_pointer_size);
+	Assert(biggest_size != 0);
+
 	if (for_compression)
 		skip_colflags |= TOASTCOL_INCOMPRESSIBLE;
 
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 017a0f196c2b..430a38b1a216 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4280,7 +4280,7 @@ WriteControlFile(void)
 	ControlFile->nameDataLen = NAMEDATALEN;
 	ControlFile->indexMaxKeys = INDEX_MAX_KEYS;
 
-	ControlFile->toast_max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
+	ControlFile->toast_max_chunk_size = TOAST_MAX_CHUNK_SIZE;
 	ControlFile->loblksize = LOBLKSIZE;
 
 	ControlFile->float8ByVal = true;	/* vestigial */
@@ -4533,15 +4533,15 @@ ReadControlFile(void)
 						   "INDEX_MAX_KEYS", ControlFile->indexMaxKeys,
 						   "INDEX_MAX_KEYS", INDEX_MAX_KEYS),
 				 errhint("It looks like you need to recompile or initdb.")));
-	if (ControlFile->toast_max_chunk_size != TOAST_OID_MAX_CHUNK_SIZE)
+	if (ControlFile->toast_max_chunk_size != TOAST_MAX_CHUNK_SIZE)
 		ereport(FATAL,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("database files are incompatible with server"),
 		/* translator: %s is a variable name and %d is its value */
 				 errdetail("The database cluster was initialized with %s %d,"
 						   " but the server was compiled with %s %d.",
-						   "TOAST_OID_MAX_CHUNK_SIZE", ControlFile->toast_max_chunk_size,
-						   "TOAST_OID_MAX_CHUNK_SIZE", (int) TOAST_OID_MAX_CHUNK_SIZE),
+						   "TOAST_MAX_CHUNK_SIZE", ControlFile->toast_max_chunk_size,
+						   "TOAST_MAX_CHUNK_SIZE", (int) TOAST_MAX_CHUNK_SIZE),
 				 errhint("It looks like you need to recompile or initdb.")));
 	if (ControlFile->loblksize != LOBLKSIZE)
 		ereport(FATAL,
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 2c9840d4b9cd..e64501c84de4 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -92,6 +92,7 @@
 #include "access/detoast.h"
 #include "access/heapam.h"
 #include "access/rewriteheap.h"
+#include "access/toast_external.h"
 #include "access/transam.h"
 #include "access/xact.h"
 #include "access/xlog_internal.h"
@@ -5136,7 +5137,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		struct varlena *varlena;
 
 		/* va_rawsize is the size of the original datum -- including header */
-		varatt_external_oid toast_pointer;
+		toast_external_data toast_pointer;
 		struct varatt_indirect redirect_pointer;
 		struct varlena *new_datum = NULL;
 		struct varlena *reconstructed;
@@ -5162,8 +5163,8 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		if (!VARATT_IS_EXTERNAL(varlena))
 			continue;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, varlena);
-		toast_valueid = toast_pointer.va_valueid;
+		toast_external_info_get_data(varlena, &toast_pointer);
+		toast_valueid = toast_pointer.valueid;
 
 		/*
 		 * Check whether the toast tuple changed, replace if so.
@@ -5181,7 +5182,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 
 		free[natt] = true;
 
-		reconstructed = palloc0(toast_pointer.va_rawsize);
+		reconstructed = palloc0(toast_pointer.rawsize);
 
 		ent->reconstructed = reconstructed;
 
@@ -5206,10 +5207,10 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 				   VARSIZE(chunk) - VARHDRSZ);
 			data_done += VARSIZE(chunk) - VARHDRSZ;
 		}
-		Assert(data_done == VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer));
+		Assert(data_done == toast_pointer.extsize);
 
 		/* make sure its marked as compressed or not */
-		if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+		if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 			SET_VARSIZE_COMPRESSED(reconstructed, data_done + VARHDRSZ);
 		else
 			SET_VARSIZE(reconstructed, data_done + VARHDRSZ);
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index dfcf888c7c43..97d6e960143c 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -19,6 +19,7 @@
 
 #include "access/detoast.h"
 #include "access/toast_compression.h"
+#include "access/toast_external.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_type.h"
 #include "common/hashfn.h"
@@ -4195,7 +4196,7 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 {
 	int			typlen;
 	struct varlena *attr;
-	varatt_external_oid toast_pointer;
+	Oid8		toast_valueid;
 
 	/* On first call, get the input type's typlen, and save at *fn_extra */
 	if (fcinfo->flinfo->fn_extra == NULL)
@@ -4222,9 +4223,9 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		PG_RETURN_NULL();
 
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_valueid = toast_external_info_get_valueid(attr);
 
-	PG_RETURN_OID(toast_pointer.va_valueid);
+	PG_RETURN_OID(toast_valueid);
 }
 
 /*
diff --git a/src/bin/pg_resetwal/pg_resetwal.c b/src/bin/pg_resetwal/pg_resetwal.c
index deef3261f444..9bfab8c307bc 100644
--- a/src/bin/pg_resetwal/pg_resetwal.c
+++ b/src/bin/pg_resetwal/pg_resetwal.c
@@ -733,7 +733,7 @@ GuessControlValues(void)
 	ControlFile.xlog_seg_size = DEFAULT_XLOG_SEG_SIZE;
 	ControlFile.nameDataLen = NAMEDATALEN;
 	ControlFile.indexMaxKeys = INDEX_MAX_KEYS;
-	ControlFile.toast_max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
+	ControlFile.toast_max_chunk_size = TOAST_MAX_CHUNK_SIZE;
 	ControlFile.loblksize = LOBLKSIZE;
 	ControlFile.float8ByVal = true; /* vestigial */
 
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 5f51e8d55153..47f662f8b08a 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -16,6 +16,7 @@
 #include "access/multixact.h"
 #include "access/relation.h"
 #include "access/table.h"
+#include "access/toast_external.h"
 #include "access/toast_internals.h"
 #include "access/visibilitymap.h"
 #include "access/xact.h"
@@ -73,7 +74,8 @@ typedef enum SkipPages
  */
 typedef struct ToastedAttribute
 {
-	varatt_external_oid toast_pointer;
+	toast_external_data toast_pointer;
+	const toast_external_info *info;
 	BlockNumber blkno;			/* block in main table */
 	OffsetNumber offnum;		/* offset in main table */
 	AttrNumber	attnum;			/* attribute in main table */
@@ -1564,9 +1566,9 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	Oid8		toast_valueid;
 	int32		max_chunk_size;
 
-	toast_valueid = ta->toast_pointer.va_valueid;
+	toast_valueid = ta->toast_pointer.valueid;
 
-	max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
+	max_chunk_size = ta->info->maximum_chunk_size;
 	last_chunk_seq = (extsize - 1) / max_chunk_size;
 
 	/* Sanity-check the sequence number. */
@@ -1672,7 +1674,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	uint16		infomask;
 	Oid8		toast_pointer_valueid;
 	CompactAttribute *thisatt;
-	varatt_external_oid toast_pointer;
+	toast_external_data toast_pointer;
 
 	infomask = ctx->tuphdr->t_infomask;
 	thisatt = TupleDescCompactAttr(RelationGetDescr(ctx->rel), ctx->attnum);
@@ -1778,24 +1780,24 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	/*
 	 * Must copy attr into toast_pointer for alignment considerations
 	 */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-	toast_pointer_valueid = toast_pointer.va_valueid;
+	toast_external_info_get_data(attr, &toast_pointer);
+	toast_pointer_valueid = toast_pointer.valueid;
 
 	/* Toasted attributes too large to be untoasted should never be stored */
-	if (toast_pointer.va_rawsize > VARLENA_SIZE_LIMIT)
+	if (toast_pointer.rawsize > VARLENA_SIZE_LIMIT)
 		report_corruption(ctx,
 						  psprintf("toast value " OID8_FORMAT " rawsize %d exceeds limit %d",
 								   toast_pointer_valueid,
-								   toast_pointer.va_rawsize,
+								   toast_pointer.rawsize,
 								   VARLENA_SIZE_LIMIT));
 
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+	if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 	{
 		ToastCompressionId cmid;
 		bool		valid = false;
 
 		/* Compressed attributes should have a valid compression method */
-		cmid = TOAST_COMPRESS_METHOD(&toast_pointer);
+		cmid = toast_pointer.compression_method;
 		switch (cmid)
 		{
 				/* List of all valid compression method IDs */
@@ -1849,7 +1851,8 @@ check_tuple_attribute(HeapCheckContext *ctx)
 
 		ta = palloc0_object(ToastedAttribute);
 
-		VARATT_EXTERNAL_GET_POINTER(ta->toast_pointer, attr);
+		toast_external_info_get_data(attr, &ta->toast_pointer);
+		ta->info = toast_external_get_info(VARTAG_EXTERNAL(attr));
 		ta->blkno = ctx->blkno;
 		ta->offnum = ctx->offnum;
 		ta->attnum = ctx->attnum;
@@ -1876,9 +1879,11 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	int32		expected_chunk_seq = 0;
 	int32		last_chunk_seq;
 	Oid8		toast_valueid;
-	int32		max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
+	int32		max_chunk_size;
 
-	extsize = VARATT_EXTERNAL_GET_EXTSIZE(ta->toast_pointer);
+	extsize = ta->toast_pointer.extsize;
+
+	max_chunk_size = ta->info->maximum_chunk_size;
 	last_chunk_seq = (extsize - 1) / max_chunk_size;
 
 	/*
@@ -1887,7 +1892,7 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	ScanKeyInit(&toastkey,
 				(AttrNumber) 1,
 				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(ta->toast_pointer.va_valueid));
+				ObjectIdGetDatum(ta->toast_pointer.valueid));
 
 	/*
 	 * Check if any chunks for this toasted object exist in the toast table,
@@ -1907,7 +1912,7 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	}
 	systable_endscan_ordered(toastscan);
 
-	toast_valueid = ta->toast_pointer.va_valueid;
+	toast_valueid = ta->toast_pointer.valueid;
 
 	if (!found_toasttup)
 		report_toast_corruption(ctx, ta,
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 04845d5e6809..1cded8d21711 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -4197,6 +4197,8 @@ timeout_params
 timerCA
 tlist_vinfo
 toast_compress_header
+toast_external_data
+toast_external_info
 tokenize_error_callback_arg
 transferMode
 transfer_thread_arg
-- 
2.51.0

v9-0006-Move-static-inline-routines-of-varatt_external_oi.patchtext/x-diff; charset=us-asciiDownload
From 20d9745c84f6de1b31ea4ced594bd09b0be0431b Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 8 Aug 2025 15:40:04 +0900
Subject: [PATCH v9 06/15] Move static inline routines of varatt_external_oid
 to toast_external.c

This isolates most of the knowledge of varatt_external_oid into the
local area where it is manipulated through the toast_external transition
type, with the backend code not requiring it.  Extension code should not
need it either, as toast_external should be the layer to use when
looking at external on-dist TOAST varlenas.
---
 src/include/varatt.h                       | 31 -----------------
 src/backend/access/common/toast_external.c | 40 ++++++++++++++++++++--
 2 files changed, 37 insertions(+), 34 deletions(-)

diff --git a/src/include/varatt.h b/src/include/varatt.h
index 790d9f844c91..035c0f95e5b6 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -513,22 +513,6 @@ VARDATA_COMPRESSED_GET_COMPRESS_METHOD(const void *PTR)
 	return ((varattrib_4b *) PTR)->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS;
 }
 
-/*
- * Same for external Datums; but note argument is a struct
- * varatt_external_oid.
- */
-static inline Size
-VARATT_EXTERNAL_GET_EXTSIZE(varatt_external_oid toast_pointer)
-{
-	return toast_pointer.va_extinfo & VARLENA_EXTSIZE_MASK;
-}
-
-static inline uint32
-VARATT_EXTERNAL_GET_COMPRESS_METHOD(varatt_external_oid toast_pointer)
-{
-	return toast_pointer.va_extinfo >> VARLENA_EXTSIZE_BITS;
-}
-
 /* Set size and compress method of an externally-stored varlena datum */
 /* This has to remain a macro; beware multiple evaluations! */
 #define VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, len, cm) \
@@ -538,19 +522,4 @@ VARATT_EXTERNAL_GET_COMPRESS_METHOD(varatt_external_oid toast_pointer)
 		((toast_pointer).va_extinfo = \
 			(len) | ((uint32) (cm) << VARLENA_EXTSIZE_BITS)); \
 	} while (0)
-
-/*
- * Testing whether an externally-stored value is compressed now requires
- * comparing size stored in va_extinfo (the actual length of the external data)
- * to rawsize (the original uncompressed datum's size).  The latter includes
- * VARHDRSZ overhead, the former doesn't.  We never use compression unless it
- * actually saves space, so we expect either equality or less-than.
- */
-static inline bool
-VARATT_EXTERNAL_IS_COMPRESSED(varatt_external_oid toast_pointer)
-{
-	return VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) <
-		(Size) (toast_pointer.va_rawsize - VARHDRSZ);
-}
-
 #endif
diff --git a/src/backend/access/common/toast_external.c b/src/backend/access/common/toast_external.c
index 2154152b8bfb..4c500720e0d1 100644
--- a/src/backend/access/common/toast_external.c
+++ b/src/backend/access/common/toast_external.c
@@ -26,6 +26,40 @@ static void ondisk_oid_to_external_data(struct varlena *attr,
 										toast_external_data *data);
 static struct varlena *ondisk_oid_create_external_data(toast_external_data data);
 
+/*
+ * Decompressed size of an on-disk varlena; but note argument is a struct
+ * varatt_external_oid.
+ */
+static inline Size
+varatt_external_oid_get_extsize(varatt_external_oid toast_pointer)
+{
+	return toast_pointer.va_extinfo & VARLENA_EXTSIZE_MASK;
+}
+
+/*
+ * Compression method of an on-disk varlena; but note argument is a struct
+ *  varatt_external_oid.
+ */
+static inline uint32
+varatt_external_oid_get_compress_method(varatt_external_oid toast_pointer)
+{
+	return toast_pointer.va_extinfo >> VARLENA_EXTSIZE_BITS;
+}
+
+/*
+ * Testing whether an externally-stored TOAST value is compressed now requires
+ * comparing size stored in va_extinfo (the actual length of the external data)
+ * to rawsize (the original uncompressed datum's size).  The latter includes
+ * VARHDRSZ overhead, the former doesn't.  We never use compression unless it
+ * actually saves space, so we expect either equality or less-than.
+ */
+static inline bool
+varatt_external_oid_is_compressed(varatt_external_oid toast_pointer)
+{
+	return varatt_external_oid_get_extsize(toast_pointer) <
+		(Size) (toast_pointer.va_rawsize - VARHDRSZ);
+}
+
 /*
  * Size of an EXTERNAL datum that contains a standard TOAST pointer (OID
  * value).
@@ -146,10 +180,10 @@ ondisk_oid_to_external_data(struct varlena *attr, toast_external_data *data)
 	 * External size and compression methods are stored in the same field,
 	 * extract.
 	 */
-	if (VARATT_EXTERNAL_IS_COMPRESSED(external))
+	if (varatt_external_oid_is_compressed(external))
 	{
-		data->extsize = VARATT_EXTERNAL_GET_EXTSIZE(external);
-		data->compression_method = VARATT_EXTERNAL_GET_COMPRESS_METHOD(external);
+		data->extsize = varatt_external_oid_get_extsize(external);
+		data->compression_method = varatt_external_oid_get_compress_method(external);
 	}
 	else
 	{
-- 
2.51.0

v9-0007-Split-VARATT_EXTERNAL_GET_POINTER-for-indirect-an.patchtext/x-diff; charset=us-asciiDownload
From 1ca5d73dbb9b34bf34d94d4a97e7aa659ca8d68a Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 13 Aug 2025 19:38:50 +0900
Subject: [PATCH v9 07/15] Split VARATT_EXTERNAL_GET_POINTER for indirect and
 OID TOAST pointers

VARATT_EXTERNAL_GET_POINTER() is renamed to
VARATT_INDIRECT_GET_POINTER() with the external on-disk TOAST pointers
for OID values being now located within toast_external.c, splitting both
concepts completely.
---
 src/include/access/detoast.h               | 16 ++++++++--------
 src/backend/access/common/detoast.c        | 10 +++++-----
 src/backend/access/common/toast_external.c | 21 ++++++++++++++++++++-
 src/backend/utils/adt/expandeddatum.c      |  2 +-
 4 files changed, 34 insertions(+), 15 deletions(-)

diff --git a/src/include/access/detoast.h b/src/include/access/detoast.h
index 2f71fbd95f88..31e9786848ef 100644
--- a/src/include/access/detoast.h
+++ b/src/include/access/detoast.h
@@ -13,17 +13,17 @@
 #define DETOAST_H
 
 /*
- * Macro to fetch the possibly-unaligned contents of an EXTERNAL datum
- * into a local "varatt_external_*" toast pointer, as supported
- * in toast_external.h and varatt.h.  This should be just a memcpy, but
- * some versions of gcc seem to produce broken code that assumes the datum
- * contents are aligned.  Introducing an explicit intermediate
- * "varattrib_1b_e *" variable seems to fix it.
+ * Macro to fetch the possibly-unaligned contents of an indirect datum
+ * into a local "varatt_indirect" toast pointer, as supported
+ * in varatt.h.  This should be just a memcpy, but some versions of gcc
+ * seem to produce broken code that assumes the datum contents are aligned.
+ * Introducing an explicit intermediate "varattrib_1b_e *" variable seems
+ * to fix it.
  */
-#define VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr) \
+#define VARATT_INDIRECT_GET_POINTER(toast_pointer, attr) \
 do { \
 	varattrib_1b_e *attre = (varattrib_1b_e *) (attr); \
-	Assert(VARATT_IS_EXTERNAL(attre)); \
+	Assert(VARATT_IS_EXTERNAL_INDIRECT(attre)); \
 	Assert(VARSIZE_EXTERNAL(attre) == sizeof(toast_pointer) + VARHDRSZ_EXTERNAL); \
 	memcpy(&(toast_pointer), VARDATA_EXTERNAL(attre), sizeof(toast_pointer)); \
 } while (0)
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 8531c27439e4..b645988667f0 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -61,7 +61,7 @@ detoast_external_attr(struct varlena *attr)
 		 */
 		struct varatt_indirect redirect;
 
-		VARATT_EXTERNAL_GET_POINTER(redirect, attr);
+		VARATT_INDIRECT_GET_POINTER(redirect, attr);
 		attr = (struct varlena *) redirect.pointer;
 
 		/* nested indirect Datums aren't allowed */
@@ -138,7 +138,7 @@ detoast_attr(struct varlena *attr)
 		 */
 		struct varatt_indirect redirect;
 
-		VARATT_EXTERNAL_GET_POINTER(redirect, attr);
+		VARATT_INDIRECT_GET_POINTER(redirect, attr);
 		attr = (struct varlena *) redirect.pointer;
 
 		/* nested indirect Datums aren't allowed */
@@ -268,7 +268,7 @@ detoast_attr_slice(struct varlena *attr,
 	{
 		struct varatt_indirect redirect;
 
-		VARATT_EXTERNAL_GET_POINTER(redirect, attr);
+		VARATT_INDIRECT_GET_POINTER(redirect, attr);
 
 		/* nested indirect Datums aren't allowed */
 		Assert(!VARATT_IS_EXTERNAL_INDIRECT(redirect.pointer));
@@ -561,7 +561,7 @@ toast_raw_datum_size(Datum value)
 	{
 		struct varatt_indirect toast_pointer;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		VARATT_INDIRECT_GET_POINTER(toast_pointer, attr);
 
 		/* nested indirect Datums aren't allowed */
 		Assert(!VARATT_IS_EXTERNAL_INDIRECT(toast_pointer.pointer));
@@ -618,7 +618,7 @@ toast_datum_size(Datum value)
 	{
 		struct varatt_indirect toast_pointer;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		VARATT_INDIRECT_GET_POINTER(toast_pointer, attr);
 
 		/* nested indirect Datums aren't allowed */
 		Assert(!VARATT_IS_EXTERNAL_INDIRECT(attr));
diff --git a/src/backend/access/common/toast_external.c b/src/backend/access/common/toast_external.c
index 4c500720e0d1..e2f0a9dc1c50 100644
--- a/src/backend/access/common/toast_external.c
+++ b/src/backend/access/common/toast_external.c
@@ -26,6 +26,25 @@ static void ondisk_oid_to_external_data(struct varlena *attr,
 										toast_external_data *data);
 static struct varlena *ondisk_oid_create_external_data(toast_external_data data);
 
+/*
+ * Fetch the possibly-unaligned contents of an on-disk external TOAST with
+ * OID values into a local "varatt_external_oid" pointer.
+ *
+ * This should be just a memcpy, but some versions of gcc seem to produce
+ * broken code that assumes the datum contents are aligned.  Introducing
+ * an explicit intermediate "varattrib_1b_e *" variable seems to fix it.
+ */
+static inline void
+varatt_external_oid_get_pointer(varatt_external_oid *toast_pointer,
+								struct varlena *attr)
+{
+	varattrib_1b_e *attre = (varattrib_1b_e *) attr;
+
+	Assert(VARATT_IS_EXTERNAL_ONDISK_OID(attre));
+	Assert(VARSIZE_EXTERNAL(attre) == sizeof(varatt_external_oid) + VARHDRSZ_EXTERNAL);
+	memcpy(toast_pointer, VARDATA_EXTERNAL(attre), sizeof(varatt_external_oid));
+}
+
 /*
  * Decompressed size of an on-disk varlena; but note argument is a struct
  * varatt_external_oid.
@@ -173,7 +192,7 @@ ondisk_oid_to_external_data(struct varlena *attr, toast_external_data *data)
 {
 	varatt_external_oid external;
 
-	VARATT_EXTERNAL_GET_POINTER(external, attr);
+	varatt_external_oid_get_pointer(&external, attr);
 	data->rawsize = external.va_rawsize;
 
 	/*
diff --git a/src/backend/utils/adt/expandeddatum.c b/src/backend/utils/adt/expandeddatum.c
index 6b4b8eaf005c..4c04671d23ed 100644
--- a/src/backend/utils/adt/expandeddatum.c
+++ b/src/backend/utils/adt/expandeddatum.c
@@ -23,7 +23,7 @@
  * Given a Datum that is an expanded-object reference, extract the pointer.
  *
  * This is a bit tedious since the pointer may not be properly aligned;
- * compare VARATT_EXTERNAL_GET_POINTER().
+ * compare VARATT_INDIRECT_GET_POINTER().
  */
 ExpandedObjectHeader *
 DatumGetEOHP(Datum d)
-- 
2.51.0

v9-0008-Switch-pg_column_toast_chunk_id-return-value-from.patchtext/x-diff; charset=us-asciiDownload
From e6cbbf42b4a8c9afaed991c55d3d59ae123bd753 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 13 Aug 2025 19:55:39 +0900
Subject: [PATCH v9 08/15] Switch pg_column_toast_chunk_id() return value from
 oid to oid8

This is required for a follow-up patch that will add support for 8-byte
TOAST values, with this function being changed so as it is able to
support the largest TOAST value type available.

XXX: Bump catalog version.
---
 src/include/catalog/pg_proc.dat              | 2 +-
 src/backend/utils/adt/varlena.c              | 2 +-
 src/test/regress/expected/misc_functions.out | 2 +-
 src/test/regress/sql/misc_functions.sql      | 2 +-
 doc/src/sgml/func/func-admin.sgml            | 2 +-
 5 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index af0042d221b8..8fcc14980378 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7764,7 +7764,7 @@
   proargtypes => 'any', prosrc => 'pg_column_compression' },
 { oid => '6316', descr => 'chunk ID of on-disk TOASTed value',
   proname => 'pg_column_toast_chunk_id', provolatile => 's',
-  prorettype => 'oid', proargtypes => 'any',
+  prorettype => 'oid8', proargtypes => 'any',
   prosrc => 'pg_column_toast_chunk_id' },
 { oid => '2322',
   descr => 'total disk space usage for the specified tablespace',
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 97d6e960143c..6fb8ddd5d8b3 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -4225,7 +4225,7 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 
 	toast_valueid = toast_external_info_get_valueid(attr);
 
-	PG_RETURN_OID(toast_valueid);
+	PG_RETURN_OID8(toast_valueid);
 }
 
 /*
diff --git a/src/test/regress/expected/misc_functions.out b/src/test/regress/expected/misc_functions.out
index d7d965d884a1..1028934b9aba 100644
--- a/src/test/regress/expected/misc_functions.out
+++ b/src/test/regress/expected/misc_functions.out
@@ -962,7 +962,7 @@ SELECT t.relname AS toastrel FROM pg_class c
   WHERE c.relname = 'test_chunk_id'
 \gset
 SELECT pg_column_toast_chunk_id(a) IS NULL,
-  pg_column_toast_chunk_id(b) IN (SELECT chunk_id FROM pg_toast.:toastrel)
+  pg_column_toast_chunk_id(b) IN (SELECT chunk_id::oid8 FROM pg_toast.:toastrel)
   FROM test_chunk_id;
  ?column? | ?column? 
 ----------+----------
diff --git a/src/test/regress/sql/misc_functions.sql b/src/test/regress/sql/misc_functions.sql
index 0fc20fbb6b40..05ed8f517af0 100644
--- a/src/test/regress/sql/misc_functions.sql
+++ b/src/test/regress/sql/misc_functions.sql
@@ -440,7 +440,7 @@ SELECT t.relname AS toastrel FROM pg_class c
   WHERE c.relname = 'test_chunk_id'
 \gset
 SELECT pg_column_toast_chunk_id(a) IS NULL,
-  pg_column_toast_chunk_id(b) IN (SELECT chunk_id FROM pg_toast.:toastrel)
+  pg_column_toast_chunk_id(b) IN (SELECT chunk_id::oid8 FROM pg_toast.:toastrel)
   FROM test_chunk_id;
 DROP TABLE test_chunk_id;
 DROP FUNCTION explain_mask_costs(text, bool, bool, bool, bool);
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 2896cd9e4290..41bbd7cdef0d 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1588,7 +1588,7 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
          <primary>pg_column_toast_chunk_id</primary>
         </indexterm>
         <function>pg_column_toast_chunk_id</function> ( <type>"any"</type> )
-        <returnvalue>oid</returnvalue>
+        <returnvalue>oid8</returnvalue>
        </para>
        <para>
         Shows the <structfield>chunk_id</structfield> of an on-disk
-- 
2.51.0

v9-0009-Add-catcache-support-for-OID8OID.patchtext/x-diff; charset=us-asciiDownload
From e5e263e80dd9172ca4c5839e272ebef68e230cd2 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 13 Aug 2025 20:00:30 +0900
Subject: [PATCH v9 09/15] Add catcache support for OID8OID

This is required to be able to do catalog cache lookups of oid8 fields
for toast values of the same type.
---
 src/backend/utils/cache/catcache.c | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index 1d09c66ac959..e41f90849c71 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -240,6 +240,18 @@ int4hashfast(Datum datum)
 	return murmurhash32((int32) DatumGetInt32(datum));
 }
 
+static bool
+oid8eqfast(Datum a, Datum b)
+{
+	return DatumGetObjectId8(a) == DatumGetObjectId8(b);
+}
+
+static uint32
+oid8hashfast(Datum datum)
+{
+	return murmurhash64(DatumGetObjectId8(datum));
+}
+
 static bool
 texteqfast(Datum a, Datum b)
 {
@@ -300,6 +312,11 @@ GetCCHashEqFuncs(Oid keytype, CCHashFN *hashfunc, RegProcedure *eqfunc, CCFastEq
 			*fasteqfunc = int4eqfast;
 			*eqfunc = F_INT4EQ;
 			break;
+		case OID8OID:
+			*hashfunc = oid8hashfast;
+			*fasteqfunc = oid8eqfast;
+			*eqfunc = F_OID8EQ;
+			break;
 		case TEXTOID:
 			*hashfunc = texthashfast;
 			*fasteqfunc = texteqfast;
-- 
2.51.0

v9-0010-Add-support-for-TOAST-chunk_id-type-in-binary-upg.patchtext/x-diff; charset=us-asciiDownload
From 651225c459a4a07427389f13811cb9c54151da10 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 10:57:59 +0900
Subject: [PATCH v9 10/15] Add support for TOAST chunk_id type in binary
 upgrades

This commit adds a new function, which would set the type of a chunk_id
attribute for a TOAST table across upgrades.  This piece currently works
only with chunk_id = OIDOID, but it is required in a follow-up patch
where support for chunk_id = OID8OID is supported on top of the existing
one.
---
 src/include/catalog/binary_upgrade.h          |  1 +
 src/include/catalog/pg_proc.dat               |  4 ++++
 src/backend/catalog/heap.c                    |  1 +
 src/backend/catalog/toasting.c                | 20 ++++++++++++++++++-
 src/backend/utils/adt/pg_upgrade_support.c    | 11 ++++++++++
 src/bin/pg_dump/pg_dump.c                     | 10 +++++++++-
 .../expected/spgist_name_ops.out              |  6 ++++--
 7 files changed, 49 insertions(+), 4 deletions(-)

diff --git a/src/include/catalog/binary_upgrade.h b/src/include/catalog/binary_upgrade.h
index 6fcc59edebd8..3deb0423d795 100644
--- a/src/include/catalog/binary_upgrade.h
+++ b/src/include/catalog/binary_upgrade.h
@@ -29,6 +29,7 @@ extern PGDLLIMPORT Oid binary_upgrade_next_index_pg_class_oid;
 extern PGDLLIMPORT RelFileNumber binary_upgrade_next_index_pg_class_relfilenumber;
 extern PGDLLIMPORT Oid binary_upgrade_next_toast_pg_class_oid;
 extern PGDLLIMPORT RelFileNumber binary_upgrade_next_toast_pg_class_relfilenumber;
+extern PGDLLIMPORT Oid binary_upgrade_next_toast_chunk_id_typoid;
 
 extern PGDLLIMPORT Oid binary_upgrade_next_pg_enum_oid;
 extern PGDLLIMPORT Oid binary_upgrade_next_pg_authid_oid;
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 8fcc14980378..d2e9949dfc62 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11787,6 +11787,10 @@
   proname => 'binary_upgrade_set_next_toast_pg_class_oid', provolatile => 'v',
   proparallel => 'r', prorettype => 'void', proargtypes => 'oid',
   prosrc => 'binary_upgrade_set_next_toast_pg_class_oid' },
+{ oid => '8219', descr => 'for use by pg_upgrade',
+  proname => 'binary_upgrade_set_next_toast_chunk_id_typoid', provolatile => 'v',
+  proparallel => 'r', prorettype => 'void', proargtypes => 'oid',
+  prosrc => 'binary_upgrade_set_next_toast_chunk_id_typoid' },
 { oid => '3589', descr => 'for use by pg_upgrade',
   proname => 'binary_upgrade_set_next_pg_enum_oid', provolatile => 'v',
   proparallel => 'r', prorettype => 'void', proargtypes => 'oid',
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 265cc3e5fbf4..f1a4ad10d5f1 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -80,6 +80,7 @@
 /* Potentially set by pg_upgrade_support functions */
 Oid			binary_upgrade_next_heap_pg_class_oid = InvalidOid;
 Oid			binary_upgrade_next_toast_pg_class_oid = InvalidOid;
+Oid			binary_upgrade_next_toast_chunk_id_typoid = InvalidOid;
 RelFileNumber binary_upgrade_next_heap_pg_class_relfilenumber = InvalidRelFileNumber;
 RelFileNumber binary_upgrade_next_toast_pg_class_relfilenumber = InvalidRelFileNumber;
 
diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c
index 874a8fc89adb..f1d76d8acd51 100644
--- a/src/backend/catalog/toasting.c
+++ b/src/backend/catalog/toasting.c
@@ -145,6 +145,7 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	int16		coloptions[2];
 	ObjectAddress baseobject,
 				toastobject;
+	Oid			toast_chunkid_typid = OIDOID;
 
 	/*
 	 * Is it already toasted?
@@ -183,6 +184,23 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 		 */
 		if (!OidIsValid(binary_upgrade_next_toast_pg_class_oid))
 			return false;
+
+		/*
+		 * The attribute type for chunk_id should have been set when requesting
+		 * a TOAST table creation.
+		 */
+		if (!OidIsValid(binary_upgrade_next_toast_chunk_id_typoid))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("toast chunk_id type not set while in binary upgrade mode")));
+		if (binary_upgrade_next_toast_chunk_id_typoid != OIDOID)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("cannot support toast chunk_id type %u in binary upgrade mode",
+							binary_upgrade_next_toast_chunk_id_typoid)));
+
+		toast_chunkid_typid = binary_upgrade_next_toast_chunk_id_typoid;
+		binary_upgrade_next_toast_chunk_id_typoid = InvalidOid;
 	}
 
 	/*
@@ -204,7 +222,7 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	tupdesc = CreateTemplateTupleDesc(3);
 	TupleDescInitEntry(tupdesc, (AttrNumber) 1,
 					   "chunk_id",
-					   OIDOID,
+					   toast_chunkid_typid,
 					   -1, 0);
 	TupleDescInitEntry(tupdesc, (AttrNumber) 2,
 					   "chunk_seq",
diff --git a/src/backend/utils/adt/pg_upgrade_support.c b/src/backend/utils/adt/pg_upgrade_support.c
index a4f8b4faa90d..200ffcdbab44 100644
--- a/src/backend/utils/adt/pg_upgrade_support.c
+++ b/src/backend/utils/adt/pg_upgrade_support.c
@@ -149,6 +149,17 @@ binary_upgrade_set_next_toast_pg_class_oid(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+Datum
+binary_upgrade_set_next_toast_chunk_id_typoid(PG_FUNCTION_ARGS)
+{
+	Oid			typoid = PG_GETARG_OID(0);
+
+	CHECK_IS_BINARY_UPGRADE;
+	binary_upgrade_next_toast_chunk_id_typoid = typoid;
+
+	PG_RETURN_VOID();
+}
+
 Datum
 binary_upgrade_set_next_toast_relfilenode(PG_FUNCTION_ARGS)
 {
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 24ad201af2f9..d8856d48b4c3 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -103,6 +103,7 @@ typedef struct
 	RelFileNumber relfilenumber;	/* object filenode */
 	Oid			toast_oid;		/* toast table OID */
 	RelFileNumber toast_relfilenumber;	/* toast table filenode */
+	Oid			toast_chunk_id_typoid;	/* type of chunk_id attribute */
 	Oid			toast_index_oid;	/* toast table index OID */
 	RelFileNumber toast_index_relfilenumber;	/* toast table index filenode */
 } BinaryUpgradeClassOidItem;
@@ -5826,7 +5827,10 @@ collectBinaryUpgradeClassOids(Archive *fout)
 	const char *query;
 
 	query = "SELECT c.oid, c.relkind, c.relfilenode, c.reltoastrelid, "
-		"ct.relfilenode, i.indexrelid, cti.relfilenode "
+		"ct.relfilenode, i.indexrelid, cti.relfilenode, "
+		"(SELECT a.atttypid FROM pg_attribute AS a "
+		"  WHERE a.attrelid = c.reltoastrelid AND attname = 'chunk_id'::text) "
+		"  AS toastchunktypid "
 		"FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_index i "
 		"ON (c.reltoastrelid = i.indrelid AND i.indisvalid) "
 		"LEFT JOIN pg_catalog.pg_class ct ON (c.reltoastrelid = ct.oid) "
@@ -5848,6 +5852,7 @@ collectBinaryUpgradeClassOids(Archive *fout)
 		binaryUpgradeClassOids[i].toast_relfilenumber = atooid(PQgetvalue(res, i, 4));
 		binaryUpgradeClassOids[i].toast_index_oid = atooid(PQgetvalue(res, i, 5));
 		binaryUpgradeClassOids[i].toast_index_relfilenumber = atooid(PQgetvalue(res, i, 6));
+		binaryUpgradeClassOids[i].toast_chunk_id_typoid = atooid(PQgetvalue(res, i, 7));
 	}
 
 	PQclear(res);
@@ -5912,6 +5917,9 @@ binary_upgrade_set_pg_class_oids(Archive *fout,
 			appendPQExpBuffer(upgrade_buffer,
 							  "SELECT pg_catalog.binary_upgrade_set_next_toast_relfilenode('%u'::pg_catalog.oid);\n",
 							  entry->toast_relfilenumber);
+			appendPQExpBuffer(upgrade_buffer,
+							  "SELECT pg_catalog.binary_upgrade_set_next_toast_chunk_id_typoid('%u'::pg_catalog.oid);\n",
+							  entry->toast_chunk_id_typoid);
 
 			/* every toast table has an index */
 			appendPQExpBuffer(upgrade_buffer,
diff --git a/src/test/modules/spgist_name_ops/expected/spgist_name_ops.out b/src/test/modules/spgist_name_ops/expected/spgist_name_ops.out
index 1ee65ede2430..35e59d0cd83c 100644
--- a/src/test/modules/spgist_name_ops/expected/spgist_name_ops.out
+++ b/src/test/modules/spgist_name_ops/expected/spgist_name_ops.out
@@ -61,9 +61,10 @@ select * from t
  binary_upgrade_set_next_pg_enum_oid                  |    | binary_upgrade_set_next_pg_enum_oid
  binary_upgrade_set_next_pg_tablespace_oid            |    | binary_upgrade_set_next_pg_tablespace_oid
  binary_upgrade_set_next_pg_type_oid                  |    | binary_upgrade_set_next_pg_type_oid
+ binary_upgrade_set_next_toast_chunk_id_typoid        |    | binary_upgrade_set_next_toast_chunk_id_typoid
  binary_upgrade_set_next_toast_pg_class_oid           |  1 | binary_upgrade_set_next_toast_pg_class_oid
  binary_upgrade_set_next_toast_relfilenode            |    | binary_upgrade_set_next_toast_relfilenode
-(13 rows)
+(14 rows)
 
 -- Verify clean failure when INCLUDE'd columns result in overlength tuple
 -- The error message details are platform-dependent, so show only SQLSTATE
@@ -110,9 +111,10 @@ select * from t
  binary_upgrade_set_next_pg_enum_oid                  |    | binary_upgrade_set_next_pg_enum_oid
  binary_upgrade_set_next_pg_tablespace_oid            |    | binary_upgrade_set_next_pg_tablespace_oid
  binary_upgrade_set_next_pg_type_oid                  |    | binary_upgrade_set_next_pg_type_oid
+ binary_upgrade_set_next_toast_chunk_id_typoid        |    | binary_upgrade_set_next_toast_chunk_id_typoid
  binary_upgrade_set_next_toast_pg_class_oid           |  1 | binary_upgrade_set_next_toast_pg_class_oid
  binary_upgrade_set_next_toast_relfilenode            |    | binary_upgrade_set_next_toast_relfilenode
-(13 rows)
+(14 rows)
 
 \set VERBOSITY sqlstate
 insert into t values(repeat('xyzzy', 12), 42, repeat('xyzzy', 4000));
-- 
2.51.0

v9-0011-Enlarge-OID-generation-to-8-bytes.patchtext/x-diff; charset=us-asciiDownload
From e08af019c973736b94ce53a941d8502af2e9d29d Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 12:15:15 +0900
Subject: [PATCH v9 11/15] Enlarge OID generation to 8 bytes

This adds a new routine called GetNewObjectId8() in varsup.c, which is
able to retrieve a 64b OID.  GetNewObjectId() is kept compatible with
its origin, where we still check that the lower 32 bits of the counter
do not wraparound, handling the FirstNormalObjectId case.

pg_resetwal -o/--next-oid is updated to be able to handle 8-byte OIDs.
---
 src/include/access/transam.h              |  3 +-
 src/include/access/xlog.h                 |  2 +-
 src/include/catalog/pg_control.h          |  2 +-
 src/backend/access/rmgrdesc/xlogdesc.c    |  8 +--
 src/backend/access/transam/varsup.c       | 62 ++++++++++++++++-------
 src/backend/access/transam/xlog.c         |  8 +--
 src/backend/access/transam/xlogrecovery.c |  2 +-
 src/bin/pg_controldata/pg_controldata.c   |  2 +-
 src/bin/pg_resetwal/pg_resetwal.c         | 10 ++--
 doc/src/sgml/ref/pg_resetwal.sgml         |  6 +--
 10 files changed, 66 insertions(+), 39 deletions(-)

diff --git a/src/include/access/transam.h b/src/include/access/transam.h
index c9e204182757..2cde38e28b3d 100644
--- a/src/include/access/transam.h
+++ b/src/include/access/transam.h
@@ -211,7 +211,7 @@ typedef struct TransamVariablesData
 	/*
 	 * These fields are protected by OidGenLock.
 	 */
-	Oid			nextOid;		/* next OID to assign */
+	Oid8		nextOid;		/* next OID (8 bytes) to assign */
 	uint32		oidCount;		/* OIDs available before must do XLOG work */
 
 	/*
@@ -355,6 +355,7 @@ extern void SetTransactionIdLimit(TransactionId oldest_datfrozenxid,
 extern void AdvanceOldestClogXid(TransactionId oldest_datfrozenxid);
 extern bool ForceTransactionIdLimitUpdate(void);
 extern Oid	GetNewObjectId(void);
+extern Oid8 GetNewObjectId8(void);
 extern void StopGeneratingPinnedObjectIds(void);
 
 #ifdef USE_ASSERT_CHECKING
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index 605280ed8fb6..9c8d79394625 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -244,7 +244,7 @@ extern void ShutdownXLOG(int code, Datum arg);
 extern bool CreateCheckPoint(int flags);
 extern bool CreateRestartPoint(int flags);
 extern WALAvailability GetWALAvailability(XLogRecPtr targetLSN);
-extern void XLogPutNextOid(Oid nextOid);
+extern void XLogPutNextOid(Oid8 nextOid);
 extern XLogRecPtr XLogRestorePoint(const char *rpName);
 extern void UpdateFullPageWrites(void);
 extern void GetFullPageWriteInfo(XLogRecPtr *RedoRecPtr_p, bool *doPageWrites_p);
diff --git a/src/include/catalog/pg_control.h b/src/include/catalog/pg_control.h
index 293e9e03f599..2bd747c4f829 100644
--- a/src/include/catalog/pg_control.h
+++ b/src/include/catalog/pg_control.h
@@ -42,7 +42,7 @@ typedef struct CheckPoint
 	bool		fullPageWrites; /* current full_page_writes */
 	int			wal_level;		/* current wal_level */
 	FullTransactionId nextXid;	/* next free transaction ID */
-	Oid			nextOid;		/* next free OID */
+	Oid8		nextOid;		/* next free OID */
 	MultiXactId nextMulti;		/* next free MultiXactId */
 	MultiXactOffset nextMultiOffset;	/* next free MultiXact offset */
 	TransactionId oldestXid;	/* cluster-wide minimum datfrozenxid */
diff --git a/src/backend/access/rmgrdesc/xlogdesc.c b/src/backend/access/rmgrdesc/xlogdesc.c
index 441034f5929c..3ac4bab7a8a2 100644
--- a/src/backend/access/rmgrdesc/xlogdesc.c
+++ b/src/backend/access/rmgrdesc/xlogdesc.c
@@ -66,7 +66,7 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
 		CheckPoint *checkpoint = (CheckPoint *) rec;
 
 		appendStringInfo(buf, "redo %X/%08X; "
-						 "tli %u; prev tli %u; fpw %s; wal_level %s; xid %u:%u; oid %u; multi %u; offset %" PRIu64 "; "
+						 "tli %u; prev tli %u; fpw %s; wal_level %s; xid %u:%u; oid " OID8_FORMAT "; multi %u; offset %" PRIu64 "; "
 						 "oldest xid %u in DB %u; oldest multi %u in DB %u; "
 						 "oldest/newest commit timestamp xid: %u/%u; "
 						 "oldest running xid %u; %s",
@@ -91,10 +91,10 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
 	}
 	else if (info == XLOG_NEXTOID)
 	{
-		Oid			nextOid;
+		Oid8		nextOid;
 
-		memcpy(&nextOid, rec, sizeof(Oid));
-		appendStringInfo(buf, "%u", nextOid);
+		memcpy(&nextOid, rec, sizeof(Oid8));
+		appendStringInfo(buf, OID8_FORMAT, nextOid);
 	}
 	else if (info == XLOG_RESTORE_POINT)
 	{
diff --git a/src/backend/access/transam/varsup.c b/src/backend/access/transam/varsup.c
index f8c4dada7c93..662d1dcaed16 100644
--- a/src/backend/access/transam/varsup.c
+++ b/src/backend/access/transam/varsup.c
@@ -542,31 +542,51 @@ ForceTransactionIdLimitUpdate(void)
 
 
 /*
- * GetNewObjectId -- allocate a new OID
+ * GetNewObjectId -- allocate a new OID (4 bytes)
  *
- * OIDs are generated by a cluster-wide counter.  Since they are only 32 bits
- * wide, counter wraparound will occur eventually, and therefore it is unwise
- * to assume they are unique unless precautions are taken to make them so.
- * Hence, this routine should generally not be used directly.  The only direct
- * callers should be GetNewOidWithIndex() and GetNewRelFileNumber() in
- * catalog/catalog.c.
+ * OIDs are generated by a cluster-wide counter.  The callers of this routine
+ * expect a 32 bit-wide counter, and counter wraparound will occur eventually,
+ * and therefore it is unwise to assume they are unique unless precautions are
+ * taken to make them so.  This routine should generally not be used directly.
+ * The only direct callers should be GetNewOidWithIndex() and
+ * GetNewRelFileNumber() in catalog/catalog.c.
  */
 Oid
 GetNewObjectId(void)
 {
-	Oid			result;
+	return (Oid) GetNewObjectId8();
+}
+
+/*
+ * GetNewObjectId8 -- allocate a new OID (8 bytes)
+ *
+ * This routine can be called directly if the consumer of the OID allocated
+ * stores the counter in an 8-byte space, where wraparound does not matter.
+ * We still need to care about the wraparound case in the low 32 bits of the
+ * space allocated, GetNewObjectId() expecting OIDs to never be allocated
+ * up to FirstNormalObjectId.
+ */
+Oid8
+GetNewObjectId8(void)
+{
+	Oid8		result;
+	Oid			nextoid_lo;
+	uint32		nextoid_hi;
 
 	/* safety check, we should never get this far in a HS standby */
 	if (RecoveryInProgress())
 		elog(ERROR, "cannot assign OIDs during recovery");
 
 	LWLockAcquire(OidGenLock, LW_EXCLUSIVE);
+	nextoid_lo = (Oid) TransamVariables->nextOid;
+	nextoid_hi = (uint32) (TransamVariables->nextOid >> 32);
 
 	/*
-	 * Check for wraparound of the OID counter.  We *must* not return 0
-	 * (InvalidOid), and in normal operation we mustn't return anything below
-	 * FirstNormalObjectId since that range is reserved for initdb (see
-	 * IsCatalogRelationOid()).  Note we are relying on unsigned comparison.
+	 * Check for wraparound of the OID counter in its lower 4 bytes.  We
+	 * *must* not return 0 (InvalidOid), and in normal operation we
+	 * mustn't return anything below FirstNormalObjectId since that range
+	 * is reserved for initdb (see IsCatalogRelationOid()).  Note we are
+	 * relying on unsigned comparison.
 	 *
 	 * During initdb, we start the OID generator at FirstGenbkiObjectId, so we
 	 * only wrap if before that point when in bootstrap or standalone mode.
@@ -576,26 +596,32 @@ GetNewObjectId(void)
 	 * available for automatic assignment during initdb, while ensuring they
 	 * will never conflict with user-assigned OIDs.
 	 */
-	if (TransamVariables->nextOid < ((Oid) FirstNormalObjectId))
+	if (nextoid_lo < ((Oid) FirstNormalObjectId))
 	{
 		if (IsPostmasterEnvironment)
 		{
 			/* wraparound, or first post-initdb assignment, in normal mode */
-			TransamVariables->nextOid = FirstNormalObjectId;
+			nextoid_lo = FirstNormalObjectId;
 			TransamVariables->oidCount = 0;
 		}
 		else
 		{
 			/* we may be bootstrapping, so don't enforce the full range */
-			if (TransamVariables->nextOid < ((Oid) FirstGenbkiObjectId))
+			if (nextoid_lo < ((Oid) FirstGenbkiObjectId))
 			{
 				/* wraparound in standalone mode (unlikely but possible) */
-				TransamVariables->nextOid = FirstNormalObjectId;
+				nextoid_lo = FirstNormalObjectId;
 				TransamVariables->oidCount = 0;
 			}
 		}
 	}
 
+	/*
+	 * Set next OID in its 8-byte space, skipping the first post-init
+	 * assignment.
+	 */
+	TransamVariables->nextOid = ((Oid8) nextoid_hi) << 32 | nextoid_lo;
+
 	/* If we run out of logged for use oids then we must log more */
 	if (TransamVariables->oidCount == 0)
 	{
@@ -620,7 +646,7 @@ GetNewObjectId(void)
  * to the specified value.
  */
 static void
-SetNextObjectId(Oid nextOid)
+SetNextObjectId(Oid8 nextOid)
 {
 	/* Safety check, this is only allowable during initdb */
 	if (IsPostmasterEnvironment)
@@ -630,7 +656,7 @@ SetNextObjectId(Oid nextOid)
 	LWLockAcquire(OidGenLock, LW_EXCLUSIVE);
 
 	if (TransamVariables->nextOid > nextOid)
-		elog(ERROR, "too late to advance OID counter to %u, it is now %u",
+		elog(ERROR, "too late to advance OID counter to " OID8_FORMAT ", it is now " OID8_FORMAT,
 			 nextOid, TransamVariables->nextOid);
 
 	TransamVariables->nextOid = nextOid;
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 430a38b1a216..13d7393566c7 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -8094,10 +8094,10 @@ KeepLogSeg(XLogRecPtr recptr, XLogSegNo *logSegNo)
  * Write a NEXTOID log record
  */
 void
-XLogPutNextOid(Oid nextOid)
+XLogPutNextOid(Oid8 nextOid)
 {
 	XLogBeginInsert();
-	XLogRegisterData(&nextOid, sizeof(Oid));
+	XLogRegisterData(&nextOid, sizeof(Oid8));
 	(void) XLogInsert(RM_XLOG_ID, XLOG_NEXTOID);
 
 	/*
@@ -8320,7 +8320,7 @@ xlog_redo(XLogReaderState *record)
 
 	if (info == XLOG_NEXTOID)
 	{
-		Oid			nextOid;
+		Oid8		nextOid;
 
 		/*
 		 * We used to try to take the maximum of TransamVariables->nextOid and
@@ -8329,7 +8329,7 @@ xlog_redo(XLogReaderState *record)
 		 * anyway, better to just believe the record exactly.  We still take
 		 * OidGenLock while setting the variable, just in case.
 		 */
-		memcpy(&nextOid, XLogRecGetData(record), sizeof(Oid));
+		memcpy(&nextOid, XLogRecGetData(record), sizeof(Oid8));
 		LWLockAcquire(OidGenLock, LW_EXCLUSIVE);
 		TransamVariables->nextOid = nextOid;
 		TransamVariables->oidCount = 0;
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index 38b594d21709..a0a464caf1fa 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -892,7 +892,7 @@ InitWalRecovery(ControlFileData *ControlFile, bool *wasShutdown_ptr,
 							LSN_FORMAT_ARGS(checkPoint.redo),
 							wasShutdown ? "true" : "false"));
 	ereport(DEBUG1,
-			(errmsg_internal("next transaction ID: " UINT64_FORMAT "; next OID: %u",
+			(errmsg_internal("next transaction ID: " UINT64_FORMAT "; next OID: " OID8_FORMAT,
 							 U64FromFullTransactionId(checkPoint.nextXid),
 							 checkPoint.nextOid)));
 	ereport(DEBUG1,
diff --git a/src/bin/pg_controldata/pg_controldata.c b/src/bin/pg_controldata/pg_controldata.c
index a4060309ae0e..bfb89536280f 100644
--- a/src/bin/pg_controldata/pg_controldata.c
+++ b/src/bin/pg_controldata/pg_controldata.c
@@ -267,7 +267,7 @@ main(int argc, char *argv[])
 	printf(_("Latest checkpoint's NextXID:          %u:%u\n"),
 		   EpochFromFullTransactionId(ControlFile->checkPointCopy.nextXid),
 		   XidFromFullTransactionId(ControlFile->checkPointCopy.nextXid));
-	printf(_("Latest checkpoint's NextOID:          %u\n"),
+	printf(_("Latest checkpoint's NextOID:          " OID8_FORMAT "\n"),
 		   ControlFile->checkPointCopy.nextOid);
 	printf(_("Latest checkpoint's NextMultiXactId:  %u\n"),
 		   ControlFile->checkPointCopy.nextMulti);
diff --git a/src/bin/pg_resetwal/pg_resetwal.c b/src/bin/pg_resetwal/pg_resetwal.c
index 9bfab8c307bc..9e69a54254d3 100644
--- a/src/bin/pg_resetwal/pg_resetwal.c
+++ b/src/bin/pg_resetwal/pg_resetwal.c
@@ -82,7 +82,7 @@ static TransactionId oldest_commit_ts_xid_val;
 static TransactionId newest_commit_ts_xid_val;
 
 static bool next_oid_given = false;
-static Oid	next_oid_val;
+static Oid8	next_oid_val;
 
 static bool mxids_given = false;
 static MultiXactId next_mxid_val;
@@ -253,7 +253,7 @@ main(int argc, char *argv[])
 
 			case 'o':
 				errno = 0;
-				next_oid_val = strtouint32_strict(optarg, &endptr, 0);
+				next_oid_val = strtou64(optarg, &endptr, 0);
 				if (endptr == optarg || *endptr != '\0' || errno != 0)
 				{
 					pg_log_error("invalid argument for option %s", "-o");
@@ -771,7 +771,7 @@ PrintControlValues(bool guessed)
 	printf(_("Latest checkpoint's NextXID:          %u:%u\n"),
 		   EpochFromFullTransactionId(ControlFile.checkPointCopy.nextXid),
 		   XidFromFullTransactionId(ControlFile.checkPointCopy.nextXid));
-	printf(_("Latest checkpoint's NextOID:          %u\n"),
+	printf(_("Latest checkpoint's NextOID:          " OID8_FORMAT "\n"),
 		   ControlFile.checkPointCopy.nextOid);
 	printf(_("Latest checkpoint's NextMultiXactId:  %u\n"),
 		   ControlFile.checkPointCopy.nextMulti);
@@ -857,7 +857,7 @@ PrintNewControlValues(void)
 
 	if (next_oid_given)
 	{
-		printf(_("NextOID:                              %u\n"),
+		printf(_("NextOID:                              " OID8_FORMAT "\n"),
 			   ControlFile.checkPointCopy.nextOid);
 	}
 
@@ -1227,7 +1227,7 @@ usage(void)
 	printf(_("  -e, --epoch=XIDEPOCH             set next transaction ID epoch\n"));
 	printf(_("  -l, --next-wal-file=WALFILE      set minimum starting location for new WAL\n"));
 	printf(_("  -m, --multixact-ids=MXID,MXID    set next and oldest multitransaction ID\n"));
-	printf(_("  -o, --next-oid=OID               set next OID\n"));
+	printf(_("  -o, --next-oid=OID8              set next OID (8 bytes)\n"));
 	printf(_("  -O, --multixact-offset=OFFSET    set next multitransaction offset\n"));
 	printf(_("  -u, --oldest-transaction-id=XID  set oldest transaction ID\n"));
 	printf(_("  -x, --next-transaction-id=XID    set next transaction ID\n"));
diff --git a/doc/src/sgml/ref/pg_resetwal.sgml b/doc/src/sgml/ref/pg_resetwal.sgml
index 41f2b1d480c5..83483d883bd1 100644
--- a/doc/src/sgml/ref/pg_resetwal.sgml
+++ b/doc/src/sgml/ref/pg_resetwal.sgml
@@ -282,11 +282,11 @@ PostgreSQL documentation
    </varlistentry>
 
    <varlistentry>
-    <term><option>-o <replaceable class="parameter">oid</replaceable></option></term>
-    <term><option>--next-oid=<replaceable class="parameter">oid</replaceable></option></term>
+    <term><option>-o <replaceable class="parameter">oid8</replaceable></option></term>
+    <term><option>--next-oid=<replaceable class="parameter">oid8</replaceable></option></term>
     <listitem>
      <para>
-      Manually set the next OID.
+      Manually set the next OID (8 bytes).
      </para>
 
      <para>
-- 
2.51.0

v9-0012-Add-relation-option-toast_value_type.patchtext/x-diff; charset=us-asciiDownload
From 66149352d7e2213059793e063667f2dbe0c7fc89 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 12:54:58 +0900
Subject: [PATCH v9 12/15] Add relation option toast_value_type

This relation option gives the possibility to define the attribute type
that can be used for chunk_id in a TOAST table when it is created
initially.  This parameter has no effect if a TOAST table exists, even
after it is modified later on, even on rewrites.

This can be set only to "oid" currently, and will be expanded with a
second mode later.

Note: perhaps it would make sense to introduce that only when support
for 8-byte OID values are added to TOAST, the split is here to ease
review.
---
 src/include/utils/rel.h                | 17 +++++++++++++++++
 src/backend/access/common/reloptions.c | 21 +++++++++++++++++++++
 src/backend/catalog/toasting.c         |  6 ++++++
 src/bin/psql/tab-complete.in.c         |  1 +
 doc/src/sgml/ref/create_table.sgml     | 18 ++++++++++++++++++
 5 files changed, 63 insertions(+)

diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 80286076a111..fcd59099535f 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -338,11 +338,20 @@ typedef enum StdRdOptIndexCleanup
 	STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON,
 } StdRdOptIndexCleanup;
 
+/* StdRdOptions->toast_value_type values */
+typedef enum StdRdOptToastValueType
+{
+	STDRD_OPTION_TOAST_VALUE_TYPE_INVALID = 0,
+	STDRD_OPTION_TOAST_VALUE_TYPE_OID,
+} StdRdOptToastValueType;
+
 typedef struct StdRdOptions
 {
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	int			fillfactor;		/* page fill factor in percent (0..100) */
 	int			toast_tuple_target; /* target for tuple toasting */
+	StdRdOptToastValueType	toast_value_type;	/* type assigned to chunk_id
+												 * at toast table creation */
 	AutoVacOpts autovacuum;		/* autovacuum-related options */
 	bool		user_catalog_table; /* use as an additional catalog relation */
 	int			parallel_workers;	/* max number of parallel workers */
@@ -368,6 +377,14 @@ typedef struct StdRdOptions
 	((relation)->rd_options ? \
 	 ((StdRdOptions *) (relation)->rd_options)->toast_tuple_target : (defaulttarg))
 
+/*
+ * RelationGetToastValueType
+ *		Returns the relation's toast_value_type.  Note multiple eval of argument!
+ */
+#define RelationGetToastValueType(relation, defaulttarg) \
+	((relation)->rd_options ? \
+	 ((StdRdOptions *) (relation)->rd_options)->toast_value_type : defaulttarg)
+
 /*
  * RelationGetFillFactor
  *		Returns the relation's fillfactor.  Note multiple eval of argument!
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 31926d8a368a..cafa72ae1c11 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -525,6 +525,14 @@ static relopt_enum_elt_def viewCheckOptValues[] =
 	{(const char *) NULL}		/* list terminator */
 };
 
+/* values from StdRdOptToastValueType */
+static relopt_enum_elt_def StdRdOptToastValueTypes[] =
+{
+	/* no value for INVALID */
+	{"oid", STDRD_OPTION_TOAST_VALUE_TYPE_OID},
+	{(const char *) NULL}		/* list terminator */
+};
+
 static relopt_enum enumRelOpts[] =
 {
 	{
@@ -538,6 +546,17 @@ static relopt_enum enumRelOpts[] =
 		STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO,
 		gettext_noop("Valid values are \"on\", \"off\", and \"auto\".")
 	},
+	{
+		{
+			"toast_value_type",
+			"Controls the attribute type of chunk_id at toast table creation",
+			RELOPT_KIND_HEAP,
+			ShareUpdateExclusiveLock
+		},
+		StdRdOptToastValueTypes,
+		STDRD_OPTION_TOAST_VALUE_TYPE_OID,
+		gettext_noop("Valid values are \"oid\".")
+	},
 	{
 		{
 			"buffering",
@@ -1909,6 +1928,8 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
 		offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_analyze_min_duration)},
 		{"toast_tuple_target", RELOPT_TYPE_INT,
 		offsetof(StdRdOptions, toast_tuple_target)},
+		{"toast_value_type", RELOPT_TYPE_ENUM,
+		offsetof(StdRdOptions, toast_value_type)},
 		{"autovacuum_vacuum_cost_delay", RELOPT_TYPE_REAL,
 		offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_delay)},
 		{"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c
index f1d76d8acd51..545983b5be9d 100644
--- a/src/backend/catalog/toasting.c
+++ b/src/backend/catalog/toasting.c
@@ -158,9 +158,15 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	 */
 	if (!IsBinaryUpgrade)
 	{
+		StdRdOptToastValueType value_type;
+
 		/* Normal mode, normal check */
 		if (!needs_toast_table(rel))
 			return false;
+
+		value_type = RelationGetToastValueType(rel, STDRD_OPTION_TOAST_VALUE_TYPE_OID);
+		if (value_type == STDRD_OPTION_TOAST_VALUE_TYPE_OID)
+			toast_chunkid_typid = OIDOID;
 	}
 	else
 	{
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index b1ff6f6cd949..a112e20a5c7c 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -1453,6 +1453,7 @@ static const char *const table_storage_parameters[] = {
 	"toast.vacuum_max_eager_freeze_failure_rate",
 	"toast.vacuum_truncate",
 	"toast_tuple_target",
+	"toast_value_type",
 	"user_catalog_table",
 	"vacuum_index_cleanup",
 	"vacuum_max_eager_freeze_failure_rate",
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 77c5a763d450..200dfc81bf8f 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1632,6 +1632,24 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     </listitem>
    </varlistentry>
 
+   <varlistentry id="reloption-toast-value-type" xreflabel="toast_value_type">
+    <term><literal>toast_value_type</literal> (<type>enum</type>)
+    <indexterm>
+     <primary><varname>toast_value_type</varname> storage parameter</primary>
+    </indexterm>
+    </term>
+    <listitem>
+     <para>
+      The toast_value_type specifies the attribute type of
+      <literal>chunk_id</literal> used when initially creating  a toast
+      relation for this table.
+      By default this parameter is <literal>oid</literal>, to assign
+      <type>oid</type> as attribute type to <literal>chunk_id</literal>.
+      This parameter cannot be set for TOAST tables.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="reloption-parallel-workers" xreflabel="parallel_workers">
     <term><literal>parallel_workers</literal> (<type>integer</type>)
      <indexterm>
-- 
2.51.0

v9-0013-Add-support-for-oid8-TOAST-values.patchtext/x-diff; charset=us-asciiDownload
From d7952217044203376081a3da5b55c925855acbd3 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 17:06:10 +0900
Subject: [PATCH v9 13/15] Add support for oid8 TOAST values

This commit adds the possibility to define TOAST tables with oid8 as
value ID, based on the reloption toast_value_type.  All the external
TOAST pointers still rely on varatt_external and a single vartag, with
all the values inserted in the bigint TOAST tables fed from the existing
OID value generator.  This will be changed in an upcoming patch that
adds more vartag_external types and its associated structures, with the
code being able to use a different external TOAST pointer depending on
the attribute type of chunk_id in TOAST relations.

All the changes done here are mechanical, with all the TOAST code able
to do chunk ID lookups based on the two types now supported.

XXX: Catalog version bump required.
---
 src/include/catalog/pg_opclass.dat          |  3 +-
 src/include/utils/rel.h                     |  1 +
 src/backend/access/common/reloptions.c      |  1 +
 src/backend/access/common/toast_internals.c | 94 +++++++++++++++------
 src/backend/access/heap/heaptoast.c         | 20 ++++-
 src/backend/catalog/toasting.c              | 24 +++++-
 doc/src/sgml/ref/create_table.sgml          |  2 +
 doc/src/sgml/storage.sgml                   |  7 +-
 contrib/amcheck/verify_heapam.c             | 19 ++++-
 9 files changed, 131 insertions(+), 40 deletions(-)

diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index c0de88fabc49..b8f2bc2d69c4 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -179,7 +179,8 @@
   opcintype => 'xid8' },
 { opcmethod => 'hash', opcname => 'oid8_ops', opcfamily => 'hash/oid8_ops',
   opcintype => 'oid8' },
-{ opcmethod => 'btree', opcname => 'oid8_ops', opcfamily => 'btree/oid8_ops',
+{ oid => '8285', oid_symbol => 'OID8_BTREE_OPS_OID',
+  opcmethod => 'btree', opcname => 'oid8_ops', opcfamily => 'btree/oid8_ops',
   opcintype => 'oid8' },
 { opcmethod => 'hash', opcname => 'cid_ops', opcfamily => 'hash/cid_ops',
   opcintype => 'cid' },
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index fcd59099535f..c04f7c1f87fb 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -343,6 +343,7 @@ typedef enum StdRdOptToastValueType
 {
 	STDRD_OPTION_TOAST_VALUE_TYPE_INVALID = 0,
 	STDRD_OPTION_TOAST_VALUE_TYPE_OID,
+	STDRD_OPTION_TOAST_VALUE_TYPE_OID8,
 } StdRdOptToastValueType;
 
 typedef struct StdRdOptions
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index cafa72ae1c11..808c94a82102 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -530,6 +530,7 @@ static relopt_enum_elt_def StdRdOptToastValueTypes[] =
 {
 	/* no value for INVALID */
 	{"oid", STDRD_OPTION_TOAST_VALUE_TYPE_OID},
+	{"oid8", STDRD_OPTION_TOAST_VALUE_TYPE_OID8},
 	{(const char *) NULL}		/* list terminator */
 };
 
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index f614a49e1815..5279f56f475a 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -26,6 +26,7 @@
 #include "utils/fmgroids.h"
 #include "utils/rel.h"
 #include "utils/snapmgr.h"
+#include "utils/lsyscache.h"
 
 static bool toastrel_valueid_exists(Relation toastrel, Oid8 valueid);
 static bool toastid_valueid_exists(Oid toastrelid, Oid8 valueid);
@@ -134,8 +135,10 @@ toast_save_datum(Relation rel, Datum value,
 	int			validIndex;
 	const toast_external_info *info;
 	uint8		tag = VARTAG_INDIRECT;	/* init value does not matter */
+	Oid			toast_typid = get_atttype(rel->rd_rel->reltoastrelid, 1);
 
 	Assert(!VARATT_IS_EXTERNAL(dval));
+	Assert(OidIsValid(toast_typid));
 
 	/*
 	 * Open the toast relation and its indexes.  We can use the index to check
@@ -216,24 +219,32 @@ toast_save_datum(Relation rel, Datum value,
 		toast_pointer.toastrelid = RelationGetRelid(toastrel);
 
 	/*
-	 * Choose an OID to use as the value ID for this toast value.
+	 * Choose a new value to use as the value ID for this toast value, be it
+	 * for OID or int8-based TOAST relations.
 	 *
-	 * Normally we just choose an unused OID within the toast table.  But
+	 * Normally we just choose an unused value within the toast table.  But
 	 * during table-rewriting operations where we are preserving an existing
-	 * toast table OID, we want to preserve toast value OIDs too.  So, if
+	 * toast table OID, we want to preserve toast value IDs too.  So, if
 	 * rd_toastoid is set and we had a prior external value from that same
 	 * toast table, re-use its value ID.  If we didn't have a prior external
 	 * value (which is a corner case, but possible if the table's attstorage
 	 * options have been changed), we have to pick a value ID that doesn't
-	 * conflict with either new or existing toast value OIDs.
+	 * conflict with either new or existing toast value IDs.  If the TOAST
+	 * table uses 8-byte value IDs, we should not really care much about
+	 * that.
 	 */
 	if (!OidIsValid(rel->rd_toastoid))
 	{
 		/* normal case: just choose an unused OID */
-		toast_pointer.valueid =
-			GetNewOidWithIndex(toastrel,
-							   RelationGetRelid(toastidxs[validIndex]),
-							   (AttrNumber) 1);
+		if (toast_typid == OIDOID)
+			toast_pointer.valueid =
+				GetNewOidWithIndex(toastrel,
+								   RelationGetRelid(toastidxs[validIndex]),
+								   (AttrNumber) 1);
+		else if (toast_typid == OID8OID)
+			toast_pointer.valueid = GetNewObjectId8();
+		else
+			Assert(false);
 	}
 	else
 	{
@@ -279,17 +290,22 @@ toast_save_datum(Relation rel, Datum value,
 		if (toast_pointer.valueid == InvalidOid8)
 		{
 			/*
-			 * new value; must choose an OID that doesn't conflict in either
-			 * old or new toast table
+			 * new value; must choose a value that doesn't conflict in either
+			 * old or new toast table.
 			 */
-			do
+			if (toast_typid == OIDOID)
 			{
-				toast_pointer.valueid =
-					GetNewOidWithIndex(toastrel,
-									   RelationGetRelid(toastidxs[validIndex]),
-									   (AttrNumber) 1);
-			} while (toastid_valueid_exists(rel->rd_toastoid,
-											toast_pointer.valueid));
+				do
+				{
+					toast_pointer.valueid =
+						GetNewOidWithIndex(toastrel,
+										   RelationGetRelid(toastidxs[validIndex]),
+										   (AttrNumber) 1);
+				} while (toastid_valueid_exists(rel->rd_toastoid,
+												toast_pointer.valueid));
+			}
+			else if (toast_typid == OID8OID)
+				toast_pointer.valueid = GetNewObjectId8();
 		}
 	}
 
@@ -327,7 +343,10 @@ toast_save_datum(Relation rel, Datum value,
 		/*
 		 * Build a tuple and store it
 		 */
-		t_values[0] = ObjectIdGetDatum(toast_pointer.valueid);
+		if (toast_typid == OIDOID)
+			t_values[0] = ObjectIdGetDatum(toast_pointer.valueid);
+		else if (toast_typid == OID8OID)
+			t_values[0] = ObjectId8GetDatum(toast_pointer.valueid);
 		t_values[1] = Int32GetDatum(chunk_seq++);
 		SET_VARSIZE(&chunk_data, chunk_size + VARHDRSZ);
 		memcpy(VARDATA(&chunk_data), data_p, chunk_size);
@@ -406,6 +425,7 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	HeapTuple	toasttup;
 	int			num_indexes;
 	int			validIndex;
+	Oid			toast_typid;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		return;
@@ -417,6 +437,8 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	 * Open the toast relation and its indexes
 	 */
 	toastrel = table_open(toast_pointer.toastrelid, RowExclusiveLock);
+	toast_typid = TupleDescAttr(toastrel->rd_att, 0)->atttypid;
+	Assert(toast_typid == OIDOID || toast_typid == OID8OID);
 
 	/* Fetch valid relation used for process */
 	validIndex = toast_open_indexes(toastrel,
@@ -427,10 +449,18 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	/*
 	 * Setup a scan key to find chunks with matching va_valueid
 	 */
-	ScanKeyInit(&toastkey,
-				(AttrNumber) 1,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(toast_pointer.valueid));
+	if (toast_typid == OIDOID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(toast_pointer.valueid));
+	else if (toast_typid == OID8OID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OID8EQ,
+					ObjectId8GetDatum(toast_pointer.valueid));
+	else
+		Assert(false);
 
 	/*
 	 * Find all the chunks.  (We don't actually care whether we see them in
@@ -477,6 +507,7 @@ toastrel_valueid_exists(Relation toastrel, Oid8 valueid)
 	int			num_indexes;
 	int			validIndex;
 	Relation   *toastidxs;
+	Oid			toast_typid;
 
 	/* Fetch a valid index relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -484,13 +515,24 @@ toastrel_valueid_exists(Relation toastrel, Oid8 valueid)
 									&toastidxs,
 									&num_indexes);
 
+	toast_typid = TupleDescAttr(toastrel->rd_att, 0)->atttypid;
+	Assert(toast_typid == OIDOID || toast_typid == OID8OID);
+
 	/*
 	 * Setup a scan key to find chunks with matching va_valueid
 	 */
-	ScanKeyInit(&toastkey,
-				(AttrNumber) 1,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(valueid));
+	if (toast_typid == OIDOID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(valueid));
+	else if (toast_typid == OID8OID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OID8EQ,
+					ObjectId8GetDatum(valueid));
+	else
+		Assert(false);
 
 	/*
 	 * Is there any such chunk?
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index b3cd0ec0dbf4..da4d9594b243 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -654,6 +654,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 	int32		max_chunk_size;
 	const toast_external_info *info;
 	uint8		tag = VARTAG_INDIRECT;	/* init value does not matter */
+	Oid			toast_typid;
 
 	/* Look for the valid index of toast relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -667,16 +668,27 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 
 	max_chunk_size = info->maximum_chunk_size;
 
+	toast_typid = TupleDescAttr(toastrel->rd_att, 0)->atttypid;
+	Assert(toast_typid == OIDOID || toast_typid == OID8OID);
+
 	totalchunks = ((attrsize - 1) / max_chunk_size) + 1;
 	startchunk = sliceoffset / max_chunk_size;
 	endchunk = (sliceoffset + slicelength - 1) / max_chunk_size;
 	Assert(endchunk <= totalchunks);
 
 	/* Set up a scan key to fetch from the index. */
-	ScanKeyInit(&toastkey[0],
-				(AttrNumber) 1,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(valueid));
+	if (toast_typid == OIDOID)
+		ScanKeyInit(&toastkey[0],
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(valueid));
+	else if (toast_typid == OID8OID)
+		ScanKeyInit(&toastkey[0],
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OID8EQ,
+					ObjectId8GetDatum(valueid));
+	else
+		Assert(false);
 
 	/*
 	 * No additional condition if fetching all chunks. Otherwise, use an
diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c
index 545983b5be9d..2288311b22a4 100644
--- a/src/backend/catalog/toasting.c
+++ b/src/backend/catalog/toasting.c
@@ -31,6 +31,7 @@
 #include "nodes/makefuncs.h"
 #include "utils/fmgroids.h"
 #include "utils/rel.h"
+#include "utils/lsyscache.h"
 #include "utils/syscache.h"
 
 static void CheckAndCreateToastTable(Oid relOid, Datum reloptions,
@@ -167,6 +168,8 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 		value_type = RelationGetToastValueType(rel, STDRD_OPTION_TOAST_VALUE_TYPE_OID);
 		if (value_type == STDRD_OPTION_TOAST_VALUE_TYPE_OID)
 			toast_chunkid_typid = OIDOID;
+		else if (value_type == STDRD_OPTION_TOAST_VALUE_TYPE_OID8)
+			toast_chunkid_typid = OID8OID;
 	}
 	else
 	{
@@ -199,7 +202,8 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("toast chunk_id type not set while in binary upgrade mode")));
-		if (binary_upgrade_next_toast_chunk_id_typoid != OIDOID)
+		if (binary_upgrade_next_toast_chunk_id_typoid != OIDOID &&
+			binary_upgrade_next_toast_chunk_id_typoid != OID8OID)
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("cannot support toast chunk_id type %u in binary upgrade mode",
@@ -224,6 +228,19 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	snprintf(toast_idxname, sizeof(toast_idxname),
 			 "pg_toast_%u_index", relOid);
 
+	/*
+	 * Special case here.  If OIDOldToast is defined, we need to rely on the
+	 * existing table for the job because we do not want to create an
+	 * inconsistent relation that would conflict with the parent and break
+	 * the world.
+	 */
+	if (OidIsValid(OIDOldToast))
+	{
+		toast_chunkid_typid = get_atttype(OIDOldToast, 1);
+		if (!OidIsValid(toast_chunkid_typid))
+			elog(ERROR, "cache lookup failed for relation %u", OIDOldToast);
+	}
+
 	/* this is pretty painful...  need a tuple descriptor */
 	tupdesc = CreateTemplateTupleDesc(3);
 	TupleDescInitEntry(tupdesc, (AttrNumber) 1,
@@ -336,7 +353,10 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	collationIds[0] = InvalidOid;
 	collationIds[1] = InvalidOid;
 
-	opclassIds[0] = OID_BTREE_OPS_OID;
+	if (toast_chunkid_typid == OIDOID)
+		opclassIds[0] = OID_BTREE_OPS_OID;
+	else if (toast_chunkid_typid == OID8OID)
+		opclassIds[0] = OID8_BTREE_OPS_OID;
 	opclassIds[1] = INT4_BTREE_OPS_OID;
 
 	coloptions[0] = 0;
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 200dfc81bf8f..cf64387cc86b 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1645,6 +1645,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       relation for this table.
       By default this parameter is <literal>oid</literal>, to assign
       <type>oid</type> as attribute type to <literal>chunk_id</literal>.
+      This parameter can be set to <type>oid8</type> to use <type>oid8</type>
+      as attribute type for <literal>chunk_id</literal>.
       This parameter cannot be set for TOAST tables.
      </para>
     </listitem>
diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml
index 67600fd974d7..afddf663fec5 100644
--- a/doc/src/sgml/storage.sgml
+++ b/doc/src/sgml/storage.sgml
@@ -421,14 +421,15 @@ most <symbol>TOAST_OID_MAX_CHUNK_SIZE</symbol> bytes (by default this value is c
 so that four chunk rows will fit on a page, making it about 2000 bytes).
 Each chunk is stored as a separate row in the <acronym>TOAST</acronym> table
 belonging to the owning table.  Every
-<acronym>TOAST</acronym> table has the columns <structfield>chunk_id</structfield> (an OID
-identifying the particular <acronym>TOAST</acronym>ed value),
+<acronym>TOAST</acronym> table has the columns
+<structfield>chunk_id</structfield> (an OID or an 8-byte integer identifying
+the particular <acronym>TOAST</acronym>ed value),
 <structfield>chunk_seq</structfield> (a sequence number for the chunk within its value),
 and <structfield>chunk_data</structfield> (the actual data of the chunk).  A unique index
 on <structfield>chunk_id</structfield> and <structfield>chunk_seq</structfield> provides fast
 retrieval of the values.  A pointer datum representing an out-of-line on-disk
 <acronym>TOAST</acronym>ed value therefore needs to store the OID of the
-<acronym>TOAST</acronym> table in which to look and the OID of the specific value
+<acronym>TOAST</acronym> table in which to look and the specific value
 (its <structfield>chunk_id</structfield>).  For convenience, pointer datums also store the
 logical datum size (original uncompressed data length), physical stored size
 (different if compression was applied), and the compression method used, if
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 47f662f8b08a..80de3545e73a 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1880,6 +1880,9 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	int32		last_chunk_seq;
 	Oid8		toast_valueid;
 	int32		max_chunk_size;
+	Oid			toast_typid;
+
+	toast_typid = TupleDescAttr(ctx->toast_rel->rd_att, 0)->atttypid;
 
 	extsize = ta->toast_pointer.extsize;
 
@@ -1889,10 +1892,18 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	/*
 	 * Setup a scan key to find chunks in toast table with matching va_valueid
 	 */
-	ScanKeyInit(&toastkey,
-				(AttrNumber) 1,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(ta->toast_pointer.valueid));
+	if (toast_typid == OIDOID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(ta->toast_pointer.valueid));
+	else if (toast_typid == OID8OID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OID8EQ,
+					ObjectId8GetDatum(ta->toast_pointer.valueid));
+	else
+		Assert(false);
 
 	/*
 	 * Check if any chunks for this toasted object exist in the toast table,
-- 
2.51.0

v9-0014-Add-tests-for-TOAST-relations-with-bigint-as-valu.patchtext/x-diff; charset=us-asciiDownload
From 77541f961f12318a6f365efb2b93f974541cdc71 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 13:43:19 +0900
Subject: [PATCH v9 14/15] Add tests for TOAST relations with bigint as value
 type

This adds coverage for relations created with default_toast_type =
'int8', for external TOAST pointers both compressed and uncompressed.
---
 src/test/regress/expected/strings.out     | 231 ++++++++++++++++++----
 src/test/regress/expected/type_sanity.out |   6 +-
 src/test/regress/sql/strings.sql          | 134 +++++++++----
 src/test/regress/sql/type_sanity.sql      |   6 +-
 4 files changed, 296 insertions(+), 81 deletions(-)

diff --git a/src/test/regress/expected/strings.out b/src/test/regress/expected/strings.out
index 727304f60e74..ed1921b32280 100644
--- a/src/test/regress/expected/strings.out
+++ b/src/test/regress/expected/strings.out
@@ -2012,21 +2012,37 @@ SELECT text 'text' || varchar ' and varchar' AS "Concat text to varchar";
 (1 row)
 
 --
--- test substr with toasted text values
+-- Test substr with toasted text values, for all types of TOAST relations
+-- supported.
 --
-CREATE TABLE toasttest(f1 text);
-insert into toasttest values(repeat('1234567890',10000));
-insert into toasttest values(repeat('1234567890',10000));
+CREATE TABLE toasttest_oid(f1 text) with (toast_value_type = 'oid');
+CREATE TABLE toasttest_oid8(f1 text) with (toast_value_type = 'oid8');
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid8 values(repeat('1234567890',10000));
+insert into toasttest_oid8 values(repeat('1234567890',10000));
 --
 -- Ensure that some values are uncompressed, to test the faster substring
 -- operation used in that case
 --
-alter table toasttest alter column f1 set storage external;
-insert into toasttest values(repeat('1234567890',10000));
-insert into toasttest values(repeat('1234567890',10000));
+alter table toasttest_oid alter column f1 set storage external;
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+alter table toasttest_oid8 alter column f1 set storage external;
+insert into toasttest_oid8 values(repeat('1234567890',10000));
+insert into toasttest_oid8 values(repeat('1234567890',10000));
 -- If the starting position is zero or less, then return from the start of the string
 -- adjusting the length to be consistent with the "negative start" per SQL.
-SELECT substr(f1, -1, 5) from toasttest;
+SELECT substr(f1, -1, 5) from toasttest_oid;
+ substr 
+--------
+ 123
+ 123
+ 123
+ 123
+(4 rows)
+
+SELECT substr(f1, -1, 5) from toasttest_oid8;
  substr 
 --------
  123
@@ -2036,11 +2052,22 @@ SELECT substr(f1, -1, 5) from toasttest;
 (4 rows)
 
 -- If the length is less than zero, an ERROR is thrown.
-SELECT substr(f1, 5, -1) from toasttest;
+SELECT substr(f1, 5, -1) from toasttest_oid;
+ERROR:  negative substring length not allowed
+SELECT substr(f1, 5, -1) from toasttest_oid8;
 ERROR:  negative substring length not allowed
 -- If no third argument (length) is provided, the length to the end of the
 -- string is assumed.
-SELECT substr(f1, 99995) from toasttest;
+SELECT substr(f1, 99995) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995) from toasttest_oid8;
  substr 
 --------
  567890
@@ -2051,7 +2078,7 @@ SELECT substr(f1, 99995) from toasttest;
 
 -- If start plus length is > string length, the result is truncated to
 -- string length
-SELECT substr(f1, 99995, 10) from toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
  substr 
 --------
  567890
@@ -2060,50 +2087,105 @@ SELECT substr(f1, 99995, 10) from toasttest;
  567890
 (4 rows)
 
-TRUNCATE TABLE toasttest;
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
+SELECT substr(f1, 99995, 10) from toasttest_oid8;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+-- TRUNCATE cases for TOAST relations with OID values.
+TRUNCATE TABLE toasttest_oid;
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
 -- expect >0 blocks
 SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
-  FROM pg_class where relname = 'toasttest';
+  FROM pg_class where relname = 'toasttest_oid';
  is_empty 
 ----------
  f
 (1 row)
 
-TRUNCATE TABLE toasttest;
-ALTER TABLE toasttest set (toast_tuple_target = 4080);
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
+TRUNCATE TABLE toasttest_oid;
+ALTER TABLE toasttest_oid set (toast_tuple_target = 4080);
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
 -- expect 0 blocks
 SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
-  FROM pg_class where relname = 'toasttest';
+  FROM pg_class where relname = 'toasttest_oid';
  is_empty 
 ----------
  t
 (1 row)
 
-DROP TABLE toasttest;
+DROP TABLE toasttest_oid;
+-- TRUNCATE cases for TOAST relation with int8 values.
+TRUNCATE TABLE toasttest_oid8;
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+-- expect >0 blocks
+SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
+  FROM pg_class where relname = 'toasttest_oid8';
+ is_empty 
+----------
+ f
+(1 row)
+
+TRUNCATE TABLE toasttest_oid8;
+ALTER TABLE toasttest_oid8 set (toast_tuple_target = 4080);
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+-- expect 0 blocks
+SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
+  FROM pg_class where relname = 'toasttest_oid8';
+ is_empty 
+----------
+ t
+(1 row)
+
+DROP TABLE toasttest_oid8;
 --
--- test substr with toasted bytea values
+-- test substr with toasted bytea values, for all types of TOAST relations
+-- supported. Do not drop these two relations, for pg_upgrade.
 --
-CREATE TABLE toasttest(f1 bytea);
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
+CREATE TABLE toasttest_oid(f1 bytea) WITH (toast_value_type = 'oid');
+CREATE TABLE toasttest_oid8(f1 bytea) WITH (toast_value_type = 'oid8');
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
 --
 -- Ensure that some values are uncompressed, to test the faster substring
 -- operation used in that case
 --
-alter table toasttest alter column f1 set storage external;
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
+alter table toasttest_oid alter column f1 set storage external;
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+alter table toasttest_oid8 alter column f1 set storage external;
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
 -- If the starting position is zero or less, then return from the start of the string
 -- adjusting the length to be consistent with the "negative start" per SQL.
-SELECT substr(f1, -1, 5) from toasttest;
+SELECT substr(f1, -1, 5) from toasttest_oid;
+ substr 
+--------
+ 123
+ 123
+ 123
+ 123
+(4 rows)
+
+SELECT substr(f1, -1, 5) from toasttest_oid8;
  substr 
 --------
  123
@@ -2113,11 +2195,22 @@ SELECT substr(f1, -1, 5) from toasttest;
 (4 rows)
 
 -- If the length is less than zero, an ERROR is thrown.
-SELECT substr(f1, 5, -1) from toasttest;
+SELECT substr(f1, 5, -1) from toasttest_oid;
+ERROR:  negative substring length not allowed
+SELECT substr(f1, 5, -1) from toasttest_oid8;
 ERROR:  negative substring length not allowed
 -- If no third argument (length) is provided, the length to the end of the
 -- string is assumed.
-SELECT substr(f1, 99995) from toasttest;
+SELECT substr(f1, 99995) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995) from toasttest_oid8;
  substr 
 --------
  567890
@@ -2128,7 +2221,72 @@ SELECT substr(f1, 99995) from toasttest;
 
 -- If start plus length is > string length, the result is truncated to
 -- string length
-SELECT substr(f1, 99995, 10) from toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995, 10) from toasttest_oid8;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+-- A relation rewrite leaves the TOAST value attributes unchanged.
+VACUUM FULL toasttest_oid;
+VACUUM FULL toasttest_oid8;
+SELECT c1.relname, a.atttypid::regtype
+  FROM pg_attribute AS a,
+       pg_class AS c1,
+       pg_class AS c2
+  WHERE
+       c1.relname IN ('toasttest_oid', 'toasttest_oid8') AND
+       c1.reltoastrelid = c2.oid AND
+       a.attrelid = c2.oid AND
+       a.attname = 'chunk_id'
+  ORDER BY c1.relname COLLATE "C";
+    relname     | atttypid 
+----------------+----------
+ toasttest_oid  | oid
+ toasttest_oid8 | oid8
+(2 rows)
+
+-- Check that data slices are still accessible.
+SELECT substr(f1, 99995) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995) from toasttest_oid8;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995, 10) from toasttest_oid8;
  substr 
 --------
  567890
@@ -2137,7 +2295,6 @@ SELECT substr(f1, 99995, 10) from toasttest;
  567890
 (4 rows)
 
-DROP TABLE toasttest;
 -- test internally compressing datums
 -- this tests compressing a datum to a very small size which exercises a
 -- corner case in packed-varlena handling: even though small, the compressed
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 9ddcacec6bf4..88faa57772c3 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -578,15 +578,15 @@ WHERE c1.relnatts != (SELECT count(*) FROM pg_attribute AS a1
 (0 rows)
 
 -- Cross-check against pg_type entry
--- NOTE: we allow attstorage to be 'plain' even when typstorage is not;
--- this is mainly for toast tables.
+-- NOTE: we allow attstorage to be 'plain' or 'external' even when typstorage
+-- is not; this is mainly for toast tables.
 SELECT a1.attrelid, a1.attname, t1.oid, t1.typname
 FROM pg_attribute AS a1, pg_type AS t1
 WHERE a1.atttypid = t1.oid AND
     (a1.attlen != t1.typlen OR
      a1.attalign != t1.typalign OR
      a1.attbyval != t1.typbyval OR
-     (a1.attstorage != t1.typstorage AND a1.attstorage != 'p'));
+     (a1.attstorage != t1.typstorage AND a1.attstorage NOT IN ('e', 'p')));
  attrelid | attname | oid | typname 
 ----------+---------+-----+---------
 (0 rows)
diff --git a/src/test/regress/sql/strings.sql b/src/test/regress/sql/strings.sql
index 88aa4c2983ba..5c97f5e72eb5 100644
--- a/src/test/regress/sql/strings.sql
+++ b/src/test/regress/sql/strings.sql
@@ -572,89 +572,147 @@ SELECT text 'text' || char(20) ' and characters' AS "Concat text to char";
 SELECT text 'text' || varchar ' and varchar' AS "Concat text to varchar";
 
 --
--- test substr with toasted text values
+-- Test substr with toasted text values, for all types of TOAST relations
+-- supported.
 --
-CREATE TABLE toasttest(f1 text);
+CREATE TABLE toasttest_oid(f1 text) with (toast_value_type = 'oid');
+CREATE TABLE toasttest_oid8(f1 text) with (toast_value_type = 'oid8');
 
-insert into toasttest values(repeat('1234567890',10000));
-insert into toasttest values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid8 values(repeat('1234567890',10000));
+insert into toasttest_oid8 values(repeat('1234567890',10000));
 
 --
 -- Ensure that some values are uncompressed, to test the faster substring
 -- operation used in that case
 --
-alter table toasttest alter column f1 set storage external;
-insert into toasttest values(repeat('1234567890',10000));
-insert into toasttest values(repeat('1234567890',10000));
+alter table toasttest_oid alter column f1 set storage external;
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+alter table toasttest_oid8 alter column f1 set storage external;
+insert into toasttest_oid8 values(repeat('1234567890',10000));
+insert into toasttest_oid8 values(repeat('1234567890',10000));
 
 -- If the starting position is zero or less, then return from the start of the string
 -- adjusting the length to be consistent with the "negative start" per SQL.
-SELECT substr(f1, -1, 5) from toasttest;
+SELECT substr(f1, -1, 5) from toasttest_oid;
+SELECT substr(f1, -1, 5) from toasttest_oid8;
 
 -- If the length is less than zero, an ERROR is thrown.
-SELECT substr(f1, 5, -1) from toasttest;
+SELECT substr(f1, 5, -1) from toasttest_oid;
+SELECT substr(f1, 5, -1) from toasttest_oid8;
 
 -- If no third argument (length) is provided, the length to the end of the
 -- string is assumed.
-SELECT substr(f1, 99995) from toasttest;
+SELECT substr(f1, 99995) from toasttest_oid;
+SELECT substr(f1, 99995) from toasttest_oid8;
 
 -- If start plus length is > string length, the result is truncated to
 -- string length
-SELECT substr(f1, 99995, 10) from toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+SELECT substr(f1, 99995, 10) from toasttest_oid8;
 
-TRUNCATE TABLE toasttest;
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
+-- TRUNCATE cases for TOAST relations with OID values.
+TRUNCATE TABLE toasttest_oid;
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
 -- expect >0 blocks
 SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
-  FROM pg_class where relname = 'toasttest';
-
-TRUNCATE TABLE toasttest;
-ALTER TABLE toasttest set (toast_tuple_target = 4080);
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
+  FROM pg_class where relname = 'toasttest_oid';
+TRUNCATE TABLE toasttest_oid;
+ALTER TABLE toasttest_oid set (toast_tuple_target = 4080);
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
 -- expect 0 blocks
 SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
-  FROM pg_class where relname = 'toasttest';
+  FROM pg_class where relname = 'toasttest_oid';
+DROP TABLE toasttest_oid;
 
-DROP TABLE toasttest;
+-- TRUNCATE cases for TOAST relation with int8 values.
+TRUNCATE TABLE toasttest_oid8;
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+-- expect >0 blocks
+SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
+  FROM pg_class where relname = 'toasttest_oid8';
+TRUNCATE TABLE toasttest_oid8;
+ALTER TABLE toasttest_oid8 set (toast_tuple_target = 4080);
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+-- expect 0 blocks
+SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
+  FROM pg_class where relname = 'toasttest_oid8';
+DROP TABLE toasttest_oid8;
 
 --
--- test substr with toasted bytea values
+-- test substr with toasted bytea values, for all types of TOAST relations
+-- supported. Do not drop these two relations, for pg_upgrade.
 --
-CREATE TABLE toasttest(f1 bytea);
+CREATE TABLE toasttest_oid(f1 bytea) WITH (toast_value_type = 'oid');
+CREATE TABLE toasttest_oid8(f1 bytea) WITH (toast_value_type = 'oid8');
 
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
 
 --
 -- Ensure that some values are uncompressed, to test the faster substring
 -- operation used in that case
 --
-alter table toasttest alter column f1 set storage external;
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
+alter table toasttest_oid alter column f1 set storage external;
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+alter table toasttest_oid8 alter column f1 set storage external;
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
 
 -- If the starting position is zero or less, then return from the start of the string
 -- adjusting the length to be consistent with the "negative start" per SQL.
-SELECT substr(f1, -1, 5) from toasttest;
+SELECT substr(f1, -1, 5) from toasttest_oid;
+SELECT substr(f1, -1, 5) from toasttest_oid8;
 
 -- If the length is less than zero, an ERROR is thrown.
-SELECT substr(f1, 5, -1) from toasttest;
+SELECT substr(f1, 5, -1) from toasttest_oid;
+SELECT substr(f1, 5, -1) from toasttest_oid8;
 
 -- If no third argument (length) is provided, the length to the end of the
 -- string is assumed.
-SELECT substr(f1, 99995) from toasttest;
+SELECT substr(f1, 99995) from toasttest_oid;
+SELECT substr(f1, 99995) from toasttest_oid8;
 
 -- If start plus length is > string length, the result is truncated to
 -- string length
-SELECT substr(f1, 99995, 10) from toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+SELECT substr(f1, 99995, 10) from toasttest_oid8;
 
-DROP TABLE toasttest;
+-- A relation rewrite leaves the TOAST value attributes unchanged.
+VACUUM FULL toasttest_oid;
+VACUUM FULL toasttest_oid8;
+SELECT c1.relname, a.atttypid::regtype
+  FROM pg_attribute AS a,
+       pg_class AS c1,
+       pg_class AS c2
+  WHERE
+       c1.relname IN ('toasttest_oid', 'toasttest_oid8') AND
+       c1.reltoastrelid = c2.oid AND
+       a.attrelid = c2.oid AND
+       a.attname = 'chunk_id'
+  ORDER BY c1.relname COLLATE "C";
+-- Check that data slices are still accessible.
+SELECT substr(f1, 99995) from toasttest_oid;
+SELECT substr(f1, 99995) from toasttest_oid8;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+SELECT substr(f1, 99995, 10) from toasttest_oid8;
 
 -- test internally compressing datums
 
diff --git a/src/test/regress/sql/type_sanity.sql b/src/test/regress/sql/type_sanity.sql
index c2496823d90e..a0d2e8bcf00b 100644
--- a/src/test/regress/sql/type_sanity.sql
+++ b/src/test/regress/sql/type_sanity.sql
@@ -420,8 +420,8 @@ WHERE c1.relnatts != (SELECT count(*) FROM pg_attribute AS a1
                       WHERE a1.attrelid = c1.oid AND a1.attnum > 0);
 
 -- Cross-check against pg_type entry
--- NOTE: we allow attstorage to be 'plain' even when typstorage is not;
--- this is mainly for toast tables.
+-- NOTE: we allow attstorage to be 'plain' or 'external' even when typstorage
+-- is not; this is mainly for toast tables.
 
 SELECT a1.attrelid, a1.attname, t1.oid, t1.typname
 FROM pg_attribute AS a1, pg_type AS t1
@@ -429,7 +429,7 @@ WHERE a1.atttypid = t1.oid AND
     (a1.attlen != t1.typlen OR
      a1.attalign != t1.typalign OR
      a1.attbyval != t1.typbyval OR
-     (a1.attstorage != t1.typstorage AND a1.attstorage != 'p'));
+     (a1.attstorage != t1.typstorage AND a1.attstorage NOT IN ('e', 'p')));
 
 -- Look for IsCatalogTextUniqueIndexOid() omissions.
 
-- 
2.51.0

v9-0015-Add-new-vartag_external-for-8-byte-TOAST-values.patchtext/x-diff; charset=us-asciiDownload
From ab4a39a70fba34762163208612809ee5246febd4 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 14:10:36 +0900
Subject: [PATCH v9 15/15] Add new vartag_external for 8-byte TOAST values

This is a new type of external TOAST pointer, able to be fed 8-byte
TOAST values.  It uses a dedicated vartag_external, which is used when
a TOAST table uses bigint for its chunk_id.

The relevant callbacks are added to toast_external.c.
---
 src/include/access/heaptoast.h                |   8 +-
 src/include/varatt.h                          |  34 +++-
 src/backend/access/common/toast_external.c    | 145 ++++++++++++++++--
 src/backend/access/heap/heaptoast.c           |   1 +
 .../replication/logical/reorderbuffer.c       |  10 +-
 doc/src/sgml/storage.sgml                     |   6 +-
 contrib/amcheck/verify_heapam.c               |   2 +-
 7 files changed, 189 insertions(+), 17 deletions(-)

diff --git a/src/include/access/heaptoast.h b/src/include/access/heaptoast.h
index 12c9702af689..0f65e076efe5 100644
--- a/src/include/access/heaptoast.h
+++ b/src/include/access/heaptoast.h
@@ -81,6 +81,12 @@
 
 #define EXTERN_TUPLE_MAX_SIZE	MaximumBytesPerTuple(EXTERN_TUPLES_PER_PAGE)
 
+#define TOAST_OID8_MAX_CHUNK_SIZE	\
+	(EXTERN_TUPLE_MAX_SIZE -							\
+	 MAXALIGN(SizeofHeapTupleHeader) -					\
+	 (sizeof(uint32) * 2) -								\
+	 sizeof(int32) -									\
+	 VARHDRSZ)
 #define TOAST_OID_MAX_CHUNK_SIZE	\
 	(EXTERN_TUPLE_MAX_SIZE -							\
 	 MAXALIGN(SizeofHeapTupleHeader) -					\
@@ -89,7 +95,7 @@
 	 VARHDRSZ)
 
 /* Maximum size of chunk possible */
-#define TOAST_MAX_CHUNK_SIZE	TOAST_OID_MAX_CHUNK_SIZE
+#define TOAST_MAX_CHUNK_SIZE	Max(TOAST_OID_MAX_CHUNK_SIZE, TOAST_OID8_MAX_CHUNK_SIZE)
 
 /* ----------
  * heap_toast_insert_or_update -
diff --git a/src/include/varatt.h b/src/include/varatt.h
index 035c0f95e5b6..de38d1cd1ce1 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -41,6 +41,27 @@ typedef struct varatt_external_oid
 	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
 }			varatt_external_oid;
 
+/*
+ * struct varatt_external_oid8 is a "larger" version of "TOAST pointer",
+ * that uses an 8-byte integer as value.
+ *
+ * This follows the same properties as varatt_external_oid, except that
+ * this is used in TOAST relations with oid8 as attribute for chunk_id.
+ */
+typedef struct varatt_external_oid8
+{
+	int32		va_rawsize;		/* Original data size (includes header) */
+	uint32		va_extinfo;		/* External saved size (without header) and
+								 * compression method */
+	/*
+	 * Unique ID of value within TOAST table, as two uint32 for alignment
+	 * and padding.
+	 */
+	uint32		va_valueid_lo;
+	uint32		va_valueid_hi;
+	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
+}			varatt_external_oid8;
+
 /*
  * These macros define the "saved size" portion of va_extinfo.  Its remaining
  * two high-order bits identify the compression method.
@@ -90,6 +111,7 @@ typedef enum vartag_external
 	VARTAG_INDIRECT = 1,
 	VARTAG_EXPANDED_RO = 2,
 	VARTAG_EXPANDED_RW = 3,
+	VARTAG_ONDISK_OID8 = 4,
 	VARTAG_ONDISK_OID = 18
 } vartag_external;
 
@@ -111,6 +133,8 @@ VARTAG_SIZE(vartag_external tag)
 		return sizeof(varatt_expanded);
 	else if (tag == VARTAG_ONDISK_OID)
 		return sizeof(varatt_external_oid);
+	else if (tag == VARTAG_ONDISK_OID8)
+		return sizeof(varatt_external_oid8);
 	else
 	{
 		Assert(false);
@@ -367,11 +391,19 @@ VARATT_IS_EXTERNAL_ONDISK_OID(const void *PTR)
 	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID;
 }
 
+/* Is varlena datum a pointer to on-disk toasted data with OID8 value? */
+static inline bool
+VARATT_IS_EXTERNAL_ONDISK_OID8(const void *PTR)
+{
+	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID8;
+}
+
 /* Is varlena datum a pointer to on-disk toasted data? */
 static inline bool
 VARATT_IS_EXTERNAL_ONDISK(const void *PTR)
 {
-	return VARATT_IS_EXTERNAL_ONDISK_OID(PTR);
+	return VARATT_IS_EXTERNAL_ONDISK_OID(PTR) ||
+		VARATT_IS_EXTERNAL_ONDISK_OID8(PTR);
 }
 
 /* Is varlena datum an indirect pointer? */
diff --git a/src/backend/access/common/toast_external.c b/src/backend/access/common/toast_external.c
index e2f0a9dc1c50..431258b2be96 100644
--- a/src/backend/access/common/toast_external.c
+++ b/src/backend/access/common/toast_external.c
@@ -18,8 +18,19 @@
 #include "postgres.h"
 
 #include "access/detoast.h"
+#include "access/genam.h"
 #include "access/heaptoast.h"
 #include "access/toast_external.h"
+#include "catalog/catalog.h"
+#include "miscadmin.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+
+
+/* Callbacks for VARTAG_ONDISK_OID8 */
+static void ondisk_oid8_to_external_data(struct varlena *attr,
+										 toast_external_data *data);
+static struct varlena *ondisk_oid8_create_external_data(toast_external_data data);
 
 /* Callbacks for VARTAG_ONDISK_OID */
 static void ondisk_oid_to_external_data(struct varlena *attr,
@@ -28,7 +39,7 @@ static struct varlena *ondisk_oid_create_external_data(toast_external_data data)
 
 /*
  * Fetch the possibly-unaligned contents of an on-disk external TOAST with
- * OID values into a local "varatt_external_oid" pointer.
+ * OID or OID8 values into a local "varatt_external_*" pointer.
  *
  * This should be just a memcpy, but some versions of gcc seem to produce
  * broken code that assumes the datum contents are aligned.  Introducing
@@ -45,9 +56,20 @@ varatt_external_oid_get_pointer(varatt_external_oid *toast_pointer,
 	memcpy(toast_pointer, VARDATA_EXTERNAL(attre), sizeof(varatt_external_oid));
 }
 
+static inline void
+varatt_external_oid8_get_pointer(varatt_external_oid8 *toast_pointer,
+								 struct varlena *attr)
+{
+	varattrib_1b_e *attre = (varattrib_1b_e *) attr;
+
+	Assert(VARATT_IS_EXTERNAL_ONDISK_OID8(attre));
+	Assert(VARSIZE_EXTERNAL(attre) == sizeof(varatt_external_oid8) + VARHDRSZ_EXTERNAL);
+	memcpy(toast_pointer, VARDATA_EXTERNAL(attre), sizeof(varatt_external_oid8));
+}
+
 /*
  * Decompressed size of an on-disk varlena; but note argument is a struct
- * varatt_external_oid.
+ * varatt_external_oid or varatt_external_oid8.
  */
 static inline Size
 varatt_external_oid_get_extsize(varatt_external_oid toast_pointer)
@@ -55,9 +77,15 @@ varatt_external_oid_get_extsize(varatt_external_oid toast_pointer)
 	return toast_pointer.va_extinfo & VARLENA_EXTSIZE_MASK;
 }
 
+static inline Size
+varatt_external_oid8_get_extsize(varatt_external_oid8 toast_pointer)
+{
+	return toast_pointer.va_extinfo & VARLENA_EXTSIZE_MASK;
+}
+
 /*
  * Compression method of an on-disk varlena; but note argument is a struct
- *  varatt_external_oid.
+ *  varatt_external_oid or varatt_external_oid8.
  */
 static inline uint32
 varatt_external_oid_get_compress_method(varatt_external_oid toast_pointer)
@@ -65,6 +93,12 @@ varatt_external_oid_get_compress_method(varatt_external_oid toast_pointer)
 	return toast_pointer.va_extinfo >> VARLENA_EXTSIZE_BITS;
 }
 
+static inline uint32
+varatt_external_oid8_get_compress_method(varatt_external_oid8 toast_pointer)
+{
+	return toast_pointer.va_extinfo >> VARLENA_EXTSIZE_BITS;
+}
+
 /*
  * Testing whether an externally-stored TOAST value is compressed now requires
  * comparing size stored in va_extinfo (the actual length of the external data)
@@ -79,6 +113,19 @@ varatt_external_oid_is_compressed(varatt_external_oid toast_pointer)
 		(Size) (toast_pointer.va_rawsize - VARHDRSZ);
 }
 
+static inline bool
+varatt_external_oid8_is_compressed(varatt_external_oid8 toast_pointer)
+{
+	return varatt_external_oid8_get_extsize(toast_pointer) <
+		(Size) (toast_pointer.va_rawsize - VARHDRSZ);
+}
+
+/*
+ * Size of an EXTERNAL datum that contains a standard TOAST pointer
+ * (oid8 value).
+ */
+#define TOAST_POINTER_OID8_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_oid8))
+
 /*
  * Size of an EXTERNAL datum that contains a standard TOAST pointer (OID
  * value).
@@ -99,6 +146,12 @@ varatt_external_oid_is_compressed(varatt_external_oid toast_pointer)
  * individual fields.
  */
 static const toast_external_info toast_external_infos[TOAST_EXTERNAL_INFO_SIZE] = {
+	[VARTAG_ONDISK_OID8] = {
+		.toast_pointer_size = TOAST_POINTER_OID8_SIZE,
+		.maximum_chunk_size = TOAST_OID_MAX_CHUNK_SIZE,
+		.to_external_data = ondisk_oid8_to_external_data,
+		.create_external_data = ondisk_oid8_create_external_data,
+	},
 	[VARTAG_ONDISK_OID] = {
 		.toast_pointer_size = TOAST_OID_POINTER_SIZE,
 		.maximum_chunk_size = TOAST_OID_MAX_CHUNK_SIZE,
@@ -155,22 +208,33 @@ toast_external_info_get_pointer_size(uint8 tag)
 uint8
 toast_external_assign_vartag(Oid toastrelid, Oid8 valueid)
 {
+	Oid		toast_typid;
+
 	/*
-	 * If dealing with a code path where a TOAST relation may not be assigned,
-	 * like heap_toast_insert_or_update(), just use the legacy
-	 * vartag_external.
+	 * If dealing with a code path where a TOAST relation may not be assigned
+	 * like heap_toast_insert_or_update(), just use the default with an OID
+	 * type.
+	 *
+	 * In bootstrap mode, we should not do any kind of syscache lookups,
+	 * so also rely on OID.
 	 */
-	if (!OidIsValid(toastrelid))
+	if (!OidIsValid(toastrelid) || IsBootstrapProcessingMode())
 		return VARTAG_ONDISK_OID;
 
 	/*
-	 * Currently there is only one type of vartag_external supported: 4-byte
-	 * value with OID for the chunk_id type.
+	 * Two types of vartag_external are currently supported: OID and OID8,
+	 * which depend on the type assigned to "chunk_id" for the TOAST table.
 	 *
-	 * Note: This routine will be extended to be able to use multiple
-	 * vartag_external within a single TOAST relation type, that may change
-	 * depending on the value used.
+	 * XXX: Should we assign from the start an OID vartag if dealing with
+	 * a TOAST relation with OID8 as value if the value assigned is less
+	 * than UINT_MAX?  This just takes the "safe" approach of assigning
+	 * the larger vartag in all cases, but this can be made cheaper
+	 * depending on the OID consumption.
 	 */
+	toast_typid = get_atttype(toastrelid, 1);
+	if (toast_typid == OID8OID)
+		return VARTAG_ONDISK_OID8;
+
 	return VARTAG_ONDISK_OID;
 }
 
@@ -179,6 +243,63 @@ toast_external_assign_vartag(Oid toastrelid, Oid8 valueid)
  * the in-memory representation toast_external_data used in the backend.
  */
 
+/* Callbacks for VARTAG_ONDISK_OID8 */
+static void
+ondisk_oid8_to_external_data(struct varlena *attr, toast_external_data *data)
+{
+	varatt_external_oid8	external;
+
+	varatt_external_oid8_get_pointer(&external, attr);
+	data->rawsize = external.va_rawsize;
+
+	/* External size and compression methods are stored in the same field */
+	if (varatt_external_oid8_is_compressed(external))
+	{
+		data->extsize = varatt_external_oid8_get_extsize(external);
+		data->compression_method = varatt_external_oid8_get_compress_method(external);
+	}
+	else
+	{
+		data->extsize = external.va_extinfo;
+		data->compression_method = TOAST_INVALID_COMPRESSION_ID;
+	}
+
+	data->valueid = (((uint64) external.va_valueid_hi) << 32) |
+		external.va_valueid_lo;
+	data->toastrelid = external.va_toastrelid;
+
+}
+
+static struct varlena *
+ondisk_oid8_create_external_data(toast_external_data data)
+{
+	struct varlena *result = NULL;
+	varatt_external_oid8 external;
+
+	external.va_rawsize = data.rawsize;
+
+	if (data.compression_method != TOAST_INVALID_COMPRESSION_ID)
+	{
+		/* Set size and compression method, in a single field. */
+		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(external,
+													 data.extsize,
+													 data.compression_method);
+	}
+	else
+		external.va_extinfo = data.extsize;
+
+	external.va_toastrelid = data.toastrelid;
+	external.va_valueid_hi = (((uint64) data.valueid) >> 32);
+	external.va_valueid_lo = (uint32) data.valueid;
+
+	result = (struct varlena *) palloc(TOAST_POINTER_OID8_SIZE);
+	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_OID8);
+	memcpy(VARDATA_EXTERNAL(result), &external, sizeof(external));
+
+	return result;
+}
+
+
 /* Callbacks for VARTAG_ONDISK_OID */
 
 /*
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index da4d9594b243..8434105ea790 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -32,6 +32,7 @@
 #include "access/toast_helper.h"
 #include "access/toast_internals.h"
 #include "utils/fmgroids.h"
+#include "utils/syscache.h"
 
 
 /* ----------
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index e64501c84de4..2668e924f224 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -5005,14 +5005,22 @@ ReorderBufferToastAppendChunk(ReorderBuffer *rb, ReorderBufferTXN *txn,
 	TupleDesc	desc = RelationGetDescr(relation);
 	Oid8		chunk_id;
 	int32		chunk_seq;
+	Oid			toast_typid;
 
 	if (txn->toast_hash == NULL)
 		ReorderBufferToastInitHash(rb, txn);
+	toast_typid = TupleDescAttr(desc, 0)->atttypid;
 
 	Assert(IsToastRelation(relation));
 
 	newtup = change->data.tp.newtuple;
-	chunk_id = DatumGetObjectId(fastgetattr(newtup, 1, desc, &isnull));
+	/* This depends on the type of TOAST value dealt with. */
+	if (toast_typid == OIDOID)
+		chunk_id = DatumGetObjectId(fastgetattr(newtup, 1, desc, &isnull));
+	else if (toast_typid == INT8OID)
+		chunk_id = DatumGetUInt64(fastgetattr(newtup, 1, desc, &isnull));
+	else
+		Assert(false);
 	Assert(!isnull);
 	chunk_seq = DatumGetInt32(fastgetattr(newtup, 2, desc, &isnull));
 	Assert(!isnull);
diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml
index afddf663fec5..dbec30d48b4a 100644
--- a/doc/src/sgml/storage.sgml
+++ b/doc/src/sgml/storage.sgml
@@ -417,7 +417,11 @@ described in more detail below.
 
 <para>
 Out-of-line values are divided (after compression if used) into chunks of at
-most <symbol>TOAST_OID_MAX_CHUNK_SIZE</symbol> bytes (by default this value is chosen
+most <symbol>TOAST_OID_MAX_CHUNK_SIZE</symbol> bytes if the
+<acronym>TOAST</acronym> relation uses the <literal>oid</literal> type for
+<literal>chunk_id</literal>, or <symbol>TOAST_OID8_MAX_CHUNK_SIZE</symbol>
+bytes if the <acronym>TOAST</acronym> relation uses the <literal>oid8</literal>
+type for <literal>chunk_id</literal> (by default these values are chosen
 so that four chunk rows will fit on a page, making it about 2000 bytes).
 Each chunk is stored as a separate row in the <acronym>TOAST</acronym> table
 belonging to the owning table.  Every
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 80de3545e73a..b95adcb9b57c 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1733,7 +1733,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	{
 		uint8		va_tag = VARTAG_EXTERNAL(tp + ctx->offset);
 
-		if (va_tag != VARTAG_ONDISK_OID)
+		if (va_tag != VARTAG_ONDISK_OID && va_tag != VARTAG_ONDISK_OID8)
 		{
 			report_corruption(ctx,
 							  psprintf("toasted attribute has unexpected TOAST tag %u",
-- 
2.51.0

#54Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#53)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

On Thu, Dec 18, 2025 at 02:40:24PM +0900, Michael Paquier wrote:

I am also looking at what it would take to implement what the brutal
approach I have mentioned upthread. This requires a bit more
reorganization than what I had in mind initially. By putting first in
the patch set some of the parts that are kind of relevant with the two
designs, things seem to be a bit leaner. I need to spend a few more
hours on that beforebeing sure, though..

I am still working on redesigning the patch set to avoid the pointer
redirection and the callbacks assigned to the vartags.

One thing that stands out for me is that I am going to need the oid8
data type anyway, to be store the 8-byte values in the catalogs, with
a C type to mark some of the TOAST apis with it across the board, so I
would like to apply that to move this part out of the way (0001 only)
and move the needle. If there are any objections to that, please feel
free.
--
Michael

#55Nikhil Kumar Veldanda
veldanda.nikhilkumar17@gmail.com
In reply to: Michael Paquier (#54)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

On Sun, Jan 4, 2026 at 2:23 PM Michael Paquier <michael@paquier.xyz> wrote:

One thing that stands out for me is that I am going to need the oid8
data type anyway, to be store the 8-byte values in the catalogs, with
a C type to mark some of the TOAST apis with it across the board, so I
would like to apply that to move this part out of the way (0001 only)
and move the needle. If there are any objections to that, please feel
free.

Hi Michael,

Two quick notes from my review:

Semantics: the regression test accepts '-1040' and wraps modulo 2^64.
Is that intentional? If yes, please document it; if not, oid8in()
should reject negative input and the test should expect an error.

Nits: oid8.c appears to include <ctype.h> / <limits.h> without using
them, and there’s an empty src/test/regress/expected/oid8.sql
(intentional?).
--
Nikhil Veldanda

#56Michael Paquier
michael@paquier.xyz
In reply to: Nikhil Kumar Veldanda (#55)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

On Sun, Jan 04, 2026 at 11:39:28PM -0800, Nikhil Kumar Veldanda wrote:

Semantics: the regression test accepts '-1040' and wraps modulo 2^64.
Is that intentional? If yes, please document it; if not, oid8in()
should reject negative input and the test should expect an error.

Yes, like for the oid type this is intentional.

Nits: oid8.c appears to include <ctype.h> / <limits.h> without using
them, and there’s an empty src/test/regress/expected/oid8.sql
(intentional?).

Indeed, I'll clean up that. Thanks.
--
Michael

#57zengman
zengman@halodbtech.com
In reply to: Michael Paquier (#56)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

Indeed, I'll clean up that. Thanks.

Hello Mr. Michael

I noticed a small detail in the latest submission that could be adjusted.
doc/src/sgml/datatype.sgml:
-    they can be cast to integer
+   They can be cast to integer

--
Regards,
Man Zeng
www.openhalo.org

#58Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#56)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

On Wed, Jan 07, 2026 at 08:09:35AM +0900, Michael Paquier wrote:

Indeed, I'll clean up that. Thanks.

So, I have spent a couple of extra hours on this part, and tweaked
a couple of things before applying the result:
- Removal of atooid8(). I did not use it in the patch set here, and
one could just use strtou64() instead.
- More tests, closing gaps for related to ordering, hash (hash_func,
HashAggregate), btree, compare functions, UINT64_MAX values.
- Some tweaks to the docs.

While the final commit has sticked with oid8 for the data type and
Oid8 for the C type, we still have a couple of months to tweak the
names if necessary. I would keep these as they are, but if there are
more voices for a Oid64, feel free of course. What's on HEAD now
works fine enough for me.
--
Michael

#59Michael Paquier
michael@paquier.xyz
In reply to: zengman (#57)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

On Wed, Jan 07, 2026 at 11:49:24AM +0800, zengman wrote:

I noticed a small detail in the latest submission that could be adjusted.
doc/src/sgml/datatype.sgml:
-    they can be cast to integer
+   They can be cast to integer

Thanks, fixed.
--
Michael

#60Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#58)
14 attachment(s)
Re: Support for 8-byte TOAST values (aka the TOAST infinite loop problem)

On Wed, Jan 07, 2026 at 01:43:26PM +0900, Michael Paquier wrote:

While the final commit has sticked with oid8 for the data type and
Oid8 for the C type, we still have a couple of months to tweak the
names if necessary. I would keep these as they are, but if there are
more voices for a Oid64, feel free of course. What's on HEAD now
works fine enough for me.

I am still looking at a different approach that does not use the
redirection. For now, please find a rebased version of the patch,
which is the rest minus the oid8 support now committed and some
conflicts resolved, in case someone has any comments.
--
Michael

Attachments:

v10-0003-Renames-around-varatt_external-varatt_external_o.patchtext/x-diff; charset=us-asciiDownload
From efe435208505a0a2cd785b567178a80967d010b8 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 13 Aug 2025 18:28:10 +0900
Subject: [PATCH v10 03/14] Renames around varatt_external->varatt_external_oid

This impacts a few things:
- VARTAG_ONDISK -> VARTAG_ONDISK_OID
- TOAST_POINTER_SIZE -> TOAST_OID_POINTER_SIZE
- TOAST_MAX_CHUNK_SIZE -> TOAST_OID_MAX_CHUNK_SIZE

The "struct" around varatt_external is cleaned up in most places, while
on it.

This rename is in preparation of a follow-up commit that aims at adding
support for multiple types of external on-disk TOAST pointers, where the
OID type is only one subset of them.
---
 src/include/access/detoast.h                  |  4 +--
 src/include/access/heaptoast.h                |  6 ++--
 src/include/varatt.h                          | 34 +++++++++++--------
 src/backend/access/common/detoast.c           | 10 +++---
 src/backend/access/common/toast_compression.c |  2 +-
 src/backend/access/common/toast_internals.c   | 14 ++++----
 src/backend/access/heap/heaptoast.c           |  2 +-
 src/backend/access/table/toast_helper.c       |  4 +--
 src/backend/access/transam/xlog.c             |  8 ++---
 .../replication/logical/reorderbuffer.c       |  2 +-
 src/backend/utils/adt/varlena.c               |  2 +-
 src/bin/pg_resetwal/pg_resetwal.c             |  2 +-
 doc/src/sgml/func/func-info.sgml              |  2 +-
 doc/src/sgml/storage.sgml                     |  2 +-
 contrib/amcheck/verify_heapam.c               | 10 +++---
 15 files changed, 54 insertions(+), 50 deletions(-)

diff --git a/src/include/access/detoast.h b/src/include/access/detoast.h
index 6db3a29191ee..f1399be7c4ea 100644
--- a/src/include/access/detoast.h
+++ b/src/include/access/detoast.h
@@ -14,7 +14,7 @@
 
 /*
  * Macro to fetch the possibly-unaligned contents of an EXTERNAL datum
- * into a local "struct varatt_external" toast pointer.  This should be
+ * into a local "varatt_external_oid" toast pointer.  This should be
  * just a memcpy, but some versions of gcc seem to produce broken code
  * that assumes the datum contents are aligned.  Introducing an explicit
  * intermediate "varattrib_1b_e *" variable seems to fix it.
@@ -28,7 +28,7 @@ do { \
 } while (0)
 
 /* Size of an EXTERNAL datum that contains a standard TOAST pointer */
-#define TOAST_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external))
+#define TOAST_OID_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_oid))
 
 /* Size of an EXTERNAL datum that contains an indirection pointer */
 #define INDIRECT_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_indirect))
diff --git a/src/include/access/heaptoast.h b/src/include/access/heaptoast.h
index 893be58687a1..df77aa9ce61d 100644
--- a/src/include/access/heaptoast.h
+++ b/src/include/access/heaptoast.h
@@ -69,19 +69,19 @@
 
 /*
  * When we store an oversize datum externally, we divide it into chunks
- * containing at most TOAST_MAX_CHUNK_SIZE data bytes.  This number *must*
+ * containing at most TOAST_OID_MAX_CHUNK_SIZE data bytes.  This number *must*
  * be small enough that the completed toast-table tuple (including the
  * ID and sequence fields and all overhead) will fit on a page.
  * The coding here sets the size on the theory that we want to fit
  * EXTERN_TUPLES_PER_PAGE tuples of maximum size onto a page.
  *
- * NB: Changing TOAST_MAX_CHUNK_SIZE requires an initdb.
+ * NB: Changing TOAST_OID_MAX_CHUNK_SIZE requires an initdb.
  */
 #define EXTERN_TUPLES_PER_PAGE	4	/* tweak only this */
 
 #define EXTERN_TUPLE_MAX_SIZE	MaximumBytesPerTuple(EXTERN_TUPLES_PER_PAGE)
 
-#define TOAST_MAX_CHUNK_SIZE	\
+#define TOAST_OID_MAX_CHUNK_SIZE	\
 	(EXTERN_TUPLE_MAX_SIZE -							\
 	 MAXALIGN(SizeofHeapTupleHeader) -					\
 	 sizeof(Oid) -										\
diff --git a/src/include/varatt.h b/src/include/varatt.h
index eccd3ca04d66..c13939ba8b4c 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -16,7 +16,7 @@
 #define VARATT_H
 
 /*
- * struct varatt_external is a traditional "TOAST pointer", that is, the
+ * varatt_external_oid is a traditional "TOAST pointer", that is, the
  * information needed to fetch a Datum stored out-of-line in a TOAST table.
  * The data is compressed if and only if the external size stored in
  * va_extinfo is less than va_rawsize - VARHDRSZ.
@@ -29,14 +29,14 @@
  * you can look at these fields!  (The reason we use memcmp is to avoid
  * having to do that just to detect equality of two TOAST pointers...)
  */
-typedef struct varatt_external
+typedef struct varatt_external_oid
 {
 	int32		va_rawsize;		/* Original data size (includes header) */
 	uint32		va_extinfo;		/* External saved size (without header) and
 								 * compression method */
 	Oid			va_valueid;		/* Unique ID of value within TOAST table */
 	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
-}			varatt_external;
+}			varatt_external_oid;
 
 /*
  * These macros define the "saved size" portion of va_extinfo.  Its remaining
@@ -51,7 +51,7 @@ typedef struct varatt_external
  * The creator of such a Datum is entirely responsible that the referenced
  * storage survives for as long as referencing pointer Datums can exist.
  *
- * Note that just as for struct varatt_external, this struct is stored
+ * Note that just as for varatt_external_oid, this struct is stored
  * unaligned within any containing tuple.
  */
 typedef struct varatt_indirect
@@ -66,7 +66,7 @@ typedef struct varatt_indirect
  * storage.  APIs for this, in particular the definition of struct
  * ExpandedObjectHeader, are in src/include/utils/expandeddatum.h.
  *
- * Note that just as for struct varatt_external, this struct is stored
+ * Note that just as for varatt_external_oid, this struct is stored
  * unaligned within any containing tuple.
  */
 typedef struct ExpandedObjectHeader ExpandedObjectHeader;
@@ -78,15 +78,16 @@ typedef struct varatt_expanded
 
 /*
  * Type tag for the various sorts of "TOAST pointer" datums.  The peculiar
- * value for VARTAG_ONDISK comes from a requirement for on-disk compatibility
- * with a previous notion that the tag field was the pointer datum's length.
+ * value for VARTAG_ONDISK_OID comes from a requirement for on-disk
+ * compatibility with a previous notion that the tag field was the pointer
+ * datum's length.
  */
 typedef enum vartag_external
 {
 	VARTAG_INDIRECT = 1,
 	VARTAG_EXPANDED_RO = 2,
 	VARTAG_EXPANDED_RW = 3,
-	VARTAG_ONDISK = 18
+	VARTAG_ONDISK_OID = 18
 } vartag_external;
 
 /* Is a TOAST pointer either type of expanded-object pointer? */
@@ -105,8 +106,8 @@ VARTAG_SIZE(vartag_external tag)
 		return sizeof(varatt_indirect);
 	else if (VARTAG_IS_EXPANDED(tag))
 		return sizeof(varatt_expanded);
-	else if (tag == VARTAG_ONDISK)
-		return sizeof(varatt_external);
+	else if (tag == VARTAG_ONDISK_OID)
+		return sizeof(varatt_external_oid);
 	else
 	{
 		Assert(false);
@@ -360,7 +361,7 @@ VARATT_IS_EXTERNAL(const void *PTR)
 static inline bool
 VARATT_IS_EXTERNAL_ONDISK(const void *PTR)
 {
-	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK;
+	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID;
 }
 
 /* Is varlena datum an indirect pointer? */
@@ -502,15 +503,18 @@ VARDATA_COMPRESSED_GET_COMPRESS_METHOD(const void *PTR)
 	return ((varattrib_4b *) PTR)->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS;
 }
 
-/* Same for external Datums; but note argument is a struct varatt_external */
+/*
+ * Same for external Datums; but note argument is a struct
+ * varatt_external_oid.
+ */
 static inline Size
-VARATT_EXTERNAL_GET_EXTSIZE(struct varatt_external toast_pointer)
+VARATT_EXTERNAL_GET_EXTSIZE(varatt_external_oid toast_pointer)
 {
 	return toast_pointer.va_extinfo & VARLENA_EXTSIZE_MASK;
 }
 
 static inline uint32
-VARATT_EXTERNAL_GET_COMPRESS_METHOD(struct varatt_external toast_pointer)
+VARATT_EXTERNAL_GET_COMPRESS_METHOD(varatt_external_oid toast_pointer)
 {
 	return toast_pointer.va_extinfo >> VARLENA_EXTSIZE_BITS;
 }
@@ -533,7 +537,7 @@ VARATT_EXTERNAL_GET_COMPRESS_METHOD(struct varatt_external toast_pointer)
  * actually saves space, so we expect either equality or less-than.
  */
 static inline bool
-VARATT_EXTERNAL_IS_COMPRESSED(struct varatt_external toast_pointer)
+VARATT_EXTERNAL_IS_COMPRESSED(varatt_external_oid toast_pointer)
 {
 	return VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) <
 		(Size) (toast_pointer.va_rawsize - VARHDRSZ);
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 7bef01bb5f35..6dd8b261bf9b 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -225,7 +225,7 @@ detoast_attr_slice(struct varlena *attr,
 
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
-		struct varatt_external toast_pointer;
+		varatt_external_oid toast_pointer;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 
@@ -344,7 +344,7 @@ toast_fetch_datum(struct varlena *attr)
 {
 	Relation	toastrel;
 	struct varlena *result;
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 	int32		attrsize;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
@@ -398,7 +398,7 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 {
 	Relation	toastrel;
 	struct varlena *result;
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 	int32		attrsize;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
@@ -550,7 +550,7 @@ toast_raw_datum_size(Datum value)
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
 		/* va_rawsize is the size of the original datum -- including header */
-		struct varatt_external toast_pointer;
+		varatt_external_oid toast_pointer;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 		result = toast_pointer.va_rawsize;
@@ -610,7 +610,7 @@ toast_datum_size(Datum value)
 		 * compressed or not.  We do not count the size of the toast pointer
 		 * ... should we?
 		 */
-		struct varatt_external toast_pointer;
+		varatt_external_oid toast_pointer;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 		result = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c
index 1336328cc0b5..0c5ed937cff8 100644
--- a/src/backend/access/common/toast_compression.c
+++ b/src/backend/access/common/toast_compression.c
@@ -262,7 +262,7 @@ toast_get_compression_id(struct varlena *attr)
 	 */
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
-		struct varatt_external toast_pointer;
+		varatt_external_oid toast_pointer;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index fd195f7d9c4f..2becbcc33338 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -124,7 +124,7 @@ toast_save_datum(Relation rel, Datum value,
 	TupleDesc	toasttupDesc;
 	CommandId	mycid = GetCurrentCommandId(true);
 	struct varlena *result;
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 	int32		chunk_seq = 0;
 	char	   *data_p;
 	int32		data_todo;
@@ -225,7 +225,7 @@ toast_save_datum(Relation rel, Datum value,
 		toast_pointer.va_valueid = InvalidOid;
 		if (oldexternal != NULL)
 		{
-			struct varatt_external old_toast_pointer;
+			varatt_external_oid old_toast_pointer;
 
 			Assert(VARATT_IS_EXTERNAL_ONDISK(oldexternal));
 			/* Must copy to access aligned fields */
@@ -289,7 +289,7 @@ toast_save_datum(Relation rel, Datum value,
 		{
 			alignas(int32) struct varlena hdr;
 			/* this is to make the union big enough for a chunk: */
-			char		data[TOAST_MAX_CHUNK_SIZE + VARHDRSZ];
+			char		data[TOAST_OID_MAX_CHUNK_SIZE + VARHDRSZ];
 		}			chunk_data;
 		int32		chunk_size;
 
@@ -298,7 +298,7 @@ toast_save_datum(Relation rel, Datum value,
 		/*
 		 * Calculate the size of this chunk
 		 */
-		chunk_size = Min(TOAST_MAX_CHUNK_SIZE, data_todo);
+		chunk_size = Min(TOAST_OID_MAX_CHUNK_SIZE, data_todo);
 
 		/*
 		 * Build a tuple and store it
@@ -359,8 +359,8 @@ toast_save_datum(Relation rel, Datum value,
 	/*
 	 * Create the TOAST pointer value that we'll return
 	 */
-	result = (struct varlena *) palloc(TOAST_POINTER_SIZE);
-	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK);
+	result = (struct varlena *) palloc(TOAST_OID_POINTER_SIZE);
+	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_OID);
 	memcpy(VARDATA_EXTERNAL(result), &toast_pointer, sizeof(toast_pointer));
 
 	return PointerGetDatum(result);
@@ -376,7 +376,7 @@ void
 toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 {
 	struct varlena *attr = (struct varlena *) DatumGetPointer(value);
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 	Relation	toastrel;
 	Relation   *toastidxs;
 	ScanKeyData toastkey;
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index 9a48b1fcc89d..8e914c61ecae 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -647,7 +647,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 									&toastidxs,
 									&num_indexes);
 
-	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
 
 	totalchunks = ((attrsize - 1) / max_chunk_size) + 1;
 	startchunk = sliceoffset / max_chunk_size;
diff --git a/src/backend/access/table/toast_helper.c b/src/backend/access/table/toast_helper.c
index d8a604a0b3e2..8b1c4f8237fd 100644
--- a/src/backend/access/table/toast_helper.c
+++ b/src/backend/access/table/toast_helper.c
@@ -171,7 +171,7 @@ toast_tuple_init(ToastTupleContext *ttc)
  * The column must have attstorage EXTERNAL or EXTENDED if check_main is
  * false, and must have attstorage MAIN if check_main is true.
  *
- * The column must have a minimum size of MAXALIGN(TOAST_POINTER_SIZE);
+ * The column must have a minimum size of MAXALIGN(TOAST_OID_POINTER_SIZE);
  * if not, no benefit is to be expected by compressing it.
  *
  * The return value is the index of the biggest suitable column, or
@@ -184,7 +184,7 @@ toast_tuple_find_biggest_attribute(ToastTupleContext *ttc,
 	TupleDesc	tupleDesc = ttc->ttc_rel->rd_att;
 	int			numAttrs = tupleDesc->natts;
 	int			biggest_attno = -1;
-	int32		biggest_size = MAXALIGN(TOAST_POINTER_SIZE);
+	int32		biggest_size = MAXALIGN(TOAST_OID_POINTER_SIZE);
 	int32		skip_colflags = TOASTCOL_IGNORE;
 	int			i;
 
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 81dc86847c01..d623646a6edf 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4297,7 +4297,7 @@ WriteControlFile(void)
 	ControlFile->nameDataLen = NAMEDATALEN;
 	ControlFile->indexMaxKeys = INDEX_MAX_KEYS;
 
-	ControlFile->toast_max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	ControlFile->toast_max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
 	ControlFile->loblksize = LOBLKSIZE;
 
 	ControlFile->float8ByVal = true;	/* vestigial */
@@ -4550,15 +4550,15 @@ ReadControlFile(void)
 						   "INDEX_MAX_KEYS", ControlFile->indexMaxKeys,
 						   "INDEX_MAX_KEYS", INDEX_MAX_KEYS),
 				 errhint("It looks like you need to recompile or initdb.")));
-	if (ControlFile->toast_max_chunk_size != TOAST_MAX_CHUNK_SIZE)
+	if (ControlFile->toast_max_chunk_size != TOAST_OID_MAX_CHUNK_SIZE)
 		ereport(FATAL,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("database files are incompatible with server"),
 		/* translator: %s is a variable name and %d is its value */
 				 errdetail("The database cluster was initialized with %s %d,"
 						   " but the server was compiled with %s %d.",
-						   "TOAST_MAX_CHUNK_SIZE", ControlFile->toast_max_chunk_size,
-						   "TOAST_MAX_CHUNK_SIZE", (int) TOAST_MAX_CHUNK_SIZE),
+						   "TOAST_OID_MAX_CHUNK_SIZE", ControlFile->toast_max_chunk_size,
+						   "TOAST_OID_MAX_CHUNK_SIZE", (int) TOAST_OID_MAX_CHUNK_SIZE),
 				 errhint("It looks like you need to recompile or initdb.")));
 	if (ControlFile->loblksize != LOBLKSIZE)
 		ereport(FATAL,
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 965091c10627..ac5ea4d454cc 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -5136,7 +5136,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		struct varlena *varlena;
 
 		/* va_rawsize is the size of the original datum -- including header */
-		struct varatt_external toast_pointer;
+		varatt_external_oid toast_pointer;
 		struct varatt_indirect redirect_pointer;
 		struct varlena *new_datum = NULL;
 		struct varlena *reconstructed;
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index c80191f0a224..95623b1f04c9 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -4195,7 +4195,7 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 {
 	int			typlen;
 	struct varlena *attr;
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 
 	/* On first call, get the input type's typlen, and save at *fn_extra */
 	if (fcinfo->flinfo->fn_extra == NULL)
diff --git a/src/bin/pg_resetwal/pg_resetwal.c b/src/bin/pg_resetwal/pg_resetwal.c
index b2c4b9db395a..53f9339dffda 100644
--- a/src/bin/pg_resetwal/pg_resetwal.c
+++ b/src/bin/pg_resetwal/pg_resetwal.c
@@ -733,7 +733,7 @@ GuessControlValues(void)
 	ControlFile.xlog_seg_size = DEFAULT_XLOG_SEG_SIZE;
 	ControlFile.nameDataLen = NAMEDATALEN;
 	ControlFile.indexMaxKeys = INDEX_MAX_KEYS;
-	ControlFile.toast_max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	ControlFile.toast_max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
 	ControlFile.loblksize = LOBLKSIZE;
 	ControlFile.float8ByVal = true; /* vestigial */
 
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index 175f18315cd4..63884ecdda61 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3566,7 +3566,7 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
       </row>
 
       <row>
-       <entry><structfield>max_toast_chunk_size</structfield></entry>
+       <entry><structfield>max_toast_oid_chunk_size</structfield></entry>
        <entry><type>integer</type></entry>
       </row>
 
diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml
index 02ddfda834a2..67600fd974d7 100644
--- a/doc/src/sgml/storage.sgml
+++ b/doc/src/sgml/storage.sgml
@@ -417,7 +417,7 @@ described in more detail below.
 
 <para>
 Out-of-line values are divided (after compression if used) into chunks of at
-most <symbol>TOAST_MAX_CHUNK_SIZE</symbol> bytes (by default this value is chosen
+most <symbol>TOAST_OID_MAX_CHUNK_SIZE</symbol> bytes (by default this value is chosen
 so that four chunk rows will fit on a page, making it about 2000 bytes).
 Each chunk is stored as a separate row in the <acronym>TOAST</acronym> table
 belonging to the owning table.  Every
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 0cbd9d140762..2d0f40ca2914 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -73,7 +73,7 @@ typedef enum SkipPages
  */
 typedef struct ToastedAttribute
 {
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 	BlockNumber blkno;			/* block in main table */
 	OffsetNumber offnum;		/* offset in main table */
 	AttrNumber	attnum;			/* attribute in main table */
@@ -1566,7 +1566,7 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 
 	toast_valueid = ta->toast_pointer.va_valueid;
 
-	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
 	last_chunk_seq = (extsize - 1) / max_chunk_size;
 
 	/* Sanity-check the sequence number. */
@@ -1672,7 +1672,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	uint16		infomask;
 	Oid8		toast_pointer_valueid;
 	CompactAttribute *thisatt;
-	struct varatt_external toast_pointer;
+	varatt_external_oid toast_pointer;
 
 	infomask = ctx->tuphdr->t_infomask;
 	thisatt = TupleDescCompactAttr(RelationGetDescr(ctx->rel), ctx->attnum);
@@ -1731,7 +1731,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	{
 		uint8		va_tag = VARTAG_EXTERNAL(tp + ctx->offset);
 
-		if (va_tag != VARTAG_ONDISK)
+		if (va_tag != VARTAG_ONDISK_OID)
 		{
 			report_corruption(ctx,
 							  psprintf("toasted attribute has unexpected TOAST tag %u",
@@ -1876,7 +1876,7 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	int32		expected_chunk_seq = 0;
 	int32		last_chunk_seq;
 	Oid8		toast_valueid;
-	int32		max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	int32		max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
 
 	extsize = VARATT_EXTERNAL_GET_EXTSIZE(ta->toast_pointer);
 	last_chunk_seq = (extsize - 1) / max_chunk_size;
-- 
2.51.0

v10-0004-Refactor-external-TOAST-pointer-code-for-better-.patchtext/x-diff; charset=us-asciiDownload
From 013874bfedb5f78532c9c0b2d24b85ac3bdb2969 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 30 Sep 2025 15:09:13 +0900
Subject: [PATCH v10 04/14] Refactor external TOAST pointer code for better
 pluggability

This commit introduces a new interface for external TOAST pointers,
which is able to make a translation of the varlena pointers stored on
disk to/from an new in-memory structure called toast_external.  The
types of varatt_external supported on disk need to be registered into a
new subsystem in a new file, called toast_external.[c|h], then define a
set of callbacks to allow the toasting and detoasting code to use it.

A follow-up change will rely on this refactoring to introduce new
vartag_external values with an associated varatt_external_* that is
able, which would be used in int8 TOAST tables.
---
 src/include/access/detoast.h                  |  12 +-
 src/include/access/heaptoast.h                |   3 +
 src/include/access/toast_external.h           | 176 ++++++++++++++++
 src/include/access/toast_helper.h             |   1 +
 src/include/varatt.h                          |  16 +-
 src/backend/access/common/Makefile            |   1 +
 src/backend/access/common/detoast.c           |  57 +++--
 src/backend/access/common/meson.build         |   1 +
 src/backend/access/common/toast_compression.c |  10 +-
 src/backend/access/common/toast_external.c    | 196 ++++++++++++++++++
 src/backend/access/common/toast_internals.c   |  84 +++++---
 src/backend/access/heap/heaptoast.c           |  20 +-
 src/backend/access/table/toast_helper.c       |  12 +-
 src/backend/access/transam/xlog.c             |   8 +-
 .../replication/logical/reorderbuffer.c       |  13 +-
 src/backend/utils/adt/varlena.c               |   7 +-
 src/bin/pg_resetwal/pg_resetwal.c             |   2 +-
 contrib/amcheck/verify_heapam.c               |  35 ++--
 src/tools/pgindent/typedefs.list              |   2 +
 19 files changed, 545 insertions(+), 111 deletions(-)
 create mode 100644 src/include/access/toast_external.h
 create mode 100644 src/backend/access/common/toast_external.c

diff --git a/src/include/access/detoast.h b/src/include/access/detoast.h
index f1399be7c4ea..630f8c7e04e8 100644
--- a/src/include/access/detoast.h
+++ b/src/include/access/detoast.h
@@ -14,10 +14,11 @@
 
 /*
  * Macro to fetch the possibly-unaligned contents of an EXTERNAL datum
- * into a local "varatt_external_oid" toast pointer.  This should be
- * just a memcpy, but some versions of gcc seem to produce broken code
- * that assumes the datum contents are aligned.  Introducing an explicit
- * intermediate "varattrib_1b_e *" variable seems to fix it.
+ * into a local "varatt_external_*" toast pointer, as supported
+ * in toast_external.h and varatt.h.  This should be just a memcpy, but
+ * some versions of gcc seem to produce broken code that assumes the datum
+ * contents are aligned.  Introducing an explicit intermediate
+ * "varattrib_1b_e *" variable seems to fix it.
  */
 #define VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr) \
 do { \
@@ -27,9 +28,6 @@ do { \
 	memcpy(&(toast_pointer), VARDATA_EXTERNAL(attre), sizeof(toast_pointer)); \
 } while (0)
 
-/* Size of an EXTERNAL datum that contains a standard TOAST pointer */
-#define TOAST_OID_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_oid))
-
 /* Size of an EXTERNAL datum that contains an indirection pointer */
 #define INDIRECT_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_indirect))
 
diff --git a/src/include/access/heaptoast.h b/src/include/access/heaptoast.h
index df77aa9ce61d..3128539f4716 100644
--- a/src/include/access/heaptoast.h
+++ b/src/include/access/heaptoast.h
@@ -88,6 +88,9 @@
 	 sizeof(int32) -									\
 	 VARHDRSZ)
 
+/* Maximum size of chunk possible */
+#define TOAST_MAX_CHUNK_SIZE	TOAST_OID_MAX_CHUNK_SIZE
+
 /* ----------
  * heap_toast_insert_or_update -
  *
diff --git a/src/include/access/toast_external.h b/src/include/access/toast_external.h
new file mode 100644
index 000000000000..6450343eab25
--- /dev/null
+++ b/src/include/access/toast_external.h
@@ -0,0 +1,176 @@
+/*-------------------------------------------------------------------------
+ *
+ * toast_external.h
+ *	  Support for on-disk external TOAST pointers
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1995, Regents of the University of California
+ *
+ * src/include/access/toast_external.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef TOAST_EXTERNAL_H
+#define TOAST_EXTERNAL_H
+
+#include "access/toast_compression.h"
+#include "varatt.h"
+
+/*
+ * Intermediate in-memory structure used when creating on-disk
+ * varatt_external_* or when deserializing varlena contents.
+ */
+typedef struct toast_external_data
+{
+	/* Original data size (includes header) */
+	int32		rawsize;
+
+	/* External saved size (without header) */
+	uint32		extsize;
+
+	/*
+	 * Compression method.
+	 *
+	 * If not compressed, set to TOAST_INVALID_COMPRESSION_ID.
+	 */
+	ToastCompressionId compression_method;
+
+	/* Relation OID of TOAST table containing the value */
+	Oid			toastrelid;
+
+	/*
+	 * Unique ID of value within TOAST table.  This could be an OID or an Oid8
+	 * value.  This field is large enough to be able to store any of these.
+	 */
+	Oid8		valueid;
+} toast_external_data;
+
+/*
+ * Metadata for external TOAST pointer kinds, separated based on their
+ * vartag_external.
+ */
+typedef struct toast_external_info
+{
+	/*
+	 * Maximum chunk of data authorized for this type of external TOAST
+	 * pointer, when dividing an entry by chunks.  Sized depending on the size
+	 * of its varatt_external_* structure.
+	 */
+	int32		maximum_chunk_size;
+
+	/*
+	 * Size of an external TOAST pointer of this type, typically
+	 * (VARHDRSZ_EXTERNAL + sizeof(varatt_external_struct)).
+	 */
+	int32		toast_pointer_size;
+
+	/*
+	 * Map an input varlena to a toast_external_data, for consumption in the
+	 * backend code.  "data" is an input/output result.
+	 */
+	void		(*to_external_data) (struct varlena *attr,
+									 toast_external_data *data);
+
+	/*
+	 * Create a varlena that will be used on-disk for the given TOAST type,
+	 * based on the given input data.
+	 *
+	 * The result is the varlena created, for on-disk insertion.
+	 */
+	struct varlena *(*create_external_data) (toast_external_data data);
+
+} toast_external_info;
+
+/* Retrieve a toast_external_info from a vartag */
+extern const toast_external_info *toast_external_get_info(uint8 tag);
+
+/* Retrieve toast_pointer_size using a TOAST attribute type */
+extern int32 toast_external_info_get_pointer_size(uint8 tag);
+
+/* Retrieve the vartag to assign to a TOAST typle */
+extern uint8 toast_external_assign_vartag(Oid toastrelid, Oid8 value);
+
+/*
+ * Testing whether an externally-stored value is compressed now requires
+ * comparing size stored in extsize (the actual length of the external data)
+ * to rawsize (the original uncompressed datum's size).  The latter includes
+ * VARHDRSZ overhead, the former doesn't.  We never use compression unless it
+ * actually saves space, so we expect either equality or less-than.
+ */
+static inline bool
+TOAST_EXTERNAL_IS_COMPRESSED(toast_external_data data)
+{
+	return data.extsize < (data.rawsize - VARHDRSZ);
+}
+
+/* Full data structure */
+static inline void
+toast_external_info_get_data(struct varlena *attr, toast_external_data *data)
+{
+	uint8		tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+
+	info->to_external_data(attr, data);
+}
+
+/*
+ * Helper routines to recover specific fields in toast_external_data.  Most
+ * code paths doing work with on-disk external TOAST pointers care about
+ * these.
+ */
+
+/* Detoasted "raw" size */
+static inline Size
+toast_external_info_get_rawsize(struct varlena *attr)
+{
+	uint8		tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+	toast_external_data data;
+
+	info->to_external_data(attr, &data);
+
+	return data.rawsize;
+}
+
+/* External saved size */
+static inline Size
+toast_external_info_get_extsize(struct varlena *attr)
+{
+	uint8		tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+	toast_external_data data;
+
+	info->to_external_data(attr, &data);
+
+	return data.extsize;
+}
+
+/* Compression method ID */
+static inline ToastCompressionId
+toast_external_info_get_compression_method(struct varlena *attr)
+{
+	uint8		tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+	toast_external_data data;
+
+	info->to_external_data(attr, &data);
+
+	return data.compression_method;
+}
+
+/* Value ID */
+static inline Oid8
+toast_external_info_get_valueid(struct varlena *attr)
+{
+	uint8		tag = VARTAG_EXTERNAL(attr);
+	const toast_external_info *info = toast_external_get_info(tag);
+	toast_external_data data;
+
+	info->to_external_data(attr, &data);
+
+	return data.valueid;
+}
+
+#endif							/* TOAST_EXTERNAL_H */
diff --git a/src/include/access/toast_helper.h b/src/include/access/toast_helper.h
index 9bd6bfaffe55..4a1381a3fbc5 100644
--- a/src/include/access/toast_helper.h
+++ b/src/include/access/toast_helper.h
@@ -47,6 +47,7 @@ typedef struct
 	 * should be NULL in the case of an insert.
 	 */
 	Relation	ttc_rel;		/* the relation that contains the tuple */
+	int32		ttc_toast_pointer_size; /* size of external TOAST pointer */
 	Datum	   *ttc_values;		/* values from the tuple columns */
 	bool	   *ttc_isnull;		/* null flags for the tuple columns */
 	Datum	   *ttc_oldvalues;	/* values from previous tuple */
diff --git a/src/include/varatt.h b/src/include/varatt.h
index c13939ba8b4c..0807b371c87f 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -21,6 +21,9 @@
  * The data is compressed if and only if the external size stored in
  * va_extinfo is less than va_rawsize - VARHDRSZ.
  *
+ * The value ID is an OID, used for TOAST relations with OID as attribute
+ * for chunk_id.
+ *
  * This struct must not contain any padding, because we sometimes compare
  * these pointers using memcmp.
  *
@@ -51,7 +54,7 @@ typedef struct varatt_external_oid
  * The creator of such a Datum is entirely responsible that the referenced
  * storage survives for as long as referencing pointer Datums can exist.
  *
- * Note that just as for varatt_external_oid, this struct is stored
+ * Note that just as for varatt_external_*, this struct is stored
  * unaligned within any containing tuple.
  */
 typedef struct varatt_indirect
@@ -66,7 +69,7 @@ typedef struct varatt_indirect
  * storage.  APIs for this, in particular the definition of struct
  * ExpandedObjectHeader, are in src/include/utils/expandeddatum.h.
  *
- * Note that just as for varatt_external_oid, this struct is stored
+ * Note that just as for varatt_external_*, this struct is stored
  * unaligned within any containing tuple.
  */
 typedef struct ExpandedObjectHeader ExpandedObjectHeader;
@@ -357,11 +360,18 @@ VARATT_IS_EXTERNAL(const void *PTR)
 	return VARATT_IS_1B_E(PTR);
 }
 
+/* Is varlena datum a pointer to on-disk toasted data with OID value? */
+static inline bool
+VARATT_IS_EXTERNAL_ONDISK_OID(const void *PTR)
+{
+	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID;
+}
+
 /* Is varlena datum a pointer to on-disk toasted data? */
 static inline bool
 VARATT_IS_EXTERNAL_ONDISK(const void *PTR)
 {
-	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID;
+	return VARATT_IS_EXTERNAL_ONDISK_OID(PTR);
 }
 
 /* Is varlena datum an indirect pointer? */
diff --git a/src/backend/access/common/Makefile b/src/backend/access/common/Makefile
index e78de312659e..1ef86a245886 100644
--- a/src/backend/access/common/Makefile
+++ b/src/backend/access/common/Makefile
@@ -27,6 +27,7 @@ OBJS = \
 	syncscan.o \
 	tidstore.o \
 	toast_compression.o \
+	toast_external.o \
 	toast_internals.o \
 	tupconvert.o \
 	tupdesc.o
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 6dd8b261bf9b..d24a84d6509a 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -16,6 +16,7 @@
 #include "access/detoast.h"
 #include "access/table.h"
 #include "access/tableam.h"
+#include "access/toast_external.h"
 #include "access/toast_internals.h"
 #include "common/int.h"
 #include "common/pg_lzcompress.h"
@@ -225,12 +226,12 @@ detoast_attr_slice(struct varlena *attr,
 
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
-		varatt_external_oid toast_pointer;
+		toast_external_data toast_pointer;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		toast_external_info_get_data(attr, &toast_pointer);
 
 		/* fast path for non-compressed external datums */
-		if (!VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+		if (!TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 			return toast_fetch_datum_slice(attr, sliceoffset, slicelength);
 
 		/*
@@ -240,7 +241,7 @@ detoast_attr_slice(struct varlena *attr,
 		 */
 		if (slicelimit >= 0)
 		{
-			int32		max_size = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+			int32		max_size = toast_pointer.extsize;
 
 			/*
 			 * Determine maximum amount of compressed data needed for a prefix
@@ -251,8 +252,7 @@ detoast_attr_slice(struct varlena *attr,
 			 * determine how much compressed data we need to be sure of being
 			 * able to decompress the required slice.
 			 */
-			if (VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) ==
-				TOAST_PGLZ_COMPRESSION_ID)
+			if (toast_pointer.compression_method == TOAST_PGLZ_COMPRESSION_ID)
 				max_size = pglz_maximum_compressed_size(slicelimit, max_size);
 
 			/*
@@ -344,20 +344,21 @@ toast_fetch_datum(struct varlena *attr)
 {
 	Relation	toastrel;
 	struct varlena *result;
-	varatt_external_oid toast_pointer;
+	toast_external_data toast_pointer;
 	int32		attrsize;
+	Oid8		valueid;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		elog(ERROR, "toast_fetch_datum shouldn't be called for non-ondisk datums");
 
 	/* Must copy to access aligned fields */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_external_info_get_data(attr, &toast_pointer);
 
-	attrsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+	attrsize = toast_pointer.extsize;
 
 	result = (struct varlena *) palloc(attrsize + VARHDRSZ);
 
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+	if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 		SET_VARSIZE_COMPRESSED(result, attrsize + VARHDRSZ);
 	else
 		SET_VARSIZE(result, attrsize + VARHDRSZ);
@@ -365,14 +366,15 @@ toast_fetch_datum(struct varlena *attr)
 	if (attrsize == 0)
 		return result;			/* Probably shouldn't happen, but just in
 								 * case. */
+	valueid = toast_pointer.valueid;
 
 	/*
 	 * Open the toast relation and its indexes
 	 */
-	toastrel = table_open(toast_pointer.va_toastrelid, AccessShareLock);
+	toastrel = table_open(toast_pointer.toastrelid, AccessShareLock);
 
 	/* Fetch all chunks */
-	table_relation_fetch_toast_slice(toastrel, toast_pointer.va_valueid,
+	table_relation_fetch_toast_slice(toastrel, valueid,
 									 attrsize, 0, attrsize, result);
 
 	/* Close toast table */
@@ -398,23 +400,26 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 {
 	Relation	toastrel;
 	struct varlena *result;
-	varatt_external_oid toast_pointer;
+	toast_external_data toast_pointer;
 	int32		attrsize;
+	Oid8		valueid;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		elog(ERROR, "toast_fetch_datum_slice shouldn't be called for non-ondisk datums");
 
 	/* Must copy to access aligned fields */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_external_info_get_data(attr, &toast_pointer);
+
+	valueid = toast_pointer.valueid;
 
 	/*
 	 * It's nonsense to fetch slices of a compressed datum unless when it's a
 	 * prefix -- this isn't lo_* we can't return a compressed datum which is
 	 * meaningful to toast later.
 	 */
-	Assert(!VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer) || 0 == sliceoffset);
+	Assert(!TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer) || 0 == sliceoffset);
 
-	attrsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+	attrsize = toast_pointer.extsize;
 
 	if (sliceoffset >= attrsize)
 	{
@@ -427,7 +432,7 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 	 * space required by va_tcinfo, which is stored at the beginning as an
 	 * int32 value.
 	 */
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer) && slicelength > 0)
+	if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer) && slicelength > 0)
 		slicelength = slicelength + sizeof(int32);
 
 	/*
@@ -440,7 +445,7 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 
 	result = (struct varlena *) palloc(slicelength + VARHDRSZ);
 
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+	if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 		SET_VARSIZE_COMPRESSED(result, slicelength + VARHDRSZ);
 	else
 		SET_VARSIZE(result, slicelength + VARHDRSZ);
@@ -449,10 +454,11 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 		return result;			/* Can save a lot of work at this point! */
 
 	/* Open the toast relation */
-	toastrel = table_open(toast_pointer.va_toastrelid, AccessShareLock);
+	toastrel = table_open(toast_pointer.toastrelid, AccessShareLock);
 
 	/* Fetch all chunks */
-	table_relation_fetch_toast_slice(toastrel, toast_pointer.va_valueid,
+	table_relation_fetch_toast_slice(toastrel,
+									 valueid,
 									 attrsize, sliceoffset, slicelength,
 									 result);
 
@@ -549,11 +555,7 @@ toast_raw_datum_size(Datum value)
 
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
-		/* va_rawsize is the size of the original datum -- including header */
-		varatt_external_oid toast_pointer;
-
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-		result = toast_pointer.va_rawsize;
+		result = toast_external_info_get_rawsize(attr);
 	}
 	else if (VARATT_IS_EXTERNAL_INDIRECT(attr))
 	{
@@ -610,10 +612,7 @@ toast_datum_size(Datum value)
 		 * compressed or not.  We do not count the size of the toast pointer
 		 * ... should we?
 		 */
-		varatt_external_oid toast_pointer;
-
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-		result = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+		result = toast_external_info_get_extsize(attr);
 	}
 	else if (VARATT_IS_EXTERNAL_INDIRECT(attr))
 	{
diff --git a/src/backend/access/common/meson.build b/src/backend/access/common/meson.build
index 35e89b5ea67d..bab3ed7d3558 100644
--- a/src/backend/access/common/meson.build
+++ b/src/backend/access/common/meson.build
@@ -15,6 +15,7 @@ backend_sources += files(
   'syncscan.c',
   'tidstore.c',
   'toast_compression.c',
+  'toast_external.c',
   'toast_internals.c',
   'tupconvert.c',
   'tupdesc.c',
diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c
index 0c5ed937cff8..0b69c25175ad 100644
--- a/src/backend/access/common/toast_compression.c
+++ b/src/backend/access/common/toast_compression.c
@@ -19,6 +19,7 @@
 
 #include "access/detoast.h"
 #include "access/toast_compression.h"
+#include "access/toast_external.h"
 #include "common/pg_lzcompress.h"
 #include "varatt.h"
 
@@ -261,14 +262,7 @@ toast_get_compression_id(struct varlena *attr)
 	 * toast compression header.
 	 */
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
-	{
-		varatt_external_oid toast_pointer;
-
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-
-		if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
-			cmid = VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer);
-	}
+		cmid = toast_external_info_get_compression_method(attr);
 	else if (VARATT_IS_COMPRESSED(attr))
 		cmid = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(attr);
 
diff --git a/src/backend/access/common/toast_external.c b/src/backend/access/common/toast_external.c
new file mode 100644
index 000000000000..2154152b8bfb
--- /dev/null
+++ b/src/backend/access/common/toast_external.c
@@ -0,0 +1,196 @@
+/*-------------------------------------------------------------------------
+ *
+ * toast_external.c
+ *	  Functions for the support of external on-disk TOAST pointers.
+ *
+ * This includes all the types of external on-disk TOAST pointers supported
+ * by the backend, based on the callbacks and data defined in
+ * toast_external.h.
+ *
+ * Copyright (c) 2025, PostgreSQL Global Development Group
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/common/toast_external.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/detoast.h"
+#include "access/heaptoast.h"
+#include "access/toast_external.h"
+
+/* Callbacks for VARTAG_ONDISK_OID */
+static void ondisk_oid_to_external_data(struct varlena *attr,
+										toast_external_data *data);
+static struct varlena *ondisk_oid_create_external_data(toast_external_data data);
+
+/*
+ * Size of an EXTERNAL datum that contains a standard TOAST pointer (OID
+ * value).
+ */
+#define TOAST_OID_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_oid))
+
+/*
+ * For now there are only two types, all defined in this file.  For now this
+ * is the maximum value of vartag_external, which is a historical choice.
+ */
+#define TOAST_EXTERNAL_INFO_SIZE	(VARTAG_ONDISK_OID + 1)
+
+/*
+ * The different kinds of on-disk external TOAST pointers, divided by
+ * vartag_external.
+ *
+ * See comments for struct toast_external_info about the details of the
+ * individual fields.
+ */
+static const toast_external_info toast_external_infos[TOAST_EXTERNAL_INFO_SIZE] = {
+	[VARTAG_ONDISK_OID] = {
+		.toast_pointer_size = TOAST_OID_POINTER_SIZE,
+		.maximum_chunk_size = TOAST_OID_MAX_CHUNK_SIZE,
+		.to_external_data = ondisk_oid_to_external_data,
+		.create_external_data = ondisk_oid_create_external_data,
+	},
+};
+
+/*
+ * toast_external_get_info
+ *
+ * Get toast_external_info of the defined vartag_external, central set of
+ * callbacks, based on a "tag", which is a vartag_external value for an
+ * on-disk external varlena.
+ */
+const toast_external_info *
+toast_external_get_info(uint8 tag)
+{
+	const toast_external_info *res = &toast_external_infos[tag];
+
+	/* check tag for invalid range */
+	if (tag >= TOAST_EXTERNAL_INFO_SIZE)
+		elog(ERROR, "incorrect value %u for toast_external_info", tag);
+
+	/* sanity check with tag in valid range */
+	res = &toast_external_infos[tag];
+	if (res == NULL)
+		elog(ERROR, "incorrect value %u for toast_external_info", tag);
+	return res;
+}
+
+/*
+ * toast_external_info_get_pointer_size
+ *
+ * Get external TOAST pointer size based on the attribute type of a TOAST
+ * value.  "tag" is a vartag_external value.
+ */
+int32
+toast_external_info_get_pointer_size(uint8 tag)
+{
+	return toast_external_infos[tag].toast_pointer_size;
+}
+
+/*
+ * toast_external_assign_vartag
+ *
+ * Assign the vartag_external of a TOAST tuple, based on the TOAST relation
+ * it uses and its value.
+ *
+ * An invalid value can be given by the caller of this routine, in which
+ * case a default vartag should be provided based on only the toast relation
+ * used.
+ */
+uint8
+toast_external_assign_vartag(Oid toastrelid, Oid8 valueid)
+{
+	/*
+	 * If dealing with a code path where a TOAST relation may not be assigned,
+	 * like heap_toast_insert_or_update(), just use the legacy
+	 * vartag_external.
+	 */
+	if (!OidIsValid(toastrelid))
+		return VARTAG_ONDISK_OID;
+
+	/*
+	 * Currently there is only one type of vartag_external supported: 4-byte
+	 * value with OID for the chunk_id type.
+	 *
+	 * Note: This routine will be extended to be able to use multiple
+	 * vartag_external within a single TOAST relation type, that may change
+	 * depending on the value used.
+	 */
+	return VARTAG_ONDISK_OID;
+}
+
+/*
+ * Helper routines able to translate the various varatt_external_* from/to
+ * the in-memory representation toast_external_data used in the backend.
+ */
+
+/* Callbacks for VARTAG_ONDISK_OID */
+
+/*
+ * ondisk_oid_to_external_data
+ *
+ * Translate a varlena to its toast_external_data representation, to be used
+ * by the backend code.
+ */
+static void
+ondisk_oid_to_external_data(struct varlena *attr, toast_external_data *data)
+{
+	varatt_external_oid external;
+
+	VARATT_EXTERNAL_GET_POINTER(external, attr);
+	data->rawsize = external.va_rawsize;
+
+	/*
+	 * External size and compression methods are stored in the same field,
+	 * extract.
+	 */
+	if (VARATT_EXTERNAL_IS_COMPRESSED(external))
+	{
+		data->extsize = VARATT_EXTERNAL_GET_EXTSIZE(external);
+		data->compression_method = VARATT_EXTERNAL_GET_COMPRESS_METHOD(external);
+	}
+	else
+	{
+		data->extsize = external.va_extinfo;
+		data->compression_method = TOAST_INVALID_COMPRESSION_ID;
+	}
+
+	data->valueid = (Oid8) external.va_valueid;
+	data->toastrelid = external.va_toastrelid;
+}
+
+/*
+ * ondisk_oid_create_external_data
+ *
+ * Create a new varlena based on the input toast_external_data, to be used
+ * when saving a new TOAST value.
+ */
+static struct varlena *
+ondisk_oid_create_external_data(toast_external_data data)
+{
+	struct varlena *result = NULL;
+	varatt_external_oid external;
+
+	external.va_rawsize = data.rawsize;
+
+	if (data.compression_method != TOAST_INVALID_COMPRESSION_ID)
+	{
+		/* Set size and compression method, in a single field. */
+		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(external,
+													 data.extsize,
+													 data.compression_method);
+	}
+	else
+		external.va_extinfo = data.extsize;
+
+	external.va_toastrelid = data.toastrelid;
+	external.va_valueid = (Oid) data.valueid;
+
+	result = (struct varlena *) palloc(TOAST_OID_POINTER_SIZE);
+	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_OID);
+	memcpy(VARDATA_EXTERNAL(result), &external, sizeof(external));
+
+	return result;
+}
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 2becbcc33338..90e3f43eac59 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -18,6 +18,7 @@
 #include "access/heapam.h"
 #include "access/heaptoast.h"
 #include "access/table.h"
+#include "access/toast_external.h"
 #include "access/toast_internals.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -124,13 +125,15 @@ toast_save_datum(Relation rel, Datum value,
 	TupleDesc	toasttupDesc;
 	CommandId	mycid = GetCurrentCommandId(true);
 	struct varlena *result;
-	varatt_external_oid toast_pointer;
+	toast_external_data toast_pointer;
 	int32		chunk_seq = 0;
 	char	   *data_p;
 	int32		data_todo;
 	Pointer		dval = DatumGetPointer(value);
 	int			num_indexes;
 	int			validIndex;
+	const toast_external_info *info;
+	uint8		tag = VARTAG_INDIRECT;	/* init value does not matter */
 
 	Assert(!VARATT_IS_EXTERNAL(dval));
 
@@ -162,28 +165,41 @@ toast_save_datum(Relation rel, Datum value,
 	{
 		data_p = VARDATA_SHORT(dval);
 		data_todo = VARSIZE_SHORT(dval) - VARHDRSZ_SHORT;
-		toast_pointer.va_rawsize = data_todo + VARHDRSZ;	/* as if not short */
-		toast_pointer.va_extinfo = data_todo;
+		toast_pointer.rawsize = data_todo + VARHDRSZ;	/* as if not short */
+		toast_pointer.extsize = data_todo;
+
+		/*
+		 * TOAST_INVALID_COMPRESSION_ID means that the varlena is not
+		 * compressed, see toast_get_compression_id().
+		 */
+		toast_pointer.compression_method = TOAST_INVALID_COMPRESSION_ID;
 	}
 	else if (VARATT_IS_COMPRESSED(dval))
 	{
 		data_p = VARDATA(dval);
 		data_todo = VARSIZE(dval) - VARHDRSZ;
 		/* rawsize in a compressed datum is just the size of the payload */
-		toast_pointer.va_rawsize = VARDATA_COMPRESSED_GET_EXTSIZE(dval) + VARHDRSZ;
+		toast_pointer.rawsize = VARDATA_COMPRESSED_GET_EXTSIZE(dval) + VARHDRSZ;
 
 		/* set external size and compression method */
-		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, data_todo,
-													 VARDATA_COMPRESSED_GET_COMPRESS_METHOD(dval));
+		toast_pointer.extsize = data_todo;
+		toast_pointer.compression_method = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(dval);
+
 		/* Assert that the numbers look like it's compressed */
-		Assert(VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer));
+		Assert(TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer));
 	}
 	else
 	{
 		data_p = VARDATA(dval);
 		data_todo = VARSIZE(dval) - VARHDRSZ;
-		toast_pointer.va_rawsize = VARSIZE(dval);
-		toast_pointer.va_extinfo = data_todo;
+		toast_pointer.rawsize = VARSIZE(dval);
+		toast_pointer.extsize = data_todo;
+
+		/*
+		 * TOAST_INVALID_COMPRESSION_ID means that the varlena is not
+		 * compressed, see toast_get_compression_id().
+		 */
+		toast_pointer.compression_method = TOAST_INVALID_COMPRESSION_ID;
 	}
 
 	/*
@@ -195,9 +211,9 @@ toast_save_datum(Relation rel, Datum value,
 	 * if we have to substitute such an OID.
 	 */
 	if (OidIsValid(rel->rd_toastoid))
-		toast_pointer.va_toastrelid = rel->rd_toastoid;
+		toast_pointer.toastrelid = rel->rd_toastoid;
 	else
-		toast_pointer.va_toastrelid = RelationGetRelid(toastrel);
+		toast_pointer.toastrelid = RelationGetRelid(toastrel);
 
 	/*
 	 * Choose an OID to use as the value ID for this toast value.
@@ -214,7 +230,7 @@ toast_save_datum(Relation rel, Datum value,
 	if (!OidIsValid(rel->rd_toastoid))
 	{
 		/* normal case: just choose an unused OID */
-		toast_pointer.va_valueid =
+		toast_pointer.valueid =
 			GetNewOidWithIndex(toastrel,
 							   RelationGetRelid(toastidxs[validIndex]),
 							   (AttrNumber) 1);
@@ -222,18 +238,18 @@ toast_save_datum(Relation rel, Datum value,
 	else
 	{
 		/* rewrite case: check to see if value was in old toast table */
-		toast_pointer.va_valueid = InvalidOid;
+		toast_pointer.valueid = InvalidOid8;
 		if (oldexternal != NULL)
 		{
-			varatt_external_oid old_toast_pointer;
+			toast_external_data old_toast_pointer;
 
 			Assert(VARATT_IS_EXTERNAL_ONDISK(oldexternal));
-			/* Must copy to access aligned fields */
-			VARATT_EXTERNAL_GET_POINTER(old_toast_pointer, oldexternal);
-			if (old_toast_pointer.va_toastrelid == rel->rd_toastoid)
+			toast_external_info_get_data(oldexternal, &old_toast_pointer);
+
+			if (old_toast_pointer.toastrelid == rel->rd_toastoid)
 			{
 				/* This value came from the old toast table; reuse its OID */
-				toast_pointer.va_valueid = old_toast_pointer.va_valueid;
+				toast_pointer.valueid = old_toast_pointer.valueid;
 
 				/*
 				 * There is a corner case here: the table rewrite might have
@@ -253,14 +269,14 @@ toast_save_datum(Relation rel, Datum value,
 				 * be reclaimed by VACUUM.
 				 */
 				if (toastrel_valueid_exists(toastrel,
-											toast_pointer.va_valueid))
+											toast_pointer.valueid))
 				{
 					/* Match, so short-circuit the data storage loop below */
 					data_todo = 0;
 				}
 			}
 		}
-		if (toast_pointer.va_valueid == InvalidOid)
+		if (toast_pointer.valueid == InvalidOid8)
 		{
 			/*
 			 * new value; must choose an OID that doesn't conflict in either
@@ -268,15 +284,23 @@ toast_save_datum(Relation rel, Datum value,
 			 */
 			do
 			{
-				toast_pointer.va_valueid =
+				toast_pointer.valueid =
 					GetNewOidWithIndex(toastrel,
 									   RelationGetRelid(toastidxs[validIndex]),
 									   (AttrNumber) 1);
 			} while (toastid_valueid_exists(rel->rd_toastoid,
-											toast_pointer.va_valueid));
+											toast_pointer.valueid));
 		}
 	}
 
+	/*
+	 * Retrieve the vartag that can be assigned for the new TOAST tuple. This
+	 * depends on the type of TOAST table and its assigned value.
+	 */
+	tag = toast_external_assign_vartag(toast_pointer.toastrelid,
+									   toast_pointer.valueid);
+	info = toast_external_get_info(tag);
+
 	/*
 	 * Split up the item into chunks
 	 */
@@ -298,12 +322,12 @@ toast_save_datum(Relation rel, Datum value,
 		/*
 		 * Calculate the size of this chunk
 		 */
-		chunk_size = Min(TOAST_OID_MAX_CHUNK_SIZE, data_todo);
+		chunk_size = Min(info->maximum_chunk_size, data_todo);
 
 		/*
 		 * Build a tuple and store it
 		 */
-		t_values[0] = ObjectIdGetDatum(toast_pointer.va_valueid);
+		t_values[0] = ObjectIdGetDatum(toast_pointer.valueid);
 		t_values[1] = Int32GetDatum(chunk_seq++);
 		SET_VARSIZE(&chunk_data, chunk_size + VARHDRSZ);
 		memcpy(VARDATA(&chunk_data), data_p, chunk_size);
@@ -359,9 +383,7 @@ toast_save_datum(Relation rel, Datum value,
 	/*
 	 * Create the TOAST pointer value that we'll return
 	 */
-	result = (struct varlena *) palloc(TOAST_OID_POINTER_SIZE);
-	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_OID);
-	memcpy(VARDATA_EXTERNAL(result), &toast_pointer, sizeof(toast_pointer));
+	result = info->create_external_data(toast_pointer);
 
 	return PointerGetDatum(result);
 }
@@ -376,7 +398,7 @@ void
 toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 {
 	struct varlena *attr = (struct varlena *) DatumGetPointer(value);
-	varatt_external_oid toast_pointer;
+	toast_external_data toast_pointer;
 	Relation	toastrel;
 	Relation   *toastidxs;
 	ScanKeyData toastkey;
@@ -389,12 +411,12 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 		return;
 
 	/* Must copy to access aligned fields */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_external_info_get_data(attr, &toast_pointer);
 
 	/*
 	 * Open the toast relation and its indexes
 	 */
-	toastrel = table_open(toast_pointer.va_toastrelid, RowExclusiveLock);
+	toastrel = table_open(toast_pointer.toastrelid, RowExclusiveLock);
 
 	/* Fetch valid relation used for process */
 	validIndex = toast_open_indexes(toastrel,
@@ -408,7 +430,7 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	ScanKeyInit(&toastkey,
 				(AttrNumber) 1,
 				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(toast_pointer.va_valueid));
+				ObjectIdGetDatum(toast_pointer.valueid));
 
 	/*
 	 * Find all the chunks.  (We don't actually care whether we see them in
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index 8e914c61ecae..fd102037be43 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -28,6 +28,7 @@
 #include "access/genam.h"
 #include "access/heapam.h"
 #include "access/heaptoast.h"
+#include "access/toast_external.h"
 #include "access/toast_helper.h"
 #include "access/toast_internals.h"
 #include "utils/fmgroids.h"
@@ -109,6 +110,7 @@ heap_toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup,
 	Datum		toast_oldvalues[MaxHeapAttributeNumber];
 	ToastAttrInfo toast_attr[MaxHeapAttributeNumber];
 	ToastTupleContext ttc;
+	uint8		tag;
 
 	/*
 	 * Ignore the INSERT_SPECULATIVE option. Speculative insertions/super
@@ -140,6 +142,16 @@ heap_toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup,
 	 * Prepare for toasting
 	 * ----------
 	 */
+
+	/*
+	 * Retrieve the toast pointer size based on the type of external TOAST
+	 * pointer assumed to be used.
+	 */
+
+	/* The default value is invalid, to work as a default. */
+	tag = toast_external_assign_vartag(rel->rd_rel->reltoastrelid, InvalidOid8);
+	ttc.ttc_toast_pointer_size = toast_external_info_get_pointer_size(tag);
+
 	ttc.ttc_rel = rel;
 	ttc.ttc_values = toast_values;
 	ttc.ttc_isnull = toast_isnull;
@@ -640,6 +652,8 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 	int			num_indexes;
 	int			validIndex;
 	int32		max_chunk_size;
+	const toast_external_info *info;
+	uint8		tag = VARTAG_INDIRECT;	/* init value does not matter */
 
 	/* Look for the valid index of toast relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -647,7 +661,11 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 									&toastidxs,
 									&num_indexes);
 
-	max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
+	/* Grab the information for toast_external_data */
+	tag = toast_external_assign_vartag(RelationGetRelid(toastrel), valueid);
+	info = toast_external_get_info(tag);
+
+	max_chunk_size = info->maximum_chunk_size;
 
 	totalchunks = ((attrsize - 1) / max_chunk_size) + 1;
 	startchunk = sliceoffset / max_chunk_size;
diff --git a/src/backend/access/table/toast_helper.c b/src/backend/access/table/toast_helper.c
index 8b1c4f8237fd..38f9d8b3a040 100644
--- a/src/backend/access/table/toast_helper.c
+++ b/src/backend/access/table/toast_helper.c
@@ -171,8 +171,10 @@ toast_tuple_init(ToastTupleContext *ttc)
  * The column must have attstorage EXTERNAL or EXTENDED if check_main is
  * false, and must have attstorage MAIN if check_main is true.
  *
- * The column must have a minimum size of MAXALIGN(TOAST_OID_POINTER_SIZE);
- * if not, no benefit is to be expected by compressing it.
+ * The column must have a minimum size of MAXALIGN(tcc_toast_pointer_size);
+ * if not, no benefit is to be expected by compressing it.  The TOAST
+ * pointer size is given by the caller, depending on the type of TOAST
+ * table we are dealing with.
  *
  * The return value is the index of the biggest suitable column, or
  * -1 if there is none.
@@ -184,10 +186,14 @@ toast_tuple_find_biggest_attribute(ToastTupleContext *ttc,
 	TupleDesc	tupleDesc = ttc->ttc_rel->rd_att;
 	int			numAttrs = tupleDesc->natts;
 	int			biggest_attno = -1;
-	int32		biggest_size = MAXALIGN(TOAST_OID_POINTER_SIZE);
+	int32		biggest_size = 0;
 	int32		skip_colflags = TOASTCOL_IGNORE;
 	int			i;
 
+	/* Define the lower-bound */
+	biggest_size = MAXALIGN(ttc->ttc_toast_pointer_size);
+	Assert(biggest_size != 0);
+
 	if (for_compression)
 		skip_colflags |= TOASTCOL_INCOMPRESSIBLE;
 
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index d623646a6edf..81dc86847c01 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4297,7 +4297,7 @@ WriteControlFile(void)
 	ControlFile->nameDataLen = NAMEDATALEN;
 	ControlFile->indexMaxKeys = INDEX_MAX_KEYS;
 
-	ControlFile->toast_max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
+	ControlFile->toast_max_chunk_size = TOAST_MAX_CHUNK_SIZE;
 	ControlFile->loblksize = LOBLKSIZE;
 
 	ControlFile->float8ByVal = true;	/* vestigial */
@@ -4550,15 +4550,15 @@ ReadControlFile(void)
 						   "INDEX_MAX_KEYS", ControlFile->indexMaxKeys,
 						   "INDEX_MAX_KEYS", INDEX_MAX_KEYS),
 				 errhint("It looks like you need to recompile or initdb.")));
-	if (ControlFile->toast_max_chunk_size != TOAST_OID_MAX_CHUNK_SIZE)
+	if (ControlFile->toast_max_chunk_size != TOAST_MAX_CHUNK_SIZE)
 		ereport(FATAL,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("database files are incompatible with server"),
 		/* translator: %s is a variable name and %d is its value */
 				 errdetail("The database cluster was initialized with %s %d,"
 						   " but the server was compiled with %s %d.",
-						   "TOAST_OID_MAX_CHUNK_SIZE", ControlFile->toast_max_chunk_size,
-						   "TOAST_OID_MAX_CHUNK_SIZE", (int) TOAST_OID_MAX_CHUNK_SIZE),
+						   "TOAST_MAX_CHUNK_SIZE", ControlFile->toast_max_chunk_size,
+						   "TOAST_MAX_CHUNK_SIZE", (int) TOAST_MAX_CHUNK_SIZE),
 				 errhint("It looks like you need to recompile or initdb.")));
 	if (ControlFile->loblksize != LOBLKSIZE)
 		ereport(FATAL,
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index ac5ea4d454cc..681eb9930587 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -92,6 +92,7 @@
 #include "access/detoast.h"
 #include "access/heapam.h"
 #include "access/rewriteheap.h"
+#include "access/toast_external.h"
 #include "access/transam.h"
 #include "access/xact.h"
 #include "access/xlog_internal.h"
@@ -5136,7 +5137,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		struct varlena *varlena;
 
 		/* va_rawsize is the size of the original datum -- including header */
-		varatt_external_oid toast_pointer;
+		toast_external_data toast_pointer;
 		struct varatt_indirect redirect_pointer;
 		struct varlena *new_datum = NULL;
 		struct varlena *reconstructed;
@@ -5162,8 +5163,8 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		if (!VARATT_IS_EXTERNAL(varlena))
 			continue;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, varlena);
-		toast_valueid = toast_pointer.va_valueid;
+		toast_external_info_get_data(varlena, &toast_pointer);
+		toast_valueid = toast_pointer.valueid;
 
 		/*
 		 * Check whether the toast tuple changed, replace if so.
@@ -5181,7 +5182,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 
 		free[natt] = true;
 
-		reconstructed = palloc0(toast_pointer.va_rawsize);
+		reconstructed = palloc0(toast_pointer.rawsize);
 
 		ent->reconstructed = reconstructed;
 
@@ -5206,10 +5207,10 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 				   VARSIZE(chunk) - VARHDRSZ);
 			data_done += VARSIZE(chunk) - VARHDRSZ;
 		}
-		Assert(data_done == VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer));
+		Assert(data_done == toast_pointer.extsize);
 
 		/* make sure its marked as compressed or not */
-		if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+		if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 			SET_VARSIZE_COMPRESSED(reconstructed, data_done + VARHDRSZ);
 		else
 			SET_VARSIZE(reconstructed, data_done + VARHDRSZ);
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 95623b1f04c9..266266091631 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -19,6 +19,7 @@
 
 #include "access/detoast.h"
 #include "access/toast_compression.h"
+#include "access/toast_external.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_type.h"
 #include "common/hashfn.h"
@@ -4195,7 +4196,7 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 {
 	int			typlen;
 	struct varlena *attr;
-	varatt_external_oid toast_pointer;
+	Oid8		toast_valueid;
 
 	/* On first call, get the input type's typlen, and save at *fn_extra */
 	if (fcinfo->flinfo->fn_extra == NULL)
@@ -4222,9 +4223,9 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		PG_RETURN_NULL();
 
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_valueid = toast_external_info_get_valueid(attr);
 
-	PG_RETURN_OID(toast_pointer.va_valueid);
+	PG_RETURN_OID(toast_valueid);
 }
 
 /*
diff --git a/src/bin/pg_resetwal/pg_resetwal.c b/src/bin/pg_resetwal/pg_resetwal.c
index 53f9339dffda..b2c4b9db395a 100644
--- a/src/bin/pg_resetwal/pg_resetwal.c
+++ b/src/bin/pg_resetwal/pg_resetwal.c
@@ -733,7 +733,7 @@ GuessControlValues(void)
 	ControlFile.xlog_seg_size = DEFAULT_XLOG_SEG_SIZE;
 	ControlFile.nameDataLen = NAMEDATALEN;
 	ControlFile.indexMaxKeys = INDEX_MAX_KEYS;
-	ControlFile.toast_max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
+	ControlFile.toast_max_chunk_size = TOAST_MAX_CHUNK_SIZE;
 	ControlFile.loblksize = LOBLKSIZE;
 	ControlFile.float8ByVal = true; /* vestigial */
 
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 2d0f40ca2914..8be9f5a6dfb1 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -16,6 +16,7 @@
 #include "access/multixact.h"
 #include "access/relation.h"
 #include "access/table.h"
+#include "access/toast_external.h"
 #include "access/toast_internals.h"
 #include "access/visibilitymap.h"
 #include "access/xact.h"
@@ -73,7 +74,8 @@ typedef enum SkipPages
  */
 typedef struct ToastedAttribute
 {
-	varatt_external_oid toast_pointer;
+	toast_external_data toast_pointer;
+	const toast_external_info *info;
 	BlockNumber blkno;			/* block in main table */
 	OffsetNumber offnum;		/* offset in main table */
 	AttrNumber	attnum;			/* attribute in main table */
@@ -1564,9 +1566,9 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	Oid8		toast_valueid;
 	int32		max_chunk_size;
 
-	toast_valueid = ta->toast_pointer.va_valueid;
+	toast_valueid = ta->toast_pointer.valueid;
 
-	max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
+	max_chunk_size = ta->info->maximum_chunk_size;
 	last_chunk_seq = (extsize - 1) / max_chunk_size;
 
 	/* Sanity-check the sequence number. */
@@ -1672,7 +1674,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	uint16		infomask;
 	Oid8		toast_pointer_valueid;
 	CompactAttribute *thisatt;
-	varatt_external_oid toast_pointer;
+	toast_external_data toast_pointer;
 
 	infomask = ctx->tuphdr->t_infomask;
 	thisatt = TupleDescCompactAttr(RelationGetDescr(ctx->rel), ctx->attnum);
@@ -1778,24 +1780,24 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	/*
 	 * Must copy attr into toast_pointer for alignment considerations
 	 */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-	toast_pointer_valueid = toast_pointer.va_valueid;
+	toast_external_info_get_data(attr, &toast_pointer);
+	toast_pointer_valueid = toast_pointer.valueid;
 
 	/* Toasted attributes too large to be untoasted should never be stored */
-	if (toast_pointer.va_rawsize > VARLENA_SIZE_LIMIT)
+	if (toast_pointer.rawsize > VARLENA_SIZE_LIMIT)
 		report_corruption(ctx,
 						  psprintf("toast value " OID8_FORMAT " rawsize %d exceeds limit %d",
 								   toast_pointer_valueid,
-								   toast_pointer.va_rawsize,
+								   toast_pointer.rawsize,
 								   VARLENA_SIZE_LIMIT));
 
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+	if (TOAST_EXTERNAL_IS_COMPRESSED(toast_pointer))
 	{
 		ToastCompressionId cmid;
 		bool		valid = false;
 
 		/* Compressed attributes should have a valid compression method */
-		cmid = TOAST_COMPRESS_METHOD(&toast_pointer);
+		cmid = toast_pointer.compression_method;
 		switch (cmid)
 		{
 				/* List of all valid compression method IDs */
@@ -1849,7 +1851,8 @@ check_tuple_attribute(HeapCheckContext *ctx)
 
 		ta = palloc0_object(ToastedAttribute);
 
-		VARATT_EXTERNAL_GET_POINTER(ta->toast_pointer, attr);
+		toast_external_info_get_data(attr, &ta->toast_pointer);
+		ta->info = toast_external_get_info(VARTAG_EXTERNAL(attr));
 		ta->blkno = ctx->blkno;
 		ta->offnum = ctx->offnum;
 		ta->attnum = ctx->attnum;
@@ -1876,9 +1879,11 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	int32		expected_chunk_seq = 0;
 	int32		last_chunk_seq;
 	Oid8		toast_valueid;
-	int32		max_chunk_size = TOAST_OID_MAX_CHUNK_SIZE;
+	int32		max_chunk_size;
 
-	extsize = VARATT_EXTERNAL_GET_EXTSIZE(ta->toast_pointer);
+	extsize = ta->toast_pointer.extsize;
+
+	max_chunk_size = ta->info->maximum_chunk_size;
 	last_chunk_seq = (extsize - 1) / max_chunk_size;
 
 	/*
@@ -1887,7 +1892,7 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	ScanKeyInit(&toastkey,
 				(AttrNumber) 1,
 				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(ta->toast_pointer.va_valueid));
+				ObjectIdGetDatum(ta->toast_pointer.valueid));
 
 	/*
 	 * Check if any chunks for this toasted object exist in the toast table,
@@ -1907,7 +1912,7 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	}
 	systable_endscan_ordered(toastscan);
 
-	toast_valueid = ta->toast_pointer.va_valueid;
+	toast_valueid = ta->toast_pointer.valueid;
 
 	if (!found_toasttup)
 		report_toast_corruption(ctx, ta,
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 09e7f1d420ed..f12c7d52db08 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -4198,6 +4198,8 @@ timeout_params
 timerCA
 tlist_vinfo
 toast_compress_header
+toast_external_data
+toast_external_info
 tokenize_error_callback_arg
 transferMode
 transfer_thread_arg
-- 
2.51.0

v10-0005-Move-static-inline-routines-of-varatt_external_o.patchtext/x-diff; charset=us-asciiDownload
From 1b0fb58002f51ba4340c0cd3f17cc54b9e5f0bf7 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 8 Aug 2025 15:40:04 +0900
Subject: [PATCH v10 05/14] Move static inline routines of varatt_external_oid
 to toast_external.c

This isolates most of the knowledge of varatt_external_oid into the
local area where it is manipulated through the toast_external transition
type, with the backend code not requiring it.  Extension code should not
need it either, as toast_external should be the layer to use when
looking at external on-dist TOAST varlenas.
---
 src/include/varatt.h                       | 31 -----------------
 src/backend/access/common/toast_external.c | 40 ++++++++++++++++++++--
 2 files changed, 37 insertions(+), 34 deletions(-)

diff --git a/src/include/varatt.h b/src/include/varatt.h
index 0807b371c87f..35baefd2a748 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -513,22 +513,6 @@ VARDATA_COMPRESSED_GET_COMPRESS_METHOD(const void *PTR)
 	return ((varattrib_4b *) PTR)->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS;
 }
 
-/*
- * Same for external Datums; but note argument is a struct
- * varatt_external_oid.
- */
-static inline Size
-VARATT_EXTERNAL_GET_EXTSIZE(varatt_external_oid toast_pointer)
-{
-	return toast_pointer.va_extinfo & VARLENA_EXTSIZE_MASK;
-}
-
-static inline uint32
-VARATT_EXTERNAL_GET_COMPRESS_METHOD(varatt_external_oid toast_pointer)
-{
-	return toast_pointer.va_extinfo >> VARLENA_EXTSIZE_BITS;
-}
-
 /* Set size and compress method of an externally-stored varlena datum */
 /* This has to remain a macro; beware multiple evaluations! */
 #define VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, len, cm) \
@@ -538,19 +522,4 @@ VARATT_EXTERNAL_GET_COMPRESS_METHOD(varatt_external_oid toast_pointer)
 		((toast_pointer).va_extinfo = \
 			(len) | ((uint32) (cm) << VARLENA_EXTSIZE_BITS)); \
 	} while (0)
-
-/*
- * Testing whether an externally-stored value is compressed now requires
- * comparing size stored in va_extinfo (the actual length of the external data)
- * to rawsize (the original uncompressed datum's size).  The latter includes
- * VARHDRSZ overhead, the former doesn't.  We never use compression unless it
- * actually saves space, so we expect either equality or less-than.
- */
-static inline bool
-VARATT_EXTERNAL_IS_COMPRESSED(varatt_external_oid toast_pointer)
-{
-	return VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) <
-		(Size) (toast_pointer.va_rawsize - VARHDRSZ);
-}
-
 #endif
diff --git a/src/backend/access/common/toast_external.c b/src/backend/access/common/toast_external.c
index 2154152b8bfb..4c500720e0d1 100644
--- a/src/backend/access/common/toast_external.c
+++ b/src/backend/access/common/toast_external.c
@@ -26,6 +26,40 @@ static void ondisk_oid_to_external_data(struct varlena *attr,
 										toast_external_data *data);
 static struct varlena *ondisk_oid_create_external_data(toast_external_data data);
 
+/*
+ * Decompressed size of an on-disk varlena; but note argument is a struct
+ * varatt_external_oid.
+ */
+static inline Size
+varatt_external_oid_get_extsize(varatt_external_oid toast_pointer)
+{
+	return toast_pointer.va_extinfo & VARLENA_EXTSIZE_MASK;
+}
+
+/*
+ * Compression method of an on-disk varlena; but note argument is a struct
+ *  varatt_external_oid.
+ */
+static inline uint32
+varatt_external_oid_get_compress_method(varatt_external_oid toast_pointer)
+{
+	return toast_pointer.va_extinfo >> VARLENA_EXTSIZE_BITS;
+}
+
+/*
+ * Testing whether an externally-stored TOAST value is compressed now requires
+ * comparing size stored in va_extinfo (the actual length of the external data)
+ * to rawsize (the original uncompressed datum's size).  The latter includes
+ * VARHDRSZ overhead, the former doesn't.  We never use compression unless it
+ * actually saves space, so we expect either equality or less-than.
+ */
+static inline bool
+varatt_external_oid_is_compressed(varatt_external_oid toast_pointer)
+{
+	return varatt_external_oid_get_extsize(toast_pointer) <
+		(Size) (toast_pointer.va_rawsize - VARHDRSZ);
+}
+
 /*
  * Size of an EXTERNAL datum that contains a standard TOAST pointer (OID
  * value).
@@ -146,10 +180,10 @@ ondisk_oid_to_external_data(struct varlena *attr, toast_external_data *data)
 	 * External size and compression methods are stored in the same field,
 	 * extract.
 	 */
-	if (VARATT_EXTERNAL_IS_COMPRESSED(external))
+	if (varatt_external_oid_is_compressed(external))
 	{
-		data->extsize = VARATT_EXTERNAL_GET_EXTSIZE(external);
-		data->compression_method = VARATT_EXTERNAL_GET_COMPRESS_METHOD(external);
+		data->extsize = varatt_external_oid_get_extsize(external);
+		data->compression_method = varatt_external_oid_get_compress_method(external);
 	}
 	else
 	{
-- 
2.51.0

v10-0006-Split-VARATT_EXTERNAL_GET_POINTER-for-indirect-a.patchtext/x-diff; charset=us-asciiDownload
From fa0fb81ecc0783246e68457701275f608108a05a Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 13 Aug 2025 19:38:50 +0900
Subject: [PATCH v10 06/14] Split VARATT_EXTERNAL_GET_POINTER for indirect and
 OID TOAST pointers

VARATT_EXTERNAL_GET_POINTER() is renamed to
VARATT_INDIRECT_GET_POINTER() with the external on-disk TOAST pointers
for OID values being now located within toast_external.c, splitting both
concepts completely.
---
 src/include/access/detoast.h               | 16 ++++++++--------
 src/backend/access/common/detoast.c        | 10 +++++-----
 src/backend/access/common/toast_external.c | 21 ++++++++++++++++++++-
 src/backend/utils/adt/expandeddatum.c      |  2 +-
 4 files changed, 34 insertions(+), 15 deletions(-)

diff --git a/src/include/access/detoast.h b/src/include/access/detoast.h
index 630f8c7e04e8..3b4d45c74d62 100644
--- a/src/include/access/detoast.h
+++ b/src/include/access/detoast.h
@@ -13,17 +13,17 @@
 #define DETOAST_H
 
 /*
- * Macro to fetch the possibly-unaligned contents of an EXTERNAL datum
- * into a local "varatt_external_*" toast pointer, as supported
- * in toast_external.h and varatt.h.  This should be just a memcpy, but
- * some versions of gcc seem to produce broken code that assumes the datum
- * contents are aligned.  Introducing an explicit intermediate
- * "varattrib_1b_e *" variable seems to fix it.
+ * Macro to fetch the possibly-unaligned contents of an indirect datum
+ * into a local "varatt_indirect" toast pointer, as supported
+ * in varatt.h.  This should be just a memcpy, but some versions of gcc
+ * seem to produce broken code that assumes the datum contents are aligned.
+ * Introducing an explicit intermediate "varattrib_1b_e *" variable seems
+ * to fix it.
  */
-#define VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr) \
+#define VARATT_INDIRECT_GET_POINTER(toast_pointer, attr) \
 do { \
 	varattrib_1b_e *attre = (varattrib_1b_e *) (attr); \
-	Assert(VARATT_IS_EXTERNAL(attre)); \
+	Assert(VARATT_IS_EXTERNAL_INDIRECT(attre)); \
 	Assert(VARSIZE_EXTERNAL(attre) == sizeof(toast_pointer) + VARHDRSZ_EXTERNAL); \
 	memcpy(&(toast_pointer), VARDATA_EXTERNAL(attre), sizeof(toast_pointer)); \
 } while (0)
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index d24a84d6509a..b21fad2be26d 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -61,7 +61,7 @@ detoast_external_attr(struct varlena *attr)
 		 */
 		struct varatt_indirect redirect;
 
-		VARATT_EXTERNAL_GET_POINTER(redirect, attr);
+		VARATT_INDIRECT_GET_POINTER(redirect, attr);
 		attr = (struct varlena *) redirect.pointer;
 
 		/* nested indirect Datums aren't allowed */
@@ -138,7 +138,7 @@ detoast_attr(struct varlena *attr)
 		 */
 		struct varatt_indirect redirect;
 
-		VARATT_EXTERNAL_GET_POINTER(redirect, attr);
+		VARATT_INDIRECT_GET_POINTER(redirect, attr);
 		attr = (struct varlena *) redirect.pointer;
 
 		/* nested indirect Datums aren't allowed */
@@ -268,7 +268,7 @@ detoast_attr_slice(struct varlena *attr,
 	{
 		struct varatt_indirect redirect;
 
-		VARATT_EXTERNAL_GET_POINTER(redirect, attr);
+		VARATT_INDIRECT_GET_POINTER(redirect, attr);
 
 		/* nested indirect Datums aren't allowed */
 		Assert(!VARATT_IS_EXTERNAL_INDIRECT(redirect.pointer));
@@ -561,7 +561,7 @@ toast_raw_datum_size(Datum value)
 	{
 		struct varatt_indirect toast_pointer;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		VARATT_INDIRECT_GET_POINTER(toast_pointer, attr);
 
 		/* nested indirect Datums aren't allowed */
 		Assert(!VARATT_IS_EXTERNAL_INDIRECT(toast_pointer.pointer));
@@ -618,7 +618,7 @@ toast_datum_size(Datum value)
 	{
 		struct varatt_indirect toast_pointer;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		VARATT_INDIRECT_GET_POINTER(toast_pointer, attr);
 
 		/* nested indirect Datums aren't allowed */
 		Assert(!VARATT_IS_EXTERNAL_INDIRECT(attr));
diff --git a/src/backend/access/common/toast_external.c b/src/backend/access/common/toast_external.c
index 4c500720e0d1..e2f0a9dc1c50 100644
--- a/src/backend/access/common/toast_external.c
+++ b/src/backend/access/common/toast_external.c
@@ -26,6 +26,25 @@ static void ondisk_oid_to_external_data(struct varlena *attr,
 										toast_external_data *data);
 static struct varlena *ondisk_oid_create_external_data(toast_external_data data);
 
+/*
+ * Fetch the possibly-unaligned contents of an on-disk external TOAST with
+ * OID values into a local "varatt_external_oid" pointer.
+ *
+ * This should be just a memcpy, but some versions of gcc seem to produce
+ * broken code that assumes the datum contents are aligned.  Introducing
+ * an explicit intermediate "varattrib_1b_e *" variable seems to fix it.
+ */
+static inline void
+varatt_external_oid_get_pointer(varatt_external_oid *toast_pointer,
+								struct varlena *attr)
+{
+	varattrib_1b_e *attre = (varattrib_1b_e *) attr;
+
+	Assert(VARATT_IS_EXTERNAL_ONDISK_OID(attre));
+	Assert(VARSIZE_EXTERNAL(attre) == sizeof(varatt_external_oid) + VARHDRSZ_EXTERNAL);
+	memcpy(toast_pointer, VARDATA_EXTERNAL(attre), sizeof(varatt_external_oid));
+}
+
 /*
  * Decompressed size of an on-disk varlena; but note argument is a struct
  * varatt_external_oid.
@@ -173,7 +192,7 @@ ondisk_oid_to_external_data(struct varlena *attr, toast_external_data *data)
 {
 	varatt_external_oid external;
 
-	VARATT_EXTERNAL_GET_POINTER(external, attr);
+	varatt_external_oid_get_pointer(&external, attr);
 	data->rawsize = external.va_rawsize;
 
 	/*
diff --git a/src/backend/utils/adt/expandeddatum.c b/src/backend/utils/adt/expandeddatum.c
index b7d600384577..828b17f097e7 100644
--- a/src/backend/utils/adt/expandeddatum.c
+++ b/src/backend/utils/adt/expandeddatum.c
@@ -23,7 +23,7 @@
  * Given a Datum that is an expanded-object reference, extract the pointer.
  *
  * This is a bit tedious since the pointer may not be properly aligned;
- * compare VARATT_EXTERNAL_GET_POINTER().
+ * compare VARATT_INDIRECT_GET_POINTER().
  */
 ExpandedObjectHeader *
 DatumGetEOHP(Datum d)
-- 
2.51.0

v10-0001-Refactor-some-TOAST-value-ID-code-to-use-Oid8-in.patchtext/x-diff; charset=us-asciiDownload
From 7d6204d120bf636a4b103d9081e0f3142a301002 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 13 Aug 2025 17:26:36 +0900
Subject: [PATCH v10 01/14] Refactor some TOAST value ID code to use Oid8
 instead of Oid

This change is a mechanical switch to change most of the code paths that
assume TOAST value IDs to be Oids to become Oid8, easing an upcoming
change to allow larger TOAST values, at 8 bytes.

The areas touched are related to table AM, amcheck and logical
decoding's reorder buffer.  A good chunk of the changes involve
switching printf() markers from %u to OID8_FORMAT.
---
 src/include/access/heaptoast.h                |  2 +-
 src/include/access/tableam.h                  |  4 +-
 src/backend/access/common/toast_internals.c   |  8 +--
 src/backend/access/heap/heaptoast.c           | 12 ++--
 .../replication/logical/reorderbuffer.c       | 14 +++--
 contrib/amcheck/verify_heapam.c               | 56 +++++++++++--------
 6 files changed, 53 insertions(+), 43 deletions(-)

diff --git a/src/include/access/heaptoast.h b/src/include/access/heaptoast.h
index 21baa0834b75..893be58687a1 100644
--- a/src/include/access/heaptoast.h
+++ b/src/include/access/heaptoast.h
@@ -142,7 +142,7 @@ extern HeapTuple toast_build_flattened_tuple(TupleDesc tupleDesc,
  *	Fetch a slice from a toast value stored in a heap table.
  * ----------
  */
-extern void heap_fetch_toast_slice(Relation toastrel, Oid valueid,
+extern void heap_fetch_toast_slice(Relation toastrel, Oid8 valueid,
 								   int32 attrsize, int32 sliceoffset,
 								   int32 slicelength, struct varlena *result);
 
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index e2ec5289d4da..38d343f2224e 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -746,7 +746,7 @@ typedef struct TableAmRoutine
 	 * table implemented by this AM.  See table_relation_fetch_toast_slice()
 	 * for more details.
 	 */
-	void		(*relation_fetch_toast_slice) (Relation toastrel, Oid valueid,
+	void		(*relation_fetch_toast_slice) (Relation toastrel, Oid8 valueid,
 											   int32 attrsize,
 											   int32 sliceoffset,
 											   int32 slicelength,
@@ -1892,7 +1892,7 @@ table_relation_toast_am(Relation rel)
  * stored.
  */
 static inline void
-table_relation_fetch_toast_slice(Relation toastrel, Oid valueid,
+table_relation_fetch_toast_slice(Relation toastrel, Oid8 valueid,
 								 int32 attrsize, int32 sliceoffset,
 								 int32 slicelength, struct varlena *result)
 {
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 6836786fd056..fd195f7d9c4f 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -26,8 +26,8 @@
 #include "utils/rel.h"
 #include "utils/snapmgr.h"
 
-static bool toastrel_valueid_exists(Relation toastrel, Oid valueid);
-static bool toastid_valueid_exists(Oid toastrelid, Oid valueid);
+static bool toastrel_valueid_exists(Relation toastrel, Oid8 valueid);
+static bool toastid_valueid_exists(Oid toastrelid, Oid8 valueid);
 
 /* ----------
  * toast_compress_datum -
@@ -447,7 +447,7 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
  * ----------
  */
 static bool
-toastrel_valueid_exists(Relation toastrel, Oid valueid)
+toastrel_valueid_exists(Relation toastrel, Oid8 valueid)
 {
 	bool		result = false;
 	ScanKeyData toastkey;
@@ -495,7 +495,7 @@ toastrel_valueid_exists(Relation toastrel, Oid valueid)
  * ----------
  */
 static bool
-toastid_valueid_exists(Oid toastrelid, Oid valueid)
+toastid_valueid_exists(Oid toastrelid, Oid8 valueid)
 {
 	bool		result;
 	Relation	toastrel;
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index e28fe47a4498..4569864f142a 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -623,7 +623,7 @@ toast_build_flattened_tuple(TupleDesc tupleDesc,
  * result is the varlena into which the results should be written.
  */
 void
-heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
+heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 					   int32 sliceoffset, int32 slicelength,
 					   struct varlena *result)
 {
@@ -725,7 +725,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
 		else
 		{
 			/* should never happen */
-			elog(ERROR, "found toasted toast chunk for toast value %u in %s",
+			elog(ERROR, "found toasted toast chunk for toast value " OID8_FORMAT " in %s",
 				 valueid, RelationGetRelationName(toastrel));
 			chunksize = 0;		/* keep compiler quiet */
 			chunkdata = NULL;
@@ -737,13 +737,13 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
 		if (curchunk != expectedchunk)
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
-					 errmsg_internal("unexpected chunk number %d (expected %d) for toast value %u in %s",
+					 errmsg_internal("unexpected chunk number %d (expected %d) for toast value " OID8_FORMAT " in %s",
 									 curchunk, expectedchunk, valueid,
 									 RelationGetRelationName(toastrel))));
 		if (curchunk > endchunk)
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
-					 errmsg_internal("unexpected chunk number %d (out of range %d..%d) for toast value %u in %s",
+					 errmsg_internal("unexpected chunk number %d (out of range %d..%d) for toast value " OID8_FORMAT " in %s",
 									 curchunk,
 									 startchunk, endchunk, valueid,
 									 RelationGetRelationName(toastrel))));
@@ -752,7 +752,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
 		if (chunksize != expected_size)
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
-					 errmsg_internal("unexpected chunk size %d (expected %d) in chunk %d of %d for toast value %u in %s",
+					 errmsg_internal("unexpected chunk size %d (expected %d) in chunk %d of %d for toast value " OID8_FORMAT " in %s",
 									 chunksize, expected_size,
 									 curchunk, totalchunks, valueid,
 									 RelationGetRelationName(toastrel))));
@@ -781,7 +781,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
 	if (expectedchunk != (endchunk + 1))
 		ereport(ERROR,
 				(errcode(ERRCODE_DATA_CORRUPTED),
-				 errmsg_internal("missing chunk number %d for toast value %u in %s",
+				 errmsg_internal("missing chunk number %d for toast value " OID8_FORMAT " in %s",
 								 expectedchunk, valueid,
 								 RelationGetRelationName(toastrel))));
 
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index a0293f6ec7c5..965091c10627 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -176,7 +176,7 @@ typedef struct ReorderBufferIterTXNState
 /* toast datastructures */
 typedef struct ReorderBufferToastEnt
 {
-	Oid			chunk_id;		/* toast_table.chunk_id */
+	Oid8		chunk_id;		/* toast_table.chunk_id */
 	int32		last_chunk_seq; /* toast_table.chunk_seq of the last chunk we
 								 * have seen */
 	Size		num_chunks;		/* number of chunks we've already seen */
@@ -4978,7 +4978,7 @@ ReorderBufferToastInitHash(ReorderBuffer *rb, ReorderBufferTXN *txn)
 
 	Assert(txn->toast_hash == NULL);
 
-	hash_ctl.keysize = sizeof(Oid);
+	hash_ctl.keysize = sizeof(Oid8);
 	hash_ctl.entrysize = sizeof(ReorderBufferToastEnt);
 	hash_ctl.hcxt = rb->context;
 	txn->toast_hash = hash_create("ReorderBufferToastHash", 5, &hash_ctl,
@@ -5002,7 +5002,7 @@ ReorderBufferToastAppendChunk(ReorderBuffer *rb, ReorderBufferTXN *txn,
 	bool		isnull;
 	Pointer		chunk;
 	TupleDesc	desc = RelationGetDescr(relation);
-	Oid			chunk_id;
+	Oid8		chunk_id;
 	int32		chunk_seq;
 
 	if (txn->toast_hash == NULL)
@@ -5029,11 +5029,11 @@ ReorderBufferToastAppendChunk(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		dlist_init(&ent->chunks);
 
 		if (chunk_seq != 0)
-			elog(ERROR, "got sequence entry %d for toast chunk %u instead of seq 0",
+			elog(ERROR, "got sequence entry %d for toast chunk " OID8_FORMAT " instead of seq 0",
 				 chunk_seq, chunk_id);
 	}
 	else if (found && chunk_seq != ent->last_chunk_seq + 1)
-		elog(ERROR, "got sequence entry %d for toast chunk %u instead of seq %d",
+		elog(ERROR, "got sequence entry %d for toast chunk " OID8_FORMAT " instead of seq %d",
 			 chunk_seq, chunk_id, ent->last_chunk_seq + 1);
 
 	chunk = DatumGetPointer(fastgetattr(newtup, 3, desc, &isnull));
@@ -5142,6 +5142,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		struct varlena *reconstructed;
 		dlist_iter	it;
 		Size		data_done = 0;
+		Oid8		toast_valueid;
 
 		if (attr->attisdropped)
 			continue;
@@ -5162,13 +5163,14 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 			continue;
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, varlena);
+		toast_valueid = toast_pointer.va_valueid;
 
 		/*
 		 * Check whether the toast tuple changed, replace if so.
 		 */
 		ent = (ReorderBufferToastEnt *)
 			hash_search(txn->toast_hash,
-						&toast_pointer.va_valueid,
+						&toast_valueid,
 						HASH_FIND,
 						NULL);
 		if (ent == NULL)
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 30c2f5831731..f8da89cc3d66 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1561,6 +1561,9 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	bool		isnull;
 	int32		chunksize;
 	int32		expected_size;
+	Oid8		toast_valueid;
+
+	toast_valueid = ta->toast_pointer.va_valueid;
 
 	/* Sanity-check the sequence number. */
 	chunk_seq = DatumGetInt32(fastgetattr(toasttup, 2,
@@ -1568,16 +1571,16 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	if (isnull)
 	{
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u has toast chunk with null sequence number",
-										 ta->toast_pointer.va_valueid));
+								psprintf("toast value " OID8_FORMAT " has toast chunk with null sequence number",
+										 toast_valueid));
 		return;
 	}
 	if (chunk_seq != *expected_chunk_seq)
 	{
 		/* Either the TOAST index is corrupt, or we don't have all chunks. */
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u index scan returned chunk %d when expecting chunk %d",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value " OID8_FORMAT " index scan returned chunk %d when expecting chunk %d",
+										 toast_valueid,
 										 chunk_seq, *expected_chunk_seq));
 	}
 	*expected_chunk_seq = chunk_seq + 1;
@@ -1588,8 +1591,8 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	if (isnull)
 	{
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u chunk %d has null data",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value " OID8_FORMAT " chunk %d has null data",
+										 toast_valueid,
 										 chunk_seq));
 		return;
 	}
@@ -1608,8 +1611,8 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 		uint32		header = ((varattrib_4b *) chunk)->va_4byte.va_header;
 
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u chunk %d has invalid varlena header %0x",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value " OID8_FORMAT " chunk %d has invalid varlena header %0x",
+										 toast_valueid,
 										 chunk_seq, header));
 		return;
 	}
@@ -1620,8 +1623,8 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 	if (chunk_seq > last_chunk_seq)
 	{
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u chunk %d follows last expected chunk %d",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value " OID8_FORMAT " chunk %d follows last expected chunk %d",
+										 toast_valueid,
 										 chunk_seq, last_chunk_seq));
 		return;
 	}
@@ -1631,8 +1634,8 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 
 	if (chunksize != expected_size)
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u chunk %d has size %u, but expected size %u",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value " OID8_FORMAT " chunk %d has size %u, but expected size %u",
+										 toast_valueid,
 										 chunk_seq, chunksize, expected_size));
 }
 
@@ -1663,6 +1666,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	struct varlena *attr;
 	char	   *tp;				/* pointer to the tuple data */
 	uint16		infomask;
+	Oid8		toast_pointer_valueid;
 	CompactAttribute *thisatt;
 	struct varatt_external toast_pointer;
 
@@ -1771,12 +1775,13 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	 * Must copy attr into toast_pointer for alignment considerations
 	 */
 	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	toast_pointer_valueid = toast_pointer.va_valueid;
 
 	/* Toasted attributes too large to be untoasted should never be stored */
 	if (toast_pointer.va_rawsize > VARLENA_SIZE_LIMIT)
 		report_corruption(ctx,
-						  psprintf("toast value %u rawsize %d exceeds limit %d",
-								   toast_pointer.va_valueid,
+						  psprintf("toast value " OID8_FORMAT " rawsize %d exceeds limit %d",
+								   toast_pointer_valueid,
 								   toast_pointer.va_rawsize,
 								   VARLENA_SIZE_LIMIT));
 
@@ -1803,16 +1808,16 @@ check_tuple_attribute(HeapCheckContext *ctx)
 		}
 		if (!valid)
 			report_corruption(ctx,
-							  psprintf("toast value %u has invalid compression method id %d",
-									   toast_pointer.va_valueid, cmid));
+							  psprintf("toast value " OID8_FORMAT " has invalid compression method id %d",
+									   toast_pointer_valueid, cmid));
 	}
 
 	/* The tuple header better claim to contain toasted values */
 	if (!(infomask & HEAP_HASEXTERNAL))
 	{
 		report_corruption(ctx,
-						  psprintf("toast value %u is external but tuple header flag HEAP_HASEXTERNAL not set",
-								   toast_pointer.va_valueid));
+						  psprintf("toast value " OID8_FORMAT " is external but tuple header flag HEAP_HASEXTERNAL not set",
+								   toast_pointer_valueid));
 		return true;
 	}
 
@@ -1820,8 +1825,8 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	if (!ctx->rel->rd_rel->reltoastrelid)
 	{
 		report_corruption(ctx,
-						  psprintf("toast value %u is external but relation has no toast relation",
-								   toast_pointer.va_valueid));
+						  psprintf("toast value " OID8_FORMAT " is external but relation has no toast relation",
+								   toast_pointer_valueid));
 		return true;
 	}
 
@@ -1866,6 +1871,7 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	uint32		extsize;
 	int32		expected_chunk_seq = 0;
 	int32		last_chunk_seq;
+	Oid8		toast_valueid;
 
 	extsize = VARATT_EXTERNAL_GET_EXTSIZE(ta->toast_pointer);
 	last_chunk_seq = (extsize - 1) / TOAST_MAX_CHUNK_SIZE;
@@ -1896,14 +1902,16 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	}
 	systable_endscan_ordered(toastscan);
 
+	toast_valueid = ta->toast_pointer.va_valueid;
+
 	if (!found_toasttup)
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u not found in toast table",
-										 ta->toast_pointer.va_valueid));
+								psprintf("toast value " OID8_FORMAT " not found in toast table",
+										 toast_valueid));
 	else if (expected_chunk_seq <= last_chunk_seq)
 		report_toast_corruption(ctx, ta,
-								psprintf("toast value %u was expected to end at chunk %d, but ended while expecting chunk %d",
-										 ta->toast_pointer.va_valueid,
+								psprintf("toast value " OID8_FORMAT " was expected to end at chunk %d, but ended while expecting chunk %d",
+										 toast_valueid,
 										 last_chunk_seq, expected_chunk_seq));
 }
 
-- 
2.51.0

v10-0002-Minimize-footprint-of-TOAST_MAX_CHUNK_SIZE-in-he.patchtext/x-diff; charset=us-asciiDownload
From 6c7f7d352f54e9d2ef2f9b9c958f79aca25e8a09 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 13 Aug 2025 17:40:13 +0900
Subject: [PATCH v10 02/14] Minimize footprint of TOAST_MAX_CHUNK_SIZE in heap
 and amcheck

This eases a follow-up change to support 8-byte TOAST value IDs, as the
maximum chunk size allowed for a single chunk of TOASTed data depends on
the size of the value ID.
---
 src/backend/access/heap/heaptoast.c | 20 ++++++++++++--------
 contrib/amcheck/verify_heapam.c     | 13 +++++++++----
 2 files changed, 21 insertions(+), 12 deletions(-)

diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index 4569864f142a..9a48b1fcc89d 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -634,11 +634,12 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 	SysScanDesc toastscan;
 	HeapTuple	ttup;
 	int32		expectedchunk;
-	int32		totalchunks = ((attrsize - 1) / TOAST_MAX_CHUNK_SIZE) + 1;
+	int32		totalchunks;
 	int			startchunk;
 	int			endchunk;
 	int			num_indexes;
 	int			validIndex;
+	int32		max_chunk_size;
 
 	/* Look for the valid index of toast relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -646,8 +647,11 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 									&toastidxs,
 									&num_indexes);
 
-	startchunk = sliceoffset / TOAST_MAX_CHUNK_SIZE;
-	endchunk = (sliceoffset + slicelength - 1) / TOAST_MAX_CHUNK_SIZE;
+	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+
+	totalchunks = ((attrsize - 1) / max_chunk_size) + 1;
+	startchunk = sliceoffset / max_chunk_size;
+	endchunk = (sliceoffset + slicelength - 1) / max_chunk_size;
 	Assert(endchunk <= totalchunks);
 
 	/* Set up a scan key to fetch from the index. */
@@ -747,8 +751,8 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 									 curchunk,
 									 startchunk, endchunk, valueid,
 									 RelationGetRelationName(toastrel))));
-		expected_size = curchunk < totalchunks - 1 ? TOAST_MAX_CHUNK_SIZE
-			: attrsize - ((totalchunks - 1) * TOAST_MAX_CHUNK_SIZE);
+		expected_size = curchunk < totalchunks - 1 ? max_chunk_size
+			: attrsize - ((totalchunks - 1) * max_chunk_size);
 		if (chunksize != expected_size)
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
@@ -763,12 +767,12 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 		chcpystrt = 0;
 		chcpyend = chunksize - 1;
 		if (curchunk == startchunk)
-			chcpystrt = sliceoffset % TOAST_MAX_CHUNK_SIZE;
+			chcpystrt = sliceoffset % max_chunk_size;
 		if (curchunk == endchunk)
-			chcpyend = (sliceoffset + slicelength - 1) % TOAST_MAX_CHUNK_SIZE;
+			chcpyend = (sliceoffset + slicelength - 1) % max_chunk_size;
 
 		memcpy(VARDATA(result) +
-			   (curchunk * TOAST_MAX_CHUNK_SIZE - sliceoffset) + chcpystrt,
+			   (curchunk * max_chunk_size - sliceoffset) + chcpystrt,
 			   chunkdata + chcpystrt,
 			   (chcpyend - chcpystrt) + 1);
 
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index f8da89cc3d66..0cbd9d140762 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1556,15 +1556,19 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 				  uint32 extsize)
 {
 	int32		chunk_seq;
-	int32		last_chunk_seq = (extsize - 1) / TOAST_MAX_CHUNK_SIZE;
+	int32		last_chunk_seq;
 	Pointer		chunk;
 	bool		isnull;
 	int32		chunksize;
 	int32		expected_size;
 	Oid8		toast_valueid;
+	int32		max_chunk_size;
 
 	toast_valueid = ta->toast_pointer.va_valueid;
 
+	max_chunk_size = TOAST_MAX_CHUNK_SIZE;
+	last_chunk_seq = (extsize - 1) / max_chunk_size;
+
 	/* Sanity-check the sequence number. */
 	chunk_seq = DatumGetInt32(fastgetattr(toasttup, 2,
 										  ctx->toast_rel->rd_att, &isnull));
@@ -1629,8 +1633,8 @@ check_toast_tuple(HeapTuple toasttup, HeapCheckContext *ctx,
 		return;
 	}
 
-	expected_size = chunk_seq < last_chunk_seq ? TOAST_MAX_CHUNK_SIZE
-		: extsize - (last_chunk_seq * TOAST_MAX_CHUNK_SIZE);
+	expected_size = chunk_seq < last_chunk_seq ? max_chunk_size
+		: extsize - (last_chunk_seq * max_chunk_size);
 
 	if (chunksize != expected_size)
 		report_toast_corruption(ctx, ta,
@@ -1872,9 +1876,10 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	int32		expected_chunk_seq = 0;
 	int32		last_chunk_seq;
 	Oid8		toast_valueid;
+	int32		max_chunk_size = TOAST_MAX_CHUNK_SIZE;
 
 	extsize = VARATT_EXTERNAL_GET_EXTSIZE(ta->toast_pointer);
-	last_chunk_seq = (extsize - 1) / TOAST_MAX_CHUNK_SIZE;
+	last_chunk_seq = (extsize - 1) / max_chunk_size;
 
 	/*
 	 * Setup a scan key to find chunks in toast table with matching va_valueid
-- 
2.51.0

v10-0007-Switch-pg_column_toast_chunk_id-return-value-fro.patchtext/x-diff; charset=us-asciiDownload
From 2d7989be8b3905f0baa67fcd5ae48e7fa14978ff Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 13 Aug 2025 19:55:39 +0900
Subject: [PATCH v10 07/14] Switch pg_column_toast_chunk_id() return value from
 oid to oid8

This is required for a follow-up patch that will add support for 8-byte
TOAST values, with this function being changed so as it is able to
support the largest TOAST value type available.

XXX: Bump catalog version.
---
 src/include/catalog/pg_proc.dat              | 2 +-
 src/backend/utils/adt/varlena.c              | 2 +-
 src/test/regress/expected/misc_functions.out | 2 +-
 src/test/regress/sql/misc_functions.sql      | 2 +-
 doc/src/sgml/func/func-admin.sgml            | 2 +-
 5 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 2ac69bf2df55..f0101a869c86 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7771,7 +7771,7 @@
   proargtypes => 'any', prosrc => 'pg_column_compression' },
 { oid => '6316', descr => 'chunk ID of on-disk TOASTed value',
   proname => 'pg_column_toast_chunk_id', provolatile => 's',
-  prorettype => 'oid', proargtypes => 'any',
+  prorettype => 'oid8', proargtypes => 'any',
   prosrc => 'pg_column_toast_chunk_id' },
 { oid => '2322',
   descr => 'total disk space usage for the specified tablespace',
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 266266091631..f81481180509 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -4225,7 +4225,7 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 
 	toast_valueid = toast_external_info_get_valueid(attr);
 
-	PG_RETURN_OID(toast_valueid);
+	PG_RETURN_OID8(toast_valueid);
 }
 
 /*
diff --git a/src/test/regress/expected/misc_functions.out b/src/test/regress/expected/misc_functions.out
index 6c03b1a79d75..be0b1554f07e 100644
--- a/src/test/regress/expected/misc_functions.out
+++ b/src/test/regress/expected/misc_functions.out
@@ -962,7 +962,7 @@ SELECT t.relname AS toastrel FROM pg_class c
   WHERE c.relname = 'test_chunk_id'
 \gset
 SELECT pg_column_toast_chunk_id(a) IS NULL,
-  pg_column_toast_chunk_id(b) IN (SELECT chunk_id FROM pg_toast.:toastrel)
+  pg_column_toast_chunk_id(b) IN (SELECT chunk_id::oid8 FROM pg_toast.:toastrel)
   FROM test_chunk_id;
  ?column? | ?column? 
 ----------+----------
diff --git a/src/test/regress/sql/misc_functions.sql b/src/test/regress/sql/misc_functions.sql
index 35b7983996c4..f28fed6b52ae 100644
--- a/src/test/regress/sql/misc_functions.sql
+++ b/src/test/regress/sql/misc_functions.sql
@@ -440,7 +440,7 @@ SELECT t.relname AS toastrel FROM pg_class c
   WHERE c.relname = 'test_chunk_id'
 \gset
 SELECT pg_column_toast_chunk_id(a) IS NULL,
-  pg_column_toast_chunk_id(b) IN (SELECT chunk_id FROM pg_toast.:toastrel)
+  pg_column_toast_chunk_id(b) IN (SELECT chunk_id::oid8 FROM pg_toast.:toastrel)
   FROM test_chunk_id;
 DROP TABLE test_chunk_id;
 DROP FUNCTION explain_mask_costs(text, bool, bool, bool, bool);
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 2896cd9e4290..41bbd7cdef0d 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1588,7 +1588,7 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
          <primary>pg_column_toast_chunk_id</primary>
         </indexterm>
         <function>pg_column_toast_chunk_id</function> ( <type>"any"</type> )
-        <returnvalue>oid</returnvalue>
+        <returnvalue>oid8</returnvalue>
        </para>
        <para>
         Shows the <structfield>chunk_id</structfield> of an on-disk
-- 
2.51.0

v10-0008-Add-catcache-support-for-OID8OID.patchtext/x-diff; charset=us-asciiDownload
From a1cab2e2a87f34dd996def3ee8f1fbfb39d5b1ab Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 13 Aug 2025 20:00:30 +0900
Subject: [PATCH v10 08/14] Add catcache support for OID8OID

This is required to be able to do catalog cache lookups of oid8 fields
for toast values of the same type.
---
 src/backend/utils/cache/catcache.c | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index 9cfda91a877c..5ffe1e38b326 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -240,6 +240,18 @@ int4hashfast(Datum datum)
 	return murmurhash32((int32) DatumGetInt32(datum));
 }
 
+static bool
+oid8eqfast(Datum a, Datum b)
+{
+	return DatumGetObjectId8(a) == DatumGetObjectId8(b);
+}
+
+static uint32
+oid8hashfast(Datum datum)
+{
+	return murmurhash64(DatumGetObjectId8(datum));
+}
+
 static bool
 texteqfast(Datum a, Datum b)
 {
@@ -300,6 +312,11 @@ GetCCHashEqFuncs(Oid keytype, CCHashFN *hashfunc, RegProcedure *eqfunc, CCFastEq
 			*fasteqfunc = int4eqfast;
 			*eqfunc = F_INT4EQ;
 			break;
+		case OID8OID:
+			*hashfunc = oid8hashfast;
+			*fasteqfunc = oid8eqfast;
+			*eqfunc = F_OID8EQ;
+			break;
 		case TEXTOID:
 			*hashfunc = texthashfast;
 			*fasteqfunc = texteqfast;
-- 
2.51.0

v10-0009-Add-support-for-TOAST-chunk_id-type-in-binary-up.patchtext/x-diff; charset=us-asciiDownload
From e07713b582c137b782632e5221a4fad9ffcf08f9 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 10:57:59 +0900
Subject: [PATCH v10 09/14] Add support for TOAST chunk_id type in binary
 upgrades

This commit adds a new function, which would set the type of a chunk_id
attribute for a TOAST table across upgrades.  This piece currently works
only with chunk_id = OIDOID, but it is required in a follow-up patch
where support for chunk_id = OID8OID is supported on top of the existing
one.
---
 src/include/catalog/binary_upgrade.h          |  1 +
 src/include/catalog/pg_proc.dat               |  4 ++++
 src/backend/catalog/heap.c                    |  1 +
 src/backend/catalog/toasting.c                | 20 ++++++++++++++++++-
 src/backend/utils/adt/pg_upgrade_support.c    | 11 ++++++++++
 src/bin/pg_dump/pg_dump.c                     | 10 +++++++++-
 .../expected/spgist_name_ops.out              |  6 ++++--
 7 files changed, 49 insertions(+), 4 deletions(-)

diff --git a/src/include/catalog/binary_upgrade.h b/src/include/catalog/binary_upgrade.h
index 7bf7ae443859..117f3e3f7746 100644
--- a/src/include/catalog/binary_upgrade.h
+++ b/src/include/catalog/binary_upgrade.h
@@ -29,6 +29,7 @@ extern PGDLLIMPORT Oid binary_upgrade_next_index_pg_class_oid;
 extern PGDLLIMPORT RelFileNumber binary_upgrade_next_index_pg_class_relfilenumber;
 extern PGDLLIMPORT Oid binary_upgrade_next_toast_pg_class_oid;
 extern PGDLLIMPORT RelFileNumber binary_upgrade_next_toast_pg_class_relfilenumber;
+extern PGDLLIMPORT Oid binary_upgrade_next_toast_chunk_id_typoid;
 
 extern PGDLLIMPORT Oid binary_upgrade_next_pg_enum_oid;
 extern PGDLLIMPORT Oid binary_upgrade_next_pg_authid_oid;
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f0101a869c86..1fc8059a5076 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11794,6 +11794,10 @@
   proname => 'binary_upgrade_set_next_toast_pg_class_oid', provolatile => 'v',
   proparallel => 'r', prorettype => 'void', proargtypes => 'oid',
   prosrc => 'binary_upgrade_set_next_toast_pg_class_oid' },
+{ oid => '8219', descr => 'for use by pg_upgrade',
+  proname => 'binary_upgrade_set_next_toast_chunk_id_typoid', provolatile => 'v',
+  proparallel => 'r', prorettype => 'void', proargtypes => 'oid',
+  prosrc => 'binary_upgrade_set_next_toast_chunk_id_typoid' },
 { oid => '3589', descr => 'for use by pg_upgrade',
   proname => 'binary_upgrade_set_next_pg_enum_oid', provolatile => 'v',
   proparallel => 'r', prorettype => 'void', proargtypes => 'oid',
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 606434823cf4..8e92f0726a17 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -80,6 +80,7 @@
 /* Potentially set by pg_upgrade_support functions */
 Oid			binary_upgrade_next_heap_pg_class_oid = InvalidOid;
 Oid			binary_upgrade_next_toast_pg_class_oid = InvalidOid;
+Oid			binary_upgrade_next_toast_chunk_id_typoid = InvalidOid;
 RelFileNumber binary_upgrade_next_heap_pg_class_relfilenumber = InvalidRelFileNumber;
 RelFileNumber binary_upgrade_next_toast_pg_class_relfilenumber = InvalidRelFileNumber;
 
diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c
index c78dcea98c1f..e99f259bac52 100644
--- a/src/backend/catalog/toasting.c
+++ b/src/backend/catalog/toasting.c
@@ -145,6 +145,7 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	int16		coloptions[2];
 	ObjectAddress baseobject,
 				toastobject;
+	Oid			toast_chunkid_typid = OIDOID;
 
 	/*
 	 * Is it already toasted?
@@ -183,6 +184,23 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 		 */
 		if (!OidIsValid(binary_upgrade_next_toast_pg_class_oid))
 			return false;
+
+		/*
+		 * The attribute type for chunk_id should have been set when requesting
+		 * a TOAST table creation.
+		 */
+		if (!OidIsValid(binary_upgrade_next_toast_chunk_id_typoid))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("toast chunk_id type not set while in binary upgrade mode")));
+		if (binary_upgrade_next_toast_chunk_id_typoid != OIDOID)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("cannot support toast chunk_id type %u in binary upgrade mode",
+							binary_upgrade_next_toast_chunk_id_typoid)));
+
+		toast_chunkid_typid = binary_upgrade_next_toast_chunk_id_typoid;
+		binary_upgrade_next_toast_chunk_id_typoid = InvalidOid;
 	}
 
 	/*
@@ -204,7 +222,7 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	tupdesc = CreateTemplateTupleDesc(3);
 	TupleDescInitEntry(tupdesc, (AttrNumber) 1,
 					   "chunk_id",
-					   OIDOID,
+					   toast_chunkid_typid,
 					   -1, 0);
 	TupleDescInitEntry(tupdesc, (AttrNumber) 2,
 					   "chunk_seq",
diff --git a/src/backend/utils/adt/pg_upgrade_support.c b/src/backend/utils/adt/pg_upgrade_support.c
index 8953a17753e8..cf2aa49c6d84 100644
--- a/src/backend/utils/adt/pg_upgrade_support.c
+++ b/src/backend/utils/adt/pg_upgrade_support.c
@@ -149,6 +149,17 @@ binary_upgrade_set_next_toast_pg_class_oid(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+Datum
+binary_upgrade_set_next_toast_chunk_id_typoid(PG_FUNCTION_ARGS)
+{
+	Oid			typoid = PG_GETARG_OID(0);
+
+	CHECK_IS_BINARY_UPGRADE;
+	binary_upgrade_next_toast_chunk_id_typoid = typoid;
+
+	PG_RETURN_VOID();
+}
+
 Datum
 binary_upgrade_set_next_toast_relfilenode(PG_FUNCTION_ARGS)
 {
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 7df56d8b1b0f..2413bebc68f1 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -103,6 +103,7 @@ typedef struct
 	RelFileNumber relfilenumber;	/* object filenode */
 	Oid			toast_oid;		/* toast table OID */
 	RelFileNumber toast_relfilenumber;	/* toast table filenode */
+	Oid			toast_chunk_id_typoid;	/* type of chunk_id attribute */
 	Oid			toast_index_oid;	/* toast table index OID */
 	RelFileNumber toast_index_relfilenumber;	/* toast table index filenode */
 } BinaryUpgradeClassOidItem;
@@ -5826,7 +5827,10 @@ collectBinaryUpgradeClassOids(Archive *fout)
 	const char *query;
 
 	query = "SELECT c.oid, c.relkind, c.relfilenode, c.reltoastrelid, "
-		"ct.relfilenode, i.indexrelid, cti.relfilenode "
+		"ct.relfilenode, i.indexrelid, cti.relfilenode, "
+		"(SELECT a.atttypid FROM pg_attribute AS a "
+		"  WHERE a.attrelid = c.reltoastrelid AND attname = 'chunk_id'::text) "
+		"  AS toastchunktypid "
 		"FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_index i "
 		"ON (c.reltoastrelid = i.indrelid AND i.indisvalid) "
 		"LEFT JOIN pg_catalog.pg_class ct ON (c.reltoastrelid = ct.oid) "
@@ -5848,6 +5852,7 @@ collectBinaryUpgradeClassOids(Archive *fout)
 		binaryUpgradeClassOids[i].toast_relfilenumber = atooid(PQgetvalue(res, i, 4));
 		binaryUpgradeClassOids[i].toast_index_oid = atooid(PQgetvalue(res, i, 5));
 		binaryUpgradeClassOids[i].toast_index_relfilenumber = atooid(PQgetvalue(res, i, 6));
+		binaryUpgradeClassOids[i].toast_chunk_id_typoid = atooid(PQgetvalue(res, i, 7));
 	}
 
 	PQclear(res);
@@ -5912,6 +5917,9 @@ binary_upgrade_set_pg_class_oids(Archive *fout,
 			appendPQExpBuffer(upgrade_buffer,
 							  "SELECT pg_catalog.binary_upgrade_set_next_toast_relfilenode('%u'::pg_catalog.oid);\n",
 							  entry->toast_relfilenumber);
+			appendPQExpBuffer(upgrade_buffer,
+							  "SELECT pg_catalog.binary_upgrade_set_next_toast_chunk_id_typoid('%u'::pg_catalog.oid);\n",
+							  entry->toast_chunk_id_typoid);
 
 			/* every toast table has an index */
 			appendPQExpBuffer(upgrade_buffer,
diff --git a/src/test/modules/spgist_name_ops/expected/spgist_name_ops.out b/src/test/modules/spgist_name_ops/expected/spgist_name_ops.out
index 1ee65ede2430..35e59d0cd83c 100644
--- a/src/test/modules/spgist_name_ops/expected/spgist_name_ops.out
+++ b/src/test/modules/spgist_name_ops/expected/spgist_name_ops.out
@@ -61,9 +61,10 @@ select * from t
  binary_upgrade_set_next_pg_enum_oid                  |    | binary_upgrade_set_next_pg_enum_oid
  binary_upgrade_set_next_pg_tablespace_oid            |    | binary_upgrade_set_next_pg_tablespace_oid
  binary_upgrade_set_next_pg_type_oid                  |    | binary_upgrade_set_next_pg_type_oid
+ binary_upgrade_set_next_toast_chunk_id_typoid        |    | binary_upgrade_set_next_toast_chunk_id_typoid
  binary_upgrade_set_next_toast_pg_class_oid           |  1 | binary_upgrade_set_next_toast_pg_class_oid
  binary_upgrade_set_next_toast_relfilenode            |    | binary_upgrade_set_next_toast_relfilenode
-(13 rows)
+(14 rows)
 
 -- Verify clean failure when INCLUDE'd columns result in overlength tuple
 -- The error message details are platform-dependent, so show only SQLSTATE
@@ -110,9 +111,10 @@ select * from t
  binary_upgrade_set_next_pg_enum_oid                  |    | binary_upgrade_set_next_pg_enum_oid
  binary_upgrade_set_next_pg_tablespace_oid            |    | binary_upgrade_set_next_pg_tablespace_oid
  binary_upgrade_set_next_pg_type_oid                  |    | binary_upgrade_set_next_pg_type_oid
+ binary_upgrade_set_next_toast_chunk_id_typoid        |    | binary_upgrade_set_next_toast_chunk_id_typoid
  binary_upgrade_set_next_toast_pg_class_oid           |  1 | binary_upgrade_set_next_toast_pg_class_oid
  binary_upgrade_set_next_toast_relfilenode            |    | binary_upgrade_set_next_toast_relfilenode
-(13 rows)
+(14 rows)
 
 \set VERBOSITY sqlstate
 insert into t values(repeat('xyzzy', 12), 42, repeat('xyzzy', 4000));
-- 
2.51.0

v10-0010-Enlarge-OID-generation-to-8-bytes.patchtext/x-diff; charset=us-asciiDownload
From 006cf4fee64f9292bdeb9cf1f002850c8398dfcf Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 12:15:15 +0900
Subject: [PATCH v10 10/14] Enlarge OID generation to 8 bytes

This adds a new routine called GetNewObjectId8() in varsup.c, which is
able to retrieve a 64b OID.  GetNewObjectId() is kept compatible with
its origin, where we still check that the lower 32 bits of the counter
do not wraparound, handling the FirstNormalObjectId case.

pg_resetwal -o/--next-oid is updated to be able to handle 8-byte OIDs.
---
 src/include/access/transam.h              |  3 +-
 src/include/access/xlog.h                 |  2 +-
 src/include/catalog/pg_control.h          |  2 +-
 src/backend/access/rmgrdesc/xlogdesc.c    |  8 +--
 src/backend/access/transam/varsup.c       | 62 ++++++++++++++++-------
 src/backend/access/transam/xlog.c         |  8 +--
 src/backend/access/transam/xlogrecovery.c |  2 +-
 src/bin/pg_controldata/pg_controldata.c   |  2 +-
 src/bin/pg_resetwal/pg_resetwal.c         | 10 ++--
 doc/src/sgml/ref/pg_resetwal.sgml         |  6 +--
 10 files changed, 66 insertions(+), 39 deletions(-)

diff --git a/src/include/access/transam.h b/src/include/access/transam.h
index 6fa91bfcdc03..3e9fd29f0425 100644
--- a/src/include/access/transam.h
+++ b/src/include/access/transam.h
@@ -211,7 +211,7 @@ typedef struct TransamVariablesData
 	/*
 	 * These fields are protected by OidGenLock.
 	 */
-	Oid			nextOid;		/* next OID to assign */
+	Oid8		nextOid;		/* next OID (8 bytes) to assign */
 	uint32		oidCount;		/* OIDs available before must do XLOG work */
 
 	/*
@@ -355,6 +355,7 @@ extern void SetTransactionIdLimit(TransactionId oldest_datfrozenxid,
 extern void AdvanceOldestClogXid(TransactionId oldest_datfrozenxid);
 extern bool ForceTransactionIdLimitUpdate(void);
 extern Oid	GetNewObjectId(void);
+extern Oid8 GetNewObjectId8(void);
 extern void StopGeneratingPinnedObjectIds(void);
 
 #ifdef USE_ASSERT_CHECKING
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index 0591a885dd1b..443d7c99fb62 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -255,7 +255,7 @@ extern void ShutdownXLOG(int code, Datum arg);
 extern bool CreateCheckPoint(int flags);
 extern bool CreateRestartPoint(int flags);
 extern WALAvailability GetWALAvailability(XLogRecPtr targetLSN);
-extern void XLogPutNextOid(Oid nextOid);
+extern void XLogPutNextOid(Oid8 nextOid);
 extern XLogRecPtr XLogRestorePoint(const char *rpName);
 extern void UpdateFullPageWrites(void);
 extern void GetFullPageWriteInfo(XLogRecPtr *RedoRecPtr_p, bool *doPageWrites_p);
diff --git a/src/include/catalog/pg_control.h b/src/include/catalog/pg_control.h
index 7503db1af51c..ca0ca206b554 100644
--- a/src/include/catalog/pg_control.h
+++ b/src/include/catalog/pg_control.h
@@ -43,7 +43,7 @@ typedef struct CheckPoint
 	int			wal_level;		/* current wal_level */
 	bool		logicalDecodingEnabled; /* current logical decoding status */
 	FullTransactionId nextXid;	/* next free transaction ID */
-	Oid			nextOid;		/* next free OID */
+	Oid8		nextOid;		/* next free OID */
 	MultiXactId nextMulti;		/* next free MultiXactId */
 	MultiXactOffset nextMultiOffset;	/* next free MultiXact offset */
 	TransactionId oldestXid;	/* cluster-wide minimum datfrozenxid */
diff --git a/src/backend/access/rmgrdesc/xlogdesc.c b/src/backend/access/rmgrdesc/xlogdesc.c
index ff078f22264d..77a51ae24f35 100644
--- a/src/backend/access/rmgrdesc/xlogdesc.c
+++ b/src/backend/access/rmgrdesc/xlogdesc.c
@@ -66,7 +66,7 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
 		CheckPoint *checkpoint = (CheckPoint *) rec;
 
 		appendStringInfo(buf, "redo %X/%08X; "
-						 "tli %u; prev tli %u; fpw %s; wal_level %s; logical decoding %s; xid %u:%u; oid %u; multi %u; offset %" PRIu64 "; "
+						 "tli %u; prev tli %u; fpw %s; wal_level %s; logical decoding %s; xid %u:%u; oid " OID8_FORMAT "; multi %u; offset %" PRIu64 "; "
 						 "oldest xid %u in DB %u; oldest multi %u in DB %u; "
 						 "oldest/newest commit timestamp xid: %u/%u; "
 						 "oldest running xid %u; %s",
@@ -92,10 +92,10 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
 	}
 	else if (info == XLOG_NEXTOID)
 	{
-		Oid			nextOid;
+		Oid8		nextOid;
 
-		memcpy(&nextOid, rec, sizeof(Oid));
-		appendStringInfo(buf, "%u", nextOid);
+		memcpy(&nextOid, rec, sizeof(Oid8));
+		appendStringInfo(buf, OID8_FORMAT, nextOid);
 	}
 	else if (info == XLOG_RESTORE_POINT)
 	{
diff --git a/src/backend/access/transam/varsup.c b/src/backend/access/transam/varsup.c
index 3e95d4cfd164..0dab8f3369e7 100644
--- a/src/backend/access/transam/varsup.c
+++ b/src/backend/access/transam/varsup.c
@@ -542,31 +542,51 @@ ForceTransactionIdLimitUpdate(void)
 
 
 /*
- * GetNewObjectId -- allocate a new OID
+ * GetNewObjectId -- allocate a new OID (4 bytes)
  *
- * OIDs are generated by a cluster-wide counter.  Since they are only 32 bits
- * wide, counter wraparound will occur eventually, and therefore it is unwise
- * to assume they are unique unless precautions are taken to make them so.
- * Hence, this routine should generally not be used directly.  The only direct
- * callers should be GetNewOidWithIndex() and GetNewRelFileNumber() in
- * catalog/catalog.c.
+ * OIDs are generated by a cluster-wide counter.  The callers of this routine
+ * expect a 32 bit-wide counter, and counter wraparound will occur eventually,
+ * and therefore it is unwise to assume they are unique unless precautions are
+ * taken to make them so.  This routine should generally not be used directly.
+ * The only direct callers should be GetNewOidWithIndex() and
+ * GetNewRelFileNumber() in catalog/catalog.c.
  */
 Oid
 GetNewObjectId(void)
 {
-	Oid			result;
+	return (Oid) GetNewObjectId8();
+}
+
+/*
+ * GetNewObjectId8 -- allocate a new OID (8 bytes)
+ *
+ * This routine can be called directly if the consumer of the OID allocated
+ * stores the counter in an 8-byte space, where wraparound does not matter.
+ * We still need to care about the wraparound case in the low 32 bits of the
+ * space allocated, GetNewObjectId() expecting OIDs to never be allocated
+ * up to FirstNormalObjectId.
+ */
+Oid8
+GetNewObjectId8(void)
+{
+	Oid8		result;
+	Oid			nextoid_lo;
+	uint32		nextoid_hi;
 
 	/* safety check, we should never get this far in a HS standby */
 	if (RecoveryInProgress())
 		elog(ERROR, "cannot assign OIDs during recovery");
 
 	LWLockAcquire(OidGenLock, LW_EXCLUSIVE);
+	nextoid_lo = (Oid) TransamVariables->nextOid;
+	nextoid_hi = (uint32) (TransamVariables->nextOid >> 32);
 
 	/*
-	 * Check for wraparound of the OID counter.  We *must* not return 0
-	 * (InvalidOid), and in normal operation we mustn't return anything below
-	 * FirstNormalObjectId since that range is reserved for initdb (see
-	 * IsCatalogRelationOid()).  Note we are relying on unsigned comparison.
+	 * Check for wraparound of the OID counter in its lower 4 bytes.  We
+	 * *must* not return 0 (InvalidOid), and in normal operation we
+	 * mustn't return anything below FirstNormalObjectId since that range
+	 * is reserved for initdb (see IsCatalogRelationOid()).  Note we are
+	 * relying on unsigned comparison.
 	 *
 	 * During initdb, we start the OID generator at FirstGenbkiObjectId, so we
 	 * only wrap if before that point when in bootstrap or standalone mode.
@@ -576,26 +596,32 @@ GetNewObjectId(void)
 	 * available for automatic assignment during initdb, while ensuring they
 	 * will never conflict with user-assigned OIDs.
 	 */
-	if (TransamVariables->nextOid < ((Oid) FirstNormalObjectId))
+	if (nextoid_lo < ((Oid) FirstNormalObjectId))
 	{
 		if (IsPostmasterEnvironment)
 		{
 			/* wraparound, or first post-initdb assignment, in normal mode */
-			TransamVariables->nextOid = FirstNormalObjectId;
+			nextoid_lo = FirstNormalObjectId;
 			TransamVariables->oidCount = 0;
 		}
 		else
 		{
 			/* we may be bootstrapping, so don't enforce the full range */
-			if (TransamVariables->nextOid < ((Oid) FirstGenbkiObjectId))
+			if (nextoid_lo < ((Oid) FirstGenbkiObjectId))
 			{
 				/* wraparound in standalone mode (unlikely but possible) */
-				TransamVariables->nextOid = FirstNormalObjectId;
+				nextoid_lo = FirstNormalObjectId;
 				TransamVariables->oidCount = 0;
 			}
 		}
 	}
 
+	/*
+	 * Set next OID in its 8-byte space, skipping the first post-init
+	 * assignment.
+	 */
+	TransamVariables->nextOid = ((Oid8) nextoid_hi) << 32 | nextoid_lo;
+
 	/* If we run out of logged for use oids then we must log more */
 	if (TransamVariables->oidCount == 0)
 	{
@@ -620,7 +646,7 @@ GetNewObjectId(void)
  * to the specified value.
  */
 static void
-SetNextObjectId(Oid nextOid)
+SetNextObjectId(Oid8 nextOid)
 {
 	/* Safety check, this is only allowable during initdb */
 	if (IsPostmasterEnvironment)
@@ -630,7 +656,7 @@ SetNextObjectId(Oid nextOid)
 	LWLockAcquire(OidGenLock, LW_EXCLUSIVE);
 
 	if (TransamVariables->nextOid > nextOid)
-		elog(ERROR, "too late to advance OID counter to %u, it is now %u",
+		elog(ERROR, "too late to advance OID counter to " OID8_FORMAT ", it is now " OID8_FORMAT,
 			 nextOid, TransamVariables->nextOid);
 
 	TransamVariables->nextOid = nextOid;
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 81dc86847c01..a104024f7984 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -8153,10 +8153,10 @@ KeepLogSeg(XLogRecPtr recptr, XLogSegNo *logSegNo)
  * Write a NEXTOID log record
  */
 void
-XLogPutNextOid(Oid nextOid)
+XLogPutNextOid(Oid8 nextOid)
 {
 	XLogBeginInsert();
-	XLogRegisterData(&nextOid, sizeof(Oid));
+	XLogRegisterData(&nextOid, sizeof(Oid8));
 	(void) XLogInsert(RM_XLOG_ID, XLOG_NEXTOID);
 
 	/*
@@ -8379,7 +8379,7 @@ xlog_redo(XLogReaderState *record)
 
 	if (info == XLOG_NEXTOID)
 	{
-		Oid			nextOid;
+		Oid8		nextOid;
 
 		/*
 		 * We used to try to take the maximum of TransamVariables->nextOid and
@@ -8388,7 +8388,7 @@ xlog_redo(XLogReaderState *record)
 		 * anyway, better to just believe the record exactly.  We still take
 		 * OidGenLock while setting the variable, just in case.
 		 */
-		memcpy(&nextOid, XLogRecGetData(record), sizeof(Oid));
+		memcpy(&nextOid, XLogRecGetData(record), sizeof(Oid8));
 		LWLockAcquire(OidGenLock, LW_EXCLUSIVE);
 		TransamVariables->nextOid = nextOid;
 		TransamVariables->oidCount = 0;
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index 0b5f871abe74..edbee448db04 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -892,7 +892,7 @@ InitWalRecovery(ControlFileData *ControlFile, bool *wasShutdown_ptr,
 							LSN_FORMAT_ARGS(checkPoint.redo),
 							wasShutdown ? "true" : "false"));
 	ereport(DEBUG1,
-			(errmsg_internal("next transaction ID: " UINT64_FORMAT "; next OID: %u",
+			(errmsg_internal("next transaction ID: " UINT64_FORMAT "; next OID: " OID8_FORMAT,
 							 U64FromFullTransactionId(checkPoint.nextXid),
 							 checkPoint.nextOid)));
 	ereport(DEBUG1,
diff --git a/src/bin/pg_controldata/pg_controldata.c b/src/bin/pg_controldata/pg_controldata.c
index a4060309ae0e..bfb89536280f 100644
--- a/src/bin/pg_controldata/pg_controldata.c
+++ b/src/bin/pg_controldata/pg_controldata.c
@@ -267,7 +267,7 @@ main(int argc, char *argv[])
 	printf(_("Latest checkpoint's NextXID:          %u:%u\n"),
 		   EpochFromFullTransactionId(ControlFile->checkPointCopy.nextXid),
 		   XidFromFullTransactionId(ControlFile->checkPointCopy.nextXid));
-	printf(_("Latest checkpoint's NextOID:          %u\n"),
+	printf(_("Latest checkpoint's NextOID:          " OID8_FORMAT "\n"),
 		   ControlFile->checkPointCopy.nextOid);
 	printf(_("Latest checkpoint's NextMultiXactId:  %u\n"),
 		   ControlFile->checkPointCopy.nextMulti);
diff --git a/src/bin/pg_resetwal/pg_resetwal.c b/src/bin/pg_resetwal/pg_resetwal.c
index b2c4b9db395a..1959bf419419 100644
--- a/src/bin/pg_resetwal/pg_resetwal.c
+++ b/src/bin/pg_resetwal/pg_resetwal.c
@@ -82,7 +82,7 @@ static TransactionId oldest_commit_ts_xid_val;
 static TransactionId newest_commit_ts_xid_val;
 
 static bool next_oid_given = false;
-static Oid	next_oid_val;
+static Oid8	next_oid_val;
 
 static bool mxids_given = false;
 static MultiXactId next_mxid_val;
@@ -253,7 +253,7 @@ main(int argc, char *argv[])
 
 			case 'o':
 				errno = 0;
-				next_oid_val = strtouint32_strict(optarg, &endptr, 0);
+				next_oid_val = strtou64(optarg, &endptr, 0);
 				if (endptr == optarg || *endptr != '\0' || errno != 0)
 				{
 					pg_log_error("invalid argument for option %s", "-o");
@@ -771,7 +771,7 @@ PrintControlValues(bool guessed)
 	printf(_("Latest checkpoint's NextXID:          %u:%u\n"),
 		   EpochFromFullTransactionId(ControlFile.checkPointCopy.nextXid),
 		   XidFromFullTransactionId(ControlFile.checkPointCopy.nextXid));
-	printf(_("Latest checkpoint's NextOID:          %u\n"),
+	printf(_("Latest checkpoint's NextOID:          " OID8_FORMAT "\n"),
 		   ControlFile.checkPointCopy.nextOid);
 	printf(_("Latest checkpoint's NextMultiXactId:  %u\n"),
 		   ControlFile.checkPointCopy.nextMulti);
@@ -857,7 +857,7 @@ PrintNewControlValues(void)
 
 	if (next_oid_given)
 	{
-		printf(_("NextOID:                              %u\n"),
+		printf(_("NextOID:                              " OID8_FORMAT "\n"),
 			   ControlFile.checkPointCopy.nextOid);
 	}
 
@@ -1227,7 +1227,7 @@ usage(void)
 	printf(_("  -e, --epoch=XIDEPOCH             set next transaction ID epoch\n"));
 	printf(_("  -l, --next-wal-file=WALFILE      set minimum starting location for new WAL\n"));
 	printf(_("  -m, --multixact-ids=MXID,MXID    set next and oldest multitransaction ID\n"));
-	printf(_("  -o, --next-oid=OID               set next OID\n"));
+	printf(_("  -o, --next-oid=OID8              set next OID (8 bytes)\n"));
 	printf(_("  -O, --multixact-offset=OFFSET    set next multitransaction offset\n"));
 	printf(_("  -u, --oldest-transaction-id=XID  set oldest transaction ID\n"));
 	printf(_("  -x, --next-transaction-id=XID    set next transaction ID\n"));
diff --git a/doc/src/sgml/ref/pg_resetwal.sgml b/doc/src/sgml/ref/pg_resetwal.sgml
index 41f2b1d480c5..83483d883bd1 100644
--- a/doc/src/sgml/ref/pg_resetwal.sgml
+++ b/doc/src/sgml/ref/pg_resetwal.sgml
@@ -282,11 +282,11 @@ PostgreSQL documentation
    </varlistentry>
 
    <varlistentry>
-    <term><option>-o <replaceable class="parameter">oid</replaceable></option></term>
-    <term><option>--next-oid=<replaceable class="parameter">oid</replaceable></option></term>
+    <term><option>-o <replaceable class="parameter">oid8</replaceable></option></term>
+    <term><option>--next-oid=<replaceable class="parameter">oid8</replaceable></option></term>
     <listitem>
      <para>
-      Manually set the next OID.
+      Manually set the next OID (8 bytes).
      </para>
 
      <para>
-- 
2.51.0

v10-0011-Add-relation-option-toast_value_type.patchtext/x-diff; charset=us-asciiDownload
From c099251b8c5022025c409950c07aeb9bfb841dcf Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 12:54:58 +0900
Subject: [PATCH v10 11/14] Add relation option toast_value_type

This relation option gives the possibility to define the attribute type
that can be used for chunk_id in a TOAST table when it is created
initially.  This parameter has no effect if a TOAST table exists, even
after it is modified later on, even on rewrites.

This can be set only to "oid" currently, and will be expanded with a
second mode later.

Note: perhaps it would make sense to introduce that only when support
for 8-byte OID values are added to TOAST, the split is here to ease
review.
---
 src/include/utils/rel.h                | 17 +++++++++++++++++
 src/backend/access/common/reloptions.c | 21 +++++++++++++++++++++
 src/backend/catalog/toasting.c         |  6 ++++++
 src/bin/psql/tab-complete.in.c         |  1 +
 doc/src/sgml/ref/create_table.sgml     | 18 ++++++++++++++++++
 5 files changed, 63 insertions(+)

diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index d03ab247788b..1ad0ff84c528 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -338,11 +338,20 @@ typedef enum StdRdOptIndexCleanup
 	STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON,
 } StdRdOptIndexCleanup;
 
+/* StdRdOptions->toast_value_type values */
+typedef enum StdRdOptToastValueType
+{
+	STDRD_OPTION_TOAST_VALUE_TYPE_INVALID = 0,
+	STDRD_OPTION_TOAST_VALUE_TYPE_OID,
+} StdRdOptToastValueType;
+
 typedef struct StdRdOptions
 {
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	int			fillfactor;		/* page fill factor in percent (0..100) */
 	int			toast_tuple_target; /* target for tuple toasting */
+	StdRdOptToastValueType	toast_value_type;	/* type assigned to chunk_id
+												 * at toast table creation */
 	AutoVacOpts autovacuum;		/* autovacuum-related options */
 	bool		user_catalog_table; /* use as an additional catalog relation */
 	int			parallel_workers;	/* max number of parallel workers */
@@ -368,6 +377,14 @@ typedef struct StdRdOptions
 	((relation)->rd_options ? \
 	 ((StdRdOptions *) (relation)->rd_options)->toast_tuple_target : (defaulttarg))
 
+/*
+ * RelationGetToastValueType
+ *		Returns the relation's toast_value_type.  Note multiple eval of argument!
+ */
+#define RelationGetToastValueType(relation, defaulttarg) \
+	((relation)->rd_options ? \
+	 ((StdRdOptions *) (relation)->rd_options)->toast_value_type : defaulttarg)
+
 /*
  * RelationGetFillFactor
  *		Returns the relation's fillfactor.  Note multiple eval of argument!
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 0b83f98ed5f0..79580a4cc290 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -525,6 +525,14 @@ static relopt_enum_elt_def viewCheckOptValues[] =
 	{(const char *) NULL}		/* list terminator */
 };
 
+/* values from StdRdOptToastValueType */
+static relopt_enum_elt_def StdRdOptToastValueTypes[] =
+{
+	/* no value for INVALID */
+	{"oid", STDRD_OPTION_TOAST_VALUE_TYPE_OID},
+	{(const char *) NULL}		/* list terminator */
+};
+
 static relopt_enum enumRelOpts[] =
 {
 	{
@@ -538,6 +546,17 @@ static relopt_enum enumRelOpts[] =
 		STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO,
 		gettext_noop("Valid values are \"on\", \"off\", and \"auto\".")
 	},
+	{
+		{
+			"toast_value_type",
+			"Controls the attribute type of chunk_id at toast table creation",
+			RELOPT_KIND_HEAP,
+			ShareUpdateExclusiveLock
+		},
+		StdRdOptToastValueTypes,
+		STDRD_OPTION_TOAST_VALUE_TYPE_OID,
+		gettext_noop("Valid values are \"oid\".")
+	},
 	{
 		{
 			"buffering",
@@ -1909,6 +1928,8 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
 		offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_analyze_min_duration)},
 		{"toast_tuple_target", RELOPT_TYPE_INT,
 		offsetof(StdRdOptions, toast_tuple_target)},
+		{"toast_value_type", RELOPT_TYPE_ENUM,
+		offsetof(StdRdOptions, toast_value_type)},
 		{"autovacuum_vacuum_cost_delay", RELOPT_TYPE_REAL,
 		offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_delay)},
 		{"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c
index e99f259bac52..5874574cd4ef 100644
--- a/src/backend/catalog/toasting.c
+++ b/src/backend/catalog/toasting.c
@@ -158,9 +158,15 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	 */
 	if (!IsBinaryUpgrade)
 	{
+		StdRdOptToastValueType value_type;
+
 		/* Normal mode, normal check */
 		if (!needs_toast_table(rel))
 			return false;
+
+		value_type = RelationGetToastValueType(rel, STDRD_OPTION_TOAST_VALUE_TYPE_OID);
+		if (value_type == STDRD_OPTION_TOAST_VALUE_TYPE_OID)
+			toast_chunkid_typid = OIDOID;
 	}
 	else
 	{
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 06edea98f060..6dd5e8a10f57 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -1453,6 +1453,7 @@ static const char *const table_storage_parameters[] = {
 	"toast.vacuum_max_eager_freeze_failure_rate",
 	"toast.vacuum_truncate",
 	"toast_tuple_target",
+	"toast_value_type",
 	"user_catalog_table",
 	"vacuum_index_cleanup",
 	"vacuum_max_eager_freeze_failure_rate",
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 77c5a763d450..200dfc81bf8f 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1632,6 +1632,24 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     </listitem>
    </varlistentry>
 
+   <varlistentry id="reloption-toast-value-type" xreflabel="toast_value_type">
+    <term><literal>toast_value_type</literal> (<type>enum</type>)
+    <indexterm>
+     <primary><varname>toast_value_type</varname> storage parameter</primary>
+    </indexterm>
+    </term>
+    <listitem>
+     <para>
+      The toast_value_type specifies the attribute type of
+      <literal>chunk_id</literal> used when initially creating  a toast
+      relation for this table.
+      By default this parameter is <literal>oid</literal>, to assign
+      <type>oid</type> as attribute type to <literal>chunk_id</literal>.
+      This parameter cannot be set for TOAST tables.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="reloption-parallel-workers" xreflabel="parallel_workers">
     <term><literal>parallel_workers</literal> (<type>integer</type>)
      <indexterm>
-- 
2.51.0

v10-0012-Add-support-for-oid8-TOAST-values.patchtext/x-diff; charset=us-asciiDownload
From 49bd5888d3df332d416bef7508eb82a9f98a0412 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 17:06:10 +0900
Subject: [PATCH v10 12/14] Add support for oid8 TOAST values

This commit adds the possibility to define TOAST tables with oid8 as
value ID, based on the reloption toast_value_type.  All the external
TOAST pointers still rely on varatt_external and a single vartag, with
all the values inserted in the bigint TOAST tables fed from the existing
OID value generator.  This will be changed in an upcoming patch that
adds more vartag_external types and its associated structures, with the
code being able to use a different external TOAST pointer depending on
the attribute type of chunk_id in TOAST relations.

All the changes done here are mechanical, with all the TOAST code able
to do chunk ID lookups based on the two types now supported.

XXX: Catalog version bump required.
---
 src/include/catalog/pg_opclass.dat          |  3 +-
 src/include/utils/rel.h                     |  1 +
 src/backend/access/common/reloptions.c      |  1 +
 src/backend/access/common/toast_internals.c | 94 +++++++++++++++------
 src/backend/access/heap/heaptoast.c         | 20 ++++-
 src/backend/catalog/toasting.c              | 24 +++++-
 doc/src/sgml/ref/create_table.sgml          |  2 +
 doc/src/sgml/storage.sgml                   |  7 +-
 contrib/amcheck/verify_heapam.c             | 19 ++++-
 9 files changed, 131 insertions(+), 40 deletions(-)

diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 0eddd0298f83..79fbab98d888 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -179,7 +179,8 @@
   opcintype => 'xid8' },
 { opcmethod => 'hash', opcname => 'oid8_ops', opcfamily => 'hash/oid8_ops',
   opcintype => 'oid8' },
-{ opcmethod => 'btree', opcname => 'oid8_ops', opcfamily => 'btree/oid8_ops',
+{ oid => '8285', oid_symbol => 'OID8_BTREE_OPS_OID',
+  opcmethod => 'btree', opcname => 'oid8_ops', opcfamily => 'btree/oid8_ops',
   opcintype => 'oid8' },
 { opcmethod => 'hash', opcname => 'cid_ops', opcfamily => 'hash/cid_ops',
   opcintype => 'cid' },
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 1ad0ff84c528..9b4ba693cd0b 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -343,6 +343,7 @@ typedef enum StdRdOptToastValueType
 {
 	STDRD_OPTION_TOAST_VALUE_TYPE_INVALID = 0,
 	STDRD_OPTION_TOAST_VALUE_TYPE_OID,
+	STDRD_OPTION_TOAST_VALUE_TYPE_OID8,
 } StdRdOptToastValueType;
 
 typedef struct StdRdOptions
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 79580a4cc290..35da8a6fdd2f 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -530,6 +530,7 @@ static relopt_enum_elt_def StdRdOptToastValueTypes[] =
 {
 	/* no value for INVALID */
 	{"oid", STDRD_OPTION_TOAST_VALUE_TYPE_OID},
+	{"oid8", STDRD_OPTION_TOAST_VALUE_TYPE_OID8},
 	{(const char *) NULL}		/* list terminator */
 };
 
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 90e3f43eac59..88691daa6023 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -26,6 +26,7 @@
 #include "utils/fmgroids.h"
 #include "utils/rel.h"
 #include "utils/snapmgr.h"
+#include "utils/lsyscache.h"
 
 static bool toastrel_valueid_exists(Relation toastrel, Oid8 valueid);
 static bool toastid_valueid_exists(Oid toastrelid, Oid8 valueid);
@@ -134,8 +135,10 @@ toast_save_datum(Relation rel, Datum value,
 	int			validIndex;
 	const toast_external_info *info;
 	uint8		tag = VARTAG_INDIRECT;	/* init value does not matter */
+	Oid			toast_typid = get_atttype(rel->rd_rel->reltoastrelid, 1);
 
 	Assert(!VARATT_IS_EXTERNAL(dval));
+	Assert(OidIsValid(toast_typid));
 
 	/*
 	 * Open the toast relation and its indexes.  We can use the index to check
@@ -216,24 +219,32 @@ toast_save_datum(Relation rel, Datum value,
 		toast_pointer.toastrelid = RelationGetRelid(toastrel);
 
 	/*
-	 * Choose an OID to use as the value ID for this toast value.
+	 * Choose a new value to use as the value ID for this toast value, be it
+	 * for OID or int8-based TOAST relations.
 	 *
-	 * Normally we just choose an unused OID within the toast table.  But
+	 * Normally we just choose an unused value within the toast table.  But
 	 * during table-rewriting operations where we are preserving an existing
-	 * toast table OID, we want to preserve toast value OIDs too.  So, if
+	 * toast table OID, we want to preserve toast value IDs too.  So, if
 	 * rd_toastoid is set and we had a prior external value from that same
 	 * toast table, re-use its value ID.  If we didn't have a prior external
 	 * value (which is a corner case, but possible if the table's attstorage
 	 * options have been changed), we have to pick a value ID that doesn't
-	 * conflict with either new or existing toast value OIDs.
+	 * conflict with either new or existing toast value IDs.  If the TOAST
+	 * table uses 8-byte value IDs, we should not really care much about
+	 * that.
 	 */
 	if (!OidIsValid(rel->rd_toastoid))
 	{
 		/* normal case: just choose an unused OID */
-		toast_pointer.valueid =
-			GetNewOidWithIndex(toastrel,
-							   RelationGetRelid(toastidxs[validIndex]),
-							   (AttrNumber) 1);
+		if (toast_typid == OIDOID)
+			toast_pointer.valueid =
+				GetNewOidWithIndex(toastrel,
+								   RelationGetRelid(toastidxs[validIndex]),
+								   (AttrNumber) 1);
+		else if (toast_typid == OID8OID)
+			toast_pointer.valueid = GetNewObjectId8();
+		else
+			Assert(false);
 	}
 	else
 	{
@@ -279,17 +290,22 @@ toast_save_datum(Relation rel, Datum value,
 		if (toast_pointer.valueid == InvalidOid8)
 		{
 			/*
-			 * new value; must choose an OID that doesn't conflict in either
-			 * old or new toast table
+			 * new value; must choose a value that doesn't conflict in either
+			 * old or new toast table.
 			 */
-			do
+			if (toast_typid == OIDOID)
 			{
-				toast_pointer.valueid =
-					GetNewOidWithIndex(toastrel,
-									   RelationGetRelid(toastidxs[validIndex]),
-									   (AttrNumber) 1);
-			} while (toastid_valueid_exists(rel->rd_toastoid,
-											toast_pointer.valueid));
+				do
+				{
+					toast_pointer.valueid =
+						GetNewOidWithIndex(toastrel,
+										   RelationGetRelid(toastidxs[validIndex]),
+										   (AttrNumber) 1);
+				} while (toastid_valueid_exists(rel->rd_toastoid,
+												toast_pointer.valueid));
+			}
+			else if (toast_typid == OID8OID)
+				toast_pointer.valueid = GetNewObjectId8();
 		}
 	}
 
@@ -327,7 +343,10 @@ toast_save_datum(Relation rel, Datum value,
 		/*
 		 * Build a tuple and store it
 		 */
-		t_values[0] = ObjectIdGetDatum(toast_pointer.valueid);
+		if (toast_typid == OIDOID)
+			t_values[0] = ObjectIdGetDatum(toast_pointer.valueid);
+		else if (toast_typid == OID8OID)
+			t_values[0] = ObjectId8GetDatum(toast_pointer.valueid);
 		t_values[1] = Int32GetDatum(chunk_seq++);
 		SET_VARSIZE(&chunk_data, chunk_size + VARHDRSZ);
 		memcpy(VARDATA(&chunk_data), data_p, chunk_size);
@@ -406,6 +425,7 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	HeapTuple	toasttup;
 	int			num_indexes;
 	int			validIndex;
+	Oid			toast_typid;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		return;
@@ -417,6 +437,8 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	 * Open the toast relation and its indexes
 	 */
 	toastrel = table_open(toast_pointer.toastrelid, RowExclusiveLock);
+	toast_typid = TupleDescAttr(toastrel->rd_att, 0)->atttypid;
+	Assert(toast_typid == OIDOID || toast_typid == OID8OID);
 
 	/* Fetch valid relation used for process */
 	validIndex = toast_open_indexes(toastrel,
@@ -427,10 +449,18 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	/*
 	 * Setup a scan key to find chunks with matching va_valueid
 	 */
-	ScanKeyInit(&toastkey,
-				(AttrNumber) 1,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(toast_pointer.valueid));
+	if (toast_typid == OIDOID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(toast_pointer.valueid));
+	else if (toast_typid == OID8OID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OID8EQ,
+					ObjectId8GetDatum(toast_pointer.valueid));
+	else
+		Assert(false);
 
 	/*
 	 * Find all the chunks.  (We don't actually care whether we see them in
@@ -477,6 +507,7 @@ toastrel_valueid_exists(Relation toastrel, Oid8 valueid)
 	int			num_indexes;
 	int			validIndex;
 	Relation   *toastidxs;
+	Oid			toast_typid;
 
 	/* Fetch a valid index relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -484,13 +515,24 @@ toastrel_valueid_exists(Relation toastrel, Oid8 valueid)
 									&toastidxs,
 									&num_indexes);
 
+	toast_typid = TupleDescAttr(toastrel->rd_att, 0)->atttypid;
+	Assert(toast_typid == OIDOID || toast_typid == OID8OID);
+
 	/*
 	 * Setup a scan key to find chunks with matching va_valueid
 	 */
-	ScanKeyInit(&toastkey,
-				(AttrNumber) 1,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(valueid));
+	if (toast_typid == OIDOID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(valueid));
+	else if (toast_typid == OID8OID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OID8EQ,
+					ObjectId8GetDatum(valueid));
+	else
+		Assert(false);
 
 	/*
 	 * Is there any such chunk?
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index fd102037be43..f669b75e2362 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -654,6 +654,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 	int32		max_chunk_size;
 	const toast_external_info *info;
 	uint8		tag = VARTAG_INDIRECT;	/* init value does not matter */
+	Oid			toast_typid;
 
 	/* Look for the valid index of toast relation */
 	validIndex = toast_open_indexes(toastrel,
@@ -667,16 +668,27 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 
 	max_chunk_size = info->maximum_chunk_size;
 
+	toast_typid = TupleDescAttr(toastrel->rd_att, 0)->atttypid;
+	Assert(toast_typid == OIDOID || toast_typid == OID8OID);
+
 	totalchunks = ((attrsize - 1) / max_chunk_size) + 1;
 	startchunk = sliceoffset / max_chunk_size;
 	endchunk = (sliceoffset + slicelength - 1) / max_chunk_size;
 	Assert(endchunk <= totalchunks);
 
 	/* Set up a scan key to fetch from the index. */
-	ScanKeyInit(&toastkey[0],
-				(AttrNumber) 1,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(valueid));
+	if (toast_typid == OIDOID)
+		ScanKeyInit(&toastkey[0],
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(valueid));
+	else if (toast_typid == OID8OID)
+		ScanKeyInit(&toastkey[0],
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OID8EQ,
+					ObjectId8GetDatum(valueid));
+	else
+		Assert(false);
 
 	/*
 	 * No additional condition if fetching all chunks. Otherwise, use an
diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c
index 5874574cd4ef..f0f6fcb8051d 100644
--- a/src/backend/catalog/toasting.c
+++ b/src/backend/catalog/toasting.c
@@ -31,6 +31,7 @@
 #include "nodes/makefuncs.h"
 #include "utils/fmgroids.h"
 #include "utils/rel.h"
+#include "utils/lsyscache.h"
 #include "utils/syscache.h"
 
 static void CheckAndCreateToastTable(Oid relOid, Datum reloptions,
@@ -167,6 +168,8 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 		value_type = RelationGetToastValueType(rel, STDRD_OPTION_TOAST_VALUE_TYPE_OID);
 		if (value_type == STDRD_OPTION_TOAST_VALUE_TYPE_OID)
 			toast_chunkid_typid = OIDOID;
+		else if (value_type == STDRD_OPTION_TOAST_VALUE_TYPE_OID8)
+			toast_chunkid_typid = OID8OID;
 	}
 	else
 	{
@@ -199,7 +202,8 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("toast chunk_id type not set while in binary upgrade mode")));
-		if (binary_upgrade_next_toast_chunk_id_typoid != OIDOID)
+		if (binary_upgrade_next_toast_chunk_id_typoid != OIDOID &&
+			binary_upgrade_next_toast_chunk_id_typoid != OID8OID)
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("cannot support toast chunk_id type %u in binary upgrade mode",
@@ -224,6 +228,19 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	snprintf(toast_idxname, sizeof(toast_idxname),
 			 "pg_toast_%u_index", relOid);
 
+	/*
+	 * Special case here.  If OIDOldToast is defined, we need to rely on the
+	 * existing table for the job because we do not want to create an
+	 * inconsistent relation that would conflict with the parent and break
+	 * the world.
+	 */
+	if (OidIsValid(OIDOldToast))
+	{
+		toast_chunkid_typid = get_atttype(OIDOldToast, 1);
+		if (!OidIsValid(toast_chunkid_typid))
+			elog(ERROR, "cache lookup failed for relation %u", OIDOldToast);
+	}
+
 	/* this is pretty painful...  need a tuple descriptor */
 	tupdesc = CreateTemplateTupleDesc(3);
 	TupleDescInitEntry(tupdesc, (AttrNumber) 1,
@@ -336,7 +353,10 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 	collationIds[0] = InvalidOid;
 	collationIds[1] = InvalidOid;
 
-	opclassIds[0] = OID_BTREE_OPS_OID;
+	if (toast_chunkid_typid == OIDOID)
+		opclassIds[0] = OID_BTREE_OPS_OID;
+	else if (toast_chunkid_typid == OID8OID)
+		opclassIds[0] = OID8_BTREE_OPS_OID;
 	opclassIds[1] = INT4_BTREE_OPS_OID;
 
 	coloptions[0] = 0;
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 200dfc81bf8f..cf64387cc86b 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1645,6 +1645,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       relation for this table.
       By default this parameter is <literal>oid</literal>, to assign
       <type>oid</type> as attribute type to <literal>chunk_id</literal>.
+      This parameter can be set to <type>oid8</type> to use <type>oid8</type>
+      as attribute type for <literal>chunk_id</literal>.
       This parameter cannot be set for TOAST tables.
      </para>
     </listitem>
diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml
index 67600fd974d7..afddf663fec5 100644
--- a/doc/src/sgml/storage.sgml
+++ b/doc/src/sgml/storage.sgml
@@ -421,14 +421,15 @@ most <symbol>TOAST_OID_MAX_CHUNK_SIZE</symbol> bytes (by default this value is c
 so that four chunk rows will fit on a page, making it about 2000 bytes).
 Each chunk is stored as a separate row in the <acronym>TOAST</acronym> table
 belonging to the owning table.  Every
-<acronym>TOAST</acronym> table has the columns <structfield>chunk_id</structfield> (an OID
-identifying the particular <acronym>TOAST</acronym>ed value),
+<acronym>TOAST</acronym> table has the columns
+<structfield>chunk_id</structfield> (an OID or an 8-byte integer identifying
+the particular <acronym>TOAST</acronym>ed value),
 <structfield>chunk_seq</structfield> (a sequence number for the chunk within its value),
 and <structfield>chunk_data</structfield> (the actual data of the chunk).  A unique index
 on <structfield>chunk_id</structfield> and <structfield>chunk_seq</structfield> provides fast
 retrieval of the values.  A pointer datum representing an out-of-line on-disk
 <acronym>TOAST</acronym>ed value therefore needs to store the OID of the
-<acronym>TOAST</acronym> table in which to look and the OID of the specific value
+<acronym>TOAST</acronym> table in which to look and the specific value
 (its <structfield>chunk_id</structfield>).  For convenience, pointer datums also store the
 logical datum size (original uncompressed data length), physical stored size
 (different if compression was applied), and the compression method used, if
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 8be9f5a6dfb1..7f1aea8cc93e 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1880,6 +1880,9 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	int32		last_chunk_seq;
 	Oid8		toast_valueid;
 	int32		max_chunk_size;
+	Oid			toast_typid;
+
+	toast_typid = TupleDescAttr(ctx->toast_rel->rd_att, 0)->atttypid;
 
 	extsize = ta->toast_pointer.extsize;
 
@@ -1889,10 +1892,18 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta)
 	/*
 	 * Setup a scan key to find chunks in toast table with matching va_valueid
 	 */
-	ScanKeyInit(&toastkey,
-				(AttrNumber) 1,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(ta->toast_pointer.valueid));
+	if (toast_typid == OIDOID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(ta->toast_pointer.valueid));
+	else if (toast_typid == OID8OID)
+		ScanKeyInit(&toastkey,
+					(AttrNumber) 1,
+					BTEqualStrategyNumber, F_OID8EQ,
+					ObjectId8GetDatum(ta->toast_pointer.valueid));
+	else
+		Assert(false);
 
 	/*
 	 * Check if any chunks for this toasted object exist in the toast table,
-- 
2.51.0

v10-0013-Add-tests-for-TOAST-relations-with-bigint-as-val.patchtext/x-diff; charset=us-asciiDownload
From a5b671406a7f135edc98f3e8810c15508a6a16ee Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 13:43:19 +0900
Subject: [PATCH v10 13/14] Add tests for TOAST relations with bigint as value
 type

This adds coverage for relations created with default_toast_type =
'int8', for external TOAST pointers both compressed and uncompressed.
---
 src/test/regress/expected/strings.out     | 231 ++++++++++++++++++----
 src/test/regress/expected/type_sanity.out |   6 +-
 src/test/regress/sql/strings.sql          | 134 +++++++++----
 src/test/regress/sql/type_sanity.sql      |   6 +-
 4 files changed, 296 insertions(+), 81 deletions(-)

diff --git a/src/test/regress/expected/strings.out b/src/test/regress/expected/strings.out
index 727304f60e74..ed1921b32280 100644
--- a/src/test/regress/expected/strings.out
+++ b/src/test/regress/expected/strings.out
@@ -2012,21 +2012,37 @@ SELECT text 'text' || varchar ' and varchar' AS "Concat text to varchar";
 (1 row)
 
 --
--- test substr with toasted text values
+-- Test substr with toasted text values, for all types of TOAST relations
+-- supported.
 --
-CREATE TABLE toasttest(f1 text);
-insert into toasttest values(repeat('1234567890',10000));
-insert into toasttest values(repeat('1234567890',10000));
+CREATE TABLE toasttest_oid(f1 text) with (toast_value_type = 'oid');
+CREATE TABLE toasttest_oid8(f1 text) with (toast_value_type = 'oid8');
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid8 values(repeat('1234567890',10000));
+insert into toasttest_oid8 values(repeat('1234567890',10000));
 --
 -- Ensure that some values are uncompressed, to test the faster substring
 -- operation used in that case
 --
-alter table toasttest alter column f1 set storage external;
-insert into toasttest values(repeat('1234567890',10000));
-insert into toasttest values(repeat('1234567890',10000));
+alter table toasttest_oid alter column f1 set storage external;
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+alter table toasttest_oid8 alter column f1 set storage external;
+insert into toasttest_oid8 values(repeat('1234567890',10000));
+insert into toasttest_oid8 values(repeat('1234567890',10000));
 -- If the starting position is zero or less, then return from the start of the string
 -- adjusting the length to be consistent with the "negative start" per SQL.
-SELECT substr(f1, -1, 5) from toasttest;
+SELECT substr(f1, -1, 5) from toasttest_oid;
+ substr 
+--------
+ 123
+ 123
+ 123
+ 123
+(4 rows)
+
+SELECT substr(f1, -1, 5) from toasttest_oid8;
  substr 
 --------
  123
@@ -2036,11 +2052,22 @@ SELECT substr(f1, -1, 5) from toasttest;
 (4 rows)
 
 -- If the length is less than zero, an ERROR is thrown.
-SELECT substr(f1, 5, -1) from toasttest;
+SELECT substr(f1, 5, -1) from toasttest_oid;
+ERROR:  negative substring length not allowed
+SELECT substr(f1, 5, -1) from toasttest_oid8;
 ERROR:  negative substring length not allowed
 -- If no third argument (length) is provided, the length to the end of the
 -- string is assumed.
-SELECT substr(f1, 99995) from toasttest;
+SELECT substr(f1, 99995) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995) from toasttest_oid8;
  substr 
 --------
  567890
@@ -2051,7 +2078,7 @@ SELECT substr(f1, 99995) from toasttest;
 
 -- If start plus length is > string length, the result is truncated to
 -- string length
-SELECT substr(f1, 99995, 10) from toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
  substr 
 --------
  567890
@@ -2060,50 +2087,105 @@ SELECT substr(f1, 99995, 10) from toasttest;
  567890
 (4 rows)
 
-TRUNCATE TABLE toasttest;
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
+SELECT substr(f1, 99995, 10) from toasttest_oid8;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+-- TRUNCATE cases for TOAST relations with OID values.
+TRUNCATE TABLE toasttest_oid;
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
 -- expect >0 blocks
 SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
-  FROM pg_class where relname = 'toasttest';
+  FROM pg_class where relname = 'toasttest_oid';
  is_empty 
 ----------
  f
 (1 row)
 
-TRUNCATE TABLE toasttest;
-ALTER TABLE toasttest set (toast_tuple_target = 4080);
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
+TRUNCATE TABLE toasttest_oid;
+ALTER TABLE toasttest_oid set (toast_tuple_target = 4080);
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
 -- expect 0 blocks
 SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
-  FROM pg_class where relname = 'toasttest';
+  FROM pg_class where relname = 'toasttest_oid';
  is_empty 
 ----------
  t
 (1 row)
 
-DROP TABLE toasttest;
+DROP TABLE toasttest_oid;
+-- TRUNCATE cases for TOAST relation with int8 values.
+TRUNCATE TABLE toasttest_oid8;
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+-- expect >0 blocks
+SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
+  FROM pg_class where relname = 'toasttest_oid8';
+ is_empty 
+----------
+ f
+(1 row)
+
+TRUNCATE TABLE toasttest_oid8;
+ALTER TABLE toasttest_oid8 set (toast_tuple_target = 4080);
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+-- expect 0 blocks
+SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
+  FROM pg_class where relname = 'toasttest_oid8';
+ is_empty 
+----------
+ t
+(1 row)
+
+DROP TABLE toasttest_oid8;
 --
--- test substr with toasted bytea values
+-- test substr with toasted bytea values, for all types of TOAST relations
+-- supported. Do not drop these two relations, for pg_upgrade.
 --
-CREATE TABLE toasttest(f1 bytea);
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
+CREATE TABLE toasttest_oid(f1 bytea) WITH (toast_value_type = 'oid');
+CREATE TABLE toasttest_oid8(f1 bytea) WITH (toast_value_type = 'oid8');
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
 --
 -- Ensure that some values are uncompressed, to test the faster substring
 -- operation used in that case
 --
-alter table toasttest alter column f1 set storage external;
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
+alter table toasttest_oid alter column f1 set storage external;
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+alter table toasttest_oid8 alter column f1 set storage external;
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
 -- If the starting position is zero or less, then return from the start of the string
 -- adjusting the length to be consistent with the "negative start" per SQL.
-SELECT substr(f1, -1, 5) from toasttest;
+SELECT substr(f1, -1, 5) from toasttest_oid;
+ substr 
+--------
+ 123
+ 123
+ 123
+ 123
+(4 rows)
+
+SELECT substr(f1, -1, 5) from toasttest_oid8;
  substr 
 --------
  123
@@ -2113,11 +2195,22 @@ SELECT substr(f1, -1, 5) from toasttest;
 (4 rows)
 
 -- If the length is less than zero, an ERROR is thrown.
-SELECT substr(f1, 5, -1) from toasttest;
+SELECT substr(f1, 5, -1) from toasttest_oid;
+ERROR:  negative substring length not allowed
+SELECT substr(f1, 5, -1) from toasttest_oid8;
 ERROR:  negative substring length not allowed
 -- If no third argument (length) is provided, the length to the end of the
 -- string is assumed.
-SELECT substr(f1, 99995) from toasttest;
+SELECT substr(f1, 99995) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995) from toasttest_oid8;
  substr 
 --------
  567890
@@ -2128,7 +2221,72 @@ SELECT substr(f1, 99995) from toasttest;
 
 -- If start plus length is > string length, the result is truncated to
 -- string length
-SELECT substr(f1, 99995, 10) from toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995, 10) from toasttest_oid8;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+-- A relation rewrite leaves the TOAST value attributes unchanged.
+VACUUM FULL toasttest_oid;
+VACUUM FULL toasttest_oid8;
+SELECT c1.relname, a.atttypid::regtype
+  FROM pg_attribute AS a,
+       pg_class AS c1,
+       pg_class AS c2
+  WHERE
+       c1.relname IN ('toasttest_oid', 'toasttest_oid8') AND
+       c1.reltoastrelid = c2.oid AND
+       a.attrelid = c2.oid AND
+       a.attname = 'chunk_id'
+  ORDER BY c1.relname COLLATE "C";
+    relname     | atttypid 
+----------------+----------
+ toasttest_oid  | oid
+ toasttest_oid8 | oid8
+(2 rows)
+
+-- Check that data slices are still accessible.
+SELECT substr(f1, 99995) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995) from toasttest_oid8;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+ substr 
+--------
+ 567890
+ 567890
+ 567890
+ 567890
+(4 rows)
+
+SELECT substr(f1, 99995, 10) from toasttest_oid8;
  substr 
 --------
  567890
@@ -2137,7 +2295,6 @@ SELECT substr(f1, 99995, 10) from toasttest;
  567890
 (4 rows)
 
-DROP TABLE toasttest;
 -- test internally compressing datums
 -- this tests compressing a datum to a very small size which exercises a
 -- corner case in packed-varlena handling: even though small, the compressed
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 9ddcacec6bf4..88faa57772c3 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -578,15 +578,15 @@ WHERE c1.relnatts != (SELECT count(*) FROM pg_attribute AS a1
 (0 rows)
 
 -- Cross-check against pg_type entry
--- NOTE: we allow attstorage to be 'plain' even when typstorage is not;
--- this is mainly for toast tables.
+-- NOTE: we allow attstorage to be 'plain' or 'external' even when typstorage
+-- is not; this is mainly for toast tables.
 SELECT a1.attrelid, a1.attname, t1.oid, t1.typname
 FROM pg_attribute AS a1, pg_type AS t1
 WHERE a1.atttypid = t1.oid AND
     (a1.attlen != t1.typlen OR
      a1.attalign != t1.typalign OR
      a1.attbyval != t1.typbyval OR
-     (a1.attstorage != t1.typstorage AND a1.attstorage != 'p'));
+     (a1.attstorage != t1.typstorage AND a1.attstorage NOT IN ('e', 'p')));
  attrelid | attname | oid | typname 
 ----------+---------+-----+---------
 (0 rows)
diff --git a/src/test/regress/sql/strings.sql b/src/test/regress/sql/strings.sql
index 88aa4c2983ba..5c97f5e72eb5 100644
--- a/src/test/regress/sql/strings.sql
+++ b/src/test/regress/sql/strings.sql
@@ -572,89 +572,147 @@ SELECT text 'text' || char(20) ' and characters' AS "Concat text to char";
 SELECT text 'text' || varchar ' and varchar' AS "Concat text to varchar";
 
 --
--- test substr with toasted text values
+-- Test substr with toasted text values, for all types of TOAST relations
+-- supported.
 --
-CREATE TABLE toasttest(f1 text);
+CREATE TABLE toasttest_oid(f1 text) with (toast_value_type = 'oid');
+CREATE TABLE toasttest_oid8(f1 text) with (toast_value_type = 'oid8');
 
-insert into toasttest values(repeat('1234567890',10000));
-insert into toasttest values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid8 values(repeat('1234567890',10000));
+insert into toasttest_oid8 values(repeat('1234567890',10000));
 
 --
 -- Ensure that some values are uncompressed, to test the faster substring
 -- operation used in that case
 --
-alter table toasttest alter column f1 set storage external;
-insert into toasttest values(repeat('1234567890',10000));
-insert into toasttest values(repeat('1234567890',10000));
+alter table toasttest_oid alter column f1 set storage external;
+insert into toasttest_oid values(repeat('1234567890',10000));
+insert into toasttest_oid values(repeat('1234567890',10000));
+alter table toasttest_oid8 alter column f1 set storage external;
+insert into toasttest_oid8 values(repeat('1234567890',10000));
+insert into toasttest_oid8 values(repeat('1234567890',10000));
 
 -- If the starting position is zero or less, then return from the start of the string
 -- adjusting the length to be consistent with the "negative start" per SQL.
-SELECT substr(f1, -1, 5) from toasttest;
+SELECT substr(f1, -1, 5) from toasttest_oid;
+SELECT substr(f1, -1, 5) from toasttest_oid8;
 
 -- If the length is less than zero, an ERROR is thrown.
-SELECT substr(f1, 5, -1) from toasttest;
+SELECT substr(f1, 5, -1) from toasttest_oid;
+SELECT substr(f1, 5, -1) from toasttest_oid8;
 
 -- If no third argument (length) is provided, the length to the end of the
 -- string is assumed.
-SELECT substr(f1, 99995) from toasttest;
+SELECT substr(f1, 99995) from toasttest_oid;
+SELECT substr(f1, 99995) from toasttest_oid8;
 
 -- If start plus length is > string length, the result is truncated to
 -- string length
-SELECT substr(f1, 99995, 10) from toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+SELECT substr(f1, 99995, 10) from toasttest_oid8;
 
-TRUNCATE TABLE toasttest;
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
+-- TRUNCATE cases for TOAST relations with OID values.
+TRUNCATE TABLE toasttest_oid;
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
 -- expect >0 blocks
 SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
-  FROM pg_class where relname = 'toasttest';
-
-TRUNCATE TABLE toasttest;
-ALTER TABLE toasttest set (toast_tuple_target = 4080);
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
-INSERT INTO toasttest values (repeat('1234567890',300));
+  FROM pg_class where relname = 'toasttest_oid';
+TRUNCATE TABLE toasttest_oid;
+ALTER TABLE toasttest_oid set (toast_tuple_target = 4080);
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
+INSERT INTO toasttest_oid values (repeat('1234567890',300));
 -- expect 0 blocks
 SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
-  FROM pg_class where relname = 'toasttest';
+  FROM pg_class where relname = 'toasttest_oid';
+DROP TABLE toasttest_oid;
 
-DROP TABLE toasttest;
+-- TRUNCATE cases for TOAST relation with int8 values.
+TRUNCATE TABLE toasttest_oid8;
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+-- expect >0 blocks
+SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
+  FROM pg_class where relname = 'toasttest_oid8';
+TRUNCATE TABLE toasttest_oid8;
+ALTER TABLE toasttest_oid8 set (toast_tuple_target = 4080);
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+INSERT INTO toasttest_oid8 values (repeat('1234567890',300));
+-- expect 0 blocks
+SELECT pg_relation_size(reltoastrelid) = 0 AS is_empty
+  FROM pg_class where relname = 'toasttest_oid8';
+DROP TABLE toasttest_oid8;
 
 --
--- test substr with toasted bytea values
+-- test substr with toasted bytea values, for all types of TOAST relations
+-- supported. Do not drop these two relations, for pg_upgrade.
 --
-CREATE TABLE toasttest(f1 bytea);
+CREATE TABLE toasttest_oid(f1 bytea) WITH (toast_value_type = 'oid');
+CREATE TABLE toasttest_oid8(f1 bytea) WITH (toast_value_type = 'oid8');
 
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
 
 --
 -- Ensure that some values are uncompressed, to test the faster substring
 -- operation used in that case
 --
-alter table toasttest alter column f1 set storage external;
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
-insert into toasttest values(decode(repeat('1234567890',10000),'escape'));
+alter table toasttest_oid alter column f1 set storage external;
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid values(decode(repeat('1234567890',10000),'escape'));
+alter table toasttest_oid8 alter column f1 set storage external;
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
+insert into toasttest_oid8 values(decode(repeat('1234567890',10000),'escape'));
 
 -- If the starting position is zero or less, then return from the start of the string
 -- adjusting the length to be consistent with the "negative start" per SQL.
-SELECT substr(f1, -1, 5) from toasttest;
+SELECT substr(f1, -1, 5) from toasttest_oid;
+SELECT substr(f1, -1, 5) from toasttest_oid8;
 
 -- If the length is less than zero, an ERROR is thrown.
-SELECT substr(f1, 5, -1) from toasttest;
+SELECT substr(f1, 5, -1) from toasttest_oid;
+SELECT substr(f1, 5, -1) from toasttest_oid8;
 
 -- If no third argument (length) is provided, the length to the end of the
 -- string is assumed.
-SELECT substr(f1, 99995) from toasttest;
+SELECT substr(f1, 99995) from toasttest_oid;
+SELECT substr(f1, 99995) from toasttest_oid8;
 
 -- If start plus length is > string length, the result is truncated to
 -- string length
-SELECT substr(f1, 99995, 10) from toasttest;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+SELECT substr(f1, 99995, 10) from toasttest_oid8;
 
-DROP TABLE toasttest;
+-- A relation rewrite leaves the TOAST value attributes unchanged.
+VACUUM FULL toasttest_oid;
+VACUUM FULL toasttest_oid8;
+SELECT c1.relname, a.atttypid::regtype
+  FROM pg_attribute AS a,
+       pg_class AS c1,
+       pg_class AS c2
+  WHERE
+       c1.relname IN ('toasttest_oid', 'toasttest_oid8') AND
+       c1.reltoastrelid = c2.oid AND
+       a.attrelid = c2.oid AND
+       a.attname = 'chunk_id'
+  ORDER BY c1.relname COLLATE "C";
+-- Check that data slices are still accessible.
+SELECT substr(f1, 99995) from toasttest_oid;
+SELECT substr(f1, 99995) from toasttest_oid8;
+SELECT substr(f1, 99995, 10) from toasttest_oid;
+SELECT substr(f1, 99995, 10) from toasttest_oid8;
 
 -- test internally compressing datums
 
diff --git a/src/test/regress/sql/type_sanity.sql b/src/test/regress/sql/type_sanity.sql
index c2496823d90e..a0d2e8bcf00b 100644
--- a/src/test/regress/sql/type_sanity.sql
+++ b/src/test/regress/sql/type_sanity.sql
@@ -420,8 +420,8 @@ WHERE c1.relnatts != (SELECT count(*) FROM pg_attribute AS a1
                       WHERE a1.attrelid = c1.oid AND a1.attnum > 0);
 
 -- Cross-check against pg_type entry
--- NOTE: we allow attstorage to be 'plain' even when typstorage is not;
--- this is mainly for toast tables.
+-- NOTE: we allow attstorage to be 'plain' or 'external' even when typstorage
+-- is not; this is mainly for toast tables.
 
 SELECT a1.attrelid, a1.attname, t1.oid, t1.typname
 FROM pg_attribute AS a1, pg_type AS t1
@@ -429,7 +429,7 @@ WHERE a1.atttypid = t1.oid AND
     (a1.attlen != t1.typlen OR
      a1.attalign != t1.typalign OR
      a1.attbyval != t1.typbyval OR
-     (a1.attstorage != t1.typstorage AND a1.attstorage != 'p'));
+     (a1.attstorage != t1.typstorage AND a1.attstorage NOT IN ('e', 'p')));
 
 -- Look for IsCatalogTextUniqueIndexOid() omissions.
 
-- 
2.51.0

v10-0014-Add-new-vartag_external-for-8-byte-TOAST-values.patchtext/x-diff; charset=us-asciiDownload
From 659aeca19db6b6c5d1adecaa6c44d5a31dbaec85 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 14 Aug 2025 14:10:36 +0900
Subject: [PATCH v10 14/14] Add new vartag_external for 8-byte TOAST values

This is a new type of external TOAST pointer, able to be fed 8-byte
TOAST values.  It uses a dedicated vartag_external, which is used when
a TOAST table uses bigint for its chunk_id.

The relevant callbacks are added to toast_external.c.
---
 src/include/access/heaptoast.h                |   8 +-
 src/include/varatt.h                          |  34 +++-
 src/backend/access/common/toast_external.c    | 145 ++++++++++++++++--
 src/backend/access/heap/heaptoast.c           |   1 +
 .../replication/logical/reorderbuffer.c       |  10 +-
 doc/src/sgml/storage.sgml                     |   6 +-
 contrib/amcheck/verify_heapam.c               |   2 +-
 7 files changed, 189 insertions(+), 17 deletions(-)

diff --git a/src/include/access/heaptoast.h b/src/include/access/heaptoast.h
index 3128539f4716..227c5fca0004 100644
--- a/src/include/access/heaptoast.h
+++ b/src/include/access/heaptoast.h
@@ -81,6 +81,12 @@
 
 #define EXTERN_TUPLE_MAX_SIZE	MaximumBytesPerTuple(EXTERN_TUPLES_PER_PAGE)
 
+#define TOAST_OID8_MAX_CHUNK_SIZE	\
+	(EXTERN_TUPLE_MAX_SIZE -							\
+	 MAXALIGN(SizeofHeapTupleHeader) -					\
+	 (sizeof(uint32) * 2) -								\
+	 sizeof(int32) -									\
+	 VARHDRSZ)
 #define TOAST_OID_MAX_CHUNK_SIZE	\
 	(EXTERN_TUPLE_MAX_SIZE -							\
 	 MAXALIGN(SizeofHeapTupleHeader) -					\
@@ -89,7 +95,7 @@
 	 VARHDRSZ)
 
 /* Maximum size of chunk possible */
-#define TOAST_MAX_CHUNK_SIZE	TOAST_OID_MAX_CHUNK_SIZE
+#define TOAST_MAX_CHUNK_SIZE	Max(TOAST_OID_MAX_CHUNK_SIZE, TOAST_OID8_MAX_CHUNK_SIZE)
 
 /* ----------
  * heap_toast_insert_or_update -
diff --git a/src/include/varatt.h b/src/include/varatt.h
index 35baefd2a748..3127f96ff965 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -41,6 +41,27 @@ typedef struct varatt_external_oid
 	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
 }			varatt_external_oid;
 
+/*
+ * struct varatt_external_oid8 is a "larger" version of "TOAST pointer",
+ * that uses an 8-byte integer as value.
+ *
+ * This follows the same properties as varatt_external_oid, except that
+ * this is used in TOAST relations with oid8 as attribute for chunk_id.
+ */
+typedef struct varatt_external_oid8
+{
+	int32		va_rawsize;		/* Original data size (includes header) */
+	uint32		va_extinfo;		/* External saved size (without header) and
+								 * compression method */
+	/*
+	 * Unique ID of value within TOAST table, as two uint32 for alignment
+	 * and padding.
+	 */
+	uint32		va_valueid_lo;
+	uint32		va_valueid_hi;
+	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
+}			varatt_external_oid8;
+
 /*
  * These macros define the "saved size" portion of va_extinfo.  Its remaining
  * two high-order bits identify the compression method.
@@ -90,6 +111,7 @@ typedef enum vartag_external
 	VARTAG_INDIRECT = 1,
 	VARTAG_EXPANDED_RO = 2,
 	VARTAG_EXPANDED_RW = 3,
+	VARTAG_ONDISK_OID8 = 4,
 	VARTAG_ONDISK_OID = 18
 } vartag_external;
 
@@ -111,6 +133,8 @@ VARTAG_SIZE(vartag_external tag)
 		return sizeof(varatt_expanded);
 	else if (tag == VARTAG_ONDISK_OID)
 		return sizeof(varatt_external_oid);
+	else if (tag == VARTAG_ONDISK_OID8)
+		return sizeof(varatt_external_oid8);
 	else
 	{
 		Assert(false);
@@ -367,11 +391,19 @@ VARATT_IS_EXTERNAL_ONDISK_OID(const void *PTR)
 	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID;
 }
 
+/* Is varlena datum a pointer to on-disk toasted data with OID8 value? */
+static inline bool
+VARATT_IS_EXTERNAL_ONDISK_OID8(const void *PTR)
+{
+	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_OID8;
+}
+
 /* Is varlena datum a pointer to on-disk toasted data? */
 static inline bool
 VARATT_IS_EXTERNAL_ONDISK(const void *PTR)
 {
-	return VARATT_IS_EXTERNAL_ONDISK_OID(PTR);
+	return VARATT_IS_EXTERNAL_ONDISK_OID(PTR) ||
+		VARATT_IS_EXTERNAL_ONDISK_OID8(PTR);
 }
 
 /* Is varlena datum an indirect pointer? */
diff --git a/src/backend/access/common/toast_external.c b/src/backend/access/common/toast_external.c
index e2f0a9dc1c50..431258b2be96 100644
--- a/src/backend/access/common/toast_external.c
+++ b/src/backend/access/common/toast_external.c
@@ -18,8 +18,19 @@
 #include "postgres.h"
 
 #include "access/detoast.h"
+#include "access/genam.h"
 #include "access/heaptoast.h"
 #include "access/toast_external.h"
+#include "catalog/catalog.h"
+#include "miscadmin.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+
+
+/* Callbacks for VARTAG_ONDISK_OID8 */
+static void ondisk_oid8_to_external_data(struct varlena *attr,
+										 toast_external_data *data);
+static struct varlena *ondisk_oid8_create_external_data(toast_external_data data);
 
 /* Callbacks for VARTAG_ONDISK_OID */
 static void ondisk_oid_to_external_data(struct varlena *attr,
@@ -28,7 +39,7 @@ static struct varlena *ondisk_oid_create_external_data(toast_external_data data)
 
 /*
  * Fetch the possibly-unaligned contents of an on-disk external TOAST with
- * OID values into a local "varatt_external_oid" pointer.
+ * OID or OID8 values into a local "varatt_external_*" pointer.
  *
  * This should be just a memcpy, but some versions of gcc seem to produce
  * broken code that assumes the datum contents are aligned.  Introducing
@@ -45,9 +56,20 @@ varatt_external_oid_get_pointer(varatt_external_oid *toast_pointer,
 	memcpy(toast_pointer, VARDATA_EXTERNAL(attre), sizeof(varatt_external_oid));
 }
 
+static inline void
+varatt_external_oid8_get_pointer(varatt_external_oid8 *toast_pointer,
+								 struct varlena *attr)
+{
+	varattrib_1b_e *attre = (varattrib_1b_e *) attr;
+
+	Assert(VARATT_IS_EXTERNAL_ONDISK_OID8(attre));
+	Assert(VARSIZE_EXTERNAL(attre) == sizeof(varatt_external_oid8) + VARHDRSZ_EXTERNAL);
+	memcpy(toast_pointer, VARDATA_EXTERNAL(attre), sizeof(varatt_external_oid8));
+}
+
 /*
  * Decompressed size of an on-disk varlena; but note argument is a struct
- * varatt_external_oid.
+ * varatt_external_oid or varatt_external_oid8.
  */
 static inline Size
 varatt_external_oid_get_extsize(varatt_external_oid toast_pointer)
@@ -55,9 +77,15 @@ varatt_external_oid_get_extsize(varatt_external_oid toast_pointer)
 	return toast_pointer.va_extinfo & VARLENA_EXTSIZE_MASK;
 }
 
+static inline Size
+varatt_external_oid8_get_extsize(varatt_external_oid8 toast_pointer)
+{
+	return toast_pointer.va_extinfo & VARLENA_EXTSIZE_MASK;
+}
+
 /*
  * Compression method of an on-disk varlena; but note argument is a struct
- *  varatt_external_oid.
+ *  varatt_external_oid or varatt_external_oid8.
  */
 static inline uint32
 varatt_external_oid_get_compress_method(varatt_external_oid toast_pointer)
@@ -65,6 +93,12 @@ varatt_external_oid_get_compress_method(varatt_external_oid toast_pointer)
 	return toast_pointer.va_extinfo >> VARLENA_EXTSIZE_BITS;
 }
 
+static inline uint32
+varatt_external_oid8_get_compress_method(varatt_external_oid8 toast_pointer)
+{
+	return toast_pointer.va_extinfo >> VARLENA_EXTSIZE_BITS;
+}
+
 /*
  * Testing whether an externally-stored TOAST value is compressed now requires
  * comparing size stored in va_extinfo (the actual length of the external data)
@@ -79,6 +113,19 @@ varatt_external_oid_is_compressed(varatt_external_oid toast_pointer)
 		(Size) (toast_pointer.va_rawsize - VARHDRSZ);
 }
 
+static inline bool
+varatt_external_oid8_is_compressed(varatt_external_oid8 toast_pointer)
+{
+	return varatt_external_oid8_get_extsize(toast_pointer) <
+		(Size) (toast_pointer.va_rawsize - VARHDRSZ);
+}
+
+/*
+ * Size of an EXTERNAL datum that contains a standard TOAST pointer
+ * (oid8 value).
+ */
+#define TOAST_POINTER_OID8_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external_oid8))
+
 /*
  * Size of an EXTERNAL datum that contains a standard TOAST pointer (OID
  * value).
@@ -99,6 +146,12 @@ varatt_external_oid_is_compressed(varatt_external_oid toast_pointer)
  * individual fields.
  */
 static const toast_external_info toast_external_infos[TOAST_EXTERNAL_INFO_SIZE] = {
+	[VARTAG_ONDISK_OID8] = {
+		.toast_pointer_size = TOAST_POINTER_OID8_SIZE,
+		.maximum_chunk_size = TOAST_OID_MAX_CHUNK_SIZE,
+		.to_external_data = ondisk_oid8_to_external_data,
+		.create_external_data = ondisk_oid8_create_external_data,
+	},
 	[VARTAG_ONDISK_OID] = {
 		.toast_pointer_size = TOAST_OID_POINTER_SIZE,
 		.maximum_chunk_size = TOAST_OID_MAX_CHUNK_SIZE,
@@ -155,22 +208,33 @@ toast_external_info_get_pointer_size(uint8 tag)
 uint8
 toast_external_assign_vartag(Oid toastrelid, Oid8 valueid)
 {
+	Oid		toast_typid;
+
 	/*
-	 * If dealing with a code path where a TOAST relation may not be assigned,
-	 * like heap_toast_insert_or_update(), just use the legacy
-	 * vartag_external.
+	 * If dealing with a code path where a TOAST relation may not be assigned
+	 * like heap_toast_insert_or_update(), just use the default with an OID
+	 * type.
+	 *
+	 * In bootstrap mode, we should not do any kind of syscache lookups,
+	 * so also rely on OID.
 	 */
-	if (!OidIsValid(toastrelid))
+	if (!OidIsValid(toastrelid) || IsBootstrapProcessingMode())
 		return VARTAG_ONDISK_OID;
 
 	/*
-	 * Currently there is only one type of vartag_external supported: 4-byte
-	 * value with OID for the chunk_id type.
+	 * Two types of vartag_external are currently supported: OID and OID8,
+	 * which depend on the type assigned to "chunk_id" for the TOAST table.
 	 *
-	 * Note: This routine will be extended to be able to use multiple
-	 * vartag_external within a single TOAST relation type, that may change
-	 * depending on the value used.
+	 * XXX: Should we assign from the start an OID vartag if dealing with
+	 * a TOAST relation with OID8 as value if the value assigned is less
+	 * than UINT_MAX?  This just takes the "safe" approach of assigning
+	 * the larger vartag in all cases, but this can be made cheaper
+	 * depending on the OID consumption.
 	 */
+	toast_typid = get_atttype(toastrelid, 1);
+	if (toast_typid == OID8OID)
+		return VARTAG_ONDISK_OID8;
+
 	return VARTAG_ONDISK_OID;
 }
 
@@ -179,6 +243,63 @@ toast_external_assign_vartag(Oid toastrelid, Oid8 valueid)
  * the in-memory representation toast_external_data used in the backend.
  */
 
+/* Callbacks for VARTAG_ONDISK_OID8 */
+static void
+ondisk_oid8_to_external_data(struct varlena *attr, toast_external_data *data)
+{
+	varatt_external_oid8	external;
+
+	varatt_external_oid8_get_pointer(&external, attr);
+	data->rawsize = external.va_rawsize;
+
+	/* External size and compression methods are stored in the same field */
+	if (varatt_external_oid8_is_compressed(external))
+	{
+		data->extsize = varatt_external_oid8_get_extsize(external);
+		data->compression_method = varatt_external_oid8_get_compress_method(external);
+	}
+	else
+	{
+		data->extsize = external.va_extinfo;
+		data->compression_method = TOAST_INVALID_COMPRESSION_ID;
+	}
+
+	data->valueid = (((uint64) external.va_valueid_hi) << 32) |
+		external.va_valueid_lo;
+	data->toastrelid = external.va_toastrelid;
+
+}
+
+static struct varlena *
+ondisk_oid8_create_external_data(toast_external_data data)
+{
+	struct varlena *result = NULL;
+	varatt_external_oid8 external;
+
+	external.va_rawsize = data.rawsize;
+
+	if (data.compression_method != TOAST_INVALID_COMPRESSION_ID)
+	{
+		/* Set size and compression method, in a single field. */
+		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(external,
+													 data.extsize,
+													 data.compression_method);
+	}
+	else
+		external.va_extinfo = data.extsize;
+
+	external.va_toastrelid = data.toastrelid;
+	external.va_valueid_hi = (((uint64) data.valueid) >> 32);
+	external.va_valueid_lo = (uint32) data.valueid;
+
+	result = (struct varlena *) palloc(TOAST_POINTER_OID8_SIZE);
+	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_OID8);
+	memcpy(VARDATA_EXTERNAL(result), &external, sizeof(external));
+
+	return result;
+}
+
+
 /* Callbacks for VARTAG_ONDISK_OID */
 
 /*
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index f669b75e2362..a94740125a87 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -32,6 +32,7 @@
 #include "access/toast_helper.h"
 #include "access/toast_internals.h"
 #include "utils/fmgroids.h"
+#include "utils/syscache.h"
 
 
 /* ----------
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 681eb9930587..e89fbfa67b03 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -5005,14 +5005,22 @@ ReorderBufferToastAppendChunk(ReorderBuffer *rb, ReorderBufferTXN *txn,
 	TupleDesc	desc = RelationGetDescr(relation);
 	Oid8		chunk_id;
 	int32		chunk_seq;
+	Oid			toast_typid;
 
 	if (txn->toast_hash == NULL)
 		ReorderBufferToastInitHash(rb, txn);
+	toast_typid = TupleDescAttr(desc, 0)->atttypid;
 
 	Assert(IsToastRelation(relation));
 
 	newtup = change->data.tp.newtuple;
-	chunk_id = DatumGetObjectId(fastgetattr(newtup, 1, desc, &isnull));
+	/* This depends on the type of TOAST value dealt with. */
+	if (toast_typid == OIDOID)
+		chunk_id = DatumGetObjectId(fastgetattr(newtup, 1, desc, &isnull));
+	else if (toast_typid == INT8OID)
+		chunk_id = DatumGetUInt64(fastgetattr(newtup, 1, desc, &isnull));
+	else
+		Assert(false);
 	Assert(!isnull);
 	chunk_seq = DatumGetInt32(fastgetattr(newtup, 2, desc, &isnull));
 	Assert(!isnull);
diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml
index afddf663fec5..dbec30d48b4a 100644
--- a/doc/src/sgml/storage.sgml
+++ b/doc/src/sgml/storage.sgml
@@ -417,7 +417,11 @@ described in more detail below.
 
 <para>
 Out-of-line values are divided (after compression if used) into chunks of at
-most <symbol>TOAST_OID_MAX_CHUNK_SIZE</symbol> bytes (by default this value is chosen
+most <symbol>TOAST_OID_MAX_CHUNK_SIZE</symbol> bytes if the
+<acronym>TOAST</acronym> relation uses the <literal>oid</literal> type for
+<literal>chunk_id</literal>, or <symbol>TOAST_OID8_MAX_CHUNK_SIZE</symbol>
+bytes if the <acronym>TOAST</acronym> relation uses the <literal>oid8</literal>
+type for <literal>chunk_id</literal> (by default these values are chosen
 so that four chunk rows will fit on a page, making it about 2000 bytes).
 Each chunk is stored as a separate row in the <acronym>TOAST</acronym> table
 belonging to the owning table.  Every
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 7f1aea8cc93e..e5bb40621faf 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1733,7 +1733,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	{
 		uint8		va_tag = VARTAG_EXTERNAL(tp + ctx->offset);
 
-		if (va_tag != VARTAG_ONDISK_OID)
+		if (va_tag != VARTAG_ONDISK_OID && va_tag != VARTAG_ONDISK_OID8)
 		{
 			report_corruption(ctx,
 							  psprintf("toasted attribute has unexpected TOAST tag %u",
-- 
2.51.0