[PATCH] Add zstd compression for TOAST using extended header format

Started by Dharin Shah30 days ago17 messages
#1Dharin Shah
dharinshah95@gmail.com
1 attachment(s)

Hello PG Hackers,

Want to submit a patch that implements zstd compression for TOAST data
using a 20-byte TOAST pointer format, directly addressing the concerns
raised in prior discussions [1
</messages/by-id/CAFAfj_F4qeRCNCYPk1vgH42fDZpjQWKO+ufq3FyoVyUa5AviFA@mail.gmail.com
][2
</messages/by-id/CAJ7c6TOtAB0z1UrksvGTStNE-herK-43bj22=5xVBg7S4vr5rQ@mail.gmail.com&gt;
][3
</messages/by-id/YoMiNmkztrslDbNS@paquier.xyz&gt;%5D.

A bit of a background in the 2022 thread [3
</messages/by-id/YoMiNmkztrslDbNS@paquier.xyz&gt;%5D,
Robert Haas suggested:
"we had better reserve the fourth bit pattern for something extensible e.g.
another byte or several to specify the actual method"

i.e. something like:
00 = PGLZ
01 = LZ4
10 = reserved for future emergencies
11 = extended header with additional type byte

Michael also asked whether we should have "something a bit more extensible
for the design of an extensible varlena header."

This patch implements that idea.
The format:

struct varatt_external_extended {
int32 va_rawsize; /* same as legacy */
uint32 va_extinfo; /* cmid=3 signals extended format */
uint8 va_flags; /* feature flags */
uint8 va_data[3]; /* va_data[0] = compression method */
Oid va_valueid; /* same as legacy */
Oid va_toastrelid; /* same as legacy */
};

*A few notes:*

- Zstd only applies to external TOAST, not inline compression. The 2-bit
limit in va_tcinfo stays as-is for inline data, where pglz/lz4 work fine
anyway. Zstd's wins show up on larger values.
- A GUC use_extended_toast_header controls whether pglz/lz4 also use the
20-byte format (defaults to off for compatibility, can enable it if you
want consistency).
- Legacy 16-byte pointers continue to work - we check the vartag to
determine which format to read.

The 4 extra bytes per pointer is negligible for typical TOAST data sizes,
and it gives us room to grow.

Regards,
Dharin

Attachments:

zstd-toast-compression-external.patchapplication/octet-stream; name=zstd-toast-compression-external.patchDownload
From ee7ba3a0a160bfe8811bad2230b2a023175180d5 Mon Sep 17 00:00:00 2001
From: Dharin Shah <8616130+Dharin-shah@users.noreply.github.com>
Date: Sat, 13 Dec 2025 11:16:35 +0100
Subject: [PATCH] Add zstd compression support for TOAST using extended header
 format

---
 contrib/amcheck/verify_heapam.c               |  69 +++++-
 src/backend/access/common/detoast.c           | 164 ++++++++++---
 src/backend/access/common/toast_compression.c | 199 ++++++++++++++-
 src/backend/access/common/toast_internals.c   | 198 +++++++++++++--
 src/backend/access/table/toast_helper.c       |   2 +-
 .../replication/logical/reorderbuffer.c       |  38 ++-
 src/backend/utils/adt/varlena.c               |  26 +-
 src/backend/utils/misc/guc_parameters.dat     |   7 +-
 src/backend/utils/misc/guc_tables.c           |   3 +
 src/include/access/detoast.h                  |  41 +++-
 src/include/access/toast_compression.h        |  36 +++
 src/include/access/toast_internals.h          |  10 +-
 src/include/varatt.h                          | 160 +++++++++++-
 src/test/modules/meson.build                  |   1 +
 src/test/modules/test_toast_ext/Makefile      |  20 ++
 .../expected/test_toast_ext.out               | 229 ++++++++++++++++++
 src/test/modules/test_toast_ext/meson.build   |  33 +++
 .../test_toast_ext/sql/test_toast_ext.sql     | 169 +++++++++++++
 .../test_toast_ext/test_toast_ext--1.0.sql    |  19 ++
 .../modules/test_toast_ext/test_toast_ext.c   | 200 +++++++++++++++
 .../test_toast_ext/test_toast_ext.control     |   5 +
 21 files changed, 1538 insertions(+), 91 deletions(-)
 create mode 100644 src/test/modules/test_toast_ext/Makefile
 create mode 100644 src/test/modules/test_toast_ext/expected/test_toast_ext.out
 create mode 100644 src/test/modules/test_toast_ext/meson.build
 create mode 100644 src/test/modules/test_toast_ext/sql/test_toast_ext.sql
 create mode 100644 src/test/modules/test_toast_ext/test_toast_ext--1.0.sql
 create mode 100644 src/test/modules/test_toast_ext/test_toast_ext.c
 create mode 100644 src/test/modules/test_toast_ext/test_toast_ext.control

diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 130b3533463..25cae4d0380 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1665,6 +1665,8 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	uint16		infomask;
 	CompactAttribute *thisatt;
 	struct varatt_external toast_pointer;
+	struct varatt_external_extended toast_pointer_ext;
+	bool		is_extended;
 
 	infomask = ctx->tuphdr->t_infomask;
 	thisatt = TupleDescCompactAttr(RelationGetDescr(ctx->rel), ctx->attnum);
@@ -1717,13 +1719,14 @@ check_tuple_attribute(HeapCheckContext *ctx)
 
 	/*
 	 * Check that VARTAG_SIZE won't hit an Assert on a corrupt va_tag before
-	 * risking a call into att_addlength_pointer
+	 * risking a call into att_addlength_pointer.  Both legacy (VARTAG_ONDISK)
+	 * and extended (VARTAG_ONDISK_EXTENDED) on-disk formats are valid.
 	 */
 	if (VARATT_IS_EXTERNAL(tp + ctx->offset))
 	{
 		uint8		va_tag = VARTAG_EXTERNAL(tp + ctx->offset);
 
-		if (va_tag != VARTAG_ONDISK)
+		if (va_tag != VARTAG_ONDISK && va_tag != VARTAG_ONDISK_EXTENDED)
 		{
 			report_corruption(ctx,
 							  psprintf("toasted attribute has unexpected TOAST tag %u",
@@ -1768,9 +1771,23 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	/* It is external, and we're looking at a page on disk */
 
 	/*
-	 * Must copy attr into toast_pointer for alignment considerations
+	 * Must copy attr into toast_pointer for alignment considerations.
+	 * Handle both legacy (VARTAG_ONDISK) and extended (VARTAG_ONDISK_EXTENDED)
+	 * formats.
 	 */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	is_extended = (VARTAG_EXTERNAL(attr) == VARTAG_ONDISK_EXTENDED);
+
+	if (is_extended)
+	{
+		VARATT_EXTERNAL_GET_POINTER_EXTENDED(toast_pointer_ext, attr);
+		/* Copy common fields for simpler code below */
+		toast_pointer.va_rawsize = toast_pointer_ext.va_rawsize;
+		toast_pointer.va_extinfo = toast_pointer_ext.va_extinfo;
+		toast_pointer.va_valueid = toast_pointer_ext.va_valueid;
+		toast_pointer.va_toastrelid = toast_pointer_ext.va_toastrelid;
+	}
+	else
+		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 
 	/* Toasted attributes too large to be untoasted should never be stored */
 	if (toast_pointer.va_rawsize > VARLENA_SIZE_LIMIT)
@@ -1785,8 +1802,11 @@ check_tuple_attribute(HeapCheckContext *ctx)
 		ToastCompressionId cmid;
 		bool		valid = false;
 
-		/* Compressed attributes should have a valid compression method */
-		cmid = TOAST_COMPRESS_METHOD(&toast_pointer);
+		/*
+		 * Compressed attributes should have a valid compression method.
+		 * For extended pointers with cmid==3, the actual method is in va_data[0].
+		 */
+		cmid = VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer);
 		switch (cmid)
 		{
 				/* List of all valid compression method IDs */
@@ -1795,6 +1815,27 @@ check_tuple_attribute(HeapCheckContext *ctx)
 				valid = true;
 				break;
 
+				/* Extended compression (zstd or pglz/lz4 in extended format) */
+			case TOAST_EXTENDED_COMPRESSION_ID:
+				if (is_extended)
+				{
+					uint8	ext_method = VARATT_EXTERNAL_GET_EXT_COMPRESSION_METHOD(toast_pointer_ext);
+
+					/* Validate extended compression method */
+					switch (ext_method)
+					{
+						case TOAST_PGLZ_EXT_METHOD:
+						case TOAST_LZ4_EXT_METHOD:
+						case TOAST_ZSTD_EXT_METHOD:
+							valid = true;
+							break;
+						default:
+							/* Invalid extended method will be reported below */
+							break;
+					}
+				}
+				break;
+
 				/* Recognized but invalid compression method ID */
 			case TOAST_INVALID_COMPRESSION_ID:
 				break;
@@ -1840,7 +1881,21 @@ check_tuple_attribute(HeapCheckContext *ctx)
 
 		ta = palloc0_object(ToastedAttribute);
 
-		VARATT_EXTERNAL_GET_POINTER(ta->toast_pointer, attr);
+		/*
+		 * Extract toast pointer based on format.  For extended format,
+		 * copy common fields from toast_pointer which we already extracted
+		 * above.
+		 */
+		if (is_extended)
+		{
+			ta->toast_pointer.va_rawsize = toast_pointer.va_rawsize;
+			ta->toast_pointer.va_extinfo = toast_pointer.va_extinfo;
+			ta->toast_pointer.va_valueid = toast_pointer.va_valueid;
+			ta->toast_pointer.va_toastrelid = toast_pointer.va_toastrelid;
+		}
+		else
+			VARATT_EXTERNAL_GET_POINTER(ta->toast_pointer, attr);
+
 		ta->blkno = ctx->blkno;
 		ta->offnum = ctx->offnum;
 		ta->attnum = ctx->attnum;
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 62651787742..6d1c08900e8 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_compression.h"
 #include "access/toast_internals.h"
 #include "common/int.h"
 #include "common/pg_lzcompress.h"
@@ -225,12 +226,47 @@ detoast_attr_slice(struct varlena *attr,
 
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
-		struct varatt_external toast_pointer;
+		int32		max_size;
+		bool		is_compressed;
+		bool		is_pglz = false;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		/*
+		 * Handle both legacy 16-byte and extended 20-byte on-disk TOAST
+		 * pointers.  Check the vartag to determine which format.
+		 */
+		if (VARTAG_EXTERNAL(attr) == VARTAG_ONDISK_EXTENDED)
+		{
+			struct varatt_external_extended toast_pointer_ext;
+			uint8		ext_method;
+
+			VARATT_EXTERNAL_GET_POINTER_EXTENDED(toast_pointer_ext, attr);
+			max_size = VARATT_EXTERNAL_GET_EXTSIZE_EXTENDED(toast_pointer_ext);
+			is_compressed = VARATT_EXTERNAL_IS_COMPRESSED_EXTENDED(toast_pointer_ext);
+
+			/* Check if this is pglz for slice optimization */
+			if (is_compressed &&
+				VARATT_EXTERNAL_HAS_FLAG(toast_pointer_ext, TOAST_EXT_FLAG_COMPRESSION))
+			{
+				ext_method = VARATT_EXTERNAL_GET_EXT_COMPRESSION_METHOD(toast_pointer_ext);
+				is_pglz = (ext_method == TOAST_PGLZ_EXT_METHOD);
+			}
+		}
+		else
+		{
+			struct varatt_external toast_pointer;
+
+			VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+			max_size = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+			is_compressed = VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer);
+
+			/* Check if this is pglz for slice optimization */
+			if (is_compressed)
+				is_pglz = (VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) ==
+						   TOAST_PGLZ_COMPRESSION_ID);
+		}
 
 		/* fast path for non-compressed external datums */
-		if (!VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+		if (!is_compressed)
 			return toast_fetch_datum_slice(attr, sliceoffset, slicelength);
 
 		/*
@@ -240,19 +276,16 @@ detoast_attr_slice(struct varlena *attr,
 		 */
 		if (slicelimit >= 0)
 		{
-			int32		max_size = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
-
 			/*
 			 * 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 zstd 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 (VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) ==
-				TOAST_PGLZ_COMPRESSION_ID)
+			if (is_pglz)
 				max_size = pglz_maximum_compressed_size(slicelimit, max_size);
 
 			/*
@@ -344,20 +377,42 @@ toast_fetch_datum(struct varlena *attr)
 {
 	Relation	toastrel;
 	struct varlena *result;
-	struct varatt_external toast_pointer;
 	int32		attrsize;
+	Oid			toastrelid;
+	Oid			valueid;
+	bool		is_compressed;
 
 	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);
+	/*
+	 * Handle both legacy 16-byte and extended 20-byte on-disk TOAST pointers.
+	 * Check the vartag to determine which format we're dealing with.
+	 */
+	if (VARTAG_EXTERNAL(attr) == VARTAG_ONDISK_EXTENDED)
+	{
+		struct varatt_external_extended toast_pointer_ext;
+
+		VARATT_EXTERNAL_GET_POINTER_EXTENDED(toast_pointer_ext, attr);
+		attrsize = VARATT_EXTERNAL_GET_EXTSIZE_EXTENDED(toast_pointer_ext);
+		toastrelid = toast_pointer_ext.va_toastrelid;
+		valueid = toast_pointer_ext.va_valueid;
+		is_compressed = VARATT_EXTERNAL_IS_COMPRESSED_EXTENDED(toast_pointer_ext);
+	}
+	else
+	{
+		struct varatt_external toast_pointer;
 
-	attrsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		attrsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+		toastrelid = toast_pointer.va_toastrelid;
+		valueid = toast_pointer.va_valueid;
+		is_compressed = VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer);
+	}
 
 	result = (struct varlena *) palloc(attrsize + VARHDRSZ);
 
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+	if (is_compressed)
 		SET_VARSIZE_COMPRESSED(result, attrsize + VARHDRSZ);
 	else
 		SET_VARSIZE(result, attrsize + VARHDRSZ);
@@ -369,10 +424,10 @@ toast_fetch_datum(struct varlena *attr)
 	/*
 	 * Open the toast relation and its indexes
 	 */
-	toastrel = table_open(toast_pointer.va_toastrelid, AccessShareLock);
+	toastrel = table_open(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 +453,45 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 {
 	Relation	toastrel;
 	struct varlena *result;
-	struct varatt_external toast_pointer;
 	int32		attrsize;
+	Oid			toastrelid;
+	Oid			valueid;
+	bool		is_compressed;
 
 	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);
+	/*
+	 * Handle both legacy 16-byte and extended 20-byte on-disk TOAST pointers.
+	 * Check the vartag to determine which format we're dealing with.
+	 */
+	if (VARTAG_EXTERNAL(attr) == VARTAG_ONDISK_EXTENDED)
+	{
+		struct varatt_external_extended toast_pointer_ext;
+
+		VARATT_EXTERNAL_GET_POINTER_EXTENDED(toast_pointer_ext, attr);
+		attrsize = VARATT_EXTERNAL_GET_EXTSIZE_EXTENDED(toast_pointer_ext);
+		toastrelid = toast_pointer_ext.va_toastrelid;
+		valueid = toast_pointer_ext.va_valueid;
+		is_compressed = VARATT_EXTERNAL_IS_COMPRESSED_EXTENDED(toast_pointer_ext);
+	}
+	else
+	{
+		struct varatt_external toast_pointer;
+
+		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		attrsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+		toastrelid = toast_pointer.va_toastrelid;
+		valueid = toast_pointer.va_valueid;
+		is_compressed = VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer);
+	}
 
 	/*
 	 * 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);
-
-	attrsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+	Assert(!is_compressed || 0 == sliceoffset);
 
 	if (sliceoffset >= attrsize)
 	{
@@ -427,7 +504,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 (is_compressed && slicelength > 0)
 		slicelength = slicelength + sizeof(int32);
 
 	/*
@@ -440,7 +517,7 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 
 	result = (struct varlena *) palloc(slicelength + VARHDRSZ);
 
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+	if (is_compressed)
 		SET_VARSIZE_COMPRESSED(result, slicelength + VARHDRSZ);
 	else
 		SET_VARSIZE(result, slicelength + VARHDRSZ);
@@ -449,10 +526,10 @@ 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(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);
 
@@ -485,6 +562,9 @@ toast_decompress_datum(struct varlena *attr)
 			return pglz_decompress_datum(attr);
 		case TOAST_LZ4_COMPRESSION_ID:
 			return lz4_decompress_datum(attr);
+		case TOAST_EXTENDED_COMPRESSION_ID:
+			/* zstd-compressed data */
+			return zstd_decompress_datum(attr);
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 			return NULL;		/* keep compiler quiet */
@@ -528,6 +608,9 @@ 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_EXTENDED_COMPRESSION_ID:
+			/* zstd-compressed data */
+			return zstd_decompress_datum_slice(attr, slicelength);
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 			return NULL;		/* keep compiler quiet */
@@ -549,11 +632,15 @@ 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;
+		/*
+		 * va_rawsize is the size of the original datum -- including header.
+		 * It's at offset 0 in both varatt_external and varatt_external_extended,
+		 * so we can read just the first 4 bytes regardless of format.
+		 */
+		int32	va_rawsize;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-		result = toast_pointer.va_rawsize;
+		memcpy(&va_rawsize, VARDATA_EXTERNAL(attr), sizeof(va_rawsize));
+		result = va_rawsize;
 	}
 	else if (VARATT_IS_EXTERNAL_INDIRECT(attr))
 	{
@@ -609,11 +696,18 @@ 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?
+		 *
+		 * va_extinfo is at offset 4 in both varatt_external and
+		 * varatt_external_extended, so we can read the first 8 bytes
+		 * regardless of format.
 		 */
-		struct varatt_external toast_pointer;
+		struct {
+			int32	va_rawsize;
+			uint32	va_extinfo;
+		} common;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-		result = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+		memcpy(&common, VARDATA_EXTERNAL(attr), sizeof(common));
+		result = common.va_extinfo & VARLENA_EXTSIZE_MASK;
 	}
 	else if (VARATT_IS_EXTERNAL_INDIRECT(attr))
 	{
diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c
index 926f1e4008a..422e2c5967a 100644
--- a/src/backend/access/common/toast_compression.c
+++ b/src/backend/access/common/toast_compression.c
@@ -17,13 +17,19 @@
 #include <lz4.h>
 #endif
 
+#ifdef USE_ZSTD
+#include <zstd.h>
+#endif
+
 #include "access/detoast.h"
 #include "access/toast_compression.h"
 #include "common/pg_lzcompress.h"
+#include "utils/memutils.h"
 #include "varatt.h"
 
 /* GUC */
 int			default_toast_compression = TOAST_PGLZ_COMPRESSION;
+bool		use_extended_toast_header = false;
 
 #define NO_COMPRESSION_SUPPORT(method) \
 	ereport(ERROR, \
@@ -249,11 +255,16 @@ lz4_decompress_datum_slice(const struct varlena *value, int32 slicelength)
  * Extract compression ID from a varlena.
  *
  * Returns TOAST_INVALID_COMPRESSION_ID if the varlena is not compressed.
+ *
+ * For external data stored in extended format (VARTAG_ONDISK_EXTENDED),
+ * the actual compression method is stored in va_data[0].  We map that
+ * back to the appropriate ToastCompressionId for legacy compatibility.
  */
 ToastCompressionId
 toast_get_compression_id(struct varlena *attr)
 {
 	ToastCompressionId cmid = TOAST_INVALID_COMPRESSION_ID;
+	vartag_external tag;
 
 	/*
 	 * If it is stored externally then fetch the compression method id from
@@ -262,12 +273,52 @@ toast_get_compression_id(struct varlena *attr)
 	 */
 	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);
+		tag = VARTAG_EXTERNAL(attr);
+		if (tag == VARTAG_ONDISK)
+		{
+			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);
+		}
+		else
+		{
+			struct varatt_external_extended toast_pointer_ext;
+			uint8		ext_method;
+
+			Assert(tag == VARTAG_ONDISK_EXTENDED);
+			VARATT_EXTERNAL_GET_POINTER_EXTENDED(toast_pointer_ext, attr);
+
+			if (VARATT_EXTERNAL_IS_COMPRESSED_EXTENDED(toast_pointer_ext))
+			{
+				/*
+				 * Extended format stores the actual method in va_data[0].
+				 * Map it back to ToastCompressionId for reporting purposes.
+				 */
+				ext_method = VARATT_EXTERNAL_GET_EXT_COMPRESSION_METHOD(toast_pointer_ext);
+				switch (ext_method)
+				{
+					case TOAST_PGLZ_EXT_METHOD:
+						cmid = TOAST_PGLZ_COMPRESSION_ID;
+						break;
+					case TOAST_LZ4_EXT_METHOD:
+						cmid = TOAST_LZ4_COMPRESSION_ID;
+						break;
+					case TOAST_ZSTD_EXT_METHOD:
+						cmid = TOAST_EXTENDED_COMPRESSION_ID;
+						break;
+					case TOAST_UNCOMPRESSED_EXT_METHOD:
+						/* Uncompressed data in extended format */
+						cmid = TOAST_INVALID_COMPRESSION_ID;
+						break;
+					default:
+						elog(ERROR, "invalid extended compression method %d",
+							 ext_method);
+				}
+			}
+		}
 	}
 	else if (VARATT_IS_COMPRESSED(attr))
 		cmid = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(attr);
@@ -275,6 +326,133 @@ toast_get_compression_id(struct varlena *attr)
 	return cmid;
 }
 
+/*
+ * Zstandard (zstd) compression/decompression for TOAST (extended methods).
+ *
+ * These routines use the same basic shape as the pglz and LZ4 helpers,
+ * but are only available when PostgreSQL is built with USE_ZSTD.
+ */
+
+/*
+ * Compress a varlena using ZSTD.
+ *
+ * Returns the compressed varlena, or NULL if compression fails or does
+ * not save space.
+ */
+static struct varlena *
+zstd_compress_datum_internal(const struct varlena *value, int level)
+{
+#ifndef USE_ZSTD
+	NO_COMPRESSION_SUPPORT("zstd");
+	return NULL;				/* keep compiler quiet */
+#else
+	Size		valsize;
+	Size		max_size;
+	Size		out_size;
+	struct varlena *tmp;
+	size_t		rc;
+
+	valsize = VARSIZE_ANY_EXHDR(value);
+
+	/*
+	 * Compute an upper bound for the compressed size and allocate enough
+	 * space for the compressed payload plus the varlena header.
+	 */
+	max_size = ZSTD_compressBound(valsize);
+	if (max_size > (Size) (MaxAllocSize - VARHDRSZ_COMPRESSED))
+		ereport(ERROR,
+				(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				 errmsg("compressed data would exceed maximum allocation size")));
+
+	tmp = (struct varlena *) palloc(max_size + VARHDRSZ_COMPRESSED);
+
+	rc = ZSTD_compress((char *) tmp + VARHDRSZ_COMPRESSED, max_size,
+					   VARDATA_ANY(value), valsize, level);
+	if (ZSTD_isError(rc))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATA_CORRUPTED),
+				 errmsg_internal("zstd compression failed: %s",
+								 ZSTD_getErrorName(rc))));
+
+	out_size = (Size) rc;
+
+	/*
+	 * If the compressed representation is not smaller than the original
+	 * payload, give up and return NULL so that callers can fall back to
+	 * storing the datum uncompressed or with a different method.
+	 */
+	if (out_size >= valsize)
+	{
+		pfree(tmp);
+		return NULL;
+	}
+
+	SET_VARSIZE_COMPRESSED(tmp, out_size + VARHDRSZ_COMPRESSED);
+
+	return tmp;
+#endif							/* USE_ZSTD */
+}
+
+struct varlena *
+zstd_compress_datum(const struct varlena *value)
+{
+#ifndef USE_ZSTD
+	NO_COMPRESSION_SUPPORT("zstd");
+	return NULL;				/* keep compiler quiet */
+#else
+	return zstd_compress_datum_internal(value, ZSTD_CLEVEL_DEFAULT);
+#endif
+}
+
+/*
+ * Decompress a varlena that was compressed using ZSTD.
+ */
+struct varlena *
+zstd_decompress_datum(const struct varlena *value)
+{
+#ifndef USE_ZSTD
+	NO_COMPRESSION_SUPPORT("zstd");
+	return NULL;				/* keep compiler quiet */
+#else
+	struct varlena *result;
+	Size		rawsize;
+	size_t		rc;
+
+	/* allocate memory for the uncompressed data */
+	rawsize = VARDATA_COMPRESSED_GET_EXTSIZE(value);
+	result = (struct varlena *) palloc(rawsize + VARHDRSZ);
+
+	rc = ZSTD_decompress(VARDATA(result), rawsize,
+						 (char *) value + VARHDRSZ_COMPRESSED,
+						 VARSIZE(value) - VARHDRSZ_COMPRESSED);
+	if (ZSTD_isError(rc) || rc != rawsize)
+		ereport(ERROR,
+				(errcode(ERRCODE_DATA_CORRUPTED),
+				 errmsg_internal("compressed zstd data is corrupt or truncated")));
+
+	SET_VARSIZE(result, rawsize + VARHDRSZ);
+
+	return result;
+#endif							/* USE_ZSTD */
+}
+
+/*
+ * Decompress part of a varlena that was compressed using ZSTD.
+ *
+ * At least initially we don't try to be clever with streaming slice
+ * decompression here; instead we just decompress the full datum and
+ * let higher layers perform the slicing.  Callers should prefer the
+ * regular zstd_decompress_datum() when they know they need the whole
+ * value anyway.
+ */
+struct varlena *
+zstd_decompress_datum_slice(const struct varlena *value, int32 slicelength)
+{
+	/* For now, just fall back to full decompression. */
+	(void) slicelength;
+	return zstd_decompress_datum(value);
+}
+
 /*
  * CompressionNameToMethod - Get compression method from compression name
  *
@@ -293,6 +471,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;
 }
@@ -309,6 +494,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 d06af82de15..039ccc42249 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_compression.h"
 #include "access/toast_internals.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -71,6 +72,9 @@ toast_compress_datum(Datum value, char cmethod)
 			tmp = lz4_compress_datum((const struct varlena *) DatumGetPointer(value));
 			cmid = TOAST_LZ4_COMPRESSION_ID;
 			break;
+		case TOAST_ZSTD_COMPRESSION:
+			/* zstd uses external storage only; handled by toast_save_datum */
+			return PointerGetDatum(NULL);
 		default:
 			elog(ERROR, "invalid compression method %c", cmethod);
 	}
@@ -113,11 +117,13 @@ toast_compress_datum(Datum value, char cmethod)
  * value: datum to be pushed to toast storage
  * oldexternal: if not NULL, toast pointer previously representing the datum
  * options: options to be passed to heap_insert() for toast rows
+ * cmethod: compression method to use for uncompressed data
  * ----------
  */
 Datum
 toast_save_datum(Relation rel, Datum value,
-				 struct varlena *oldexternal, int options)
+				 struct varlena *oldexternal, int options,
+				 char cmethod)
 {
 	Relation	toastrel;
 	Relation   *toastidxs;
@@ -125,12 +131,16 @@ toast_save_datum(Relation rel, Datum value,
 	CommandId	mycid = GetCurrentCommandId(true);
 	struct varlena *result;
 	struct varatt_external toast_pointer;
+	struct varatt_external_extended toast_pointer_ext;
 	int32		chunk_seq = 0;
 	char	   *data_p;
 	int32		data_todo;
 	Pointer		dval = DatumGetPointer(value);
 	int			num_indexes;
 	int			validIndex;
+	bool		use_extended = false;
+	uint8		ext_method = 0;
+	struct varlena *compressed_to_free = NULL;	/* track allocated buffer */
 
 	Assert(!VARATT_IS_EXTERNAL(dval));
 
@@ -167,23 +177,99 @@ toast_save_datum(Relation rel, Datum value,
 	}
 	else if (VARATT_IS_COMPRESSED(dval))
 	{
+		ToastCompressionId cmid;
+
 		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;
 
+		/* Get compression method from compressed datum */
+		cmid = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(dval);
+
+		/* Decide whether to use extended 20-byte or legacy 16-byte format */
+		if (cmid == TOAST_EXTENDED_COMPRESSION_ID)
+		{
+			use_extended = true;
+			ext_method = TOAST_ZSTD_EXT_METHOD;
+		}
+		else if (use_extended_toast_header)
+		{
+			/* Use extended format for pglz/lz4 when GUC is enabled */
+			use_extended = true;
+			switch (cmid)
+			{
+				case TOAST_PGLZ_COMPRESSION_ID:
+					ext_method = TOAST_PGLZ_EXT_METHOD;
+					break;
+				case TOAST_LZ4_COMPRESSION_ID:
+					ext_method = TOAST_LZ4_EXT_METHOD;
+					break;
+				default:
+					/* Should not happen, but fall back to legacy format */
+					use_extended = false;
+					break;
+			}
+		}
+
 		/* set external size and compression method */
-		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, data_todo,
-													 VARDATA_COMPRESSED_GET_COMPRESS_METHOD(dval));
+		if (use_extended)
+			VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, data_todo,
+														VARATT_EXTERNAL_EXTENDED_CMID);
+		else
+			VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, data_todo, cmid);
+
 		/* Assert that the numbers look like it's compressed */
 		Assert(VARATT_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;
+		/*
+		 * Uncompressed data.  If the caller specified zstd compression,
+		 * try to compress it now before storing to the TOAST table.
+		 */
+		if (cmethod == TOAST_ZSTD_COMPRESSION)
+		{
+			struct varlena *compressed;
+			int32		rawsize;
+
+			rawsize = VARSIZE_ANY_EXHDR((const struct varlena *) dval);
+			compressed = zstd_compress_datum((const struct varlena *) dval);
+			if (compressed != NULL)
+			{
+				/* Set compression method in va_tcinfo */
+				TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(compressed, rawsize,
+															TOAST_EXTENDED_COMPRESSION_ID);
+
+				/* Compression succeeded - use the compressed data */
+				compressed_to_free = compressed;	/* track for cleanup */
+				dval = (Pointer) compressed;
+				data_p = VARDATA(compressed);
+				data_todo = VARSIZE(compressed) - VARHDRSZ;
+				toast_pointer.va_rawsize = rawsize + VARHDRSZ;
+
+				/* Use extended format for zstd */
+				use_extended = true;
+				ext_method = TOAST_ZSTD_EXT_METHOD;
+				VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, data_todo,
+															VARATT_EXTERNAL_EXTENDED_CMID);
+			}
+			else
+			{
+				/* Compression failed or didn't save space - store uncompressed */
+				data_p = VARDATA(dval);
+				data_todo = VARSIZE(dval) - VARHDRSZ;
+				toast_pointer.va_rawsize = VARSIZE(dval);
+				toast_pointer.va_extinfo = data_todo;
+			}
+		}
+		else
+		{
+			data_p = VARDATA(dval);
+			data_todo = VARSIZE(dval) - VARHDRSZ;
+			toast_pointer.va_rawsize = VARSIZE(dval);
+			toast_pointer.va_extinfo = data_todo;
+		}
 	}
 
 	/*
@@ -225,15 +311,36 @@ toast_save_datum(Relation rel, Datum value,
 		toast_pointer.va_valueid = InvalidOid;
 		if (oldexternal != NULL)
 		{
-			struct varatt_external old_toast_pointer;
+			Oid			old_toastrelid;
+			Oid			old_valueid;
 
 			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)
+
+			/*
+			 * Extract toastrelid and valueid from the old pointer.
+			 * Handle both legacy 16-byte and extended 20-byte formats.
+			 */
+			if (VARTAG_EXTERNAL(oldexternal) == VARTAG_ONDISK_EXTENDED)
+			{
+				struct varatt_external_extended old_toast_pointer_ext;
+
+				VARATT_EXTERNAL_GET_POINTER_EXTENDED(old_toast_pointer_ext, oldexternal);
+				old_toastrelid = old_toast_pointer_ext.va_toastrelid;
+				old_valueid = old_toast_pointer_ext.va_valueid;
+			}
+			else
+			{
+				struct varatt_external old_toast_pointer;
+
+				VARATT_EXTERNAL_GET_POINTER(old_toast_pointer, oldexternal);
+				old_toastrelid = old_toast_pointer.va_toastrelid;
+				old_valueid = old_toast_pointer.va_valueid;
+			}
+
+			if (old_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.va_valueid = old_valueid;
 
 				/*
 				 * There is a corner case here: the table rewrite might have
@@ -348,6 +455,10 @@ toast_save_datum(Relation rel, Datum value,
 		data_p += chunk_size;
 	}
 
+	/* Free compressed buffer if we allocated one */
+	if (compressed_to_free != NULL)
+		pfree(compressed_to_free);
+
 	/*
 	 * Done - close toast relation and its indexes but keep the lock until
 	 * commit, so as a concurrent reindex done directly on the toast relation
@@ -356,12 +467,35 @@ toast_save_datum(Relation rel, Datum value,
 	toast_close_indexes(toastidxs, num_indexes, NoLock);
 	table_close(toastrel, NoLock);
 
-	/*
-	 * 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));
+	/* Create the TOAST pointer value that we'll return */
+	if (use_extended)
+	{
+		/*
+		 * Build extended TOAST pointer.  Copy the common fields from
+		 * toast_pointer, then set the extended-format-specific fields.
+		 */
+		toast_pointer_ext.va_rawsize = toast_pointer.va_rawsize;
+		toast_pointer_ext.va_extinfo = toast_pointer.va_extinfo;
+		toast_pointer_ext.va_valueid = toast_pointer.va_valueid;
+		toast_pointer_ext.va_toastrelid = toast_pointer.va_toastrelid;
+
+		/* Set extended format fields */
+		toast_pointer_ext.va_flags = TOAST_EXT_FLAG_COMPRESSION;
+		toast_pointer_ext.va_data[0] = ext_method;
+		toast_pointer_ext.va_data[1] = 0;
+		toast_pointer_ext.va_data[2] = 0;
+
+		result = (struct varlena *) palloc(TOAST_POINTER_SIZE_EXTENDED);
+		SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_EXTENDED);
+		memcpy(VARDATA_EXTERNAL(result), &toast_pointer_ext, sizeof(toast_pointer_ext));
+	}
+	else
+	{
+		/* Standard 16-byte TOAST pointer */
+		result = (struct varlena *) palloc(TOAST_POINTER_SIZE);
+		SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK);
+		memcpy(VARDATA_EXTERNAL(result), &toast_pointer, sizeof(toast_pointer));
+	}
 
 	return PointerGetDatum(result);
 }
@@ -377,6 +511,7 @@ 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_extended toast_pointer_ext;
 	Relation	toastrel;
 	Relation   *toastidxs;
 	ScanKeyData toastkey;
@@ -384,17 +519,36 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	HeapTuple	toasttup;
 	int			num_indexes;
 	int			validIndex;
+	Oid			toastrelid;
+	Oid			valueid;
+	bool		is_extended;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		return;
 
-	/* Must copy to access aligned fields */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	/*
+	 * Must copy to access aligned fields.  Handle both legacy (16-byte) and
+	 * extended (20-byte) on-disk TOAST pointers based on the tag.
+	 */
+	is_extended = (VARTAG_EXTERNAL(attr) == VARTAG_ONDISK_EXTENDED);
+
+	if (!is_extended)
+	{
+		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		toastrelid = toast_pointer.va_toastrelid;
+		valueid = toast_pointer.va_valueid;
+	}
+	else
+	{
+		VARATT_EXTERNAL_GET_POINTER_EXTENDED(toast_pointer_ext, attr);
+		toastrelid = toast_pointer_ext.va_toastrelid;
+		valueid = toast_pointer_ext.va_valueid;
+	}
 
 	/*
 	 * Open the toast relation and its indexes
 	 */
-	toastrel = table_open(toast_pointer.va_toastrelid, RowExclusiveLock);
+	toastrel = table_open(toastrelid, RowExclusiveLock);
 
 	/* Fetch valid relation used for process */
 	validIndex = toast_open_indexes(toastrel,
@@ -408,7 +562,7 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	ScanKeyInit(&toastkey,
 				(AttrNumber) 1,
 				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(toast_pointer.va_valueid));
+				ObjectIdGetDatum(valueid));
 
 	/*
 	 * Find all the chunks.  (We don't actually care whether we see them in
diff --git a/src/backend/access/table/toast_helper.c b/src/backend/access/table/toast_helper.c
index 11f97d65367..21381004ba6 100644
--- a/src/backend/access/table/toast_helper.c
+++ b/src/backend/access/table/toast_helper.c
@@ -261,7 +261,7 @@ toast_tuple_externalize(ToastTupleContext *ttc, int attribute, int options)
 
 	attr->tai_colflags |= TOASTCOL_IGNORE;
 	*value = toast_save_datum(ttc->ttc_rel, old_value, attr->tai_oldexternal,
-							  options);
+							  options, attr->tai_compression);
 	if ((attr->tai_colflags & TOASTCOL_NEEDS_FREE) != 0)
 		pfree(DatumGetPointer(old_value));
 	attr->tai_colflags |= TOASTCOL_NEEDS_FREE;
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index f18c6fb52b5..9e83ab5978d 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -5137,11 +5137,17 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 
 		/* va_rawsize is the size of the original datum -- including header */
 		struct varatt_external toast_pointer;
+		struct varatt_external_extended toast_pointer_ext;
 		struct varatt_indirect redirect_pointer;
 		struct varlena *new_datum = NULL;
 		struct varlena *reconstructed;
 		dlist_iter	it;
 		Size		data_done = 0;
+		bool		is_extended;
+		Oid			valueid;
+		int32		rawsize;
+		int32		extsize;
+		bool		is_compressed;
 
 		if (attr->attisdropped)
 			continue;
@@ -5161,14 +5167,36 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		if (!VARATT_IS_EXTERNAL(varlena))
 			continue;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, varlena);
+		/*
+		 * Handle both legacy 16-byte and extended 20-byte on-disk TOAST
+		 * pointers based on the tag.
+		 */
+		is_extended = VARATT_IS_EXTERNAL_ONDISK(varlena) &&
+					  (VARTAG_EXTERNAL(varlena) == VARTAG_ONDISK_EXTENDED);
+
+		if (is_extended)
+		{
+			VARATT_EXTERNAL_GET_POINTER_EXTENDED(toast_pointer_ext, varlena);
+			valueid = toast_pointer_ext.va_valueid;
+			rawsize = toast_pointer_ext.va_rawsize;
+			extsize = VARATT_EXTERNAL_GET_EXTSIZE_EXTENDED(toast_pointer_ext);
+			is_compressed = VARATT_EXTERNAL_IS_COMPRESSED_EXTENDED(toast_pointer_ext);
+		}
+		else
+		{
+			VARATT_EXTERNAL_GET_POINTER(toast_pointer, varlena);
+			valueid = toast_pointer.va_valueid;
+			rawsize = toast_pointer.va_rawsize;
+			extsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+			is_compressed = VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer);
+		}
 
 		/*
 		 * Check whether the toast tuple changed, replace if so.
 		 */
 		ent = (ReorderBufferToastEnt *)
 			hash_search(txn->toast_hash,
-						&toast_pointer.va_valueid,
+						&valueid,
 						HASH_FIND,
 						NULL);
 		if (ent == NULL)
@@ -5179,7 +5207,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 
 		free[natt] = true;
 
-		reconstructed = palloc0(toast_pointer.va_rawsize);
+		reconstructed = palloc0(rawsize);
 
 		ent->reconstructed = reconstructed;
 
@@ -5204,10 +5232,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 == extsize);
 
 		/* make sure its marked as compressed or not */
-		if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+		if (is_compressed)
 			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 baa5b44ea8d..71a410dc617 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -4206,6 +4206,10 @@ pg_column_compression(PG_FUNCTION_ARGS)
 		case TOAST_LZ4_COMPRESSION_ID:
 			result = "lz4";
 			break;
+		case TOAST_EXTENDED_COMPRESSION_ID:
+			/* Extended format currently only supports zstd */
+			result = "zstd";
+			break;
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 	}
@@ -4222,7 +4226,7 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 {
 	int			typlen;
 	struct varlena *attr;
-	struct varatt_external toast_pointer;
+	Oid			valueid;
 
 	/* On first call, get the input type's typlen, and save at *fn_extra */
 	if (fcinfo->flinfo->fn_extra == NULL)
@@ -4249,9 +4253,25 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		PG_RETURN_NULL();
 
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	/*
+	 * Handle both legacy 16-byte and extended 20-byte on-disk TOAST pointers.
+	 */
+	if (VARTAG_EXTERNAL(attr) == VARTAG_ONDISK_EXTENDED)
+	{
+		struct varatt_external_extended toast_pointer_ext;
+
+		VARATT_EXTERNAL_GET_POINTER_EXTENDED(toast_pointer_ext, attr);
+		valueid = toast_pointer_ext.va_valueid;
+	}
+	else
+	{
+		struct varatt_external toast_pointer;
+
+		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		valueid = toast_pointer.va_valueid;
+	}
 
-	PG_RETURN_OID(toast_pointer.va_valueid);
+	PG_RETURN_OID(valueid);
 }
 
 /*
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 3b9d8349078..38c68d1d0a6 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -738,7 +738,6 @@
   boot_val => 'TOAST_PGLZ_COMPRESSION',
   options => 'default_toast_compression_options',
 },
-
 { name => 'default_transaction_deferrable', type => 'bool', context => 'PGC_USERSET', group => 'CLIENT_CONN_STATEMENT',
   short_desc => 'Sets the default deferrable status of new transactions.',
   variable => 'DefaultXactDeferrable',
@@ -3175,6 +3174,12 @@
   boot_val => 'DEFAULT_UPDATE_PROCESS_TITLE',
 },
 
+{ name => 'use_extended_toast_header', type => 'bool', context => 'PGC_USERSET', group => 'CLIENT_CONN_STATEMENT',
+  short_desc => 'Use 20-byte extended TOAST header format (required for zstd).',
+  variable => 'use_extended_toast_header',
+  boot_val => 'false',
+},
+
 { name => 'vacuum_buffer_usage_limit', type => 'int', context => 'PGC_USERSET', group => 'RESOURCES_MEM',
   short_desc => 'Sets the buffer pool size for VACUUM, ANALYZE, and autovacuum.',
   flags => 'GUC_UNIT_KB',
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index f87b558c2c6..f6c09260f1a 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -460,6 +460,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/include/access/detoast.h b/src/include/access/detoast.h
index e603a2276c3..e591a59569b 100644
--- a/src/include/access/detoast.h
+++ b/src/include/access/detoast.h
@@ -14,25 +14,58 @@
 
 /*
  * 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.
+ *
+ * This currently supports only the legacy on-disk TOAST pointer format,
+ * which has VARTAG_ONDISK and a payload size of sizeof(varatt_external).
+ * Extended on-disk pointers (VARTAG_ONDISK_EXTENDED) must be accessed via
+ * VARATT_EXTERNAL_GET_POINTER_EXTENDED().
+ *
+ * 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 { \
 	varattrib_1b_e *attre = (varattrib_1b_e *) (attr); \
 	Assert(VARATT_IS_EXTERNAL(attre)); \
+	Assert(VARTAG_EXTERNAL(attre) == VARTAG_ONDISK); \
 	Assert(VARSIZE_EXTERNAL(attre) == sizeof(toast_pointer) + VARHDRSZ_EXTERNAL); \
 	memcpy(&(toast_pointer), VARDATA_EXTERNAL(attre), sizeof(toast_pointer)); \
 } while (0)
 
+/*
+ * Variant of VARATT_EXTERNAL_GET_POINTER for the extended on-disk TOAST
+ * pointer format.  Callers should only use this when they have already
+ * established that the tag is VARTAG_ONDISK_EXTENDED.
+ */
+#define VARATT_EXTERNAL_GET_POINTER_EXTENDED(toast_pointer_ext, attr) \
+do { \
+	varattrib_1b_e *attre = (varattrib_1b_e *) (attr); \
+	Assert(VARATT_IS_EXTERNAL(attre)); \
+	Assert(VARTAG_EXTERNAL(attre) == VARTAG_ONDISK_EXTENDED); \
+	Assert(VARSIZE_EXTERNAL(attre) == sizeof(toast_pointer_ext) + VARHDRSZ_EXTERNAL); \
+	memcpy(&(toast_pointer_ext), VARDATA_EXTERNAL(attre), sizeof(toast_pointer_ext)); \
+} 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))
 
+/* Size of an EXTERNAL datum that contains an extended TOAST pointer */
+#define TOAST_POINTER_SIZE_EXTENDED (VARHDRSZ_EXTERNAL + sizeof(varatt_external_extended))
+
+/* Validation helpers for TOAST pointer sizes */
+#define TOAST_POINTER_SIZE_IS_VALID(size) \
+	((size) == TOAST_POINTER_SIZE || \
+	 (size) == TOAST_POINTER_SIZE_EXTENDED || \
+	 (size) == INDIRECT_POINTER_SIZE)
+
+#define TOAST_POINTER_IS_EXTENDED_SIZE(size) \
+	((size) == TOAST_POINTER_SIZE_EXTENDED)
+
 /* ----------
  * detoast_external_attr() -
  *
diff --git a/src/include/access/toast_compression.h b/src/include/access/toast_compression.h
index 13c4612ceed..b769d1bc72d 100644
--- a/src/include/access/toast_compression.h
+++ b/src/include/access/toast_compression.h
@@ -13,14 +13,21 @@
 #ifndef TOAST_COMPRESSION_H
 #define TOAST_COMPRESSION_H
 
+#include "varatt.h"
+
 /*
  * GUC support.
  *
  * default_toast_compression is an integer for purposes of the GUC machinery,
  * but the value is one of the char values defined below, as they appear in
  * pg_attribute.attcompression, e.g. TOAST_PGLZ_COMPRESSION.
+ *
+ * use_extended_toast_header controls whether to use the 20-byte extended
+ * TOAST pointer format (required for zstd) instead of the legacy 16-byte
+ * format. When false, zstd compression falls back to pglz.
  */
 extern PGDLLIMPORT int default_toast_compression;
+extern PGDLLIMPORT bool use_extended_toast_header;
 
 /*
  * Built-in compression method ID.  The toast compression header will store
@@ -39,6 +46,7 @@ typedef enum ToastCompressionId
 	TOAST_PGLZ_COMPRESSION_ID = 0,
 	TOAST_LZ4_COMPRESSION_ID = 1,
 	TOAST_INVALID_COMPRESSION_ID = 2,
+	TOAST_EXTENDED_COMPRESSION_ID = 3,	/* extended format for future methods */
 } ToastCompressionId;
 
 /*
@@ -48,6 +56,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)
@@ -65,9 +74,36 @@ extern struct varlena *lz4_decompress_datum(const struct varlena *value);
 extern struct varlena *lz4_decompress_datum_slice(const struct varlena *value,
 												  int32 slicelength);
 
+/* zstd compression/decompression routines (extended methods) */
+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);
 extern const char *GetCompressionMethodName(char method);
 
+/*
+ * Feature flags for extended TOAST pointers (varatt_external_extended).
+ * These alias VARATT_EXTERNAL_FLAG_* from varatt.h.
+ */
+#define TOAST_EXT_FLAG_COMPRESSION      VARATT_EXTERNAL_FLAG_COMPRESSION
+#define TOAST_EXT_FLAG_CHECKSUM         VARATT_EXTERNAL_FLAG_CHECKSUM
+
+/*
+ * Extended compression method IDs for use with extended TOAST format.
+ * Stored in va_data[0] when TOAST_EXT_FLAG_COMPRESSION is set.
+ */
+#define TOAST_PGLZ_EXT_METHOD          0
+#define TOAST_LZ4_EXT_METHOD           1
+#define TOAST_ZSTD_EXT_METHOD          2
+#define TOAST_UNCOMPRESSED_EXT_METHOD  3
+
+/* Validation macros for extended format */
+#define ExtendedCompressionMethodIsValid(method) ((method) <= 255)
+#define ExtendedFlagsAreValid(flags) \
+	(((flags) & ~(TOAST_EXT_FLAG_COMPRESSION | TOAST_EXT_FLAG_CHECKSUM)) == 0)
+
 #endif							/* TOAST_COMPRESSION_H */
diff --git a/src/include/access/toast_internals.h b/src/include/access/toast_internals.h
index 06ae8583c1e..d6bc5c4d179 100644
--- a/src/include/access/toast_internals.h
+++ b/src/include/access/toast_internals.h
@@ -36,11 +36,16 @@ typedef struct toast_compress_header
 #define TOAST_COMPRESS_METHOD(ptr) \
 	(((toast_compress_header *) (ptr))->tcinfo >> VARLENA_EXTSIZE_BITS)
 
+/*
+ * Set the size and compression method in a compressed datum's header.
+ * Accepts TOAST_EXTENDED_COMPRESSION_ID for extended compression methods.
+ */
 #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_LZ4_COMPRESSION_ID || \
+			   (cm_method) == TOAST_EXTENDED_COMPRESSION_ID); \
 		((toast_compress_header *) (ptr))->tcinfo = \
 			(len) | ((uint32) (cm_method) << VARLENA_EXTSIZE_BITS); \
 	} while (0)
@@ -50,7 +55,8 @@ extern Oid	toast_get_valid_index(Oid toastoid, LOCKMODE lock);
 
 extern void toast_delete_datum(Relation rel, Datum value, bool is_speculative);
 extern Datum toast_save_datum(Relation rel, Datum value,
-							  struct varlena *oldexternal, int options);
+							  struct varlena *oldexternal, int options,
+							  char cmethod);
 
 extern int	toast_open_indexes(Relation toastrel,
 							   LOCKMODE lock,
diff --git a/src/include/varatt.h b/src/include/varatt.h
index aeeabf9145b..5f5829a1ec4 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -45,6 +45,23 @@ typedef struct varatt_external
 #define VARLENA_EXTSIZE_BITS	30
 #define VARLENA_EXTSIZE_MASK	((1U << VARLENA_EXTSIZE_BITS) - 1)
 
+/*
+ * Compression method ID stored in the 2 high-order bits of va_extinfo.
+ * Value 3 indicates an extended TOAST pointer format (varatt_external_extended).
+ * This constant is also defined in toast_compression.h for use by TOAST code.
+ */
+#define VARATT_EXTERNAL_EXTENDED_CMID	3
+
+/*
+ * Feature flags for extended on-disk TOAST pointers (varatt_external_extended).
+ *
+ * Keep these in varatt.h (not access/toast headers) so low-level code can
+ * safely manipulate the on-disk representation without depending on higher
+ * layers' header include order.
+ */
+#define VARATT_EXTERNAL_FLAG_COMPRESSION	0x01	/* va_data[0] = method ID */
+#define VARATT_EXTERNAL_FLAG_CHECKSUM		0x02	/* va_data[1-2] = checksum */
+
 /*
  * struct varatt_indirect is a "TOAST pointer" representing an out-of-line
  * Datum that's stored in memory, not in an external toast relation.
@@ -76,6 +93,26 @@ typedef struct varatt_expanded
 	ExpandedObjectHeader *eohptr;
 } varatt_expanded;
 
+/*
+ * Extended TOAST pointer, extending varatt_external from 16 to 20 bytes.
+ *
+ * Identified by compression method ID 3 in va_extinfo bits 30-31.  The
+ * va_flags field indicates which optional features are enabled; va_data[]
+ * contains feature-specific data (e.g., compression method in va_data[0]).
+ *
+ * Like varatt_external, stored unaligned and requires memcpy for access.
+ */
+typedef struct varatt_external_extended
+{
+	int32		va_rawsize;		/* Original data size (includes header) */
+	uint32		va_extinfo;		/* External saved size (30 bits) + extended
+								 * indicator (2 bits, value = 3) */
+	uint8		va_flags;		/* Feature flags indicating enabled extensions */
+	uint8		va_data[3];		/* Extension data - interpretation depends on flags */
+	Oid			va_valueid;		/* Unique ID of value within TOAST table */
+	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
+}			varatt_external_extended;
+
 /*
  * Type tag for the various sorts of "TOAST pointer" datums.  The peculiar
  * value for VARTAG_ONDISK comes from a requirement for on-disk compatibility
@@ -86,7 +123,17 @@ typedef enum vartag_external
 	VARTAG_INDIRECT = 1,
 	VARTAG_EXPANDED_RO = 2,
 	VARTAG_EXPANDED_RW = 3,
-	VARTAG_ONDISK = 18
+	VARTAG_ONDISK = 18,
+
+	/*
+	 * VARTAG_ONDISK_EXTENDED is used for the extended TOAST pointer format,
+	 * which increases the on-disk payload from 16 to 20 bytes.  The first
+	 * 8 bytes (va_rawsize, va_extinfo) are layout-compatible with
+	 * struct varatt_external so that existing code inspecting those fields
+	 * continues to work.  Older PostgreSQL versions do not know about this
+	 * tag and therefore must not be used to read clusters that contain it.
+	 */
+	VARTAG_ONDISK_EXTENDED = 19
 } vartag_external;
 
 /* Is a TOAST pointer either type of expanded-object pointer? */
@@ -97,7 +144,14 @@ VARTAG_IS_EXPANDED(vartag_external tag)
 	return ((tag & ~1) == VARTAG_EXPANDED_RO);
 }
 
-/* Size of the data part of a "TOAST pointer" datum */
+/*
+ * Size of the data part of a "TOAST pointer" datum.
+ *
+ * For on-disk TOAST pointers we now support two payload sizes:
+ * the original 16-byte format (VARTAG_ONDISK) described by struct
+ * varatt_external, and a 20-byte extended format
+ * (VARTAG_ONDISK_EXTENDED) described by struct varatt_external_extended.
+ */
 static inline Size
 VARTAG_SIZE(vartag_external tag)
 {
@@ -107,6 +161,8 @@ VARTAG_SIZE(vartag_external tag)
 		return sizeof(varatt_expanded);
 	else if (tag == VARTAG_ONDISK)
 		return sizeof(varatt_external);
+	else if (tag == VARTAG_ONDISK_EXTENDED)
+		return sizeof(varatt_external_extended);
 	else
 	{
 		Assert(false);
@@ -360,7 +416,13 @@ 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;
+	vartag_external tag;
+
+	if (!VARATT_IS_EXTERNAL(PTR))
+		return false;
+
+	tag = VARTAG_EXTERNAL(PTR);
+	return tag == VARTAG_ONDISK || tag == VARTAG_ONDISK_EXTENDED;
 }
 
 /* Is varlena datum an indirect pointer? */
@@ -516,11 +578,11 @@ VARATT_EXTERNAL_GET_COMPRESS_METHOD(struct varatt_external toast_pointer)
 }
 
 /* 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) \
 	do { \
 		Assert((cm) == TOAST_PGLZ_COMPRESSION_ID || \
-			   (cm) == TOAST_LZ4_COMPRESSION_ID); \
+			   (cm) == TOAST_LZ4_COMPRESSION_ID || \
+			   (cm) == VARATT_EXTERNAL_EXTENDED_CMID); \
 		((toast_pointer).va_extinfo = \
 			(len) | ((uint32) (cm) << VARLENA_EXTSIZE_BITS)); \
 	} while (0)
@@ -539,4 +601,92 @@ VARATT_EXTERNAL_IS_COMPRESSED(struct varatt_external toast_pointer)
 		(Size) (toast_pointer.va_rawsize - VARHDRSZ);
 }
 
+/* Macros for extended TOAST pointers (varatt_external_extended) */
+
+/*
+ * Check if a TOAST pointer uses the extended on-disk format.
+ *
+ * Callers must have already verified VARATT_IS_EXTERNAL_ONDISK() before
+ * calling this; here we look only at the compression-method bits embedded
+ * in va_extinfo.
+ */
+static inline bool
+VARATT_EXTERNAL_IS_EXTENDED(struct varatt_external toast_pointer)
+{
+	return VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) ==
+		VARATT_EXTERNAL_EXTENDED_CMID;
+}
+
+/* Get feature flags from extended pointer */
+static inline uint8
+VARATT_EXTERNAL_GET_FLAGS(struct varatt_external_extended toast_pointer_ext)
+{
+	return toast_pointer_ext.va_flags;
+}
+
+/* Set feature flags in extended pointer */
+#define VARATT_EXTERNAL_SET_FLAGS(toast_pointer_ext, flags) \
+	do { \
+		(toast_pointer_ext).va_flags = (flags); \
+	} while (0)
+
+/* Test if a specific flag is set */
+#define VARATT_EXTERNAL_HAS_FLAG(toast_pointer_ext, flag) \
+	(((toast_pointer_ext).va_flags & (flag)) != 0)
+
+/* Get pointer to extension data array */
+#define VARATT_EXTERNAL_GET_EXT_DATA(toast_pointer_ext) \
+	((toast_pointer_ext).va_data)
+
+/* Get extended compression method (when TOAST_EXT_FLAG_COMPRESSION is set) */
+static inline uint8
+VARATT_EXTERNAL_GET_EXT_COMPRESSION_METHOD(struct varatt_external_extended toast_pointer_ext)
+{
+	return toast_pointer_ext.va_data[0];
+}
+
+/* Set extended compression method */
+#define VARATT_EXTERNAL_SET_EXT_COMPRESSION_METHOD(toast_pointer_ext, method) \
+	do { \
+		(toast_pointer_ext).va_data[0] = (method); \
+	} while (0)
+
+/* Get extsize and compress method from extended pointer (same as standard) */
+static inline Size
+VARATT_EXTERNAL_GET_EXTSIZE_EXTENDED(struct varatt_external_extended toast_pointer_ext)
+{
+	return toast_pointer_ext.va_extinfo & VARLENA_EXTSIZE_MASK;
+}
+
+static inline uint32
+VARATT_EXTERNAL_GET_COMPRESS_METHOD_EXTENDED(struct varatt_external_extended toast_pointer_ext)
+{
+	return toast_pointer_ext.va_extinfo >> VARLENA_EXTSIZE_BITS;
+}
+
+/* Set size and extended indicator in va_extinfo */
+#define VARATT_EXTERNAL_SET_SIZE_AND_EXT_FLAGS(toast_pointer_ext, len, flags) \
+	do { \
+		Assert((len) > 0 && (len) <= VARLENA_EXTSIZE_MASK); \
+		(toast_pointer_ext).va_extinfo = \
+			(len) | ((uint32) VARATT_EXTERNAL_EXTENDED_CMID << VARLENA_EXTSIZE_BITS); \
+		(toast_pointer_ext).va_flags = (flags); \
+		memset((toast_pointer_ext).va_data, 0, 3); \
+	} while (0)
+
+/* Convenience macro for setting extended pointer with compression method */
+#define VARATT_EXTERNAL_SET_SIZE_AND_EXT_COMPRESSION(toast_pointer_ext, len, method) \
+	do { \
+		VARATT_EXTERNAL_SET_SIZE_AND_EXT_FLAGS(toast_pointer_ext, len, VARATT_EXTERNAL_FLAG_COMPRESSION); \
+		VARATT_EXTERNAL_SET_EXT_COMPRESSION_METHOD(toast_pointer_ext, method); \
+	} while (0)
+
+/* Test if extended pointer is compressed (same logic as standard) */
+static inline bool
+VARATT_EXTERNAL_IS_COMPRESSED_EXTENDED(struct varatt_external_extended toast_pointer_ext)
+{
+	return VARATT_EXTERNAL_GET_EXTSIZE_EXTENDED(toast_pointer_ext) <
+		(Size) (toast_pointer_ext.va_rawsize - VARHDRSZ);
+}
+
 #endif
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 068fd859a8f..9dff119aa22 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -47,6 +47,7 @@ subdir('test_rls_hooks')
 subdir('test_shm_mq')
 subdir('test_slru')
 subdir('test_tidstore')
+subdir('test_toast_ext')
 subdir('typcache')
 subdir('unsafe_tests')
 subdir('worker_spi')
diff --git a/src/test/modules/test_toast_ext/Makefile b/src/test/modules/test_toast_ext/Makefile
new file mode 100644
index 00000000000..5e2409f918c
--- /dev/null
+++ b/src/test/modules/test_toast_ext/Makefile
@@ -0,0 +1,20 @@
+# src/test/modules/test_toast_ext/Makefile
+
+MODULE_big = test_toast_ext
+OBJS = test_toast_ext.o
+
+EXTENSION = test_toast_ext
+DATA = test_toast_ext--1.0.sql
+
+REGRESS = test_toast_ext
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_toast_ext
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_toast_ext/expected/test_toast_ext.out b/src/test/modules/test_toast_ext/expected/test_toast_ext.out
new file mode 100644
index 00000000000..1ad073dcef9
--- /dev/null
+++ b/src/test/modules/test_toast_ext/expected/test_toast_ext.out
@@ -0,0 +1,229 @@
+--
+-- Tests for extended TOAST header structures and zstd compression
+--
+CREATE EXTENSION test_toast_ext;
+--
+-- Compile-time validation tests (always run)
+--
+-- Verify structure sizes match expected values (catches ABI issues)
+SELECT test_toast_structure_sizes();
+          test_toast_structure_sizes
+-----------------------------------------------
+ PASS: varatt_external is 16 bytes            +
+ PASS: varatt_external_extended is 20 bytes   +
+ PASS: TOAST_POINTER_SIZE is 18 bytes         +
+ PASS: TOAST_POINTER_SIZE_EXTENDED is 22 bytes+
+ PASS: All field offsets correct (no padding) +
+                                              +
+ Result: ALL TESTS PASSED                     +
+
+(1 row)
+
+-- Verify flag validation macros work correctly
+SELECT test_toast_flag_validation();
+       test_toast_flag_validation
+----------------------------------------
+ PASS: Valid flags (0x00-0x03) accepted+
+ PASS: Invalid flags (0x04+) rejected  +
+ PASS: Compression methods 0-255 valid +
+ PASS: Compression method IDs correct  +
+                                       +
+ Result: ALL TESTS PASSED              +
+
+(1 row)
+
+-- Verify compression ID constants are consistent
+SELECT test_toast_compression_ids();
+            test_toast_compression_ids
+--------------------------------------------------
+ PASS: Standard compression IDs correct (0,1,2,3)+
+ PASS: PGLZ/LZ4 IDs consistent between formats   +
+                                                 +
+ Result: ALL TESTS PASSED                        +
+
+(1 row)
+
+--
+-- Functional tests for zstd TOAST compression
+-- These tests require PostgreSQL built with USE_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
+-- Test basic zstd compression round-trip
+CREATE TABLE test_zstd_basic (id serial, data text COMPRESSION zstd);
+INSERT INTO test_zstd_basic (data)
+    VALUES (repeat('PostgreSQL zstd TOAST compression test. ', 3000));
+-- Verify compression and readback
+SELECT id,
+       pg_column_compression(data) AS compression,
+       length(data) AS data_length,
+       left(data, 42) AS data_prefix
+FROM test_zstd_basic;
+ id | compression | data_length |                data_prefix
+----+-------------+-------------+--------------------------------------------
+  1 | zstd        |      120000 | PostgreSQL zstd TOAST compression test. Po
+(1 row)
+
+-- Test slice access (partial decompression)
+SELECT id, substr(data, 100, 42) AS slice FROM test_zstd_basic;
+ id |                   slice
+----+--------------------------------------------
+  1 | ST compression test. PostgreSQL zstd TOAST
+(1 row)
+
+-- Test UPDATE with zstd compressed data
+UPDATE test_zstd_basic SET data = repeat('Updated zstd data for TOAST test. ', 3000);
+SELECT id,
+       pg_column_compression(data) AS compression,
+       length(data) AS data_length,
+       left(data, 35) AS data_prefix
+FROM test_zstd_basic;
+ id | compression | data_length |             data_prefix
+----+-------------+-------------+-------------------------------------
+  1 | zstd        |      102000 | Updated zstd data for TOAST test. U
+(1 row)
+
+--
+-- Test extended header format with legacy compression methods
+--
+-- When use_extended_toast_header is on, pglz/lz4 use the 20-byte format
+SET use_extended_toast_header = on;
+CREATE TABLE test_pglz_extended (data text COMPRESSION pglz);
+INSERT INTO test_pglz_extended (data)
+    VALUES (repeat('PGLZ with extended header format. ', 3000));
+SELECT pg_column_compression(data) AS compression,
+       length(data) AS data_length
+FROM test_pglz_extended;
+ compression | data_length
+-------------+-------------
+ pglz        |      102000
+(1 row)
+
+-- Verify slice access works with extended format
+SELECT substr(data, 50, 34) AS slice FROM test_pglz_extended;
+               slice
+------------------------------------
+ ded header format. PGLZ with exten
+(1 row)
+
+--
+-- Test data integrity across compression methods
+--
+CREATE TABLE test_integrity (
+    method text,
+    original_data text,
+    compressed_data text
+);
+-- Store test data with each method
+INSERT INTO test_integrity VALUES
+    ('pglz', repeat('Integrity test data pattern. ', 2000), NULL),
+    ('zstd', repeat('Integrity test data pattern. ', 2000), NULL);
+-- Create compressed versions
+CREATE TABLE test_pglz_integrity (data text COMPRESSION pglz);
+CREATE TABLE test_zstd_integrity (data text COMPRESSION zstd);
+INSERT INTO test_pglz_integrity SELECT original_data FROM test_integrity WHERE method = 'pglz';
+INSERT INTO test_zstd_integrity SELECT original_data FROM test_integrity WHERE method = 'zstd';
+-- Verify checksums match
+SELECT 'pglz' AS method,
+       md5((SELECT original_data FROM test_integrity WHERE method = 'pglz')) =
+       md5((SELECT data FROM test_pglz_integrity)) AS checksum_match;
+ method | checksum_match
+--------+----------------
+ pglz   | t
+(1 row)
+
+SELECT 'zstd' AS method,
+       md5((SELECT original_data FROM test_integrity WHERE method = 'zstd')) =
+       md5((SELECT data FROM test_zstd_integrity)) AS checksum_match;
+ method | checksum_match
+--------+----------------
+ zstd   | t
+(1 row)
+
+--
+-- Test table rewrite operations (CLUSTER, VACUUM FULL)
+--
+CREATE TABLE test_cluster_zstd (id serial PRIMARY KEY, data text COMPRESSION zstd);
+INSERT INTO test_cluster_zstd (data)
+    VALUES (repeat('Data for CLUSTER test with zstd compression. ', 2500));
+-- Capture original data hash
+SELECT 'before_cluster' AS stage, md5(data) AS hash FROM test_cluster_zstd;
+     stage      |               hash
+----------------+----------------------------------
+ before_cluster | b4132e799bbd065a7e9266159aa82dc1
+(1 row)
+
+CLUSTER test_cluster_zstd USING test_cluster_zstd_pkey;
+SELECT 'after_cluster' AS stage,
+       pg_column_compression(data) AS compression,
+       md5(data) AS hash
+FROM test_cluster_zstd;
+     stage     | compression |               hash
+---------------+-------------+----------------------------------
+ after_cluster | zstd        | b4132e799bbd065a7e9266159aa82dc1
+(1 row)
+
+VACUUM FULL test_cluster_zstd;
+SELECT 'after_vacuum_full' AS stage,
+       pg_column_compression(data) AS compression,
+       md5(data) AS hash
+FROM test_cluster_zstd;
+       stage       | compression |               hash
+-------------------+-------------+----------------------------------
+ after_vacuum_full | zstd        | b4132e799bbd065a7e9266159aa82dc1
+(1 row)
+
+--
+-- Test mixed format data (GUC toggling)
+--
+SET use_extended_toast_header = on;
+CREATE TABLE test_guc_toggle (id serial, data text COMPRESSION pglz);
+INSERT INTO test_guc_toggle (data)
+    VALUES (repeat('Data created with extended header on. ', 3000));
+SELECT 'with_ext_on' AS stage,
+       pg_column_compression(data) AS compression,
+       length(data) AS data_length
+FROM test_guc_toggle;
+    stage    | compression | data_length
+-------------+-------------+-------------
+ with_ext_on | pglz        |      114000
+(1 row)
+
+SET use_extended_toast_header = off;
+INSERT INTO test_guc_toggle (data)
+    VALUES (repeat('Data created with extended header off. ', 3000));
+-- Both rows readable
+SELECT id,
+       pg_column_compression(data) AS compression,
+       length(data) AS data_length,
+       left(data, 39) AS data_prefix
+FROM test_guc_toggle ORDER BY id;
+ id | compression | data_length |               data_prefix
+----+-------------+-------------+-----------------------------------------
+  1 | pglz        |      114000 | Data created with extended header on. D
+  2 | pglz        |      117000 | Data created with extended header off.
+(2 rows)
+
+SET use_extended_toast_header = on;
+SELECT id, length(data) AS data_length FROM test_guc_toggle ORDER BY id;
+ id | data_length
+----+-------------
+  1 |      114000
+  2 |      117000
+(2 rows)
+
+--
+-- Cleanup
+--
+DROP TABLE test_zstd_basic;
+DROP TABLE test_pglz_extended;
+DROP TABLE test_integrity;
+DROP TABLE test_pglz_integrity;
+DROP TABLE test_zstd_integrity;
+DROP TABLE test_cluster_zstd;
+DROP TABLE test_guc_toggle;
+DROP EXTENSION test_toast_ext;
diff --git a/src/test/modules/test_toast_ext/meson.build b/src/test/modules/test_toast_ext/meson.build
new file mode 100644
index 00000000000..61c07ea1912
--- /dev/null
+++ b/src/test/modules/test_toast_ext/meson.build
@@ -0,0 +1,33 @@
+# Copyright (c) 2022-2025, PostgreSQL Global Development Group
+
+test_toast_ext_sources = files(
+  'test_toast_ext.c',
+)
+
+if host_system == 'windows'
+  test_toast_ext_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_toast_ext',
+    '--FILEDESC', 'test_toast_ext - test code for extended TOAST headers',])
+endif
+
+test_toast_ext = shared_module('test_toast_ext',
+  test_toast_ext_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_toast_ext
+
+test_install_data += files(
+  'test_toast_ext.control',
+  'test_toast_ext--1.0.sql',
+)
+
+tests += {
+  'name': 'test_toast_ext',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'regress': {
+    'sql': [
+      'test_toast_ext',
+    ],
+  },
+}
diff --git a/src/test/modules/test_toast_ext/sql/test_toast_ext.sql b/src/test/modules/test_toast_ext/sql/test_toast_ext.sql
new file mode 100644
index 00000000000..a66f4007890
--- /dev/null
+++ b/src/test/modules/test_toast_ext/sql/test_toast_ext.sql
@@ -0,0 +1,169 @@
+--
+-- Tests for extended TOAST header structures and zstd compression
+--
+
+CREATE EXTENSION test_toast_ext;
+
+--
+-- Compile-time validation tests (always run)
+--
+
+-- Verify structure sizes match expected values (catches ABI issues)
+SELECT test_toast_structure_sizes();
+
+-- Verify flag validation macros work correctly
+SELECT test_toast_flag_validation();
+
+-- Verify compression ID constants are consistent
+SELECT test_toast_compression_ids();
+
+--
+-- Functional tests for zstd TOAST compression
+-- These tests require PostgreSQL built with USE_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
+
+-- Test basic zstd compression round-trip
+CREATE TABLE test_zstd_basic (id serial, data text COMPRESSION zstd);
+INSERT INTO test_zstd_basic (data)
+    VALUES (repeat('PostgreSQL zstd TOAST compression test. ', 3000));
+
+-- Verify compression and readback
+SELECT id,
+       pg_column_compression(data) AS compression,
+       length(data) AS data_length,
+       left(data, 42) AS data_prefix
+FROM test_zstd_basic;
+
+-- Test slice access (partial decompression)
+SELECT id, substr(data, 100, 42) AS slice FROM test_zstd_basic;
+
+-- Test UPDATE with zstd compressed data
+UPDATE test_zstd_basic SET data = repeat('Updated zstd data for TOAST test. ', 3000);
+SELECT id,
+       pg_column_compression(data) AS compression,
+       length(data) AS data_length,
+       left(data, 35) AS data_prefix
+FROM test_zstd_basic;
+
+--
+-- Test extended header format with legacy compression methods
+--
+
+-- When use_extended_toast_header is on, pglz/lz4 use the 20-byte format
+SET use_extended_toast_header = on;
+
+CREATE TABLE test_pglz_extended (data text COMPRESSION pglz);
+INSERT INTO test_pglz_extended (data)
+    VALUES (repeat('PGLZ with extended header format. ', 3000));
+
+SELECT pg_column_compression(data) AS compression,
+       length(data) AS data_length
+FROM test_pglz_extended;
+
+-- Verify slice access works with extended format
+SELECT substr(data, 50, 34) AS slice FROM test_pglz_extended;
+
+--
+-- Test data integrity across compression methods
+--
+
+CREATE TABLE test_integrity (
+    method text,
+    original_data text,
+    compressed_data text
+);
+
+-- Store test data with each method
+INSERT INTO test_integrity VALUES
+    ('pglz', repeat('Integrity test data pattern. ', 2000), NULL),
+    ('zstd', repeat('Integrity test data pattern. ', 2000), NULL);
+
+-- Create compressed versions
+CREATE TABLE test_pglz_integrity (data text COMPRESSION pglz);
+CREATE TABLE test_zstd_integrity (data text COMPRESSION zstd);
+
+INSERT INTO test_pglz_integrity SELECT original_data FROM test_integrity WHERE method = 'pglz';
+INSERT INTO test_zstd_integrity SELECT original_data FROM test_integrity WHERE method = 'zstd';
+
+-- Verify checksums match
+SELECT 'pglz' AS method,
+       md5((SELECT original_data FROM test_integrity WHERE method = 'pglz')) =
+       md5((SELECT data FROM test_pglz_integrity)) AS checksum_match;
+
+SELECT 'zstd' AS method,
+       md5((SELECT original_data FROM test_integrity WHERE method = 'zstd')) =
+       md5((SELECT data FROM test_zstd_integrity)) AS checksum_match;
+
+--
+-- Test table rewrite operations (CLUSTER, VACUUM FULL)
+--
+
+CREATE TABLE test_cluster_zstd (id serial PRIMARY KEY, data text COMPRESSION zstd);
+INSERT INTO test_cluster_zstd (data)
+    VALUES (repeat('Data for CLUSTER test with zstd compression. ', 2500));
+
+-- Capture original data hash
+SELECT 'before_cluster' AS stage, md5(data) AS hash FROM test_cluster_zstd;
+
+CLUSTER test_cluster_zstd USING test_cluster_zstd_pkey;
+
+SELECT 'after_cluster' AS stage,
+       pg_column_compression(data) AS compression,
+       md5(data) AS hash
+FROM test_cluster_zstd;
+
+VACUUM FULL test_cluster_zstd;
+
+SELECT 'after_vacuum_full' AS stage,
+       pg_column_compression(data) AS compression,
+       md5(data) AS hash
+FROM test_cluster_zstd;
+
+--
+-- Test mixed format data (GUC toggling)
+--
+
+SET use_extended_toast_header = on;
+CREATE TABLE test_guc_toggle (id serial, data text COMPRESSION pglz);
+INSERT INTO test_guc_toggle (data)
+    VALUES (repeat('Data created with extended header on. ', 3000));
+
+SELECT 'with_ext_on' AS stage,
+       pg_column_compression(data) AS compression,
+       length(data) AS data_length
+FROM test_guc_toggle;
+
+SET use_extended_toast_header = off;
+INSERT INTO test_guc_toggle (data)
+    VALUES (repeat('Data created with extended header off. ', 3000));
+
+-- Both rows readable
+SELECT id,
+       pg_column_compression(data) AS compression,
+       length(data) AS data_length,
+       left(data, 39) AS data_prefix
+FROM test_guc_toggle ORDER BY id;
+
+SET use_extended_toast_header = on;
+SELECT id, length(data) AS data_length FROM test_guc_toggle ORDER BY id;
+
+--
+-- Cleanup
+--
+
+DROP TABLE test_zstd_basic;
+DROP TABLE test_pglz_extended;
+DROP TABLE test_integrity;
+DROP TABLE test_pglz_integrity;
+DROP TABLE test_zstd_integrity;
+DROP TABLE test_cluster_zstd;
+DROP TABLE test_guc_toggle;
+
+DROP EXTENSION test_toast_ext;
diff --git a/src/test/modules/test_toast_ext/test_toast_ext--1.0.sql b/src/test/modules/test_toast_ext/test_toast_ext--1.0.sql
new file mode 100644
index 00000000000..ada7c1916c3
--- /dev/null
+++ b/src/test/modules/test_toast_ext/test_toast_ext--1.0.sql
@@ -0,0 +1,19 @@
+/* src/test/modules/test_toast_ext/test_toast_ext--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_toast_ext" to load this file. \quit
+
+CREATE FUNCTION test_toast_structure_sizes()
+RETURNS text
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_toast_flag_validation()
+RETURNS text
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_toast_compression_ids()
+RETURNS text
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT;
diff --git a/src/test/modules/test_toast_ext/test_toast_ext.c b/src/test/modules/test_toast_ext/test_toast_ext.c
new file mode 100644
index 00000000000..8251e89cb50
--- /dev/null
+++ b/src/test/modules/test_toast_ext/test_toast_ext.c
@@ -0,0 +1,200 @@
+/*-------------------------------------------------------------------------
+ *
+ * test_toast_ext.c
+ *		Test module for extended TOAST header structures and zstd compression
+ *
+ * Copyright (c) 2025, PostgreSQL Global Development Group
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "access/detoast.h"
+#include "access/toast_compression.h"
+#include "utils/builtins.h"
+#include "varatt.h"
+
+PG_MODULE_MAGIC;
+
+/*
+ * Test structure sizes for extended TOAST pointers
+ */
+PG_FUNCTION_INFO_V1(test_toast_structure_sizes);
+
+Datum
+test_toast_structure_sizes(PG_FUNCTION_ARGS)
+{
+	StringInfoData buf;
+	bool		all_passed = true;
+
+	initStringInfo(&buf);
+
+	/* Test standard structure size */
+	if (sizeof(varatt_external) != 16)
+	{
+		appendStringInfo(&buf, "FAIL: varatt_external is %zu bytes, expected 16\n",
+						 sizeof(varatt_external));
+		all_passed = false;
+	}
+	else
+		appendStringInfo(&buf, "PASS: varatt_external is 16 bytes\n");
+
+	/* Test extended structure size */
+	if (sizeof(varatt_external_extended) != 20)
+	{
+		appendStringInfo(&buf, "FAIL: varatt_external_extended is %zu bytes, expected 20\n",
+						 sizeof(varatt_external_extended));
+		all_passed = false;
+	}
+	else
+		appendStringInfo(&buf, "PASS: varatt_external_extended is 20 bytes\n");
+
+	/* Test TOAST pointer sizes */
+	if (TOAST_POINTER_SIZE != 18)
+	{
+		appendStringInfo(&buf, "FAIL: TOAST_POINTER_SIZE is %zu, expected 18\n",
+						 (Size) TOAST_POINTER_SIZE);
+		all_passed = false;
+	}
+	else
+		appendStringInfo(&buf, "PASS: TOAST_POINTER_SIZE is 18 bytes\n");
+
+	if (TOAST_POINTER_SIZE_EXTENDED != 22)
+	{
+		appendStringInfo(&buf, "FAIL: TOAST_POINTER_SIZE_EXTENDED is %zu, expected 22\n",
+						 (Size) TOAST_POINTER_SIZE_EXTENDED);
+		all_passed = false;
+	}
+	else
+		appendStringInfo(&buf, "PASS: TOAST_POINTER_SIZE_EXTENDED is 22 bytes\n");
+
+	/* Test field offsets */
+	if (offsetof(varatt_external_extended, va_rawsize) != 0)
+		appendStringInfo(&buf, "FAIL: va_rawsize offset\n"), all_passed = false;
+	if (offsetof(varatt_external_extended, va_extinfo) != 4)
+		appendStringInfo(&buf, "FAIL: va_extinfo offset\n"), all_passed = false;
+	if (offsetof(varatt_external_extended, va_flags) != 8)
+		appendStringInfo(&buf, "FAIL: va_flags offset\n"), all_passed = false;
+	if (offsetof(varatt_external_extended, va_data) != 9)
+		appendStringInfo(&buf, "FAIL: va_data offset\n"), all_passed = false;
+	if (offsetof(varatt_external_extended, va_valueid) != 12)
+		appendStringInfo(&buf, "FAIL: va_valueid offset\n"), all_passed = false;
+	if (offsetof(varatt_external_extended, va_toastrelid) != 16)
+		appendStringInfo(&buf, "FAIL: va_toastrelid offset\n"), all_passed = false;
+	else
+		appendStringInfo(&buf, "PASS: All field offsets correct (no padding)\n");
+
+	if (all_passed)
+		appendStringInfo(&buf, "\nResult: ALL TESTS PASSED\n");
+	else
+		appendStringInfo(&buf, "\nResult: SOME TESTS FAILED\n");
+
+	PG_RETURN_TEXT_P(cstring_to_text(buf.data));
+}
+
+/*
+ * Test flag validation macros
+ */
+PG_FUNCTION_INFO_V1(test_toast_flag_validation);
+
+Datum
+test_toast_flag_validation(PG_FUNCTION_ARGS)
+{
+	StringInfoData buf;
+	bool		all_passed = true;
+
+	initStringInfo(&buf);
+
+	/* Test valid flags */
+	if (!ExtendedFlagsAreValid(0x00))
+		appendStringInfo(&buf, "FAIL: flags 0x00 should be valid\n"), all_passed = false;
+	if (!ExtendedFlagsAreValid(0x01))
+		appendStringInfo(&buf, "FAIL: flags 0x01 should be valid\n"), all_passed = false;
+	if (!ExtendedFlagsAreValid(0x02))
+		appendStringInfo(&buf, "FAIL: flags 0x02 should be valid\n"), all_passed = false;
+	if (!ExtendedFlagsAreValid(0x03))
+		appendStringInfo(&buf, "FAIL: flags 0x03 should be valid\n"), all_passed = false;
+	else
+		appendStringInfo(&buf, "PASS: Valid flags (0x00-0x03) accepted\n");
+
+	/* Test invalid flags */
+	if (ExtendedFlagsAreValid(0x04))
+		appendStringInfo(&buf, "FAIL: flags 0x04 should be invalid\n"), all_passed = false;
+	if (ExtendedFlagsAreValid(0x08))
+		appendStringInfo(&buf, "FAIL: flags 0x08 should be invalid\n"), all_passed = false;
+	if (ExtendedFlagsAreValid(0xFF))
+		appendStringInfo(&buf, "FAIL: flags 0xFF should be invalid\n"), all_passed = false;
+	else
+		appendStringInfo(&buf, "PASS: Invalid flags (0x04+) rejected\n");
+
+	/* Test compression method validation */
+	if (!ExtendedCompressionMethodIsValid(0))
+		appendStringInfo(&buf, "FAIL: method 0 should be valid\n"), all_passed = false;
+	if (!ExtendedCompressionMethodIsValid(255))
+		appendStringInfo(&buf, "FAIL: method 255 should be valid\n"), all_passed = false;
+	else
+		appendStringInfo(&buf, "PASS: Compression methods 0-255 valid\n");
+
+	/* Test compression method IDs */
+	if (TOAST_PGLZ_EXT_METHOD != 0)
+		appendStringInfo(&buf, "FAIL: TOAST_PGLZ_EXT_METHOD should be 0\n"), all_passed = false;
+	if (TOAST_LZ4_EXT_METHOD != 1)
+		appendStringInfo(&buf, "FAIL: TOAST_LZ4_EXT_METHOD should be 1\n"), all_passed = false;
+	if (TOAST_ZSTD_EXT_METHOD != 2)
+		appendStringInfo(&buf, "FAIL: TOAST_ZSTD_EXT_METHOD should be 2\n"), all_passed = false;
+	if (TOAST_UNCOMPRESSED_EXT_METHOD != 3)
+		appendStringInfo(&buf, "FAIL: TOAST_UNCOMPRESSED_EXT_METHOD should be 3\n"), all_passed = false;
+	else
+		appendStringInfo(&buf, "PASS: Compression method IDs correct\n");
+
+	if (all_passed)
+		appendStringInfo(&buf, "\nResult: ALL TESTS PASSED\n");
+	else
+		appendStringInfo(&buf, "\nResult: SOME TESTS FAILED\n");
+
+	PG_RETURN_TEXT_P(cstring_to_text(buf.data));
+}
+
+/*
+ * Test compression ID constants
+ */
+PG_FUNCTION_INFO_V1(test_toast_compression_ids);
+
+Datum
+test_toast_compression_ids(PG_FUNCTION_ARGS)
+{
+	StringInfoData buf;
+	bool		all_passed = true;
+
+	initStringInfo(&buf);
+
+	/* Standard compression IDs */
+	if (TOAST_PGLZ_COMPRESSION_ID != 0)
+		appendStringInfo(&buf, "FAIL: TOAST_PGLZ_COMPRESSION_ID != 0\n"), all_passed = false;
+	if (TOAST_LZ4_COMPRESSION_ID != 1)
+		appendStringInfo(&buf, "FAIL: TOAST_LZ4_COMPRESSION_ID != 1\n"), all_passed = false;
+	if (TOAST_INVALID_COMPRESSION_ID != 2)
+		appendStringInfo(&buf, "FAIL: TOAST_INVALID_COMPRESSION_ID != 2\n"), all_passed = false;
+	if (TOAST_EXTENDED_COMPRESSION_ID != 3)
+		appendStringInfo(&buf, "FAIL: TOAST_EXTENDED_COMPRESSION_ID != 3\n"), all_passed = false;
+	else
+		appendStringInfo(&buf, "PASS: Standard compression IDs correct (0,1,2,3)\n");
+
+	/* Extended compression IDs match standard where applicable */
+	if (TOAST_PGLZ_EXT_METHOD != TOAST_PGLZ_COMPRESSION_ID)
+		appendStringInfo(&buf, "FAIL: PGLZ IDs don't match (standard=%d, extended=%d)\n",
+						 TOAST_PGLZ_COMPRESSION_ID, TOAST_PGLZ_EXT_METHOD), all_passed = false;
+	if (TOAST_LZ4_EXT_METHOD != TOAST_LZ4_COMPRESSION_ID)
+		appendStringInfo(&buf, "FAIL: LZ4 IDs don't match (standard=%d, extended=%d)\n",
+						 TOAST_LZ4_COMPRESSION_ID, TOAST_LZ4_EXT_METHOD), all_passed = false;
+	else
+		appendStringInfo(&buf, "PASS: PGLZ/LZ4 IDs consistent between formats\n");
+
+	if (all_passed)
+		appendStringInfo(&buf, "\nResult: ALL TESTS PASSED\n");
+	else
+		appendStringInfo(&buf, "\nResult: SOME TESTS FAILED\n");
+
+	PG_RETURN_TEXT_P(cstring_to_text(buf.data));
+}
diff --git a/src/test/modules/test_toast_ext/test_toast_ext.control b/src/test/modules/test_toast_ext/test_toast_ext.control
new file mode 100644
index 00000000000..d59ee14ad64
--- /dev/null
+++ b/src/test/modules/test_toast_ext/test_toast_ext.control
@@ -0,0 +1,5 @@
+# test_toast_ext extension
+comment = 'Test module for extended TOAST headers and zstd compression'
+default_version = '1.0'
+module_pathname = '$libdir/test_toast_ext'
+relocatable = true
-- 
2.39.3 (Apple Git-146)

#2Dharin Shah
dharinshah95@gmail.com
In reply to: Dharin Shah (#1)
1 attachment(s)
Re: [PATCH] Add zstd compression for TOAST using extended header format

Hello,

Apologies for the spam, updated the patch with the tests corrected.

Thanks,
Dharin

On Sat, Dec 13, 2025 at 6:31 PM Dharin Shah <dharinshah95@gmail.com> wrote:

Show quoted text

Hello PG Hackers,

Want to submit a patch that implements zstd compression for TOAST data
using a 20-byte TOAST pointer format, directly addressing the concerns
raised in prior discussions [1
</messages/by-id/CAFAfj_F4qeRCNCYPk1vgH42fDZpjQWKO+ufq3FyoVyUa5AviFA@mail.gmail.com
][2
</messages/by-id/CAJ7c6TOtAB0z1UrksvGTStNE-herK-43bj22=5xVBg7S4vr5rQ@mail.gmail.com&gt;
][3
</messages/by-id/YoMiNmkztrslDbNS@paquier.xyz&gt;
].

A bit of a background in the 2022 thread [3
</messages/by-id/YoMiNmkztrslDbNS@paquier.xyz&gt;%5D,
Robert Haas suggested:
"we had better reserve the fourth bit pattern for something extensible
e.g. another byte or several to specify the actual method"

i.e. something like:
00 = PGLZ
01 = LZ4
10 = reserved for future emergencies
11 = extended header with additional type byte

Michael also asked whether we should have "something a bit more extensible
for the design of an extensible varlena header."

This patch implements that idea.
The format:

struct varatt_external_extended {
int32 va_rawsize; /* same as legacy */
uint32 va_extinfo; /* cmid=3 signals extended format */
uint8 va_flags; /* feature flags */
uint8 va_data[3]; /* va_data[0] = compression method */
Oid va_valueid; /* same as legacy */
Oid va_toastrelid; /* same as legacy */
};

*A few notes:*

- Zstd only applies to external TOAST, not inline compression. The 2-bit
limit in va_tcinfo stays as-is for inline data, where pglz/lz4 work fine
anyway. Zstd's wins show up on larger values.
- A GUC use_extended_toast_header controls whether pglz/lz4 also use the
20-byte format (defaults to off for compatibility, can enable it if you
want consistency).
- Legacy 16-byte pointers continue to work - we check the vartag to
determine which format to read.

The 4 extra bytes per pointer is negligible for typical TOAST data sizes,
and it gives us room to grow.

Regards,
Dharin

Attachments:

zstd-toast-compression-external.patchapplication/octet-stream; name=zstd-toast-compression-external.patchDownload
From fdaae5dc9e9837f73b991100adcba6d76dda1f40 Mon Sep 17 00:00:00 2001
From: Dharin Shah <8616130+Dharin-shah@users.noreply.github.com>
Date: Sat, 13 Dec 2025 11:16:35 +0100
Subject: [PATCH] Add zstd compression support for TOAST using extended header
 format

---
 contrib/amcheck/verify_heapam.c               |  69 +++++-
 src/backend/access/common/detoast.c           | 164 ++++++++++++---
 src/backend/access/common/toast_compression.c | 199 +++++++++++++++++-
 src/backend/access/common/toast_internals.c   | 198 +++++++++++++++--
 src/backend/access/table/toast_helper.c       |   2 +-
 .../replication/logical/reorderbuffer.c       |  38 +++-
 src/backend/utils/adt/varlena.c               |  26 ++-
 src/backend/utils/misc/guc_parameters.dat     |   7 +-
 src/backend/utils/misc/guc_tables.c           |   3 +
 src/include/access/detoast.h                  |  41 +++-
 src/include/access/toast_compression.h        |  36 ++++
 src/include/access/toast_internals.h          |  10 +-
 src/include/varatt.h                          | 160 +++++++++++++-
 src/test/modules/meson.build                  |   1 +
 src/test/modules/test_toast_ext/Makefile      |  20 ++
 .../expected/test_toast_ext.out               | 187 ++++++++++++++++
 .../expected/test_toast_ext_1.out             |  37 ++++
 src/test/modules/test_toast_ext/meson.build   |  33 +++
 .../test_toast_ext/sql/test_toast_ext.sql     | 136 ++++++++++++
 .../test_toast_ext/test_toast_ext--1.0.sql    |  19 ++
 .../modules/test_toast_ext/test_toast_ext.c   | 140 ++++++++++++
 .../test_toast_ext/test_toast_ext.control     |   5 +
 22 files changed, 1440 insertions(+), 91 deletions(-)
 create mode 100644 src/test/modules/test_toast_ext/Makefile
 create mode 100644 src/test/modules/test_toast_ext/expected/test_toast_ext.out
 create mode 100644 src/test/modules/test_toast_ext/expected/test_toast_ext_1.out
 create mode 100644 src/test/modules/test_toast_ext/meson.build
 create mode 100644 src/test/modules/test_toast_ext/sql/test_toast_ext.sql
 create mode 100644 src/test/modules/test_toast_ext/test_toast_ext--1.0.sql
 create mode 100644 src/test/modules/test_toast_ext/test_toast_ext.c
 create mode 100644 src/test/modules/test_toast_ext/test_toast_ext.control

diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 130b3533463..25cae4d0380 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1665,6 +1665,8 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	uint16		infomask;
 	CompactAttribute *thisatt;
 	struct varatt_external toast_pointer;
+	struct varatt_external_extended toast_pointer_ext;
+	bool		is_extended;
 
 	infomask = ctx->tuphdr->t_infomask;
 	thisatt = TupleDescCompactAttr(RelationGetDescr(ctx->rel), ctx->attnum);
@@ -1717,13 +1719,14 @@ check_tuple_attribute(HeapCheckContext *ctx)
 
 	/*
 	 * Check that VARTAG_SIZE won't hit an Assert on a corrupt va_tag before
-	 * risking a call into att_addlength_pointer
+	 * risking a call into att_addlength_pointer.  Both legacy (VARTAG_ONDISK)
+	 * and extended (VARTAG_ONDISK_EXTENDED) on-disk formats are valid.
 	 */
 	if (VARATT_IS_EXTERNAL(tp + ctx->offset))
 	{
 		uint8		va_tag = VARTAG_EXTERNAL(tp + ctx->offset);
 
-		if (va_tag != VARTAG_ONDISK)
+		if (va_tag != VARTAG_ONDISK && va_tag != VARTAG_ONDISK_EXTENDED)
 		{
 			report_corruption(ctx,
 							  psprintf("toasted attribute has unexpected TOAST tag %u",
@@ -1768,9 +1771,23 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	/* It is external, and we're looking at a page on disk */
 
 	/*
-	 * Must copy attr into toast_pointer for alignment considerations
+	 * Must copy attr into toast_pointer for alignment considerations.
+	 * Handle both legacy (VARTAG_ONDISK) and extended (VARTAG_ONDISK_EXTENDED)
+	 * formats.
 	 */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	is_extended = (VARTAG_EXTERNAL(attr) == VARTAG_ONDISK_EXTENDED);
+
+	if (is_extended)
+	{
+		VARATT_EXTERNAL_GET_POINTER_EXTENDED(toast_pointer_ext, attr);
+		/* Copy common fields for simpler code below */
+		toast_pointer.va_rawsize = toast_pointer_ext.va_rawsize;
+		toast_pointer.va_extinfo = toast_pointer_ext.va_extinfo;
+		toast_pointer.va_valueid = toast_pointer_ext.va_valueid;
+		toast_pointer.va_toastrelid = toast_pointer_ext.va_toastrelid;
+	}
+	else
+		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 
 	/* Toasted attributes too large to be untoasted should never be stored */
 	if (toast_pointer.va_rawsize > VARLENA_SIZE_LIMIT)
@@ -1785,8 +1802,11 @@ check_tuple_attribute(HeapCheckContext *ctx)
 		ToastCompressionId cmid;
 		bool		valid = false;
 
-		/* Compressed attributes should have a valid compression method */
-		cmid = TOAST_COMPRESS_METHOD(&toast_pointer);
+		/*
+		 * Compressed attributes should have a valid compression method.
+		 * For extended pointers with cmid==3, the actual method is in va_data[0].
+		 */
+		cmid = VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer);
 		switch (cmid)
 		{
 				/* List of all valid compression method IDs */
@@ -1795,6 +1815,27 @@ check_tuple_attribute(HeapCheckContext *ctx)
 				valid = true;
 				break;
 
+				/* Extended compression (zstd or pglz/lz4 in extended format) */
+			case TOAST_EXTENDED_COMPRESSION_ID:
+				if (is_extended)
+				{
+					uint8	ext_method = VARATT_EXTERNAL_GET_EXT_COMPRESSION_METHOD(toast_pointer_ext);
+
+					/* Validate extended compression method */
+					switch (ext_method)
+					{
+						case TOAST_PGLZ_EXT_METHOD:
+						case TOAST_LZ4_EXT_METHOD:
+						case TOAST_ZSTD_EXT_METHOD:
+							valid = true;
+							break;
+						default:
+							/* Invalid extended method will be reported below */
+							break;
+					}
+				}
+				break;
+
 				/* Recognized but invalid compression method ID */
 			case TOAST_INVALID_COMPRESSION_ID:
 				break;
@@ -1840,7 +1881,21 @@ check_tuple_attribute(HeapCheckContext *ctx)
 
 		ta = palloc0_object(ToastedAttribute);
 
-		VARATT_EXTERNAL_GET_POINTER(ta->toast_pointer, attr);
+		/*
+		 * Extract toast pointer based on format.  For extended format,
+		 * copy common fields from toast_pointer which we already extracted
+		 * above.
+		 */
+		if (is_extended)
+		{
+			ta->toast_pointer.va_rawsize = toast_pointer.va_rawsize;
+			ta->toast_pointer.va_extinfo = toast_pointer.va_extinfo;
+			ta->toast_pointer.va_valueid = toast_pointer.va_valueid;
+			ta->toast_pointer.va_toastrelid = toast_pointer.va_toastrelid;
+		}
+		else
+			VARATT_EXTERNAL_GET_POINTER(ta->toast_pointer, attr);
+
 		ta->blkno = ctx->blkno;
 		ta->offnum = ctx->offnum;
 		ta->attnum = ctx->attnum;
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 62651787742..6d1c08900e8 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_compression.h"
 #include "access/toast_internals.h"
 #include "common/int.h"
 #include "common/pg_lzcompress.h"
@@ -225,12 +226,47 @@ detoast_attr_slice(struct varlena *attr,
 
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
-		struct varatt_external toast_pointer;
+		int32		max_size;
+		bool		is_compressed;
+		bool		is_pglz = false;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		/*
+		 * Handle both legacy 16-byte and extended 20-byte on-disk TOAST
+		 * pointers.  Check the vartag to determine which format.
+		 */
+		if (VARTAG_EXTERNAL(attr) == VARTAG_ONDISK_EXTENDED)
+		{
+			struct varatt_external_extended toast_pointer_ext;
+			uint8		ext_method;
+
+			VARATT_EXTERNAL_GET_POINTER_EXTENDED(toast_pointer_ext, attr);
+			max_size = VARATT_EXTERNAL_GET_EXTSIZE_EXTENDED(toast_pointer_ext);
+			is_compressed = VARATT_EXTERNAL_IS_COMPRESSED_EXTENDED(toast_pointer_ext);
+
+			/* Check if this is pglz for slice optimization */
+			if (is_compressed &&
+				VARATT_EXTERNAL_HAS_FLAG(toast_pointer_ext, TOAST_EXT_FLAG_COMPRESSION))
+			{
+				ext_method = VARATT_EXTERNAL_GET_EXT_COMPRESSION_METHOD(toast_pointer_ext);
+				is_pglz = (ext_method == TOAST_PGLZ_EXT_METHOD);
+			}
+		}
+		else
+		{
+			struct varatt_external toast_pointer;
+
+			VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+			max_size = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+			is_compressed = VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer);
+
+			/* Check if this is pglz for slice optimization */
+			if (is_compressed)
+				is_pglz = (VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) ==
+						   TOAST_PGLZ_COMPRESSION_ID);
+		}
 
 		/* fast path for non-compressed external datums */
-		if (!VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+		if (!is_compressed)
 			return toast_fetch_datum_slice(attr, sliceoffset, slicelength);
 
 		/*
@@ -240,19 +276,16 @@ detoast_attr_slice(struct varlena *attr,
 		 */
 		if (slicelimit >= 0)
 		{
-			int32		max_size = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
-
 			/*
 			 * 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 zstd 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 (VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) ==
-				TOAST_PGLZ_COMPRESSION_ID)
+			if (is_pglz)
 				max_size = pglz_maximum_compressed_size(slicelimit, max_size);
 
 			/*
@@ -344,20 +377,42 @@ toast_fetch_datum(struct varlena *attr)
 {
 	Relation	toastrel;
 	struct varlena *result;
-	struct varatt_external toast_pointer;
 	int32		attrsize;
+	Oid			toastrelid;
+	Oid			valueid;
+	bool		is_compressed;
 
 	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);
+	/*
+	 * Handle both legacy 16-byte and extended 20-byte on-disk TOAST pointers.
+	 * Check the vartag to determine which format we're dealing with.
+	 */
+	if (VARTAG_EXTERNAL(attr) == VARTAG_ONDISK_EXTENDED)
+	{
+		struct varatt_external_extended toast_pointer_ext;
+
+		VARATT_EXTERNAL_GET_POINTER_EXTENDED(toast_pointer_ext, attr);
+		attrsize = VARATT_EXTERNAL_GET_EXTSIZE_EXTENDED(toast_pointer_ext);
+		toastrelid = toast_pointer_ext.va_toastrelid;
+		valueid = toast_pointer_ext.va_valueid;
+		is_compressed = VARATT_EXTERNAL_IS_COMPRESSED_EXTENDED(toast_pointer_ext);
+	}
+	else
+	{
+		struct varatt_external toast_pointer;
 
-	attrsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		attrsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+		toastrelid = toast_pointer.va_toastrelid;
+		valueid = toast_pointer.va_valueid;
+		is_compressed = VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer);
+	}
 
 	result = (struct varlena *) palloc(attrsize + VARHDRSZ);
 
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+	if (is_compressed)
 		SET_VARSIZE_COMPRESSED(result, attrsize + VARHDRSZ);
 	else
 		SET_VARSIZE(result, attrsize + VARHDRSZ);
@@ -369,10 +424,10 @@ toast_fetch_datum(struct varlena *attr)
 	/*
 	 * Open the toast relation and its indexes
 	 */
-	toastrel = table_open(toast_pointer.va_toastrelid, AccessShareLock);
+	toastrel = table_open(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 +453,45 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 {
 	Relation	toastrel;
 	struct varlena *result;
-	struct varatt_external toast_pointer;
 	int32		attrsize;
+	Oid			toastrelid;
+	Oid			valueid;
+	bool		is_compressed;
 
 	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);
+	/*
+	 * Handle both legacy 16-byte and extended 20-byte on-disk TOAST pointers.
+	 * Check the vartag to determine which format we're dealing with.
+	 */
+	if (VARTAG_EXTERNAL(attr) == VARTAG_ONDISK_EXTENDED)
+	{
+		struct varatt_external_extended toast_pointer_ext;
+
+		VARATT_EXTERNAL_GET_POINTER_EXTENDED(toast_pointer_ext, attr);
+		attrsize = VARATT_EXTERNAL_GET_EXTSIZE_EXTENDED(toast_pointer_ext);
+		toastrelid = toast_pointer_ext.va_toastrelid;
+		valueid = toast_pointer_ext.va_valueid;
+		is_compressed = VARATT_EXTERNAL_IS_COMPRESSED_EXTENDED(toast_pointer_ext);
+	}
+	else
+	{
+		struct varatt_external toast_pointer;
+
+		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		attrsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+		toastrelid = toast_pointer.va_toastrelid;
+		valueid = toast_pointer.va_valueid;
+		is_compressed = VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer);
+	}
 
 	/*
 	 * 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);
-
-	attrsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+	Assert(!is_compressed || 0 == sliceoffset);
 
 	if (sliceoffset >= attrsize)
 	{
@@ -427,7 +504,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 (is_compressed && slicelength > 0)
 		slicelength = slicelength + sizeof(int32);
 
 	/*
@@ -440,7 +517,7 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 
 	result = (struct varlena *) palloc(slicelength + VARHDRSZ);
 
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+	if (is_compressed)
 		SET_VARSIZE_COMPRESSED(result, slicelength + VARHDRSZ);
 	else
 		SET_VARSIZE(result, slicelength + VARHDRSZ);
@@ -449,10 +526,10 @@ 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(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);
 
@@ -485,6 +562,9 @@ toast_decompress_datum(struct varlena *attr)
 			return pglz_decompress_datum(attr);
 		case TOAST_LZ4_COMPRESSION_ID:
 			return lz4_decompress_datum(attr);
+		case TOAST_EXTENDED_COMPRESSION_ID:
+			/* zstd-compressed data */
+			return zstd_decompress_datum(attr);
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 			return NULL;		/* keep compiler quiet */
@@ -528,6 +608,9 @@ 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_EXTENDED_COMPRESSION_ID:
+			/* zstd-compressed data */
+			return zstd_decompress_datum_slice(attr, slicelength);
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 			return NULL;		/* keep compiler quiet */
@@ -549,11 +632,15 @@ 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;
+		/*
+		 * va_rawsize is the size of the original datum -- including header.
+		 * It's at offset 0 in both varatt_external and varatt_external_extended,
+		 * so we can read just the first 4 bytes regardless of format.
+		 */
+		int32	va_rawsize;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-		result = toast_pointer.va_rawsize;
+		memcpy(&va_rawsize, VARDATA_EXTERNAL(attr), sizeof(va_rawsize));
+		result = va_rawsize;
 	}
 	else if (VARATT_IS_EXTERNAL_INDIRECT(attr))
 	{
@@ -609,11 +696,18 @@ 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?
+		 *
+		 * va_extinfo is at offset 4 in both varatt_external and
+		 * varatt_external_extended, so we can read the first 8 bytes
+		 * regardless of format.
 		 */
-		struct varatt_external toast_pointer;
+		struct {
+			int32	va_rawsize;
+			uint32	va_extinfo;
+		} common;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-		result = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+		memcpy(&common, VARDATA_EXTERNAL(attr), sizeof(common));
+		result = common.va_extinfo & VARLENA_EXTSIZE_MASK;
 	}
 	else if (VARATT_IS_EXTERNAL_INDIRECT(attr))
 	{
diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c
index 926f1e4008a..422e2c5967a 100644
--- a/src/backend/access/common/toast_compression.c
+++ b/src/backend/access/common/toast_compression.c
@@ -17,13 +17,19 @@
 #include <lz4.h>
 #endif
 
+#ifdef USE_ZSTD
+#include <zstd.h>
+#endif
+
 #include "access/detoast.h"
 #include "access/toast_compression.h"
 #include "common/pg_lzcompress.h"
+#include "utils/memutils.h"
 #include "varatt.h"
 
 /* GUC */
 int			default_toast_compression = TOAST_PGLZ_COMPRESSION;
+bool		use_extended_toast_header = false;
 
 #define NO_COMPRESSION_SUPPORT(method) \
 	ereport(ERROR, \
@@ -249,11 +255,16 @@ lz4_decompress_datum_slice(const struct varlena *value, int32 slicelength)
  * Extract compression ID from a varlena.
  *
  * Returns TOAST_INVALID_COMPRESSION_ID if the varlena is not compressed.
+ *
+ * For external data stored in extended format (VARTAG_ONDISK_EXTENDED),
+ * the actual compression method is stored in va_data[0].  We map that
+ * back to the appropriate ToastCompressionId for legacy compatibility.
  */
 ToastCompressionId
 toast_get_compression_id(struct varlena *attr)
 {
 	ToastCompressionId cmid = TOAST_INVALID_COMPRESSION_ID;
+	vartag_external tag;
 
 	/*
 	 * If it is stored externally then fetch the compression method id from
@@ -262,12 +273,52 @@ toast_get_compression_id(struct varlena *attr)
 	 */
 	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);
+		tag = VARTAG_EXTERNAL(attr);
+		if (tag == VARTAG_ONDISK)
+		{
+			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);
+		}
+		else
+		{
+			struct varatt_external_extended toast_pointer_ext;
+			uint8		ext_method;
+
+			Assert(tag == VARTAG_ONDISK_EXTENDED);
+			VARATT_EXTERNAL_GET_POINTER_EXTENDED(toast_pointer_ext, attr);
+
+			if (VARATT_EXTERNAL_IS_COMPRESSED_EXTENDED(toast_pointer_ext))
+			{
+				/*
+				 * Extended format stores the actual method in va_data[0].
+				 * Map it back to ToastCompressionId for reporting purposes.
+				 */
+				ext_method = VARATT_EXTERNAL_GET_EXT_COMPRESSION_METHOD(toast_pointer_ext);
+				switch (ext_method)
+				{
+					case TOAST_PGLZ_EXT_METHOD:
+						cmid = TOAST_PGLZ_COMPRESSION_ID;
+						break;
+					case TOAST_LZ4_EXT_METHOD:
+						cmid = TOAST_LZ4_COMPRESSION_ID;
+						break;
+					case TOAST_ZSTD_EXT_METHOD:
+						cmid = TOAST_EXTENDED_COMPRESSION_ID;
+						break;
+					case TOAST_UNCOMPRESSED_EXT_METHOD:
+						/* Uncompressed data in extended format */
+						cmid = TOAST_INVALID_COMPRESSION_ID;
+						break;
+					default:
+						elog(ERROR, "invalid extended compression method %d",
+							 ext_method);
+				}
+			}
+		}
 	}
 	else if (VARATT_IS_COMPRESSED(attr))
 		cmid = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(attr);
@@ -275,6 +326,133 @@ toast_get_compression_id(struct varlena *attr)
 	return cmid;
 }
 
+/*
+ * Zstandard (zstd) compression/decompression for TOAST (extended methods).
+ *
+ * These routines use the same basic shape as the pglz and LZ4 helpers,
+ * but are only available when PostgreSQL is built with USE_ZSTD.
+ */
+
+/*
+ * Compress a varlena using ZSTD.
+ *
+ * Returns the compressed varlena, or NULL if compression fails or does
+ * not save space.
+ */
+static struct varlena *
+zstd_compress_datum_internal(const struct varlena *value, int level)
+{
+#ifndef USE_ZSTD
+	NO_COMPRESSION_SUPPORT("zstd");
+	return NULL;				/* keep compiler quiet */
+#else
+	Size		valsize;
+	Size		max_size;
+	Size		out_size;
+	struct varlena *tmp;
+	size_t		rc;
+
+	valsize = VARSIZE_ANY_EXHDR(value);
+
+	/*
+	 * Compute an upper bound for the compressed size and allocate enough
+	 * space for the compressed payload plus the varlena header.
+	 */
+	max_size = ZSTD_compressBound(valsize);
+	if (max_size > (Size) (MaxAllocSize - VARHDRSZ_COMPRESSED))
+		ereport(ERROR,
+				(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				 errmsg("compressed data would exceed maximum allocation size")));
+
+	tmp = (struct varlena *) palloc(max_size + VARHDRSZ_COMPRESSED);
+
+	rc = ZSTD_compress((char *) tmp + VARHDRSZ_COMPRESSED, max_size,
+					   VARDATA_ANY(value), valsize, level);
+	if (ZSTD_isError(rc))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATA_CORRUPTED),
+				 errmsg_internal("zstd compression failed: %s",
+								 ZSTD_getErrorName(rc))));
+
+	out_size = (Size) rc;
+
+	/*
+	 * If the compressed representation is not smaller than the original
+	 * payload, give up and return NULL so that callers can fall back to
+	 * storing the datum uncompressed or with a different method.
+	 */
+	if (out_size >= valsize)
+	{
+		pfree(tmp);
+		return NULL;
+	}
+
+	SET_VARSIZE_COMPRESSED(tmp, out_size + VARHDRSZ_COMPRESSED);
+
+	return tmp;
+#endif							/* USE_ZSTD */
+}
+
+struct varlena *
+zstd_compress_datum(const struct varlena *value)
+{
+#ifndef USE_ZSTD
+	NO_COMPRESSION_SUPPORT("zstd");
+	return NULL;				/* keep compiler quiet */
+#else
+	return zstd_compress_datum_internal(value, ZSTD_CLEVEL_DEFAULT);
+#endif
+}
+
+/*
+ * Decompress a varlena that was compressed using ZSTD.
+ */
+struct varlena *
+zstd_decompress_datum(const struct varlena *value)
+{
+#ifndef USE_ZSTD
+	NO_COMPRESSION_SUPPORT("zstd");
+	return NULL;				/* keep compiler quiet */
+#else
+	struct varlena *result;
+	Size		rawsize;
+	size_t		rc;
+
+	/* allocate memory for the uncompressed data */
+	rawsize = VARDATA_COMPRESSED_GET_EXTSIZE(value);
+	result = (struct varlena *) palloc(rawsize + VARHDRSZ);
+
+	rc = ZSTD_decompress(VARDATA(result), rawsize,
+						 (char *) value + VARHDRSZ_COMPRESSED,
+						 VARSIZE(value) - VARHDRSZ_COMPRESSED);
+	if (ZSTD_isError(rc) || rc != rawsize)
+		ereport(ERROR,
+				(errcode(ERRCODE_DATA_CORRUPTED),
+				 errmsg_internal("compressed zstd data is corrupt or truncated")));
+
+	SET_VARSIZE(result, rawsize + VARHDRSZ);
+
+	return result;
+#endif							/* USE_ZSTD */
+}
+
+/*
+ * Decompress part of a varlena that was compressed using ZSTD.
+ *
+ * At least initially we don't try to be clever with streaming slice
+ * decompression here; instead we just decompress the full datum and
+ * let higher layers perform the slicing.  Callers should prefer the
+ * regular zstd_decompress_datum() when they know they need the whole
+ * value anyway.
+ */
+struct varlena *
+zstd_decompress_datum_slice(const struct varlena *value, int32 slicelength)
+{
+	/* For now, just fall back to full decompression. */
+	(void) slicelength;
+	return zstd_decompress_datum(value);
+}
+
 /*
  * CompressionNameToMethod - Get compression method from compression name
  *
@@ -293,6 +471,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;
 }
@@ -309,6 +494,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 d06af82de15..039ccc42249 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_compression.h"
 #include "access/toast_internals.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -71,6 +72,9 @@ toast_compress_datum(Datum value, char cmethod)
 			tmp = lz4_compress_datum((const struct varlena *) DatumGetPointer(value));
 			cmid = TOAST_LZ4_COMPRESSION_ID;
 			break;
+		case TOAST_ZSTD_COMPRESSION:
+			/* zstd uses external storage only; handled by toast_save_datum */
+			return PointerGetDatum(NULL);
 		default:
 			elog(ERROR, "invalid compression method %c", cmethod);
 	}
@@ -113,11 +117,13 @@ toast_compress_datum(Datum value, char cmethod)
  * value: datum to be pushed to toast storage
  * oldexternal: if not NULL, toast pointer previously representing the datum
  * options: options to be passed to heap_insert() for toast rows
+ * cmethod: compression method to use for uncompressed data
  * ----------
  */
 Datum
 toast_save_datum(Relation rel, Datum value,
-				 struct varlena *oldexternal, int options)
+				 struct varlena *oldexternal, int options,
+				 char cmethod)
 {
 	Relation	toastrel;
 	Relation   *toastidxs;
@@ -125,12 +131,16 @@ toast_save_datum(Relation rel, Datum value,
 	CommandId	mycid = GetCurrentCommandId(true);
 	struct varlena *result;
 	struct varatt_external toast_pointer;
+	struct varatt_external_extended toast_pointer_ext;
 	int32		chunk_seq = 0;
 	char	   *data_p;
 	int32		data_todo;
 	Pointer		dval = DatumGetPointer(value);
 	int			num_indexes;
 	int			validIndex;
+	bool		use_extended = false;
+	uint8		ext_method = 0;
+	struct varlena *compressed_to_free = NULL;	/* track allocated buffer */
 
 	Assert(!VARATT_IS_EXTERNAL(dval));
 
@@ -167,23 +177,99 @@ toast_save_datum(Relation rel, Datum value,
 	}
 	else if (VARATT_IS_COMPRESSED(dval))
 	{
+		ToastCompressionId cmid;
+
 		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;
 
+		/* Get compression method from compressed datum */
+		cmid = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(dval);
+
+		/* Decide whether to use extended 20-byte or legacy 16-byte format */
+		if (cmid == TOAST_EXTENDED_COMPRESSION_ID)
+		{
+			use_extended = true;
+			ext_method = TOAST_ZSTD_EXT_METHOD;
+		}
+		else if (use_extended_toast_header)
+		{
+			/* Use extended format for pglz/lz4 when GUC is enabled */
+			use_extended = true;
+			switch (cmid)
+			{
+				case TOAST_PGLZ_COMPRESSION_ID:
+					ext_method = TOAST_PGLZ_EXT_METHOD;
+					break;
+				case TOAST_LZ4_COMPRESSION_ID:
+					ext_method = TOAST_LZ4_EXT_METHOD;
+					break;
+				default:
+					/* Should not happen, but fall back to legacy format */
+					use_extended = false;
+					break;
+			}
+		}
+
 		/* set external size and compression method */
-		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, data_todo,
-													 VARDATA_COMPRESSED_GET_COMPRESS_METHOD(dval));
+		if (use_extended)
+			VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, data_todo,
+														VARATT_EXTERNAL_EXTENDED_CMID);
+		else
+			VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, data_todo, cmid);
+
 		/* Assert that the numbers look like it's compressed */
 		Assert(VARATT_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;
+		/*
+		 * Uncompressed data.  If the caller specified zstd compression,
+		 * try to compress it now before storing to the TOAST table.
+		 */
+		if (cmethod == TOAST_ZSTD_COMPRESSION)
+		{
+			struct varlena *compressed;
+			int32		rawsize;
+
+			rawsize = VARSIZE_ANY_EXHDR((const struct varlena *) dval);
+			compressed = zstd_compress_datum((const struct varlena *) dval);
+			if (compressed != NULL)
+			{
+				/* Set compression method in va_tcinfo */
+				TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(compressed, rawsize,
+															TOAST_EXTENDED_COMPRESSION_ID);
+
+				/* Compression succeeded - use the compressed data */
+				compressed_to_free = compressed;	/* track for cleanup */
+				dval = (Pointer) compressed;
+				data_p = VARDATA(compressed);
+				data_todo = VARSIZE(compressed) - VARHDRSZ;
+				toast_pointer.va_rawsize = rawsize + VARHDRSZ;
+
+				/* Use extended format for zstd */
+				use_extended = true;
+				ext_method = TOAST_ZSTD_EXT_METHOD;
+				VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, data_todo,
+															VARATT_EXTERNAL_EXTENDED_CMID);
+			}
+			else
+			{
+				/* Compression failed or didn't save space - store uncompressed */
+				data_p = VARDATA(dval);
+				data_todo = VARSIZE(dval) - VARHDRSZ;
+				toast_pointer.va_rawsize = VARSIZE(dval);
+				toast_pointer.va_extinfo = data_todo;
+			}
+		}
+		else
+		{
+			data_p = VARDATA(dval);
+			data_todo = VARSIZE(dval) - VARHDRSZ;
+			toast_pointer.va_rawsize = VARSIZE(dval);
+			toast_pointer.va_extinfo = data_todo;
+		}
 	}
 
 	/*
@@ -225,15 +311,36 @@ toast_save_datum(Relation rel, Datum value,
 		toast_pointer.va_valueid = InvalidOid;
 		if (oldexternal != NULL)
 		{
-			struct varatt_external old_toast_pointer;
+			Oid			old_toastrelid;
+			Oid			old_valueid;
 
 			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)
+
+			/*
+			 * Extract toastrelid and valueid from the old pointer.
+			 * Handle both legacy 16-byte and extended 20-byte formats.
+			 */
+			if (VARTAG_EXTERNAL(oldexternal) == VARTAG_ONDISK_EXTENDED)
+			{
+				struct varatt_external_extended old_toast_pointer_ext;
+
+				VARATT_EXTERNAL_GET_POINTER_EXTENDED(old_toast_pointer_ext, oldexternal);
+				old_toastrelid = old_toast_pointer_ext.va_toastrelid;
+				old_valueid = old_toast_pointer_ext.va_valueid;
+			}
+			else
+			{
+				struct varatt_external old_toast_pointer;
+
+				VARATT_EXTERNAL_GET_POINTER(old_toast_pointer, oldexternal);
+				old_toastrelid = old_toast_pointer.va_toastrelid;
+				old_valueid = old_toast_pointer.va_valueid;
+			}
+
+			if (old_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.va_valueid = old_valueid;
 
 				/*
 				 * There is a corner case here: the table rewrite might have
@@ -348,6 +455,10 @@ toast_save_datum(Relation rel, Datum value,
 		data_p += chunk_size;
 	}
 
+	/* Free compressed buffer if we allocated one */
+	if (compressed_to_free != NULL)
+		pfree(compressed_to_free);
+
 	/*
 	 * Done - close toast relation and its indexes but keep the lock until
 	 * commit, so as a concurrent reindex done directly on the toast relation
@@ -356,12 +467,35 @@ toast_save_datum(Relation rel, Datum value,
 	toast_close_indexes(toastidxs, num_indexes, NoLock);
 	table_close(toastrel, NoLock);
 
-	/*
-	 * 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));
+	/* Create the TOAST pointer value that we'll return */
+	if (use_extended)
+	{
+		/*
+		 * Build extended TOAST pointer.  Copy the common fields from
+		 * toast_pointer, then set the extended-format-specific fields.
+		 */
+		toast_pointer_ext.va_rawsize = toast_pointer.va_rawsize;
+		toast_pointer_ext.va_extinfo = toast_pointer.va_extinfo;
+		toast_pointer_ext.va_valueid = toast_pointer.va_valueid;
+		toast_pointer_ext.va_toastrelid = toast_pointer.va_toastrelid;
+
+		/* Set extended format fields */
+		toast_pointer_ext.va_flags = TOAST_EXT_FLAG_COMPRESSION;
+		toast_pointer_ext.va_data[0] = ext_method;
+		toast_pointer_ext.va_data[1] = 0;
+		toast_pointer_ext.va_data[2] = 0;
+
+		result = (struct varlena *) palloc(TOAST_POINTER_SIZE_EXTENDED);
+		SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_EXTENDED);
+		memcpy(VARDATA_EXTERNAL(result), &toast_pointer_ext, sizeof(toast_pointer_ext));
+	}
+	else
+	{
+		/* Standard 16-byte TOAST pointer */
+		result = (struct varlena *) palloc(TOAST_POINTER_SIZE);
+		SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK);
+		memcpy(VARDATA_EXTERNAL(result), &toast_pointer, sizeof(toast_pointer));
+	}
 
 	return PointerGetDatum(result);
 }
@@ -377,6 +511,7 @@ 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_extended toast_pointer_ext;
 	Relation	toastrel;
 	Relation   *toastidxs;
 	ScanKeyData toastkey;
@@ -384,17 +519,36 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	HeapTuple	toasttup;
 	int			num_indexes;
 	int			validIndex;
+	Oid			toastrelid;
+	Oid			valueid;
+	bool		is_extended;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		return;
 
-	/* Must copy to access aligned fields */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	/*
+	 * Must copy to access aligned fields.  Handle both legacy (16-byte) and
+	 * extended (20-byte) on-disk TOAST pointers based on the tag.
+	 */
+	is_extended = (VARTAG_EXTERNAL(attr) == VARTAG_ONDISK_EXTENDED);
+
+	if (!is_extended)
+	{
+		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		toastrelid = toast_pointer.va_toastrelid;
+		valueid = toast_pointer.va_valueid;
+	}
+	else
+	{
+		VARATT_EXTERNAL_GET_POINTER_EXTENDED(toast_pointer_ext, attr);
+		toastrelid = toast_pointer_ext.va_toastrelid;
+		valueid = toast_pointer_ext.va_valueid;
+	}
 
 	/*
 	 * Open the toast relation and its indexes
 	 */
-	toastrel = table_open(toast_pointer.va_toastrelid, RowExclusiveLock);
+	toastrel = table_open(toastrelid, RowExclusiveLock);
 
 	/* Fetch valid relation used for process */
 	validIndex = toast_open_indexes(toastrel,
@@ -408,7 +562,7 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	ScanKeyInit(&toastkey,
 				(AttrNumber) 1,
 				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(toast_pointer.va_valueid));
+				ObjectIdGetDatum(valueid));
 
 	/*
 	 * Find all the chunks.  (We don't actually care whether we see them in
diff --git a/src/backend/access/table/toast_helper.c b/src/backend/access/table/toast_helper.c
index 11f97d65367..21381004ba6 100644
--- a/src/backend/access/table/toast_helper.c
+++ b/src/backend/access/table/toast_helper.c
@@ -261,7 +261,7 @@ toast_tuple_externalize(ToastTupleContext *ttc, int attribute, int options)
 
 	attr->tai_colflags |= TOASTCOL_IGNORE;
 	*value = toast_save_datum(ttc->ttc_rel, old_value, attr->tai_oldexternal,
-							  options);
+							  options, attr->tai_compression);
 	if ((attr->tai_colflags & TOASTCOL_NEEDS_FREE) != 0)
 		pfree(DatumGetPointer(old_value));
 	attr->tai_colflags |= TOASTCOL_NEEDS_FREE;
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index f18c6fb52b5..9e83ab5978d 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -5137,11 +5137,17 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 
 		/* va_rawsize is the size of the original datum -- including header */
 		struct varatt_external toast_pointer;
+		struct varatt_external_extended toast_pointer_ext;
 		struct varatt_indirect redirect_pointer;
 		struct varlena *new_datum = NULL;
 		struct varlena *reconstructed;
 		dlist_iter	it;
 		Size		data_done = 0;
+		bool		is_extended;
+		Oid			valueid;
+		int32		rawsize;
+		int32		extsize;
+		bool		is_compressed;
 
 		if (attr->attisdropped)
 			continue;
@@ -5161,14 +5167,36 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		if (!VARATT_IS_EXTERNAL(varlena))
 			continue;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, varlena);
+		/*
+		 * Handle both legacy 16-byte and extended 20-byte on-disk TOAST
+		 * pointers based on the tag.
+		 */
+		is_extended = VARATT_IS_EXTERNAL_ONDISK(varlena) &&
+					  (VARTAG_EXTERNAL(varlena) == VARTAG_ONDISK_EXTENDED);
+
+		if (is_extended)
+		{
+			VARATT_EXTERNAL_GET_POINTER_EXTENDED(toast_pointer_ext, varlena);
+			valueid = toast_pointer_ext.va_valueid;
+			rawsize = toast_pointer_ext.va_rawsize;
+			extsize = VARATT_EXTERNAL_GET_EXTSIZE_EXTENDED(toast_pointer_ext);
+			is_compressed = VARATT_EXTERNAL_IS_COMPRESSED_EXTENDED(toast_pointer_ext);
+		}
+		else
+		{
+			VARATT_EXTERNAL_GET_POINTER(toast_pointer, varlena);
+			valueid = toast_pointer.va_valueid;
+			rawsize = toast_pointer.va_rawsize;
+			extsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+			is_compressed = VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer);
+		}
 
 		/*
 		 * Check whether the toast tuple changed, replace if so.
 		 */
 		ent = (ReorderBufferToastEnt *)
 			hash_search(txn->toast_hash,
-						&toast_pointer.va_valueid,
+						&valueid,
 						HASH_FIND,
 						NULL);
 		if (ent == NULL)
@@ -5179,7 +5207,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 
 		free[natt] = true;
 
-		reconstructed = palloc0(toast_pointer.va_rawsize);
+		reconstructed = palloc0(rawsize);
 
 		ent->reconstructed = reconstructed;
 
@@ -5204,10 +5232,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 == extsize);
 
 		/* make sure its marked as compressed or not */
-		if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+		if (is_compressed)
 			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 baa5b44ea8d..71a410dc617 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -4206,6 +4206,10 @@ pg_column_compression(PG_FUNCTION_ARGS)
 		case TOAST_LZ4_COMPRESSION_ID:
 			result = "lz4";
 			break;
+		case TOAST_EXTENDED_COMPRESSION_ID:
+			/* Extended format currently only supports zstd */
+			result = "zstd";
+			break;
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 	}
@@ -4222,7 +4226,7 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 {
 	int			typlen;
 	struct varlena *attr;
-	struct varatt_external toast_pointer;
+	Oid			valueid;
 
 	/* On first call, get the input type's typlen, and save at *fn_extra */
 	if (fcinfo->flinfo->fn_extra == NULL)
@@ -4249,9 +4253,25 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		PG_RETURN_NULL();
 
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	/*
+	 * Handle both legacy 16-byte and extended 20-byte on-disk TOAST pointers.
+	 */
+	if (VARTAG_EXTERNAL(attr) == VARTAG_ONDISK_EXTENDED)
+	{
+		struct varatt_external_extended toast_pointer_ext;
+
+		VARATT_EXTERNAL_GET_POINTER_EXTENDED(toast_pointer_ext, attr);
+		valueid = toast_pointer_ext.va_valueid;
+	}
+	else
+	{
+		struct varatt_external toast_pointer;
+
+		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		valueid = toast_pointer.va_valueid;
+	}
 
-	PG_RETURN_OID(toast_pointer.va_valueid);
+	PG_RETURN_OID(valueid);
 }
 
 /*
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 3b9d8349078..38c68d1d0a6 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -738,7 +738,6 @@
   boot_val => 'TOAST_PGLZ_COMPRESSION',
   options => 'default_toast_compression_options',
 },
-
 { name => 'default_transaction_deferrable', type => 'bool', context => 'PGC_USERSET', group => 'CLIENT_CONN_STATEMENT',
   short_desc => 'Sets the default deferrable status of new transactions.',
   variable => 'DefaultXactDeferrable',
@@ -3175,6 +3174,12 @@
   boot_val => 'DEFAULT_UPDATE_PROCESS_TITLE',
 },
 
+{ name => 'use_extended_toast_header', type => 'bool', context => 'PGC_USERSET', group => 'CLIENT_CONN_STATEMENT',
+  short_desc => 'Use 20-byte extended TOAST header format (required for zstd).',
+  variable => 'use_extended_toast_header',
+  boot_val => 'false',
+},
+
 { name => 'vacuum_buffer_usage_limit', type => 'int', context => 'PGC_USERSET', group => 'RESOURCES_MEM',
   short_desc => 'Sets the buffer pool size for VACUUM, ANALYZE, and autovacuum.',
   flags => 'GUC_UNIT_KB',
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index f87b558c2c6..f6c09260f1a 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -460,6 +460,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/include/access/detoast.h b/src/include/access/detoast.h
index e603a2276c3..e591a59569b 100644
--- a/src/include/access/detoast.h
+++ b/src/include/access/detoast.h
@@ -14,25 +14,58 @@
 
 /*
  * 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.
+ *
+ * This currently supports only the legacy on-disk TOAST pointer format,
+ * which has VARTAG_ONDISK and a payload size of sizeof(varatt_external).
+ * Extended on-disk pointers (VARTAG_ONDISK_EXTENDED) must be accessed via
+ * VARATT_EXTERNAL_GET_POINTER_EXTENDED().
+ *
+ * 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 { \
 	varattrib_1b_e *attre = (varattrib_1b_e *) (attr); \
 	Assert(VARATT_IS_EXTERNAL(attre)); \
+	Assert(VARTAG_EXTERNAL(attre) == VARTAG_ONDISK); \
 	Assert(VARSIZE_EXTERNAL(attre) == sizeof(toast_pointer) + VARHDRSZ_EXTERNAL); \
 	memcpy(&(toast_pointer), VARDATA_EXTERNAL(attre), sizeof(toast_pointer)); \
 } while (0)
 
+/*
+ * Variant of VARATT_EXTERNAL_GET_POINTER for the extended on-disk TOAST
+ * pointer format.  Callers should only use this when they have already
+ * established that the tag is VARTAG_ONDISK_EXTENDED.
+ */
+#define VARATT_EXTERNAL_GET_POINTER_EXTENDED(toast_pointer_ext, attr) \
+do { \
+	varattrib_1b_e *attre = (varattrib_1b_e *) (attr); \
+	Assert(VARATT_IS_EXTERNAL(attre)); \
+	Assert(VARTAG_EXTERNAL(attre) == VARTAG_ONDISK_EXTENDED); \
+	Assert(VARSIZE_EXTERNAL(attre) == sizeof(toast_pointer_ext) + VARHDRSZ_EXTERNAL); \
+	memcpy(&(toast_pointer_ext), VARDATA_EXTERNAL(attre), sizeof(toast_pointer_ext)); \
+} 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))
 
+/* Size of an EXTERNAL datum that contains an extended TOAST pointer */
+#define TOAST_POINTER_SIZE_EXTENDED (VARHDRSZ_EXTERNAL + sizeof(varatt_external_extended))
+
+/* Validation helpers for TOAST pointer sizes */
+#define TOAST_POINTER_SIZE_IS_VALID(size) \
+	((size) == TOAST_POINTER_SIZE || \
+	 (size) == TOAST_POINTER_SIZE_EXTENDED || \
+	 (size) == INDIRECT_POINTER_SIZE)
+
+#define TOAST_POINTER_IS_EXTENDED_SIZE(size) \
+	((size) == TOAST_POINTER_SIZE_EXTENDED)
+
 /* ----------
  * detoast_external_attr() -
  *
diff --git a/src/include/access/toast_compression.h b/src/include/access/toast_compression.h
index 13c4612ceed..b769d1bc72d 100644
--- a/src/include/access/toast_compression.h
+++ b/src/include/access/toast_compression.h
@@ -13,14 +13,21 @@
 #ifndef TOAST_COMPRESSION_H
 #define TOAST_COMPRESSION_H
 
+#include "varatt.h"
+
 /*
  * GUC support.
  *
  * default_toast_compression is an integer for purposes of the GUC machinery,
  * but the value is one of the char values defined below, as they appear in
  * pg_attribute.attcompression, e.g. TOAST_PGLZ_COMPRESSION.
+ *
+ * use_extended_toast_header controls whether to use the 20-byte extended
+ * TOAST pointer format (required for zstd) instead of the legacy 16-byte
+ * format. When false, zstd compression falls back to pglz.
  */
 extern PGDLLIMPORT int default_toast_compression;
+extern PGDLLIMPORT bool use_extended_toast_header;
 
 /*
  * Built-in compression method ID.  The toast compression header will store
@@ -39,6 +46,7 @@ typedef enum ToastCompressionId
 	TOAST_PGLZ_COMPRESSION_ID = 0,
 	TOAST_LZ4_COMPRESSION_ID = 1,
 	TOAST_INVALID_COMPRESSION_ID = 2,
+	TOAST_EXTENDED_COMPRESSION_ID = 3,	/* extended format for future methods */
 } ToastCompressionId;
 
 /*
@@ -48,6 +56,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)
@@ -65,9 +74,36 @@ extern struct varlena *lz4_decompress_datum(const struct varlena *value);
 extern struct varlena *lz4_decompress_datum_slice(const struct varlena *value,
 												  int32 slicelength);
 
+/* zstd compression/decompression routines (extended methods) */
+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);
 extern const char *GetCompressionMethodName(char method);
 
+/*
+ * Feature flags for extended TOAST pointers (varatt_external_extended).
+ * These alias VARATT_EXTERNAL_FLAG_* from varatt.h.
+ */
+#define TOAST_EXT_FLAG_COMPRESSION      VARATT_EXTERNAL_FLAG_COMPRESSION
+#define TOAST_EXT_FLAG_CHECKSUM         VARATT_EXTERNAL_FLAG_CHECKSUM
+
+/*
+ * Extended compression method IDs for use with extended TOAST format.
+ * Stored in va_data[0] when TOAST_EXT_FLAG_COMPRESSION is set.
+ */
+#define TOAST_PGLZ_EXT_METHOD          0
+#define TOAST_LZ4_EXT_METHOD           1
+#define TOAST_ZSTD_EXT_METHOD          2
+#define TOAST_UNCOMPRESSED_EXT_METHOD  3
+
+/* Validation macros for extended format */
+#define ExtendedCompressionMethodIsValid(method) ((method) <= 255)
+#define ExtendedFlagsAreValid(flags) \
+	(((flags) & ~(TOAST_EXT_FLAG_COMPRESSION | TOAST_EXT_FLAG_CHECKSUM)) == 0)
+
 #endif							/* TOAST_COMPRESSION_H */
diff --git a/src/include/access/toast_internals.h b/src/include/access/toast_internals.h
index 06ae8583c1e..d6bc5c4d179 100644
--- a/src/include/access/toast_internals.h
+++ b/src/include/access/toast_internals.h
@@ -36,11 +36,16 @@ typedef struct toast_compress_header
 #define TOAST_COMPRESS_METHOD(ptr) \
 	(((toast_compress_header *) (ptr))->tcinfo >> VARLENA_EXTSIZE_BITS)
 
+/*
+ * Set the size and compression method in a compressed datum's header.
+ * Accepts TOAST_EXTENDED_COMPRESSION_ID for extended compression methods.
+ */
 #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_LZ4_COMPRESSION_ID || \
+			   (cm_method) == TOAST_EXTENDED_COMPRESSION_ID); \
 		((toast_compress_header *) (ptr))->tcinfo = \
 			(len) | ((uint32) (cm_method) << VARLENA_EXTSIZE_BITS); \
 	} while (0)
@@ -50,7 +55,8 @@ extern Oid	toast_get_valid_index(Oid toastoid, LOCKMODE lock);
 
 extern void toast_delete_datum(Relation rel, Datum value, bool is_speculative);
 extern Datum toast_save_datum(Relation rel, Datum value,
-							  struct varlena *oldexternal, int options);
+							  struct varlena *oldexternal, int options,
+							  char cmethod);
 
 extern int	toast_open_indexes(Relation toastrel,
 							   LOCKMODE lock,
diff --git a/src/include/varatt.h b/src/include/varatt.h
index aeeabf9145b..5f5829a1ec4 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -45,6 +45,23 @@ typedef struct varatt_external
 #define VARLENA_EXTSIZE_BITS	30
 #define VARLENA_EXTSIZE_MASK	((1U << VARLENA_EXTSIZE_BITS) - 1)
 
+/*
+ * Compression method ID stored in the 2 high-order bits of va_extinfo.
+ * Value 3 indicates an extended TOAST pointer format (varatt_external_extended).
+ * This constant is also defined in toast_compression.h for use by TOAST code.
+ */
+#define VARATT_EXTERNAL_EXTENDED_CMID	3
+
+/*
+ * Feature flags for extended on-disk TOAST pointers (varatt_external_extended).
+ *
+ * Keep these in varatt.h (not access/toast headers) so low-level code can
+ * safely manipulate the on-disk representation without depending on higher
+ * layers' header include order.
+ */
+#define VARATT_EXTERNAL_FLAG_COMPRESSION	0x01	/* va_data[0] = method ID */
+#define VARATT_EXTERNAL_FLAG_CHECKSUM		0x02	/* va_data[1-2] = checksum */
+
 /*
  * struct varatt_indirect is a "TOAST pointer" representing an out-of-line
  * Datum that's stored in memory, not in an external toast relation.
@@ -76,6 +93,26 @@ typedef struct varatt_expanded
 	ExpandedObjectHeader *eohptr;
 } varatt_expanded;
 
+/*
+ * Extended TOAST pointer, extending varatt_external from 16 to 20 bytes.
+ *
+ * Identified by compression method ID 3 in va_extinfo bits 30-31.  The
+ * va_flags field indicates which optional features are enabled; va_data[]
+ * contains feature-specific data (e.g., compression method in va_data[0]).
+ *
+ * Like varatt_external, stored unaligned and requires memcpy for access.
+ */
+typedef struct varatt_external_extended
+{
+	int32		va_rawsize;		/* Original data size (includes header) */
+	uint32		va_extinfo;		/* External saved size (30 bits) + extended
+								 * indicator (2 bits, value = 3) */
+	uint8		va_flags;		/* Feature flags indicating enabled extensions */
+	uint8		va_data[3];		/* Extension data - interpretation depends on flags */
+	Oid			va_valueid;		/* Unique ID of value within TOAST table */
+	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
+}			varatt_external_extended;
+
 /*
  * Type tag for the various sorts of "TOAST pointer" datums.  The peculiar
  * value for VARTAG_ONDISK comes from a requirement for on-disk compatibility
@@ -86,7 +123,17 @@ typedef enum vartag_external
 	VARTAG_INDIRECT = 1,
 	VARTAG_EXPANDED_RO = 2,
 	VARTAG_EXPANDED_RW = 3,
-	VARTAG_ONDISK = 18
+	VARTAG_ONDISK = 18,
+
+	/*
+	 * VARTAG_ONDISK_EXTENDED is used for the extended TOAST pointer format,
+	 * which increases the on-disk payload from 16 to 20 bytes.  The first
+	 * 8 bytes (va_rawsize, va_extinfo) are layout-compatible with
+	 * struct varatt_external so that existing code inspecting those fields
+	 * continues to work.  Older PostgreSQL versions do not know about this
+	 * tag and therefore must not be used to read clusters that contain it.
+	 */
+	VARTAG_ONDISK_EXTENDED = 19
 } vartag_external;
 
 /* Is a TOAST pointer either type of expanded-object pointer? */
@@ -97,7 +144,14 @@ VARTAG_IS_EXPANDED(vartag_external tag)
 	return ((tag & ~1) == VARTAG_EXPANDED_RO);
 }
 
-/* Size of the data part of a "TOAST pointer" datum */
+/*
+ * Size of the data part of a "TOAST pointer" datum.
+ *
+ * For on-disk TOAST pointers we now support two payload sizes:
+ * the original 16-byte format (VARTAG_ONDISK) described by struct
+ * varatt_external, and a 20-byte extended format
+ * (VARTAG_ONDISK_EXTENDED) described by struct varatt_external_extended.
+ */
 static inline Size
 VARTAG_SIZE(vartag_external tag)
 {
@@ -107,6 +161,8 @@ VARTAG_SIZE(vartag_external tag)
 		return sizeof(varatt_expanded);
 	else if (tag == VARTAG_ONDISK)
 		return sizeof(varatt_external);
+	else if (tag == VARTAG_ONDISK_EXTENDED)
+		return sizeof(varatt_external_extended);
 	else
 	{
 		Assert(false);
@@ -360,7 +416,13 @@ 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;
+	vartag_external tag;
+
+	if (!VARATT_IS_EXTERNAL(PTR))
+		return false;
+
+	tag = VARTAG_EXTERNAL(PTR);
+	return tag == VARTAG_ONDISK || tag == VARTAG_ONDISK_EXTENDED;
 }
 
 /* Is varlena datum an indirect pointer? */
@@ -516,11 +578,11 @@ VARATT_EXTERNAL_GET_COMPRESS_METHOD(struct varatt_external toast_pointer)
 }
 
 /* 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) \
 	do { \
 		Assert((cm) == TOAST_PGLZ_COMPRESSION_ID || \
-			   (cm) == TOAST_LZ4_COMPRESSION_ID); \
+			   (cm) == TOAST_LZ4_COMPRESSION_ID || \
+			   (cm) == VARATT_EXTERNAL_EXTENDED_CMID); \
 		((toast_pointer).va_extinfo = \
 			(len) | ((uint32) (cm) << VARLENA_EXTSIZE_BITS)); \
 	} while (0)
@@ -539,4 +601,92 @@ VARATT_EXTERNAL_IS_COMPRESSED(struct varatt_external toast_pointer)
 		(Size) (toast_pointer.va_rawsize - VARHDRSZ);
 }
 
+/* Macros for extended TOAST pointers (varatt_external_extended) */
+
+/*
+ * Check if a TOAST pointer uses the extended on-disk format.
+ *
+ * Callers must have already verified VARATT_IS_EXTERNAL_ONDISK() before
+ * calling this; here we look only at the compression-method bits embedded
+ * in va_extinfo.
+ */
+static inline bool
+VARATT_EXTERNAL_IS_EXTENDED(struct varatt_external toast_pointer)
+{
+	return VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) ==
+		VARATT_EXTERNAL_EXTENDED_CMID;
+}
+
+/* Get feature flags from extended pointer */
+static inline uint8
+VARATT_EXTERNAL_GET_FLAGS(struct varatt_external_extended toast_pointer_ext)
+{
+	return toast_pointer_ext.va_flags;
+}
+
+/* Set feature flags in extended pointer */
+#define VARATT_EXTERNAL_SET_FLAGS(toast_pointer_ext, flags) \
+	do { \
+		(toast_pointer_ext).va_flags = (flags); \
+	} while (0)
+
+/* Test if a specific flag is set */
+#define VARATT_EXTERNAL_HAS_FLAG(toast_pointer_ext, flag) \
+	(((toast_pointer_ext).va_flags & (flag)) != 0)
+
+/* Get pointer to extension data array */
+#define VARATT_EXTERNAL_GET_EXT_DATA(toast_pointer_ext) \
+	((toast_pointer_ext).va_data)
+
+/* Get extended compression method (when TOAST_EXT_FLAG_COMPRESSION is set) */
+static inline uint8
+VARATT_EXTERNAL_GET_EXT_COMPRESSION_METHOD(struct varatt_external_extended toast_pointer_ext)
+{
+	return toast_pointer_ext.va_data[0];
+}
+
+/* Set extended compression method */
+#define VARATT_EXTERNAL_SET_EXT_COMPRESSION_METHOD(toast_pointer_ext, method) \
+	do { \
+		(toast_pointer_ext).va_data[0] = (method); \
+	} while (0)
+
+/* Get extsize and compress method from extended pointer (same as standard) */
+static inline Size
+VARATT_EXTERNAL_GET_EXTSIZE_EXTENDED(struct varatt_external_extended toast_pointer_ext)
+{
+	return toast_pointer_ext.va_extinfo & VARLENA_EXTSIZE_MASK;
+}
+
+static inline uint32
+VARATT_EXTERNAL_GET_COMPRESS_METHOD_EXTENDED(struct varatt_external_extended toast_pointer_ext)
+{
+	return toast_pointer_ext.va_extinfo >> VARLENA_EXTSIZE_BITS;
+}
+
+/* Set size and extended indicator in va_extinfo */
+#define VARATT_EXTERNAL_SET_SIZE_AND_EXT_FLAGS(toast_pointer_ext, len, flags) \
+	do { \
+		Assert((len) > 0 && (len) <= VARLENA_EXTSIZE_MASK); \
+		(toast_pointer_ext).va_extinfo = \
+			(len) | ((uint32) VARATT_EXTERNAL_EXTENDED_CMID << VARLENA_EXTSIZE_BITS); \
+		(toast_pointer_ext).va_flags = (flags); \
+		memset((toast_pointer_ext).va_data, 0, 3); \
+	} while (0)
+
+/* Convenience macro for setting extended pointer with compression method */
+#define VARATT_EXTERNAL_SET_SIZE_AND_EXT_COMPRESSION(toast_pointer_ext, len, method) \
+	do { \
+		VARATT_EXTERNAL_SET_SIZE_AND_EXT_FLAGS(toast_pointer_ext, len, VARATT_EXTERNAL_FLAG_COMPRESSION); \
+		VARATT_EXTERNAL_SET_EXT_COMPRESSION_METHOD(toast_pointer_ext, method); \
+	} while (0)
+
+/* Test if extended pointer is compressed (same logic as standard) */
+static inline bool
+VARATT_EXTERNAL_IS_COMPRESSED_EXTENDED(struct varatt_external_extended toast_pointer_ext)
+{
+	return VARATT_EXTERNAL_GET_EXTSIZE_EXTENDED(toast_pointer_ext) <
+		(Size) (toast_pointer_ext.va_rawsize - VARHDRSZ);
+}
+
 #endif
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 068fd859a8f..9dff119aa22 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -47,6 +47,7 @@ subdir('test_rls_hooks')
 subdir('test_shm_mq')
 subdir('test_slru')
 subdir('test_tidstore')
+subdir('test_toast_ext')
 subdir('typcache')
 subdir('unsafe_tests')
 subdir('worker_spi')
diff --git a/src/test/modules/test_toast_ext/Makefile b/src/test/modules/test_toast_ext/Makefile
new file mode 100644
index 00000000000..5e2409f918c
--- /dev/null
+++ b/src/test/modules/test_toast_ext/Makefile
@@ -0,0 +1,20 @@
+# src/test/modules/test_toast_ext/Makefile
+
+MODULE_big = test_toast_ext
+OBJS = test_toast_ext.o
+
+EXTENSION = test_toast_ext
+DATA = test_toast_ext--1.0.sql
+
+REGRESS = test_toast_ext
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_toast_ext
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_toast_ext/expected/test_toast_ext.out b/src/test/modules/test_toast_ext/expected/test_toast_ext.out
new file mode 100644
index 00000000000..539f4437655
--- /dev/null
+++ b/src/test/modules/test_toast_ext/expected/test_toast_ext.out
@@ -0,0 +1,187 @@
+--
+-- Tests for extended TOAST header structures and zstd compression
+--
+CREATE EXTENSION test_toast_ext;
+-- Use dedicated schema for test isolation
+CREATE SCHEMA test_toast_ext_schema;
+SET search_path TO test_toast_ext_schema, public;
+-- Compile-time validation tests (always run)
+-- These error out on failure, so completing without error = pass
+SELECT test_toast_structure_sizes();
+ test_toast_structure_sizes
+----------------------------
+
+(1 row)
+
+SELECT test_toast_flag_validation();
+ test_toast_flag_validation
+----------------------------
+
+(1 row)
+
+SELECT test_toast_compression_ids();
+ test_toast_compression_ids
+----------------------------
+
+(1 row)
+
+--
+-- Functional tests for zstd TOAST compression
+-- Skip if not built with USE_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
+-- Test basic zstd compression
+CREATE TABLE test_zstd_basic (id serial, data text COMPRESSION zstd);
+INSERT INTO test_zstd_basic (data)
+    VALUES (repeat('PostgreSQL zstd TOAST compression test. ', 3000));
+SELECT id,
+       pg_column_compression(data) AS compression,
+       length(data) AS data_length,
+       left(data, 42) AS data_prefix
+FROM test_zstd_basic;
+ id | compression | data_length |                data_prefix
+----+-------------+-------------+--------------------------------------------
+  1 | zstd        |      120000 | PostgreSQL zstd TOAST compression test. Po
+(1 row)
+
+-- Test slice access
+SELECT id, substr(data, 100, 42) AS slice FROM test_zstd_basic;
+ id |                   slice
+----+--------------------------------------------
+  1 | ST compression test. PostgreSQL zstd TOAST
+(1 row)
+
+-- Test UPDATE
+UPDATE test_zstd_basic SET data = repeat('Updated zstd data for TOAST test. ', 3000);
+SELECT id,
+       pg_column_compression(data) AS compression,
+       length(data) AS data_length,
+       left(data, 35) AS data_prefix
+FROM test_zstd_basic;
+ id | compression | data_length |             data_prefix
+----+-------------+-------------+-------------------------------------
+  1 | zstd        |      102000 | Updated zstd data for TOAST test. U
+(1 row)
+
+-- Test extended header with pglz
+SET use_extended_toast_header = on;
+CREATE TABLE test_pglz_extended (data text COMPRESSION pglz);
+INSERT INTO test_pglz_extended (data)
+    VALUES (repeat('PGLZ with extended header format. ', 3000));
+SELECT pg_column_compression(data) AS compression,
+       length(data) AS data_length
+FROM test_pglz_extended;
+ compression | data_length
+-------------+-------------
+ pglz        |      102000
+(1 row)
+
+SELECT substr(data, 50, 34) AS slice FROM test_pglz_extended;
+               slice
+------------------------------------
+ ded header format. PGLZ with exten
+(1 row)
+
+-- Test data integrity
+CREATE TABLE test_integrity (
+    method text,
+    original_data text,
+    compressed_data text
+);
+INSERT INTO test_integrity VALUES
+    ('pglz', repeat('Integrity test data pattern. ', 2000), NULL),
+    ('zstd', repeat('Integrity test data pattern. ', 2000), NULL);
+CREATE TABLE test_pglz_integrity (data text COMPRESSION pglz);
+CREATE TABLE test_zstd_integrity (data text COMPRESSION zstd);
+INSERT INTO test_pglz_integrity SELECT original_data FROM test_integrity WHERE method = 'pglz';
+INSERT INTO test_zstd_integrity SELECT original_data FROM test_integrity WHERE method = 'zstd';
+SELECT 'pglz' AS method,
+       md5((SELECT original_data FROM test_integrity WHERE method = 'pglz')) =
+       md5((SELECT data FROM test_pglz_integrity)) AS checksum_match;
+ method | checksum_match
+--------+----------------
+ pglz   | t
+(1 row)
+
+SELECT 'zstd' AS method,
+       md5((SELECT original_data FROM test_integrity WHERE method = 'zstd')) =
+       md5((SELECT data FROM test_zstd_integrity)) AS checksum_match;
+ method | checksum_match
+--------+----------------
+ zstd   | t
+(1 row)
+
+-- Test CLUSTER and VACUUM FULL
+CREATE TABLE test_cluster_zstd (id serial PRIMARY KEY, data text COMPRESSION zstd);
+INSERT INTO test_cluster_zstd (data)
+    VALUES (repeat('Data for CLUSTER test with zstd compression. ', 2500));
+SELECT 'before_cluster' AS stage, md5(data) AS hash FROM test_cluster_zstd;
+     stage      |               hash
+----------------+----------------------------------
+ before_cluster | b4132e799bbd065a7e9266159aa82dc1
+(1 row)
+
+CLUSTER test_cluster_zstd USING test_cluster_zstd_pkey;
+SELECT 'after_cluster' AS stage,
+       pg_column_compression(data) AS compression,
+       md5(data) AS hash
+FROM test_cluster_zstd;
+     stage     | compression |               hash
+---------------+-------------+----------------------------------
+ after_cluster | zstd        | b4132e799bbd065a7e9266159aa82dc1
+(1 row)
+
+VACUUM FULL test_cluster_zstd;
+SELECT 'after_vacuum_full' AS stage,
+       pg_column_compression(data) AS compression,
+       md5(data) AS hash
+FROM test_cluster_zstd;
+       stage       | compression |               hash
+-------------------+-------------+----------------------------------
+ after_vacuum_full | zstd        | b4132e799bbd065a7e9266159aa82dc1
+(1 row)
+
+-- Test GUC toggling (mixed formats in same table)
+SET use_extended_toast_header = on;
+CREATE TABLE test_guc_toggle (id serial, data text COMPRESSION pglz);
+INSERT INTO test_guc_toggle (data)
+    VALUES (repeat('Data created with extended header on. ', 3000));
+SELECT 'with_ext_on' AS stage,
+       pg_column_compression(data) AS compression,
+       length(data) AS data_length
+FROM test_guc_toggle;
+    stage    | compression | data_length
+-------------+-------------+-------------
+ with_ext_on | pglz        |      114000
+(1 row)
+
+SET use_extended_toast_header = off;
+INSERT INTO test_guc_toggle (data)
+    VALUES (repeat('Data created with extended header off. ', 3000));
+SELECT id,
+       pg_column_compression(data) AS compression,
+       length(data) AS data_length,
+       left(data, 39) AS data_prefix
+FROM test_guc_toggle ORDER BY id;
+ id | compression | data_length |               data_prefix
+----+-------------+-------------+-----------------------------------------
+  1 | pglz        |      114000 | Data created with extended header on. D
+  2 | pglz        |      117000 | Data created with extended header off.
+(2 rows)
+
+SET use_extended_toast_header = on;
+SELECT id, length(data) AS data_length FROM test_guc_toggle ORDER BY id;
+ id | data_length
+----+-------------
+  1 |      114000
+  2 |      117000
+(2 rows)
+
+-- Cleanup
+DROP SCHEMA test_toast_ext_schema CASCADE;
+DROP EXTENSION test_toast_ext;
diff --git a/src/test/modules/test_toast_ext/expected/test_toast_ext_1.out b/src/test/modules/test_toast_ext/expected/test_toast_ext_1.out
new file mode 100644
index 00000000000..897661fc2a4
--- /dev/null
+++ b/src/test/modules/test_toast_ext/expected/test_toast_ext_1.out
@@ -0,0 +1,37 @@
+--
+-- Tests for extended TOAST header structures and zstd compression
+--
+CREATE EXTENSION test_toast_ext;
+-- Use dedicated schema for test isolation
+CREATE SCHEMA test_toast_ext_schema;
+SET search_path TO test_toast_ext_schema, public;
+-- Compile-time validation tests (always run)
+-- These error out on failure, so completing without error = pass
+SELECT test_toast_structure_sizes();
+ test_toast_structure_sizes
+----------------------------
+
+(1 row)
+
+SELECT test_toast_flag_validation();
+ test_toast_flag_validation
+----------------------------
+
+(1 row)
+
+SELECT test_toast_compression_ids();
+ test_toast_compression_ids
+----------------------------
+
+(1 row)
+
+--
+-- Functional tests for zstd TOAST compression
+-- Skip if not built with USE_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/modules/test_toast_ext/meson.build b/src/test/modules/test_toast_ext/meson.build
new file mode 100644
index 00000000000..61c07ea1912
--- /dev/null
+++ b/src/test/modules/test_toast_ext/meson.build
@@ -0,0 +1,33 @@
+# Copyright (c) 2022-2025, PostgreSQL Global Development Group
+
+test_toast_ext_sources = files(
+  'test_toast_ext.c',
+)
+
+if host_system == 'windows'
+  test_toast_ext_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_toast_ext',
+    '--FILEDESC', 'test_toast_ext - test code for extended TOAST headers',])
+endif
+
+test_toast_ext = shared_module('test_toast_ext',
+  test_toast_ext_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_toast_ext
+
+test_install_data += files(
+  'test_toast_ext.control',
+  'test_toast_ext--1.0.sql',
+)
+
+tests += {
+  'name': 'test_toast_ext',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'regress': {
+    'sql': [
+      'test_toast_ext',
+    ],
+  },
+}
diff --git a/src/test/modules/test_toast_ext/sql/test_toast_ext.sql b/src/test/modules/test_toast_ext/sql/test_toast_ext.sql
new file mode 100644
index 00000000000..82e36c57b34
--- /dev/null
+++ b/src/test/modules/test_toast_ext/sql/test_toast_ext.sql
@@ -0,0 +1,136 @@
+--
+-- Tests for extended TOAST header structures and zstd compression
+--
+
+CREATE EXTENSION test_toast_ext;
+
+-- Use dedicated schema for test isolation
+CREATE SCHEMA test_toast_ext_schema;
+SET search_path TO test_toast_ext_schema, public;
+
+-- Compile-time validation tests (always run)
+-- These error out on failure, so completing without error = pass
+SELECT test_toast_structure_sizes();
+SELECT test_toast_flag_validation();
+SELECT test_toast_compression_ids();
+
+--
+-- Functional tests for zstd TOAST compression
+-- Skip if not built with USE_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
+
+-- Test basic zstd compression
+CREATE TABLE test_zstd_basic (id serial, data text COMPRESSION zstd);
+INSERT INTO test_zstd_basic (data)
+    VALUES (repeat('PostgreSQL zstd TOAST compression test. ', 3000));
+
+SELECT id,
+       pg_column_compression(data) AS compression,
+       length(data) AS data_length,
+       left(data, 42) AS data_prefix
+FROM test_zstd_basic;
+
+-- Test slice access
+SELECT id, substr(data, 100, 42) AS slice FROM test_zstd_basic;
+
+-- Test UPDATE
+UPDATE test_zstd_basic SET data = repeat('Updated zstd data for TOAST test. ', 3000);
+SELECT id,
+       pg_column_compression(data) AS compression,
+       length(data) AS data_length,
+       left(data, 35) AS data_prefix
+FROM test_zstd_basic;
+
+-- Test extended header with pglz
+SET use_extended_toast_header = on;
+
+CREATE TABLE test_pglz_extended (data text COMPRESSION pglz);
+INSERT INTO test_pglz_extended (data)
+    VALUES (repeat('PGLZ with extended header format. ', 3000));
+
+SELECT pg_column_compression(data) AS compression,
+       length(data) AS data_length
+FROM test_pglz_extended;
+
+SELECT substr(data, 50, 34) AS slice FROM test_pglz_extended;
+
+-- Test data integrity
+CREATE TABLE test_integrity (
+    method text,
+    original_data text,
+    compressed_data text
+);
+
+INSERT INTO test_integrity VALUES
+    ('pglz', repeat('Integrity test data pattern. ', 2000), NULL),
+    ('zstd', repeat('Integrity test data pattern. ', 2000), NULL);
+
+CREATE TABLE test_pglz_integrity (data text COMPRESSION pglz);
+CREATE TABLE test_zstd_integrity (data text COMPRESSION zstd);
+
+INSERT INTO test_pglz_integrity SELECT original_data FROM test_integrity WHERE method = 'pglz';
+INSERT INTO test_zstd_integrity SELECT original_data FROM test_integrity WHERE method = 'zstd';
+
+SELECT 'pglz' AS method,
+       md5((SELECT original_data FROM test_integrity WHERE method = 'pglz')) =
+       md5((SELECT data FROM test_pglz_integrity)) AS checksum_match;
+
+SELECT 'zstd' AS method,
+       md5((SELECT original_data FROM test_integrity WHERE method = 'zstd')) =
+       md5((SELECT data FROM test_zstd_integrity)) AS checksum_match;
+
+-- Test CLUSTER and VACUUM FULL
+CREATE TABLE test_cluster_zstd (id serial PRIMARY KEY, data text COMPRESSION zstd);
+INSERT INTO test_cluster_zstd (data)
+    VALUES (repeat('Data for CLUSTER test with zstd compression. ', 2500));
+
+SELECT 'before_cluster' AS stage, md5(data) AS hash FROM test_cluster_zstd;
+
+CLUSTER test_cluster_zstd USING test_cluster_zstd_pkey;
+
+SELECT 'after_cluster' AS stage,
+       pg_column_compression(data) AS compression,
+       md5(data) AS hash
+FROM test_cluster_zstd;
+
+VACUUM FULL test_cluster_zstd;
+
+SELECT 'after_vacuum_full' AS stage,
+       pg_column_compression(data) AS compression,
+       md5(data) AS hash
+FROM test_cluster_zstd;
+
+-- Test GUC toggling (mixed formats in same table)
+SET use_extended_toast_header = on;
+CREATE TABLE test_guc_toggle (id serial, data text COMPRESSION pglz);
+INSERT INTO test_guc_toggle (data)
+    VALUES (repeat('Data created with extended header on. ', 3000));
+
+SELECT 'with_ext_on' AS stage,
+       pg_column_compression(data) AS compression,
+       length(data) AS data_length
+FROM test_guc_toggle;
+
+SET use_extended_toast_header = off;
+INSERT INTO test_guc_toggle (data)
+    VALUES (repeat('Data created with extended header off. ', 3000));
+
+SELECT id,
+       pg_column_compression(data) AS compression,
+       length(data) AS data_length,
+       left(data, 39) AS data_prefix
+FROM test_guc_toggle ORDER BY id;
+
+SET use_extended_toast_header = on;
+SELECT id, length(data) AS data_length FROM test_guc_toggle ORDER BY id;
+
+-- Cleanup
+DROP SCHEMA test_toast_ext_schema CASCADE;
+DROP EXTENSION test_toast_ext;
diff --git a/src/test/modules/test_toast_ext/test_toast_ext--1.0.sql b/src/test/modules/test_toast_ext/test_toast_ext--1.0.sql
new file mode 100644
index 00000000000..f74d5069fbf
--- /dev/null
+++ b/src/test/modules/test_toast_ext/test_toast_ext--1.0.sql
@@ -0,0 +1,19 @@
+/* src/test/modules/test_toast_ext/test_toast_ext--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_toast_ext" to load this file. \quit
+
+CREATE FUNCTION test_toast_structure_sizes()
+RETURNS void
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_toast_flag_validation()
+RETURNS void
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_toast_compression_ids()
+RETURNS void
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT;
diff --git a/src/test/modules/test_toast_ext/test_toast_ext.c b/src/test/modules/test_toast_ext/test_toast_ext.c
new file mode 100644
index 00000000000..59884f2b6d0
--- /dev/null
+++ b/src/test/modules/test_toast_ext/test_toast_ext.c
@@ -0,0 +1,140 @@
+/*-------------------------------------------------------------------------
+ *
+ * test_toast_ext.c
+ *		Test module for extended TOAST header structures.
+ *
+ * Copyright (c) 2025, PostgreSQL Global Development Group
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "access/detoast.h"
+#include "access/toast_compression.h"
+#include "varatt.h"
+
+PG_MODULE_MAGIC;
+
+PG_FUNCTION_INFO_V1(test_toast_structure_sizes);
+PG_FUNCTION_INFO_V1(test_toast_flag_validation);
+PG_FUNCTION_INFO_V1(test_toast_compression_ids);
+
+/*
+ * Verify TOAST structure sizes match expected values.
+ * Errors out if any size is wrong (catches ABI issues).
+ */
+Datum
+test_toast_structure_sizes(PG_FUNCTION_ARGS)
+{
+	/* Standard structure must be 16 bytes */
+	if (sizeof(varatt_external) != 16)
+		elog(ERROR, "varatt_external is %zu bytes, expected 16",
+			 sizeof(varatt_external));
+
+	/* Extended structure must be 20 bytes */
+	if (sizeof(varatt_external_extended) != 20)
+		elog(ERROR, "varatt_external_extended is %zu bytes, expected 20",
+			 sizeof(varatt_external_extended));
+
+	/* TOAST pointer sizes (include 2-byte external header) */
+	if (TOAST_POINTER_SIZE != 18)
+		elog(ERROR, "TOAST_POINTER_SIZE is %zu, expected 18",
+			 (Size) TOAST_POINTER_SIZE);
+
+	if (TOAST_POINTER_SIZE_EXTENDED != 22)
+		elog(ERROR, "TOAST_POINTER_SIZE_EXTENDED is %zu, expected 22",
+			 (Size) TOAST_POINTER_SIZE_EXTENDED);
+
+	/* Verify field offsets (no unexpected padding) */
+	if (offsetof(varatt_external_extended, va_rawsize) != 0)
+		elog(ERROR, "va_rawsize offset is %zu, expected 0",
+			 offsetof(varatt_external_extended, va_rawsize));
+	if (offsetof(varatt_external_extended, va_extinfo) != 4)
+		elog(ERROR, "va_extinfo offset is %zu, expected 4",
+			 offsetof(varatt_external_extended, va_extinfo));
+	if (offsetof(varatt_external_extended, va_flags) != 8)
+		elog(ERROR, "va_flags offset is %zu, expected 8",
+			 offsetof(varatt_external_extended, va_flags));
+	if (offsetof(varatt_external_extended, va_data) != 9)
+		elog(ERROR, "va_data offset is %zu, expected 9",
+			 offsetof(varatt_external_extended, va_data));
+	if (offsetof(varatt_external_extended, va_valueid) != 12)
+		elog(ERROR, "va_valueid offset is %zu, expected 12",
+			 offsetof(varatt_external_extended, va_valueid));
+	if (offsetof(varatt_external_extended, va_toastrelid) != 16)
+		elog(ERROR, "va_toastrelid offset is %zu, expected 16",
+			 offsetof(varatt_external_extended, va_toastrelid));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Verify flag validation macros work correctly.
+ */
+Datum
+test_toast_flag_validation(PG_FUNCTION_ARGS)
+{
+	/* Valid flags should pass */
+	if (!ExtendedFlagsAreValid(0x00))
+		elog(ERROR, "flags 0x00 should be valid");
+	if (!ExtendedFlagsAreValid(0x01))
+		elog(ERROR, "flags 0x01 should be valid");
+	if (!ExtendedFlagsAreValid(0x02))
+		elog(ERROR, "flags 0x02 should be valid");
+	if (!ExtendedFlagsAreValid(0x03))
+		elog(ERROR, "flags 0x03 should be valid");
+
+	/* Invalid flags should fail */
+	if (ExtendedFlagsAreValid(0x04))
+		elog(ERROR, "flags 0x04 should be invalid");
+	if (ExtendedFlagsAreValid(0x08))
+		elog(ERROR, "flags 0x08 should be invalid");
+	if (ExtendedFlagsAreValid(0xFF))
+		elog(ERROR, "flags 0xFF should be invalid");
+
+	/* Compression methods 0-255 are valid */
+	if (!ExtendedCompressionMethodIsValid(0))
+		elog(ERROR, "compression method 0 should be valid");
+	if (!ExtendedCompressionMethodIsValid(255))
+		elog(ERROR, "compression method 255 should be valid");
+
+	/* Verify method ID constants */
+	if (TOAST_PGLZ_EXT_METHOD != 0)
+		elog(ERROR, "TOAST_PGLZ_EXT_METHOD is %d, expected 0", TOAST_PGLZ_EXT_METHOD);
+	if (TOAST_LZ4_EXT_METHOD != 1)
+		elog(ERROR, "TOAST_LZ4_EXT_METHOD is %d, expected 1", TOAST_LZ4_EXT_METHOD);
+	if (TOAST_ZSTD_EXT_METHOD != 2)
+		elog(ERROR, "TOAST_ZSTD_EXT_METHOD is %d, expected 2", TOAST_ZSTD_EXT_METHOD);
+	if (TOAST_UNCOMPRESSED_EXT_METHOD != 3)
+		elog(ERROR, "TOAST_UNCOMPRESSED_EXT_METHOD is %d, expected 3", TOAST_UNCOMPRESSED_EXT_METHOD);
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Verify compression ID constants are consistent.
+ */
+Datum
+test_toast_compression_ids(PG_FUNCTION_ARGS)
+{
+	/* Standard compression IDs */
+	if (TOAST_PGLZ_COMPRESSION_ID != 0)
+		elog(ERROR, "TOAST_PGLZ_COMPRESSION_ID is %d, expected 0", TOAST_PGLZ_COMPRESSION_ID);
+	if (TOAST_LZ4_COMPRESSION_ID != 1)
+		elog(ERROR, "TOAST_LZ4_COMPRESSION_ID is %d, expected 1", TOAST_LZ4_COMPRESSION_ID);
+	if (TOAST_INVALID_COMPRESSION_ID != 2)
+		elog(ERROR, "TOAST_INVALID_COMPRESSION_ID is %d, expected 2", TOAST_INVALID_COMPRESSION_ID);
+	if (TOAST_EXTENDED_COMPRESSION_ID != 3)
+		elog(ERROR, "TOAST_EXTENDED_COMPRESSION_ID is %d, expected 3", TOAST_EXTENDED_COMPRESSION_ID);
+
+	/* Extended IDs should match standard where applicable */
+	if (TOAST_PGLZ_EXT_METHOD != TOAST_PGLZ_COMPRESSION_ID)
+		elog(ERROR, "PGLZ IDs mismatch: standard=%d, extended=%d",
+			 TOAST_PGLZ_COMPRESSION_ID, TOAST_PGLZ_EXT_METHOD);
+	if (TOAST_LZ4_EXT_METHOD != TOAST_LZ4_COMPRESSION_ID)
+		elog(ERROR, "LZ4 IDs mismatch: standard=%d, extended=%d",
+			 TOAST_LZ4_COMPRESSION_ID, TOAST_LZ4_EXT_METHOD);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/test/modules/test_toast_ext/test_toast_ext.control b/src/test/modules/test_toast_ext/test_toast_ext.control
new file mode 100644
index 00000000000..d59ee14ad64
--- /dev/null
+++ b/src/test/modules/test_toast_ext/test_toast_ext.control
@@ -0,0 +1,5 @@
+# test_toast_ext extension
+comment = 'Test module for extended TOAST headers and zstd compression'
+default_version = '1.0'
+module_pathname = '$libdir/test_toast_ext'
+relocatable = true
-- 
2.39.3 (Apple Git-146)

#3Dharin Shah
dharinshah95@gmail.com
In reply to: Dharin Shah (#1)
1 attachment(s)

Hello PG Hackers,

Want to submit a patch that implements zstd compression for TOAST data
using a 20-byte TOAST pointer format, directly addressing the concerns
raised in prior discussions [1
</messages/by-id/CAFAfj_F4qeRCNCYPk1vgH42fDZpjQWKO+ufq3FyoVyUa5AviFA@mail.gmail.com
][2
</messages/by-id/CAJ7c6TOtAB0z1UrksvGTStNE-herK-43bj22=5xVBg7S4vr5rQ@mail.gmail.com&gt;
][3
</messages/by-id/YoMiNmkztrslDbNS@paquier.xyz&gt;%5D.

A bit of a background in the 2022 thread [3
</messages/by-id/YoMiNmkztrslDbNS@paquier.xyz&gt;%5D,
The overall suggestion was to have something extensible for the TOAST header

i.e. something like:
00 = PGLZ
01 = LZ4
10 = reserved for future emergencies
11 = extended header with additional type byte

This patch implements that idea.
The new header format:

struct varatt_external_extended {
int32 va_rawsize; /* same as legacy */
uint32 va_extinfo; /* cmid=3 signals extended format */
uint8 va_flags; /* feature flags */
uint8 va_data[3]; /* va_data[0] = compression method */
Oid va_valueid; /* same as legacy */
Oid va_toastrelid; /* same as legacy */
};

*A few notes:*

- Zstd only applies to external TOAST, not inline compression. The 2-bit
limit in va_tcinfo stays as-is for inline data, where pglz/lz4 work fine
anyway. Zstd's wins show up on larger values.
- A GUC use_extended_toast_header controls whether pglz/lz4 also use the
20-byte format (defaults to off for compatibility, can enable it if you
want consistency).
- Legacy 16-byte pointers continue to work - we check the vartag to
determine which format to read.

The 4 extra bytes per pointer is negligible for typical TOAST data sizes,
and it gives us room to grow.

Regards,
Dharin

Attachments:

zstd-toast-compression-external.patchapplication/x-patch; name=zstd-toast-compression-external.patchDownload
From fdaae5dc9e9837f73b991100adcba6d76dda1f40 Mon Sep 17 00:00:00 2001
From: Dharin Shah <8616130+Dharin-shah@users.noreply.github.com>
Date: Sat, 13 Dec 2025 11:16:35 +0100
Subject: [PATCH] Add zstd compression support for TOAST using extended header
 format

---
 contrib/amcheck/verify_heapam.c               |  69 +++++-
 src/backend/access/common/detoast.c           | 164 ++++++++++++---
 src/backend/access/common/toast_compression.c | 199 +++++++++++++++++-
 src/backend/access/common/toast_internals.c   | 198 +++++++++++++++--
 src/backend/access/table/toast_helper.c       |   2 +-
 .../replication/logical/reorderbuffer.c       |  38 +++-
 src/backend/utils/adt/varlena.c               |  26 ++-
 src/backend/utils/misc/guc_parameters.dat     |   7 +-
 src/backend/utils/misc/guc_tables.c           |   3 +
 src/include/access/detoast.h                  |  41 +++-
 src/include/access/toast_compression.h        |  36 ++++
 src/include/access/toast_internals.h          |  10 +-
 src/include/varatt.h                          | 160 +++++++++++++-
 src/test/modules/meson.build                  |   1 +
 src/test/modules/test_toast_ext/Makefile      |  20 ++
 .../expected/test_toast_ext.out               | 187 ++++++++++++++++
 .../expected/test_toast_ext_1.out             |  37 ++++
 src/test/modules/test_toast_ext/meson.build   |  33 +++
 .../test_toast_ext/sql/test_toast_ext.sql     | 136 ++++++++++++
 .../test_toast_ext/test_toast_ext--1.0.sql    |  19 ++
 .../modules/test_toast_ext/test_toast_ext.c   | 140 ++++++++++++
 .../test_toast_ext/test_toast_ext.control     |   5 +
 22 files changed, 1440 insertions(+), 91 deletions(-)
 create mode 100644 src/test/modules/test_toast_ext/Makefile
 create mode 100644 src/test/modules/test_toast_ext/expected/test_toast_ext.out
 create mode 100644 src/test/modules/test_toast_ext/expected/test_toast_ext_1.out
 create mode 100644 src/test/modules/test_toast_ext/meson.build
 create mode 100644 src/test/modules/test_toast_ext/sql/test_toast_ext.sql
 create mode 100644 src/test/modules/test_toast_ext/test_toast_ext--1.0.sql
 create mode 100644 src/test/modules/test_toast_ext/test_toast_ext.c
 create mode 100644 src/test/modules/test_toast_ext/test_toast_ext.control

diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 130b3533463..25cae4d0380 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1665,6 +1665,8 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	uint16		infomask;
 	CompactAttribute *thisatt;
 	struct varatt_external toast_pointer;
+	struct varatt_external_extended toast_pointer_ext;
+	bool		is_extended;
 
 	infomask = ctx->tuphdr->t_infomask;
 	thisatt = TupleDescCompactAttr(RelationGetDescr(ctx->rel), ctx->attnum);
@@ -1717,13 +1719,14 @@ check_tuple_attribute(HeapCheckContext *ctx)
 
 	/*
 	 * Check that VARTAG_SIZE won't hit an Assert on a corrupt va_tag before
-	 * risking a call into att_addlength_pointer
+	 * risking a call into att_addlength_pointer.  Both legacy (VARTAG_ONDISK)
+	 * and extended (VARTAG_ONDISK_EXTENDED) on-disk formats are valid.
 	 */
 	if (VARATT_IS_EXTERNAL(tp + ctx->offset))
 	{
 		uint8		va_tag = VARTAG_EXTERNAL(tp + ctx->offset);
 
-		if (va_tag != VARTAG_ONDISK)
+		if (va_tag != VARTAG_ONDISK && va_tag != VARTAG_ONDISK_EXTENDED)
 		{
 			report_corruption(ctx,
 							  psprintf("toasted attribute has unexpected TOAST tag %u",
@@ -1768,9 +1771,23 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	/* It is external, and we're looking at a page on disk */
 
 	/*
-	 * Must copy attr into toast_pointer for alignment considerations
+	 * Must copy attr into toast_pointer for alignment considerations.
+	 * Handle both legacy (VARTAG_ONDISK) and extended (VARTAG_ONDISK_EXTENDED)
+	 * formats.
 	 */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	is_extended = (VARTAG_EXTERNAL(attr) == VARTAG_ONDISK_EXTENDED);
+
+	if (is_extended)
+	{
+		VARATT_EXTERNAL_GET_POINTER_EXTENDED(toast_pointer_ext, attr);
+		/* Copy common fields for simpler code below */
+		toast_pointer.va_rawsize = toast_pointer_ext.va_rawsize;
+		toast_pointer.va_extinfo = toast_pointer_ext.va_extinfo;
+		toast_pointer.va_valueid = toast_pointer_ext.va_valueid;
+		toast_pointer.va_toastrelid = toast_pointer_ext.va_toastrelid;
+	}
+	else
+		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 
 	/* Toasted attributes too large to be untoasted should never be stored */
 	if (toast_pointer.va_rawsize > VARLENA_SIZE_LIMIT)
@@ -1785,8 +1802,11 @@ check_tuple_attribute(HeapCheckContext *ctx)
 		ToastCompressionId cmid;
 		bool		valid = false;
 
-		/* Compressed attributes should have a valid compression method */
-		cmid = TOAST_COMPRESS_METHOD(&toast_pointer);
+		/*
+		 * Compressed attributes should have a valid compression method.
+		 * For extended pointers with cmid==3, the actual method is in va_data[0].
+		 */
+		cmid = VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer);
 		switch (cmid)
 		{
 				/* List of all valid compression method IDs */
@@ -1795,6 +1815,27 @@ check_tuple_attribute(HeapCheckContext *ctx)
 				valid = true;
 				break;
 
+				/* Extended compression (zstd or pglz/lz4 in extended format) */
+			case TOAST_EXTENDED_COMPRESSION_ID:
+				if (is_extended)
+				{
+					uint8	ext_method = VARATT_EXTERNAL_GET_EXT_COMPRESSION_METHOD(toast_pointer_ext);
+
+					/* Validate extended compression method */
+					switch (ext_method)
+					{
+						case TOAST_PGLZ_EXT_METHOD:
+						case TOAST_LZ4_EXT_METHOD:
+						case TOAST_ZSTD_EXT_METHOD:
+							valid = true;
+							break;
+						default:
+							/* Invalid extended method will be reported below */
+							break;
+					}
+				}
+				break;
+
 				/* Recognized but invalid compression method ID */
 			case TOAST_INVALID_COMPRESSION_ID:
 				break;
@@ -1840,7 +1881,21 @@ check_tuple_attribute(HeapCheckContext *ctx)
 
 		ta = palloc0_object(ToastedAttribute);
 
-		VARATT_EXTERNAL_GET_POINTER(ta->toast_pointer, attr);
+		/*
+		 * Extract toast pointer based on format.  For extended format,
+		 * copy common fields from toast_pointer which we already extracted
+		 * above.
+		 */
+		if (is_extended)
+		{
+			ta->toast_pointer.va_rawsize = toast_pointer.va_rawsize;
+			ta->toast_pointer.va_extinfo = toast_pointer.va_extinfo;
+			ta->toast_pointer.va_valueid = toast_pointer.va_valueid;
+			ta->toast_pointer.va_toastrelid = toast_pointer.va_toastrelid;
+		}
+		else
+			VARATT_EXTERNAL_GET_POINTER(ta->toast_pointer, attr);
+
 		ta->blkno = ctx->blkno;
 		ta->offnum = ctx->offnum;
 		ta->attnum = ctx->attnum;
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 62651787742..6d1c08900e8 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_compression.h"
 #include "access/toast_internals.h"
 #include "common/int.h"
 #include "common/pg_lzcompress.h"
@@ -225,12 +226,47 @@ detoast_attr_slice(struct varlena *attr,
 
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
-		struct varatt_external toast_pointer;
+		int32		max_size;
+		bool		is_compressed;
+		bool		is_pglz = false;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		/*
+		 * Handle both legacy 16-byte and extended 20-byte on-disk TOAST
+		 * pointers.  Check the vartag to determine which format.
+		 */
+		if (VARTAG_EXTERNAL(attr) == VARTAG_ONDISK_EXTENDED)
+		{
+			struct varatt_external_extended toast_pointer_ext;
+			uint8		ext_method;
+
+			VARATT_EXTERNAL_GET_POINTER_EXTENDED(toast_pointer_ext, attr);
+			max_size = VARATT_EXTERNAL_GET_EXTSIZE_EXTENDED(toast_pointer_ext);
+			is_compressed = VARATT_EXTERNAL_IS_COMPRESSED_EXTENDED(toast_pointer_ext);
+
+			/* Check if this is pglz for slice optimization */
+			if (is_compressed &&
+				VARATT_EXTERNAL_HAS_FLAG(toast_pointer_ext, TOAST_EXT_FLAG_COMPRESSION))
+			{
+				ext_method = VARATT_EXTERNAL_GET_EXT_COMPRESSION_METHOD(toast_pointer_ext);
+				is_pglz = (ext_method == TOAST_PGLZ_EXT_METHOD);
+			}
+		}
+		else
+		{
+			struct varatt_external toast_pointer;
+
+			VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+			max_size = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+			is_compressed = VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer);
+
+			/* Check if this is pglz for slice optimization */
+			if (is_compressed)
+				is_pglz = (VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) ==
+						   TOAST_PGLZ_COMPRESSION_ID);
+		}
 
 		/* fast path for non-compressed external datums */
-		if (!VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+		if (!is_compressed)
 			return toast_fetch_datum_slice(attr, sliceoffset, slicelength);
 
 		/*
@@ -240,19 +276,16 @@ detoast_attr_slice(struct varlena *attr,
 		 */
 		if (slicelimit >= 0)
 		{
-			int32		max_size = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
-
 			/*
 			 * 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 zstd 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 (VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) ==
-				TOAST_PGLZ_COMPRESSION_ID)
+			if (is_pglz)
 				max_size = pglz_maximum_compressed_size(slicelimit, max_size);
 
 			/*
@@ -344,20 +377,42 @@ toast_fetch_datum(struct varlena *attr)
 {
 	Relation	toastrel;
 	struct varlena *result;
-	struct varatt_external toast_pointer;
 	int32		attrsize;
+	Oid			toastrelid;
+	Oid			valueid;
+	bool		is_compressed;
 
 	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);
+	/*
+	 * Handle both legacy 16-byte and extended 20-byte on-disk TOAST pointers.
+	 * Check the vartag to determine which format we're dealing with.
+	 */
+	if (VARTAG_EXTERNAL(attr) == VARTAG_ONDISK_EXTENDED)
+	{
+		struct varatt_external_extended toast_pointer_ext;
+
+		VARATT_EXTERNAL_GET_POINTER_EXTENDED(toast_pointer_ext, attr);
+		attrsize = VARATT_EXTERNAL_GET_EXTSIZE_EXTENDED(toast_pointer_ext);
+		toastrelid = toast_pointer_ext.va_toastrelid;
+		valueid = toast_pointer_ext.va_valueid;
+		is_compressed = VARATT_EXTERNAL_IS_COMPRESSED_EXTENDED(toast_pointer_ext);
+	}
+	else
+	{
+		struct varatt_external toast_pointer;
 
-	attrsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		attrsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+		toastrelid = toast_pointer.va_toastrelid;
+		valueid = toast_pointer.va_valueid;
+		is_compressed = VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer);
+	}
 
 	result = (struct varlena *) palloc(attrsize + VARHDRSZ);
 
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+	if (is_compressed)
 		SET_VARSIZE_COMPRESSED(result, attrsize + VARHDRSZ);
 	else
 		SET_VARSIZE(result, attrsize + VARHDRSZ);
@@ -369,10 +424,10 @@ toast_fetch_datum(struct varlena *attr)
 	/*
 	 * Open the toast relation and its indexes
 	 */
-	toastrel = table_open(toast_pointer.va_toastrelid, AccessShareLock);
+	toastrel = table_open(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 +453,45 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 {
 	Relation	toastrel;
 	struct varlena *result;
-	struct varatt_external toast_pointer;
 	int32		attrsize;
+	Oid			toastrelid;
+	Oid			valueid;
+	bool		is_compressed;
 
 	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);
+	/*
+	 * Handle both legacy 16-byte and extended 20-byte on-disk TOAST pointers.
+	 * Check the vartag to determine which format we're dealing with.
+	 */
+	if (VARTAG_EXTERNAL(attr) == VARTAG_ONDISK_EXTENDED)
+	{
+		struct varatt_external_extended toast_pointer_ext;
+
+		VARATT_EXTERNAL_GET_POINTER_EXTENDED(toast_pointer_ext, attr);
+		attrsize = VARATT_EXTERNAL_GET_EXTSIZE_EXTENDED(toast_pointer_ext);
+		toastrelid = toast_pointer_ext.va_toastrelid;
+		valueid = toast_pointer_ext.va_valueid;
+		is_compressed = VARATT_EXTERNAL_IS_COMPRESSED_EXTENDED(toast_pointer_ext);
+	}
+	else
+	{
+		struct varatt_external toast_pointer;
+
+		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		attrsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+		toastrelid = toast_pointer.va_toastrelid;
+		valueid = toast_pointer.va_valueid;
+		is_compressed = VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer);
+	}
 
 	/*
 	 * 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);
-
-	attrsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+	Assert(!is_compressed || 0 == sliceoffset);
 
 	if (sliceoffset >= attrsize)
 	{
@@ -427,7 +504,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 (is_compressed && slicelength > 0)
 		slicelength = slicelength + sizeof(int32);
 
 	/*
@@ -440,7 +517,7 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 
 	result = (struct varlena *) palloc(slicelength + VARHDRSZ);
 
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+	if (is_compressed)
 		SET_VARSIZE_COMPRESSED(result, slicelength + VARHDRSZ);
 	else
 		SET_VARSIZE(result, slicelength + VARHDRSZ);
@@ -449,10 +526,10 @@ 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(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);
 
@@ -485,6 +562,9 @@ toast_decompress_datum(struct varlena *attr)
 			return pglz_decompress_datum(attr);
 		case TOAST_LZ4_COMPRESSION_ID:
 			return lz4_decompress_datum(attr);
+		case TOAST_EXTENDED_COMPRESSION_ID:
+			/* zstd-compressed data */
+			return zstd_decompress_datum(attr);
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 			return NULL;		/* keep compiler quiet */
@@ -528,6 +608,9 @@ 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_EXTENDED_COMPRESSION_ID:
+			/* zstd-compressed data */
+			return zstd_decompress_datum_slice(attr, slicelength);
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 			return NULL;		/* keep compiler quiet */
@@ -549,11 +632,15 @@ 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;
+		/*
+		 * va_rawsize is the size of the original datum -- including header.
+		 * It's at offset 0 in both varatt_external and varatt_external_extended,
+		 * so we can read just the first 4 bytes regardless of format.
+		 */
+		int32	va_rawsize;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-		result = toast_pointer.va_rawsize;
+		memcpy(&va_rawsize, VARDATA_EXTERNAL(attr), sizeof(va_rawsize));
+		result = va_rawsize;
 	}
 	else if (VARATT_IS_EXTERNAL_INDIRECT(attr))
 	{
@@ -609,11 +696,18 @@ 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?
+		 *
+		 * va_extinfo is at offset 4 in both varatt_external and
+		 * varatt_external_extended, so we can read the first 8 bytes
+		 * regardless of format.
 		 */
-		struct varatt_external toast_pointer;
+		struct {
+			int32	va_rawsize;
+			uint32	va_extinfo;
+		} common;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-		result = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+		memcpy(&common, VARDATA_EXTERNAL(attr), sizeof(common));
+		result = common.va_extinfo & VARLENA_EXTSIZE_MASK;
 	}
 	else if (VARATT_IS_EXTERNAL_INDIRECT(attr))
 	{
diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c
index 926f1e4008a..422e2c5967a 100644
--- a/src/backend/access/common/toast_compression.c
+++ b/src/backend/access/common/toast_compression.c
@@ -17,13 +17,19 @@
 #include <lz4.h>
 #endif
 
+#ifdef USE_ZSTD
+#include <zstd.h>
+#endif
+
 #include "access/detoast.h"
 #include "access/toast_compression.h"
 #include "common/pg_lzcompress.h"
+#include "utils/memutils.h"
 #include "varatt.h"
 
 /* GUC */
 int			default_toast_compression = TOAST_PGLZ_COMPRESSION;
+bool		use_extended_toast_header = false;
 
 #define NO_COMPRESSION_SUPPORT(method) \
 	ereport(ERROR, \
@@ -249,11 +255,16 @@ lz4_decompress_datum_slice(const struct varlena *value, int32 slicelength)
  * Extract compression ID from a varlena.
  *
  * Returns TOAST_INVALID_COMPRESSION_ID if the varlena is not compressed.
+ *
+ * For external data stored in extended format (VARTAG_ONDISK_EXTENDED),
+ * the actual compression method is stored in va_data[0].  We map that
+ * back to the appropriate ToastCompressionId for legacy compatibility.
  */
 ToastCompressionId
 toast_get_compression_id(struct varlena *attr)
 {
 	ToastCompressionId cmid = TOAST_INVALID_COMPRESSION_ID;
+	vartag_external tag;
 
 	/*
 	 * If it is stored externally then fetch the compression method id from
@@ -262,12 +273,52 @@ toast_get_compression_id(struct varlena *attr)
 	 */
 	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);
+		tag = VARTAG_EXTERNAL(attr);
+		if (tag == VARTAG_ONDISK)
+		{
+			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);
+		}
+		else
+		{
+			struct varatt_external_extended toast_pointer_ext;
+			uint8		ext_method;
+
+			Assert(tag == VARTAG_ONDISK_EXTENDED);
+			VARATT_EXTERNAL_GET_POINTER_EXTENDED(toast_pointer_ext, attr);
+
+			if (VARATT_EXTERNAL_IS_COMPRESSED_EXTENDED(toast_pointer_ext))
+			{
+				/*
+				 * Extended format stores the actual method in va_data[0].
+				 * Map it back to ToastCompressionId for reporting purposes.
+				 */
+				ext_method = VARATT_EXTERNAL_GET_EXT_COMPRESSION_METHOD(toast_pointer_ext);
+				switch (ext_method)
+				{
+					case TOAST_PGLZ_EXT_METHOD:
+						cmid = TOAST_PGLZ_COMPRESSION_ID;
+						break;
+					case TOAST_LZ4_EXT_METHOD:
+						cmid = TOAST_LZ4_COMPRESSION_ID;
+						break;
+					case TOAST_ZSTD_EXT_METHOD:
+						cmid = TOAST_EXTENDED_COMPRESSION_ID;
+						break;
+					case TOAST_UNCOMPRESSED_EXT_METHOD:
+						/* Uncompressed data in extended format */
+						cmid = TOAST_INVALID_COMPRESSION_ID;
+						break;
+					default:
+						elog(ERROR, "invalid extended compression method %d",
+							 ext_method);
+				}
+			}
+		}
 	}
 	else if (VARATT_IS_COMPRESSED(attr))
 		cmid = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(attr);
@@ -275,6 +326,133 @@ toast_get_compression_id(struct varlena *attr)
 	return cmid;
 }
 
+/*
+ * Zstandard (zstd) compression/decompression for TOAST (extended methods).
+ *
+ * These routines use the same basic shape as the pglz and LZ4 helpers,
+ * but are only available when PostgreSQL is built with USE_ZSTD.
+ */
+
+/*
+ * Compress a varlena using ZSTD.
+ *
+ * Returns the compressed varlena, or NULL if compression fails or does
+ * not save space.
+ */
+static struct varlena *
+zstd_compress_datum_internal(const struct varlena *value, int level)
+{
+#ifndef USE_ZSTD
+	NO_COMPRESSION_SUPPORT("zstd");
+	return NULL;				/* keep compiler quiet */
+#else
+	Size		valsize;
+	Size		max_size;
+	Size		out_size;
+	struct varlena *tmp;
+	size_t		rc;
+
+	valsize = VARSIZE_ANY_EXHDR(value);
+
+	/*
+	 * Compute an upper bound for the compressed size and allocate enough
+	 * space for the compressed payload plus the varlena header.
+	 */
+	max_size = ZSTD_compressBound(valsize);
+	if (max_size > (Size) (MaxAllocSize - VARHDRSZ_COMPRESSED))
+		ereport(ERROR,
+				(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				 errmsg("compressed data would exceed maximum allocation size")));
+
+	tmp = (struct varlena *) palloc(max_size + VARHDRSZ_COMPRESSED);
+
+	rc = ZSTD_compress((char *) tmp + VARHDRSZ_COMPRESSED, max_size,
+					   VARDATA_ANY(value), valsize, level);
+	if (ZSTD_isError(rc))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATA_CORRUPTED),
+				 errmsg_internal("zstd compression failed: %s",
+								 ZSTD_getErrorName(rc))));
+
+	out_size = (Size) rc;
+
+	/*
+	 * If the compressed representation is not smaller than the original
+	 * payload, give up and return NULL so that callers can fall back to
+	 * storing the datum uncompressed or with a different method.
+	 */
+	if (out_size >= valsize)
+	{
+		pfree(tmp);
+		return NULL;
+	}
+
+	SET_VARSIZE_COMPRESSED(tmp, out_size + VARHDRSZ_COMPRESSED);
+
+	return tmp;
+#endif							/* USE_ZSTD */
+}
+
+struct varlena *
+zstd_compress_datum(const struct varlena *value)
+{
+#ifndef USE_ZSTD
+	NO_COMPRESSION_SUPPORT("zstd");
+	return NULL;				/* keep compiler quiet */
+#else
+	return zstd_compress_datum_internal(value, ZSTD_CLEVEL_DEFAULT);
+#endif
+}
+
+/*
+ * Decompress a varlena that was compressed using ZSTD.
+ */
+struct varlena *
+zstd_decompress_datum(const struct varlena *value)
+{
+#ifndef USE_ZSTD
+	NO_COMPRESSION_SUPPORT("zstd");
+	return NULL;				/* keep compiler quiet */
+#else
+	struct varlena *result;
+	Size		rawsize;
+	size_t		rc;
+
+	/* allocate memory for the uncompressed data */
+	rawsize = VARDATA_COMPRESSED_GET_EXTSIZE(value);
+	result = (struct varlena *) palloc(rawsize + VARHDRSZ);
+
+	rc = ZSTD_decompress(VARDATA(result), rawsize,
+						 (char *) value + VARHDRSZ_COMPRESSED,
+						 VARSIZE(value) - VARHDRSZ_COMPRESSED);
+	if (ZSTD_isError(rc) || rc != rawsize)
+		ereport(ERROR,
+				(errcode(ERRCODE_DATA_CORRUPTED),
+				 errmsg_internal("compressed zstd data is corrupt or truncated")));
+
+	SET_VARSIZE(result, rawsize + VARHDRSZ);
+
+	return result;
+#endif							/* USE_ZSTD */
+}
+
+/*
+ * Decompress part of a varlena that was compressed using ZSTD.
+ *
+ * At least initially we don't try to be clever with streaming slice
+ * decompression here; instead we just decompress the full datum and
+ * let higher layers perform the slicing.  Callers should prefer the
+ * regular zstd_decompress_datum() when they know they need the whole
+ * value anyway.
+ */
+struct varlena *
+zstd_decompress_datum_slice(const struct varlena *value, int32 slicelength)
+{
+	/* For now, just fall back to full decompression. */
+	(void) slicelength;
+	return zstd_decompress_datum(value);
+}
+
 /*
  * CompressionNameToMethod - Get compression method from compression name
  *
@@ -293,6 +471,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;
 }
@@ -309,6 +494,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 d06af82de15..039ccc42249 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_compression.h"
 #include "access/toast_internals.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -71,6 +72,9 @@ toast_compress_datum(Datum value, char cmethod)
 			tmp = lz4_compress_datum((const struct varlena *) DatumGetPointer(value));
 			cmid = TOAST_LZ4_COMPRESSION_ID;
 			break;
+		case TOAST_ZSTD_COMPRESSION:
+			/* zstd uses external storage only; handled by toast_save_datum */
+			return PointerGetDatum(NULL);
 		default:
 			elog(ERROR, "invalid compression method %c", cmethod);
 	}
@@ -113,11 +117,13 @@ toast_compress_datum(Datum value, char cmethod)
  * value: datum to be pushed to toast storage
  * oldexternal: if not NULL, toast pointer previously representing the datum
  * options: options to be passed to heap_insert() for toast rows
+ * cmethod: compression method to use for uncompressed data
  * ----------
  */
 Datum
 toast_save_datum(Relation rel, Datum value,
-				 struct varlena *oldexternal, int options)
+				 struct varlena *oldexternal, int options,
+				 char cmethod)
 {
 	Relation	toastrel;
 	Relation   *toastidxs;
@@ -125,12 +131,16 @@ toast_save_datum(Relation rel, Datum value,
 	CommandId	mycid = GetCurrentCommandId(true);
 	struct varlena *result;
 	struct varatt_external toast_pointer;
+	struct varatt_external_extended toast_pointer_ext;
 	int32		chunk_seq = 0;
 	char	   *data_p;
 	int32		data_todo;
 	Pointer		dval = DatumGetPointer(value);
 	int			num_indexes;
 	int			validIndex;
+	bool		use_extended = false;
+	uint8		ext_method = 0;
+	struct varlena *compressed_to_free = NULL;	/* track allocated buffer */
 
 	Assert(!VARATT_IS_EXTERNAL(dval));
 
@@ -167,23 +177,99 @@ toast_save_datum(Relation rel, Datum value,
 	}
 	else if (VARATT_IS_COMPRESSED(dval))
 	{
+		ToastCompressionId cmid;
+
 		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;
 
+		/* Get compression method from compressed datum */
+		cmid = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(dval);
+
+		/* Decide whether to use extended 20-byte or legacy 16-byte format */
+		if (cmid == TOAST_EXTENDED_COMPRESSION_ID)
+		{
+			use_extended = true;
+			ext_method = TOAST_ZSTD_EXT_METHOD;
+		}
+		else if (use_extended_toast_header)
+		{
+			/* Use extended format for pglz/lz4 when GUC is enabled */
+			use_extended = true;
+			switch (cmid)
+			{
+				case TOAST_PGLZ_COMPRESSION_ID:
+					ext_method = TOAST_PGLZ_EXT_METHOD;
+					break;
+				case TOAST_LZ4_COMPRESSION_ID:
+					ext_method = TOAST_LZ4_EXT_METHOD;
+					break;
+				default:
+					/* Should not happen, but fall back to legacy format */
+					use_extended = false;
+					break;
+			}
+		}
+
 		/* set external size and compression method */
-		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, data_todo,
-													 VARDATA_COMPRESSED_GET_COMPRESS_METHOD(dval));
+		if (use_extended)
+			VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, data_todo,
+														VARATT_EXTERNAL_EXTENDED_CMID);
+		else
+			VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, data_todo, cmid);
+
 		/* Assert that the numbers look like it's compressed */
 		Assert(VARATT_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;
+		/*
+		 * Uncompressed data.  If the caller specified zstd compression,
+		 * try to compress it now before storing to the TOAST table.
+		 */
+		if (cmethod == TOAST_ZSTD_COMPRESSION)
+		{
+			struct varlena *compressed;
+			int32		rawsize;
+
+			rawsize = VARSIZE_ANY_EXHDR((const struct varlena *) dval);
+			compressed = zstd_compress_datum((const struct varlena *) dval);
+			if (compressed != NULL)
+			{
+				/* Set compression method in va_tcinfo */
+				TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(compressed, rawsize,
+															TOAST_EXTENDED_COMPRESSION_ID);
+
+				/* Compression succeeded - use the compressed data */
+				compressed_to_free = compressed;	/* track for cleanup */
+				dval = (Pointer) compressed;
+				data_p = VARDATA(compressed);
+				data_todo = VARSIZE(compressed) - VARHDRSZ;
+				toast_pointer.va_rawsize = rawsize + VARHDRSZ;
+
+				/* Use extended format for zstd */
+				use_extended = true;
+				ext_method = TOAST_ZSTD_EXT_METHOD;
+				VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, data_todo,
+															VARATT_EXTERNAL_EXTENDED_CMID);
+			}
+			else
+			{
+				/* Compression failed or didn't save space - store uncompressed */
+				data_p = VARDATA(dval);
+				data_todo = VARSIZE(dval) - VARHDRSZ;
+				toast_pointer.va_rawsize = VARSIZE(dval);
+				toast_pointer.va_extinfo = data_todo;
+			}
+		}
+		else
+		{
+			data_p = VARDATA(dval);
+			data_todo = VARSIZE(dval) - VARHDRSZ;
+			toast_pointer.va_rawsize = VARSIZE(dval);
+			toast_pointer.va_extinfo = data_todo;
+		}
 	}
 
 	/*
@@ -225,15 +311,36 @@ toast_save_datum(Relation rel, Datum value,
 		toast_pointer.va_valueid = InvalidOid;
 		if (oldexternal != NULL)
 		{
-			struct varatt_external old_toast_pointer;
+			Oid			old_toastrelid;
+			Oid			old_valueid;
 
 			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)
+
+			/*
+			 * Extract toastrelid and valueid from the old pointer.
+			 * Handle both legacy 16-byte and extended 20-byte formats.
+			 */
+			if (VARTAG_EXTERNAL(oldexternal) == VARTAG_ONDISK_EXTENDED)
+			{
+				struct varatt_external_extended old_toast_pointer_ext;
+
+				VARATT_EXTERNAL_GET_POINTER_EXTENDED(old_toast_pointer_ext, oldexternal);
+				old_toastrelid = old_toast_pointer_ext.va_toastrelid;
+				old_valueid = old_toast_pointer_ext.va_valueid;
+			}
+			else
+			{
+				struct varatt_external old_toast_pointer;
+
+				VARATT_EXTERNAL_GET_POINTER(old_toast_pointer, oldexternal);
+				old_toastrelid = old_toast_pointer.va_toastrelid;
+				old_valueid = old_toast_pointer.va_valueid;
+			}
+
+			if (old_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.va_valueid = old_valueid;
 
 				/*
 				 * There is a corner case here: the table rewrite might have
@@ -348,6 +455,10 @@ toast_save_datum(Relation rel, Datum value,
 		data_p += chunk_size;
 	}
 
+	/* Free compressed buffer if we allocated one */
+	if (compressed_to_free != NULL)
+		pfree(compressed_to_free);
+
 	/*
 	 * Done - close toast relation and its indexes but keep the lock until
 	 * commit, so as a concurrent reindex done directly on the toast relation
@@ -356,12 +467,35 @@ toast_save_datum(Relation rel, Datum value,
 	toast_close_indexes(toastidxs, num_indexes, NoLock);
 	table_close(toastrel, NoLock);
 
-	/*
-	 * 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));
+	/* Create the TOAST pointer value that we'll return */
+	if (use_extended)
+	{
+		/*
+		 * Build extended TOAST pointer.  Copy the common fields from
+		 * toast_pointer, then set the extended-format-specific fields.
+		 */
+		toast_pointer_ext.va_rawsize = toast_pointer.va_rawsize;
+		toast_pointer_ext.va_extinfo = toast_pointer.va_extinfo;
+		toast_pointer_ext.va_valueid = toast_pointer.va_valueid;
+		toast_pointer_ext.va_toastrelid = toast_pointer.va_toastrelid;
+
+		/* Set extended format fields */
+		toast_pointer_ext.va_flags = TOAST_EXT_FLAG_COMPRESSION;
+		toast_pointer_ext.va_data[0] = ext_method;
+		toast_pointer_ext.va_data[1] = 0;
+		toast_pointer_ext.va_data[2] = 0;
+
+		result = (struct varlena *) palloc(TOAST_POINTER_SIZE_EXTENDED);
+		SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_EXTENDED);
+		memcpy(VARDATA_EXTERNAL(result), &toast_pointer_ext, sizeof(toast_pointer_ext));
+	}
+	else
+	{
+		/* Standard 16-byte TOAST pointer */
+		result = (struct varlena *) palloc(TOAST_POINTER_SIZE);
+		SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK);
+		memcpy(VARDATA_EXTERNAL(result), &toast_pointer, sizeof(toast_pointer));
+	}
 
 	return PointerGetDatum(result);
 }
@@ -377,6 +511,7 @@ 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_extended toast_pointer_ext;
 	Relation	toastrel;
 	Relation   *toastidxs;
 	ScanKeyData toastkey;
@@ -384,17 +519,36 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	HeapTuple	toasttup;
 	int			num_indexes;
 	int			validIndex;
+	Oid			toastrelid;
+	Oid			valueid;
+	bool		is_extended;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		return;
 
-	/* Must copy to access aligned fields */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	/*
+	 * Must copy to access aligned fields.  Handle both legacy (16-byte) and
+	 * extended (20-byte) on-disk TOAST pointers based on the tag.
+	 */
+	is_extended = (VARTAG_EXTERNAL(attr) == VARTAG_ONDISK_EXTENDED);
+
+	if (!is_extended)
+	{
+		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		toastrelid = toast_pointer.va_toastrelid;
+		valueid = toast_pointer.va_valueid;
+	}
+	else
+	{
+		VARATT_EXTERNAL_GET_POINTER_EXTENDED(toast_pointer_ext, attr);
+		toastrelid = toast_pointer_ext.va_toastrelid;
+		valueid = toast_pointer_ext.va_valueid;
+	}
 
 	/*
 	 * Open the toast relation and its indexes
 	 */
-	toastrel = table_open(toast_pointer.va_toastrelid, RowExclusiveLock);
+	toastrel = table_open(toastrelid, RowExclusiveLock);
 
 	/* Fetch valid relation used for process */
 	validIndex = toast_open_indexes(toastrel,
@@ -408,7 +562,7 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	ScanKeyInit(&toastkey,
 				(AttrNumber) 1,
 				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(toast_pointer.va_valueid));
+				ObjectIdGetDatum(valueid));
 
 	/*
 	 * Find all the chunks.  (We don't actually care whether we see them in
diff --git a/src/backend/access/table/toast_helper.c b/src/backend/access/table/toast_helper.c
index 11f97d65367..21381004ba6 100644
--- a/src/backend/access/table/toast_helper.c
+++ b/src/backend/access/table/toast_helper.c
@@ -261,7 +261,7 @@ toast_tuple_externalize(ToastTupleContext *ttc, int attribute, int options)
 
 	attr->tai_colflags |= TOASTCOL_IGNORE;
 	*value = toast_save_datum(ttc->ttc_rel, old_value, attr->tai_oldexternal,
-							  options);
+							  options, attr->tai_compression);
 	if ((attr->tai_colflags & TOASTCOL_NEEDS_FREE) != 0)
 		pfree(DatumGetPointer(old_value));
 	attr->tai_colflags |= TOASTCOL_NEEDS_FREE;
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index f18c6fb52b5..9e83ab5978d 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -5137,11 +5137,17 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 
 		/* va_rawsize is the size of the original datum -- including header */
 		struct varatt_external toast_pointer;
+		struct varatt_external_extended toast_pointer_ext;
 		struct varatt_indirect redirect_pointer;
 		struct varlena *new_datum = NULL;
 		struct varlena *reconstructed;
 		dlist_iter	it;
 		Size		data_done = 0;
+		bool		is_extended;
+		Oid			valueid;
+		int32		rawsize;
+		int32		extsize;
+		bool		is_compressed;
 
 		if (attr->attisdropped)
 			continue;
@@ -5161,14 +5167,36 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		if (!VARATT_IS_EXTERNAL(varlena))
 			continue;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, varlena);
+		/*
+		 * Handle both legacy 16-byte and extended 20-byte on-disk TOAST
+		 * pointers based on the tag.
+		 */
+		is_extended = VARATT_IS_EXTERNAL_ONDISK(varlena) &&
+					  (VARTAG_EXTERNAL(varlena) == VARTAG_ONDISK_EXTENDED);
+
+		if (is_extended)
+		{
+			VARATT_EXTERNAL_GET_POINTER_EXTENDED(toast_pointer_ext, varlena);
+			valueid = toast_pointer_ext.va_valueid;
+			rawsize = toast_pointer_ext.va_rawsize;
+			extsize = VARATT_EXTERNAL_GET_EXTSIZE_EXTENDED(toast_pointer_ext);
+			is_compressed = VARATT_EXTERNAL_IS_COMPRESSED_EXTENDED(toast_pointer_ext);
+		}
+		else
+		{
+			VARATT_EXTERNAL_GET_POINTER(toast_pointer, varlena);
+			valueid = toast_pointer.va_valueid;
+			rawsize = toast_pointer.va_rawsize;
+			extsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+			is_compressed = VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer);
+		}
 
 		/*
 		 * Check whether the toast tuple changed, replace if so.
 		 */
 		ent = (ReorderBufferToastEnt *)
 			hash_search(txn->toast_hash,
-						&toast_pointer.va_valueid,
+						&valueid,
 						HASH_FIND,
 						NULL);
 		if (ent == NULL)
@@ -5179,7 +5207,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 
 		free[natt] = true;
 
-		reconstructed = palloc0(toast_pointer.va_rawsize);
+		reconstructed = palloc0(rawsize);
 
 		ent->reconstructed = reconstructed;
 
@@ -5204,10 +5232,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 == extsize);
 
 		/* make sure its marked as compressed or not */
-		if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+		if (is_compressed)
 			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 baa5b44ea8d..71a410dc617 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -4206,6 +4206,10 @@ pg_column_compression(PG_FUNCTION_ARGS)
 		case TOAST_LZ4_COMPRESSION_ID:
 			result = "lz4";
 			break;
+		case TOAST_EXTENDED_COMPRESSION_ID:
+			/* Extended format currently only supports zstd */
+			result = "zstd";
+			break;
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 	}
@@ -4222,7 +4226,7 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 {
 	int			typlen;
 	struct varlena *attr;
-	struct varatt_external toast_pointer;
+	Oid			valueid;
 
 	/* On first call, get the input type's typlen, and save at *fn_extra */
 	if (fcinfo->flinfo->fn_extra == NULL)
@@ -4249,9 +4253,25 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		PG_RETURN_NULL();
 
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	/*
+	 * Handle both legacy 16-byte and extended 20-byte on-disk TOAST pointers.
+	 */
+	if (VARTAG_EXTERNAL(attr) == VARTAG_ONDISK_EXTENDED)
+	{
+		struct varatt_external_extended toast_pointer_ext;
+
+		VARATT_EXTERNAL_GET_POINTER_EXTENDED(toast_pointer_ext, attr);
+		valueid = toast_pointer_ext.va_valueid;
+	}
+	else
+	{
+		struct varatt_external toast_pointer;
+
+		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		valueid = toast_pointer.va_valueid;
+	}
 
-	PG_RETURN_OID(toast_pointer.va_valueid);
+	PG_RETURN_OID(valueid);
 }
 
 /*
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 3b9d8349078..38c68d1d0a6 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -738,7 +738,6 @@
   boot_val => 'TOAST_PGLZ_COMPRESSION',
   options => 'default_toast_compression_options',
 },
-
 { name => 'default_transaction_deferrable', type => 'bool', context => 'PGC_USERSET', group => 'CLIENT_CONN_STATEMENT',
   short_desc => 'Sets the default deferrable status of new transactions.',
   variable => 'DefaultXactDeferrable',
@@ -3175,6 +3174,12 @@
   boot_val => 'DEFAULT_UPDATE_PROCESS_TITLE',
 },
 
+{ name => 'use_extended_toast_header', type => 'bool', context => 'PGC_USERSET', group => 'CLIENT_CONN_STATEMENT',
+  short_desc => 'Use 20-byte extended TOAST header format (required for zstd).',
+  variable => 'use_extended_toast_header',
+  boot_val => 'false',
+},
+
 { name => 'vacuum_buffer_usage_limit', type => 'int', context => 'PGC_USERSET', group => 'RESOURCES_MEM',
   short_desc => 'Sets the buffer pool size for VACUUM, ANALYZE, and autovacuum.',
   flags => 'GUC_UNIT_KB',
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index f87b558c2c6..f6c09260f1a 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -460,6 +460,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/include/access/detoast.h b/src/include/access/detoast.h
index e603a2276c3..e591a59569b 100644
--- a/src/include/access/detoast.h
+++ b/src/include/access/detoast.h
@@ -14,25 +14,58 @@
 
 /*
  * 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.
+ *
+ * This currently supports only the legacy on-disk TOAST pointer format,
+ * which has VARTAG_ONDISK and a payload size of sizeof(varatt_external).
+ * Extended on-disk pointers (VARTAG_ONDISK_EXTENDED) must be accessed via
+ * VARATT_EXTERNAL_GET_POINTER_EXTENDED().
+ *
+ * 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 { \
 	varattrib_1b_e *attre = (varattrib_1b_e *) (attr); \
 	Assert(VARATT_IS_EXTERNAL(attre)); \
+	Assert(VARTAG_EXTERNAL(attre) == VARTAG_ONDISK); \
 	Assert(VARSIZE_EXTERNAL(attre) == sizeof(toast_pointer) + VARHDRSZ_EXTERNAL); \
 	memcpy(&(toast_pointer), VARDATA_EXTERNAL(attre), sizeof(toast_pointer)); \
 } while (0)
 
+/*
+ * Variant of VARATT_EXTERNAL_GET_POINTER for the extended on-disk TOAST
+ * pointer format.  Callers should only use this when they have already
+ * established that the tag is VARTAG_ONDISK_EXTENDED.
+ */
+#define VARATT_EXTERNAL_GET_POINTER_EXTENDED(toast_pointer_ext, attr) \
+do { \
+	varattrib_1b_e *attre = (varattrib_1b_e *) (attr); \
+	Assert(VARATT_IS_EXTERNAL(attre)); \
+	Assert(VARTAG_EXTERNAL(attre) == VARTAG_ONDISK_EXTENDED); \
+	Assert(VARSIZE_EXTERNAL(attre) == sizeof(toast_pointer_ext) + VARHDRSZ_EXTERNAL); \
+	memcpy(&(toast_pointer_ext), VARDATA_EXTERNAL(attre), sizeof(toast_pointer_ext)); \
+} 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))
 
+/* Size of an EXTERNAL datum that contains an extended TOAST pointer */
+#define TOAST_POINTER_SIZE_EXTENDED (VARHDRSZ_EXTERNAL + sizeof(varatt_external_extended))
+
+/* Validation helpers for TOAST pointer sizes */
+#define TOAST_POINTER_SIZE_IS_VALID(size) \
+	((size) == TOAST_POINTER_SIZE || \
+	 (size) == TOAST_POINTER_SIZE_EXTENDED || \
+	 (size) == INDIRECT_POINTER_SIZE)
+
+#define TOAST_POINTER_IS_EXTENDED_SIZE(size) \
+	((size) == TOAST_POINTER_SIZE_EXTENDED)
+
 /* ----------
  * detoast_external_attr() -
  *
diff --git a/src/include/access/toast_compression.h b/src/include/access/toast_compression.h
index 13c4612ceed..b769d1bc72d 100644
--- a/src/include/access/toast_compression.h
+++ b/src/include/access/toast_compression.h
@@ -13,14 +13,21 @@
 #ifndef TOAST_COMPRESSION_H
 #define TOAST_COMPRESSION_H
 
+#include "varatt.h"
+
 /*
  * GUC support.
  *
  * default_toast_compression is an integer for purposes of the GUC machinery,
  * but the value is one of the char values defined below, as they appear in
  * pg_attribute.attcompression, e.g. TOAST_PGLZ_COMPRESSION.
+ *
+ * use_extended_toast_header controls whether to use the 20-byte extended
+ * TOAST pointer format (required for zstd) instead of the legacy 16-byte
+ * format. When false, zstd compression falls back to pglz.
  */
 extern PGDLLIMPORT int default_toast_compression;
+extern PGDLLIMPORT bool use_extended_toast_header;
 
 /*
  * Built-in compression method ID.  The toast compression header will store
@@ -39,6 +46,7 @@ typedef enum ToastCompressionId
 	TOAST_PGLZ_COMPRESSION_ID = 0,
 	TOAST_LZ4_COMPRESSION_ID = 1,
 	TOAST_INVALID_COMPRESSION_ID = 2,
+	TOAST_EXTENDED_COMPRESSION_ID = 3,	/* extended format for future methods */
 } ToastCompressionId;
 
 /*
@@ -48,6 +56,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)
@@ -65,9 +74,36 @@ extern struct varlena *lz4_decompress_datum(const struct varlena *value);
 extern struct varlena *lz4_decompress_datum_slice(const struct varlena *value,
 												  int32 slicelength);
 
+/* zstd compression/decompression routines (extended methods) */
+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);
 extern const char *GetCompressionMethodName(char method);
 
+/*
+ * Feature flags for extended TOAST pointers (varatt_external_extended).
+ * These alias VARATT_EXTERNAL_FLAG_* from varatt.h.
+ */
+#define TOAST_EXT_FLAG_COMPRESSION      VARATT_EXTERNAL_FLAG_COMPRESSION
+#define TOAST_EXT_FLAG_CHECKSUM         VARATT_EXTERNAL_FLAG_CHECKSUM
+
+/*
+ * Extended compression method IDs for use with extended TOAST format.
+ * Stored in va_data[0] when TOAST_EXT_FLAG_COMPRESSION is set.
+ */
+#define TOAST_PGLZ_EXT_METHOD          0
+#define TOAST_LZ4_EXT_METHOD           1
+#define TOAST_ZSTD_EXT_METHOD          2
+#define TOAST_UNCOMPRESSED_EXT_METHOD  3
+
+/* Validation macros for extended format */
+#define ExtendedCompressionMethodIsValid(method) ((method) <= 255)
+#define ExtendedFlagsAreValid(flags) \
+	(((flags) & ~(TOAST_EXT_FLAG_COMPRESSION | TOAST_EXT_FLAG_CHECKSUM)) == 0)
+
 #endif							/* TOAST_COMPRESSION_H */
diff --git a/src/include/access/toast_internals.h b/src/include/access/toast_internals.h
index 06ae8583c1e..d6bc5c4d179 100644
--- a/src/include/access/toast_internals.h
+++ b/src/include/access/toast_internals.h
@@ -36,11 +36,16 @@ typedef struct toast_compress_header
 #define TOAST_COMPRESS_METHOD(ptr) \
 	(((toast_compress_header *) (ptr))->tcinfo >> VARLENA_EXTSIZE_BITS)
 
+/*
+ * Set the size and compression method in a compressed datum's header.
+ * Accepts TOAST_EXTENDED_COMPRESSION_ID for extended compression methods.
+ */
 #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_LZ4_COMPRESSION_ID || \
+			   (cm_method) == TOAST_EXTENDED_COMPRESSION_ID); \
 		((toast_compress_header *) (ptr))->tcinfo = \
 			(len) | ((uint32) (cm_method) << VARLENA_EXTSIZE_BITS); \
 	} while (0)
@@ -50,7 +55,8 @@ extern Oid	toast_get_valid_index(Oid toastoid, LOCKMODE lock);
 
 extern void toast_delete_datum(Relation rel, Datum value, bool is_speculative);
 extern Datum toast_save_datum(Relation rel, Datum value,
-							  struct varlena *oldexternal, int options);
+							  struct varlena *oldexternal, int options,
+							  char cmethod);
 
 extern int	toast_open_indexes(Relation toastrel,
 							   LOCKMODE lock,
diff --git a/src/include/varatt.h b/src/include/varatt.h
index aeeabf9145b..5f5829a1ec4 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -45,6 +45,23 @@ typedef struct varatt_external
 #define VARLENA_EXTSIZE_BITS	30
 #define VARLENA_EXTSIZE_MASK	((1U << VARLENA_EXTSIZE_BITS) - 1)
 
+/*
+ * Compression method ID stored in the 2 high-order bits of va_extinfo.
+ * Value 3 indicates an extended TOAST pointer format (varatt_external_extended).
+ * This constant is also defined in toast_compression.h for use by TOAST code.
+ */
+#define VARATT_EXTERNAL_EXTENDED_CMID	3
+
+/*
+ * Feature flags for extended on-disk TOAST pointers (varatt_external_extended).
+ *
+ * Keep these in varatt.h (not access/toast headers) so low-level code can
+ * safely manipulate the on-disk representation without depending on higher
+ * layers' header include order.
+ */
+#define VARATT_EXTERNAL_FLAG_COMPRESSION	0x01	/* va_data[0] = method ID */
+#define VARATT_EXTERNAL_FLAG_CHECKSUM		0x02	/* va_data[1-2] = checksum */
+
 /*
  * struct varatt_indirect is a "TOAST pointer" representing an out-of-line
  * Datum that's stored in memory, not in an external toast relation.
@@ -76,6 +93,26 @@ typedef struct varatt_expanded
 	ExpandedObjectHeader *eohptr;
 } varatt_expanded;
 
+/*
+ * Extended TOAST pointer, extending varatt_external from 16 to 20 bytes.
+ *
+ * Identified by compression method ID 3 in va_extinfo bits 30-31.  The
+ * va_flags field indicates which optional features are enabled; va_data[]
+ * contains feature-specific data (e.g., compression method in va_data[0]).
+ *
+ * Like varatt_external, stored unaligned and requires memcpy for access.
+ */
+typedef struct varatt_external_extended
+{
+	int32		va_rawsize;		/* Original data size (includes header) */
+	uint32		va_extinfo;		/* External saved size (30 bits) + extended
+								 * indicator (2 bits, value = 3) */
+	uint8		va_flags;		/* Feature flags indicating enabled extensions */
+	uint8		va_data[3];		/* Extension data - interpretation depends on flags */
+	Oid			va_valueid;		/* Unique ID of value within TOAST table */
+	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
+}			varatt_external_extended;
+
 /*
  * Type tag for the various sorts of "TOAST pointer" datums.  The peculiar
  * value for VARTAG_ONDISK comes from a requirement for on-disk compatibility
@@ -86,7 +123,17 @@ typedef enum vartag_external
 	VARTAG_INDIRECT = 1,
 	VARTAG_EXPANDED_RO = 2,
 	VARTAG_EXPANDED_RW = 3,
-	VARTAG_ONDISK = 18
+	VARTAG_ONDISK = 18,
+
+	/*
+	 * VARTAG_ONDISK_EXTENDED is used for the extended TOAST pointer format,
+	 * which increases the on-disk payload from 16 to 20 bytes.  The first
+	 * 8 bytes (va_rawsize, va_extinfo) are layout-compatible with
+	 * struct varatt_external so that existing code inspecting those fields
+	 * continues to work.  Older PostgreSQL versions do not know about this
+	 * tag and therefore must not be used to read clusters that contain it.
+	 */
+	VARTAG_ONDISK_EXTENDED = 19
 } vartag_external;
 
 /* Is a TOAST pointer either type of expanded-object pointer? */
@@ -97,7 +144,14 @@ VARTAG_IS_EXPANDED(vartag_external tag)
 	return ((tag & ~1) == VARTAG_EXPANDED_RO);
 }
 
-/* Size of the data part of a "TOAST pointer" datum */
+/*
+ * Size of the data part of a "TOAST pointer" datum.
+ *
+ * For on-disk TOAST pointers we now support two payload sizes:
+ * the original 16-byte format (VARTAG_ONDISK) described by struct
+ * varatt_external, and a 20-byte extended format
+ * (VARTAG_ONDISK_EXTENDED) described by struct varatt_external_extended.
+ */
 static inline Size
 VARTAG_SIZE(vartag_external tag)
 {
@@ -107,6 +161,8 @@ VARTAG_SIZE(vartag_external tag)
 		return sizeof(varatt_expanded);
 	else if (tag == VARTAG_ONDISK)
 		return sizeof(varatt_external);
+	else if (tag == VARTAG_ONDISK_EXTENDED)
+		return sizeof(varatt_external_extended);
 	else
 	{
 		Assert(false);
@@ -360,7 +416,13 @@ 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;
+	vartag_external tag;
+
+	if (!VARATT_IS_EXTERNAL(PTR))
+		return false;
+
+	tag = VARTAG_EXTERNAL(PTR);
+	return tag == VARTAG_ONDISK || tag == VARTAG_ONDISK_EXTENDED;
 }
 
 /* Is varlena datum an indirect pointer? */
@@ -516,11 +578,11 @@ VARATT_EXTERNAL_GET_COMPRESS_METHOD(struct varatt_external toast_pointer)
 }
 
 /* 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) \
 	do { \
 		Assert((cm) == TOAST_PGLZ_COMPRESSION_ID || \
-			   (cm) == TOAST_LZ4_COMPRESSION_ID); \
+			   (cm) == TOAST_LZ4_COMPRESSION_ID || \
+			   (cm) == VARATT_EXTERNAL_EXTENDED_CMID); \
 		((toast_pointer).va_extinfo = \
 			(len) | ((uint32) (cm) << VARLENA_EXTSIZE_BITS)); \
 	} while (0)
@@ -539,4 +601,92 @@ VARATT_EXTERNAL_IS_COMPRESSED(struct varatt_external toast_pointer)
 		(Size) (toast_pointer.va_rawsize - VARHDRSZ);
 }
 
+/* Macros for extended TOAST pointers (varatt_external_extended) */
+
+/*
+ * Check if a TOAST pointer uses the extended on-disk format.
+ *
+ * Callers must have already verified VARATT_IS_EXTERNAL_ONDISK() before
+ * calling this; here we look only at the compression-method bits embedded
+ * in va_extinfo.
+ */
+static inline bool
+VARATT_EXTERNAL_IS_EXTENDED(struct varatt_external toast_pointer)
+{
+	return VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) ==
+		VARATT_EXTERNAL_EXTENDED_CMID;
+}
+
+/* Get feature flags from extended pointer */
+static inline uint8
+VARATT_EXTERNAL_GET_FLAGS(struct varatt_external_extended toast_pointer_ext)
+{
+	return toast_pointer_ext.va_flags;
+}
+
+/* Set feature flags in extended pointer */
+#define VARATT_EXTERNAL_SET_FLAGS(toast_pointer_ext, flags) \
+	do { \
+		(toast_pointer_ext).va_flags = (flags); \
+	} while (0)
+
+/* Test if a specific flag is set */
+#define VARATT_EXTERNAL_HAS_FLAG(toast_pointer_ext, flag) \
+	(((toast_pointer_ext).va_flags & (flag)) != 0)
+
+/* Get pointer to extension data array */
+#define VARATT_EXTERNAL_GET_EXT_DATA(toast_pointer_ext) \
+	((toast_pointer_ext).va_data)
+
+/* Get extended compression method (when TOAST_EXT_FLAG_COMPRESSION is set) */
+static inline uint8
+VARATT_EXTERNAL_GET_EXT_COMPRESSION_METHOD(struct varatt_external_extended toast_pointer_ext)
+{
+	return toast_pointer_ext.va_data[0];
+}
+
+/* Set extended compression method */
+#define VARATT_EXTERNAL_SET_EXT_COMPRESSION_METHOD(toast_pointer_ext, method) \
+	do { \
+		(toast_pointer_ext).va_data[0] = (method); \
+	} while (0)
+
+/* Get extsize and compress method from extended pointer (same as standard) */
+static inline Size
+VARATT_EXTERNAL_GET_EXTSIZE_EXTENDED(struct varatt_external_extended toast_pointer_ext)
+{
+	return toast_pointer_ext.va_extinfo & VARLENA_EXTSIZE_MASK;
+}
+
+static inline uint32
+VARATT_EXTERNAL_GET_COMPRESS_METHOD_EXTENDED(struct varatt_external_extended toast_pointer_ext)
+{
+	return toast_pointer_ext.va_extinfo >> VARLENA_EXTSIZE_BITS;
+}
+
+/* Set size and extended indicator in va_extinfo */
+#define VARATT_EXTERNAL_SET_SIZE_AND_EXT_FLAGS(toast_pointer_ext, len, flags) \
+	do { \
+		Assert((len) > 0 && (len) <= VARLENA_EXTSIZE_MASK); \
+		(toast_pointer_ext).va_extinfo = \
+			(len) | ((uint32) VARATT_EXTERNAL_EXTENDED_CMID << VARLENA_EXTSIZE_BITS); \
+		(toast_pointer_ext).va_flags = (flags); \
+		memset((toast_pointer_ext).va_data, 0, 3); \
+	} while (0)
+
+/* Convenience macro for setting extended pointer with compression method */
+#define VARATT_EXTERNAL_SET_SIZE_AND_EXT_COMPRESSION(toast_pointer_ext, len, method) \
+	do { \
+		VARATT_EXTERNAL_SET_SIZE_AND_EXT_FLAGS(toast_pointer_ext, len, VARATT_EXTERNAL_FLAG_COMPRESSION); \
+		VARATT_EXTERNAL_SET_EXT_COMPRESSION_METHOD(toast_pointer_ext, method); \
+	} while (0)
+
+/* Test if extended pointer is compressed (same logic as standard) */
+static inline bool
+VARATT_EXTERNAL_IS_COMPRESSED_EXTENDED(struct varatt_external_extended toast_pointer_ext)
+{
+	return VARATT_EXTERNAL_GET_EXTSIZE_EXTENDED(toast_pointer_ext) <
+		(Size) (toast_pointer_ext.va_rawsize - VARHDRSZ);
+}
+
 #endif
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 068fd859a8f..9dff119aa22 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -47,6 +47,7 @@ subdir('test_rls_hooks')
 subdir('test_shm_mq')
 subdir('test_slru')
 subdir('test_tidstore')
+subdir('test_toast_ext')
 subdir('typcache')
 subdir('unsafe_tests')
 subdir('worker_spi')
diff --git a/src/test/modules/test_toast_ext/Makefile b/src/test/modules/test_toast_ext/Makefile
new file mode 100644
index 00000000000..5e2409f918c
--- /dev/null
+++ b/src/test/modules/test_toast_ext/Makefile
@@ -0,0 +1,20 @@
+# src/test/modules/test_toast_ext/Makefile
+
+MODULE_big = test_toast_ext
+OBJS = test_toast_ext.o
+
+EXTENSION = test_toast_ext
+DATA = test_toast_ext--1.0.sql
+
+REGRESS = test_toast_ext
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_toast_ext
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_toast_ext/expected/test_toast_ext.out b/src/test/modules/test_toast_ext/expected/test_toast_ext.out
new file mode 100644
index 00000000000..539f4437655
--- /dev/null
+++ b/src/test/modules/test_toast_ext/expected/test_toast_ext.out
@@ -0,0 +1,187 @@
+--
+-- Tests for extended TOAST header structures and zstd compression
+--
+CREATE EXTENSION test_toast_ext;
+-- Use dedicated schema for test isolation
+CREATE SCHEMA test_toast_ext_schema;
+SET search_path TO test_toast_ext_schema, public;
+-- Compile-time validation tests (always run)
+-- These error out on failure, so completing without error = pass
+SELECT test_toast_structure_sizes();
+ test_toast_structure_sizes
+----------------------------
+
+(1 row)
+
+SELECT test_toast_flag_validation();
+ test_toast_flag_validation
+----------------------------
+
+(1 row)
+
+SELECT test_toast_compression_ids();
+ test_toast_compression_ids
+----------------------------
+
+(1 row)
+
+--
+-- Functional tests for zstd TOAST compression
+-- Skip if not built with USE_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
+-- Test basic zstd compression
+CREATE TABLE test_zstd_basic (id serial, data text COMPRESSION zstd);
+INSERT INTO test_zstd_basic (data)
+    VALUES (repeat('PostgreSQL zstd TOAST compression test. ', 3000));
+SELECT id,
+       pg_column_compression(data) AS compression,
+       length(data) AS data_length,
+       left(data, 42) AS data_prefix
+FROM test_zstd_basic;
+ id | compression | data_length |                data_prefix
+----+-------------+-------------+--------------------------------------------
+  1 | zstd        |      120000 | PostgreSQL zstd TOAST compression test. Po
+(1 row)
+
+-- Test slice access
+SELECT id, substr(data, 100, 42) AS slice FROM test_zstd_basic;
+ id |                   slice
+----+--------------------------------------------
+  1 | ST compression test. PostgreSQL zstd TOAST
+(1 row)
+
+-- Test UPDATE
+UPDATE test_zstd_basic SET data = repeat('Updated zstd data for TOAST test. ', 3000);
+SELECT id,
+       pg_column_compression(data) AS compression,
+       length(data) AS data_length,
+       left(data, 35) AS data_prefix
+FROM test_zstd_basic;
+ id | compression | data_length |             data_prefix
+----+-------------+-------------+-------------------------------------
+  1 | zstd        |      102000 | Updated zstd data for TOAST test. U
+(1 row)
+
+-- Test extended header with pglz
+SET use_extended_toast_header = on;
+CREATE TABLE test_pglz_extended (data text COMPRESSION pglz);
+INSERT INTO test_pglz_extended (data)
+    VALUES (repeat('PGLZ with extended header format. ', 3000));
+SELECT pg_column_compression(data) AS compression,
+       length(data) AS data_length
+FROM test_pglz_extended;
+ compression | data_length
+-------------+-------------
+ pglz        |      102000
+(1 row)
+
+SELECT substr(data, 50, 34) AS slice FROM test_pglz_extended;
+               slice
+------------------------------------
+ ded header format. PGLZ with exten
+(1 row)
+
+-- Test data integrity
+CREATE TABLE test_integrity (
+    method text,
+    original_data text,
+    compressed_data text
+);
+INSERT INTO test_integrity VALUES
+    ('pglz', repeat('Integrity test data pattern. ', 2000), NULL),
+    ('zstd', repeat('Integrity test data pattern. ', 2000), NULL);
+CREATE TABLE test_pglz_integrity (data text COMPRESSION pglz);
+CREATE TABLE test_zstd_integrity (data text COMPRESSION zstd);
+INSERT INTO test_pglz_integrity SELECT original_data FROM test_integrity WHERE method = 'pglz';
+INSERT INTO test_zstd_integrity SELECT original_data FROM test_integrity WHERE method = 'zstd';
+SELECT 'pglz' AS method,
+       md5((SELECT original_data FROM test_integrity WHERE method = 'pglz')) =
+       md5((SELECT data FROM test_pglz_integrity)) AS checksum_match;
+ method | checksum_match
+--------+----------------
+ pglz   | t
+(1 row)
+
+SELECT 'zstd' AS method,
+       md5((SELECT original_data FROM test_integrity WHERE method = 'zstd')) =
+       md5((SELECT data FROM test_zstd_integrity)) AS checksum_match;
+ method | checksum_match
+--------+----------------
+ zstd   | t
+(1 row)
+
+-- Test CLUSTER and VACUUM FULL
+CREATE TABLE test_cluster_zstd (id serial PRIMARY KEY, data text COMPRESSION zstd);
+INSERT INTO test_cluster_zstd (data)
+    VALUES (repeat('Data for CLUSTER test with zstd compression. ', 2500));
+SELECT 'before_cluster' AS stage, md5(data) AS hash FROM test_cluster_zstd;
+     stage      |               hash
+----------------+----------------------------------
+ before_cluster | b4132e799bbd065a7e9266159aa82dc1
+(1 row)
+
+CLUSTER test_cluster_zstd USING test_cluster_zstd_pkey;
+SELECT 'after_cluster' AS stage,
+       pg_column_compression(data) AS compression,
+       md5(data) AS hash
+FROM test_cluster_zstd;
+     stage     | compression |               hash
+---------------+-------------+----------------------------------
+ after_cluster | zstd        | b4132e799bbd065a7e9266159aa82dc1
+(1 row)
+
+VACUUM FULL test_cluster_zstd;
+SELECT 'after_vacuum_full' AS stage,
+       pg_column_compression(data) AS compression,
+       md5(data) AS hash
+FROM test_cluster_zstd;
+       stage       | compression |               hash
+-------------------+-------------+----------------------------------
+ after_vacuum_full | zstd        | b4132e799bbd065a7e9266159aa82dc1
+(1 row)
+
+-- Test GUC toggling (mixed formats in same table)
+SET use_extended_toast_header = on;
+CREATE TABLE test_guc_toggle (id serial, data text COMPRESSION pglz);
+INSERT INTO test_guc_toggle (data)
+    VALUES (repeat('Data created with extended header on. ', 3000));
+SELECT 'with_ext_on' AS stage,
+       pg_column_compression(data) AS compression,
+       length(data) AS data_length
+FROM test_guc_toggle;
+    stage    | compression | data_length
+-------------+-------------+-------------
+ with_ext_on | pglz        |      114000
+(1 row)
+
+SET use_extended_toast_header = off;
+INSERT INTO test_guc_toggle (data)
+    VALUES (repeat('Data created with extended header off. ', 3000));
+SELECT id,
+       pg_column_compression(data) AS compression,
+       length(data) AS data_length,
+       left(data, 39) AS data_prefix
+FROM test_guc_toggle ORDER BY id;
+ id | compression | data_length |               data_prefix
+----+-------------+-------------+-----------------------------------------
+  1 | pglz        |      114000 | Data created with extended header on. D
+  2 | pglz        |      117000 | Data created with extended header off.
+(2 rows)
+
+SET use_extended_toast_header = on;
+SELECT id, length(data) AS data_length FROM test_guc_toggle ORDER BY id;
+ id | data_length
+----+-------------
+  1 |      114000
+  2 |      117000
+(2 rows)
+
+-- Cleanup
+DROP SCHEMA test_toast_ext_schema CASCADE;
+DROP EXTENSION test_toast_ext;
diff --git a/src/test/modules/test_toast_ext/expected/test_toast_ext_1.out b/src/test/modules/test_toast_ext/expected/test_toast_ext_1.out
new file mode 100644
index 00000000000..897661fc2a4
--- /dev/null
+++ b/src/test/modules/test_toast_ext/expected/test_toast_ext_1.out
@@ -0,0 +1,37 @@
+--
+-- Tests for extended TOAST header structures and zstd compression
+--
+CREATE EXTENSION test_toast_ext;
+-- Use dedicated schema for test isolation
+CREATE SCHEMA test_toast_ext_schema;
+SET search_path TO test_toast_ext_schema, public;
+-- Compile-time validation tests (always run)
+-- These error out on failure, so completing without error = pass
+SELECT test_toast_structure_sizes();
+ test_toast_structure_sizes
+----------------------------
+
+(1 row)
+
+SELECT test_toast_flag_validation();
+ test_toast_flag_validation
+----------------------------
+
+(1 row)
+
+SELECT test_toast_compression_ids();
+ test_toast_compression_ids
+----------------------------
+
+(1 row)
+
+--
+-- Functional tests for zstd TOAST compression
+-- Skip if not built with USE_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/modules/test_toast_ext/meson.build b/src/test/modules/test_toast_ext/meson.build
new file mode 100644
index 00000000000..61c07ea1912
--- /dev/null
+++ b/src/test/modules/test_toast_ext/meson.build
@@ -0,0 +1,33 @@
+# Copyright (c) 2022-2025, PostgreSQL Global Development Group
+
+test_toast_ext_sources = files(
+  'test_toast_ext.c',
+)
+
+if host_system == 'windows'
+  test_toast_ext_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_toast_ext',
+    '--FILEDESC', 'test_toast_ext - test code for extended TOAST headers',])
+endif
+
+test_toast_ext = shared_module('test_toast_ext',
+  test_toast_ext_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_toast_ext
+
+test_install_data += files(
+  'test_toast_ext.control',
+  'test_toast_ext--1.0.sql',
+)
+
+tests += {
+  'name': 'test_toast_ext',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'regress': {
+    'sql': [
+      'test_toast_ext',
+    ],
+  },
+}
diff --git a/src/test/modules/test_toast_ext/sql/test_toast_ext.sql b/src/test/modules/test_toast_ext/sql/test_toast_ext.sql
new file mode 100644
index 00000000000..82e36c57b34
--- /dev/null
+++ b/src/test/modules/test_toast_ext/sql/test_toast_ext.sql
@@ -0,0 +1,136 @@
+--
+-- Tests for extended TOAST header structures and zstd compression
+--
+
+CREATE EXTENSION test_toast_ext;
+
+-- Use dedicated schema for test isolation
+CREATE SCHEMA test_toast_ext_schema;
+SET search_path TO test_toast_ext_schema, public;
+
+-- Compile-time validation tests (always run)
+-- These error out on failure, so completing without error = pass
+SELECT test_toast_structure_sizes();
+SELECT test_toast_flag_validation();
+SELECT test_toast_compression_ids();
+
+--
+-- Functional tests for zstd TOAST compression
+-- Skip if not built with USE_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
+
+-- Test basic zstd compression
+CREATE TABLE test_zstd_basic (id serial, data text COMPRESSION zstd);
+INSERT INTO test_zstd_basic (data)
+    VALUES (repeat('PostgreSQL zstd TOAST compression test. ', 3000));
+
+SELECT id,
+       pg_column_compression(data) AS compression,
+       length(data) AS data_length,
+       left(data, 42) AS data_prefix
+FROM test_zstd_basic;
+
+-- Test slice access
+SELECT id, substr(data, 100, 42) AS slice FROM test_zstd_basic;
+
+-- Test UPDATE
+UPDATE test_zstd_basic SET data = repeat('Updated zstd data for TOAST test. ', 3000);
+SELECT id,
+       pg_column_compression(data) AS compression,
+       length(data) AS data_length,
+       left(data, 35) AS data_prefix
+FROM test_zstd_basic;
+
+-- Test extended header with pglz
+SET use_extended_toast_header = on;
+
+CREATE TABLE test_pglz_extended (data text COMPRESSION pglz);
+INSERT INTO test_pglz_extended (data)
+    VALUES (repeat('PGLZ with extended header format. ', 3000));
+
+SELECT pg_column_compression(data) AS compression,
+       length(data) AS data_length
+FROM test_pglz_extended;
+
+SELECT substr(data, 50, 34) AS slice FROM test_pglz_extended;
+
+-- Test data integrity
+CREATE TABLE test_integrity (
+    method text,
+    original_data text,
+    compressed_data text
+);
+
+INSERT INTO test_integrity VALUES
+    ('pglz', repeat('Integrity test data pattern. ', 2000), NULL),
+    ('zstd', repeat('Integrity test data pattern. ', 2000), NULL);
+
+CREATE TABLE test_pglz_integrity (data text COMPRESSION pglz);
+CREATE TABLE test_zstd_integrity (data text COMPRESSION zstd);
+
+INSERT INTO test_pglz_integrity SELECT original_data FROM test_integrity WHERE method = 'pglz';
+INSERT INTO test_zstd_integrity SELECT original_data FROM test_integrity WHERE method = 'zstd';
+
+SELECT 'pglz' AS method,
+       md5((SELECT original_data FROM test_integrity WHERE method = 'pglz')) =
+       md5((SELECT data FROM test_pglz_integrity)) AS checksum_match;
+
+SELECT 'zstd' AS method,
+       md5((SELECT original_data FROM test_integrity WHERE method = 'zstd')) =
+       md5((SELECT data FROM test_zstd_integrity)) AS checksum_match;
+
+-- Test CLUSTER and VACUUM FULL
+CREATE TABLE test_cluster_zstd (id serial PRIMARY KEY, data text COMPRESSION zstd);
+INSERT INTO test_cluster_zstd (data)
+    VALUES (repeat('Data for CLUSTER test with zstd compression. ', 2500));
+
+SELECT 'before_cluster' AS stage, md5(data) AS hash FROM test_cluster_zstd;
+
+CLUSTER test_cluster_zstd USING test_cluster_zstd_pkey;
+
+SELECT 'after_cluster' AS stage,
+       pg_column_compression(data) AS compression,
+       md5(data) AS hash
+FROM test_cluster_zstd;
+
+VACUUM FULL test_cluster_zstd;
+
+SELECT 'after_vacuum_full' AS stage,
+       pg_column_compression(data) AS compression,
+       md5(data) AS hash
+FROM test_cluster_zstd;
+
+-- Test GUC toggling (mixed formats in same table)
+SET use_extended_toast_header = on;
+CREATE TABLE test_guc_toggle (id serial, data text COMPRESSION pglz);
+INSERT INTO test_guc_toggle (data)
+    VALUES (repeat('Data created with extended header on. ', 3000));
+
+SELECT 'with_ext_on' AS stage,
+       pg_column_compression(data) AS compression,
+       length(data) AS data_length
+FROM test_guc_toggle;
+
+SET use_extended_toast_header = off;
+INSERT INTO test_guc_toggle (data)
+    VALUES (repeat('Data created with extended header off. ', 3000));
+
+SELECT id,
+       pg_column_compression(data) AS compression,
+       length(data) AS data_length,
+       left(data, 39) AS data_prefix
+FROM test_guc_toggle ORDER BY id;
+
+SET use_extended_toast_header = on;
+SELECT id, length(data) AS data_length FROM test_guc_toggle ORDER BY id;
+
+-- Cleanup
+DROP SCHEMA test_toast_ext_schema CASCADE;
+DROP EXTENSION test_toast_ext;
diff --git a/src/test/modules/test_toast_ext/test_toast_ext--1.0.sql b/src/test/modules/test_toast_ext/test_toast_ext--1.0.sql
new file mode 100644
index 00000000000..f74d5069fbf
--- /dev/null
+++ b/src/test/modules/test_toast_ext/test_toast_ext--1.0.sql
@@ -0,0 +1,19 @@
+/* src/test/modules/test_toast_ext/test_toast_ext--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_toast_ext" to load this file. \quit
+
+CREATE FUNCTION test_toast_structure_sizes()
+RETURNS void
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_toast_flag_validation()
+RETURNS void
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_toast_compression_ids()
+RETURNS void
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT;
diff --git a/src/test/modules/test_toast_ext/test_toast_ext.c b/src/test/modules/test_toast_ext/test_toast_ext.c
new file mode 100644
index 00000000000..59884f2b6d0
--- /dev/null
+++ b/src/test/modules/test_toast_ext/test_toast_ext.c
@@ -0,0 +1,140 @@
+/*-------------------------------------------------------------------------
+ *
+ * test_toast_ext.c
+ *		Test module for extended TOAST header structures.
+ *
+ * Copyright (c) 2025, PostgreSQL Global Development Group
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "access/detoast.h"
+#include "access/toast_compression.h"
+#include "varatt.h"
+
+PG_MODULE_MAGIC;
+
+PG_FUNCTION_INFO_V1(test_toast_structure_sizes);
+PG_FUNCTION_INFO_V1(test_toast_flag_validation);
+PG_FUNCTION_INFO_V1(test_toast_compression_ids);
+
+/*
+ * Verify TOAST structure sizes match expected values.
+ * Errors out if any size is wrong (catches ABI issues).
+ */
+Datum
+test_toast_structure_sizes(PG_FUNCTION_ARGS)
+{
+	/* Standard structure must be 16 bytes */
+	if (sizeof(varatt_external) != 16)
+		elog(ERROR, "varatt_external is %zu bytes, expected 16",
+			 sizeof(varatt_external));
+
+	/* Extended structure must be 20 bytes */
+	if (sizeof(varatt_external_extended) != 20)
+		elog(ERROR, "varatt_external_extended is %zu bytes, expected 20",
+			 sizeof(varatt_external_extended));
+
+	/* TOAST pointer sizes (include 2-byte external header) */
+	if (TOAST_POINTER_SIZE != 18)
+		elog(ERROR, "TOAST_POINTER_SIZE is %zu, expected 18",
+			 (Size) TOAST_POINTER_SIZE);
+
+	if (TOAST_POINTER_SIZE_EXTENDED != 22)
+		elog(ERROR, "TOAST_POINTER_SIZE_EXTENDED is %zu, expected 22",
+			 (Size) TOAST_POINTER_SIZE_EXTENDED);
+
+	/* Verify field offsets (no unexpected padding) */
+	if (offsetof(varatt_external_extended, va_rawsize) != 0)
+		elog(ERROR, "va_rawsize offset is %zu, expected 0",
+			 offsetof(varatt_external_extended, va_rawsize));
+	if (offsetof(varatt_external_extended, va_extinfo) != 4)
+		elog(ERROR, "va_extinfo offset is %zu, expected 4",
+			 offsetof(varatt_external_extended, va_extinfo));
+	if (offsetof(varatt_external_extended, va_flags) != 8)
+		elog(ERROR, "va_flags offset is %zu, expected 8",
+			 offsetof(varatt_external_extended, va_flags));
+	if (offsetof(varatt_external_extended, va_data) != 9)
+		elog(ERROR, "va_data offset is %zu, expected 9",
+			 offsetof(varatt_external_extended, va_data));
+	if (offsetof(varatt_external_extended, va_valueid) != 12)
+		elog(ERROR, "va_valueid offset is %zu, expected 12",
+			 offsetof(varatt_external_extended, va_valueid));
+	if (offsetof(varatt_external_extended, va_toastrelid) != 16)
+		elog(ERROR, "va_toastrelid offset is %zu, expected 16",
+			 offsetof(varatt_external_extended, va_toastrelid));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Verify flag validation macros work correctly.
+ */
+Datum
+test_toast_flag_validation(PG_FUNCTION_ARGS)
+{
+	/* Valid flags should pass */
+	if (!ExtendedFlagsAreValid(0x00))
+		elog(ERROR, "flags 0x00 should be valid");
+	if (!ExtendedFlagsAreValid(0x01))
+		elog(ERROR, "flags 0x01 should be valid");
+	if (!ExtendedFlagsAreValid(0x02))
+		elog(ERROR, "flags 0x02 should be valid");
+	if (!ExtendedFlagsAreValid(0x03))
+		elog(ERROR, "flags 0x03 should be valid");
+
+	/* Invalid flags should fail */
+	if (ExtendedFlagsAreValid(0x04))
+		elog(ERROR, "flags 0x04 should be invalid");
+	if (ExtendedFlagsAreValid(0x08))
+		elog(ERROR, "flags 0x08 should be invalid");
+	if (ExtendedFlagsAreValid(0xFF))
+		elog(ERROR, "flags 0xFF should be invalid");
+
+	/* Compression methods 0-255 are valid */
+	if (!ExtendedCompressionMethodIsValid(0))
+		elog(ERROR, "compression method 0 should be valid");
+	if (!ExtendedCompressionMethodIsValid(255))
+		elog(ERROR, "compression method 255 should be valid");
+
+	/* Verify method ID constants */
+	if (TOAST_PGLZ_EXT_METHOD != 0)
+		elog(ERROR, "TOAST_PGLZ_EXT_METHOD is %d, expected 0", TOAST_PGLZ_EXT_METHOD);
+	if (TOAST_LZ4_EXT_METHOD != 1)
+		elog(ERROR, "TOAST_LZ4_EXT_METHOD is %d, expected 1", TOAST_LZ4_EXT_METHOD);
+	if (TOAST_ZSTD_EXT_METHOD != 2)
+		elog(ERROR, "TOAST_ZSTD_EXT_METHOD is %d, expected 2", TOAST_ZSTD_EXT_METHOD);
+	if (TOAST_UNCOMPRESSED_EXT_METHOD != 3)
+		elog(ERROR, "TOAST_UNCOMPRESSED_EXT_METHOD is %d, expected 3", TOAST_UNCOMPRESSED_EXT_METHOD);
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Verify compression ID constants are consistent.
+ */
+Datum
+test_toast_compression_ids(PG_FUNCTION_ARGS)
+{
+	/* Standard compression IDs */
+	if (TOAST_PGLZ_COMPRESSION_ID != 0)
+		elog(ERROR, "TOAST_PGLZ_COMPRESSION_ID is %d, expected 0", TOAST_PGLZ_COMPRESSION_ID);
+	if (TOAST_LZ4_COMPRESSION_ID != 1)
+		elog(ERROR, "TOAST_LZ4_COMPRESSION_ID is %d, expected 1", TOAST_LZ4_COMPRESSION_ID);
+	if (TOAST_INVALID_COMPRESSION_ID != 2)
+		elog(ERROR, "TOAST_INVALID_COMPRESSION_ID is %d, expected 2", TOAST_INVALID_COMPRESSION_ID);
+	if (TOAST_EXTENDED_COMPRESSION_ID != 3)
+		elog(ERROR, "TOAST_EXTENDED_COMPRESSION_ID is %d, expected 3", TOAST_EXTENDED_COMPRESSION_ID);
+
+	/* Extended IDs should match standard where applicable */
+	if (TOAST_PGLZ_EXT_METHOD != TOAST_PGLZ_COMPRESSION_ID)
+		elog(ERROR, "PGLZ IDs mismatch: standard=%d, extended=%d",
+			 TOAST_PGLZ_COMPRESSION_ID, TOAST_PGLZ_EXT_METHOD);
+	if (TOAST_LZ4_EXT_METHOD != TOAST_LZ4_COMPRESSION_ID)
+		elog(ERROR, "LZ4 IDs mismatch: standard=%d, extended=%d",
+			 TOAST_LZ4_COMPRESSION_ID, TOAST_LZ4_EXT_METHOD);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/test/modules/test_toast_ext/test_toast_ext.control b/src/test/modules/test_toast_ext/test_toast_ext.control
new file mode 100644
index 00000000000..d59ee14ad64
--- /dev/null
+++ b/src/test/modules/test_toast_ext/test_toast_ext.control
@@ -0,0 +1,5 @@
+# test_toast_ext extension
+comment = 'Test module for extended TOAST headers and zstd compression'
+default_version = '1.0'
+module_pathname = '$libdir/test_toast_ext'
+relocatable = true
-- 
2.39.3 (Apple Git-146)

#4Murtuza Zabuawala
murtuza.zabuawala@enterprisedb.com
In reply to: Dharin Shah (#3)
Re: [PATCH] Add zstd compression for TOAST using extended header format

Hello,

You may want to consider sending the patch to the pgsql-hackers mailing list.

Murtuza Zabuawala
enterprisedb.com <http://enterprisedb.com/&gt;

Show quoted text

On 16 Dec 2025, at 12:46 AM, Dharin Shah <dharinshah95@gmail.com> wrote:

Hello PG Hackers,

Want to submit a patch that implements zstd compression for TOAST data using a 20-byte TOAST pointer format, directly addressing the concerns raised in prior discussions [1 </messages/by-id/CAFAfj_F4qeRCNCYPk1vgH42fDZpjQWKO+ufq3FyoVyUa5AviFA@mail.gmail.com </messages/by-id/CAJ7c6TOtAB0z1UrksvGTStNE-herK-43bj22=5xVBg7S4vr5rQ@mail.gmail.com&gt;%5D%5B3 </messages/by-id/YoMiNmkztrslDbNS@paquier.xyz&gt;%5D.

A bit of a background in the 2022 thread [3 </messages/by-id/YoMiNmkztrslDbNS@paquier.xyz&gt;%5D, The overall suggestion was to have something extensible for the TOAST header

i.e. something like:
00 = PGLZ
01 = LZ4
10 = reserved for future emergencies
11 = extended header with additional type byte

This patch implements that idea.
The new header format:

struct varatt_external_extended {
int32 va_rawsize; /* same as legacy */
uint32 va_extinfo; /* cmid=3 signals extended format */
uint8 va_flags; /* feature flags */
uint8 va_data[3]; /* va_data[0] = compression method */
Oid va_valueid; /* same as legacy */
Oid va_toastrelid; /* same as legacy */
};

A few notes:

- Zstd only applies to external TOAST, not inline compression. The 2-bit limit in va_tcinfo stays as-is for inline data, where pglz/lz4 work fine anyway. Zstd's wins show up on larger values.
- A GUC use_extended_toast_header controls whether pglz/lz4 also use the 20-byte format (defaults to off for compatibility, can enable it if you want consistency).
- Legacy 16-byte pointers continue to work - we check the vartag to determine which format to read.

The 4 extra bytes per pointer is negligible for typical TOAST data sizes, and it gives us room to grow.

Regards,
Dharin
<zstd-toast-compression-external.patch>

#5Dharin Shah
dharinshah95@gmail.com
In reply to: Murtuza Zabuawala (#4)
Re: [PATCH] Add zstd compression for TOAST using extended header format

THanks Murtuza,

My bad, wrong email :(

Regards,
Dharin

On Tue, Dec 16, 2025 at 6:56 AM Murtuza Zabuawala <
murtuza.zabuawala@enterprisedb.com> wrote:

Show quoted text

Hello,

You may want to consider sending the patch to the pgsql-hackers mailing
list.

*Murtuza Zabuawala*
enterprisedb.com

On 16 Dec 2025, at 12:46 AM, Dharin Shah <dharinshah95@gmail.com> wrote:

Hello PG Hackers,

Want to submit a patch that implements zstd compression for TOAST data
using a 20-byte TOAST pointer format, directly addressing the concerns
raised in prior discussions [1
</messages/by-id/CAFAfj_F4qeRCNCYPk1vgH42fDZpjQWKO+ufq3FyoVyUa5AviFA@mail.gmail.com
][2
</messages/by-id/CAJ7c6TOtAB0z1UrksvGTStNE-herK-43bj22=5xVBg7S4vr5rQ@mail.gmail.com&gt;
][3
</messages/by-id/YoMiNmkztrslDbNS@paquier.xyz&gt;
].

A bit of a background in the 2022 thread [3
</messages/by-id/YoMiNmkztrslDbNS@paquier.xyz&gt;%5D,
The overall suggestion was to have something extensible for the TOAST header

i.e. something like:
00 = PGLZ
01 = LZ4
10 = reserved for future emergencies
11 = extended header with additional type byte

This patch implements that idea.
The new header format:

struct varatt_external_extended {
int32 va_rawsize; /* same as legacy */
uint32 va_extinfo; /* cmid=3 signals extended format */
uint8 va_flags; /* feature flags */
uint8 va_data[3]; /* va_data[0] = compression method */
Oid va_valueid; /* same as legacy */
Oid va_toastrelid; /* same as legacy */
};

*A few notes:*

- Zstd only applies to external TOAST, not inline compression. The 2-bit
limit in va_tcinfo stays as-is for inline data, where pglz/lz4 work fine
anyway. Zstd's wins show up on larger values.
- A GUC use_extended_toast_header controls whether pglz/lz4 also use the
20-byte format (defaults to off for compatibility, can enable it if you
want consistency).
- Legacy 16-byte pointers continue to work - we check the vartag to
determine which format to read.

The 4 extra bytes per pointer is negligible for typical TOAST data sizes,
and it gives us room to grow.

Regards,
Dharin
<zstd-toast-compression-external.patch>

#6Dharin Shah
dharinshah95@gmail.com
In reply to: Dharin Shah (#1)
1 attachment(s)
Fwd: [PATCH] Add zstd compression for TOAST using extended header format

Hello PG Hackers,

Want to submit a patch that implements zstd compression for TOAST data
using a 20-byte TOAST pointer format, directly addressing the concerns
raised in prior discussions [1
</messages/by-id/CAFAfj_F4qeRCNCYPk1vgH42fDZpjQWKO+ufq3FyoVyUa5AviFA@mail.gmail.com
][2
</messages/by-id/CAJ7c6TOtAB0z1UrksvGTStNE-herK-43bj22=5xVBg7S4vr5rQ@mail.gmail.com&gt;
][3
</messages/by-id/YoMiNmkztrslDbNS@paquier.xyz&gt;%5D.

A bit of a background in the 2022 thread [3
</messages/by-id/YoMiNmkztrslDbNS@paquier.xyz&gt;%5D,
The overall suggestion was to have something extensible for the TOAST header

i.e. something like:
00 = PGLZ
01 = LZ4
10 = reserved for future emergencies
11 = extended header with additional type byte

This patch implements that idea.
The new header format:

struct varatt_external_extended {
int32 va_rawsize; /* same as legacy */
uint32 va_extinfo; /* cmid=3 signals extended format */
uint8 va_flags; /* feature flags */
uint8 va_data[3]; /* va_data[0] = compression method */
Oid va_valueid; /* same as legacy */
Oid va_toastrelid; /* same as legacy */
};

*A few notes:*

- Zstd only applies to external TOAST, not inline compression. The 2-bit
limit in va_tcinfo stays as-is for inline data, where pglz/lz4 work fine
anyway. Zstd's wins show up on larger values.
- A GUC use_extended_toast_header controls whether pglz/lz4 also use the
20-byte format (defaults to off for compatibility, can enable it if you
want consistency).
- Legacy 16-byte pointers continue to work - we check the vartag to
determine which format to read.

The 4 extra bytes per pointer is negligible for typical TOAST data sizes,
and it gives us room to grow.

Regards,
Dharin

Attachments:

zstd-toast-compression-external.patchapplication/octet-stream; name=zstd-toast-compression-external.patchDownload
From fdaae5dc9e9837f73b991100adcba6d76dda1f40 Mon Sep 17 00:00:00 2001
From: Dharin Shah <8616130+Dharin-shah@users.noreply.github.com>
Date: Sat, 13 Dec 2025 11:16:35 +0100
Subject: [PATCH] Add zstd compression support for TOAST using extended header
 format

---
 contrib/amcheck/verify_heapam.c               |  69 +++++-
 src/backend/access/common/detoast.c           | 164 ++++++++++++---
 src/backend/access/common/toast_compression.c | 199 +++++++++++++++++-
 src/backend/access/common/toast_internals.c   | 198 +++++++++++++++--
 src/backend/access/table/toast_helper.c       |   2 +-
 .../replication/logical/reorderbuffer.c       |  38 +++-
 src/backend/utils/adt/varlena.c               |  26 ++-
 src/backend/utils/misc/guc_parameters.dat     |   7 +-
 src/backend/utils/misc/guc_tables.c           |   3 +
 src/include/access/detoast.h                  |  41 +++-
 src/include/access/toast_compression.h        |  36 ++++
 src/include/access/toast_internals.h          |  10 +-
 src/include/varatt.h                          | 160 +++++++++++++-
 src/test/modules/meson.build                  |   1 +
 src/test/modules/test_toast_ext/Makefile      |  20 ++
 .../expected/test_toast_ext.out               | 187 ++++++++++++++++
 .../expected/test_toast_ext_1.out             |  37 ++++
 src/test/modules/test_toast_ext/meson.build   |  33 +++
 .../test_toast_ext/sql/test_toast_ext.sql     | 136 ++++++++++++
 .../test_toast_ext/test_toast_ext--1.0.sql    |  19 ++
 .../modules/test_toast_ext/test_toast_ext.c   | 140 ++++++++++++
 .../test_toast_ext/test_toast_ext.control     |   5 +
 22 files changed, 1440 insertions(+), 91 deletions(-)
 create mode 100644 src/test/modules/test_toast_ext/Makefile
 create mode 100644 src/test/modules/test_toast_ext/expected/test_toast_ext.out
 create mode 100644 src/test/modules/test_toast_ext/expected/test_toast_ext_1.out
 create mode 100644 src/test/modules/test_toast_ext/meson.build
 create mode 100644 src/test/modules/test_toast_ext/sql/test_toast_ext.sql
 create mode 100644 src/test/modules/test_toast_ext/test_toast_ext--1.0.sql
 create mode 100644 src/test/modules/test_toast_ext/test_toast_ext.c
 create mode 100644 src/test/modules/test_toast_ext/test_toast_ext.control

diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 130b3533463..25cae4d0380 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1665,6 +1665,8 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	uint16		infomask;
 	CompactAttribute *thisatt;
 	struct varatt_external toast_pointer;
+	struct varatt_external_extended toast_pointer_ext;
+	bool		is_extended;
 
 	infomask = ctx->tuphdr->t_infomask;
 	thisatt = TupleDescCompactAttr(RelationGetDescr(ctx->rel), ctx->attnum);
@@ -1717,13 +1719,14 @@ check_tuple_attribute(HeapCheckContext *ctx)
 
 	/*
 	 * Check that VARTAG_SIZE won't hit an Assert on a corrupt va_tag before
-	 * risking a call into att_addlength_pointer
+	 * risking a call into att_addlength_pointer.  Both legacy (VARTAG_ONDISK)
+	 * and extended (VARTAG_ONDISK_EXTENDED) on-disk formats are valid.
 	 */
 	if (VARATT_IS_EXTERNAL(tp + ctx->offset))
 	{
 		uint8		va_tag = VARTAG_EXTERNAL(tp + ctx->offset);
 
-		if (va_tag != VARTAG_ONDISK)
+		if (va_tag != VARTAG_ONDISK && va_tag != VARTAG_ONDISK_EXTENDED)
 		{
 			report_corruption(ctx,
 							  psprintf("toasted attribute has unexpected TOAST tag %u",
@@ -1768,9 +1771,23 @@ check_tuple_attribute(HeapCheckContext *ctx)
 	/* It is external, and we're looking at a page on disk */
 
 	/*
-	 * Must copy attr into toast_pointer for alignment considerations
+	 * Must copy attr into toast_pointer for alignment considerations.
+	 * Handle both legacy (VARTAG_ONDISK) and extended (VARTAG_ONDISK_EXTENDED)
+	 * formats.
 	 */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	is_extended = (VARTAG_EXTERNAL(attr) == VARTAG_ONDISK_EXTENDED);
+
+	if (is_extended)
+	{
+		VARATT_EXTERNAL_GET_POINTER_EXTENDED(toast_pointer_ext, attr);
+		/* Copy common fields for simpler code below */
+		toast_pointer.va_rawsize = toast_pointer_ext.va_rawsize;
+		toast_pointer.va_extinfo = toast_pointer_ext.va_extinfo;
+		toast_pointer.va_valueid = toast_pointer_ext.va_valueid;
+		toast_pointer.va_toastrelid = toast_pointer_ext.va_toastrelid;
+	}
+	else
+		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 
 	/* Toasted attributes too large to be untoasted should never be stored */
 	if (toast_pointer.va_rawsize > VARLENA_SIZE_LIMIT)
@@ -1785,8 +1802,11 @@ check_tuple_attribute(HeapCheckContext *ctx)
 		ToastCompressionId cmid;
 		bool		valid = false;
 
-		/* Compressed attributes should have a valid compression method */
-		cmid = TOAST_COMPRESS_METHOD(&toast_pointer);
+		/*
+		 * Compressed attributes should have a valid compression method.
+		 * For extended pointers with cmid==3, the actual method is in va_data[0].
+		 */
+		cmid = VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer);
 		switch (cmid)
 		{
 				/* List of all valid compression method IDs */
@@ -1795,6 +1815,27 @@ check_tuple_attribute(HeapCheckContext *ctx)
 				valid = true;
 				break;
 
+				/* Extended compression (zstd or pglz/lz4 in extended format) */
+			case TOAST_EXTENDED_COMPRESSION_ID:
+				if (is_extended)
+				{
+					uint8	ext_method = VARATT_EXTERNAL_GET_EXT_COMPRESSION_METHOD(toast_pointer_ext);
+
+					/* Validate extended compression method */
+					switch (ext_method)
+					{
+						case TOAST_PGLZ_EXT_METHOD:
+						case TOAST_LZ4_EXT_METHOD:
+						case TOAST_ZSTD_EXT_METHOD:
+							valid = true;
+							break;
+						default:
+							/* Invalid extended method will be reported below */
+							break;
+					}
+				}
+				break;
+
 				/* Recognized but invalid compression method ID */
 			case TOAST_INVALID_COMPRESSION_ID:
 				break;
@@ -1840,7 +1881,21 @@ check_tuple_attribute(HeapCheckContext *ctx)
 
 		ta = palloc0_object(ToastedAttribute);
 
-		VARATT_EXTERNAL_GET_POINTER(ta->toast_pointer, attr);
+		/*
+		 * Extract toast pointer based on format.  For extended format,
+		 * copy common fields from toast_pointer which we already extracted
+		 * above.
+		 */
+		if (is_extended)
+		{
+			ta->toast_pointer.va_rawsize = toast_pointer.va_rawsize;
+			ta->toast_pointer.va_extinfo = toast_pointer.va_extinfo;
+			ta->toast_pointer.va_valueid = toast_pointer.va_valueid;
+			ta->toast_pointer.va_toastrelid = toast_pointer.va_toastrelid;
+		}
+		else
+			VARATT_EXTERNAL_GET_POINTER(ta->toast_pointer, attr);
+
 		ta->blkno = ctx->blkno;
 		ta->offnum = ctx->offnum;
 		ta->attnum = ctx->attnum;
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 62651787742..6d1c08900e8 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_compression.h"
 #include "access/toast_internals.h"
 #include "common/int.h"
 #include "common/pg_lzcompress.h"
@@ -225,12 +226,47 @@ detoast_attr_slice(struct varlena *attr,
 
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
-		struct varatt_external toast_pointer;
+		int32		max_size;
+		bool		is_compressed;
+		bool		is_pglz = false;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		/*
+		 * Handle both legacy 16-byte and extended 20-byte on-disk TOAST
+		 * pointers.  Check the vartag to determine which format.
+		 */
+		if (VARTAG_EXTERNAL(attr) == VARTAG_ONDISK_EXTENDED)
+		{
+			struct varatt_external_extended toast_pointer_ext;
+			uint8		ext_method;
+
+			VARATT_EXTERNAL_GET_POINTER_EXTENDED(toast_pointer_ext, attr);
+			max_size = VARATT_EXTERNAL_GET_EXTSIZE_EXTENDED(toast_pointer_ext);
+			is_compressed = VARATT_EXTERNAL_IS_COMPRESSED_EXTENDED(toast_pointer_ext);
+
+			/* Check if this is pglz for slice optimization */
+			if (is_compressed &&
+				VARATT_EXTERNAL_HAS_FLAG(toast_pointer_ext, TOAST_EXT_FLAG_COMPRESSION))
+			{
+				ext_method = VARATT_EXTERNAL_GET_EXT_COMPRESSION_METHOD(toast_pointer_ext);
+				is_pglz = (ext_method == TOAST_PGLZ_EXT_METHOD);
+			}
+		}
+		else
+		{
+			struct varatt_external toast_pointer;
+
+			VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+			max_size = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+			is_compressed = VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer);
+
+			/* Check if this is pglz for slice optimization */
+			if (is_compressed)
+				is_pglz = (VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) ==
+						   TOAST_PGLZ_COMPRESSION_ID);
+		}
 
 		/* fast path for non-compressed external datums */
-		if (!VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+		if (!is_compressed)
 			return toast_fetch_datum_slice(attr, sliceoffset, slicelength);
 
 		/*
@@ -240,19 +276,16 @@ detoast_attr_slice(struct varlena *attr,
 		 */
 		if (slicelimit >= 0)
 		{
-			int32		max_size = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
-
 			/*
 			 * 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 zstd 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 (VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) ==
-				TOAST_PGLZ_COMPRESSION_ID)
+			if (is_pglz)
 				max_size = pglz_maximum_compressed_size(slicelimit, max_size);
 
 			/*
@@ -344,20 +377,42 @@ toast_fetch_datum(struct varlena *attr)
 {
 	Relation	toastrel;
 	struct varlena *result;
-	struct varatt_external toast_pointer;
 	int32		attrsize;
+	Oid			toastrelid;
+	Oid			valueid;
+	bool		is_compressed;
 
 	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);
+	/*
+	 * Handle both legacy 16-byte and extended 20-byte on-disk TOAST pointers.
+	 * Check the vartag to determine which format we're dealing with.
+	 */
+	if (VARTAG_EXTERNAL(attr) == VARTAG_ONDISK_EXTENDED)
+	{
+		struct varatt_external_extended toast_pointer_ext;
+
+		VARATT_EXTERNAL_GET_POINTER_EXTENDED(toast_pointer_ext, attr);
+		attrsize = VARATT_EXTERNAL_GET_EXTSIZE_EXTENDED(toast_pointer_ext);
+		toastrelid = toast_pointer_ext.va_toastrelid;
+		valueid = toast_pointer_ext.va_valueid;
+		is_compressed = VARATT_EXTERNAL_IS_COMPRESSED_EXTENDED(toast_pointer_ext);
+	}
+	else
+	{
+		struct varatt_external toast_pointer;
 
-	attrsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		attrsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+		toastrelid = toast_pointer.va_toastrelid;
+		valueid = toast_pointer.va_valueid;
+		is_compressed = VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer);
+	}
 
 	result = (struct varlena *) palloc(attrsize + VARHDRSZ);
 
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+	if (is_compressed)
 		SET_VARSIZE_COMPRESSED(result, attrsize + VARHDRSZ);
 	else
 		SET_VARSIZE(result, attrsize + VARHDRSZ);
@@ -369,10 +424,10 @@ toast_fetch_datum(struct varlena *attr)
 	/*
 	 * Open the toast relation and its indexes
 	 */
-	toastrel = table_open(toast_pointer.va_toastrelid, AccessShareLock);
+	toastrel = table_open(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 +453,45 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 {
 	Relation	toastrel;
 	struct varlena *result;
-	struct varatt_external toast_pointer;
 	int32		attrsize;
+	Oid			toastrelid;
+	Oid			valueid;
+	bool		is_compressed;
 
 	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);
+	/*
+	 * Handle both legacy 16-byte and extended 20-byte on-disk TOAST pointers.
+	 * Check the vartag to determine which format we're dealing with.
+	 */
+	if (VARTAG_EXTERNAL(attr) == VARTAG_ONDISK_EXTENDED)
+	{
+		struct varatt_external_extended toast_pointer_ext;
+
+		VARATT_EXTERNAL_GET_POINTER_EXTENDED(toast_pointer_ext, attr);
+		attrsize = VARATT_EXTERNAL_GET_EXTSIZE_EXTENDED(toast_pointer_ext);
+		toastrelid = toast_pointer_ext.va_toastrelid;
+		valueid = toast_pointer_ext.va_valueid;
+		is_compressed = VARATT_EXTERNAL_IS_COMPRESSED_EXTENDED(toast_pointer_ext);
+	}
+	else
+	{
+		struct varatt_external toast_pointer;
+
+		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		attrsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+		toastrelid = toast_pointer.va_toastrelid;
+		valueid = toast_pointer.va_valueid;
+		is_compressed = VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer);
+	}
 
 	/*
 	 * 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);
-
-	attrsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+	Assert(!is_compressed || 0 == sliceoffset);
 
 	if (sliceoffset >= attrsize)
 	{
@@ -427,7 +504,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 (is_compressed && slicelength > 0)
 		slicelength = slicelength + sizeof(int32);
 
 	/*
@@ -440,7 +517,7 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 
 	result = (struct varlena *) palloc(slicelength + VARHDRSZ);
 
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+	if (is_compressed)
 		SET_VARSIZE_COMPRESSED(result, slicelength + VARHDRSZ);
 	else
 		SET_VARSIZE(result, slicelength + VARHDRSZ);
@@ -449,10 +526,10 @@ 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(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);
 
@@ -485,6 +562,9 @@ toast_decompress_datum(struct varlena *attr)
 			return pglz_decompress_datum(attr);
 		case TOAST_LZ4_COMPRESSION_ID:
 			return lz4_decompress_datum(attr);
+		case TOAST_EXTENDED_COMPRESSION_ID:
+			/* zstd-compressed data */
+			return zstd_decompress_datum(attr);
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 			return NULL;		/* keep compiler quiet */
@@ -528,6 +608,9 @@ 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_EXTENDED_COMPRESSION_ID:
+			/* zstd-compressed data */
+			return zstd_decompress_datum_slice(attr, slicelength);
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 			return NULL;		/* keep compiler quiet */
@@ -549,11 +632,15 @@ 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;
+		/*
+		 * va_rawsize is the size of the original datum -- including header.
+		 * It's at offset 0 in both varatt_external and varatt_external_extended,
+		 * so we can read just the first 4 bytes regardless of format.
+		 */
+		int32	va_rawsize;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-		result = toast_pointer.va_rawsize;
+		memcpy(&va_rawsize, VARDATA_EXTERNAL(attr), sizeof(va_rawsize));
+		result = va_rawsize;
 	}
 	else if (VARATT_IS_EXTERNAL_INDIRECT(attr))
 	{
@@ -609,11 +696,18 @@ 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?
+		 *
+		 * va_extinfo is at offset 4 in both varatt_external and
+		 * varatt_external_extended, so we can read the first 8 bytes
+		 * regardless of format.
 		 */
-		struct varatt_external toast_pointer;
+		struct {
+			int32	va_rawsize;
+			uint32	va_extinfo;
+		} common;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
-		result = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+		memcpy(&common, VARDATA_EXTERNAL(attr), sizeof(common));
+		result = common.va_extinfo & VARLENA_EXTSIZE_MASK;
 	}
 	else if (VARATT_IS_EXTERNAL_INDIRECT(attr))
 	{
diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c
index 926f1e4008a..422e2c5967a 100644
--- a/src/backend/access/common/toast_compression.c
+++ b/src/backend/access/common/toast_compression.c
@@ -17,13 +17,19 @@
 #include <lz4.h>
 #endif
 
+#ifdef USE_ZSTD
+#include <zstd.h>
+#endif
+
 #include "access/detoast.h"
 #include "access/toast_compression.h"
 #include "common/pg_lzcompress.h"
+#include "utils/memutils.h"
 #include "varatt.h"
 
 /* GUC */
 int			default_toast_compression = TOAST_PGLZ_COMPRESSION;
+bool		use_extended_toast_header = false;
 
 #define NO_COMPRESSION_SUPPORT(method) \
 	ereport(ERROR, \
@@ -249,11 +255,16 @@ lz4_decompress_datum_slice(const struct varlena *value, int32 slicelength)
  * Extract compression ID from a varlena.
  *
  * Returns TOAST_INVALID_COMPRESSION_ID if the varlena is not compressed.
+ *
+ * For external data stored in extended format (VARTAG_ONDISK_EXTENDED),
+ * the actual compression method is stored in va_data[0].  We map that
+ * back to the appropriate ToastCompressionId for legacy compatibility.
  */
 ToastCompressionId
 toast_get_compression_id(struct varlena *attr)
 {
 	ToastCompressionId cmid = TOAST_INVALID_COMPRESSION_ID;
+	vartag_external tag;
 
 	/*
 	 * If it is stored externally then fetch the compression method id from
@@ -262,12 +273,52 @@ toast_get_compression_id(struct varlena *attr)
 	 */
 	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);
+		tag = VARTAG_EXTERNAL(attr);
+		if (tag == VARTAG_ONDISK)
+		{
+			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);
+		}
+		else
+		{
+			struct varatt_external_extended toast_pointer_ext;
+			uint8		ext_method;
+
+			Assert(tag == VARTAG_ONDISK_EXTENDED);
+			VARATT_EXTERNAL_GET_POINTER_EXTENDED(toast_pointer_ext, attr);
+
+			if (VARATT_EXTERNAL_IS_COMPRESSED_EXTENDED(toast_pointer_ext))
+			{
+				/*
+				 * Extended format stores the actual method in va_data[0].
+				 * Map it back to ToastCompressionId for reporting purposes.
+				 */
+				ext_method = VARATT_EXTERNAL_GET_EXT_COMPRESSION_METHOD(toast_pointer_ext);
+				switch (ext_method)
+				{
+					case TOAST_PGLZ_EXT_METHOD:
+						cmid = TOAST_PGLZ_COMPRESSION_ID;
+						break;
+					case TOAST_LZ4_EXT_METHOD:
+						cmid = TOAST_LZ4_COMPRESSION_ID;
+						break;
+					case TOAST_ZSTD_EXT_METHOD:
+						cmid = TOAST_EXTENDED_COMPRESSION_ID;
+						break;
+					case TOAST_UNCOMPRESSED_EXT_METHOD:
+						/* Uncompressed data in extended format */
+						cmid = TOAST_INVALID_COMPRESSION_ID;
+						break;
+					default:
+						elog(ERROR, "invalid extended compression method %d",
+							 ext_method);
+				}
+			}
+		}
 	}
 	else if (VARATT_IS_COMPRESSED(attr))
 		cmid = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(attr);
@@ -275,6 +326,133 @@ toast_get_compression_id(struct varlena *attr)
 	return cmid;
 }
 
+/*
+ * Zstandard (zstd) compression/decompression for TOAST (extended methods).
+ *
+ * These routines use the same basic shape as the pglz and LZ4 helpers,
+ * but are only available when PostgreSQL is built with USE_ZSTD.
+ */
+
+/*
+ * Compress a varlena using ZSTD.
+ *
+ * Returns the compressed varlena, or NULL if compression fails or does
+ * not save space.
+ */
+static struct varlena *
+zstd_compress_datum_internal(const struct varlena *value, int level)
+{
+#ifndef USE_ZSTD
+	NO_COMPRESSION_SUPPORT("zstd");
+	return NULL;				/* keep compiler quiet */
+#else
+	Size		valsize;
+	Size		max_size;
+	Size		out_size;
+	struct varlena *tmp;
+	size_t		rc;
+
+	valsize = VARSIZE_ANY_EXHDR(value);
+
+	/*
+	 * Compute an upper bound for the compressed size and allocate enough
+	 * space for the compressed payload plus the varlena header.
+	 */
+	max_size = ZSTD_compressBound(valsize);
+	if (max_size > (Size) (MaxAllocSize - VARHDRSZ_COMPRESSED))
+		ereport(ERROR,
+				(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				 errmsg("compressed data would exceed maximum allocation size")));
+
+	tmp = (struct varlena *) palloc(max_size + VARHDRSZ_COMPRESSED);
+
+	rc = ZSTD_compress((char *) tmp + VARHDRSZ_COMPRESSED, max_size,
+					   VARDATA_ANY(value), valsize, level);
+	if (ZSTD_isError(rc))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATA_CORRUPTED),
+				 errmsg_internal("zstd compression failed: %s",
+								 ZSTD_getErrorName(rc))));
+
+	out_size = (Size) rc;
+
+	/*
+	 * If the compressed representation is not smaller than the original
+	 * payload, give up and return NULL so that callers can fall back to
+	 * storing the datum uncompressed or with a different method.
+	 */
+	if (out_size >= valsize)
+	{
+		pfree(tmp);
+		return NULL;
+	}
+
+	SET_VARSIZE_COMPRESSED(tmp, out_size + VARHDRSZ_COMPRESSED);
+
+	return tmp;
+#endif							/* USE_ZSTD */
+}
+
+struct varlena *
+zstd_compress_datum(const struct varlena *value)
+{
+#ifndef USE_ZSTD
+	NO_COMPRESSION_SUPPORT("zstd");
+	return NULL;				/* keep compiler quiet */
+#else
+	return zstd_compress_datum_internal(value, ZSTD_CLEVEL_DEFAULT);
+#endif
+}
+
+/*
+ * Decompress a varlena that was compressed using ZSTD.
+ */
+struct varlena *
+zstd_decompress_datum(const struct varlena *value)
+{
+#ifndef USE_ZSTD
+	NO_COMPRESSION_SUPPORT("zstd");
+	return NULL;				/* keep compiler quiet */
+#else
+	struct varlena *result;
+	Size		rawsize;
+	size_t		rc;
+
+	/* allocate memory for the uncompressed data */
+	rawsize = VARDATA_COMPRESSED_GET_EXTSIZE(value);
+	result = (struct varlena *) palloc(rawsize + VARHDRSZ);
+
+	rc = ZSTD_decompress(VARDATA(result), rawsize,
+						 (char *) value + VARHDRSZ_COMPRESSED,
+						 VARSIZE(value) - VARHDRSZ_COMPRESSED);
+	if (ZSTD_isError(rc) || rc != rawsize)
+		ereport(ERROR,
+				(errcode(ERRCODE_DATA_CORRUPTED),
+				 errmsg_internal("compressed zstd data is corrupt or truncated")));
+
+	SET_VARSIZE(result, rawsize + VARHDRSZ);
+
+	return result;
+#endif							/* USE_ZSTD */
+}
+
+/*
+ * Decompress part of a varlena that was compressed using ZSTD.
+ *
+ * At least initially we don't try to be clever with streaming slice
+ * decompression here; instead we just decompress the full datum and
+ * let higher layers perform the slicing.  Callers should prefer the
+ * regular zstd_decompress_datum() when they know they need the whole
+ * value anyway.
+ */
+struct varlena *
+zstd_decompress_datum_slice(const struct varlena *value, int32 slicelength)
+{
+	/* For now, just fall back to full decompression. */
+	(void) slicelength;
+	return zstd_decompress_datum(value);
+}
+
 /*
  * CompressionNameToMethod - Get compression method from compression name
  *
@@ -293,6 +471,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;
 }
@@ -309,6 +494,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 d06af82de15..039ccc42249 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_compression.h"
 #include "access/toast_internals.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -71,6 +72,9 @@ toast_compress_datum(Datum value, char cmethod)
 			tmp = lz4_compress_datum((const struct varlena *) DatumGetPointer(value));
 			cmid = TOAST_LZ4_COMPRESSION_ID;
 			break;
+		case TOAST_ZSTD_COMPRESSION:
+			/* zstd uses external storage only; handled by toast_save_datum */
+			return PointerGetDatum(NULL);
 		default:
 			elog(ERROR, "invalid compression method %c", cmethod);
 	}
@@ -113,11 +117,13 @@ toast_compress_datum(Datum value, char cmethod)
  * value: datum to be pushed to toast storage
  * oldexternal: if not NULL, toast pointer previously representing the datum
  * options: options to be passed to heap_insert() for toast rows
+ * cmethod: compression method to use for uncompressed data
  * ----------
  */
 Datum
 toast_save_datum(Relation rel, Datum value,
-				 struct varlena *oldexternal, int options)
+				 struct varlena *oldexternal, int options,
+				 char cmethod)
 {
 	Relation	toastrel;
 	Relation   *toastidxs;
@@ -125,12 +131,16 @@ toast_save_datum(Relation rel, Datum value,
 	CommandId	mycid = GetCurrentCommandId(true);
 	struct varlena *result;
 	struct varatt_external toast_pointer;
+	struct varatt_external_extended toast_pointer_ext;
 	int32		chunk_seq = 0;
 	char	   *data_p;
 	int32		data_todo;
 	Pointer		dval = DatumGetPointer(value);
 	int			num_indexes;
 	int			validIndex;
+	bool		use_extended = false;
+	uint8		ext_method = 0;
+	struct varlena *compressed_to_free = NULL;	/* track allocated buffer */
 
 	Assert(!VARATT_IS_EXTERNAL(dval));
 
@@ -167,23 +177,99 @@ toast_save_datum(Relation rel, Datum value,
 	}
 	else if (VARATT_IS_COMPRESSED(dval))
 	{
+		ToastCompressionId cmid;
+
 		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;
 
+		/* Get compression method from compressed datum */
+		cmid = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(dval);
+
+		/* Decide whether to use extended 20-byte or legacy 16-byte format */
+		if (cmid == TOAST_EXTENDED_COMPRESSION_ID)
+		{
+			use_extended = true;
+			ext_method = TOAST_ZSTD_EXT_METHOD;
+		}
+		else if (use_extended_toast_header)
+		{
+			/* Use extended format for pglz/lz4 when GUC is enabled */
+			use_extended = true;
+			switch (cmid)
+			{
+				case TOAST_PGLZ_COMPRESSION_ID:
+					ext_method = TOAST_PGLZ_EXT_METHOD;
+					break;
+				case TOAST_LZ4_COMPRESSION_ID:
+					ext_method = TOAST_LZ4_EXT_METHOD;
+					break;
+				default:
+					/* Should not happen, but fall back to legacy format */
+					use_extended = false;
+					break;
+			}
+		}
+
 		/* set external size and compression method */
-		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, data_todo,
-													 VARDATA_COMPRESSED_GET_COMPRESS_METHOD(dval));
+		if (use_extended)
+			VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, data_todo,
+														VARATT_EXTERNAL_EXTENDED_CMID);
+		else
+			VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, data_todo, cmid);
+
 		/* Assert that the numbers look like it's compressed */
 		Assert(VARATT_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;
+		/*
+		 * Uncompressed data.  If the caller specified zstd compression,
+		 * try to compress it now before storing to the TOAST table.
+		 */
+		if (cmethod == TOAST_ZSTD_COMPRESSION)
+		{
+			struct varlena *compressed;
+			int32		rawsize;
+
+			rawsize = VARSIZE_ANY_EXHDR((const struct varlena *) dval);
+			compressed = zstd_compress_datum((const struct varlena *) dval);
+			if (compressed != NULL)
+			{
+				/* Set compression method in va_tcinfo */
+				TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(compressed, rawsize,
+															TOAST_EXTENDED_COMPRESSION_ID);
+
+				/* Compression succeeded - use the compressed data */
+				compressed_to_free = compressed;	/* track for cleanup */
+				dval = (Pointer) compressed;
+				data_p = VARDATA(compressed);
+				data_todo = VARSIZE(compressed) - VARHDRSZ;
+				toast_pointer.va_rawsize = rawsize + VARHDRSZ;
+
+				/* Use extended format for zstd */
+				use_extended = true;
+				ext_method = TOAST_ZSTD_EXT_METHOD;
+				VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, data_todo,
+															VARATT_EXTERNAL_EXTENDED_CMID);
+			}
+			else
+			{
+				/* Compression failed or didn't save space - store uncompressed */
+				data_p = VARDATA(dval);
+				data_todo = VARSIZE(dval) - VARHDRSZ;
+				toast_pointer.va_rawsize = VARSIZE(dval);
+				toast_pointer.va_extinfo = data_todo;
+			}
+		}
+		else
+		{
+			data_p = VARDATA(dval);
+			data_todo = VARSIZE(dval) - VARHDRSZ;
+			toast_pointer.va_rawsize = VARSIZE(dval);
+			toast_pointer.va_extinfo = data_todo;
+		}
 	}
 
 	/*
@@ -225,15 +311,36 @@ toast_save_datum(Relation rel, Datum value,
 		toast_pointer.va_valueid = InvalidOid;
 		if (oldexternal != NULL)
 		{
-			struct varatt_external old_toast_pointer;
+			Oid			old_toastrelid;
+			Oid			old_valueid;
 
 			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)
+
+			/*
+			 * Extract toastrelid and valueid from the old pointer.
+			 * Handle both legacy 16-byte and extended 20-byte formats.
+			 */
+			if (VARTAG_EXTERNAL(oldexternal) == VARTAG_ONDISK_EXTENDED)
+			{
+				struct varatt_external_extended old_toast_pointer_ext;
+
+				VARATT_EXTERNAL_GET_POINTER_EXTENDED(old_toast_pointer_ext, oldexternal);
+				old_toastrelid = old_toast_pointer_ext.va_toastrelid;
+				old_valueid = old_toast_pointer_ext.va_valueid;
+			}
+			else
+			{
+				struct varatt_external old_toast_pointer;
+
+				VARATT_EXTERNAL_GET_POINTER(old_toast_pointer, oldexternal);
+				old_toastrelid = old_toast_pointer.va_toastrelid;
+				old_valueid = old_toast_pointer.va_valueid;
+			}
+
+			if (old_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.va_valueid = old_valueid;
 
 				/*
 				 * There is a corner case here: the table rewrite might have
@@ -348,6 +455,10 @@ toast_save_datum(Relation rel, Datum value,
 		data_p += chunk_size;
 	}
 
+	/* Free compressed buffer if we allocated one */
+	if (compressed_to_free != NULL)
+		pfree(compressed_to_free);
+
 	/*
 	 * Done - close toast relation and its indexes but keep the lock until
 	 * commit, so as a concurrent reindex done directly on the toast relation
@@ -356,12 +467,35 @@ toast_save_datum(Relation rel, Datum value,
 	toast_close_indexes(toastidxs, num_indexes, NoLock);
 	table_close(toastrel, NoLock);
 
-	/*
-	 * 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));
+	/* Create the TOAST pointer value that we'll return */
+	if (use_extended)
+	{
+		/*
+		 * Build extended TOAST pointer.  Copy the common fields from
+		 * toast_pointer, then set the extended-format-specific fields.
+		 */
+		toast_pointer_ext.va_rawsize = toast_pointer.va_rawsize;
+		toast_pointer_ext.va_extinfo = toast_pointer.va_extinfo;
+		toast_pointer_ext.va_valueid = toast_pointer.va_valueid;
+		toast_pointer_ext.va_toastrelid = toast_pointer.va_toastrelid;
+
+		/* Set extended format fields */
+		toast_pointer_ext.va_flags = TOAST_EXT_FLAG_COMPRESSION;
+		toast_pointer_ext.va_data[0] = ext_method;
+		toast_pointer_ext.va_data[1] = 0;
+		toast_pointer_ext.va_data[2] = 0;
+
+		result = (struct varlena *) palloc(TOAST_POINTER_SIZE_EXTENDED);
+		SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK_EXTENDED);
+		memcpy(VARDATA_EXTERNAL(result), &toast_pointer_ext, sizeof(toast_pointer_ext));
+	}
+	else
+	{
+		/* Standard 16-byte TOAST pointer */
+		result = (struct varlena *) palloc(TOAST_POINTER_SIZE);
+		SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK);
+		memcpy(VARDATA_EXTERNAL(result), &toast_pointer, sizeof(toast_pointer));
+	}
 
 	return PointerGetDatum(result);
 }
@@ -377,6 +511,7 @@ 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_extended toast_pointer_ext;
 	Relation	toastrel;
 	Relation   *toastidxs;
 	ScanKeyData toastkey;
@@ -384,17 +519,36 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	HeapTuple	toasttup;
 	int			num_indexes;
 	int			validIndex;
+	Oid			toastrelid;
+	Oid			valueid;
+	bool		is_extended;
 
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		return;
 
-	/* Must copy to access aligned fields */
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	/*
+	 * Must copy to access aligned fields.  Handle both legacy (16-byte) and
+	 * extended (20-byte) on-disk TOAST pointers based on the tag.
+	 */
+	is_extended = (VARTAG_EXTERNAL(attr) == VARTAG_ONDISK_EXTENDED);
+
+	if (!is_extended)
+	{
+		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		toastrelid = toast_pointer.va_toastrelid;
+		valueid = toast_pointer.va_valueid;
+	}
+	else
+	{
+		VARATT_EXTERNAL_GET_POINTER_EXTENDED(toast_pointer_ext, attr);
+		toastrelid = toast_pointer_ext.va_toastrelid;
+		valueid = toast_pointer_ext.va_valueid;
+	}
 
 	/*
 	 * Open the toast relation and its indexes
 	 */
-	toastrel = table_open(toast_pointer.va_toastrelid, RowExclusiveLock);
+	toastrel = table_open(toastrelid, RowExclusiveLock);
 
 	/* Fetch valid relation used for process */
 	validIndex = toast_open_indexes(toastrel,
@@ -408,7 +562,7 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	ScanKeyInit(&toastkey,
 				(AttrNumber) 1,
 				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(toast_pointer.va_valueid));
+				ObjectIdGetDatum(valueid));
 
 	/*
 	 * Find all the chunks.  (We don't actually care whether we see them in
diff --git a/src/backend/access/table/toast_helper.c b/src/backend/access/table/toast_helper.c
index 11f97d65367..21381004ba6 100644
--- a/src/backend/access/table/toast_helper.c
+++ b/src/backend/access/table/toast_helper.c
@@ -261,7 +261,7 @@ toast_tuple_externalize(ToastTupleContext *ttc, int attribute, int options)
 
 	attr->tai_colflags |= TOASTCOL_IGNORE;
 	*value = toast_save_datum(ttc->ttc_rel, old_value, attr->tai_oldexternal,
-							  options);
+							  options, attr->tai_compression);
 	if ((attr->tai_colflags & TOASTCOL_NEEDS_FREE) != 0)
 		pfree(DatumGetPointer(old_value));
 	attr->tai_colflags |= TOASTCOL_NEEDS_FREE;
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index f18c6fb52b5..9e83ab5978d 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -5137,11 +5137,17 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 
 		/* va_rawsize is the size of the original datum -- including header */
 		struct varatt_external toast_pointer;
+		struct varatt_external_extended toast_pointer_ext;
 		struct varatt_indirect redirect_pointer;
 		struct varlena *new_datum = NULL;
 		struct varlena *reconstructed;
 		dlist_iter	it;
 		Size		data_done = 0;
+		bool		is_extended;
+		Oid			valueid;
+		int32		rawsize;
+		int32		extsize;
+		bool		is_compressed;
 
 		if (attr->attisdropped)
 			continue;
@@ -5161,14 +5167,36 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		if (!VARATT_IS_EXTERNAL(varlena))
 			continue;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, varlena);
+		/*
+		 * Handle both legacy 16-byte and extended 20-byte on-disk TOAST
+		 * pointers based on the tag.
+		 */
+		is_extended = VARATT_IS_EXTERNAL_ONDISK(varlena) &&
+					  (VARTAG_EXTERNAL(varlena) == VARTAG_ONDISK_EXTENDED);
+
+		if (is_extended)
+		{
+			VARATT_EXTERNAL_GET_POINTER_EXTENDED(toast_pointer_ext, varlena);
+			valueid = toast_pointer_ext.va_valueid;
+			rawsize = toast_pointer_ext.va_rawsize;
+			extsize = VARATT_EXTERNAL_GET_EXTSIZE_EXTENDED(toast_pointer_ext);
+			is_compressed = VARATT_EXTERNAL_IS_COMPRESSED_EXTENDED(toast_pointer_ext);
+		}
+		else
+		{
+			VARATT_EXTERNAL_GET_POINTER(toast_pointer, varlena);
+			valueid = toast_pointer.va_valueid;
+			rawsize = toast_pointer.va_rawsize;
+			extsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+			is_compressed = VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer);
+		}
 
 		/*
 		 * Check whether the toast tuple changed, replace if so.
 		 */
 		ent = (ReorderBufferToastEnt *)
 			hash_search(txn->toast_hash,
-						&toast_pointer.va_valueid,
+						&valueid,
 						HASH_FIND,
 						NULL);
 		if (ent == NULL)
@@ -5179,7 +5207,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 
 		free[natt] = true;
 
-		reconstructed = palloc0(toast_pointer.va_rawsize);
+		reconstructed = palloc0(rawsize);
 
 		ent->reconstructed = reconstructed;
 
@@ -5204,10 +5232,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 == extsize);
 
 		/* make sure its marked as compressed or not */
-		if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+		if (is_compressed)
 			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 baa5b44ea8d..71a410dc617 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -4206,6 +4206,10 @@ pg_column_compression(PG_FUNCTION_ARGS)
 		case TOAST_LZ4_COMPRESSION_ID:
 			result = "lz4";
 			break;
+		case TOAST_EXTENDED_COMPRESSION_ID:
+			/* Extended format currently only supports zstd */
+			result = "zstd";
+			break;
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 	}
@@ -4222,7 +4226,7 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 {
 	int			typlen;
 	struct varlena *attr;
-	struct varatt_external toast_pointer;
+	Oid			valueid;
 
 	/* On first call, get the input type's typlen, and save at *fn_extra */
 	if (fcinfo->flinfo->fn_extra == NULL)
@@ -4249,9 +4253,25 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
 		PG_RETURN_NULL();
 
-	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+	/*
+	 * Handle both legacy 16-byte and extended 20-byte on-disk TOAST pointers.
+	 */
+	if (VARTAG_EXTERNAL(attr) == VARTAG_ONDISK_EXTENDED)
+	{
+		struct varatt_external_extended toast_pointer_ext;
+
+		VARATT_EXTERNAL_GET_POINTER_EXTENDED(toast_pointer_ext, attr);
+		valueid = toast_pointer_ext.va_valueid;
+	}
+	else
+	{
+		struct varatt_external toast_pointer;
+
+		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		valueid = toast_pointer.va_valueid;
+	}
 
-	PG_RETURN_OID(toast_pointer.va_valueid);
+	PG_RETURN_OID(valueid);
 }
 
 /*
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 3b9d8349078..38c68d1d0a6 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -738,7 +738,6 @@
   boot_val => 'TOAST_PGLZ_COMPRESSION',
   options => 'default_toast_compression_options',
 },
-
 { name => 'default_transaction_deferrable', type => 'bool', context => 'PGC_USERSET', group => 'CLIENT_CONN_STATEMENT',
   short_desc => 'Sets the default deferrable status of new transactions.',
   variable => 'DefaultXactDeferrable',
@@ -3175,6 +3174,12 @@
   boot_val => 'DEFAULT_UPDATE_PROCESS_TITLE',
 },
 
+{ name => 'use_extended_toast_header', type => 'bool', context => 'PGC_USERSET', group => 'CLIENT_CONN_STATEMENT',
+  short_desc => 'Use 20-byte extended TOAST header format (required for zstd).',
+  variable => 'use_extended_toast_header',
+  boot_val => 'false',
+},
+
 { name => 'vacuum_buffer_usage_limit', type => 'int', context => 'PGC_USERSET', group => 'RESOURCES_MEM',
   short_desc => 'Sets the buffer pool size for VACUUM, ANALYZE, and autovacuum.',
   flags => 'GUC_UNIT_KB',
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index f87b558c2c6..f6c09260f1a 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -460,6 +460,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/include/access/detoast.h b/src/include/access/detoast.h
index e603a2276c3..e591a59569b 100644
--- a/src/include/access/detoast.h
+++ b/src/include/access/detoast.h
@@ -14,25 +14,58 @@
 
 /*
  * 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.
+ *
+ * This currently supports only the legacy on-disk TOAST pointer format,
+ * which has VARTAG_ONDISK and a payload size of sizeof(varatt_external).
+ * Extended on-disk pointers (VARTAG_ONDISK_EXTENDED) must be accessed via
+ * VARATT_EXTERNAL_GET_POINTER_EXTENDED().
+ *
+ * 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 { \
 	varattrib_1b_e *attre = (varattrib_1b_e *) (attr); \
 	Assert(VARATT_IS_EXTERNAL(attre)); \
+	Assert(VARTAG_EXTERNAL(attre) == VARTAG_ONDISK); \
 	Assert(VARSIZE_EXTERNAL(attre) == sizeof(toast_pointer) + VARHDRSZ_EXTERNAL); \
 	memcpy(&(toast_pointer), VARDATA_EXTERNAL(attre), sizeof(toast_pointer)); \
 } while (0)
 
+/*
+ * Variant of VARATT_EXTERNAL_GET_POINTER for the extended on-disk TOAST
+ * pointer format.  Callers should only use this when they have already
+ * established that the tag is VARTAG_ONDISK_EXTENDED.
+ */
+#define VARATT_EXTERNAL_GET_POINTER_EXTENDED(toast_pointer_ext, attr) \
+do { \
+	varattrib_1b_e *attre = (varattrib_1b_e *) (attr); \
+	Assert(VARATT_IS_EXTERNAL(attre)); \
+	Assert(VARTAG_EXTERNAL(attre) == VARTAG_ONDISK_EXTENDED); \
+	Assert(VARSIZE_EXTERNAL(attre) == sizeof(toast_pointer_ext) + VARHDRSZ_EXTERNAL); \
+	memcpy(&(toast_pointer_ext), VARDATA_EXTERNAL(attre), sizeof(toast_pointer_ext)); \
+} 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))
 
+/* Size of an EXTERNAL datum that contains an extended TOAST pointer */
+#define TOAST_POINTER_SIZE_EXTENDED (VARHDRSZ_EXTERNAL + sizeof(varatt_external_extended))
+
+/* Validation helpers for TOAST pointer sizes */
+#define TOAST_POINTER_SIZE_IS_VALID(size) \
+	((size) == TOAST_POINTER_SIZE || \
+	 (size) == TOAST_POINTER_SIZE_EXTENDED || \
+	 (size) == INDIRECT_POINTER_SIZE)
+
+#define TOAST_POINTER_IS_EXTENDED_SIZE(size) \
+	((size) == TOAST_POINTER_SIZE_EXTENDED)
+
 /* ----------
  * detoast_external_attr() -
  *
diff --git a/src/include/access/toast_compression.h b/src/include/access/toast_compression.h
index 13c4612ceed..b769d1bc72d 100644
--- a/src/include/access/toast_compression.h
+++ b/src/include/access/toast_compression.h
@@ -13,14 +13,21 @@
 #ifndef TOAST_COMPRESSION_H
 #define TOAST_COMPRESSION_H
 
+#include "varatt.h"
+
 /*
  * GUC support.
  *
  * default_toast_compression is an integer for purposes of the GUC machinery,
  * but the value is one of the char values defined below, as they appear in
  * pg_attribute.attcompression, e.g. TOAST_PGLZ_COMPRESSION.
+ *
+ * use_extended_toast_header controls whether to use the 20-byte extended
+ * TOAST pointer format (required for zstd) instead of the legacy 16-byte
+ * format. When false, zstd compression falls back to pglz.
  */
 extern PGDLLIMPORT int default_toast_compression;
+extern PGDLLIMPORT bool use_extended_toast_header;
 
 /*
  * Built-in compression method ID.  The toast compression header will store
@@ -39,6 +46,7 @@ typedef enum ToastCompressionId
 	TOAST_PGLZ_COMPRESSION_ID = 0,
 	TOAST_LZ4_COMPRESSION_ID = 1,
 	TOAST_INVALID_COMPRESSION_ID = 2,
+	TOAST_EXTENDED_COMPRESSION_ID = 3,	/* extended format for future methods */
 } ToastCompressionId;
 
 /*
@@ -48,6 +56,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)
@@ -65,9 +74,36 @@ extern struct varlena *lz4_decompress_datum(const struct varlena *value);
 extern struct varlena *lz4_decompress_datum_slice(const struct varlena *value,
 												  int32 slicelength);
 
+/* zstd compression/decompression routines (extended methods) */
+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);
 extern const char *GetCompressionMethodName(char method);
 
+/*
+ * Feature flags for extended TOAST pointers (varatt_external_extended).
+ * These alias VARATT_EXTERNAL_FLAG_* from varatt.h.
+ */
+#define TOAST_EXT_FLAG_COMPRESSION      VARATT_EXTERNAL_FLAG_COMPRESSION
+#define TOAST_EXT_FLAG_CHECKSUM         VARATT_EXTERNAL_FLAG_CHECKSUM
+
+/*
+ * Extended compression method IDs for use with extended TOAST format.
+ * Stored in va_data[0] when TOAST_EXT_FLAG_COMPRESSION is set.
+ */
+#define TOAST_PGLZ_EXT_METHOD          0
+#define TOAST_LZ4_EXT_METHOD           1
+#define TOAST_ZSTD_EXT_METHOD          2
+#define TOAST_UNCOMPRESSED_EXT_METHOD  3
+
+/* Validation macros for extended format */
+#define ExtendedCompressionMethodIsValid(method) ((method) <= 255)
+#define ExtendedFlagsAreValid(flags) \
+	(((flags) & ~(TOAST_EXT_FLAG_COMPRESSION | TOAST_EXT_FLAG_CHECKSUM)) == 0)
+
 #endif							/* TOAST_COMPRESSION_H */
diff --git a/src/include/access/toast_internals.h b/src/include/access/toast_internals.h
index 06ae8583c1e..d6bc5c4d179 100644
--- a/src/include/access/toast_internals.h
+++ b/src/include/access/toast_internals.h
@@ -36,11 +36,16 @@ typedef struct toast_compress_header
 #define TOAST_COMPRESS_METHOD(ptr) \
 	(((toast_compress_header *) (ptr))->tcinfo >> VARLENA_EXTSIZE_BITS)
 
+/*
+ * Set the size and compression method in a compressed datum's header.
+ * Accepts TOAST_EXTENDED_COMPRESSION_ID for extended compression methods.
+ */
 #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_LZ4_COMPRESSION_ID || \
+			   (cm_method) == TOAST_EXTENDED_COMPRESSION_ID); \
 		((toast_compress_header *) (ptr))->tcinfo = \
 			(len) | ((uint32) (cm_method) << VARLENA_EXTSIZE_BITS); \
 	} while (0)
@@ -50,7 +55,8 @@ extern Oid	toast_get_valid_index(Oid toastoid, LOCKMODE lock);
 
 extern void toast_delete_datum(Relation rel, Datum value, bool is_speculative);
 extern Datum toast_save_datum(Relation rel, Datum value,
-							  struct varlena *oldexternal, int options);
+							  struct varlena *oldexternal, int options,
+							  char cmethod);
 
 extern int	toast_open_indexes(Relation toastrel,
 							   LOCKMODE lock,
diff --git a/src/include/varatt.h b/src/include/varatt.h
index aeeabf9145b..5f5829a1ec4 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -45,6 +45,23 @@ typedef struct varatt_external
 #define VARLENA_EXTSIZE_BITS	30
 #define VARLENA_EXTSIZE_MASK	((1U << VARLENA_EXTSIZE_BITS) - 1)
 
+/*
+ * Compression method ID stored in the 2 high-order bits of va_extinfo.
+ * Value 3 indicates an extended TOAST pointer format (varatt_external_extended).
+ * This constant is also defined in toast_compression.h for use by TOAST code.
+ */
+#define VARATT_EXTERNAL_EXTENDED_CMID	3
+
+/*
+ * Feature flags for extended on-disk TOAST pointers (varatt_external_extended).
+ *
+ * Keep these in varatt.h (not access/toast headers) so low-level code can
+ * safely manipulate the on-disk representation without depending on higher
+ * layers' header include order.
+ */
+#define VARATT_EXTERNAL_FLAG_COMPRESSION	0x01	/* va_data[0] = method ID */
+#define VARATT_EXTERNAL_FLAG_CHECKSUM		0x02	/* va_data[1-2] = checksum */
+
 /*
  * struct varatt_indirect is a "TOAST pointer" representing an out-of-line
  * Datum that's stored in memory, not in an external toast relation.
@@ -76,6 +93,26 @@ typedef struct varatt_expanded
 	ExpandedObjectHeader *eohptr;
 } varatt_expanded;
 
+/*
+ * Extended TOAST pointer, extending varatt_external from 16 to 20 bytes.
+ *
+ * Identified by compression method ID 3 in va_extinfo bits 30-31.  The
+ * va_flags field indicates which optional features are enabled; va_data[]
+ * contains feature-specific data (e.g., compression method in va_data[0]).
+ *
+ * Like varatt_external, stored unaligned and requires memcpy for access.
+ */
+typedef struct varatt_external_extended
+{
+	int32		va_rawsize;		/* Original data size (includes header) */
+	uint32		va_extinfo;		/* External saved size (30 bits) + extended
+								 * indicator (2 bits, value = 3) */
+	uint8		va_flags;		/* Feature flags indicating enabled extensions */
+	uint8		va_data[3];		/* Extension data - interpretation depends on flags */
+	Oid			va_valueid;		/* Unique ID of value within TOAST table */
+	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
+}			varatt_external_extended;
+
 /*
  * Type tag for the various sorts of "TOAST pointer" datums.  The peculiar
  * value for VARTAG_ONDISK comes from a requirement for on-disk compatibility
@@ -86,7 +123,17 @@ typedef enum vartag_external
 	VARTAG_INDIRECT = 1,
 	VARTAG_EXPANDED_RO = 2,
 	VARTAG_EXPANDED_RW = 3,
-	VARTAG_ONDISK = 18
+	VARTAG_ONDISK = 18,
+
+	/*
+	 * VARTAG_ONDISK_EXTENDED is used for the extended TOAST pointer format,
+	 * which increases the on-disk payload from 16 to 20 bytes.  The first
+	 * 8 bytes (va_rawsize, va_extinfo) are layout-compatible with
+	 * struct varatt_external so that existing code inspecting those fields
+	 * continues to work.  Older PostgreSQL versions do not know about this
+	 * tag and therefore must not be used to read clusters that contain it.
+	 */
+	VARTAG_ONDISK_EXTENDED = 19
 } vartag_external;
 
 /* Is a TOAST pointer either type of expanded-object pointer? */
@@ -97,7 +144,14 @@ VARTAG_IS_EXPANDED(vartag_external tag)
 	return ((tag & ~1) == VARTAG_EXPANDED_RO);
 }
 
-/* Size of the data part of a "TOAST pointer" datum */
+/*
+ * Size of the data part of a "TOAST pointer" datum.
+ *
+ * For on-disk TOAST pointers we now support two payload sizes:
+ * the original 16-byte format (VARTAG_ONDISK) described by struct
+ * varatt_external, and a 20-byte extended format
+ * (VARTAG_ONDISK_EXTENDED) described by struct varatt_external_extended.
+ */
 static inline Size
 VARTAG_SIZE(vartag_external tag)
 {
@@ -107,6 +161,8 @@ VARTAG_SIZE(vartag_external tag)
 		return sizeof(varatt_expanded);
 	else if (tag == VARTAG_ONDISK)
 		return sizeof(varatt_external);
+	else if (tag == VARTAG_ONDISK_EXTENDED)
+		return sizeof(varatt_external_extended);
 	else
 	{
 		Assert(false);
@@ -360,7 +416,13 @@ 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;
+	vartag_external tag;
+
+	if (!VARATT_IS_EXTERNAL(PTR))
+		return false;
+
+	tag = VARTAG_EXTERNAL(PTR);
+	return tag == VARTAG_ONDISK || tag == VARTAG_ONDISK_EXTENDED;
 }
 
 /* Is varlena datum an indirect pointer? */
@@ -516,11 +578,11 @@ VARATT_EXTERNAL_GET_COMPRESS_METHOD(struct varatt_external toast_pointer)
 }
 
 /* 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) \
 	do { \
 		Assert((cm) == TOAST_PGLZ_COMPRESSION_ID || \
-			   (cm) == TOAST_LZ4_COMPRESSION_ID); \
+			   (cm) == TOAST_LZ4_COMPRESSION_ID || \
+			   (cm) == VARATT_EXTERNAL_EXTENDED_CMID); \
 		((toast_pointer).va_extinfo = \
 			(len) | ((uint32) (cm) << VARLENA_EXTSIZE_BITS)); \
 	} while (0)
@@ -539,4 +601,92 @@ VARATT_EXTERNAL_IS_COMPRESSED(struct varatt_external toast_pointer)
 		(Size) (toast_pointer.va_rawsize - VARHDRSZ);
 }
 
+/* Macros for extended TOAST pointers (varatt_external_extended) */
+
+/*
+ * Check if a TOAST pointer uses the extended on-disk format.
+ *
+ * Callers must have already verified VARATT_IS_EXTERNAL_ONDISK() before
+ * calling this; here we look only at the compression-method bits embedded
+ * in va_extinfo.
+ */
+static inline bool
+VARATT_EXTERNAL_IS_EXTENDED(struct varatt_external toast_pointer)
+{
+	return VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) ==
+		VARATT_EXTERNAL_EXTENDED_CMID;
+}
+
+/* Get feature flags from extended pointer */
+static inline uint8
+VARATT_EXTERNAL_GET_FLAGS(struct varatt_external_extended toast_pointer_ext)
+{
+	return toast_pointer_ext.va_flags;
+}
+
+/* Set feature flags in extended pointer */
+#define VARATT_EXTERNAL_SET_FLAGS(toast_pointer_ext, flags) \
+	do { \
+		(toast_pointer_ext).va_flags = (flags); \
+	} while (0)
+
+/* Test if a specific flag is set */
+#define VARATT_EXTERNAL_HAS_FLAG(toast_pointer_ext, flag) \
+	(((toast_pointer_ext).va_flags & (flag)) != 0)
+
+/* Get pointer to extension data array */
+#define VARATT_EXTERNAL_GET_EXT_DATA(toast_pointer_ext) \
+	((toast_pointer_ext).va_data)
+
+/* Get extended compression method (when TOAST_EXT_FLAG_COMPRESSION is set) */
+static inline uint8
+VARATT_EXTERNAL_GET_EXT_COMPRESSION_METHOD(struct varatt_external_extended toast_pointer_ext)
+{
+	return toast_pointer_ext.va_data[0];
+}
+
+/* Set extended compression method */
+#define VARATT_EXTERNAL_SET_EXT_COMPRESSION_METHOD(toast_pointer_ext, method) \
+	do { \
+		(toast_pointer_ext).va_data[0] = (method); \
+	} while (0)
+
+/* Get extsize and compress method from extended pointer (same as standard) */
+static inline Size
+VARATT_EXTERNAL_GET_EXTSIZE_EXTENDED(struct varatt_external_extended toast_pointer_ext)
+{
+	return toast_pointer_ext.va_extinfo & VARLENA_EXTSIZE_MASK;
+}
+
+static inline uint32
+VARATT_EXTERNAL_GET_COMPRESS_METHOD_EXTENDED(struct varatt_external_extended toast_pointer_ext)
+{
+	return toast_pointer_ext.va_extinfo >> VARLENA_EXTSIZE_BITS;
+}
+
+/* Set size and extended indicator in va_extinfo */
+#define VARATT_EXTERNAL_SET_SIZE_AND_EXT_FLAGS(toast_pointer_ext, len, flags) \
+	do { \
+		Assert((len) > 0 && (len) <= VARLENA_EXTSIZE_MASK); \
+		(toast_pointer_ext).va_extinfo = \
+			(len) | ((uint32) VARATT_EXTERNAL_EXTENDED_CMID << VARLENA_EXTSIZE_BITS); \
+		(toast_pointer_ext).va_flags = (flags); \
+		memset((toast_pointer_ext).va_data, 0, 3); \
+	} while (0)
+
+/* Convenience macro for setting extended pointer with compression method */
+#define VARATT_EXTERNAL_SET_SIZE_AND_EXT_COMPRESSION(toast_pointer_ext, len, method) \
+	do { \
+		VARATT_EXTERNAL_SET_SIZE_AND_EXT_FLAGS(toast_pointer_ext, len, VARATT_EXTERNAL_FLAG_COMPRESSION); \
+		VARATT_EXTERNAL_SET_EXT_COMPRESSION_METHOD(toast_pointer_ext, method); \
+	} while (0)
+
+/* Test if extended pointer is compressed (same logic as standard) */
+static inline bool
+VARATT_EXTERNAL_IS_COMPRESSED_EXTENDED(struct varatt_external_extended toast_pointer_ext)
+{
+	return VARATT_EXTERNAL_GET_EXTSIZE_EXTENDED(toast_pointer_ext) <
+		(Size) (toast_pointer_ext.va_rawsize - VARHDRSZ);
+}
+
 #endif
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 068fd859a8f..9dff119aa22 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -47,6 +47,7 @@ subdir('test_rls_hooks')
 subdir('test_shm_mq')
 subdir('test_slru')
 subdir('test_tidstore')
+subdir('test_toast_ext')
 subdir('typcache')
 subdir('unsafe_tests')
 subdir('worker_spi')
diff --git a/src/test/modules/test_toast_ext/Makefile b/src/test/modules/test_toast_ext/Makefile
new file mode 100644
index 00000000000..5e2409f918c
--- /dev/null
+++ b/src/test/modules/test_toast_ext/Makefile
@@ -0,0 +1,20 @@
+# src/test/modules/test_toast_ext/Makefile
+
+MODULE_big = test_toast_ext
+OBJS = test_toast_ext.o
+
+EXTENSION = test_toast_ext
+DATA = test_toast_ext--1.0.sql
+
+REGRESS = test_toast_ext
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_toast_ext
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_toast_ext/expected/test_toast_ext.out b/src/test/modules/test_toast_ext/expected/test_toast_ext.out
new file mode 100644
index 00000000000..539f4437655
--- /dev/null
+++ b/src/test/modules/test_toast_ext/expected/test_toast_ext.out
@@ -0,0 +1,187 @@
+--
+-- Tests for extended TOAST header structures and zstd compression
+--
+CREATE EXTENSION test_toast_ext;
+-- Use dedicated schema for test isolation
+CREATE SCHEMA test_toast_ext_schema;
+SET search_path TO test_toast_ext_schema, public;
+-- Compile-time validation tests (always run)
+-- These error out on failure, so completing without error = pass
+SELECT test_toast_structure_sizes();
+ test_toast_structure_sizes
+----------------------------
+
+(1 row)
+
+SELECT test_toast_flag_validation();
+ test_toast_flag_validation
+----------------------------
+
+(1 row)
+
+SELECT test_toast_compression_ids();
+ test_toast_compression_ids
+----------------------------
+
+(1 row)
+
+--
+-- Functional tests for zstd TOAST compression
+-- Skip if not built with USE_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
+-- Test basic zstd compression
+CREATE TABLE test_zstd_basic (id serial, data text COMPRESSION zstd);
+INSERT INTO test_zstd_basic (data)
+    VALUES (repeat('PostgreSQL zstd TOAST compression test. ', 3000));
+SELECT id,
+       pg_column_compression(data) AS compression,
+       length(data) AS data_length,
+       left(data, 42) AS data_prefix
+FROM test_zstd_basic;
+ id | compression | data_length |                data_prefix
+----+-------------+-------------+--------------------------------------------
+  1 | zstd        |      120000 | PostgreSQL zstd TOAST compression test. Po
+(1 row)
+
+-- Test slice access
+SELECT id, substr(data, 100, 42) AS slice FROM test_zstd_basic;
+ id |                   slice
+----+--------------------------------------------
+  1 | ST compression test. PostgreSQL zstd TOAST
+(1 row)
+
+-- Test UPDATE
+UPDATE test_zstd_basic SET data = repeat('Updated zstd data for TOAST test. ', 3000);
+SELECT id,
+       pg_column_compression(data) AS compression,
+       length(data) AS data_length,
+       left(data, 35) AS data_prefix
+FROM test_zstd_basic;
+ id | compression | data_length |             data_prefix
+----+-------------+-------------+-------------------------------------
+  1 | zstd        |      102000 | Updated zstd data for TOAST test. U
+(1 row)
+
+-- Test extended header with pglz
+SET use_extended_toast_header = on;
+CREATE TABLE test_pglz_extended (data text COMPRESSION pglz);
+INSERT INTO test_pglz_extended (data)
+    VALUES (repeat('PGLZ with extended header format. ', 3000));
+SELECT pg_column_compression(data) AS compression,
+       length(data) AS data_length
+FROM test_pglz_extended;
+ compression | data_length
+-------------+-------------
+ pglz        |      102000
+(1 row)
+
+SELECT substr(data, 50, 34) AS slice FROM test_pglz_extended;
+               slice
+------------------------------------
+ ded header format. PGLZ with exten
+(1 row)
+
+-- Test data integrity
+CREATE TABLE test_integrity (
+    method text,
+    original_data text,
+    compressed_data text
+);
+INSERT INTO test_integrity VALUES
+    ('pglz', repeat('Integrity test data pattern. ', 2000), NULL),
+    ('zstd', repeat('Integrity test data pattern. ', 2000), NULL);
+CREATE TABLE test_pglz_integrity (data text COMPRESSION pglz);
+CREATE TABLE test_zstd_integrity (data text COMPRESSION zstd);
+INSERT INTO test_pglz_integrity SELECT original_data FROM test_integrity WHERE method = 'pglz';
+INSERT INTO test_zstd_integrity SELECT original_data FROM test_integrity WHERE method = 'zstd';
+SELECT 'pglz' AS method,
+       md5((SELECT original_data FROM test_integrity WHERE method = 'pglz')) =
+       md5((SELECT data FROM test_pglz_integrity)) AS checksum_match;
+ method | checksum_match
+--------+----------------
+ pglz   | t
+(1 row)
+
+SELECT 'zstd' AS method,
+       md5((SELECT original_data FROM test_integrity WHERE method = 'zstd')) =
+       md5((SELECT data FROM test_zstd_integrity)) AS checksum_match;
+ method | checksum_match
+--------+----------------
+ zstd   | t
+(1 row)
+
+-- Test CLUSTER and VACUUM FULL
+CREATE TABLE test_cluster_zstd (id serial PRIMARY KEY, data text COMPRESSION zstd);
+INSERT INTO test_cluster_zstd (data)
+    VALUES (repeat('Data for CLUSTER test with zstd compression. ', 2500));
+SELECT 'before_cluster' AS stage, md5(data) AS hash FROM test_cluster_zstd;
+     stage      |               hash
+----------------+----------------------------------
+ before_cluster | b4132e799bbd065a7e9266159aa82dc1
+(1 row)
+
+CLUSTER test_cluster_zstd USING test_cluster_zstd_pkey;
+SELECT 'after_cluster' AS stage,
+       pg_column_compression(data) AS compression,
+       md5(data) AS hash
+FROM test_cluster_zstd;
+     stage     | compression |               hash
+---------------+-------------+----------------------------------
+ after_cluster | zstd        | b4132e799bbd065a7e9266159aa82dc1
+(1 row)
+
+VACUUM FULL test_cluster_zstd;
+SELECT 'after_vacuum_full' AS stage,
+       pg_column_compression(data) AS compression,
+       md5(data) AS hash
+FROM test_cluster_zstd;
+       stage       | compression |               hash
+-------------------+-------------+----------------------------------
+ after_vacuum_full | zstd        | b4132e799bbd065a7e9266159aa82dc1
+(1 row)
+
+-- Test GUC toggling (mixed formats in same table)
+SET use_extended_toast_header = on;
+CREATE TABLE test_guc_toggle (id serial, data text COMPRESSION pglz);
+INSERT INTO test_guc_toggle (data)
+    VALUES (repeat('Data created with extended header on. ', 3000));
+SELECT 'with_ext_on' AS stage,
+       pg_column_compression(data) AS compression,
+       length(data) AS data_length
+FROM test_guc_toggle;
+    stage    | compression | data_length
+-------------+-------------+-------------
+ with_ext_on | pglz        |      114000
+(1 row)
+
+SET use_extended_toast_header = off;
+INSERT INTO test_guc_toggle (data)
+    VALUES (repeat('Data created with extended header off. ', 3000));
+SELECT id,
+       pg_column_compression(data) AS compression,
+       length(data) AS data_length,
+       left(data, 39) AS data_prefix
+FROM test_guc_toggle ORDER BY id;
+ id | compression | data_length |               data_prefix
+----+-------------+-------------+-----------------------------------------
+  1 | pglz        |      114000 | Data created with extended header on. D
+  2 | pglz        |      117000 | Data created with extended header off.
+(2 rows)
+
+SET use_extended_toast_header = on;
+SELECT id, length(data) AS data_length FROM test_guc_toggle ORDER BY id;
+ id | data_length
+----+-------------
+  1 |      114000
+  2 |      117000
+(2 rows)
+
+-- Cleanup
+DROP SCHEMA test_toast_ext_schema CASCADE;
+DROP EXTENSION test_toast_ext;
diff --git a/src/test/modules/test_toast_ext/expected/test_toast_ext_1.out b/src/test/modules/test_toast_ext/expected/test_toast_ext_1.out
new file mode 100644
index 00000000000..897661fc2a4
--- /dev/null
+++ b/src/test/modules/test_toast_ext/expected/test_toast_ext_1.out
@@ -0,0 +1,37 @@
+--
+-- Tests for extended TOAST header structures and zstd compression
+--
+CREATE EXTENSION test_toast_ext;
+-- Use dedicated schema for test isolation
+CREATE SCHEMA test_toast_ext_schema;
+SET search_path TO test_toast_ext_schema, public;
+-- Compile-time validation tests (always run)
+-- These error out on failure, so completing without error = pass
+SELECT test_toast_structure_sizes();
+ test_toast_structure_sizes
+----------------------------
+
+(1 row)
+
+SELECT test_toast_flag_validation();
+ test_toast_flag_validation
+----------------------------
+
+(1 row)
+
+SELECT test_toast_compression_ids();
+ test_toast_compression_ids
+----------------------------
+
+(1 row)
+
+--
+-- Functional tests for zstd TOAST compression
+-- Skip if not built with USE_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/modules/test_toast_ext/meson.build b/src/test/modules/test_toast_ext/meson.build
new file mode 100644
index 00000000000..61c07ea1912
--- /dev/null
+++ b/src/test/modules/test_toast_ext/meson.build
@@ -0,0 +1,33 @@
+# Copyright (c) 2022-2025, PostgreSQL Global Development Group
+
+test_toast_ext_sources = files(
+  'test_toast_ext.c',
+)
+
+if host_system == 'windows'
+  test_toast_ext_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_toast_ext',
+    '--FILEDESC', 'test_toast_ext - test code for extended TOAST headers',])
+endif
+
+test_toast_ext = shared_module('test_toast_ext',
+  test_toast_ext_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_toast_ext
+
+test_install_data += files(
+  'test_toast_ext.control',
+  'test_toast_ext--1.0.sql',
+)
+
+tests += {
+  'name': 'test_toast_ext',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'regress': {
+    'sql': [
+      'test_toast_ext',
+    ],
+  },
+}
diff --git a/src/test/modules/test_toast_ext/sql/test_toast_ext.sql b/src/test/modules/test_toast_ext/sql/test_toast_ext.sql
new file mode 100644
index 00000000000..82e36c57b34
--- /dev/null
+++ b/src/test/modules/test_toast_ext/sql/test_toast_ext.sql
@@ -0,0 +1,136 @@
+--
+-- Tests for extended TOAST header structures and zstd compression
+--
+
+CREATE EXTENSION test_toast_ext;
+
+-- Use dedicated schema for test isolation
+CREATE SCHEMA test_toast_ext_schema;
+SET search_path TO test_toast_ext_schema, public;
+
+-- Compile-time validation tests (always run)
+-- These error out on failure, so completing without error = pass
+SELECT test_toast_structure_sizes();
+SELECT test_toast_flag_validation();
+SELECT test_toast_compression_ids();
+
+--
+-- Functional tests for zstd TOAST compression
+-- Skip if not built with USE_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
+
+-- Test basic zstd compression
+CREATE TABLE test_zstd_basic (id serial, data text COMPRESSION zstd);
+INSERT INTO test_zstd_basic (data)
+    VALUES (repeat('PostgreSQL zstd TOAST compression test. ', 3000));
+
+SELECT id,
+       pg_column_compression(data) AS compression,
+       length(data) AS data_length,
+       left(data, 42) AS data_prefix
+FROM test_zstd_basic;
+
+-- Test slice access
+SELECT id, substr(data, 100, 42) AS slice FROM test_zstd_basic;
+
+-- Test UPDATE
+UPDATE test_zstd_basic SET data = repeat('Updated zstd data for TOAST test. ', 3000);
+SELECT id,
+       pg_column_compression(data) AS compression,
+       length(data) AS data_length,
+       left(data, 35) AS data_prefix
+FROM test_zstd_basic;
+
+-- Test extended header with pglz
+SET use_extended_toast_header = on;
+
+CREATE TABLE test_pglz_extended (data text COMPRESSION pglz);
+INSERT INTO test_pglz_extended (data)
+    VALUES (repeat('PGLZ with extended header format. ', 3000));
+
+SELECT pg_column_compression(data) AS compression,
+       length(data) AS data_length
+FROM test_pglz_extended;
+
+SELECT substr(data, 50, 34) AS slice FROM test_pglz_extended;
+
+-- Test data integrity
+CREATE TABLE test_integrity (
+    method text,
+    original_data text,
+    compressed_data text
+);
+
+INSERT INTO test_integrity VALUES
+    ('pglz', repeat('Integrity test data pattern. ', 2000), NULL),
+    ('zstd', repeat('Integrity test data pattern. ', 2000), NULL);
+
+CREATE TABLE test_pglz_integrity (data text COMPRESSION pglz);
+CREATE TABLE test_zstd_integrity (data text COMPRESSION zstd);
+
+INSERT INTO test_pglz_integrity SELECT original_data FROM test_integrity WHERE method = 'pglz';
+INSERT INTO test_zstd_integrity SELECT original_data FROM test_integrity WHERE method = 'zstd';
+
+SELECT 'pglz' AS method,
+       md5((SELECT original_data FROM test_integrity WHERE method = 'pglz')) =
+       md5((SELECT data FROM test_pglz_integrity)) AS checksum_match;
+
+SELECT 'zstd' AS method,
+       md5((SELECT original_data FROM test_integrity WHERE method = 'zstd')) =
+       md5((SELECT data FROM test_zstd_integrity)) AS checksum_match;
+
+-- Test CLUSTER and VACUUM FULL
+CREATE TABLE test_cluster_zstd (id serial PRIMARY KEY, data text COMPRESSION zstd);
+INSERT INTO test_cluster_zstd (data)
+    VALUES (repeat('Data for CLUSTER test with zstd compression. ', 2500));
+
+SELECT 'before_cluster' AS stage, md5(data) AS hash FROM test_cluster_zstd;
+
+CLUSTER test_cluster_zstd USING test_cluster_zstd_pkey;
+
+SELECT 'after_cluster' AS stage,
+       pg_column_compression(data) AS compression,
+       md5(data) AS hash
+FROM test_cluster_zstd;
+
+VACUUM FULL test_cluster_zstd;
+
+SELECT 'after_vacuum_full' AS stage,
+       pg_column_compression(data) AS compression,
+       md5(data) AS hash
+FROM test_cluster_zstd;
+
+-- Test GUC toggling (mixed formats in same table)
+SET use_extended_toast_header = on;
+CREATE TABLE test_guc_toggle (id serial, data text COMPRESSION pglz);
+INSERT INTO test_guc_toggle (data)
+    VALUES (repeat('Data created with extended header on. ', 3000));
+
+SELECT 'with_ext_on' AS stage,
+       pg_column_compression(data) AS compression,
+       length(data) AS data_length
+FROM test_guc_toggle;
+
+SET use_extended_toast_header = off;
+INSERT INTO test_guc_toggle (data)
+    VALUES (repeat('Data created with extended header off. ', 3000));
+
+SELECT id,
+       pg_column_compression(data) AS compression,
+       length(data) AS data_length,
+       left(data, 39) AS data_prefix
+FROM test_guc_toggle ORDER BY id;
+
+SET use_extended_toast_header = on;
+SELECT id, length(data) AS data_length FROM test_guc_toggle ORDER BY id;
+
+-- Cleanup
+DROP SCHEMA test_toast_ext_schema CASCADE;
+DROP EXTENSION test_toast_ext;
diff --git a/src/test/modules/test_toast_ext/test_toast_ext--1.0.sql b/src/test/modules/test_toast_ext/test_toast_ext--1.0.sql
new file mode 100644
index 00000000000..f74d5069fbf
--- /dev/null
+++ b/src/test/modules/test_toast_ext/test_toast_ext--1.0.sql
@@ -0,0 +1,19 @@
+/* src/test/modules/test_toast_ext/test_toast_ext--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_toast_ext" to load this file. \quit
+
+CREATE FUNCTION test_toast_structure_sizes()
+RETURNS void
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_toast_flag_validation()
+RETURNS void
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_toast_compression_ids()
+RETURNS void
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT;
diff --git a/src/test/modules/test_toast_ext/test_toast_ext.c b/src/test/modules/test_toast_ext/test_toast_ext.c
new file mode 100644
index 00000000000..59884f2b6d0
--- /dev/null
+++ b/src/test/modules/test_toast_ext/test_toast_ext.c
@@ -0,0 +1,140 @@
+/*-------------------------------------------------------------------------
+ *
+ * test_toast_ext.c
+ *		Test module for extended TOAST header structures.
+ *
+ * Copyright (c) 2025, PostgreSQL Global Development Group
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "access/detoast.h"
+#include "access/toast_compression.h"
+#include "varatt.h"
+
+PG_MODULE_MAGIC;
+
+PG_FUNCTION_INFO_V1(test_toast_structure_sizes);
+PG_FUNCTION_INFO_V1(test_toast_flag_validation);
+PG_FUNCTION_INFO_V1(test_toast_compression_ids);
+
+/*
+ * Verify TOAST structure sizes match expected values.
+ * Errors out if any size is wrong (catches ABI issues).
+ */
+Datum
+test_toast_structure_sizes(PG_FUNCTION_ARGS)
+{
+	/* Standard structure must be 16 bytes */
+	if (sizeof(varatt_external) != 16)
+		elog(ERROR, "varatt_external is %zu bytes, expected 16",
+			 sizeof(varatt_external));
+
+	/* Extended structure must be 20 bytes */
+	if (sizeof(varatt_external_extended) != 20)
+		elog(ERROR, "varatt_external_extended is %zu bytes, expected 20",
+			 sizeof(varatt_external_extended));
+
+	/* TOAST pointer sizes (include 2-byte external header) */
+	if (TOAST_POINTER_SIZE != 18)
+		elog(ERROR, "TOAST_POINTER_SIZE is %zu, expected 18",
+			 (Size) TOAST_POINTER_SIZE);
+
+	if (TOAST_POINTER_SIZE_EXTENDED != 22)
+		elog(ERROR, "TOAST_POINTER_SIZE_EXTENDED is %zu, expected 22",
+			 (Size) TOAST_POINTER_SIZE_EXTENDED);
+
+	/* Verify field offsets (no unexpected padding) */
+	if (offsetof(varatt_external_extended, va_rawsize) != 0)
+		elog(ERROR, "va_rawsize offset is %zu, expected 0",
+			 offsetof(varatt_external_extended, va_rawsize));
+	if (offsetof(varatt_external_extended, va_extinfo) != 4)
+		elog(ERROR, "va_extinfo offset is %zu, expected 4",
+			 offsetof(varatt_external_extended, va_extinfo));
+	if (offsetof(varatt_external_extended, va_flags) != 8)
+		elog(ERROR, "va_flags offset is %zu, expected 8",
+			 offsetof(varatt_external_extended, va_flags));
+	if (offsetof(varatt_external_extended, va_data) != 9)
+		elog(ERROR, "va_data offset is %zu, expected 9",
+			 offsetof(varatt_external_extended, va_data));
+	if (offsetof(varatt_external_extended, va_valueid) != 12)
+		elog(ERROR, "va_valueid offset is %zu, expected 12",
+			 offsetof(varatt_external_extended, va_valueid));
+	if (offsetof(varatt_external_extended, va_toastrelid) != 16)
+		elog(ERROR, "va_toastrelid offset is %zu, expected 16",
+			 offsetof(varatt_external_extended, va_toastrelid));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Verify flag validation macros work correctly.
+ */
+Datum
+test_toast_flag_validation(PG_FUNCTION_ARGS)
+{
+	/* Valid flags should pass */
+	if (!ExtendedFlagsAreValid(0x00))
+		elog(ERROR, "flags 0x00 should be valid");
+	if (!ExtendedFlagsAreValid(0x01))
+		elog(ERROR, "flags 0x01 should be valid");
+	if (!ExtendedFlagsAreValid(0x02))
+		elog(ERROR, "flags 0x02 should be valid");
+	if (!ExtendedFlagsAreValid(0x03))
+		elog(ERROR, "flags 0x03 should be valid");
+
+	/* Invalid flags should fail */
+	if (ExtendedFlagsAreValid(0x04))
+		elog(ERROR, "flags 0x04 should be invalid");
+	if (ExtendedFlagsAreValid(0x08))
+		elog(ERROR, "flags 0x08 should be invalid");
+	if (ExtendedFlagsAreValid(0xFF))
+		elog(ERROR, "flags 0xFF should be invalid");
+
+	/* Compression methods 0-255 are valid */
+	if (!ExtendedCompressionMethodIsValid(0))
+		elog(ERROR, "compression method 0 should be valid");
+	if (!ExtendedCompressionMethodIsValid(255))
+		elog(ERROR, "compression method 255 should be valid");
+
+	/* Verify method ID constants */
+	if (TOAST_PGLZ_EXT_METHOD != 0)
+		elog(ERROR, "TOAST_PGLZ_EXT_METHOD is %d, expected 0", TOAST_PGLZ_EXT_METHOD);
+	if (TOAST_LZ4_EXT_METHOD != 1)
+		elog(ERROR, "TOAST_LZ4_EXT_METHOD is %d, expected 1", TOAST_LZ4_EXT_METHOD);
+	if (TOAST_ZSTD_EXT_METHOD != 2)
+		elog(ERROR, "TOAST_ZSTD_EXT_METHOD is %d, expected 2", TOAST_ZSTD_EXT_METHOD);
+	if (TOAST_UNCOMPRESSED_EXT_METHOD != 3)
+		elog(ERROR, "TOAST_UNCOMPRESSED_EXT_METHOD is %d, expected 3", TOAST_UNCOMPRESSED_EXT_METHOD);
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Verify compression ID constants are consistent.
+ */
+Datum
+test_toast_compression_ids(PG_FUNCTION_ARGS)
+{
+	/* Standard compression IDs */
+	if (TOAST_PGLZ_COMPRESSION_ID != 0)
+		elog(ERROR, "TOAST_PGLZ_COMPRESSION_ID is %d, expected 0", TOAST_PGLZ_COMPRESSION_ID);
+	if (TOAST_LZ4_COMPRESSION_ID != 1)
+		elog(ERROR, "TOAST_LZ4_COMPRESSION_ID is %d, expected 1", TOAST_LZ4_COMPRESSION_ID);
+	if (TOAST_INVALID_COMPRESSION_ID != 2)
+		elog(ERROR, "TOAST_INVALID_COMPRESSION_ID is %d, expected 2", TOAST_INVALID_COMPRESSION_ID);
+	if (TOAST_EXTENDED_COMPRESSION_ID != 3)
+		elog(ERROR, "TOAST_EXTENDED_COMPRESSION_ID is %d, expected 3", TOAST_EXTENDED_COMPRESSION_ID);
+
+	/* Extended IDs should match standard where applicable */
+	if (TOAST_PGLZ_EXT_METHOD != TOAST_PGLZ_COMPRESSION_ID)
+		elog(ERROR, "PGLZ IDs mismatch: standard=%d, extended=%d",
+			 TOAST_PGLZ_COMPRESSION_ID, TOAST_PGLZ_EXT_METHOD);
+	if (TOAST_LZ4_EXT_METHOD != TOAST_LZ4_COMPRESSION_ID)
+		elog(ERROR, "LZ4 IDs mismatch: standard=%d, extended=%d",
+			 TOAST_LZ4_COMPRESSION_ID, TOAST_LZ4_EXT_METHOD);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/test/modules/test_toast_ext/test_toast_ext.control b/src/test/modules/test_toast_ext/test_toast_ext.control
new file mode 100644
index 00000000000..d59ee14ad64
--- /dev/null
+++ b/src/test/modules/test_toast_ext/test_toast_ext.control
@@ -0,0 +1,5 @@
+# test_toast_ext extension
+comment = 'Test module for extended TOAST headers and zstd compression'
+default_version = '1.0'
+module_pathname = '$libdir/test_toast_ext'
+relocatable = true
-- 
2.39.3 (Apple Git-146)

#7Peter Eisentraut
peter@eisentraut.org
In reply to: Dharin Shah (#6)
Re: Fwd: [PATCH] Add zstd compression for TOAST using extended header format

On 16.12.25 11:51, Dharin Shah wrote:

- Zstd only applies to external TOAST, not inline compression. The 2-bit
limit in va_tcinfo stays as-is for inline data, where pglz/lz4 work fine
anyway. Zstd's wins show up on larger values.

This is a very complicated patch. To motivate it, you should show some
detailed performance measurements that show these wins.

#8Michael Paquier
michael@paquier.xyz
In reply to: Peter Eisentraut (#7)
Re: Fwd: [PATCH] Add zstd compression for TOAST using extended header format

On Wed, Dec 17, 2025 at 04:11:38PM +0100, Peter Eisentraut wrote:

On 16.12.25 11:51, Dharin Shah wrote:

- Zstd only applies to external TOAST, not inline compression. The 2-bit
limit in va_tcinfo stays as-is for inline data, where pglz/lz4 work fine
anyway. Zstd's wins show up on larger values.

This is a very complicated patch. To motivate it, you should show some
detailed performance measurements that show these wins.

Yes, this is expected for any patch posted. Zstd is an improved
version of lz4, acting as a sort of industry standard these days, and
any byte sequences I have looked at points that zstd leads kind of
always to a better compression ratio for less or equivalent CPU cost
compared to LZ4. Not saying that numbers are not required, they are.
But I strongly suspect numbers among these lines.

FWIW, it's not a complicated patch, it is a large mechanical patch
that enforces a bunch of TOAST code paths to do what it wants. If we
are going to do something about that and agree on something, I think
that we should just use a new vartag_external for this matter
(spoiler: I think we should use a new vartag_external value), but
keep the toast structure at 16 bytes all the time, leaving alone the
extra bit in the existing varatt_external structure so as there is no
impact for heap relations if zstd is used, as long as the TOAST value
is 32 bits. The patch introduces a new vartag_external with
VARTAG_ONDISK_EXTENDED, so while it leads to a better compatibility,
it also means that all zstd entries have to pay an extra amount of
space in the main relation as an effect of a different
default_toast_compression. The difficulty is not in the
implementation, it would be on agreeing on what folks would be OK
with in terms if vartag and varatt structures, and that's one of the
oldest areas of the PG code, that has complications and assumptions of
its own.

The test implementation looks wrong to me. Why is there any need for
an extra test module test_toast_ext? You could just reuse the same
structure as compression_lz4.sql, but adapted to zstd. That's why a
split with compression.sql has been done in 74a3fc36f314, FYI.

You should also aim at splitting the patch more to make it easier to
review: one of the sticky point of this area of the code is to untie
completely DefaultCompressionId, its GUC and the TOAST code. The GUC
default_toast_compression accepts by design only 4 values. This needs
to go further, and should be refactored as a piece of its own.

And also, I would prefer if the 32-bit value issue is tackled first,
but that's a digression here, for a different thread. :)
--
Michael

#9Dharin Shah
dharinshah95@gmail.com
In reply to: Michael Paquier (#8)
Re: Fwd: [PATCH] Add zstd compression for TOAST using extended header format

Hi Michael (and Peter),

Thanks for the detailed feedback — this is really helpful.

I want to make sure I understand your main point: you're OK with a new
`vartag_external`, but prefer we avoid increasing the heap TOAST pointer
from 16 -> 20 bytes since every zstd-toasted value would pay +4 bytes in
the main heap tuple.
I also realize the "compatibility" of the extended header doesn't buy us
much — we'll need to support the existing 16-byte varatt_external forever
for backward compatibility. Adding a 20-byte structure just means two
formats to maintain indefinitely.

A couple clarifying questions if we go with new vartag (e.g.,
`VARTAG_ONDISK_ZSTD`), same 16-byte `varatt_external` payload, vartag as
discriminator
1. How should we handle future methods beyond zstd? One tag per method, or
store a method id elsewhere (e.g., in TOAST chunk header)?
2. And re: "as long as the TOAST value is 32 bits" — are you referring to
the 30-bit extsize field in va_extinfo (i.e., avoid stealing bits from
extsize for method encoding)?

Test

Rows

Uncompressed

PGLZ

LZ4

ZSTD

PGLZ/ZSTD

LZ4/ZSTD

T1: Large JSON (~18KB/row)

500

~9,000 KB

1496 KB

1528 KB

976 KB

1.53x

1.57x

T2: Repetitive Text (~246KB/row)

500

~123,000 KB

1672 KB

648 KB

248 KB

6.74x

2.61x

T3: MD5 Hash Data (~16KB/row)

500

~8,000 KB

8288 KB

8232 KB

4256 KB

1.95x

1.93x

T4: Server Logs (~3.5KB/row)

1000

~3,500 KB

400 KB

352 KB

456 KB

0.88x

0.77x

*Key findings (i guess well known at this point):*
- ZSTD excels for repetitive/pattern-heavy data (6.7x better than PGLZ)
- For low-redundancy data (MD5 hashes), ZSTD still achieves ~2x better
- The T4 result showing zstd as "worse" is not about compression quality -
it's about missing inline storage support. ZSTD actually compresses better,
but pays unnecessary TOAST overhead.

I'll share the detailed benchmark script with the next patch revision. But
also a potential path forward could be that we could just fully replace
pglz (can bring it up later in different thread)

*On Testing and Patch Structure*
Agreed on both points:
- I'll use `compression_zstd.sql` following the `compression_lz4.sql`
pattern (removing the test_toast_ext module)
- I'll split the GUC refactoring into a separate preparatory patch

Once you confirm which representation you're advocating, I'll respin
accordingly.

Thanks,
Dharin

On Thu, Dec 18, 2025 at 7:35 AM Michael Paquier <michael@paquier.xyz> wrote:

Show quoted text

On Wed, Dec 17, 2025 at 04:11:38PM +0100, Peter Eisentraut wrote:

On 16.12.25 11:51, Dharin Shah wrote:

- Zstd only applies to external TOAST, not inline compression. The

2-bit

limit in va_tcinfo stays as-is for inline data, where pglz/lz4 work

fine

anyway. Zstd's wins show up on larger values.

This is a very complicated patch. To motivate it, you should show some
detailed performance measurements that show these wins.

Yes, this is expected for any patch posted. Zstd is an improved
version of lz4, acting as a sort of industry standard these days, and
any byte sequences I have looked at points that zstd leads kind of
always to a better compression ratio for less or equivalent CPU cost
compared to LZ4. Not saying that numbers are not required, they are.
But I strongly suspect numbers among these lines.

FWIW, it's not a complicated patch, it is a large mechanical patch
that enforces a bunch of TOAST code paths to do what it wants. If we
are going to do something about that and agree on something, I think
that we should just use a new vartag_external for this matter
(spoiler: I think we should use a new vartag_external value), but
keep the toast structure at 16 bytes all the time, leaving alone the
extra bit in the existing varatt_external structure so as there is no
impact for heap relations if zstd is used, as long as the TOAST value
is 32 bits. The patch introduces a new vartag_external with
VARTAG_ONDISK_EXTENDED, so while it leads to a better compatibility,
it also means that all zstd entries have to pay an extra amount of
space in the main relation as an effect of a different
default_toast_compression. The difficulty is not in the
implementation, it would be on agreeing on what folks would be OK
with in terms if vartag and varatt structures, and that's one of the
oldest areas of the PG code, that has complications and assumptions of
its own.

The test implementation looks wrong to me. Why is there any need for
an extra test module test_toast_ext? You could just reuse the same
structure as compression_lz4.sql, but adapted to zstd. That's why a
split with compression.sql has been done in 74a3fc36f314, FYI.

You should also aim at splitting the patch more to make it easier to
review: one of the sticky point of this area of the code is to untie
completely DefaultCompressionId, its GUC and the TOAST code. The GUC
default_toast_compression accepts by design only 4 values. This needs
to go further, and should be refactored as a piece of its own.

And also, I would prefer if the 32-bit value issue is tackled first,
but that's a digression here, for a different thread. :)
--
Michael

#10Michael Paquier
michael@paquier.xyz
In reply to: Dharin Shah (#9)
Re: Fwd: [PATCH] Add zstd compression for TOAST using extended header format

On Thu, Dec 18, 2025 at 10:44:22PM +0100, Dharin Shah wrote:

I want to make sure I understand your main point: you're OK with a new
`vartag_external`, but prefer we avoid increasing the heap TOAST pointer
from 16 -> 20 bytes since every zstd-toasted value would pay +4 bytes in
the main heap tuple.

That would be my choice, yes. Not sure about the opinion of others on
this matter.

I also realize the "compatibility" of the extended header doesn't buy us
much — we'll need to support the existing 16-byte varatt_external forever
for backward compatibility. Adding a 20-byte structure just means two
formats to maintain indefinitely.

Yes. Patches have to maintain on-disk compatibility.

A couple clarifying questions if we go with new vartag (e.g.,
`VARTAG_ONDISK_ZSTD`), same 16-byte `varatt_external` payload, vartag as
discriminator
1. How should we handle future methods beyond zstd? One tag per method, or
store a method id elsewhere (e.g., in TOAST chunk header)?

My suspicion would be that we could either use a new set of vartags in
the future for each compression method. When it comes to zstd there
is something that comes in play: we could set some bits related to
dictionnaries at tuple level. Not sure if this is the best design or
if using an attribute-level option is more adapted (for example a
JSONB blob could be applied as an attribute with common keys in a
dictionnary saving a lot of on-disk space even before compression),
but keeping some bits free in the 16-byte header leaves this option
open with a new vartag_external. Saying that, zstd is good enough
that I strongly suspect that we would not regret it for quite a few
years. One issue that has pushed towards the addition of lz4 as an
option for toast compression is that pglz was worse in terms of CPU
cost. zlib is also more expensive than lz4 or zstd, especially at
very high compression level for usually little compression gains.

2. And re: "as long as the TOAST value is 32 bits" — are you referring to
the 30-bit extsize field in va_extinfo (i.e., avoid stealing bits from
extsize for method encoding)?

I mean extending the TOAST value to 8 bytes, as per the following
issues:
/messages/by-id/764273.1669674269@sss.pgh.pa.us
https://commitfest.postgresql.org/patch/5830/

*Key findings (i guess well known at this point):*
- ZSTD excels for repetitive/pattern-heavy data (6.7x better than PGLZ)
- For low-redundancy data (MD5 hashes), ZSTD still achieves ~2x better
- The T4 result showing zstd as "worse" is not about compression quality -
it's about missing inline storage support. ZSTD actually compresses better,
but pays unnecessary TOAST overhead.

I'll share the detailed benchmark script with the next patch revision. But
also a potential path forward could be that we could just fully replace
pglz (can bring it up later in different thread)

I don't think that we will ever be able to remove pglz. It would be
nice, as final result of course, but I also expect that not being able
to decompress pglz data is going to lead to a lot of user pain. That
would be also very expensive to check at upgrade for large instances.

*On Testing and Patch Structure*
Agreed on both points:
- I'll use `compression_zstd.sql` following the `compression_lz4.sql`
pattern (removing the test_toast_ext module)

Okay.

- I'll split the GUC refactoring into a separate preparatory patch

This refactoring, if done nicely, is worth an independent piece. It's
something that I have actually done for the sake of the other thread,
though the result was not really much liked by others. Perhaps I'm
just lacking imagination with this abstraction, and I'd surely welcome
different ideas.
--
Michael

#11Dharin Shah
dharinshah95@gmail.com
In reply to: Michael Paquier (#10)
3 attachment(s)
Re: Fwd: [PATCH] Add zstd compression for TOAST using extended header format

Hello,

Following up on my earlier patch submission, I've reworked the zstd TOAST
compression implementation based on our discussion here. The new patch now
avoids the 20-byte extended header.

Current Approach
- New `VARTAG_ONDISK_ZSTD` (value 19) for ZSTD external storage
- Maintains existing 16-byte varatt_external structure
- ZSTD external-only (no inline compression)

Note: Using a dedicated VARTAG_ONDISK_ZSTD keeps the on-disk TOAST pointer
payload at 16 bytes, but it is not a general extensible metadata carrier.
If PostgreSQL later adopts a more general extensible TOAST framework, this
change should not block it; VARTAG_ONDISK_ZSTD would remain as a supported
legacy encoding, while new toasted values could be written using the newer
framework and old values rewritten via normal table rewrites.

Storage (170 MB uncompressed):
ZSTD: 22 MB (7.60x) - 38.7% space savings vs LZ4
PGLZ: 36 MB (4.76x)
LZ4: 36 MB (4.66x)

Key findings:
- Large values (>50KB): ZSTD 33% better compression than PGLZ (~30% better
than LZ4)
- Low-entropy data: ZSTD compresses what LZ77 methods cannot
- Small values: ZSTD pays external overhead vs inline PGLZ/LZ4
While ZSTD uses slightly less space overall, the external storage mechanism
incurs a TOAST fetch overhead for small values, potentially impacting
performance.
Backwards Compatibility Tests
- Mixed compression: Rows with PGLZ, LZ4, and ZSTD coexist and decompress
correctly
- Lazy recompression: ALTER COLUMN ... SET COMPRESSION zstd affects new
data; existing data is lazily recompressed upon UPDATE or VACUUM FULL.
- Inline vs external: Small values remain inline; large values use
appropriate external compression.
Data integrity: All data decompresses correctly across all methods.

Trade-offs and Design Considerations

- External-only avoids consuming cmid=3 and extended header complexity

- Slice access: no ZSTD-specific optimization (follow-up area)

- Hybrid inline/external for small values: not in this patch (feedback
welcome)

Reviewer Questions - Is vartag-based external-only acceptable?
- Should compression level (currently 3) be configurable? - Is the external
storage overhead for small values acceptable, or is hybrid inline/external
behavior needed?
Thanks, Dharin

On Thu, Dec 18, 2025 at 11:44 PM Michael Paquier <michael@paquier.xyz>
wrote:

Show quoted text

On Thu, Dec 18, 2025 at 10:44:22PM +0100, Dharin Shah wrote:

I want to make sure I understand your main point: you're OK with a new
`vartag_external`, but prefer we avoid increasing the heap TOAST pointer
from 16 -> 20 bytes since every zstd-toasted value would pay +4 bytes in
the main heap tuple.

That would be my choice, yes. Not sure about the opinion of others on
this matter.

I also realize the "compatibility" of the extended header doesn't buy us
much — we'll need to support the existing 16-byte varatt_external forever
for backward compatibility. Adding a 20-byte structure just means two
formats to maintain indefinitely.

Yes. Patches have to maintain on-disk compatibility.

A couple clarifying questions if we go with new vartag (e.g.,
`VARTAG_ONDISK_ZSTD`), same 16-byte `varatt_external` payload, vartag as
discriminator
1. How should we handle future methods beyond zstd? One tag per method,

or

store a method id elsewhere (e.g., in TOAST chunk header)?

My suspicion would be that we could either use a new set of vartags in
the future for each compression method. When it comes to zstd there
is something that comes in play: we could set some bits related to
dictionnaries at tuple level. Not sure if this is the best design or
if using an attribute-level option is more adapted (for example a
JSONB blob could be applied as an attribute with common keys in a
dictionnary saving a lot of on-disk space even before compression),
but keeping some bits free in the 16-byte header leaves this option
open with a new vartag_external. Saying that, zstd is good enough
that I strongly suspect that we would not regret it for quite a few
years. One issue that has pushed towards the addition of lz4 as an
option for toast compression is that pglz was worse in terms of CPU
cost. zlib is also more expensive than lz4 or zstd, especially at
very high compression level for usually little compression gains.

2. And re: "as long as the TOAST value is 32 bits" — are you referring to
the 30-bit extsize field in va_extinfo (i.e., avoid stealing bits from
extsize for method encoding)?

I mean extending the TOAST value to 8 bytes, as per the following
issues:
/messages/by-id/764273.1669674269@sss.pgh.pa.us
https://commitfest.postgresql.org/patch/5830/

*Key findings (i guess well known at this point):*
- ZSTD excels for repetitive/pattern-heavy data (6.7x better than PGLZ)
- For low-redundancy data (MD5 hashes), ZSTD still achieves ~2x better
- The T4 result showing zstd as "worse" is not about compression quality

-

it's about missing inline storage support. ZSTD actually compresses

better,

but pays unnecessary TOAST overhead.

I'll share the detailed benchmark script with the next patch revision.

But

also a potential path forward could be that we could just fully replace
pglz (can bring it up later in different thread)

I don't think that we will ever be able to remove pglz. It would be
nice, as final result of course, but I also expect that not being able
to decompress pglz data is going to lead to a lot of user pain. That
would be also very expensive to check at upgrade for large instances.

*On Testing and Patch Structure*
Agreed on both points:
- I'll use `compression_zstd.sql` following the `compression_lz4.sql`
pattern (removing the test_toast_ext module)

Okay.

- I'll split the GUC refactoring into a separate preparatory patch

This refactoring, if done nicely, is worth an independent piece. It's
something that I have actually done for the sake of the other thread,
though the result was not really much liked by others. Perhaps I'm
just lacking imagination with this abstraction, and I'd surely welcome
different ideas.
--
Michael

Attachments:

benchmark_toast_compression.sqlapplication/octet-stream; name=benchmark_toast_compression.sqlDownload
v3-0001-Add-ZSTD-TOAST-compression-using-VARTAG-ONDISK-ZSTD.patchapplication/octet-stream; name=v3-0001-Add-ZSTD-TOAST-compression-using-VARTAG-ONDISK-ZSTD.patchDownload
From b206ea02a266a630d1c869f19fa2adf716165809 Mon Sep 17 00:00:00 2001
From: Dharin Shah <8616130+Dharin-shah@users.noreply.github.com>
Date: Sun, 21 Dec 2025 18:38:36 +0100
Subject: [PATCH v3] Add ZSTD compression support for TOAST using
 VARTAG_ONDISK_ZSTD (Option B, level 3)

---
 src/backend/access/common/detoast.c           |  98 ++++-
 src/backend/access/common/indextuple.c        |  22 +-
 src/backend/access/common/toast_compression.c | 166 +++++++-
 src/backend/access/common/toast_internals.c   |  80 +++-
 src/backend/access/table/toast_helper.c       |  11 +-
 src/backend/replication/logical/proto.c       |   4 +-
 src/backend/replication/pgoutput/pgoutput.c   |   6 +-
 src/backend/utils/adt/varlena.c               |   6 +-
 src/backend/utils/misc/guc_tables.c           |   3 +
 src/bin/pg_dump/pg_dump.c                     |   3 +
 src/bin/psql/describe.c                       |   5 +-
 src/include/access/toast_compression.h        |  14 +
 src/include/access/toast_internals.h          |  10 +-
 src/include/varatt.h                          |  17 +-
 .../regress/expected/compression_zstd.out     | 361 ++++++++++++++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/compression_zstd.sql     | 178 +++++++++
 17 files changed, 947 insertions(+), 39 deletions(-)
 create mode 100644 src/test/regress/expected/compression_zstd.out
 create mode 100644 src/test/regress/sql/compression_zstd.sql

diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 62651787742..ebf21c85c86 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -46,7 +46,23 @@ detoast_external_attr(struct varlena *attr)
 {
 	struct varlena *result;
 
-	if (VARATT_IS_EXTERNAL_ONDISK(attr))
+	if (VARATT_IS_EXTERNAL_ONDISK_ZSTD(attr))
+	{
+		/*
+		 * This is a ZSTD-compressed external datum --- fetch and decompress it
+		 */
+		struct varatt_external toast_pointer;
+		struct varlena *compressed;
+		int32		rawsize;
+
+		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		rawsize = toast_pointer.va_rawsize - VARHDRSZ;
+
+		compressed = toast_fetch_datum(attr);
+		result = zstd_decompress_datum(compressed, rawsize);
+		pfree(compressed);
+	}
+	else if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
 		/*
 		 * This is an external stored plain value
@@ -115,7 +131,23 @@ detoast_external_attr(struct varlena *attr)
 struct varlena *
 detoast_attr(struct varlena *attr)
 {
-	if (VARATT_IS_EXTERNAL_ONDISK(attr))
+	if (VARATT_IS_EXTERNAL_ONDISK_ZSTD(attr))
+	{
+		/*
+		 * This is a ZSTD-compressed external datum --- fetch and decompress it
+		 */
+		struct varatt_external toast_pointer;
+		struct varlena *compressed;
+		int32		rawsize;
+
+		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		rawsize = toast_pointer.va_rawsize - VARHDRSZ;
+
+		compressed = toast_fetch_datum(attr);
+		attr = zstd_decompress_datum(compressed, rawsize);
+		pfree(compressed);
+	}
+	else if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
 		/*
 		 * This is an externally stored datum --- fetch it back from there
@@ -223,7 +255,23 @@ detoast_attr_slice(struct varlena *attr,
 	else if (pg_add_s32_overflow(sliceoffset, slicelength, &slicelimit))
 		slicelength = slicelimit = -1;
 
-	if (VARATT_IS_EXTERNAL_ONDISK(attr))
+	if (VARATT_IS_EXTERNAL_ONDISK_ZSTD(attr))
+	{
+		/*
+		 * This is a ZSTD-compressed external datum --- fetch, decompress, then slice
+		 */
+		struct varatt_external toast_pointer;
+		struct varlena *compressed;
+		int32		rawsize;
+
+		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		rawsize = toast_pointer.va_rawsize - VARHDRSZ;
+
+		compressed = toast_fetch_datum(attr);
+		preslice = zstd_decompress_datum_slice(compressed, rawsize, slicelimit >= 0 ? slicelimit : rawsize);
+		pfree(compressed);
+	}
+	else if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
 		struct varatt_external toast_pointer;
 
@@ -246,8 +294,8 @@ 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
+			 * 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.
 			 */
@@ -346,8 +394,9 @@ toast_fetch_datum(struct varlena *attr)
 	struct varlena *result;
 	struct varatt_external toast_pointer;
 	int32		attrsize;
+	bool		is_zstd = VARATT_IS_EXTERNAL_ONDISK_ZSTD(attr);
 
-	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
+	if (!VARATT_IS_EXTERNAL_ONDISK(attr) && !is_zstd)
 		elog(ERROR, "toast_fetch_datum shouldn't be called for non-ondisk datums");
 
 	/* Must copy to access aligned fields */
@@ -357,6 +406,17 @@ toast_fetch_datum(struct varlena *attr)
 
 	result = (struct varlena *) palloc(attrsize + VARHDRSZ);
 
+	/*
+	 * Set varlena header format based on how data is stored in TOAST:
+	 *
+	 * For PGLZ/LZ4: TOAST chunks contain tcinfo compression header followed
+	 * by compressed data. Mark as compressed varlena so decompression can
+	 * read the tcinfo metadata.
+	 *
+	 * For ZSTD: TOAST chunks contain only raw ZSTD compressed bytes (no tcinfo).
+	 * The compression method is identified by VARTAG_ONDISK_ZSTD instead of
+	 * tcinfo bits. Mark as plain varlena since there's no tcinfo header to parse.
+	 */
 	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
 		SET_VARSIZE_COMPRESSED(result, attrsize + VARHDRSZ);
 	else
@@ -400,19 +460,24 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 	struct varlena *result;
 	struct varatt_external toast_pointer;
 	int32		attrsize;
+	bool		is_zstd = VARATT_IS_EXTERNAL_ONDISK_ZSTD(attr);
+	bool		is_compressed;
 
-	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
+	if (!VARATT_IS_EXTERNAL_ONDISK(attr) && !is_zstd)
 		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);
 
+	/* For ZSTD, the vartag indicates compression; for others, check va_extinfo */
+	is_compressed = is_zstd || VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer);
+
 	/*
 	 * 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(!is_compressed || 0 == sliceoffset);
 
 	attrsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
 
@@ -425,7 +490,8 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 	/*
 	 * When fetching a prefix of a compressed external datum, account for the
 	 * space required by va_tcinfo, which is stored at the beginning as an
-	 * int32 value.
+	 * int32 value. This only applies to pglz/lz4, not zstd (which has no
+	 * tcinfo header).
 	 */
 	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer) && slicelength > 0)
 		slicelength = slicelength + sizeof(int32);
@@ -440,6 +506,10 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 
 	result = (struct varlena *) palloc(slicelength + VARHDRSZ);
 
+	/*
+	 * Use compressed varlena format only for pglz/lz4 which have tcinfo.
+	 * For zstd, use plain format since payload lacks tcinfo.
+	 */
 	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
 		SET_VARSIZE_COMPRESSED(result, slicelength + VARHDRSZ);
 	else
@@ -477,6 +547,9 @@ 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.
+	 *
+	 * Note: Zstd external data never goes through this dispatch (it uses
+	 * VARTAG_ONDISK_ZSTD and is handled separately).
 	 */
 	cmid = TOAST_COMPRESS_METHOD(attr);
 	switch (cmid)
@@ -520,6 +593,9 @@ toast_decompress_datum_slice(struct varlena *attr, int32 slicelength)
 	/*
 	 * Fetch the compression method id stored in the compression header and
 	 * decompress the data slice using the appropriate decompression routine.
+	 *
+	 * Note: Zstd external data never goes through this dispatch (it uses
+	 * VARTAG_ONDISK_ZSTD and is handled separately).
 	 */
 	cmid = TOAST_COMPRESS_METHOD(attr);
 	switch (cmid)
@@ -547,7 +623,7 @@ toast_raw_datum_size(Datum value)
 	struct varlena *attr = (struct varlena *) DatumGetPointer(value);
 	Size		result;
 
-	if (VARATT_IS_EXTERNAL_ONDISK(attr))
+	if (VARATT_IS_EXTERNAL_ONDISK(attr) || VARATT_IS_EXTERNAL_ONDISK_ZSTD(attr))
 	{
 		/* va_rawsize is the size of the original datum -- including header */
 		struct varatt_external toast_pointer;
@@ -603,7 +679,7 @@ toast_datum_size(Datum value)
 	struct varlena *attr = (struct varlena *) DatumGetPointer(value);
 	Size		result;
 
-	if (VARATT_IS_EXTERNAL_ONDISK(attr))
+	if (VARATT_IS_EXTERNAL_ONDISK(attr) || VARATT_IS_EXTERNAL_ONDISK_ZSTD(attr))
 	{
 		/*
 		 * Attribute is stored externally - return the extsize whether
diff --git a/src/backend/access/common/indextuple.c b/src/backend/access/common/indextuple.c
index 3efa3889c6f..b24902b7a25 100644
--- a/src/backend/access/common/indextuple.c
+++ b/src/backend/access/common/indextuple.c
@@ -20,6 +20,7 @@
 #include "access/heaptoast.h"
 #include "access/htup_details.h"
 #include "access/itup.h"
+#include "access/toast_compression.h"
 #include "access/toast_internals.h"
 
 /*
@@ -123,9 +124,28 @@ index_form_tuple_context(TupleDesc tupleDescriptor,
 			 att->attstorage == TYPSTORAGE_MAIN))
 		{
 			Datum		cvalue;
+			char		cmethod = att->attcompression;
+
+			/*
+			 * Index tuples must be self-contained (cannot reference external TOAST).
+			 * ZSTD compression uses external storage only (identified by vartag rather
+			 * than inline tcinfo bits). For indexed values declared COMPRESSION zstd,
+			 * fall back to inline-capable compression: prefer LZ4 when available, else PGLZ.
+			 *
+			 * Use explicit method rather than default_toast_compression so fallback
+			 * works even when default is zstd.
+			 */
+			if (cmethod == TOAST_ZSTD_COMPRESSION)
+			{
+#ifdef USE_LZ4
+				cmethod = TOAST_LZ4_COMPRESSION;
+#else
+				cmethod = TOAST_PGLZ_COMPRESSION;
+#endif
+			}
 
 			cvalue = toast_compress_datum(untoasted_values[i],
-										  att->attcompression);
+										  cmethod);
 
 			if (DatumGetPointer(cvalue) != NULL)
 			{
diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c
index 926f1e4008a..e8a4c6f328d 100644
--- a/src/backend/access/common/toast_compression.c
+++ b/src/backend/access/common/toast_compression.c
@@ -17,8 +17,13 @@
 #include <lz4.h>
 #endif
 
+#ifdef USE_ZSTD
+#include <zstd.h>
+#endif
+
 #include "access/detoast.h"
 #include "access/toast_compression.h"
+#include "access/toast_internals.h"
 #include "common/pg_lzcompress.h"
 #include "varatt.h"
 
@@ -245,6 +250,147 @@ lz4_decompress_datum_slice(const struct varlena *value, int32 slicelength)
 #endif
 }
 
+/* ----------
+ * zstd compression/decompression routines
+ *
+ * ZSTD uses VARTAG_ONDISK_ZSTD for external storage, not cmid=3.
+ * TOAST_ZSTD_COMPRESSION_ID exists only for introspection (SQL functions).
+ * ----------
+ */
+
+/*
+ * Compress a varlena using ZSTD.
+ *
+ * Returns the compressed varlena, or NULL if compression fails.
+ */
+struct varlena *
+zstd_compress_datum(const struct varlena *value)
+{
+#ifndef USE_ZSTD
+	NO_COMPRESSION_SUPPORT("zstd");
+	return NULL;				/* keep compiler quiet */
+#else
+	int32		valsize;
+	size_t		len;
+	size_t		max_size;
+	struct varlena *tmp = NULL;
+
+	valsize = VARSIZE_ANY_EXHDR(value);
+
+	/*
+	 * No point in wasting a zstd header on empty or very short inputs.
+	 */
+	if (unlikely(valsize < 32))
+		return NULL;
+
+	/*
+	 * Allocate buffer for compressed output. Return a plain varlena containing
+	 * just the ZSTD compressed frame. toast_save_datum() will store this to
+	 * external TOAST without adding tcinfo header (compression method is
+	 * identified by VARTAG_ONDISK_ZSTD instead).
+	 */
+	max_size = ZSTD_compressBound(valsize);
+	tmp = (struct varlena *) palloc(max_size + VARHDRSZ);
+
+	len = ZSTD_compress((char *) tmp + VARHDRSZ,
+						max_size,
+						VARDATA_ANY(value),
+						valsize,
+						3);		/* compression level 3 for balanced speed/ratio */
+
+	if (unlikely(ZSTD_isError(len)))
+		elog(ERROR, "zstd compression failed: %s", ZSTD_getErrorName(len));
+
+	/* data is incompressible so just free the memory and return NULL */
+	if (len >= (size_t) valsize)
+	{
+		pfree(tmp);
+		return NULL;
+	}
+
+	SET_VARSIZE(tmp, len + VARHDRSZ);
+
+	return tmp;
+#endif
+}
+
+/*
+ * Decompress a varlena that was compressed using ZSTD.
+ */
+struct varlena *
+zstd_decompress_datum(const struct varlena *value, int32 rawsize)
+{
+#ifndef USE_ZSTD
+	NO_COMPRESSION_SUPPORT("zstd");
+	return NULL;				/* keep compiler quiet */
+#else
+	size_t		decomp_size;
+	struct varlena *result;
+
+	result = (struct varlena *) palloc(rawsize + VARHDRSZ);
+
+	decomp_size = ZSTD_decompress(VARDATA(result),
+								   rawsize,
+								   (char *) value + VARHDRSZ,
+								   VARSIZE(value) - VARHDRSZ);
+
+	if (unlikely(ZSTD_isError(decomp_size)))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATA_CORRUPTED),
+				 errmsg_internal("compressed zstd data is corrupt: %s",
+								 ZSTD_getErrorName(decomp_size))));
+
+	SET_VARSIZE(result, decomp_size + VARHDRSZ);
+
+	return result;
+#endif
+}
+
+/*
+ * Decompress part of a varlena that was compressed using ZSTD.
+ *
+ * Note: We decompress the full datum then return the requested slice.
+ * This is necessary because detoast_attr_slice() calls toast_fetch_datum()
+ * first (which fetches all compressed TOAST chunks), so the real bottleneck
+ * is TOAST I/O, not decompression method. ZSTD doesn't support true random
+ * access within compressed frames, and streaming APIs don't help when the
+ * full compressed input is already materialized in memory.
+ */
+struct varlena *
+zstd_decompress_datum_slice(const struct varlena *value, int32 rawsize, int32 slicelength)
+{
+#ifndef USE_ZSTD
+	NO_COMPRESSION_SUPPORT("zstd");
+	return NULL;				/* keep compiler quiet */
+#else
+	size_t		decomp_size;
+	struct varlena *result;
+
+	/* Limit to actual size if slice request is larger */
+	if (slicelength >= rawsize)
+		return zstd_decompress_datum(value, rawsize);
+
+	/* Decompress the full data */
+	result = (struct varlena *) palloc(rawsize + VARHDRSZ);
+
+	decomp_size = ZSTD_decompress(VARDATA(result),
+								   rawsize,
+								   (char *) value + VARHDRSZ,
+								   VARSIZE(value) - VARHDRSZ);
+
+	if (unlikely(ZSTD_isError(decomp_size)))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATA_CORRUPTED),
+				 errmsg_internal("compressed zstd data is corrupt: %s",
+								 ZSTD_getErrorName(decomp_size))));
+
+	/* Truncate to requested size */
+	SET_VARSIZE(result, slicelength + VARHDRSZ);
+
+	return result;
+#endif
+}
+
 /*
  * Extract compression ID from a varlena.
  *
@@ -259,8 +405,17 @@ toast_get_compression_id(struct varlena *attr)
 	 * If it is stored externally then fetch the compression method id from
 	 * the external toast pointer.  If compressed inline, fetch it from the
 	 * toast compression header.
+	 *
+	 * For ZSTD external data, VARTAG_ONDISK_ZSTD indicates compression,
+	 * so we return TOAST_ZSTD_COMPRESSION_ID directly without checking
+	 * va_extinfo bits.
 	 */
-	if (VARATT_IS_EXTERNAL_ONDISK(attr))
+	if (VARATT_IS_EXTERNAL_ONDISK_ZSTD(attr))
+	{
+		/* ZSTD external data uses vartag to indicate compression */
+		cmid = TOAST_ZSTD_COMPRESSION_ID;
+	}
+	else if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
 		struct varatt_external toast_pointer;
 
@@ -293,6 +448,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;
 }
@@ -309,6 +471,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 d06af82de15..77fd8bc64ec 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_compression.h"
 #include "access/toast_internals.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -60,6 +61,9 @@ toast_compress_datum(Datum value, char cmethod)
 
 	/*
 	 * Call appropriate compression routine for the compression method.
+	 *
+	 * Note: Zstd does not support inline compression (returns NULL immediately).
+	 * Zstd data is always stored externally with VARTAG_ONDISK_ZSTD.
 	 */
 	switch (cmethod)
 	{
@@ -71,6 +75,9 @@ toast_compress_datum(Datum value, char cmethod)
 			tmp = lz4_compress_datum((const struct varlena *) DatumGetPointer(value));
 			cmid = TOAST_LZ4_COMPRESSION_ID;
 			break;
+		case TOAST_ZSTD_COMPRESSION:
+			/* Zstd: no inline compression, force external storage */
+			return PointerGetDatum(NULL);
 		default:
 			elog(ERROR, "invalid compression method %c", cmethod);
 	}
@@ -112,12 +119,13 @@ toast_compress_datum(Datum value, char cmethod)
  * rel: the main relation we're working with (not the toast rel!)
  * value: datum to be pushed to toast storage
  * oldexternal: if not NULL, toast pointer previously representing the datum
+ * cmethod: compression method for the column (from attcompression)
  * options: options to be passed to heap_insert() for toast rows
  * ----------
  */
 Datum
 toast_save_datum(Relation rel, Datum value,
-				 struct varlena *oldexternal, int options)
+				 struct varlena *oldexternal, char cmethod, int options)
 {
 	Relation	toastrel;
 	Relation   *toastidxs;
@@ -131,6 +139,8 @@ toast_save_datum(Relation rel, Datum value,
 	Pointer		dval = DatumGetPointer(value);
 	int			num_indexes;
 	int			validIndex;
+	bool		is_zstd = false;
+	struct varlena *zstd_compressed = NULL;
 
 	Assert(!VARATT_IS_EXTERNAL(dval));
 
@@ -172,18 +182,57 @@ toast_save_datum(Relation rel, Datum value,
 		/* rawsize in a compressed datum is just the size of the payload */
 		toast_pointer.va_rawsize = VARDATA_COMPRESSED_GET_EXTSIZE(dval) + VARHDRSZ;
 
-		/* set external size and compression method */
+		/*
+		 * Inline-compressed data (only pglz/lz4, never zstd).
+		 * Encode compression method from tcinfo into va_extinfo bits 30-31.
+		 */
 		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, data_todo,
 													 VARDATA_COMPRESSED_GET_COMPRESS_METHOD(dval));
-		/* Assert that the numbers look like it's compressed */
 		Assert(VARATT_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;
+		/*
+		 * Uncompressed data. For zstd, compress it now before storing.
+		 * If no compression method specified, use default_toast_compression.
+		 */
+		char effective_cmethod = cmethod;
+		if (!CompressionMethodIsValid(effective_cmethod))
+			effective_cmethod = default_toast_compression;
+
+		if (effective_cmethod == TOAST_ZSTD_COMPRESSION)
+		{
+			zstd_compressed = zstd_compress_datum((const struct varlena *) dval);
+			if (likely(zstd_compressed != NULL))
+			{
+				/*
+				 * Successfully compressed with ZSTD. Store raw compressed bytes
+				 * to TOAST (no tcinfo header). VARTAG_ONDISK_ZSTD identifies the
+				 * compression method.
+				 */
+				data_p = VARDATA(zstd_compressed);
+				data_todo = VARSIZE(zstd_compressed) - VARHDRSZ;
+				toast_pointer.va_rawsize = VARSIZE(dval);
+				toast_pointer.va_extinfo = data_todo;
+				is_zstd = true;
+			}
+			else
+			{
+				/* Incompressible, store uncompressed */
+				data_p = VARDATA(dval);
+				data_todo = VARSIZE(dval) - VARHDRSZ;
+				toast_pointer.va_rawsize = VARSIZE(dval);
+				toast_pointer.va_extinfo = data_todo;
+			}
+		}
+		else
+		{
+			/* pglz/lz4 or uncompressed: store as-is */
+			data_p = VARDATA(dval);
+			data_todo = VARSIZE(dval) - VARHDRSZ;
+			toast_pointer.va_rawsize = VARSIZE(dval);
+			toast_pointer.va_extinfo = data_todo;
+		}
 	}
 
 	/*
@@ -227,7 +276,8 @@ toast_save_datum(Relation rel, Datum value,
 		{
 			struct varatt_external old_toast_pointer;
 
-			Assert(VARATT_IS_EXTERNAL_ONDISK(oldexternal));
+			Assert(VARATT_IS_EXTERNAL_ONDISK(oldexternal) ||
+				   VARATT_IS_EXTERNAL_ONDISK_ZSTD(oldexternal));
 			/* Must copy to access aligned fields */
 			VARATT_EXTERNAL_GET_POINTER(old_toast_pointer, oldexternal);
 			if (old_toast_pointer.va_toastrelid == rel->rd_toastoid)
@@ -357,10 +407,18 @@ toast_save_datum(Relation rel, Datum value,
 	table_close(toastrel, NoLock);
 
 	/*
-	 * Create the TOAST pointer value that we'll return
+	 * Free the ZSTD compressed varlena if we allocated one
+	 */
+	if (zstd_compressed != NULL)
+		pfree(zstd_compressed);
+
+	/*
+	 * Create the TOAST pointer value that we'll return.
+	 * Use VARTAG_ONDISK_ZSTD for ZSTD-compressed data to indicate compression
+	 * via the vartag rather than encoding it in va_extinfo bits 30-31.
 	 */
 	result = (struct varlena *) palloc(TOAST_POINTER_SIZE);
-	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK);
+	SET_VARTAG_EXTERNAL(result, is_zstd ? VARTAG_ONDISK_ZSTD : VARTAG_ONDISK);
 	memcpy(VARDATA_EXTERNAL(result), &toast_pointer, sizeof(toast_pointer));
 
 	return PointerGetDatum(result);
@@ -385,7 +443,7 @@ toast_delete_datum(Relation rel, Datum value, bool is_speculative)
 	int			num_indexes;
 	int			validIndex;
 
-	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
+	if (!VARATT_IS_EXTERNAL_ONDISK(attr) && !VARATT_IS_EXTERNAL_ONDISK_ZSTD(attr))
 		return;
 
 	/* Must copy to access aligned fields */
diff --git a/src/backend/access/table/toast_helper.c b/src/backend/access/table/toast_helper.c
index 11f97d65367..f2371a60971 100644
--- a/src/backend/access/table/toast_helper.c
+++ b/src/backend/access/table/toast_helper.c
@@ -71,10 +71,12 @@ toast_tuple_init(ToastTupleContext *ttc)
 			 * we have to delete it later.
 			 */
 			if (att->attlen == -1 && !ttc->ttc_oldisnull[i] &&
-				VARATT_IS_EXTERNAL_ONDISK(old_value))
+				(VARATT_IS_EXTERNAL_ONDISK(old_value) ||
+				 VARATT_IS_EXTERNAL_ONDISK_ZSTD(old_value)))
 			{
 				if (ttc->ttc_isnull[i] ||
-					!VARATT_IS_EXTERNAL_ONDISK(new_value) ||
+					(!VARATT_IS_EXTERNAL_ONDISK(new_value) &&
+					 !VARATT_IS_EXTERNAL_ONDISK_ZSTD(new_value)) ||
 					memcmp(old_value, new_value,
 						   VARSIZE_EXTERNAL(old_value)) != 0)
 				{
@@ -261,7 +263,7 @@ toast_tuple_externalize(ToastTupleContext *ttc, int attribute, int options)
 
 	attr->tai_colflags |= TOASTCOL_IGNORE;
 	*value = toast_save_datum(ttc->ttc_rel, old_value, attr->tai_oldexternal,
-							  options);
+							  attr->tai_compression, options);
 	if ((attr->tai_colflags & TOASTCOL_NEEDS_FREE) != 0)
 		pfree(DatumGetPointer(old_value));
 	attr->tai_colflags |= TOASTCOL_NEEDS_FREE;
@@ -330,7 +332,8 @@ toast_delete_external(Relation rel, const Datum *values, const bool *isnull,
 
 			if (isnull[i])
 				continue;
-			else if (VARATT_IS_EXTERNAL_ONDISK(DatumGetPointer(value)))
+			else if (VARATT_IS_EXTERNAL_ONDISK(DatumGetPointer(value)) ||
+					 VARATT_IS_EXTERNAL_ONDISK_ZSTD(DatumGetPointer(value)))
 				toast_delete_datum(rel, value, is_speculative);
 		}
 	}
diff --git a/src/backend/replication/logical/proto.c b/src/backend/replication/logical/proto.c
index 27ad74fd759..09535abd778 100644
--- a/src/backend/replication/logical/proto.c
+++ b/src/backend/replication/logical/proto.c
@@ -812,7 +812,9 @@ logicalrep_write_tuple(StringInfo out, Relation rel, TupleTableSlot *slot,
 			continue;
 		}
 
-		if (att->attlen == -1 && VARATT_IS_EXTERNAL_ONDISK(DatumGetPointer(values[i])))
+		if (att->attlen == -1 &&
+			(VARATT_IS_EXTERNAL_ONDISK(DatumGetPointer(values[i])) ||
+			 VARATT_IS_EXTERNAL_ONDISK_ZSTD(DatumGetPointer(values[i]))))
 		{
 			/*
 			 * Unchanged toasted datum.  (Note that we don't promise to detect
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 787998abb8a..fbe229c262b 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -1397,8 +1397,10 @@ pgoutput_row_filter(Relation relation, TupleTableSlot *old_slot,
 		 * VARTAG_INDIRECT. See ReorderBufferToastReplace.
 		 */
 		if (att->attlen == -1 &&
-			VARATT_IS_EXTERNAL_ONDISK(DatumGetPointer(new_slot->tts_values[i])) &&
-			!VARATT_IS_EXTERNAL_ONDISK(DatumGetPointer(old_slot->tts_values[i])))
+			(VARATT_IS_EXTERNAL_ONDISK(DatumGetPointer(new_slot->tts_values[i])) ||
+			 VARATT_IS_EXTERNAL_ONDISK_ZSTD(DatumGetPointer(new_slot->tts_values[i]))) &&
+			!VARATT_IS_EXTERNAL_ONDISK(DatumGetPointer(old_slot->tts_values[i])) &&
+			!VARATT_IS_EXTERNAL_ONDISK_ZSTD(DatumGetPointer(old_slot->tts_values[i])))
 		{
 			if (!tmp_new_slot)
 			{
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 8adeb8dadc6..5f5cc5da449 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -4179,6 +4179,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);
 	}
@@ -4219,7 +4222,8 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS)
 
 	attr = (struct varlena *) DatumGetPointer(PG_GETARG_DATUM(0));
 
-	if (!VARATT_IS_EXTERNAL_ONDISK(attr))
+	if (!VARATT_IS_EXTERNAL_ONDISK(attr) &&
+		!VARATT_IS_EXTERNAL_ONDISK_ZSTD(attr))
 		PG_RETURN_NULL();
 
 	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 04ab0a26608..555b0143685 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -460,6 +460,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/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 27f6be3f0f8..4f660a19c35 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -17905,6 +17905,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 36f24502842..7d6377e27ca 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2206,8 +2206,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/include/access/toast_compression.h b/src/include/access/toast_compression.h
index 13c4612ceed..1be06aafccb 100644
--- a/src/include/access/toast_compression.h
+++ b/src/include/access/toast_compression.h
@@ -33,12 +33,17 @@ extern PGDLLIMPORT int default_toast_compression;
  * 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.
+ *
+ * Note: TOAST_ZSTD_COMPRESSION_ID is not used in 2-bit cmid fields. Zstd
+ * uses VARTAG_ONDISK_ZSTD for external storage. This ID exists only for
+ * introspection (e.g., pg_column_compression()).
  */
 typedef enum ToastCompressionId
 {
 	TOAST_PGLZ_COMPRESSION_ID = 0,
 	TOAST_LZ4_COMPRESSION_ID = 1,
 	TOAST_INVALID_COMPRESSION_ID = 2,
+	TOAST_ZSTD_COMPRESSION_ID = 3,		/* introspection only, not in cmid */
 } ToastCompressionId;
 
 /*
@@ -48,6 +53,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)
@@ -65,6 +71,14 @@ extern struct varlena *lz4_decompress_datum(const struct varlena *value);
 extern struct varlena *lz4_decompress_datum_slice(const struct varlena *value,
 												  int32 slicelength);
 
+/* zstd compression/decompression routines */
+extern struct varlena *zstd_compress_datum(const struct varlena *value);
+extern struct varlena *zstd_decompress_datum(const struct varlena *value,
+											  int32 rawsize);
+extern struct varlena *zstd_decompress_datum_slice(const struct varlena *value,
+												   int32 rawsize,
+												   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 06ae8583c1e..77d5081eeed 100644
--- a/src/include/access/toast_internals.h
+++ b/src/include/access/toast_internals.h
@@ -36,11 +36,17 @@ typedef struct toast_compress_header
 #define TOAST_COMPRESS_METHOD(ptr) \
 	(((toast_compress_header *) (ptr))->tcinfo >> VARLENA_EXTSIZE_BITS)
 
+/*
+ * Set compression header info. Zstd uses TOAST_INVALID_COMPRESSION_ID, not
+ * TOAST_ZSTD_COMPRESSION_ID (cmid=3 is not used in tcinfo).
+ */
 #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_LZ4_COMPRESSION_ID || \
+			   (cm_method) == TOAST_INVALID_COMPRESSION_ID); \
+		Assert((cm_method) != TOAST_ZSTD_COMPRESSION_ID); \
 		((toast_compress_header *) (ptr))->tcinfo = \
 			(len) | ((uint32) (cm_method) << VARLENA_EXTSIZE_BITS); \
 	} while (0)
@@ -50,7 +56,7 @@ extern Oid	toast_get_valid_index(Oid toastoid, LOCKMODE lock);
 
 extern void toast_delete_datum(Relation rel, Datum value, bool is_speculative);
 extern Datum toast_save_datum(Relation rel, Datum value,
-							  struct varlena *oldexternal, int options);
+							  struct varlena *oldexternal, char cmethod, int options);
 
 extern int	toast_open_indexes(Relation toastrel,
 							   LOCKMODE lock,
diff --git a/src/include/varatt.h b/src/include/varatt.h
index aeeabf9145b..cf5436f7bf1 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -80,13 +80,19 @@ 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.
+ *
+ * VARTAG_ONDISK_ZSTD is used for ZSTD-compressed external TOAST data.
+ * Unlike pglz and lz4 which store the compression method in va_extinfo bits
+ * 30-31, ZSTD uses a separate vartag to preserve all 32 bits of va_extinfo
+ * for future use (compression level, dictionary ID, etc.).
  */
 typedef enum vartag_external
 {
 	VARTAG_INDIRECT = 1,
 	VARTAG_EXPANDED_RO = 2,
 	VARTAG_EXPANDED_RW = 3,
-	VARTAG_ONDISK = 18
+	VARTAG_ONDISK = 18,
+	VARTAG_ONDISK_ZSTD = 19
 } vartag_external;
 
 /* Is a TOAST pointer either type of expanded-object pointer? */
@@ -105,7 +111,7 @@ VARTAG_SIZE(vartag_external tag)
 		return sizeof(varatt_indirect);
 	else if (VARTAG_IS_EXPANDED(tag))
 		return sizeof(varatt_expanded);
-	else if (tag == VARTAG_ONDISK)
+	else if (tag == VARTAG_ONDISK || tag == VARTAG_ONDISK_ZSTD)
 		return sizeof(varatt_external);
 	else
 	{
@@ -363,6 +369,13 @@ VARATT_IS_EXTERNAL_ONDISK(const void *PTR)
 	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK;
 }
 
+/* Is varlena datum a pointer to on-disk ZSTD-compressed toasted data? */
+static inline bool
+VARATT_IS_EXTERNAL_ONDISK_ZSTD(const void *PTR)
+{
+	return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK_ZSTD;
+}
+
 /* Is varlena datum an indirect pointer? */
 static inline bool
 VARATT_IS_EXTERNAL_INDIRECT(const void *PTR)
diff --git a/src/test/regress/expected/compression_zstd.out b/src/test/regress/expected/compression_zstd.out
new file mode 100644
index 00000000000..1f5bb43b542
--- /dev/null
+++ b/src/test/regress/expected/compression_zstd.out
@@ -0,0 +1,361 @@
+-- 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_pglz
+                                       Table "zstd.cmdata_pglz"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ f1     | text |           |          |         | extended | pglz        |              | 
+Indexes:
+    "idx" btree (f1)
+
+CREATE TABLE cmdata_zstd(f1 TEXT COMPRESSION zstd);
+INSERT INTO cmdata_zstd VALUES(repeat('1234567890', 1004));
+\d+ cmdata_zstd
+                                       Table "zstd.cmdata_zstd"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ f1     | text |           |          |         | extended | zstd        |              | 
+
+-- 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 
+-----------------------
+ pglz
+(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
+ pglz
+(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 
+-----------------------
+ pglz
+(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)
+
+-- test pg_column_toast_chunk_id with zstd
+SELECT pg_column_toast_chunk_id(f1) IS NOT NULL AS has_toast_chunk FROM cmdata2;
+ has_toast_chunk 
+-----------------
+ t
+(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 
+-----------------------
+ pglz
+(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_pglz
+                                       Table "zstd.cmdata_pglz"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ f1     | text |           |          |         | extended | zstd        |              | 
+Indexes:
+    "idx" btree (f1)
+Child tables: cminh
+
+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());
+-- test cross-method operations (zstd <-> lz4 if available)
+-- This tests interaction between all three compression methods
+SELECT enumvals @> '{lz4}' AS has_lz4 FROM pg_settings WHERE
+  name = 'default_toast_compression' \gset
+\if :has_lz4
+CREATE TABLE cmdata_lz4(f1 TEXT COMPRESSION lz4);
+INSERT INTO cmdata_lz4 VALUES(repeat('1234567890', 1004));
+SELECT pg_column_compression(f1) FROM cmdata_lz4;
+ pg_column_compression 
+-----------------------
+ lz4
+(1 row)
+
+-- copy from zstd to lz4 table
+CREATE TABLE cmmove4(f1 text COMPRESSION lz4);
+INSERT INTO cmmove4 SELECT * FROM cmdata_zstd;
+SELECT pg_column_compression(f1) FROM cmmove4;
+ pg_column_compression 
+-----------------------
+ lz4
+(1 row)
+
+-- copy from lz4 to zstd table
+CREATE TABLE cmmove5(f1 text COMPRESSION zstd);
+INSERT INTO cmmove5 SELECT * FROM cmdata_lz4;
+SELECT pg_column_compression(f1) FROM cmmove5;
+ pg_column_compression 
+-----------------------
+ lz4
+(1 row)
+
+\else
+\echo '*** skipping LZ4 cross-method tests (lz4 not supported) ***'
+\endif
+-- 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)
+
+\if :has_lz4
+SELECT length(f1) FROM cmdata_lz4;
+ length 
+--------
+  10040
+(1 row)
+
+\endif
+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)
+
+\if :has_lz4
+SELECT length(f1) FROM cmmove4;
+ length 
+--------
+  10040
+(1 row)
+
+SELECT length(f1) FROM cmmove5;
+ length 
+--------
+  10040
+(1 row)
+
+\endif
+-- test parallel workers with ZSTD (if supported)
+CREATE TABLE parallel_zstd_test (id int, data text COMPRESSION zstd);
+INSERT INTO parallel_zstd_test SELECT i, repeat('x' || i::text, 3000) FROM generate_series(1, 100) i;
+SELECT count(*), avg(length(data)) FROM parallel_zstd_test;
+ count |          avg          
+-------+-----------------------
+   100 | 8760.0000000000000000
+(1 row)
+
+SELECT count(*), sum(length(substring(data, 1, 50))) FROM parallel_zstd_test;
+ count | sum  
+-------+------
+   100 | 5000
+(1 row)
+
+DROP TABLE parallel_zstd_test;
+-- test COPY with ZSTD compressed data
+CREATE TABLE copy_zstd_test (id int, data text COMPRESSION zstd);
+INSERT INTO copy_zstd_test VALUES (1, repeat('copydata', 2000));
+\copy copy_zstd_test TO '/tmp/zstd_copy_test.dat'
+TRUNCATE copy_zstd_test;
+\copy copy_zstd_test FROM '/tmp/zstd_copy_test.dat'
+SELECT id, length(data), pg_column_compression(data) FROM copy_zstd_test;
+ id | length | pg_column_compression 
+----+--------+-----------------------
+  1 |  16000 | zstd
+(1 row)
+
+DROP TABLE copy_zstd_test;
+\set HIDE_TOAST_COMPRESSION true
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 905f9bca959..1cd161fa2c4 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_merge partition_split partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression compression_lz4 memoize stats predicate numa eager_aggregate
+test: partition_merge partition_split partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression compression_lz4 compression_zstd memoize stats predicate numa eager_aggregate
 
 # 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..8a38092f034
--- /dev/null
+++ b/src/test/regress/sql/compression_zstd.sql
@@ -0,0 +1,178 @@
+-- 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_pglz
+CREATE TABLE cmdata_zstd(f1 TEXT COMPRESSION zstd);
+INSERT INTO cmdata_zstd VALUES(repeat('1234567890', 1004));
+\d+ cmdata_zstd
+
+-- 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;
+
+-- test pg_column_toast_chunk_id with zstd
+SELECT pg_column_toast_chunk_id(f1) IS NOT NULL AS has_toast_chunk 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_pglz
+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());
+
+-- test cross-method operations (zstd <-> lz4 if available)
+-- This tests interaction between all three compression methods
+SELECT enumvals @> '{lz4}' AS has_lz4 FROM pg_settings WHERE
+  name = 'default_toast_compression' \gset
+\if :has_lz4
+CREATE TABLE cmdata_lz4(f1 TEXT COMPRESSION lz4);
+INSERT INTO cmdata_lz4 VALUES(repeat('1234567890', 1004));
+SELECT pg_column_compression(f1) FROM cmdata_lz4;
+
+-- copy from zstd to lz4 table
+CREATE TABLE cmmove4(f1 text COMPRESSION lz4);
+INSERT INTO cmmove4 SELECT * FROM cmdata_zstd;
+SELECT pg_column_compression(f1) FROM cmmove4;
+
+-- copy from lz4 to zstd table
+CREATE TABLE cmmove5(f1 text COMPRESSION zstd);
+INSERT INTO cmmove5 SELECT * FROM cmdata_lz4;
+SELECT pg_column_compression(f1) FROM cmmove5;
+\else
+\echo '*** skipping LZ4 cross-method tests (lz4 not supported) ***'
+\endif
+
+-- check data is ok
+SELECT length(f1) FROM cmdata_pglz;
+SELECT length(f1) FROM cmdata_zstd;
+\if :has_lz4
+SELECT length(f1) FROM cmdata_lz4;
+\endif
+SELECT length(f1) FROM cmmove1;
+SELECT length(f1) FROM cmmove2;
+SELECT length(f1) FROM cmmove3;
+\if :has_lz4
+SELECT length(f1) FROM cmmove4;
+SELECT length(f1) FROM cmmove5;
+\endif
+
+-- test parallel workers with ZSTD (if supported)
+CREATE TABLE parallel_zstd_test (id int, data text COMPRESSION zstd);
+INSERT INTO parallel_zstd_test SELECT i, repeat('x' || i::text, 3000) FROM generate_series(1, 100) i;
+SELECT count(*), avg(length(data)) FROM parallel_zstd_test;
+SELECT count(*), sum(length(substring(data, 1, 50))) FROM parallel_zstd_test;
+DROP TABLE parallel_zstd_test;
+
+-- test COPY with ZSTD compressed data
+CREATE TABLE copy_zstd_test (id int, data text COMPRESSION zstd);
+INSERT INTO copy_zstd_test VALUES (1, repeat('copydata', 2000));
+\copy copy_zstd_test TO '/tmp/zstd_copy_test.dat'
+TRUNCATE copy_zstd_test;
+\copy copy_zstd_test FROM '/tmp/zstd_copy_test.dat'
+SELECT id, length(data), pg_column_compression(data) FROM copy_zstd_test;
+DROP TABLE copy_zstd_test;
+
+\set HIDE_TOAST_COMPRESSION true
-- 
2.39.3 (Apple Git-146)

backwards_compatibility_test.sqlapplication/octet-stream; name=backwards_compatibility_test.sqlDownload
#12Robert Treat
rob@xzilla.net
In reply to: Michael Paquier (#10)
Re: Fwd: [PATCH] Add zstd compression for TOAST using extended header format

On Thu, Dec 18, 2025 at 5:44 PM Michael Paquier <michael@paquier.xyz> wrote:

On Thu, Dec 18, 2025 at 10:44:22PM +0100, Dharin Shah wrote:

I'll share the detailed benchmark script with the next patch revision. But
also a potential path forward could be that we could just fully replace
pglz (can bring it up later in different thread)

I don't think that we will ever be able to remove pglz. It would be
nice, as final result of course, but I also expect that not being able
to decompress pglz data is going to lead to a lot of user pain. That
would be also very expensive to check at upgrade for large instances.

Agreed that I can't see pglz being removed any time soon, if ever.
Thinking through what a conversion process would look like seems
unwieldy at best, so I think we definitely need it for backwards
compatibility, plus I think it is useful to have a self-contained
option. I'd almost suggest we should look at replacing lz4, but I
don't think that is significantly easier, it just has a smaller, more
invested, blast radius. That said, I do suspect ztsd could quickly
become a popular recommendation and/or default among users /
consultants / service providers.

Robert Treat
https://xzilla.net

#13Michael Paquier
michael@paquier.xyz
In reply to: Robert Treat (#12)
Re: Fwd: [PATCH] Add zstd compression for TOAST using extended header format

On Wed, Dec 24, 2025 at 11:50:48AM -0500, Robert Treat wrote:

Agreed that I can't see pglz being removed any time soon, if ever.
Thinking through what a conversion process would look like seems
unwieldy at best, so I think we definitely need it for backwards
compatibility, plus I think it is useful to have a self-contained
option. I'd almost suggest we should look at replacing lz4, but I
don't think that is significantly easier, it just has a smaller, more
invested, blast radius.

Backward-compatibility requirements make a replacement of LZ4
basically impossible to me, for the same reasons as pglz. We could
not replace the bit used in the va_extinfo to track if LZ4 compression
is used, either. One thing that I do wonder is if it would make
things simpler in the long-run if we introduced a new separated vartag
for LZ4-compressed external TOAST pointers as well. At least we'd
have a leaner design: it means that we have to keep the
varatt_external available on read, but we could update to the new
format when writing entries. Or perhaps that's not worth the
complication based on the last sentence you are writing..

That said, I do suspect ztsd could quickly
become a popular recommendation and/or default among users /
consultants / service providers.

.. Because I strongly suspect that this is going to be true, and that
zstd would just be a better replacement over lz4. That's a trend that
I see is already going on for wal_compression.

Note that I am not on board with simply reusing varatt_external for
zstd-compressed entries, neither do I think that this is the best move
ever. It makes the core patch simpler, but it makes things like
ToastCompressionId more complicated to think about. If anything, I'd
consider a rename of varatt_external as the best way to go with an
intermediate "translation" structure only used in memory as I am
proposing on the other thread (something that others seem meh enough
about but I am not seeing alternate proposals floating around,
either). This would make things like detoast_external_attr() less
confusing, I think, as the latest patch posted on this thread is
actually proving with its shortcut for toast_fetch_datum as one
example of something I'd rather not do..
--
Michael

#14Dharin Shah
dharinshah95@gmail.com
In reply to: Michael Paquier (#13)
Re: Fwd: [PATCH] Add zstd compression for TOAST using extended header format

Thanks Michael & Robert,

Agreed — I don’t think it’s realistic or practical to talk about
deprecating or replacing pglz (or lz4) given on-disk compatibility
requirements.

Note that I am not on board with simply reusing varatt_external for
zstd-compressed entries, neither do I think that this is the best move
ever. It makes the core patch simpler, but it makes things like
ToastCompressionId more complicated to think about. If anything, I'd
consider a rename of varatt_external as the best way to go with an
intermediate "translation" structure only used in memory as I am
proposing on the other thread (something that others seem meh enough
about but I am not seeing alternate proposals floating around,
either). This would make things like detoast_external_attr() less
confusing, I think, as the latest patch posted on this thread is
actually proving with its shortcut for toast_fetch_datum as one
example of something I'd rather not do..

On the design: I understand & share the same concerns that we’d end up with
multiple “sources of truth” for external compression method identification
(pglz/lz4 via va_extinfo bits, zstd via vartag), and that this pushes
method-specific shortcuts into detoast paths.

Would you be OK if I split this into two steps?

1.First, a refactor-only patch introducing a small decoded/in-memory
representation of an external TOAST pointer, so detoast/toast code paths
don’t have to reason directly about tcinfo vs vartag vs va_extinfo. This
would be a cleanup with no on-disk format change and no behavioral change
for existing methods. Is this the same “translation structure” approach you
mentioned in the other thread? If you can point me to it, I’ll align with
that proposal.

2. Then, a follow-up patch adding zstd using VARTAG_ONDISK_ZSTD,
implemented on top of that abstraction to keep zstd handling centralized
and minimize special-casing in detoast.
If that direction matches what you had in mind, I can first post the
proposed translation structure/API for feedback before respinning the zstd
patch.

Thanks,
Dharin

On Thu, Dec 25, 2025 at 1:25 AM Michael Paquier <michael@paquier.xyz> wrote:

Show quoted text

On Wed, Dec 24, 2025 at 11:50:48AM -0500, Robert Treat wrote:

Agreed that I can't see pglz being removed any time soon, if ever.
Thinking through what a conversion process would look like seems
unwieldy at best, so I think we definitely need it for backwards
compatibility, plus I think it is useful to have a self-contained
option. I'd almost suggest we should look at replacing lz4, but I
don't think that is significantly easier, it just has a smaller, more
invested, blast radius.

Backward-compatibility requirements make a replacement of LZ4
basically impossible to me, for the same reasons as pglz. We could
not replace the bit used in the va_extinfo to track if LZ4 compression
is used, either. One thing that I do wonder is if it would make
things simpler in the long-run if we introduced a new separated vartag
for LZ4-compressed external TOAST pointers as well. At least we'd
have a leaner design: it means that we have to keep the
varatt_external available on read, but we could update to the new
format when writing entries. Or perhaps that's not worth the
complication based on the last sentence you are writing..

That said, I do suspect ztsd could quickly
become a popular recommendation and/or default among users /
consultants / service providers.

.. Because I strongly suspect that this is going to be true, and that
zstd would just be a better replacement over lz4. That's a trend that
I see is already going on for wal_compression.

Note that I am not on board with simply reusing varatt_external for
zstd-compressed entries, neither do I think that this is the best move
ever. It makes the core patch simpler, but it makes things like
ToastCompressionId more complicated to think about. If anything, I'd
consider a rename of varatt_external as the best way to go with an
intermediate "translation" structure only used in memory as I am
proposing on the other thread (something that others seem meh enough
about but I am not seeing alternate proposals floating around,
either). This would make things like detoast_external_attr() less
confusing, I think, as the latest patch posted on this thread is
actually proving with its shortcut for toast_fetch_datum as one
example of something I'd rather not do..
--
Michael

#15Dharin Shah
dharinshah95@gmail.com
In reply to: Dharin Shah (#14)
2 attachment(s)
Re: Fwd: [PATCH] Add zstd compression for TOAST using extended header format

Hello Michael,

Following up on the discussion about avoiding method-specific shortcuts in
detoast paths, this patch is a refactor-only step: it introduces a small
decoded/in-memory representation of an on-disk external TOAST pointer, and
refactors detoast_attr() and detoast_attr_slice() to use it.

The goal is to centralize “how do we interpret an external datum?” so that
detoast code paths don’t have to reason directly about va_extinfo encoding
vs payload layout details. This is intended as groundwork for a follow-up
patch adding a new vartag-based method (e.g., zstd) without scattering
special cases in detoast paths.

Key changes
- Introduces DecodedExternalToast + ToastDecompressMethod +
TOAST_EXT_HAS_TCINFO in toast_internals.h.
- Adds a small static decoder in detoast.c (decode_external_toast_pointer())
- Refactors detoast_attr() and detoast_attr_slice() to use a decode ->
fetch -> decompress dispatch pattern
- No on-disk format changes; existing behavior preserved (including error
behavior for unsupported compression builds).

Why HAS_TCINFO?
- Previously, “is compressed?” was used as a proxy for whether the external
payload begins with tcinfo. This patch makes that explicit: HAS_TCINFO
captures payload layout, which is distinct from whether the value is
compressed. This separation is needed for future methods that may store
external compressed payloads without tcinfo.

Testing: Core regression suites pass

Performance: I ran a small detoast-focused benchmark that forces external
storage; results were within run-to-run variance, with no measurable
regression. (Benchmark script attached: benchmark_toast_detoast.sql for
reproduction)

Thanks,
Dharin

On Thu, Dec 25, 2025 at 1:54 AM Dharin Shah <dharinshah95@gmail.com> wrote:

Show quoted text

Thanks Michael & Robert,

Agreed — I don’t think it’s realistic or practical to talk about
deprecating or replacing pglz (or lz4) given on-disk compatibility
requirements.

Note that I am not on board with simply reusing varatt_external for
zstd-compressed entries, neither do I think that this is the best move
ever. It makes the core patch simpler, but it makes things like
ToastCompressionId more complicated to think about. If anything, I'd
consider a rename of varatt_external as the best way to go with an
intermediate "translation" structure only used in memory as I am
proposing on the other thread (something that others seem meh enough
about but I am not seeing alternate proposals floating around,
either). This would make things like detoast_external_attr() less
confusing, I think, as the latest patch posted on this thread is
actually proving with its shortcut for toast_fetch_datum as one
example of something I'd rather not do..

On the design: I understand & share the same concerns that we’d end up
with multiple “sources of truth” for external compression method
identification (pglz/lz4 via va_extinfo bits, zstd via vartag), and that
this pushes method-specific shortcuts into detoast paths.

Would you be OK if I split this into two steps?

1.First, a refactor-only patch introducing a small decoded/in-memory
representation of an external TOAST pointer, so detoast/toast code paths
don’t have to reason directly about tcinfo vs vartag vs va_extinfo. This
would be a cleanup with no on-disk format change and no behavioral change
for existing methods. Is this the same “translation structure” approach you
mentioned in the other thread? If you can point me to it, I’ll align with
that proposal.

2. Then, a follow-up patch adding zstd using VARTAG_ONDISK_ZSTD,
implemented on top of that abstraction to keep zstd handling centralized
and minimize special-casing in detoast.
If that direction matches what you had in mind, I can first post the
proposed translation structure/API for feedback before respinning the zstd
patch.

Thanks,
Dharin

On Thu, Dec 25, 2025 at 1:25 AM Michael Paquier <michael@paquier.xyz>
wrote:

On Wed, Dec 24, 2025 at 11:50:48AM -0500, Robert Treat wrote:

Agreed that I can't see pglz being removed any time soon, if ever.
Thinking through what a conversion process would look like seems
unwieldy at best, so I think we definitely need it for backwards
compatibility, plus I think it is useful to have a self-contained
option. I'd almost suggest we should look at replacing lz4, but I
don't think that is significantly easier, it just has a smaller, more
invested, blast radius.

Backward-compatibility requirements make a replacement of LZ4
basically impossible to me, for the same reasons as pglz. We could
not replace the bit used in the va_extinfo to track if LZ4 compression
is used, either. One thing that I do wonder is if it would make
things simpler in the long-run if we introduced a new separated vartag
for LZ4-compressed external TOAST pointers as well. At least we'd
have a leaner design: it means that we have to keep the
varatt_external available on read, but we could update to the new
format when writing entries. Or perhaps that's not worth the
complication based on the last sentence you are writing..

That said, I do suspect ztsd could quickly
become a popular recommendation and/or default among users /
consultants / service providers.

.. Because I strongly suspect that this is going to be true, and that
zstd would just be a better replacement over lz4. That's a trend that
I see is already going on for wal_compression.

Note that I am not on board with simply reusing varatt_external for
zstd-compressed entries, neither do I think that this is the best move
ever. It makes the core patch simpler, but it makes things like
ToastCompressionId more complicated to think about. If anything, I'd
consider a rename of varatt_external as the best way to go with an
intermediate "translation" structure only used in memory as I am
proposing on the other thread (something that others seem meh enough
about but I am not seeing alternate proposals floating around,
either). This would make things like detoast_external_attr() less
confusing, I think, as the latest patch posted on this thread is
actually proving with its shortcut for toast_fetch_datum as one
example of something I'd rather not do..
--
Michael

Attachments:

benchmark_toast_detoast.sqlapplication/octet-stream; name=benchmark_toast_detoast.sqlDownload
0001-refactor-detoast-to-use-decoded-external-toast-abstr.patchapplication/octet-stream; name=0001-refactor-detoast-to-use-decoded-external-toast-abstr.patchDownload
From 49939fe3416803d446ba43c404eae9711a3ae21b Mon Sep 17 00:00:00 2001
From: Dharin Shah <8616130+Dharin-shah@users.noreply.github.com>
Date: Sun, 28 Dec 2025 23:21:21 +0100
Subject: [PATCH v1] refactor detoast to use decoded external toast abstraction

---
 src/backend/access/common/detoast.c  | 326 ++++++++++++++++++++-------
 src/include/access/toast_internals.h |  39 ++++
 2 files changed, 278 insertions(+), 87 deletions(-)

diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 62651787742..fdade574913 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -29,6 +29,70 @@ static struct varlena *toast_fetch_datum_slice(struct varlena *attr,
 static struct varlena *toast_decompress_datum(struct varlena *attr);
 static struct varlena *toast_decompress_datum_slice(struct varlena *attr, int32 slicelength);
 
+static struct varlena *toast_fetch_datum_decoded(const DecodedExternalToast *decoded);
+static struct varlena *toast_decompress_decoded(const struct varlena *compressed,
+												const DecodedExternalToast *decoded);
+
+/* ----------
+ * decode_external_toast_pointer -
+ *
+ *	Decode external varlena into DecodedExternalToast struct.
+ *	Only handles VARTAG_ONDISK with known compression methods.
+ *	Returns false for INDIRECT, EXPANDED, or unrecognized compression.
+ * ----------
+ */
+static bool
+decode_external_toast_pointer(const struct varlena *attr,
+						   DecodedExternalToast *decoded)
+{
+	struct varatt_external toast_pointer;
+	vartag_external tag;
+
+	Assert(VARATT_IS_EXTERNAL(attr));
+
+	tag = VARTAG_EXTERNAL(attr);
+
+	if (tag == VARTAG_ONDISK)
+	{
+		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+
+		decoded->toastrelid = toast_pointer.va_toastrelid;
+		decoded->valueid = toast_pointer.va_valueid;
+		decoded->rawsize = toast_pointer.va_rawsize;
+		decoded->extsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+		decoded->flags = TOAST_EXT_IS_ONDISK;
+
+		if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+		{
+			uint32		cmid = VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer);
+
+			switch (cmid)
+			{
+				case TOAST_PGLZ_COMPRESSION_ID:
+					decoded->method = TOAST_DECOMP_PGLZ;
+					decoded->flags |= TOAST_EXT_HAS_TCINFO;
+					break;
+				case TOAST_LZ4_COMPRESSION_ID:
+					decoded->method = TOAST_DECOMP_LZ4;
+					decoded->flags |= TOAST_EXT_HAS_TCINFO;
+					break;
+				default:
+					/* Unknown compression - let caller fall back */
+					return false;
+			}
+		}
+		else
+		{
+			decoded->method = TOAST_DECOMP_NONE;
+		}
+
+		return true;
+	}
+
+	/* Not an on-disk datum (INDIRECT, EXPANDED, etc.) */
+	return false;
+}
+
 /* ----------
  * detoast_external_attr -
  *
@@ -115,57 +179,78 @@ detoast_external_attr(struct varlena *attr)
 struct varlena *
 detoast_attr(struct varlena *attr)
 {
-	if (VARATT_IS_EXTERNAL_ONDISK(attr))
+	if (VARATT_IS_EXTERNAL(attr))
 	{
-		/*
-		 * This is an externally stored datum --- fetch it back from there
-		 */
-		attr = toast_fetch_datum(attr);
-		/* If it's compressed, decompress it */
-		if (VARATT_IS_COMPRESSED(attr))
+		DecodedExternalToast decoded;
+
+		if (decode_external_toast_pointer(attr, &decoded))
 		{
-			struct varlena *tmp = attr;
+			struct varlena *fetched = toast_fetch_datum_decoded(&decoded);
+
+			if (decoded.method != TOAST_DECOMP_NONE)
+			{
+				struct varlena *result = toast_decompress_decoded(fetched, &decoded);
+				pfree(fetched);
+				return result;
+			}
+			return fetched;
+		}
 
-			attr = toast_decompress_datum(tmp);
-			pfree(tmp);
+		/* Decode failed: INDIRECT, EXPANDED, or unrecognized compression */
+		if (VARATT_IS_EXTERNAL_ONDISK(attr))
+		{
+			/* Unrecognized compression - legacy path preserves error behavior */
+			attr = toast_fetch_datum(attr);
+			if (VARATT_IS_COMPRESSED(attr))
+			{
+				struct varlena *tmp = attr;
+
+				attr = toast_decompress_datum(tmp);
+				pfree(tmp);
+			}
+			return attr;
 		}
-	}
-	else if (VARATT_IS_EXTERNAL_INDIRECT(attr))
-	{
-		/*
-		 * This is an indirect pointer --- dereference it
-		 */
-		struct varatt_indirect redirect;
+		else if (VARATT_IS_EXTERNAL_INDIRECT(attr))
+		{
+			/*
+			 * This is an indirect pointer --- dereference it
+			 */
+			struct varatt_indirect redirect;
 
-		VARATT_EXTERNAL_GET_POINTER(redirect, attr);
-		attr = (struct varlena *) redirect.pointer;
+			VARATT_EXTERNAL_GET_POINTER(redirect, attr);
+			attr = (struct varlena *) redirect.pointer;
 
-		/* nested indirect Datums aren't allowed */
-		Assert(!VARATT_IS_EXTERNAL_INDIRECT(attr));
+			/* nested indirect Datums aren't allowed */
+			Assert(!VARATT_IS_EXTERNAL_INDIRECT(attr));
 
-		/* recurse in case value is still extended in some other way */
-		attr = detoast_attr(attr);
+			/* recurse in case value is still extended in some other way */
+			attr = detoast_attr(attr);
 
-		/* if it isn't, we'd better copy it */
-		if (attr == (struct varlena *) redirect.pointer)
-		{
-			struct varlena *result;
+			/* if it isn't, we'd better copy it */
+			if (attr == (struct varlena *) redirect.pointer)
+			{
+				struct varlena *result;
 
-			result = (struct varlena *) palloc(VARSIZE_ANY(attr));
-			memcpy(result, attr, VARSIZE_ANY(attr));
-			attr = result;
+				result = (struct varlena *) palloc(VARSIZE_ANY(attr));
+				memcpy(result, attr, VARSIZE_ANY(attr));
+				attr = result;
+			}
+			return attr;
+		}
+		else if (VARATT_IS_EXTERNAL_EXPANDED(attr))
+		{
+			/*
+			 * This is an expanded-object pointer --- get flat format
+			 */
+			attr = detoast_external_attr(attr);
+			/* flatteners are not allowed to produce compressed/short output */
+			Assert(!VARATT_IS_EXTENDED(attr));
+			return attr;
 		}
 	}
-	else if (VARATT_IS_EXTERNAL_EXPANDED(attr))
-	{
-		/*
-		 * This is an expanded-object pointer --- get flat format
-		 */
-		attr = detoast_external_attr(attr);
-		/* flatteners are not allowed to produce compressed/short output */
-		Assert(!VARATT_IS_EXTENDED(attr));
-	}
-	else if (VARATT_IS_COMPRESSED(attr))
+
+	/* Handle inline cases (not external) */
+	if (VARATT_IS_COMPRESSED(attr))
 	{
 		/*
 		 * This is a compressed value inside of the main tuple
@@ -223,63 +308,75 @@ detoast_attr_slice(struct varlena *attr,
 	else if (pg_add_s32_overflow(sliceoffset, slicelength, &slicelimit))
 		slicelength = slicelimit = -1;
 
-	if (VARATT_IS_EXTERNAL_ONDISK(attr))
+	if (VARATT_IS_EXTERNAL(attr))
 	{
-		struct varatt_external toast_pointer;
+		DecodedExternalToast decoded;
 
-		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+		if (decode_external_toast_pointer(attr, &decoded))
+		{
+			if (decoded.method == TOAST_DECOMP_NONE)
+				return toast_fetch_datum_slice(attr, sliceoffset, slicelength);
+
+			/* Compressed: fetch enough to decompress the requested prefix */
+			if (slicelimit >= 0)
+			{
+				int32		max_size = decoded.extsize;
+
+				/* PGLZ supports partial decompression; LZ4 needs all data */
+				if (decoded.method == TOAST_DECOMP_PGLZ)
+					max_size = pglz_maximum_compressed_size(slicelimit, max_size);
+
+				preslice = toast_fetch_datum_slice(attr, 0, max_size);
+			}
+			else
+				preslice = toast_fetch_datum_decoded(&decoded);
+		}
+		else if (VARATT_IS_EXTERNAL_ONDISK(attr))
+		{
+			/* Unrecognized compression - legacy path */
+			struct varatt_external toast_pointer;
 
-		/* fast path for non-compressed external datums */
-		if (!VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
-			return toast_fetch_datum_slice(attr, sliceoffset, slicelength);
+			VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 
-		/*
-		 * For compressed values, we need to fetch enough slices to decompress
-		 * at least the requested part (when a prefix is requested).
-		 * Otherwise, just fetch all slices.
-		 */
-		if (slicelimit >= 0)
-		{
-			int32		max_size = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
+			if (!VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+				return toast_fetch_datum_slice(attr, sliceoffset, slicelength);
 
-			/*
-			 * 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.
-			 */
-			if (VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) ==
-				TOAST_PGLZ_COMPRESSION_ID)
-				max_size = pglz_maximum_compressed_size(slicelimit, max_size);
+			if (slicelimit >= 0)
+			{
+				int32		max_size = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer);
 
-			/*
-			 * Fetch enough compressed slices (compressed marker will get set
-			 * automatically).
-			 */
-			preslice = toast_fetch_datum_slice(attr, 0, max_size);
+				if (VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) ==
+					TOAST_PGLZ_COMPRESSION_ID)
+					max_size = pglz_maximum_compressed_size(slicelimit, max_size);
+
+				preslice = toast_fetch_datum_slice(attr, 0, max_size);
+			}
+			else
+				preslice = toast_fetch_datum(attr);
 		}
-		else
-			preslice = toast_fetch_datum(attr);
-	}
-	else if (VARATT_IS_EXTERNAL_INDIRECT(attr))
-	{
-		struct varatt_indirect redirect;
+		else if (VARATT_IS_EXTERNAL_INDIRECT(attr))
+		{
+			struct varatt_indirect redirect;
 
-		VARATT_EXTERNAL_GET_POINTER(redirect, attr);
+			VARATT_EXTERNAL_GET_POINTER(redirect, attr);
 
-		/* nested indirect Datums aren't allowed */
-		Assert(!VARATT_IS_EXTERNAL_INDIRECT(redirect.pointer));
+			/* nested indirect Datums aren't allowed */
+			Assert(!VARATT_IS_EXTERNAL_INDIRECT(redirect.pointer));
 
-		return detoast_attr_slice(redirect.pointer,
-								  sliceoffset, slicelength);
-	}
-	else if (VARATT_IS_EXTERNAL_EXPANDED(attr))
-	{
-		/* pass it off to detoast_external_attr to flatten */
-		preslice = detoast_external_attr(attr);
+			return detoast_attr_slice(redirect.pointer,
+									  sliceoffset, slicelength);
+		}
+		else if (VARATT_IS_EXTERNAL_EXPANDED(attr))
+		{
+			/* pass it off to detoast_external_attr to flatten */
+			preslice = detoast_external_attr(attr);
+		}
+		else
+		{
+			/* Should not reach here - unknown external type */
+			elog(ERROR, "unexpected external varlena type");
+			preslice = attr;	/* keep compiler quiet */
+		}
 	}
 	else
 		preslice = attr;
@@ -462,6 +559,61 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset,
 	return result;
 }
 
+/* ----------
+ * toast_fetch_datum_decoded -
+ *
+ *	Fetch TOAST data using pre-decoded pointer info.
+ * ----------
+ */
+static struct varlena *
+toast_fetch_datum_decoded(const DecodedExternalToast *decoded)
+{
+	Relation	toastrel;
+	struct varlena *result;
+	int32		attrsize = decoded->extsize;
+
+	result = (struct varlena *) palloc(attrsize + VARHDRSZ);
+
+	/* HAS_TCINFO determines header format, not "is compressed" */
+	if (decoded->flags & TOAST_EXT_HAS_TCINFO)
+		SET_VARSIZE_COMPRESSED(result, attrsize + VARHDRSZ);
+	else
+		SET_VARSIZE(result, attrsize + VARHDRSZ);
+
+	if (attrsize == 0)
+		return result;
+
+	toastrel = table_open(decoded->toastrelid, AccessShareLock);
+	table_relation_fetch_toast_slice(toastrel, decoded->valueid,
+									 attrsize, 0, attrsize, result);
+	table_close(toastrel, AccessShareLock);
+
+	return result;
+}
+
+/* ----------
+ * toast_decompress_decoded -
+ *
+ *	Decompress TOAST data using decoded method.
+ * ----------
+ */
+static struct varlena *
+toast_decompress_decoded(const struct varlena *compressed,
+						 const DecodedExternalToast *decoded)
+{
+	switch (decoded->method)
+	{
+		case TOAST_DECOMP_PGLZ:
+			return pglz_decompress_datum(compressed);
+		case TOAST_DECOMP_LZ4:
+			return lz4_decompress_datum(compressed);
+		case TOAST_DECOMP_NONE:
+		default:
+			elog(ERROR, "unexpected decompression method %d", decoded->method);
+			return NULL;
+	}
+}
+
 /* ----------
  * toast_decompress_datum -
  *
diff --git a/src/include/access/toast_internals.h b/src/include/access/toast_internals.h
index 06ae8583c1e..0245f67b246 100644
--- a/src/include/access/toast_internals.h
+++ b/src/include/access/toast_internals.h
@@ -16,6 +16,45 @@
 #include "storage/lockdefs.h"
 #include "utils/relcache.h"
 #include "utils/snapshot.h"
+#include "varatt.h"
+
+/*
+ * Decompression method for decoded toast pointers. Separate from
+ * ToastCompressionId (2-bit on-disk encoding) to allow future methods.
+ */
+typedef enum ToastDecompressMethod
+{
+	TOAST_DECOMP_NONE = 0,
+	TOAST_DECOMP_PGLZ = 1,
+	TOAST_DECOMP_LZ4 = 2
+} ToastDecompressMethod;
+
+/*
+ * Flags for DecodedExternalToast.
+ *
+ * HAS_TCINFO: Payload starts with tcinfo header. True for PGLZ/LZ4 external;
+ *   false for uncompressed or future methods storing raw compressed data.
+ * IS_ONDISK: Set for VARTAG_ONDISK (for future vartag extension).
+ */
+#define TOAST_EXT_HAS_TCINFO	0x01
+#define TOAST_EXT_IS_ONDISK		0x02
+
+/*
+ * Decoded representation of an external on-disk TOAST pointer.
+ * Normalizes vartag/va_extinfo variations; decode once, use throughout.
+ *
+ * HAS_TCINFO indicates payload format (has tcinfo header), distinct from
+ * "is compressed" (extsize < rawsize) - future methods may omit tcinfo.
+ */
+typedef struct DecodedExternalToast
+{
+	Oid			toastrelid;
+	Oid			valueid;
+	uint32		rawsize;		/* Decompressed size; for future methods without tcinfo */
+	uint32		extsize;		/* On-disk payload size */
+	ToastDecompressMethod method;
+	uint8		flags;
+} DecodedExternalToast;
 
 /*
  *	The information at the start of the compressed toast data.
-- 
2.39.3 (Apple Git-146)

#16Michael Paquier
michael@paquier.xyz
In reply to: Dharin Shah (#15)
Re: Fwd: [PATCH] Add zstd compression for TOAST using extended header format

On Mon, Dec 29, 2025 at 02:45:27PM +0100, Dharin Shah wrote:

The goal is to centralize “how do we interpret an external datum?” so that
detoast code paths don’t have to reason directly about va_extinfo encoding
vs payload layout details. This is intended as groundwork for a follow-up
patch adding a new vartag-based method (e.g., zstd) without scattering
special cases in detoast paths.

+static bool
+decode_external_toast_pointer(const struct varlena *attr,
+						   DecodedExternalToast *decoded)
[...]
+typedef enum ToastDecompressMethod
+{
+	TOAST_DECOMP_NONE = 0,
+	TOAST_DECOMP_PGLZ = 1,
+	TOAST_DECOMP_LZ4 = 2
+} ToastDecompressMethod;
+
+typedef struct DecodedExternalToast
+{
+	Oid			toastrelid;
+	Oid			valueid;
+	uint32		rawsize;		/* Decompressed size; for future methods without tcinfo */
+	uint32		extsize;		/* On-disk payload size */
+	ToastDecompressMethod method;
+	uint8		flags;
+} DecodedExternalToast;

Yeah, honestly this is a layer I have been thinking about as well as
one option, but contrary to you I have been focusing on putting that
into varatt.h, with the exception of the value being an Oid8. I think
that you have an interesting point in focusing your implementation to
be stored in the detoast part, though. I'd need to spend a bit more
time to see the result this would lead at with the larger 8-byte issue
in mind, but this is something that would come at no real cost as it
has no function pointer redirection compared to what I was first
envisioning on the other thread. That's especially true if it makes
the CompressionId business easier to mold around when adding a new
vartag.

Why HAS_TCINFO?
- Previously, “is compressed?” was used as a proxy for whether the external
payload begins with tcinfo. This patch makes that explicit: HAS_TCINFO
captures payload layout, which is distinct from whether the value is
compressed. This separation is needed for future methods that may store
external compressed payloads without tcinfo.

It is possible to model the on-memory data as we want. This
suggestion would be OK with some flags.
--
Michael

#17Dharin Shah
dharinshah95@gmail.com
In reply to: Michael Paquier (#16)
Re: Fwd: [PATCH] Add zstd compression for TOAST using extended header format

Thanks Michael,

After looking more closely at your “8‑byte TOAST values / infinite loop”
thread and patch series, I see this is very much the same direction you
outlined there: introduce a normalized in-memory representation for
external pointers (toast_external_data) and keep most call sites from
having to reason about vartag_external/va_extinfo details directly [1]/messages/by-id/CAN-LCVNsE4x0k11ZRWvU4ySTbe98fwA16qzV7p8dxogWnD5Jng@mail.gmail.com.

For this refactor patch I kept the decoder local to detoast.c to minimize
scope and avoid committing to a broader API boundary too early. But if the
consensus heads toward a shared interface closer to the format definitions
(as in your toast_external approach), I’m happy to respin/rework this patch
to align with that direction, rather than working on parallel
abstractions. It should also be straightforward to mold this refactor in
the direction of the 8‑byte value-id work without changing the overall
detoast structure.

On HAS_TCINFO flag: the intent is to make payload layout explicit. In the
current code, “external is compressed” effectively implies “payload begins
with tcinfo”, which is wired into fetch/slice logic. For a vartag-based
follow-up (e.g., zstd), we may want compressed payloads without a tcinfo
prefix, so having an explicit flag keeps detoast paths uniform and avoids
method-specific shortcuts.

Let me know what you’d prefer for next steps: keep this patch as a
detoast-local refactor, or respin it to align more directly with a shared
decoded external-pointer interface in the direction of the 8‑byte work.

[1]: /messages/by-id/CAN-LCVNsE4x0k11ZRWvU4ySTbe98fwA16qzV7p8dxogWnD5Jng@mail.gmail.com
/messages/by-id/CAN-LCVNsE4x0k11ZRWvU4ySTbe98fwA16qzV7p8dxogWnD5Jng@mail.gmail.com

Thanks,
Dharin

On Tue, Dec 30, 2025 at 12:46 AM Michael Paquier <michael@paquier.xyz>
wrote:

Show quoted text

On Mon, Dec 29, 2025 at 02:45:27PM +0100, Dharin Shah wrote:

The goal is to centralize “how do we interpret an external datum?” so

that

detoast code paths don’t have to reason directly about va_extinfo

encoding

vs payload layout details. This is intended as groundwork for a follow-up
patch adding a new vartag-based method (e.g., zstd) without scattering
special cases in detoast paths.

+static bool
+decode_external_toast_pointer(const struct varlena *attr,
+                                                  DecodedExternalToast
*decoded)
[...]
+typedef enum ToastDecompressMethod
+{
+       TOAST_DECOMP_NONE = 0,
+       TOAST_DECOMP_PGLZ = 1,
+       TOAST_DECOMP_LZ4 = 2
+} ToastDecompressMethod;
+
+typedef struct DecodedExternalToast
+{
+       Oid                     toastrelid;
+       Oid                     valueid;
+       uint32          rawsize;                /* Decompressed size; for
future methods without tcinfo */
+       uint32          extsize;                /* On-disk payload size */
+       ToastDecompressMethod method;
+       uint8           flags;
+} DecodedExternalToast;

Yeah, honestly this is a layer I have been thinking about as well as
one option, but contrary to you I have been focusing on putting that
into varatt.h, with the exception of the value being an Oid8. I think
that you have an interesting point in focusing your implementation to
be stored in the detoast part, though. I'd need to spend a bit more
time to see the result this would lead at with the larger 8-byte issue
in mind, but this is something that would come at no real cost as it
has no function pointer redirection compared to what I was first
envisioning on the other thread. That's especially true if it makes
the CompressionId business easier to mold around when adding a new
vartag.

Why HAS_TCINFO?
- Previously, “is compressed?” was used as a proxy for whether the

external

payload begins with tcinfo. This patch makes that explicit: HAS_TCINFO
captures payload layout, which is distinct from whether the value is
compressed. This separation is needed for future methods that may store
external compressed payloads without tcinfo.

It is possible to model the on-memory data as we want. This
suggestion would be OK with some flags.
--
Michael