public inbox for [email protected]help / color / mirror / Atom feed
[PATCH] Add zstd compression for TOAST using extended header format 19+ messages / 5 participants [nested] [flat]
* [PATCH] Add zstd compression for TOAST using extended header format @ 2025-12-13 17:31 Dharin Shah <[email protected]> 0 siblings, 2 replies; 19+ messages in thread From: Dharin Shah @ 2025-12-13 17:31 UTC (permalink / raw) To: [email protected] 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 <https://www.postgresql.org/message-id/flat/CAFAfj_F4qeRCNCYPk1vgH42fDZpjQWKO%2Bufq3FyoVyUa5AviFA%40m...; ][2 <https://www.postgresql.org/message-id/flat/CAJ7c6TOtAB0z1UrksvGTStNE-herK-43bj22=5xVBg7S4vr5rQ@mail....; ][3 <https://www.postgresql.org/message-id/flat/[email protected];]. A bit of a background in the 2022 thread [3 <https://www.postgresql.org/message-id/flat/[email protected];], 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: [application/octet-stream] zstd-toast-compression-external.patch (80.8K, 3-zstd-toast-compression-external.patch) download | inline diff: From ee7ba3a0a160bfe8811bad2230b2a023175180d5 Mon Sep 17 00:00:00 2001 From: Dharin Shah <[email protected]> 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) ^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: [PATCH] Add zstd compression for TOAST using extended header format @ 2025-12-13 19:32 Dharin Shah <[email protected]> parent: Dharin Shah <[email protected]> 1 sibling, 0 replies; 19+ messages in thread From: Dharin Shah @ 2025-12-13 19:32 UTC (permalink / raw) To: [email protected] 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 <[email protected]> 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 > <https://www.postgresql.org/message-id/flat/CAFAfj_F4qeRCNCYPk1vgH42fDZpjQWKO%2Bufq3FyoVyUa5AviFA%40m...; > ][2 > <https://www.postgresql.org/message-id/flat/CAJ7c6TOtAB0z1UrksvGTStNE-herK-43bj22=5xVBg7S4vr5rQ@mail....; > ][3 > <https://www.postgresql.org/message-id/flat/[email protected]; > ]. > > A bit of a background in the 2022 thread [3 > <https://www.postgresql.org/message-id/flat/[email protected];], > 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: [application/octet-stream] zstd-toast-compression-external.patch (78.2K, 3-zstd-toast-compression-external.patch) download | inline diff: From fdaae5dc9e9837f73b991100adcba6d76dda1f40 Mon Sep 17 00:00:00 2001 From: Dharin Shah <[email protected]> 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) ^ permalink raw reply [nested|flat] 19+ messages in thread
* [PATCH] Add zstd compression for TOAST using extended header format @ 2025-12-15 19:16 Dharin Shah <[email protected]> parent: Dharin Shah <[email protected]> 1 sibling, 2 replies; 19+ messages in thread From: Dharin Shah @ 2025-12-15 19:16 UTC (permalink / raw) To: [email protected] 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 <https://www.postgresql.org/message-id/flat/CAFAfj_F4qeRCNCYPk1vgH42fDZpjQWKO%2Bufq3FyoVyUa5AviFA%40m...; ][2 <https://www.postgresql.org/message-id/flat/CAJ7c6TOtAB0z1UrksvGTStNE-herK-43bj22=5xVBg7S4vr5rQ@mail....; ][3 <https://www.postgresql.org/message-id/flat/[email protected];]. A bit of a background in the 2022 thread [3 <https://www.postgresql.org/message-id/flat/[email protected];], 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: [application/x-patch] zstd-toast-compression-external.patch (78.2K, 3-zstd-toast-compression-external.patch) download | inline diff: From fdaae5dc9e9837f73b991100adcba6d76dda1f40 Mon Sep 17 00:00:00 2001 From: Dharin Shah <[email protected]> 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) ^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: [PATCH] Add zstd compression for TOAST using extended header format @ 2025-12-16 05:56 Murtuza Zabuawala <[email protected]> parent: Dharin Shah <[email protected]> 1 sibling, 1 reply; 19+ messages in thread From: Murtuza Zabuawala @ 2025-12-16 05:56 UTC (permalink / raw) To: Dharin Shah <[email protected]>; +Cc: [email protected] Hello, You may want to consider sending the patch to the pgsql-hackers mailing list. Murtuza Zabuawala enterprisedb.com <http://enterprisedb.com/; > On 16 Dec 2025, at 12:46 AM, Dharin Shah <[email protected]> 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 <https://www.postgresql.org/message-id/flat/CAFAfj_F4qeRCNCYPk1vgH42fDZpjQWKO%2Bufq3FyoVyUa5AviFA%40m...;][2 <https://www.postgresql.org/message-id/flat/CAJ7c6TOtAB0z1UrksvGTStNE-herK-43bj22=5xVBg7S4vr5rQ@mail....;][3 <https://www.postgresql.org/message-id/flat/[email protected];]. > > A bit of a background in the 2022 thread [3 <https://www.postgresql.org/message-id/flat/[email protected];], 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> ^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: [PATCH] Add zstd compression for TOAST using extended header format @ 2025-12-16 10:49 Dharin Shah <[email protected]> parent: Murtuza Zabuawala <[email protected]> 0 siblings, 0 replies; 19+ messages in thread From: Dharin Shah @ 2025-12-16 10:49 UTC (permalink / raw) To: Murtuza Zabuawala <[email protected]>; +Cc: [email protected] THanks Murtuza, My bad, wrong email :( Regards, Dharin On Tue, Dec 16, 2025 at 6:56 AM Murtuza Zabuawala < [email protected]> wrote: > 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 <[email protected]> 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 > <https://www.postgresql.org/message-id/flat/CAFAfj_F4qeRCNCYPk1vgH42fDZpjQWKO%2Bufq3FyoVyUa5AviFA%40m...; > ][2 > <https://www.postgresql.org/message-id/flat/CAJ7c6TOtAB0z1UrksvGTStNE-herK-43bj22=5xVBg7S4vr5rQ@mail....; > ][3 > <https://www.postgresql.org/message-id/flat/[email protected]; > ]. > > A bit of a background in the 2022 thread [3 > <https://www.postgresql.org/message-id/flat/[email protected];], > 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> > > > ^ permalink raw reply [nested|flat] 19+ messages in thread
* Fwd: [PATCH] Add zstd compression for TOAST using extended header format @ 2025-12-16 10:51 Dharin Shah <[email protected]> parent: Dharin Shah <[email protected]> 1 sibling, 1 reply; 19+ messages in thread From: Dharin Shah @ 2025-12-16 10:51 UTC (permalink / raw) To: [email protected] 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 <https://www.postgresql.org/message-id/flat/CAFAfj_F4qeRCNCYPk1vgH42fDZpjQWKO%2Bufq3FyoVyUa5AviFA%40m...; ][2 <https://www.postgresql.org/message-id/flat/CAJ7c6TOtAB0z1UrksvGTStNE-herK-43bj22=5xVBg7S4vr5rQ@mail....; ][3 <https://www.postgresql.org/message-id/flat/[email protected];]. A bit of a background in the 2022 thread [3 <https://www.postgresql.org/message-id/flat/[email protected];], 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: [application/octet-stream] zstd-toast-compression-external.patch (78.2K, 3-zstd-toast-compression-external.patch) download | inline diff: From fdaae5dc9e9837f73b991100adcba6d76dda1f40 Mon Sep 17 00:00:00 2001 From: Dharin Shah <[email protected]> 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) ^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Fwd: [PATCH] Add zstd compression for TOAST using extended header format @ 2025-12-17 15:11 Peter Eisentraut <[email protected]> parent: Dharin Shah <[email protected]> 0 siblings, 1 reply; 19+ messages in thread From: Peter Eisentraut @ 2025-12-17 15:11 UTC (permalink / raw) To: Dharin Shah <[email protected]>; [email protected] 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. ^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Fwd: [PATCH] Add zstd compression for TOAST using extended header format @ 2025-12-18 06:35 Michael Paquier <[email protected]> parent: Peter Eisentraut <[email protected]> 0 siblings, 1 reply; 19+ messages in thread From: Michael Paquier @ 2025-12-18 06:35 UTC (permalink / raw) To: Peter Eisentraut <[email protected]>; +Cc: Dharin Shah <[email protected]>; [email protected] 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 Attachments: [application/pgp-signature] signature.asc (833B, 2-signature.asc) download ^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Fwd: [PATCH] Add zstd compression for TOAST using extended header format @ 2025-12-18 21:44 Dharin Shah <[email protected]> parent: Michael Paquier <[email protected]> 0 siblings, 1 reply; 19+ messages in thread From: Dharin Shah @ 2025-12-18 21:44 UTC (permalink / raw) To: Michael Paquier <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; [email protected] 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 <[email protected]> wrote: > 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 > ^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Fwd: [PATCH] Add zstd compression for TOAST using extended header format @ 2025-12-18 22:44 Michael Paquier <[email protected]> parent: Dharin Shah <[email protected]> 0 siblings, 2 replies; 19+ messages in thread From: Michael Paquier @ 2025-12-18 22:44 UTC (permalink / raw) To: Dharin Shah <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; [email protected] 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: https://www.postgresql.org/message-id/764273.1669674269%40sss.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: [application/pgp-signature] signature.asc (833B, 2-signature.asc) download ^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Fwd: [PATCH] Add zstd compression for TOAST using extended header format @ 2025-12-24 00:47 Dharin Shah <[email protected]> parent: Michael Paquier <[email protected]> 1 sibling, 0 replies; 19+ messages in thread From: Dharin Shah @ 2025-12-24 00:47 UTC (permalink / raw) To: Michael Paquier <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; [email protected] 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 <[email protected]> wrote: > 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: > https://www.postgresql.org/message-id/764273.1669674269%40sss.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: [application/octet-stream] benchmark_toast_compression.sql (26.2K, 3-benchmark_toast_compression.sql) download [application/octet-stream] v3-0001-Add-ZSTD-TOAST-compression-using-VARTAG-ONDISK-ZSTD.patch (52.1K, 4-v3-0001-Add-ZSTD-TOAST-compression-using-VARTAG-ONDISK-ZSTD.patch) download | inline diff: From b206ea02a266a630d1c869f19fa2adf716165809 Mon Sep 17 00:00:00 2001 From: Dharin Shah <[email protected]> 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) [application/octet-stream] backwards_compatibility_test.sql (13.8K, 5-backwards_compatibility_test.sql) download ^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Fwd: [PATCH] Add zstd compression for TOAST using extended header format @ 2025-12-24 16:50 Robert Treat <[email protected]> parent: Michael Paquier <[email protected]> 1 sibling, 1 reply; 19+ messages in thread From: Robert Treat @ 2025-12-24 16:50 UTC (permalink / raw) To: Michael Paquier <[email protected]>; +Cc: Dharin Shah <[email protected]>; Peter Eisentraut <[email protected]>; [email protected] On Thu, Dec 18, 2025 at 5:44 PM Michael Paquier <[email protected]> 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 ^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Fwd: [PATCH] Add zstd compression for TOAST using extended header format @ 2025-12-25 00:24 Michael Paquier <[email protected]> parent: Robert Treat <[email protected]> 0 siblings, 1 reply; 19+ messages in thread From: Michael Paquier @ 2025-12-25 00:24 UTC (permalink / raw) To: Robert Treat <[email protected]>; +Cc: Dharin Shah <[email protected]>; Peter Eisentraut <[email protected]>; [email protected] 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: [application/pgp-signature] signature.asc (833B, 2-signature.asc) download ^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Fwd: [PATCH] Add zstd compression for TOAST using extended header format @ 2025-12-25 00:54 Dharin Shah <[email protected]> parent: Michael Paquier <[email protected]> 0 siblings, 1 reply; 19+ messages in thread From: Dharin Shah @ 2025-12-25 00:54 UTC (permalink / raw) To: Michael Paquier <[email protected]>; +Cc: Robert Treat <[email protected]>; Peter Eisentraut <[email protected]>; [email protected] 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 <[email protected]> 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 > ^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Fwd: [PATCH] Add zstd compression for TOAST using extended header format @ 2025-12-29 13:45 Dharin Shah <[email protected]> parent: Dharin Shah <[email protected]> 0 siblings, 1 reply; 19+ messages in thread From: Dharin Shah @ 2025-12-29 13:45 UTC (permalink / raw) To: Michael Paquier <[email protected]>; +Cc: Robert Treat <[email protected]>; Peter Eisentraut <[email protected]>; [email protected] 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 <[email protected]> wrote: > 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 <[email protected]> > 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: [application/octet-stream] benchmark_toast_detoast.sql (2.8K, 3-benchmark_toast_detoast.sql) download [application/octet-stream] 0001-refactor-detoast-to-use-decoded-external-toast-abstr.patch (13.2K, 4-0001-refactor-detoast-to-use-decoded-external-toast-abstr.patch) download | inline diff: From 49939fe3416803d446ba43c404eae9711a3ae21b Mon Sep 17 00:00:00 2001 From: Dharin Shah <[email protected]> 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) ^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Fwd: [PATCH] Add zstd compression for TOAST using extended header format @ 2025-12-29 23:45 Michael Paquier <[email protected]> parent: Dharin Shah <[email protected]> 0 siblings, 1 reply; 19+ messages in thread From: Michael Paquier @ 2025-12-29 23:45 UTC (permalink / raw) To: Dharin Shah <[email protected]>; +Cc: Robert Treat <[email protected]>; Peter Eisentraut <[email protected]>; [email protected] 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 Attachments: [application/pgp-signature] signature.asc (833B, 2-signature.asc) download ^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Fwd: [PATCH] Add zstd compression for TOAST using extended header format @ 2025-12-31 15:02 Dharin Shah <[email protected]> parent: Michael Paquier <[email protected]> 0 siblings, 1 reply; 19+ messages in thread From: Dharin Shah @ 2025-12-31 15:02 UTC (permalink / raw) To: Michael Paquier <[email protected]>; +Cc: Robert Treat <[email protected]>; Peter Eisentraut <[email protected]>; [email protected] 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]. 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] https://www.postgresql.org/message-id/flat/CAN-LCVNsE4x0k11ZRWvU4ySTbe98fwA16qzV7p8dxogWnD5Jng%40mai... Thanks, Dharin On Tue, Dec 30, 2025 at 12:46 AM Michael Paquier <[email protected]> wrote: > 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 > ^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Fwd: [PATCH] Add zstd compression for TOAST using extended header format @ 2026-01-20 06:45 Michael Paquier <[email protected]> parent: Dharin Shah <[email protected]> 0 siblings, 1 reply; 19+ messages in thread From: Michael Paquier @ 2026-01-20 06:45 UTC (permalink / raw) To: Dharin Shah <[email protected]>; +Cc: Robert Treat <[email protected]>; Peter Eisentraut <[email protected]>; [email protected] On Wed, Dec 31, 2025 at 04:02:24PM +0100, Dharin Shah wrote: > 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. My apologies for the rather long silence on this thread. As the next step of this project, I am going to put my hands of what you are suggesting here, and see how I can align it with the 64-bit toast value patch: https://www.postgresql.org/message-id/[email protected]... What I am pretty sure about at this stage is that there is little love for the patch set I have sent on the other thread where I have been using pointer redirections for the TOAST function calls with callbacks (perhaps I'll be able to apply some of the renaming patches anyway, nobody would scream at me for that), at least nobody has put a +1 on it or just ignored it, so this approach feels dead to me. What you are suggesting upthread, though, is a direction I'd like to dig into and this comes down to how I can unify what you want to do for zstd and what I want to do with Oid8. Perhaps that you are right and that it is just simpler to invest on an interface in the detoast code, but I still see that there is nothing done for the logical decoding or amcheck code paths, which is something my other patch is able to deal with transparently. -- Michael Attachments: [application/pgp-signature] signature.asc (833B, 2-signature.asc) download ^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Fwd: [PATCH] Add zstd compression for TOAST using extended header format @ 2026-03-09 06:02 Michael Paquier <[email protected]> parent: Michael Paquier <[email protected]> 0 siblings, 0 replies; 19+ messages in thread From: Michael Paquier @ 2026-03-09 06:02 UTC (permalink / raw) To: Dharin Shah <[email protected]>; +Cc: Robert Treat <[email protected]>; Peter Eisentraut <[email protected]>; [email protected]; Nikhil Kumar Veldanda <[email protected]> On Tue, Jan 20, 2026 at 03:45:12PM +0900, Michael Paquier wrote: > What I am pretty sure about at this stage is that there is little love > for the patch set I have sent on the other thread where I have been > using pointer redirections for the TOAST function calls with > callbacks (perhaps I'll be able to apply some of the renaming patches > anyway, nobody would scream at me for that), at least nobody has put a > +1 on it or just ignored it, so this approach feels dead to me. What > you are suggesting upthread, though, is a direction I'd like to dig > into and this comes down to how I can unify what you want to do for > zstd and what I want to do with Oid8. Perhaps that you are right and > that it is just simpler to invest on an interface in the detoast code, > but I still see that there is nothing done for the logical decoding or > amcheck code paths, which is something my other patch is able to deal > with transparently. (Added Nikhil in CC.) While looking at what could be achieved for this release, I have hacked on a patch that removes the limitation of the GUC enum for defailt_toast_compression, which is currently limited at 4 values due to the two bits allocated in the varlenas, and finished with the attached. This relies on the concept of a compression method "registry", saved in toast_compression.c, that includes 4 fields for each compression method supported in TOAST: - The attcompression attribute in catalogs, a char. - The compression method name. - The compression GUC enum value. - The on-disk varlena value, now moved to varatt.h. What do you think about this approach? -- Michael From 217200228cae79ee3b88d99ed5b64d395727edae Mon Sep 17 00:00:00 2001 From: Michael Paquier <[email protected]> Date: Mon, 9 Mar 2026 14:58:40 +0900 Subject: [PATCH] Refactor code logic around TOAST compression The TOAST compression code links the GUC values of default_toast_compression to the on-disk varatt attributes, limiting the number of elements in the enum GUC structure to 4 elements, to cover for the 2 bits that can be saved in va_tcinfo or va_extinfo, depending on if we are dealing with an inline compressible entry, or an external TOAST pointer. This commit refactors the TOAST code so as we have a clean split between three concepts: - The on-disk varatt values. - The GUC enum values. - The catalog attribute char values. - The name of the compression names All the knowledge of each method is now localized in a single "registry", with a set of routines that are able to retrieve some of the properties. The goal of this patch is to ease the addition of future methods, as we are going to need a split for the on-disk varatt properties across multiple vartags, while lifting the GUC enum limitation. --- src/include/access/toast_compression.h | 43 +++--- src/include/access/toast_internals.h | 4 +- src/include/varatt.h | 12 +- src/backend/access/common/detoast.c | 14 +- src/backend/access/common/toast_compression.c | 137 +++++++++++++++--- src/backend/access/common/toast_internals.c | 13 +- src/backend/utils/adt/varlena.c | 8 +- src/backend/utils/misc/guc_tables.c | 4 +- contrib/amcheck/verify_heapam.c | 19 +-- src/tools/pgindent/typedefs.list | 3 +- 10 files changed, 176 insertions(+), 81 deletions(-) diff --git a/src/include/access/toast_compression.h b/src/include/access/toast_compression.h index 3265f10b734f..fa852228235c 100644 --- a/src/include/access/toast_compression.h +++ b/src/include/access/toast_compression.h @@ -16,30 +16,22 @@ /* * 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. + * default_toast_compression is an integer for purposes of the GUC machinery. + * Its value is one of the ToastCompressionGucValue enum values defined below, + * which are independent from both the catalog char constants stored in + * pg_attribute.attcompression and the on-disk compression ID values + * (TOAST_COMPRESS_xxx from varatt.h). */ extern PGDLLIMPORT int default_toast_compression; /* - * Built-in compression method ID. The toast compression header will store - * this in the first 2 bits of the raw length. These built-in compression - * method IDs are directly mapped to the built-in compression methods. - * - * Don't use these values for anything other than understanding the meaning - * of the raw bits from a varlena; in particular, if the goal is to identify - * a compression method, use the constants TOAST_PGLZ_COMPRESSION, etc. - * below. We might someday support more than 4 compression methods, but - * we can never have more than 4 values in this enum, because there are - * only 2 bits available in the places where this is stored. + * Values for GUC default_toast_compression. */ -typedef enum ToastCompressionId +typedef enum ToastCompressionGucValue { - TOAST_PGLZ_COMPRESSION_ID = 0, - TOAST_LZ4_COMPRESSION_ID = 1, - TOAST_INVALID_COMPRESSION_ID = 2, -} ToastCompressionId; + TOAST_PGLZ_COMPRESSION_GUC = 0, + TOAST_LZ4_COMPRESSION_GUC = 1, +} ToastCompressionGucValue; /* * Built-in compression methods. pg_attribute will store these in the @@ -57,9 +49,9 @@ typedef enum ToastCompressionId * compiled-in, use it, otherwise use pglz. */ #ifdef USE_LZ4 -#define DEFAULT_TOAST_COMPRESSION TOAST_LZ4_COMPRESSION +#define DEFAULT_TOAST_COMPRESSION TOAST_LZ4_COMPRESSION_GUC #else -#define DEFAULT_TOAST_COMPRESSION TOAST_PGLZ_COMPRESSION +#define DEFAULT_TOAST_COMPRESSION TOAST_PGLZ_COMPRESSION_GUC #endif /* pglz compression/decompression routines */ @@ -75,8 +67,17 @@ extern varlena *lz4_decompress_datum_slice(const varlena *value, int32 slicelength); /* other stuff */ -extern ToastCompressionId toast_get_compression_id(varlena *attr); +extern uint32 toast_get_compression_id(varlena *attr); extern char CompressionNameToMethod(const char *compression); extern const char *GetCompressionMethodName(char method); +/* + * Method Registry translation functions. "cmid" is the on-disk varatt + * value. + */ +extern char ToastCompressionGucToMethod(ToastCompressionGucValue guc_value); +extern uint32 MethodToCompressionId(char method); +extern char CompressionIdToMethod(uint32 cmid); +extern bool CompressionIdIsValid(uint32 cmid); + #endif /* TOAST_COMPRESSION_H */ diff --git a/src/include/access/toast_internals.h b/src/include/access/toast_internals.h index d382db342620..36b3abf242ad 100644 --- a/src/include/access/toast_internals.h +++ b/src/include/access/toast_internals.h @@ -39,8 +39,8 @@ typedef struct toast_compress_header #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); \ + Assert((cm_method) == TOAST_COMPRESS_PGLZ || \ + (cm_method) == TOAST_COMPRESS_LZ4); \ ((toast_compress_header *) (ptr))->tcinfo = \ (len) | ((uint32) (cm_method) << VARLENA_EXTSIZE_BITS); \ } while (0) diff --git a/src/include/varatt.h b/src/include/varatt.h index 000bdf33b923..026b73e87277 100644 --- a/src/include/varatt.h +++ b/src/include/varatt.h @@ -45,6 +45,14 @@ typedef struct varatt_external #define VARLENA_EXTSIZE_BITS 30 #define VARLENA_EXTSIZE_MASK ((1U << VARLENA_EXTSIZE_BITS) - 1) +/* + * On-disk compression method IDs stored in the high bits of va_tcinfo + * and va_extinfo. Only 2 bits are available, so at most 4 values. + */ +#define TOAST_COMPRESS_PGLZ 0 +#define TOAST_COMPRESS_LZ4 1 +#define TOAST_COMPRESS_INVALID 2 + /* * varatt_indirect is a "TOAST pointer" representing an out-of-line * Datum that's stored in memory, not in an external toast relation. @@ -519,8 +527,8 @@ VARATT_EXTERNAL_GET_COMPRESS_METHOD(varatt_external toast_pointer) /* 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); \ + Assert((cm) == TOAST_COMPRESS_PGLZ || \ + (cm) == TOAST_COMPRESS_LZ4); \ ((toast_pointer).va_extinfo = \ (len) | ((uint32) (cm) << VARLENA_EXTSIZE_BITS)); \ } while (0) diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c index a6c1f3a734b2..20111d6b275d 100644 --- a/src/backend/access/common/detoast.c +++ b/src/backend/access/common/detoast.c @@ -252,7 +252,7 @@ detoast_attr_slice(varlena *attr, * able to decompress the required slice. */ if (VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) == - TOAST_PGLZ_COMPRESSION_ID) + TOAST_COMPRESS_PGLZ) max_size = pglz_maximum_compressed_size(slicelimit, max_size); /* @@ -470,7 +470,7 @@ toast_fetch_datum_slice(varlena *attr, int32 sliceoffset, static varlena * toast_decompress_datum(varlena *attr) { - ToastCompressionId cmid; + uint32 cmid; Assert(VARATT_IS_COMPRESSED(attr)); @@ -481,9 +481,9 @@ toast_decompress_datum(varlena *attr) cmid = TOAST_COMPRESS_METHOD(attr); switch (cmid) { - case TOAST_PGLZ_COMPRESSION_ID: + case TOAST_COMPRESS_PGLZ: return pglz_decompress_datum(attr); - case TOAST_LZ4_COMPRESSION_ID: + case TOAST_COMPRESS_LZ4: return lz4_decompress_datum(attr); default: elog(ERROR, "invalid compression method id %d", cmid); @@ -502,7 +502,7 @@ toast_decompress_datum(varlena *attr) static varlena * toast_decompress_datum_slice(varlena *attr, int32 slicelength) { - ToastCompressionId cmid; + uint32 cmid; Assert(VARATT_IS_COMPRESSED(attr)); @@ -524,9 +524,9 @@ toast_decompress_datum_slice(varlena *attr, int32 slicelength) cmid = TOAST_COMPRESS_METHOD(attr); switch (cmid) { - case TOAST_PGLZ_COMPRESSION_ID: + case TOAST_COMPRESS_PGLZ: return pglz_decompress_datum_slice(attr, slicelength); - case TOAST_LZ4_COMPRESSION_ID: + case TOAST_COMPRESS_LZ4: return lz4_decompress_datum_slice(attr, slicelength); default: elog(ERROR, "invalid compression method id %d", cmid); diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c index 5a5d579494a2..36f57f7ed404 100644 --- a/src/backend/access/common/toast_compression.c +++ b/src/backend/access/common/toast_compression.c @@ -31,6 +31,30 @@ int default_toast_compression = DEFAULT_TOAST_COMPRESSION; errmsg("compression method %s not supported", method), \ errdetail("This functionality requires the server to be built with %s support.", method))) +/* + * Compression Method Registry for TOAST. + * + * Maps between the three compression namespaces: human-readable names, + * catalog char constants (pg_attribute.attcompression), on-disk + * compression ID values (TOAST_COMPRESS_xxx from varatt.h), and GUC enum + * values. + */ +typedef struct ToastCompressionRegistryEntry +{ + const char *name; /* human-readable name, e.g. "pglz" */ + char method; /* catalog char, e.g. TOAST_PGLZ_COMPRESSION */ + uint32 cmid; /* on-disk ID, e.g. TOAST_COMPRESS_PGLZ */ + ToastCompressionGucValue guc_value; /* GUC enum int, e.g. + * TOAST_PGLZ_COMPRESSION_GUC */ +} ToastCompressionRegistryEntry; + +static const ToastCompressionRegistryEntry toast_compression_registry[] = { + {"pglz", TOAST_PGLZ_COMPRESSION, TOAST_COMPRESS_PGLZ, TOAST_PGLZ_COMPRESSION_GUC}, + {"lz4", TOAST_LZ4_COMPRESSION, TOAST_COMPRESS_LZ4, TOAST_LZ4_COMPRESSION_GUC}, +}; + +#define TOAST_NUM_COMPRESSIONS lengthof(toast_compression_registry) + /* * Compress a varlena using PGLZ. * @@ -248,12 +272,12 @@ lz4_decompress_datum_slice(const varlena *value, int32 slicelength) /* * Extract compression ID from a varlena. * - * Returns TOAST_INVALID_COMPRESSION_ID if the varlena is not compressed. + * Returns TOAST_COMPRESS_INVALID if the varlena is not compressed. */ -ToastCompressionId +uint32 toast_get_compression_id(varlena *attr) { - ToastCompressionId cmid = TOAST_INVALID_COMPRESSION_ID; + uint32 cmid = TOAST_COMPRESS_INVALID; /* * If it is stored externally then fetch the compression method id from @@ -278,39 +302,114 @@ toast_get_compression_id(varlena *attr) /* * CompressionNameToMethod - Get compression method from compression name * - * Search in the available built-in methods. If the compression not found - * in the built-in methods then return InvalidCompressionMethod. + * Search the compression registry by name. If the compression is not found + * then return InvalidCompressionMethod. */ char CompressionNameToMethod(const char *compression) { - if (strcmp(compression, "pglz") == 0) - return TOAST_PGLZ_COMPRESSION; - else if (strcmp(compression, "lz4") == 0) + for (int i = 0; i < TOAST_NUM_COMPRESSIONS; i++) { + if (strcmp(compression, toast_compression_registry[i].name) == 0) + { #ifndef USE_LZ4 - NO_COMPRESSION_SUPPORT("lz4"); + if (strcmp(compression, "lz4") == 0) + NO_COMPRESSION_SUPPORT("lz4"); #endif - return TOAST_LZ4_COMPRESSION; + return toast_compression_registry[i].method; + } } return InvalidCompressionMethod; } /* - * GetCompressionMethodName - Get compression method name + * GetCompressionMethodName + * Get compression method name, based on a compression method char. */ const char * GetCompressionMethodName(char method) { - switch (method) + for (int i = 0; i < TOAST_NUM_COMPRESSIONS; i++) { - case TOAST_PGLZ_COMPRESSION: - return "pglz"; - case TOAST_LZ4_COMPRESSION: - return "lz4"; - default: - elog(ERROR, "invalid compression method %c", method); - return NULL; /* keep compiler quiet */ + if (toast_compression_registry[i].method == method) + return toast_compression_registry[i].name; } + + elog(ERROR, "invalid compression method %c", method); + return NULL; /* keep compiler quiet */ +} + +/* + * ToastCompressionGucToMethod + * + * Translate a GUC value to a compression method char. + */ +char +ToastCompressionGucToMethod(ToastCompressionGucValue guc_value) +{ + for (int i = 0; i < TOAST_NUM_COMPRESSIONS; i++) + { + if (toast_compression_registry[i].guc_value == guc_value) + return toast_compression_registry[i].method; + } + + elog(ERROR, "invalid compression GUC value %d", guc_value); + return InvalidCompressionMethod; /* keep compiler quiet */ +} + +/* + * MethodToCompressionId + * + * Translate a catalog compression method char to the corresponding on-disk + * compression ID. + */ +uint32 +MethodToCompressionId(char method) +{ + for (int i = 0; i < TOAST_NUM_COMPRESSIONS; i++) + { + if (toast_compression_registry[i].method == method) + return toast_compression_registry[i].cmid; + } + + elog(ERROR, "invalid compression method %c", method); + return TOAST_COMPRESS_INVALID; /* keep compiler quiet */ +} + +/* + * CompressionIdToMethod + * + * Translate an on-disk compression ID to the corresponding catalog + * compression method char. + */ +char +CompressionIdToMethod(uint32 cmid) +{ + for (int i = 0; i < TOAST_NUM_COMPRESSIONS; i++) + { + if (toast_compression_registry[i].cmid == cmid) + return toast_compression_registry[i].method; + } + + elog(ERROR, "invalid compression method id %d", cmid); + return InvalidCompressionMethod; /* keep compiler quiet */ +} + +/* + * CompressionIdIsValid + * + * Check whether a compression ID is registered. Returns true if the + * ID corresponds to a known compression method, false otherwise. + */ +bool +CompressionIdIsValid(uint32 cmid) +{ + for (int i = 0; i < TOAST_NUM_COMPRESSIONS; i++) + { + if (toast_compression_registry[i].cmid == cmid) + return true; + } + + return false; } diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c index 4d0da07135e8..0731041d1ad3 100644 --- a/src/backend/access/common/toast_internals.c +++ b/src/backend/access/common/toast_internals.c @@ -47,7 +47,7 @@ toast_compress_datum(Datum value, char cmethod) { varlena *tmp = NULL; int32 valsize; - ToastCompressionId cmid = TOAST_INVALID_COMPRESSION_ID; + uint32 cmid = TOAST_COMPRESS_INVALID; Assert(!VARATT_IS_EXTERNAL(DatumGetPointer(value))); Assert(!VARATT_IS_COMPRESSED(DatumGetPointer(value))); @@ -56,20 +56,21 @@ toast_compress_datum(Datum value, char cmethod) /* If the compression method is not valid, use the current default */ if (!CompressionMethodIsValid(cmethod)) - cmethod = default_toast_compression; + cmethod = ToastCompressionGucToMethod(default_toast_compression); /* - * Call appropriate compression routine for the compression method. + * Translate the compression method char to the on-disk compression ID + * via the Method Registry, then dispatch to the appropriate compression + * routine. */ + cmid = MethodToCompressionId(cmethod); switch (cmethod) { case TOAST_PGLZ_COMPRESSION: tmp = pglz_compress_datum((const varlena *) DatumGetPointer(value)); - cmid = TOAST_PGLZ_COMPRESSION_ID; break; case TOAST_LZ4_COMPRESSION: tmp = lz4_compress_datum((const varlena *) DatumGetPointer(value)); - cmid = TOAST_LZ4_COMPRESSION_ID; break; default: elog(ERROR, "invalid compression method %c", cmethod); @@ -91,7 +92,7 @@ toast_compress_datum(Datum value, char cmethod) if (VARSIZE(tmp) < valsize - 2) { /* successful compression */ - Assert(cmid != TOAST_INVALID_COMPRESSION_ID); + Assert(cmid != TOAST_COMPRESS_INVALID); TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(tmp, valsize, cmid); return PointerGetDatum(tmp); } diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c index 7caf700fd619..5dcdcba05546 100644 --- a/src/backend/utils/adt/varlena.c +++ b/src/backend/utils/adt/varlena.c @@ -4194,7 +4194,7 @@ pg_column_compression(PG_FUNCTION_ARGS) { int typlen; char *result; - ToastCompressionId cmid; + uint32 cmid; /* On first call, get the input type's typlen, and save at *fn_extra */ if (fcinfo->flinfo->fn_extra == NULL) @@ -4219,16 +4219,16 @@ pg_column_compression(PG_FUNCTION_ARGS) /* get the compression method id stored in the compressed varlena */ cmid = toast_get_compression_id((varlena *) DatumGetPointer(PG_GETARG_DATUM(0))); - if (cmid == TOAST_INVALID_COMPRESSION_ID) + if (cmid == TOAST_COMPRESS_INVALID) PG_RETURN_NULL(); /* convert compression method id to compression method name */ switch (cmid) { - case TOAST_PGLZ_COMPRESSION_ID: + case TOAST_COMPRESS_PGLZ: result = "pglz"; break; - case TOAST_LZ4_COMPRESSION_ID: + case TOAST_COMPRESS_LZ4: result = "lz4"; break; default: diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c index 38aaf82f1209..a32a68b8bd5c 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -459,9 +459,9 @@ static const struct config_enum_entry shared_memory_options[] = { }; static const struct config_enum_entry default_toast_compression_options[] = { - {"pglz", TOAST_PGLZ_COMPRESSION, false}, + {"pglz", TOAST_PGLZ_COMPRESSION_GUC, false}, #ifdef USE_LZ4 - {"lz4", TOAST_LZ4_COMPRESSION, false}, + {"lz4", TOAST_LZ4_COMPRESSION_GUC, false}, #endif {NULL, 0, false} }; diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c index 31e19fbc6977..5aadf23b9404 100644 --- a/contrib/amcheck/verify_heapam.c +++ b/contrib/amcheck/verify_heapam.c @@ -1782,26 +1782,11 @@ check_tuple_attribute(HeapCheckContext *ctx) if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer)) { - ToastCompressionId cmid; - bool valid = false; + uint32 cmid; /* Compressed attributes should have a valid compression method */ cmid = TOAST_COMPRESS_METHOD(&toast_pointer); - switch (cmid) - { - /* List of all valid compression method IDs */ - case TOAST_PGLZ_COMPRESSION_ID: - case TOAST_LZ4_COMPRESSION_ID: - valid = true; - break; - - /* Recognized but invalid compression method ID */ - case TOAST_INVALID_COMPRESSION_ID: - break; - - /* Intentionally no default here */ - } - if (!valid) + if (!CompressionIdIsValid(cmid)) report_corruption(ctx, psprintf("toast value %u has invalid compression method id %d", toast_pointer.va_valueid, cmid)); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 3250564d4ff6..039f957d7f21 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3117,7 +3117,8 @@ TimestampTz TmFromChar TmToChar ToastAttrInfo -ToastCompressionId +ToastCompressionGucValue +ToastCompressionRegistryEntry ToastTupleContext ToastedAttribute TocEntry -- 2.53.0 Attachments: [text/plain] 0001-Refactor-code-logic-around-TOAST-compression.patch (18.8K, 2-0001-Refactor-code-logic-around-TOAST-compression.patch) download | inline diff: From 217200228cae79ee3b88d99ed5b64d395727edae Mon Sep 17 00:00:00 2001 From: Michael Paquier <[email protected]> Date: Mon, 9 Mar 2026 14:58:40 +0900 Subject: [PATCH] Refactor code logic around TOAST compression The TOAST compression code links the GUC values of default_toast_compression to the on-disk varatt attributes, limiting the number of elements in the enum GUC structure to 4 elements, to cover for the 2 bits that can be saved in va_tcinfo or va_extinfo, depending on if we are dealing with an inline compressible entry, or an external TOAST pointer. This commit refactors the TOAST code so as we have a clean split between three concepts: - The on-disk varatt values. - The GUC enum values. - The catalog attribute char values. - The name of the compression names All the knowledge of each method is now localized in a single "registry", with a set of routines that are able to retrieve some of the properties. The goal of this patch is to ease the addition of future methods, as we are going to need a split for the on-disk varatt properties across multiple vartags, while lifting the GUC enum limitation. --- src/include/access/toast_compression.h | 43 +++--- src/include/access/toast_internals.h | 4 +- src/include/varatt.h | 12 +- src/backend/access/common/detoast.c | 14 +- src/backend/access/common/toast_compression.c | 137 +++++++++++++++--- src/backend/access/common/toast_internals.c | 13 +- src/backend/utils/adt/varlena.c | 8 +- src/backend/utils/misc/guc_tables.c | 4 +- contrib/amcheck/verify_heapam.c | 19 +-- src/tools/pgindent/typedefs.list | 3 +- 10 files changed, 176 insertions(+), 81 deletions(-) diff --git a/src/include/access/toast_compression.h b/src/include/access/toast_compression.h index 3265f10b734f..fa852228235c 100644 --- a/src/include/access/toast_compression.h +++ b/src/include/access/toast_compression.h @@ -16,30 +16,22 @@ /* * 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. + * default_toast_compression is an integer for purposes of the GUC machinery. + * Its value is one of the ToastCompressionGucValue enum values defined below, + * which are independent from both the catalog char constants stored in + * pg_attribute.attcompression and the on-disk compression ID values + * (TOAST_COMPRESS_xxx from varatt.h). */ extern PGDLLIMPORT int default_toast_compression; /* - * Built-in compression method ID. The toast compression header will store - * this in the first 2 bits of the raw length. These built-in compression - * method IDs are directly mapped to the built-in compression methods. - * - * Don't use these values for anything other than understanding the meaning - * of the raw bits from a varlena; in particular, if the goal is to identify - * a compression method, use the constants TOAST_PGLZ_COMPRESSION, etc. - * below. We might someday support more than 4 compression methods, but - * we can never have more than 4 values in this enum, because there are - * only 2 bits available in the places where this is stored. + * Values for GUC default_toast_compression. */ -typedef enum ToastCompressionId +typedef enum ToastCompressionGucValue { - TOAST_PGLZ_COMPRESSION_ID = 0, - TOAST_LZ4_COMPRESSION_ID = 1, - TOAST_INVALID_COMPRESSION_ID = 2, -} ToastCompressionId; + TOAST_PGLZ_COMPRESSION_GUC = 0, + TOAST_LZ4_COMPRESSION_GUC = 1, +} ToastCompressionGucValue; /* * Built-in compression methods. pg_attribute will store these in the @@ -57,9 +49,9 @@ typedef enum ToastCompressionId * compiled-in, use it, otherwise use pglz. */ #ifdef USE_LZ4 -#define DEFAULT_TOAST_COMPRESSION TOAST_LZ4_COMPRESSION +#define DEFAULT_TOAST_COMPRESSION TOAST_LZ4_COMPRESSION_GUC #else -#define DEFAULT_TOAST_COMPRESSION TOAST_PGLZ_COMPRESSION +#define DEFAULT_TOAST_COMPRESSION TOAST_PGLZ_COMPRESSION_GUC #endif /* pglz compression/decompression routines */ @@ -75,8 +67,17 @@ extern varlena *lz4_decompress_datum_slice(const varlena *value, int32 slicelength); /* other stuff */ -extern ToastCompressionId toast_get_compression_id(varlena *attr); +extern uint32 toast_get_compression_id(varlena *attr); extern char CompressionNameToMethod(const char *compression); extern const char *GetCompressionMethodName(char method); +/* + * Method Registry translation functions. "cmid" is the on-disk varatt + * value. + */ +extern char ToastCompressionGucToMethod(ToastCompressionGucValue guc_value); +extern uint32 MethodToCompressionId(char method); +extern char CompressionIdToMethod(uint32 cmid); +extern bool CompressionIdIsValid(uint32 cmid); + #endif /* TOAST_COMPRESSION_H */ diff --git a/src/include/access/toast_internals.h b/src/include/access/toast_internals.h index d382db342620..36b3abf242ad 100644 --- a/src/include/access/toast_internals.h +++ b/src/include/access/toast_internals.h @@ -39,8 +39,8 @@ typedef struct toast_compress_header #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); \ + Assert((cm_method) == TOAST_COMPRESS_PGLZ || \ + (cm_method) == TOAST_COMPRESS_LZ4); \ ((toast_compress_header *) (ptr))->tcinfo = \ (len) | ((uint32) (cm_method) << VARLENA_EXTSIZE_BITS); \ } while (0) diff --git a/src/include/varatt.h b/src/include/varatt.h index 000bdf33b923..026b73e87277 100644 --- a/src/include/varatt.h +++ b/src/include/varatt.h @@ -45,6 +45,14 @@ typedef struct varatt_external #define VARLENA_EXTSIZE_BITS 30 #define VARLENA_EXTSIZE_MASK ((1U << VARLENA_EXTSIZE_BITS) - 1) +/* + * On-disk compression method IDs stored in the high bits of va_tcinfo + * and va_extinfo. Only 2 bits are available, so at most 4 values. + */ +#define TOAST_COMPRESS_PGLZ 0 +#define TOAST_COMPRESS_LZ4 1 +#define TOAST_COMPRESS_INVALID 2 + /* * varatt_indirect is a "TOAST pointer" representing an out-of-line * Datum that's stored in memory, not in an external toast relation. @@ -519,8 +527,8 @@ VARATT_EXTERNAL_GET_COMPRESS_METHOD(varatt_external toast_pointer) /* 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); \ + Assert((cm) == TOAST_COMPRESS_PGLZ || \ + (cm) == TOAST_COMPRESS_LZ4); \ ((toast_pointer).va_extinfo = \ (len) | ((uint32) (cm) << VARLENA_EXTSIZE_BITS)); \ } while (0) diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c index a6c1f3a734b2..20111d6b275d 100644 --- a/src/backend/access/common/detoast.c +++ b/src/backend/access/common/detoast.c @@ -252,7 +252,7 @@ detoast_attr_slice(varlena *attr, * able to decompress the required slice. */ if (VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) == - TOAST_PGLZ_COMPRESSION_ID) + TOAST_COMPRESS_PGLZ) max_size = pglz_maximum_compressed_size(slicelimit, max_size); /* @@ -470,7 +470,7 @@ toast_fetch_datum_slice(varlena *attr, int32 sliceoffset, static varlena * toast_decompress_datum(varlena *attr) { - ToastCompressionId cmid; + uint32 cmid; Assert(VARATT_IS_COMPRESSED(attr)); @@ -481,9 +481,9 @@ toast_decompress_datum(varlena *attr) cmid = TOAST_COMPRESS_METHOD(attr); switch (cmid) { - case TOAST_PGLZ_COMPRESSION_ID: + case TOAST_COMPRESS_PGLZ: return pglz_decompress_datum(attr); - case TOAST_LZ4_COMPRESSION_ID: + case TOAST_COMPRESS_LZ4: return lz4_decompress_datum(attr); default: elog(ERROR, "invalid compression method id %d", cmid); @@ -502,7 +502,7 @@ toast_decompress_datum(varlena *attr) static varlena * toast_decompress_datum_slice(varlena *attr, int32 slicelength) { - ToastCompressionId cmid; + uint32 cmid; Assert(VARATT_IS_COMPRESSED(attr)); @@ -524,9 +524,9 @@ toast_decompress_datum_slice(varlena *attr, int32 slicelength) cmid = TOAST_COMPRESS_METHOD(attr); switch (cmid) { - case TOAST_PGLZ_COMPRESSION_ID: + case TOAST_COMPRESS_PGLZ: return pglz_decompress_datum_slice(attr, slicelength); - case TOAST_LZ4_COMPRESSION_ID: + case TOAST_COMPRESS_LZ4: return lz4_decompress_datum_slice(attr, slicelength); default: elog(ERROR, "invalid compression method id %d", cmid); diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c index 5a5d579494a2..36f57f7ed404 100644 --- a/src/backend/access/common/toast_compression.c +++ b/src/backend/access/common/toast_compression.c @@ -31,6 +31,30 @@ int default_toast_compression = DEFAULT_TOAST_COMPRESSION; errmsg("compression method %s not supported", method), \ errdetail("This functionality requires the server to be built with %s support.", method))) +/* + * Compression Method Registry for TOAST. + * + * Maps between the three compression namespaces: human-readable names, + * catalog char constants (pg_attribute.attcompression), on-disk + * compression ID values (TOAST_COMPRESS_xxx from varatt.h), and GUC enum + * values. + */ +typedef struct ToastCompressionRegistryEntry +{ + const char *name; /* human-readable name, e.g. "pglz" */ + char method; /* catalog char, e.g. TOAST_PGLZ_COMPRESSION */ + uint32 cmid; /* on-disk ID, e.g. TOAST_COMPRESS_PGLZ */ + ToastCompressionGucValue guc_value; /* GUC enum int, e.g. + * TOAST_PGLZ_COMPRESSION_GUC */ +} ToastCompressionRegistryEntry; + +static const ToastCompressionRegistryEntry toast_compression_registry[] = { + {"pglz", TOAST_PGLZ_COMPRESSION, TOAST_COMPRESS_PGLZ, TOAST_PGLZ_COMPRESSION_GUC}, + {"lz4", TOAST_LZ4_COMPRESSION, TOAST_COMPRESS_LZ4, TOAST_LZ4_COMPRESSION_GUC}, +}; + +#define TOAST_NUM_COMPRESSIONS lengthof(toast_compression_registry) + /* * Compress a varlena using PGLZ. * @@ -248,12 +272,12 @@ lz4_decompress_datum_slice(const varlena *value, int32 slicelength) /* * Extract compression ID from a varlena. * - * Returns TOAST_INVALID_COMPRESSION_ID if the varlena is not compressed. + * Returns TOAST_COMPRESS_INVALID if the varlena is not compressed. */ -ToastCompressionId +uint32 toast_get_compression_id(varlena *attr) { - ToastCompressionId cmid = TOAST_INVALID_COMPRESSION_ID; + uint32 cmid = TOAST_COMPRESS_INVALID; /* * If it is stored externally then fetch the compression method id from @@ -278,39 +302,114 @@ toast_get_compression_id(varlena *attr) /* * CompressionNameToMethod - Get compression method from compression name * - * Search in the available built-in methods. If the compression not found - * in the built-in methods then return InvalidCompressionMethod. + * Search the compression registry by name. If the compression is not found + * then return InvalidCompressionMethod. */ char CompressionNameToMethod(const char *compression) { - if (strcmp(compression, "pglz") == 0) - return TOAST_PGLZ_COMPRESSION; - else if (strcmp(compression, "lz4") == 0) + for (int i = 0; i < TOAST_NUM_COMPRESSIONS; i++) { + if (strcmp(compression, toast_compression_registry[i].name) == 0) + { #ifndef USE_LZ4 - NO_COMPRESSION_SUPPORT("lz4"); + if (strcmp(compression, "lz4") == 0) + NO_COMPRESSION_SUPPORT("lz4"); #endif - return TOAST_LZ4_COMPRESSION; + return toast_compression_registry[i].method; + } } return InvalidCompressionMethod; } /* - * GetCompressionMethodName - Get compression method name + * GetCompressionMethodName + * Get compression method name, based on a compression method char. */ const char * GetCompressionMethodName(char method) { - switch (method) + for (int i = 0; i < TOAST_NUM_COMPRESSIONS; i++) { - case TOAST_PGLZ_COMPRESSION: - return "pglz"; - case TOAST_LZ4_COMPRESSION: - return "lz4"; - default: - elog(ERROR, "invalid compression method %c", method); - return NULL; /* keep compiler quiet */ + if (toast_compression_registry[i].method == method) + return toast_compression_registry[i].name; } + + elog(ERROR, "invalid compression method %c", method); + return NULL; /* keep compiler quiet */ +} + +/* + * ToastCompressionGucToMethod + * + * Translate a GUC value to a compression method char. + */ +char +ToastCompressionGucToMethod(ToastCompressionGucValue guc_value) +{ + for (int i = 0; i < TOAST_NUM_COMPRESSIONS; i++) + { + if (toast_compression_registry[i].guc_value == guc_value) + return toast_compression_registry[i].method; + } + + elog(ERROR, "invalid compression GUC value %d", guc_value); + return InvalidCompressionMethod; /* keep compiler quiet */ +} + +/* + * MethodToCompressionId + * + * Translate a catalog compression method char to the corresponding on-disk + * compression ID. + */ +uint32 +MethodToCompressionId(char method) +{ + for (int i = 0; i < TOAST_NUM_COMPRESSIONS; i++) + { + if (toast_compression_registry[i].method == method) + return toast_compression_registry[i].cmid; + } + + elog(ERROR, "invalid compression method %c", method); + return TOAST_COMPRESS_INVALID; /* keep compiler quiet */ +} + +/* + * CompressionIdToMethod + * + * Translate an on-disk compression ID to the corresponding catalog + * compression method char. + */ +char +CompressionIdToMethod(uint32 cmid) +{ + for (int i = 0; i < TOAST_NUM_COMPRESSIONS; i++) + { + if (toast_compression_registry[i].cmid == cmid) + return toast_compression_registry[i].method; + } + + elog(ERROR, "invalid compression method id %d", cmid); + return InvalidCompressionMethod; /* keep compiler quiet */ +} + +/* + * CompressionIdIsValid + * + * Check whether a compression ID is registered. Returns true if the + * ID corresponds to a known compression method, false otherwise. + */ +bool +CompressionIdIsValid(uint32 cmid) +{ + for (int i = 0; i < TOAST_NUM_COMPRESSIONS; i++) + { + if (toast_compression_registry[i].cmid == cmid) + return true; + } + + return false; } diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c index 4d0da07135e8..0731041d1ad3 100644 --- a/src/backend/access/common/toast_internals.c +++ b/src/backend/access/common/toast_internals.c @@ -47,7 +47,7 @@ toast_compress_datum(Datum value, char cmethod) { varlena *tmp = NULL; int32 valsize; - ToastCompressionId cmid = TOAST_INVALID_COMPRESSION_ID; + uint32 cmid = TOAST_COMPRESS_INVALID; Assert(!VARATT_IS_EXTERNAL(DatumGetPointer(value))); Assert(!VARATT_IS_COMPRESSED(DatumGetPointer(value))); @@ -56,20 +56,21 @@ toast_compress_datum(Datum value, char cmethod) /* If the compression method is not valid, use the current default */ if (!CompressionMethodIsValid(cmethod)) - cmethod = default_toast_compression; + cmethod = ToastCompressionGucToMethod(default_toast_compression); /* - * Call appropriate compression routine for the compression method. + * Translate the compression method char to the on-disk compression ID + * via the Method Registry, then dispatch to the appropriate compression + * routine. */ + cmid = MethodToCompressionId(cmethod); switch (cmethod) { case TOAST_PGLZ_COMPRESSION: tmp = pglz_compress_datum((const varlena *) DatumGetPointer(value)); - cmid = TOAST_PGLZ_COMPRESSION_ID; break; case TOAST_LZ4_COMPRESSION: tmp = lz4_compress_datum((const varlena *) DatumGetPointer(value)); - cmid = TOAST_LZ4_COMPRESSION_ID; break; default: elog(ERROR, "invalid compression method %c", cmethod); @@ -91,7 +92,7 @@ toast_compress_datum(Datum value, char cmethod) if (VARSIZE(tmp) < valsize - 2) { /* successful compression */ - Assert(cmid != TOAST_INVALID_COMPRESSION_ID); + Assert(cmid != TOAST_COMPRESS_INVALID); TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(tmp, valsize, cmid); return PointerGetDatum(tmp); } diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c index 7caf700fd619..5dcdcba05546 100644 --- a/src/backend/utils/adt/varlena.c +++ b/src/backend/utils/adt/varlena.c @@ -4194,7 +4194,7 @@ pg_column_compression(PG_FUNCTION_ARGS) { int typlen; char *result; - ToastCompressionId cmid; + uint32 cmid; /* On first call, get the input type's typlen, and save at *fn_extra */ if (fcinfo->flinfo->fn_extra == NULL) @@ -4219,16 +4219,16 @@ pg_column_compression(PG_FUNCTION_ARGS) /* get the compression method id stored in the compressed varlena */ cmid = toast_get_compression_id((varlena *) DatumGetPointer(PG_GETARG_DATUM(0))); - if (cmid == TOAST_INVALID_COMPRESSION_ID) + if (cmid == TOAST_COMPRESS_INVALID) PG_RETURN_NULL(); /* convert compression method id to compression method name */ switch (cmid) { - case TOAST_PGLZ_COMPRESSION_ID: + case TOAST_COMPRESS_PGLZ: result = "pglz"; break; - case TOAST_LZ4_COMPRESSION_ID: + case TOAST_COMPRESS_LZ4: result = "lz4"; break; default: diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c index 38aaf82f1209..a32a68b8bd5c 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -459,9 +459,9 @@ static const struct config_enum_entry shared_memory_options[] = { }; static const struct config_enum_entry default_toast_compression_options[] = { - {"pglz", TOAST_PGLZ_COMPRESSION, false}, + {"pglz", TOAST_PGLZ_COMPRESSION_GUC, false}, #ifdef USE_LZ4 - {"lz4", TOAST_LZ4_COMPRESSION, false}, + {"lz4", TOAST_LZ4_COMPRESSION_GUC, false}, #endif {NULL, 0, false} }; diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c index 31e19fbc6977..5aadf23b9404 100644 --- a/contrib/amcheck/verify_heapam.c +++ b/contrib/amcheck/verify_heapam.c @@ -1782,26 +1782,11 @@ check_tuple_attribute(HeapCheckContext *ctx) if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer)) { - ToastCompressionId cmid; - bool valid = false; + uint32 cmid; /* Compressed attributes should have a valid compression method */ cmid = TOAST_COMPRESS_METHOD(&toast_pointer); - switch (cmid) - { - /* List of all valid compression method IDs */ - case TOAST_PGLZ_COMPRESSION_ID: - case TOAST_LZ4_COMPRESSION_ID: - valid = true; - break; - - /* Recognized but invalid compression method ID */ - case TOAST_INVALID_COMPRESSION_ID: - break; - - /* Intentionally no default here */ - } - if (!valid) + if (!CompressionIdIsValid(cmid)) report_corruption(ctx, psprintf("toast value %u has invalid compression method id %d", toast_pointer.va_valueid, cmid)); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 3250564d4ff6..039f957d7f21 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3117,7 +3117,8 @@ TimestampTz TmFromChar TmToChar ToastAttrInfo -ToastCompressionId +ToastCompressionGucValue +ToastCompressionRegistryEntry ToastTupleContext ToastedAttribute TocEntry -- 2.53.0 [application/pgp-signature] signature.asc (833B, 3-signature.asc) download ^ permalink raw reply [nested|flat] 19+ messages in thread
end of thread, other threads:[~2026-03-09 06:02 UTC | newest] Thread overview: 19+ messages (download: mbox.gz follow: Atom feed) -- links below jump to the message on this page -- 2025-12-13 17:31 [PATCH] Add zstd compression for TOAST using extended header format Dharin Shah <[email protected]> 2025-12-13 19:32 ` Dharin Shah <[email protected]> 2025-12-15 19:16 ` Dharin Shah <[email protected]> 2025-12-16 05:56 ` Murtuza Zabuawala <[email protected]> 2025-12-16 10:49 ` Dharin Shah <[email protected]> 2025-12-16 10:51 ` Dharin Shah <[email protected]> 2025-12-17 15:11 ` Peter Eisentraut <[email protected]> 2025-12-18 06:35 ` Michael Paquier <[email protected]> 2025-12-18 21:44 ` Dharin Shah <[email protected]> 2025-12-18 22:44 ` Michael Paquier <[email protected]> 2025-12-24 00:47 ` Dharin Shah <[email protected]> 2025-12-24 16:50 ` Robert Treat <[email protected]> 2025-12-25 00:24 ` Michael Paquier <[email protected]> 2025-12-25 00:54 ` Dharin Shah <[email protected]> 2025-12-29 13:45 ` Dharin Shah <[email protected]> 2025-12-29 23:45 ` Michael Paquier <[email protected]> 2025-12-31 15:02 ` Dharin Shah <[email protected]> 2026-01-20 06:45 ` Michael Paquier <[email protected]> 2026-03-09 06:02 ` Michael Paquier <[email protected]>
This inbox is served by agora; see mirroring instructions for how to clone and mirror all data and code used for this inbox