Hi Robert > But I don't quite understand the point of this > response: it seems like you're just restating what the design does > without really justifying it. The question here isn't whether a 3-byte > header can describe a length up to 16MB; I think we all know our > powers of two well enough to agree on the answer to that question. The > question is whether it's a good use of 3 bytes, and I don't think it > is.
My initial decision to include a 3‑byte length field was driven by two goals: 1. Avoid introducing separate callbacks for each algorithm. 2. Provide a single, algorithm-agnostic mechanism for handling metadata length. After re-evaluating based on your feedback, I agree that the fixed overhead of a 3-byte length field outweighs its benefit; per-algorithm callbacks deliver the same functionality while saving three bytes per datum. > I did consider the fact that future compression algorithms might want > to use variable-length headers; but I couldn't see a reason why we > shouldn't let each of those compression algorithms decide for > themselves how to encode whatever information they need. If a > compression algorithm needs a variable-length header, then it just > needs to make that header self-describing. Worst case scenario, it can > make the first byte of that variable-length header a length byte, and > then go from there; but it's probably possible to be even smarter and > use less than a full byte. Say for example we store a dictionary ID > that in concept is a 32-bit quantity but we use a variable-length > integer representation for it. It's easy to see that we shouldn't ever > need more than 3 bits for that so a full length byte is overkill and, > in fact, would undermine the value of a variable-length representation > rather severely. (I suspect it's a bad idea anyway, but it's a worse > idea if you burn a full byte on a length header.) > I agree. Each compression algorithm can decide its own metadata size overhead. Callbacks can provide this information as well rather than storing in fixed length bytes(3 bytes). The revised patch introduces a "toast_cmpid_meta_size(const varatt_cmp_extended *hdr)", which calculates the metadata size. > But there's an even larger question here too, which is why we're > having some kind of discussion about generalized metadata when the > current project seemingly only requires a 4-byte dictionary OID. If > you have some other use of this space in mind, I don't think you've > told us what it is. If you don't, then I'm not sure why we're > designing around an up-to-16MB variable-length quantity when what we > have before us is a 4-byte fixed-length quantity. This project only requires 4 bytes of fixed-size metadata to store the dictionary ID. Updated design for extending varattrib_4b compression 1. extensible header /* * varatt_cmp_extended: an optional per‐datum header for extended compression method. * Only used when va_tcinfo's top two bits are "11". */ typedef struct varatt_cmp_extended { uint8 cmp_alg; char cmp_meta[FLEXIBLE_ARRAY_MEMBER]; /* algorithm‐specific metadata */ } varatt_cmp_extended; 2. Algorithm registry and metadata size dispatch static inline uint32 unsupported_meta_size(const varatt_cmp_extended *hdr) { elog(ERROR, "toast_cmpid_meta_size called for unsupported compression algorithm"); return 0; /* unreachable */ } /* no metadata for plain-ZSTD */ static inline uint32 zstd_nodict_meta_size(const varatt_cmp_extended *hdr) { return 0; } static inline uint32 zstd_dict_meta_size(const varatt_cmp_extended *hdr) { return sizeof(Oid); } /* * TOAST compression methods enumeration. * * NAME : algorithm identifier * VALUE : enum value * META-SIZE-FN : Calculates algorithm metadata size. */ #define TOAST_COMPRESSION_LIST \ X(PGLZ, 0, unsupported_meta_size) \ X(LZ4, 1, unsupported_meta_size) \ X(ZSTD_NODICT, 2, zstd_nodict_meta_size) \ X(ZSTD_DICT, 3, zstd_dict_meta_size) \ X(INVALID, 4, unsupported_meta_size) /* sentinel */ /* Compression algorithm identifiers */ typedef enum ToastCompressionId { #define X(name,val,fn) TOAST_##name##_COMPRESSION_ID = (val), TOAST_COMPRESSION_LIST #undef X } ToastCompressionId; /* lookup table to check if compression method uses extended format */ static const bool toast_cmpid_extended[] = { #define X(name,val,fn) \ /* PGLZ, LZ4 don't use extended format */ \ [TOAST_##name##_COMPRESSION_ID] = \ ((val) != TOAST_PGLZ_COMPRESSION_ID && \ (val) != TOAST_LZ4_COMPRESSION_ID && \ (val) != TOAST_INVALID_COMPRESSION_ID), TOAST_COMPRESSION_LIST #undef X }; #define TOAST_CMPID_EXTENDED(alg) (toast_cmpid_extended[alg]) /* * Prototype for a per-datum metadata-size callback: * given a pointer to the extended header, return * how many metadata bytes follow it. */ typedef uint32 (*ToastMetaSizeFn) (const varatt_cmp_extended *hdr); /* Callback table—indexed by ToastCompressionId */ static const ToastMetaSizeFn toast_meta_size_fns[] = { #define X(name,val,fn) [TOAST_##name##_COMPRESSION_ID] = fn, TOAST_COMPRESSION_LIST #undef X }; /* Calculates algorithm metadata size */ static inline uint32 toast_cmpid_meta_size(const varatt_cmp_extended *hdr) { Assert(hdr != NULL); return toast_meta_size_fns[hdr->cmp_alg] (hdr); } Each compression algorithm provides a static callback that returns the size of its metadata, given a pointer to the varatt_cmp_extended header. Algorithms with fixed-size metadata return a constant, while algorithms with variable-length metadata are responsible for defining and parsing their own internal headers to compute the metadata size. 3. Resulting on-disk layouts for zstd ZSTD (nodict) — datum on‑disk layout +----------------------------------+ | va_header (uint32) | +----------------------------------+ | va_tcinfo (uint32) | ← top two bits = 11 (extended) +----------------------------------+ | cmp_alg (uint8) | ← (ZSTD_NODICT) +----------------------------------+ | compressed bytes … | ← ZSTD frame +----------------------------------+ ZSTD(dict) — datum on‑disk layout +----------------------------------+ | va_header (uint32) | +----------------------------------+ | va_tcinfo (uint32) | ← top two bits = 11 (extended) +----------------------------------+ | cmp_alg (uint8) | ← (ZSTD_DICT) +----------------------------------+ | dict_id (uint32) | ← dictionary OID +----------------------------------+ | compressed bytes … | ← ZSTD frame +----------------------------------+ I hope this updated design addresses your concerns. I would appreciate any further feedback you may have. Thanks again for your guidance—it's been very helpful. v20-0001-varattrib_4b-design-proposal-to-make-it-extended.patch: varattrib_4b extensibility – adds varatt_cmp_extended, metadata size dispatch and useful macros; behaviour unchanged. v20-0002-zstd-nodict-compression.patch: Plain ZSTD (non dict) support. -- Nikhil Veldanda
From dc6172c6945354634063b03215dc6798ae22cc2f Mon Sep 17 00:00:00 2001 From: Nikhil Kumar Veldanda <nikhilkv@amazon.com> Date: Sat, 3 May 2025 02:14:02 +0000 Subject: [PATCH v20 2/2] zstd nodict compression --- contrib/amcheck/verify_heapam.c | 1 + src/backend/access/common/detoast.c | 12 +- src/backend/access/common/reloptions.c | 14 +- src/backend/access/common/toast_compression.c | 171 +++++++++++++++++- src/backend/access/common/toast_internals.c | 4 + src/backend/utils/adt/varlena.c | 3 + src/backend/utils/misc/guc_tables.c | 3 + src/backend/utils/misc/postgresql.conf.sample | 2 +- src/bin/psql/describe.c | 5 +- src/bin/psql/tab-complete.in.c | 4 +- src/include/access/toast_compression.h | 36 +++- src/include/access/toast_internals.h | 3 +- src/include/utils/attoptcache.h | 1 + src/test/regress/expected/compression.out | 5 +- src/test/regress/expected/compression_1.out | 3 + .../expected/compression_zstd_nodict.out | 152 ++++++++++++++++ .../expected/compression_zstd_nodict_1.out | 103 +++++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/compression.sql | 1 + .../regress/sql/compression_zstd_nodict.sql | 82 +++++++++ 20 files changed, 581 insertions(+), 26 deletions(-) create mode 100644 src/test/regress/expected/compression_zstd_nodict.out create mode 100644 src/test/regress/expected/compression_zstd_nodict_1.out create mode 100644 src/test/regress/sql/compression_zstd_nodict.sql diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c index d7c2ac6951a..111bb308341 100644 --- a/contrib/amcheck/verify_heapam.c +++ b/contrib/amcheck/verify_heapam.c @@ -1792,6 +1792,7 @@ check_tuple_attribute(HeapCheckContext *ctx) /* List of all valid compression method IDs */ case TOAST_PGLZ_COMPRESSION_ID: case TOAST_LZ4_COMPRESSION_ID: + case TOAST_ZSTD_NODICT_COMPRESSION_ID: valid = true; break; diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c index 01419d1c65f..451230023ec 100644 --- a/src/backend/access/common/detoast.c +++ b/src/backend/access/common/detoast.c @@ -246,10 +246,10 @@ detoast_attr_slice(struct varlena *attr, * Determine maximum amount of compressed data needed for a prefix * of a given length (after decompression). * - * At least for now, if it's LZ4 data, we'll have to fetch the - * whole thing, because there doesn't seem to be an API call to - * determine how much compressed data we need to be sure of being - * able to decompress the required slice. + * At least for now, if it's LZ4 or Zstandard data, we'll have to + * fetch the whole thing, because there doesn't seem to be an API + * call to determine how much compressed data we need to be sure + * of being able to decompress the required slice. */ if (VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) == TOAST_PGLZ_COMPRESSION_ID) @@ -485,6 +485,8 @@ toast_decompress_datum(struct varlena *attr) return pglz_decompress_datum(attr); case TOAST_LZ4_COMPRESSION_ID: return lz4_decompress_datum(attr); + case TOAST_ZSTD_NODICT_COMPRESSION_ID: + return zstd_nodict_decompress_datum(attr); default: elog(ERROR, "invalid compression method id %d", cmid); return NULL; /* keep compiler quiet */ @@ -528,6 +530,8 @@ toast_decompress_datum_slice(struct varlena *attr, int32 slicelength) return pglz_decompress_datum_slice(attr, slicelength); case TOAST_LZ4_COMPRESSION_ID: return lz4_decompress_datum_slice(attr, slicelength); + case TOAST_ZSTD_NODICT_COMPRESSION_ID: + return zstd_nodict_decompress_datum_slice(attr, slicelength); default: elog(ERROR, "invalid compression method id %d", cmid); return NULL; /* keep compiler quiet */ diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c index 46c1dce222d..1267668a242 100644 --- a/src/backend/access/common/reloptions.c +++ b/src/backend/access/common/reloptions.c @@ -24,6 +24,7 @@ #include "access/nbtree.h" #include "access/reloptions.h" #include "access/spgist_private.h" +#include "access/toast_compression.h" #include "catalog/pg_type.h" #include "commands/defrem.h" #include "commands/tablespace.h" @@ -381,7 +382,15 @@ static relopt_int intRelOpts[] = }, -1, 0, 1024 }, - + { + { + "zstd_level", + "Set column's ZSTD compression level", + RELOPT_KIND_ATTRIBUTE, + ShareUpdateExclusiveLock + }, + DEFAULT_ZSTD_LEVEL, MIN_ZSTD_LEVEL, MAX_ZSTD_LEVEL + }, /* list terminator */ {{NULL}} }; @@ -2097,7 +2106,8 @@ attribute_reloptions(Datum reloptions, bool validate) { static const relopt_parse_elt tab[] = { {"n_distinct", RELOPT_TYPE_REAL, offsetof(AttributeOpts, n_distinct)}, - {"n_distinct_inherited", RELOPT_TYPE_REAL, offsetof(AttributeOpts, n_distinct_inherited)} + {"n_distinct_inherited", RELOPT_TYPE_REAL, offsetof(AttributeOpts, n_distinct_inherited)}, + {"zstd_level", RELOPT_TYPE_INT, offsetof(AttributeOpts, zstd_level)}, }; return (bytea *) build_reloptions(reloptions, validate, diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c index 5e5d42d80ef..02823d1c435 100644 --- a/src/backend/access/common/toast_compression.c +++ b/src/backend/access/common/toast_compression.c @@ -17,6 +17,10 @@ #include <lz4.h> #endif +#ifdef USE_ZSTD +#include <zstd.h> +#endif + #include "access/detoast.h" #include "access/toast_compression.h" #include "common/pg_lzcompress.h" @@ -26,11 +30,19 @@ /* GUC */ int default_toast_compression = TOAST_PGLZ_COMPRESSION; -#define NO_LZ4_SUPPORT() \ +#ifdef USE_ZSTD +#define ZSTD_CHECK_ERROR(zstd_ret, msg) \ + do { \ + if (ZSTD_isError(zstd_ret)) \ + ereport(ERROR, (errmsg("%s: %s", (msg), ZSTD_getErrorName(zstd_ret)))); \ + } while (0) +#endif + +#define COMPRESSION_METHOD_NOT_SUPPORTED(method) \ ereport(ERROR, \ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), \ - errmsg("compression method lz4 not supported"), \ - errdetail("This functionality requires the server to be built with lz4 support."))) + errmsg("compression method %s not supported", method), \ + errdetail("This functionality requires the server to be built with %s support.", method))) /* * Compress a varlena using PGLZ. @@ -140,7 +152,7 @@ struct varlena * lz4_compress_datum(const struct varlena *value) { #ifndef USE_LZ4 - NO_LZ4_SUPPORT(); + COMPRESSION_METHOD_NOT_SUPPORTED("lz4"); return NULL; /* keep compiler quiet */ #else int32 valsize; @@ -183,7 +195,7 @@ struct varlena * lz4_decompress_datum(const struct varlena *value) { #ifndef USE_LZ4 - NO_LZ4_SUPPORT(); + COMPRESSION_METHOD_NOT_SUPPORTED("lz4"); return NULL; /* keep compiler quiet */ #else int32 rawsize; @@ -216,7 +228,7 @@ struct varlena * lz4_decompress_datum_slice(const struct varlena *value, int32 slicelength) { #ifndef USE_LZ4 - NO_LZ4_SUPPORT(); + COMPRESSION_METHOD_NOT_SUPPORTED("lz4"); return NULL; /* keep compiler quiet */ #else int32 rawsize; @@ -246,6 +258,129 @@ lz4_decompress_datum_slice(const struct varlena *value, int32 slicelength) #endif } +/* Compress datum using ZSTD with optional dictionary (using cdict) */ +struct varlena * +zstd_nodict_compress_datum(const struct varlena *value, CompressionInfo cmp) +{ +#ifdef USE_ZSTD + uint32 valsize = VARSIZE_ANY_EXHDR(value); + size_t max_size = ZSTD_compressBound(valsize); + struct varlena *compressed; + void *dest; + size_t cmp_size; + + /* Allocate space for the compressed varlena (header + data) */ + compressed = (struct varlena *) palloc(max_size + VARATT_4BCE_HDRSZ(cmp.cmp_ext)); + dest = (char *) compressed + VARATT_4BCE_HDRSZ(cmp.cmp_ext); + + cmp_size = ZSTD_compress(dest, + max_size, + VARDATA_ANY(value), + valsize, + cmp.zstd_level); + + if (ZSTD_isError(cmp_size)) + { + pfree(compressed); + ZSTD_CHECK_ERROR(cmp_size, "ZSTD compression failed"); + } + + /* + * If compression did not reduce size, return NULL so that the + * uncompressed data is stored + */ + if (cmp_size > valsize) + { + pfree(compressed); + return NULL; + } + + /* Set the compressed size in the varlena header */ + SET_VARSIZE_COMPRESSED(compressed, cmp_size + VARATT_4BCE_HDRSZ(cmp.cmp_ext)); + return compressed; + +#else + COMPRESSION_METHOD_NOT_SUPPORTED("zstd_nodict"); + return NULL; +#endif +} + +/* Decompression routine */ +struct varlena * +zstd_nodict_decompress_datum(const struct varlena *value) +{ +#ifdef USE_ZSTD + uint32 actual_size_exhdr = VARDATA_COMPRESSED_GET_EXTSIZE(value); + uint32 cmp_size_exhdr = VARATT_4BCE_PAYLOAD_SIZE(value); + struct varlena *result; + size_t uncmp_size; + + /* Allocate space for the uncompressed data */ + result = (struct varlena *) palloc(actual_size_exhdr + VARHDRSZ); + + uncmp_size = ZSTD_decompress(VARDATA(result), + actual_size_exhdr, + VARATT_4BCE_PAYLOAD_PTR(value), + cmp_size_exhdr); + + if (ZSTD_isError(uncmp_size)) + { + pfree(result); + ZSTD_CHECK_ERROR(uncmp_size, "ZSTD decompression failed"); + } + + /* Set final size in the varlena header */ + SET_VARSIZE(result, uncmp_size + VARHDRSZ); + return result; + +#else + COMPRESSION_METHOD_NOT_SUPPORTED("zstd_nodict"); + return NULL; +#endif +} + +/* Decompress a slice of the datum using the streaming API and optional dictionary */ +struct varlena * +zstd_nodict_decompress_datum_slice(const struct varlena *value, int32 slicelength) +{ +#ifdef USE_ZSTD + struct varlena *result; + ZSTD_inBuffer inBuf; + ZSTD_outBuffer outBuf; + size_t ret; + ZSTD_DCtx *ZstdDecompressionCtx = ZSTD_createDCtx(); + + inBuf.src = VARATT_4BCE_PAYLOAD_PTR(value); + inBuf.size = VARATT_4BCE_PAYLOAD_SIZE(value); + inBuf.pos = 0; + + result = (struct varlena *) palloc(slicelength + VARHDRSZ); + outBuf.dst = (char *) result + VARHDRSZ; + outBuf.size = slicelength; + outBuf.pos = 0; + + /* Common decompression loop */ + while (inBuf.pos < inBuf.size && outBuf.pos < outBuf.size) + { + ret = ZSTD_decompressStream(ZstdDecompressionCtx, &outBuf, &inBuf); + if (ZSTD_isError(ret)) + { + pfree(result); + ZSTD_freeDCtx(ZstdDecompressionCtx); + ZSTD_CHECK_ERROR(ret, "zstd decompression failed"); + } + } + + Assert(outBuf.size == slicelength && outBuf.pos == slicelength); + SET_VARSIZE(result, outBuf.pos + VARHDRSZ); + ZSTD_freeDCtx(ZstdDecompressionCtx); + return result; +#else + COMPRESSION_METHOD_NOT_SUPPORTED("zstd_nodict"); + return NULL; +#endif +} + /* * Extract compression ID from a varlena. * @@ -293,10 +428,17 @@ CompressionNameToMethod(const char *compression) else if (strcmp(compression, "lz4") == 0) { #ifndef USE_LZ4 - NO_LZ4_SUPPORT(); + COMPRESSION_METHOD_NOT_SUPPORTED("lz4"); #endif return TOAST_LZ4_COMPRESSION; } + else if (strcmp(compression, "zstd_nodict") == 0) + { +#ifndef USE_ZSTD + COMPRESSION_METHOD_NOT_SUPPORTED("zstd_nodict"); +#endif + return TOAST_ZSTD_NODICT_COMPRESSION; + } return InvalidCompressionMethod; } @@ -313,6 +455,8 @@ GetCompressionMethodName(char method) return "pglz"; case TOAST_LZ4_COMPRESSION: return "lz4"; + case TOAST_ZSTD_NODICT_COMPRESSION: + return "zstd_nodict"; default: elog(ERROR, "invalid compression method %c", method); return NULL; /* keep compiler quiet */ @@ -326,6 +470,7 @@ setup_compression_info(char cmethod, Form_pg_attribute att) /* initialize from the attribute’s default settings */ info.cmethod = cmethod; + info.zstd_level = DEFAULT_ZSTD_LEVEL; info.cmp_ext = NULL; /* If the compression method is not valid, use the current default */ @@ -337,6 +482,18 @@ setup_compression_info(char cmethod, Form_pg_attribute att) case TOAST_PGLZ_COMPRESSION: case TOAST_LZ4_COMPRESSION: break; + case TOAST_ZSTD_NODICT_COMPRESSION: + { + AttributeOpts *aopt = get_attribute_options(att->attrelid, att->attnum); + + if (aopt != NULL) + info.zstd_level = aopt->zstd_level; + + info.cmp_ext = palloc(sizeof(varatt_cmp_extended)); + + VARATT_4BCE_SET_HDR(info.cmp_ext, TOAST_ZSTD_NODICT_COMPRESSION_ID); + } + break; default: elog(ERROR, "invalid compression method %c", info.cmethod); } diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c index 83b537d51bf..5521d78bb48 100644 --- a/src/backend/access/common/toast_internals.c +++ b/src/backend/access/common/toast_internals.c @@ -68,6 +68,10 @@ toast_compress_datum(Datum value, CompressionInfo cmp) tmp = lz4_compress_datum((const struct varlena *) value); cmid = TOAST_LZ4_COMPRESSION_ID; break; + case TOAST_ZSTD_NODICT_COMPRESSION: + tmp = zstd_nodict_compress_datum((const struct varlena *) value, cmp); + cmid = TOAST_ZSTD_NODICT_COMPRESSION_ID; + break; default: elog(ERROR, "invalid compression method %c", cmp.cmethod); } diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c index 3e4d5568bde..5b9151c7e16 100644 --- a/src/backend/utils/adt/varlena.c +++ b/src/backend/utils/adt/varlena.c @@ -5301,6 +5301,9 @@ pg_column_compression(PG_FUNCTION_ARGS) case TOAST_LZ4_COMPRESSION_ID: result = "lz4"; break; + case TOAST_ZSTD_NODICT_COMPRESSION_ID: + result = "zstd_nodict"; + break; default: elog(ERROR, "invalid compression method id %d", cmid); } diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c index 2f8cbd86759..948454e2093 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_nodict", TOAST_ZSTD_NODICT_COMPRESSION, false}, #endif {NULL, 0, false} }; diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index 34826d01380..f2d2ca39514 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -756,7 +756,7 @@ autovacuum_worker_slots = 16 # autovacuum worker slots to allocate #row_security = on #default_table_access_method = 'heap' #default_tablespace = '' # a tablespace name, '' uses the default -#default_toast_compression = 'pglz' # 'pglz' or 'lz4' +#default_toast_compression = 'pglz' # 'pglz' or 'lz4' or 'zstd_nodict' #temp_tablespaces = '' # a list of tablespace names, '' uses # only default tablespace #check_function_bodies = on diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index 1d08268393e..3831a7fab03 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -2171,8 +2171,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] == 'n' ? "zstd_nodict" : + (compression[0] == '\0' ? "" : + "???")))), false, false); } diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index c916b9299a8..2441acf41ce 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -2875,11 +2875,11 @@ match_previous_words(int pattern_id, /* ALTER TABLE ALTER [COLUMN] <foo> SET ( */ else if (Matches("ALTER", "TABLE", MatchAny, "ALTER", "COLUMN", MatchAny, "SET", "(") || Matches("ALTER", "TABLE", MatchAny, "ALTER", MatchAny, "SET", "(")) - COMPLETE_WITH("n_distinct", "n_distinct_inherited"); + COMPLETE_WITH("n_distinct", "n_distinct_inherited", "zstd_level"); /* ALTER TABLE ALTER [COLUMN] <foo> SET COMPRESSION */ else if (Matches("ALTER", "TABLE", MatchAny, "ALTER", "COLUMN", MatchAny, "SET", "COMPRESSION") || Matches("ALTER", "TABLE", MatchAny, "ALTER", MatchAny, "SET", "COMPRESSION")) - COMPLETE_WITH("DEFAULT", "PGLZ", "LZ4"); + COMPLETE_WITH("DEFAULT", "PGLZ", "LZ4", "ZSTD_NODICT"); /* ALTER TABLE ALTER [COLUMN] <foo> SET EXPRESSION */ else if (Matches("ALTER", "TABLE", MatchAny, "ALTER", "COLUMN", MatchAny, "SET", "EXPRESSION") || Matches("ALTER", "TABLE", MatchAny, "ALTER", MatchAny, "SET", "EXPRESSION")) diff --git a/src/include/access/toast_compression.h b/src/include/access/toast_compression.h index 1aef65cde99..94d56c7a4ef 100644 --- a/src/include/access/toast_compression.h +++ b/src/include/access/toast_compression.h @@ -16,6 +16,10 @@ #include "varatt.h" #include "catalog/pg_attribute.h" +#ifdef USE_ZSTD +#include <zstd.h> +#endif + /* * GUC support. * @@ -36,6 +40,13 @@ unsupported_meta_size(const varatt_cmp_extended *hdr) return 0; /* unreachable */ } +/* no metadata for plain-ZSTD */ +static inline uint32 +zstd_nodict_meta_size(const varatt_cmp_extended *hdr) +{ + return 0; +} + /* * TOAST compression methods enumeration. * @@ -43,10 +54,11 @@ unsupported_meta_size(const varatt_cmp_extended *hdr) * VALUE : enum value * META-SIZE-FN : Calculates algorithm metadata size. */ -#define TOAST_COMPRESSION_LIST \ - X(PGLZ, 0, unsupported_meta_size) \ - X(LZ4, 1, unsupported_meta_size) \ - X(INVALID, 2, unsupported_meta_size) /* sentinel */ +#define TOAST_COMPRESSION_LIST \ + X(PGLZ, 0, unsupported_meta_size) \ + X(LZ4, 1, unsupported_meta_size) \ + X(ZSTD_NODICT, 2, zstd_nodict_meta_size) \ + X(INVALID, 3, unsupported_meta_size) /* sentinel */ /* Compression algorithm identifiers */ typedef enum ToastCompressionId @@ -96,6 +108,7 @@ toast_cmpid_meta_size(const varatt_cmp_extended *hdr) typedef struct CompressionInfo { char cmethod; + int zstd_level; varatt_cmp_extended *cmp_ext; /* non-NULL only if uses extended * compression methods */ } CompressionInfo; @@ -107,10 +120,20 @@ typedef struct CompressionInfo */ #define TOAST_PGLZ_COMPRESSION 'p' #define TOAST_LZ4_COMPRESSION 'l' +#define TOAST_ZSTD_NODICT_COMPRESSION 'n' #define InvalidCompressionMethod '\0' #define CompressionMethodIsValid(cm) ((cm) != InvalidCompressionMethod) +#ifdef USE_ZSTD +#define DEFAULT_ZSTD_LEVEL ZSTD_CLEVEL_DEFAULT +#define MIN_ZSTD_LEVEL (int)-ZSTD_BLOCKSIZE_MAX +#define MAX_ZSTD_LEVEL 22 +#else +#define DEFAULT_ZSTD_LEVEL 0 +#define MIN_ZSTD_LEVEL 0 +#define MAX_ZSTD_LEVEL 0 +#endif /* pglz compression/decompression routines */ extern struct varlena *pglz_compress_datum(const struct varlena *value); @@ -124,6 +147,11 @@ extern struct varlena *lz4_decompress_datum(const struct varlena *value); extern struct varlena *lz4_decompress_datum_slice(const struct varlena *value, int32 slicelength); +/* zstd nodict compression/decompression routines */ +extern struct varlena *zstd_nodict_compress_datum(const struct varlena *value, CompressionInfo cmp); +extern struct varlena *zstd_nodict_decompress_datum(const struct varlena *value); +extern struct varlena *zstd_nodict_decompress_datum_slice(const struct varlena *value, int32 slicelength); + /* other stuff */ extern ToastCompressionId toast_get_compression_id(struct varlena *attr); extern char CompressionNameToMethod(const char *compression); diff --git a/src/include/access/toast_internals.h b/src/include/access/toast_internals.h index f4a4829ad17..b1859ef202b 100644 --- a/src/include/access/toast_internals.h +++ b/src/include/access/toast_internals.h @@ -35,7 +35,8 @@ typedef struct toast_compress_header 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_ZSTD_NODICT_COMPRESSION_ID); \ if (!TOAST_CMPID_EXTENDED((cm_method))) \ { \ ((toast_compress_header *)(ptr))->tcinfo = \ diff --git a/src/include/utils/attoptcache.h b/src/include/utils/attoptcache.h index f684a772af5..51d65ebd646 100644 --- a/src/include/utils/attoptcache.h +++ b/src/include/utils/attoptcache.h @@ -21,6 +21,7 @@ typedef struct AttributeOpts int32 vl_len_; /* varlena header (do not touch directly!) */ float8 n_distinct; float8 n_distinct_inherited; + int zstd_level; } AttributeOpts; extern AttributeOpts *get_attribute_options(Oid attrelid, int attnum); diff --git a/src/test/regress/expected/compression.out b/src/test/regress/expected/compression.out index 4dd9ee7200d..c7e108a0f52 100644 --- a/src/test/regress/expected/compression.out +++ b/src/test/regress/expected/compression.out @@ -238,10 +238,11 @@ NOTICE: merging multiple inherited definitions of column "f1" -- test default_toast_compression GUC SET default_toast_compression = ''; ERROR: invalid value for parameter "default_toast_compression": "" -HINT: Available values: pglz, lz4. +HINT: Available values: pglz, lz4, zstd_nodict. SET default_toast_compression = 'I do not exist compression'; ERROR: invalid value for parameter "default_toast_compression": "I do not exist compression" -HINT: Available values: pglz, lz4. +HINT: Available values: pglz, lz4, zstd_nodict. +SET default_toast_compression = 'zstd_nodict'; SET default_toast_compression = 'lz4'; SET default_toast_compression = 'pglz'; -- test alter compression method diff --git a/src/test/regress/expected/compression_1.out b/src/test/regress/expected/compression_1.out index 7bd7642b4b9..5b10d8c5259 100644 --- a/src/test/regress/expected/compression_1.out +++ b/src/test/regress/expected/compression_1.out @@ -233,6 +233,9 @@ HINT: Available values: pglz. SET default_toast_compression = 'I do not exist compression'; ERROR: invalid value for parameter "default_toast_compression": "I do not exist compression" HINT: Available values: pglz. +SET default_toast_compression = 'zstd_nodict'; +ERROR: invalid value for parameter "default_toast_compression": "zstd_nodict" +HINT: Available values: pglz. SET default_toast_compression = 'lz4'; ERROR: invalid value for parameter "default_toast_compression": "lz4" HINT: Available values: pglz. diff --git a/src/test/regress/expected/compression_zstd_nodict.out b/src/test/regress/expected/compression_zstd_nodict.out new file mode 100644 index 00000000000..d80814e0492 --- /dev/null +++ b/src/test/regress/expected/compression_zstd_nodict.out @@ -0,0 +1,152 @@ +\set HIDE_TOAST_COMPRESSION false +-- Ensure stable results regardless of the installation's default. +SET default_toast_compression = 'pglz'; +---------------------------------------------------------------- +-- 1. Create Test Table with Zstd Compression +---------------------------------------------------------------- +DROP TABLE IF EXISTS cmdata_zstd_nodict CASCADE; +NOTICE: table "cmdata_zstd_nodict" does not exist, skipping +CREATE TABLE cmdata_zstd_nodict ( + f1 TEXT COMPRESSION zstd_nodict +); +---------------------------------------------------------------- +-- 2. Insert Data Rows +---------------------------------------------------------------- +DO $$ +BEGIN + FOR i IN 1..15 LOOP + INSERT INTO cmdata_zstd_nodict (f1) VALUES (repeat('1234567890', 1004)); + END LOOP; +END $$; +---------------------------------------------------------------- +-- 3. Verify Table Structure and Compression Settings +---------------------------------------------------------------- +-- Table Structure for cmdata_zstd +\d+ cmdata_zstd_nodict; + Table "public.cmdata_zstd_nodict" + Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description +--------+------+-----------+----------+---------+----------+-------------+--------------+------------- + f1 | text | | | | extended | zstd_nodict | | + +-- Compression Settings for f1 Column +SELECT pg_column_compression(f1) AS compression_method, + count(*) AS row_count +FROM cmdata_zstd_nodict +GROUP BY pg_column_compression(f1); + compression_method | row_count +--------------------+----------- + zstd_nodict | 15 +(1 row) + +---------------------------------------------------------------- +-- 4. Decompression Tests +---------------------------------------------------------------- +-- Decompression Slice Test (Extracting Substrings) +SELECT SUBSTR(f1, 200, 50) AS data_slice +FROM cmdata_zstd_nodict; + data_slice +---------------------------------------------------- + 01234567890123456789012345678901234567890123456789 + 01234567890123456789012345678901234567890123456789 + 01234567890123456789012345678901234567890123456789 + 01234567890123456789012345678901234567890123456789 + 01234567890123456789012345678901234567890123456789 + 01234567890123456789012345678901234567890123456789 + 01234567890123456789012345678901234567890123456789 + 01234567890123456789012345678901234567890123456789 + 01234567890123456789012345678901234567890123456789 + 01234567890123456789012345678901234567890123456789 + 01234567890123456789012345678901234567890123456789 + 01234567890123456789012345678901234567890123456789 + 01234567890123456789012345678901234567890123456789 + 01234567890123456789012345678901234567890123456789 + 01234567890123456789012345678901234567890123456789 +(15 rows) + +---------------------------------------------------------------- +-- 5. Test Table Creation with LIKE INCLUDING COMPRESSION +---------------------------------------------------------------- +DROP TABLE IF EXISTS cmdata_zstd_nodict_2; +NOTICE: table "cmdata_zstd_nodict_2" does not exist, skipping +CREATE TABLE cmdata_zstd_nodict_2 (LIKE cmdata_zstd_nodict INCLUDING COMPRESSION); +-- Table Structure for cmdata_zstd_2 +\d+ cmdata_zstd_nodict_2; + Table "public.cmdata_zstd_nodict_2" + Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description +--------+------+-----------+----------+---------+----------+-------------+--------------+------------- + f1 | text | | | | extended | zstd_nodict | | + +DROP TABLE cmdata_zstd_nodict_2; +---------------------------------------------------------------- +-- 6. Materialized View Compression Test +---------------------------------------------------------------- +DROP MATERIALIZED VIEW IF EXISTS compressmv_zstd_nodict; +NOTICE: materialized view "compressmv_zstd_nodict" does not exist, skipping +CREATE MATERIALIZED VIEW compressmv_zstd_nodict AS + SELECT f1 FROM cmdata_zstd_nodict; +-- Materialized View Structure for compressmv_zstd +\d+ compressmv_zstd_nodict; + Materialized view "public.compressmv_zstd_nodict" + Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description +--------+------+-----------+----------+---------+----------+-------------+--------------+------------- + f1 | text | | | | extended | | | +View definition: + SELECT f1 + FROM cmdata_zstd_nodict; + +-- Materialized View Compression Check +SELECT pg_column_compression(f1) AS mv_compression +FROM compressmv_zstd_nodict; + mv_compression +---------------- + zstd_nodict + zstd_nodict + zstd_nodict + zstd_nodict + zstd_nodict + zstd_nodict + zstd_nodict + zstd_nodict + zstd_nodict + zstd_nodict + zstd_nodict + zstd_nodict + zstd_nodict + zstd_nodict + zstd_nodict +(15 rows) + +---------------------------------------------------------------- +-- 7. Additional Updates and Round-Trip Tests +---------------------------------------------------------------- +-- Update some rows to check if the dictionary remains effective after modifications. +UPDATE cmdata_zstd_nodict +SET f1 = f1 || ' UPDATED'; +-- Verification of Updated Rows +SELECT SUBSTR(f1, LENGTH(f1) - 7 + 1, 7) AS preview +FROM cmdata_zstd_nodict; + preview +--------- + UPDATED + UPDATED + UPDATED + UPDATED + UPDATED + UPDATED + UPDATED + UPDATED + UPDATED + UPDATED + UPDATED + UPDATED + UPDATED + UPDATED + UPDATED +(15 rows) + +---------------------------------------------------------------- +-- 8. Clean Up +---------------------------------------------------------------- +DROP MATERIALIZED VIEW compressmv_zstd_nodict; +DROP TABLE cmdata_zstd_nodict; +\set HIDE_TOAST_COMPRESSION true diff --git a/src/test/regress/expected/compression_zstd_nodict_1.out b/src/test/regress/expected/compression_zstd_nodict_1.out new file mode 100644 index 00000000000..161d11fcae2 --- /dev/null +++ b/src/test/regress/expected/compression_zstd_nodict_1.out @@ -0,0 +1,103 @@ +\set HIDE_TOAST_COMPRESSION false +-- Ensure stable results regardless of the installation's default. +SET default_toast_compression = 'pglz'; +---------------------------------------------------------------- +-- 1. Create Test Table with Zstd Compression +---------------------------------------------------------------- +DROP TABLE IF EXISTS cmdata_zstd_nodict CASCADE; +NOTICE: table "cmdata_zstd_nodict" does not exist, skipping +CREATE TABLE cmdata_zstd_nodict ( + f1 TEXT COMPRESSION zstd_nodict +); +ERROR: compression method zstd_nodict not supported +DETAIL: This functionality requires the server to be built with zstd_nodict support. +---------------------------------------------------------------- +-- 2. Insert Data Rows +---------------------------------------------------------------- +DO $$ +BEGIN + FOR i IN 1..15 LOOP + INSERT INTO cmdata_zstd_nodict (f1) VALUES (repeat('1234567890', 1004)); + END LOOP; +END $$; +ERROR: relation "cmdata_zstd_nodict" does not exist +LINE 1: INSERT INTO cmdata_zstd_nodict (f1) VALUES (repeat('12345678... + ^ +QUERY: INSERT INTO cmdata_zstd_nodict (f1) VALUES (repeat('1234567890', 1004)) +CONTEXT: PL/pgSQL function inline_code_block line 4 at SQL statement +---------------------------------------------------------------- +-- 3. Verify Table Structure and Compression Settings +---------------------------------------------------------------- +-- Table Structure for cmdata_zstd +\d+ cmdata_zstd_nodict; +-- Compression Settings for f1 Column +SELECT pg_column_compression(f1) AS compression_method, + count(*) AS row_count +FROM cmdata_zstd_nodict +GROUP BY pg_column_compression(f1); +ERROR: relation "cmdata_zstd_nodict" does not exist +LINE 3: FROM cmdata_zstd_nodict + ^ +---------------------------------------------------------------- +-- 4. Decompression Tests +---------------------------------------------------------------- +-- Decompression Slice Test (Extracting Substrings) +SELECT SUBSTR(f1, 200, 50) AS data_slice +FROM cmdata_zstd_nodict; +ERROR: relation "cmdata_zstd_nodict" does not exist +LINE 2: FROM cmdata_zstd_nodict; + ^ +---------------------------------------------------------------- +-- 5. Test Table Creation with LIKE INCLUDING COMPRESSION +---------------------------------------------------------------- +DROP TABLE IF EXISTS cmdata_zstd_nodict_2; +NOTICE: table "cmdata_zstd_nodict_2" does not exist, skipping +CREATE TABLE cmdata_zstd_nodict_2 (LIKE cmdata_zstd_nodict INCLUDING COMPRESSION); +ERROR: relation "cmdata_zstd_nodict" does not exist +LINE 1: CREATE TABLE cmdata_zstd_nodict_2 (LIKE cmdata_zstd_nodict I... + ^ +-- Table Structure for cmdata_zstd_2 +\d+ cmdata_zstd_nodict_2; +DROP TABLE cmdata_zstd_nodict_2; +ERROR: table "cmdata_zstd_nodict_2" does not exist +---------------------------------------------------------------- +-- 6. Materialized View Compression Test +---------------------------------------------------------------- +DROP MATERIALIZED VIEW IF EXISTS compressmv_zstd_nodict; +NOTICE: materialized view "compressmv_zstd_nodict" does not exist, skipping +CREATE MATERIALIZED VIEW compressmv_zstd_nodict AS + SELECT f1 FROM cmdata_zstd_nodict; +ERROR: relation "cmdata_zstd_nodict" does not exist +LINE 2: SELECT f1 FROM cmdata_zstd_nodict; + ^ +-- Materialized View Structure for compressmv_zstd +\d+ compressmv_zstd_nodict; +-- Materialized View Compression Check +SELECT pg_column_compression(f1) AS mv_compression +FROM compressmv_zstd_nodict; +ERROR: relation "compressmv_zstd_nodict" does not exist +LINE 2: FROM compressmv_zstd_nodict; + ^ +---------------------------------------------------------------- +-- 7. Additional Updates and Round-Trip Tests +---------------------------------------------------------------- +-- Update some rows to check if the dictionary remains effective after modifications. +UPDATE cmdata_zstd_nodict +SET f1 = f1 || ' UPDATED'; +ERROR: relation "cmdata_zstd_nodict" does not exist +LINE 1: UPDATE cmdata_zstd_nodict + ^ +-- Verification of Updated Rows +SELECT SUBSTR(f1, LENGTH(f1) - 7 + 1, 7) AS preview +FROM cmdata_zstd_nodict; +ERROR: relation "cmdata_zstd_nodict" does not exist +LINE 2: FROM cmdata_zstd_nodict; + ^ +---------------------------------------------------------------- +-- 8. Clean Up +---------------------------------------------------------------- +DROP MATERIALIZED VIEW compressmv_zstd_nodict; +ERROR: materialized view "compressmv_zstd_nodict" does not exist +DROP TABLE cmdata_zstd_nodict; +ERROR: table "cmdata_zstd_nodict" does not exist +\set HIDE_TOAST_COMPRESSION true diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index a424be2a6bf..75cea22c418 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -123,7 +123,7 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion tr # The stats test resets stats, so nothing else needing stats access can be in # this group. # ---------- -test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression memoize stats predicate numa +test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression compression_zstd_nodict memoize stats predicate numa # event_trigger depends on create_am and cannot run concurrently with # any test that runs DDL diff --git a/src/test/regress/sql/compression.sql b/src/test/regress/sql/compression.sql index 490595fcfb2..27979eb7997 100644 --- a/src/test/regress/sql/compression.sql +++ b/src/test/regress/sql/compression.sql @@ -102,6 +102,7 @@ CREATE TABLE cminh() INHERITS (cmdata, cmdata3); -- test default_toast_compression GUC SET default_toast_compression = ''; SET default_toast_compression = 'I do not exist compression'; +SET default_toast_compression = 'zstd_nodict'; SET default_toast_compression = 'lz4'; SET default_toast_compression = 'pglz'; diff --git a/src/test/regress/sql/compression_zstd_nodict.sql b/src/test/regress/sql/compression_zstd_nodict.sql new file mode 100644 index 00000000000..e1f18004cc9 --- /dev/null +++ b/src/test/regress/sql/compression_zstd_nodict.sql @@ -0,0 +1,82 @@ +\set HIDE_TOAST_COMPRESSION false + +-- Ensure stable results regardless of the installation's default. +SET default_toast_compression = 'pglz'; + +---------------------------------------------------------------- +-- 1. Create Test Table with Zstd Compression +---------------------------------------------------------------- +DROP TABLE IF EXISTS cmdata_zstd_nodict CASCADE; +CREATE TABLE cmdata_zstd_nodict ( + f1 TEXT COMPRESSION zstd_nodict +); + +---------------------------------------------------------------- +-- 2. Insert Data Rows +---------------------------------------------------------------- +DO $$ +BEGIN + FOR i IN 1..15 LOOP + INSERT INTO cmdata_zstd_nodict (f1) VALUES (repeat('1234567890', 1004)); + END LOOP; +END $$; + +---------------------------------------------------------------- +-- 3. Verify Table Structure and Compression Settings +---------------------------------------------------------------- +-- Table Structure for cmdata_zstd +\d+ cmdata_zstd_nodict; + +-- Compression Settings for f1 Column +SELECT pg_column_compression(f1) AS compression_method, + count(*) AS row_count +FROM cmdata_zstd_nodict +GROUP BY pg_column_compression(f1); + +---------------------------------------------------------------- +-- 4. Decompression Tests +---------------------------------------------------------------- +-- Decompression Slice Test (Extracting Substrings) +SELECT SUBSTR(f1, 200, 50) AS data_slice +FROM cmdata_zstd_nodict; + +---------------------------------------------------------------- +-- 5. Test Table Creation with LIKE INCLUDING COMPRESSION +---------------------------------------------------------------- +DROP TABLE IF EXISTS cmdata_zstd_nodict_2; +CREATE TABLE cmdata_zstd_nodict_2 (LIKE cmdata_zstd_nodict INCLUDING COMPRESSION); +-- Table Structure for cmdata_zstd_2 +\d+ cmdata_zstd_nodict_2; +DROP TABLE cmdata_zstd_nodict_2; + +---------------------------------------------------------------- +-- 6. Materialized View Compression Test +---------------------------------------------------------------- +DROP MATERIALIZED VIEW IF EXISTS compressmv_zstd_nodict; +CREATE MATERIALIZED VIEW compressmv_zstd_nodict AS + SELECT f1 FROM cmdata_zstd_nodict; + +-- Materialized View Structure for compressmv_zstd +\d+ compressmv_zstd_nodict; + +-- Materialized View Compression Check +SELECT pg_column_compression(f1) AS mv_compression +FROM compressmv_zstd_nodict; + +---------------------------------------------------------------- +-- 7. Additional Updates and Round-Trip Tests +---------------------------------------------------------------- +-- Update some rows to check if the dictionary remains effective after modifications. +UPDATE cmdata_zstd_nodict +SET f1 = f1 || ' UPDATED'; + +-- Verification of Updated Rows +SELECT SUBSTR(f1, LENGTH(f1) - 7 + 1, 7) AS preview +FROM cmdata_zstd_nodict; +---------------------------------------------------------------- +-- 8. Clean Up +---------------------------------------------------------------- +DROP MATERIALIZED VIEW compressmv_zstd_nodict; +DROP TABLE cmdata_zstd_nodict; + +\set HIDE_TOAST_COMPRESSION true -- 2.47.1
From 3043cfc51dc345683b5f2aac6b0c431680a476b6 Mon Sep 17 00:00:00 2001 From: Nikhil Kumar Veldanda <nikhilkv@amazon.com> Date: Sat, 3 May 2025 01:57:15 +0000 Subject: [PATCH v20 1/2] varattrib_4b design proposal to make it extended to support multiple compression algorithms. --- contrib/amcheck/verify_heapam.c | 2 +- src/backend/access/brin/brin_tuple.c | 4 +- src/backend/access/common/detoast.c | 6 +- src/backend/access/common/indextuple.c | 5 +- src/backend/access/common/toast_compression.c | 38 ++++++++- src/backend/access/common/toast_internals.c | 18 ++-- src/backend/access/table/toast_helper.c | 4 +- src/include/access/toast_compression.h | 85 ++++++++++++++++--- src/include/access/toast_internals.h | 38 ++++++--- src/include/varatt.h | 75 +++++++++++++++- src/tools/pgindent/typedefs.list | 2 + 11 files changed, 232 insertions(+), 45 deletions(-) diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c index aa9cccd1da4..d7c2ac6951a 100644 --- a/contrib/amcheck/verify_heapam.c +++ b/contrib/amcheck/verify_heapam.c @@ -1786,7 +1786,7 @@ check_tuple_attribute(HeapCheckContext *ctx) bool valid = false; /* Compressed attributes should have a valid compression method */ - cmid = TOAST_COMPRESS_METHOD(&toast_pointer); + cmid = toast_get_compression_id(attr); switch (cmid) { /* List of all valid compression method IDs */ diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c index 861f397e6db..9c1e22e98c6 100644 --- a/src/backend/access/brin/brin_tuple.c +++ b/src/backend/access/brin/brin_tuple.c @@ -223,6 +223,7 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple, { Datum cvalue; char compression; + CompressionInfo cmp; Form_pg_attribute att = TupleDescAttr(brdesc->bd_tupdesc, keyno); @@ -237,7 +238,8 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple, else compression = InvalidCompressionMethod; - cvalue = toast_compress_datum(value, compression); + cmp = setup_compression_info(compression, att); + cvalue = toast_compress_datum(value, cmp); if (DatumGetPointer(cvalue) != NULL) { diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c index 62651787742..01419d1c65f 100644 --- a/src/backend/access/common/detoast.c +++ b/src/backend/access/common/detoast.c @@ -478,7 +478,7 @@ toast_decompress_datum(struct varlena *attr) * Fetch the compression method id stored in the compression header and * decompress the data using the appropriate decompression routine. */ - cmid = TOAST_COMPRESS_METHOD(attr); + cmid = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(attr); switch (cmid) { case TOAST_PGLZ_COMPRESSION_ID: @@ -514,14 +514,14 @@ toast_decompress_datum_slice(struct varlena *attr, int32 slicelength) * have been seen to give wrong results if passed an output size that is * more than the data's true decompressed size. */ - if ((uint32) slicelength >= TOAST_COMPRESS_EXTSIZE(attr)) + if ((uint32) slicelength >= VARDATA_COMPRESSED_GET_EXTSIZE(attr)) return toast_decompress_datum(attr); /* * Fetch the compression method id stored in the compression header and * decompress the data slice using the appropriate decompression routine. */ - cmid = TOAST_COMPRESS_METHOD(attr); + cmid = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(attr); switch (cmid) { case TOAST_PGLZ_COMPRESSION_ID: diff --git a/src/backend/access/common/indextuple.c b/src/backend/access/common/indextuple.c index 1986b943a28..0386f5a1491 100644 --- a/src/backend/access/common/indextuple.c +++ b/src/backend/access/common/indextuple.c @@ -123,9 +123,10 @@ index_form_tuple_context(TupleDesc tupleDescriptor, att->attstorage == TYPSTORAGE_MAIN)) { Datum cvalue; + CompressionInfo cmp; - cvalue = toast_compress_datum(untoasted_values[i], - att->attcompression); + cmp = setup_compression_info(att->attcompression, att); + cvalue = toast_compress_datum(untoasted_values[i], cmp); if (DatumGetPointer(cvalue) != NULL) { diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c index 21f2f4af97e..5e5d42d80ef 100644 --- a/src/backend/access/common/toast_compression.c +++ b/src/backend/access/common/toast_compression.c @@ -21,6 +21,7 @@ #include "access/toast_compression.h" #include "common/pg_lzcompress.h" #include "varatt.h" +#include "utils/attoptcache.h" /* GUC */ int default_toast_compression = TOAST_PGLZ_COMPRESSION; @@ -266,7 +267,10 @@ toast_get_compression_id(struct varlena *attr) VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr); - if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer)) + if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer) + && VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) == VARATT_4BCE_MASK) + cmid = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(detoast_external_attr(attr)); + else cmid = VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer); } else if (VARATT_IS_COMPRESSED(attr)) @@ -314,3 +318,35 @@ GetCompressionMethodName(char method) return NULL; /* keep compiler quiet */ } } + +CompressionInfo +setup_compression_info(char cmethod, Form_pg_attribute att) +{ + CompressionInfo info; + + /* initialize from the attribute’s default settings */ + info.cmethod = cmethod; + info.cmp_ext = NULL; + + /* If the compression method is not valid, use the current default */ + if (!CompressionMethodIsValid(cmethod)) + info.cmethod = default_toast_compression; + + switch (info.cmethod) + { + case TOAST_PGLZ_COMPRESSION: + case TOAST_LZ4_COMPRESSION: + break; + default: + elog(ERROR, "invalid compression method %c", info.cmethod); + } + + return info; +} + +void +free_compression_info(CompressionInfo *info) +{ + if (info->cmp_ext != NULL) + pfree(info->cmp_ext); +} diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c index 7d8be8346ce..83b537d51bf 100644 --- a/src/backend/access/common/toast_internals.c +++ b/src/backend/access/common/toast_internals.c @@ -43,25 +43,22 @@ static bool toastid_valueid_exists(Oid toastrelid, Oid valueid); * ---------- */ Datum -toast_compress_datum(Datum value, char cmethod) +toast_compress_datum(Datum value, CompressionInfo cmp) { struct varlena *tmp = NULL; int32 valsize; ToastCompressionId cmid = TOAST_INVALID_COMPRESSION_ID; + varatt_cmp_extended *cmp_ext = cmp.cmp_ext; Assert(!VARATT_IS_EXTERNAL(DatumGetPointer(value))); Assert(!VARATT_IS_COMPRESSED(DatumGetPointer(value))); valsize = VARSIZE_ANY_EXHDR(DatumGetPointer(value)); - /* If the compression method is not valid, use the current default */ - if (!CompressionMethodIsValid(cmethod)) - cmethod = default_toast_compression; - /* * Call appropriate compression routine for the compression method. */ - switch (cmethod) + switch (cmp.cmethod) { case TOAST_PGLZ_COMPRESSION: tmp = pglz_compress_datum((const struct varlena *) value); @@ -72,11 +69,14 @@ toast_compress_datum(Datum value, char cmethod) cmid = TOAST_LZ4_COMPRESSION_ID; break; default: - elog(ERROR, "invalid compression method %c", cmethod); + elog(ERROR, "invalid compression method %c", cmp.cmethod); } if (tmp == NULL) + { + free_compression_info(&cmp); return PointerGetDatum(NULL); + } /* * We recheck the actual size even if compression reports success, because @@ -92,13 +92,15 @@ toast_compress_datum(Datum value, char cmethod) { /* successful compression */ Assert(cmid != TOAST_INVALID_COMPRESSION_ID); - TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(tmp, valsize, cmid); + TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD_INFO(tmp, valsize, cmid, cmp_ext); + free_compression_info(&cmp); return PointerGetDatum(tmp); } else { /* incompressible data */ pfree(tmp); + free_compression_info(&cmp); return PointerGetDatum(NULL); } } diff --git a/src/backend/access/table/toast_helper.c b/src/backend/access/table/toast_helper.c index b60fab0a4d2..ba5af5db404 100644 --- a/src/backend/access/table/toast_helper.c +++ b/src/backend/access/table/toast_helper.c @@ -229,8 +229,10 @@ toast_tuple_try_compression(ToastTupleContext *ttc, int attribute) Datum *value = &ttc->ttc_values[attribute]; Datum new_value; ToastAttrInfo *attr = &ttc->ttc_attr[attribute]; + Form_pg_attribute att = TupleDescAttr(ttc->ttc_rel->rd_att, attribute); + CompressionInfo cmp = setup_compression_info(attr->tai_compression, att); - new_value = toast_compress_datum(*value, attr->tai_compression); + new_value = toast_compress_datum(*value, cmp); if (DatumGetPointer(new_value) != NULL) { diff --git a/src/include/access/toast_compression.h b/src/include/access/toast_compression.h index 13c4612ceed..1aef65cde99 100644 --- a/src/include/access/toast_compression.h +++ b/src/include/access/toast_compression.h @@ -13,6 +13,9 @@ #ifndef TOAST_COMPRESSION_H #define TOAST_COMPRESSION_H +#include "varatt.h" +#include "catalog/pg_attribute.h" + /* * GUC support. * @@ -23,24 +26,80 @@ 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. + * Stub errors if someone tries to query metadata size + * for an algorithm that doesn’t support it. + */ +static inline uint32 +unsupported_meta_size(const varatt_cmp_extended *hdr) +{ + elog(ERROR, "toast_cmpid_meta_size called for unsupported compression algorithm"); + return 0; /* unreachable */ +} + +/* + * TOAST compression methods enumeration. * - * 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. + * NAME : algorithm identifier + * VALUE : enum value + * META-SIZE-FN : Calculates algorithm metadata size. */ +#define TOAST_COMPRESSION_LIST \ + X(PGLZ, 0, unsupported_meta_size) \ + X(LZ4, 1, unsupported_meta_size) \ + X(INVALID, 2, unsupported_meta_size) /* sentinel */ + +/* Compression algorithm identifiers */ typedef enum ToastCompressionId { - TOAST_PGLZ_COMPRESSION_ID = 0, - TOAST_LZ4_COMPRESSION_ID = 1, - TOAST_INVALID_COMPRESSION_ID = 2, +#define X(name,val,fn) TOAST_##name##_COMPRESSION_ID = (val), + TOAST_COMPRESSION_LIST +#undef X } ToastCompressionId; +/* lookup table to check if compression method uses extended format */ +static const bool toast_cmpid_extended[] = { +#define X(name,val,fn) \ + /* PGLZ, LZ4 don't use extended format */ \ + [TOAST_##name##_COMPRESSION_ID] = \ + ((val) != TOAST_PGLZ_COMPRESSION_ID && \ + (val) != TOAST_LZ4_COMPRESSION_ID && \ + (val) != TOAST_INVALID_COMPRESSION_ID), + TOAST_COMPRESSION_LIST +#undef X +}; + +#define TOAST_CMPID_EXTENDED(alg) \ + (toast_cmpid_extended[alg]) + +/* + * Prototype for a per-datum metadata-size callback: + * given a pointer to the extended header, return + * how many metadata bytes follow it. + */ +typedef uint32 (*ToastMetaSizeFn) (const varatt_cmp_extended *hdr); + +/* Callback table—indexed by ToastCompressionId */ +static const ToastMetaSizeFn toast_meta_size_fns[] = { +#define X(name,val,fn) [TOAST_##name##_COMPRESSION_ID] = fn, + TOAST_COMPRESSION_LIST +#undef X +}; + +/* Calculates algorithm metadata size */ +static inline uint32 +toast_cmpid_meta_size(const varatt_cmp_extended *hdr) +{ + Assert(hdr != NULL); + return toast_meta_size_fns[hdr->cmp_alg] (hdr); +} + +typedef struct CompressionInfo +{ + char cmethod; + varatt_cmp_extended *cmp_ext; /* non-NULL only if uses extended + * compression methods */ +} CompressionInfo; + /* * Built-in compression methods. pg_attribute will store these in the * attcompression column. In attcompression, InvalidCompressionMethod @@ -69,5 +128,7 @@ extern struct varlena *lz4_decompress_datum_slice(const struct varlena *value, extern ToastCompressionId toast_get_compression_id(struct varlena *attr); extern char CompressionNameToMethod(const char *compression); extern const char *GetCompressionMethodName(char method); +extern CompressionInfo setup_compression_info(char cmethod, Form_pg_attribute att); +extern void free_compression_info(CompressionInfo *info); #endif /* TOAST_COMPRESSION_H */ diff --git a/src/include/access/toast_internals.h b/src/include/access/toast_internals.h index 06ae8583c1e..f4a4829ad17 100644 --- a/src/include/access/toast_internals.h +++ b/src/include/access/toast_internals.h @@ -31,21 +31,33 @@ typedef struct toast_compress_header * Utilities for manipulation of header information for compressed * toast entries. */ -#define TOAST_COMPRESS_EXTSIZE(ptr) \ - (((toast_compress_header *) (ptr))->tcinfo & VARLENA_EXTSIZE_MASK) -#define TOAST_COMPRESS_METHOD(ptr) \ - (((toast_compress_header *) (ptr))->tcinfo >> VARLENA_EXTSIZE_BITS) - -#define TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(ptr, len, cm_method) \ - do { \ - Assert((len) > 0 && (len) <= VARLENA_EXTSIZE_MASK); \ - Assert((cm_method) == TOAST_PGLZ_COMPRESSION_ID || \ - (cm_method) == TOAST_LZ4_COMPRESSION_ID); \ - ((toast_compress_header *) (ptr))->tcinfo = \ - (len) | ((uint32) (cm_method) << VARLENA_EXTSIZE_BITS); \ +#define TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD_INFO(ptr, len, cm_method, cmp_ext) \ + do { \ + Assert((len) > 0 && (len) <= VARLENA_EXTSIZE_MASK); \ + Assert((cm_method) == TOAST_PGLZ_COMPRESSION_ID || \ + (cm_method) == TOAST_LZ4_COMPRESSION_ID); \ + if (!TOAST_CMPID_EXTENDED((cm_method))) \ + { \ + ((toast_compress_header *)(ptr))->tcinfo = \ + ((uint32)(len)) | ((uint32)(cm_method) << VARLENA_EXTSIZE_BITS); \ + } \ + else \ + { \ + /* extended path: mark EXT flag in tcinfo */ \ + ((toast_compress_header *)(ptr))->tcinfo = \ + ((uint32)(len)) | \ + ((uint32)(VARATT_4BCE_MASK) << VARLENA_EXTSIZE_BITS); \ + Assert((cmp_ext) != NULL); \ + /* copy header + algorithm-specific metadata */ \ + memcpy( \ + VARATT_4BCE_HDR_PTR(ptr), \ + (const void *)(cmp_ext), sizeof(varatt_cmp_extended) + \ + toast_cmpid_meta_size((const varatt_cmp_extended *)(cmp_ext)) \ + ); \ + } \ } while (0) -extern Datum toast_compress_datum(Datum value, char cmethod); +extern Datum toast_compress_datum(Datum value, CompressionInfo cmp); extern Oid toast_get_valid_index(Oid toastoid, LOCKMODE lock); extern void toast_delete_datum(Relation rel, Datum value, bool is_speculative); diff --git a/src/include/varatt.h b/src/include/varatt.h index 2e8564d4998..91460f313c5 100644 --- a/src/include/varatt.h +++ b/src/include/varatt.h @@ -328,7 +328,8 @@ typedef struct #define VARDATA_COMPRESSED_GET_EXTSIZE(PTR) \ (((varattrib_4b *) (PTR))->va_compressed.va_tcinfo & VARLENA_EXTSIZE_MASK) #define VARDATA_COMPRESSED_GET_COMPRESS_METHOD(PTR) \ - (((varattrib_4b *) (PTR))->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS) + ( (VARATT_IS_4BCE(PTR)) ? (VARATT_4BCE_CMP_METHOD(PTR)) \ + : (((varattrib_4b *) (PTR))->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS)) /* Same for external Datums; but note argument is a struct varatt_external */ #define VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) \ @@ -340,8 +341,17 @@ typedef struct do { \ Assert((cm) == TOAST_PGLZ_COMPRESSION_ID || \ (cm) == TOAST_LZ4_COMPRESSION_ID); \ - ((toast_pointer).va_extinfo = \ - (len) | ((uint32) (cm) << VARLENA_EXTSIZE_BITS)); \ + if (!TOAST_CMPID_EXTENDED((cm))) \ + { \ + /* Store the actual method in va_extinfo */ \ + ((toast_pointer).va_extinfo = \ + (len) | ((uint32) (cm) << VARLENA_EXTSIZE_BITS)); \ + } \ + else \ + { \ + /* Store 11 in the top 2 bits, meaning "extended" method. */ \ + (toast_pointer).va_extinfo = (uint32)(len) | (VARATT_4BCE_MASK << VARLENA_EXTSIZE_BITS ); \ + } \ } while (0) /* @@ -355,4 +365,63 @@ typedef struct (VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) < \ (toast_pointer).va_rawsize - VARHDRSZ) +/* + * varatt_cmp_extended: an optional per‐datum header for extended compression method. + * Only used when va_tcinfo’s top two bits are “11”. + */ +typedef struct varatt_cmp_extended +{ + uint8 cmp_alg; /* algorithm id (0–255) */ + char cmp_meta[FLEXIBLE_ARRAY_MEMBER]; /* algorithm‐specific + * metadata */ +} varatt_cmp_extended; + +/* + * 1) Detect the extended‐compression flag in va_tcinfo + * (top 2 bits = 0b11 indicate “cmp_ext” path) + */ +#define VARATT_4BCE_MASK 0x3 + +#define VARATT_IS_4BCE(PTR) \ + ((((varattrib_4b *)(PTR))->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS) == VARATT_4BCE_MASK) + +/* + * 2) Pointer to varatt_cmp_extended header (just after the 8-byte varattrib_4b compressed headers) + */ +#define VARATT_4BCE_HDR_PTR(PTR) \ + ((varatt_cmp_extended *) ((char *)(PTR) + VARHDRSZ_COMPRESSED)) + +/* get the algorithm ID */ +#define VARATT_4BCE_CMP_METHOD(PTR) \ + ((ToastCompressionId)VARATT_4BCE_HDR_PTR(PTR)->cmp_alg) + +/* set the algorithm ID */ +#define VARATT_4BCE_SET_HDR(EXT_PTR, alg) ((varatt_cmp_extended*)(EXT_PTR))->cmp_alg = (uint8)(alg); + +/* + * 3) Helpers to find metadata vs payload: + * – cmp_meta[] pointer + * – compressed‐bytes pointer + * – compressed-bytes size + * – total header size + */ + +/* pointer to compression algorithm's metadata */ +#define VARATT_4BCE_META_PTR(PTR) \ + ((void *)VARATT_4BCE_HDR_PTR(PTR)->cmp_meta) + +/* pointer to compressed bytes (after metadata) */ +#define VARATT_4BCE_PAYLOAD_PTR(PTR) \ + ((void *) ((char *)VARATT_4BCE_META_PTR(PTR) + toast_cmpid_meta_size(VARATT_4BCE_HDR_PTR(PTR)))) + +/* number of compressed‐payload bytes */ +#define VARATT_4BCE_PAYLOAD_SIZE(PTR) \ + ( VARSIZE_4B(PTR) - VARHDRSZ_COMPRESSED - sizeof(varatt_cmp_extended) \ + - toast_cmpid_meta_size(VARATT_4BCE_HDR_PTR(PTR))) + +/* total header+meta size before payload */ +#define VARATT_4BCE_HDRSZ(EXT_PTR) \ + ( VARHDRSZ_COMPRESSED + sizeof(varatt_cmp_extended) \ + + toast_cmpid_meta_size(EXT_PTR)) + #endif diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index e5879e00dff..ea28675e0c9 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -482,6 +482,7 @@ CompositeIOData CompositeTypeStmt CompoundAffixFlag CompressFileHandle +CompressionInfo CompressionLocation CompressorState ComputeXidHorizonsResult @@ -4153,6 +4154,7 @@ uuid_t va_list vacuumingOptions validate_string_relopt +varatt_cmp_extended varatt_expanded varattrib_1b varattrib_1b_e base-commit: a675149e87706d01e4007150a0124b89bdef08be -- 2.47.1