From 2a43035c6c27b486cb259fe16180f9a097b4b847 Mon Sep 17 00:00:00 2001 From: Nikhil Kumar Veldanda Date: Tue, 15 Sep 2026 22:57:08 -0700 Subject: [PATCH v2 2/3] Allow more than four TOAST compression methods A compressed varlena identifies its compression method in the two high-order bits of the tcinfo word of its header, and an external TOAST pointer does the same in va_extinfo. Two of the four possible values are taken by pglz and lz4, and a third is reserved as TOAST_INVALID_COMPRESSION_ID, so adding even one more method would use up the last one. Introduce a "long" form of both structures, in which the two method bits hold VARLENA_COMPRESS_METHOD_LONG and the actual method ID is stored in a byte of its own following the fixed part: - For compressed-in-line datums the long form is described by a new struct, varattrib_4b_long: the va_compressed layout with a va_cmid byte inserted before the data, giving a header of VARHDRSZ_COMPRESSED_LONG bytes. It is deliberately not a member of the varattrib_4b union, as a member with the extra byte would pad to 12 bytes and so increase sizeof(varattrib_4b) from 8; code all over the place looks at varlena headers of unknown or smaller size through pointers of that type, and compilers use the type's size to check such accesses (-Warray-bounds). Static assertions tie its layout to va_compressed. - Two new TOAST pointer tags, VARTAG_ONDISK_OID_LONG and VARTAG_ONDISK_OID8_LONG, denote a plain varatt_external_oid or varatt_external_oid8 followed by the method ID byte. Using tags rather than a variable-size struct keeps VARTAG_SIZE() a pure function of the tag, which matters for tuple deforming, and avoids a struct with trailing padding that must not reach disk. The long tags are their plain counterparts with the low bit set; the VARTAG_IS_ONDISK*() tests hide that detail from callers. Only pglz and lz4 keep using the two bits directly, so existing data is unaffected and neither form changes size for them. Every other method, present or future, uses the long form; which is decided in one place, toast_compression_id_needs_cmid_byte(). This leaves room for IDs up to 255. TOAST_INVALID_COMPRESSION_ID moves from 2 to 3, so as to be equal to VARLENA_COMPRESS_METHOD_LONG: code that mistakenly reads the raw method bits of a long-form value as a method ID then ends up with an invalid ID rather than a real method. This also frees ID 2 for the next method. Since the method of an external value can no longer be derived from va_extinfo alone, VARATT_EXTINFO_GET_COMPRESS_METHOD() is removed in favor of the compress_method field of toast_external_data. amcheck now verifies that the compression method of an external value is stored in the pointer form appropriate for it, and that only compressed values use the long form. --- contrib/amcheck/verify_heapam.c | 33 +++-- doc/src/sgml/storage.sgml | 5 +- src/backend/access/common/toast_compression.c | 8 ++ src/backend/access/common/toast_internals.c | 53 ++++++-- src/include/access/detoast.h | 35 +++++- src/include/access/toast_compression.h | 45 +++++-- src/include/access/toast_internals.h | 20 +++- src/include/varatt.h | 113 ++++++++++++++++-- src/tools/pgindent/typedefs.list | 1 + 9 files changed, 260 insertions(+), 53 deletions(-) diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c index b6876d0f394..7c6a2ff635b 100644 --- a/contrib/amcheck/verify_heapam.c +++ b/contrib/amcheck/verify_heapam.c @@ -76,7 +76,7 @@ typedef struct ToastedAttribute { Oid8 va_valueid; /* value ID (works for both Oid and Oid8) */ uint32 va_extinfo; /* external size and compression method */ - vartag_external tag; /* VARTAG_ONDISK_OID or VARTAG_ONDISK_OID8 */ + vartag_external tag; /* one of the VARTAG_ONDISK_* tags */ BlockNumber blkno; /* block in main table */ OffsetNumber offnum; /* offset in main table */ AttrNumber attnum; /* attribute in main table */ @@ -1797,14 +1797,22 @@ check_tuple_attribute(HeapCheckContext *ctx) ToastCompressionId cmid; bool valid = false; - /* Compressed attributes should have a valid compression method */ + /* + * Compressed attributes should have a valid compression method, and + * it must be stored in the TOAST pointer form appropriate for it: the + * methods that fit in the two method bits of va_extinfo use the plain + * pointer, all others the long form. A plain pointer whose method + * bits hold VARLENA_COMPRESS_METHOD_LONG decodes as + * TOAST_INVALID_COMPRESSION_ID and so is caught here too. + */ cmid = toast_ext_data.compress_method; switch (cmid) { /* List of all valid compression method IDs */ case TOAST_PGLZ_COMPRESSION_ID: case TOAST_LZ4_COMPRESSION_ID: - valid = true; + valid = (toast_compression_id_needs_cmid_byte(cmid) == + VARTAG_IS_ONDISK_LONG(va_tag_value)); break; /* Recognized but invalid compression method ID */ @@ -1818,6 +1826,13 @@ check_tuple_attribute(HeapCheckContext *ctx) psprintf("toast value " OID8_FORMAT " has invalid compression method id %d", toast_pointer_valueid, cmid)); } + else if (VARTAG_IS_ONDISK_LONG(va_tag_value)) + { + /* Only compressed values get the long form of the pointer */ + report_corruption(ctx, + psprintf("toast value " OID8_FORMAT " is not compressed but has a long TOAST pointer", + toast_pointer_valueid)); + } /* The tuple header better claim to contain toasted values */ if (!(infomask & HEAP_HASEXTERNAL)) @@ -1884,7 +1899,6 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta) int32 max_chunk_size; Oid8 toast_valueid; Oid toast_typid; - vartag_external expected_tag; toast_valueid = ta->va_valueid; extsize = VARATT_EXTINFO_GET_EXTSIZE(ta->va_extinfo); @@ -1892,14 +1906,11 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta) /* * Take the chunk_id type from the TOAST table's own definition, not from * from the vartag in the main table as that pointer is the very thing - * under scrutiny here. The two must agree. + * under scrutiny here. The two must agree, whether the pointer is of the + * plain or of the long form. */ toast_typid = TupleDescAttr(ctx->toast_rel->rd_att, 0)->atttypid; - if (toast_typid == OID8OID) - expected_tag = VARTAG_ONDISK_OID8; - else if (toast_typid == OIDOID) - expected_tag = VARTAG_ONDISK_OID; - else + if (toast_typid != OIDOID && toast_typid != OID8OID) { report_toast_corruption(ctx, ta, psprintf("toast value " OID8_FORMAT " stored in toast table whose chunk_id has unexpected type %u", @@ -1907,7 +1918,7 @@ check_toasted_attribute(HeapCheckContext *ctx, ToastedAttribute *ta) return; } - if (ta->tag != expected_tag) + if (VARTAG_IS_ONDISK_OID8(ta->tag) != (toast_typid == OID8OID)) { report_toast_corruption(ctx, ta, psprintf("toast value " OID8_FORMAT " has TOAST tag %u, but chunk_id of toast table has type %u", diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml index 83de016eaa5..d301251d2b5 100644 --- a/doc/src/sgml/storage.sgml +++ b/doc/src/sgml/storage.sgml @@ -439,7 +439,10 @@ any. Allowing for the varlena header bytes, the total size of an on-disk TOAST pointer datum is 18 bytes when using an OID as chunk_id, or 22 bytes when using an OID8 as chunk_id, regardless of the -actual size of the represented value. +actual size of the represented value. Values compressed with a method other +than pglz or lz4 need one additional +byte to identify the compression method, both in the pointer datum and in +the header of the compressed data itself. diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c index 8bb871c9791..9bd4404fcf7 100644 --- a/src/backend/access/common/toast_compression.c +++ b/src/backend/access/common/toast_compression.c @@ -84,6 +84,8 @@ pglz_decompress_datum(const varlena *value) varlena *result; int32 rawsize; + Assert(VARDATA_COMPRESSED_GET_COMPRESS_METHOD(value) == TOAST_PGLZ_COMPRESSION_ID); + /* allocate memory for the uncompressed data */ result = (varlena *) palloc(VARDATA_COMPRESSED_GET_EXTSIZE(value) + VARHDRSZ); @@ -112,6 +114,8 @@ pglz_decompress_datum_slice(const varlena *value, varlena *result; int32 rawsize; + Assert(VARDATA_COMPRESSED_GET_COMPRESS_METHOD(value) == TOAST_PGLZ_COMPRESSION_ID); + /* allocate memory for the uncompressed data */ result = (varlena *) palloc(slicelength + VARHDRSZ); @@ -188,6 +192,8 @@ lz4_decompress_datum(const varlena *value) int32 rawsize; varlena *result; + Assert(VARDATA_COMPRESSED_GET_COMPRESS_METHOD(value) == TOAST_LZ4_COMPRESSION_ID); + /* allocate memory for the uncompressed data */ result = (varlena *) palloc(VARDATA_COMPRESSED_GET_EXTSIZE(value) + VARHDRSZ); @@ -221,6 +227,8 @@ lz4_decompress_datum_slice(const varlena *value, int32 slicelength) int32 rawsize; varlena *result; + Assert(VARDATA_COMPRESSED_GET_COMPRESS_METHOD(value) == TOAST_LZ4_COMPRESSION_ID); + /* slice decompression not supported prior to 1.8.3 */ if (LZ4_versionNumber() < 10803) return lz4_decompress_datum(value); diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c index f5e7e834921..5e187e99b4a 100644 --- a/src/backend/access/common/toast_internals.c +++ b/src/backend/access/common/toast_internals.c @@ -29,7 +29,7 @@ static bool toastrel_valueid_exists(Relation toastrel, Oid8 valueid); static bool toastid_valueid_exists(Oid toastrelid, Oid8 valueid); static varlena *toast_pointer_build(vartag_external tag, const void *fixed, - Size fixedsize); + Size fixedsize, ToastCompressionId cmid); /* ---------- * toast_compress_datum - @@ -190,6 +190,8 @@ toast_save_datum(Relation rel, Datum value, uint32 va_extinfo; Oid8 va_valueid; Oid va_toastrelid; + ToastCompressionId cmid = TOAST_INVALID_COMPRESSION_ID; + bool pointer_long = false; Assert(!VARATT_IS_EXTERNAL(dval)); @@ -215,7 +217,9 @@ toast_save_datum(Relation rel, Datum value, * * va_extinfo stored the actual size of the data payload in the toast * records and the compression method in first 2 bits if data is - * compressed. + * compressed. Methods that don't fit in those bits are flagged there + * with VARLENA_COMPRESS_METHOD_LONG and require the long form of the + * TOAST pointer, which carries the actual method ID in an extra byte. */ if (VARATT_IS_SHORT(dval)) { @@ -226,7 +230,7 @@ toast_save_datum(Relation rel, Datum value, } else if (VARATT_IS_COMPRESSED(dval)) { - uint32 cmid = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(dval); + cmid = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(dval); data_p = VARDATA(dval); data_todo = VARSIZE(dval) - VARHDRSZ; @@ -236,7 +240,14 @@ toast_save_datum(Relation rel, Datum value, /* set external size and compression method */ Assert(cmid == TOAST_PGLZ_COMPRESSION_ID || cmid == TOAST_LZ4_COMPRESSION_ID); - va_extinfo = data_todo | (cmid << VARLENA_EXTSIZE_BITS); + if (toast_compression_id_needs_cmid_byte(cmid)) + { + pointer_long = true; + va_extinfo = data_todo | + ((uint32) VARLENA_COMPRESS_METHOD_LONG << VARLENA_EXTSIZE_BITS); + } + else + va_extinfo = data_todo | ((uint32) cmid << VARLENA_EXTSIZE_BITS); /* Assert that the numbers look like it's compressed */ Assert(VARATT_EXTINFO_IS_COMPRESSED(va_extinfo, va_rawsize)); @@ -381,7 +392,8 @@ toast_save_datum(Relation rel, Datum value, table_close(toastrel, NoLock); /* - * Create the TOAST pointer value that we'll return + * Create the TOAST pointer value that we'll return. The long form is the + * plain one followed by the compression method ID byte. */ if (toast_typid == OID8OID) { @@ -392,8 +404,8 @@ toast_save_datum(Relation rel, Datum value, VARATT_EXTERNAL_OID8_SET_VALUEID(&toast_pointer, va_valueid); toast_pointer.va_toastrelid = va_toastrelid; - result = toast_pointer_build(VARTAG_ONDISK_OID8, - &toast_pointer, sizeof(toast_pointer)); + result = toast_pointer_build(pointer_long ? VARTAG_ONDISK_OID8_LONG : VARTAG_ONDISK_OID8, + &toast_pointer, sizeof(toast_pointer), cmid); } else { @@ -404,8 +416,8 @@ toast_save_datum(Relation rel, Datum value, toast_pointer.va_valueid = (Oid) va_valueid; toast_pointer.va_toastrelid = va_toastrelid; - result = toast_pointer_build(VARTAG_ONDISK_OID, - &toast_pointer, sizeof(toast_pointer)); + result = toast_pointer_build(pointer_long ? VARTAG_ONDISK_OID_LONG : VARTAG_ONDISK_OID, + &toast_pointer, sizeof(toast_pointer), cmid); } return PointerGetDatum(result); @@ -415,20 +427,35 @@ toast_save_datum(Relation rel, Datum value, * toast_pointer_build - * * Build an on-disk TOAST pointer datum of the given tag from its fixed - * part (a varatt_external_oid or varatt_external_oid8). + * part (a varatt_external_oid or varatt_external_oid8), appending the + * compression method ID byte for the long-form tags. * ---------- */ static varlena * -toast_pointer_build(vartag_external tag, const void *fixed, Size fixedsize) +toast_pointer_build(vartag_external tag, const void *fixed, Size fixedsize, + ToastCompressionId cmid) { varlena *result; + char *data; Assert(VARTAG_IS_ONDISK(tag)); - Assert(VARTAG_SIZE(tag) == fixedsize); + Assert(VARTAG_SIZE(tag) == fixedsize + + (VARTAG_IS_ONDISK_LONG(tag) ? VARATT_EXTERNAL_CMID_SIZE : 0)); result = (varlena *) palloc(VARHDRSZ_EXTERNAL + VARTAG_SIZE(tag)); SET_VARTAG_EXTERNAL(result, tag); - memcpy(VARDATA_EXTERNAL(result), fixed, fixedsize); + data = VARDATA_EXTERNAL(result); + memcpy(data, fixed, fixedsize); + + if (VARTAG_IS_ONDISK_LONG(tag)) + { + uint8 cmid_byte; + + Assert(cmid != TOAST_INVALID_COMPRESSION_ID && + toast_compression_id_needs_cmid_byte(cmid)); + cmid_byte = (uint8) cmid; + memcpy(data + fixedsize, &cmid_byte, sizeof(cmid_byte)); + } return result; } diff --git a/src/include/access/detoast.h b/src/include/access/detoast.h index 9b9c253432c..dff3dee7ac2 100644 --- a/src/include/access/detoast.h +++ b/src/include/access/detoast.h @@ -43,7 +43,8 @@ do { \ * Decoded contents of an on-disk external TOAST pointer. * * compress_method is only meaningful if the value is compressed, that is if - * VARATT_EXTINFO_IS_COMPRESSED(extinfo, rawsize). + * VARATT_EXTINFO_IS_COMPRESSED(extinfo, rawsize). Unlike the raw method bits + * of extinfo, it is the actual method ID whichever pointer form was used. */ typedef struct toast_external_data { @@ -61,32 +62,56 @@ typedef struct toast_external_data static inline void toast_external_info_get(const struct varlena *attr, toast_external_data *toast_ext_data) { + const char *ptr = VARDATA_EXTERNAL(attr); + Size fixedsize; + Assert(VARATT_IS_EXTERNAL_ONDISK(attr)); + /* + * The long form of a pointer is the plain form followed by the + * compression method ID byte, so decoding the fixed part is the same for + * both. We can't use VARATT_EXTERNAL_GET_POINTER() here because it + * insists on the datum being exactly the size of the struct. + */ toast_ext_data->tag = VARTAG_EXTERNAL(attr); - if (toast_ext_data->tag == VARTAG_ONDISK_OID8) + if (VARTAG_IS_ONDISK_OID8(toast_ext_data->tag)) { varatt_external_oid8 toast_pointer; - VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr); + memcpy(&toast_pointer, ptr, sizeof(toast_pointer)); toast_ext_data->rawsize = toast_pointer.va_rawsize; toast_ext_data->extinfo = toast_pointer.va_extinfo; toast_ext_data->valueid = VARATT_EXTERNAL_OID8_GET_VALUEID(toast_pointer); toast_ext_data->toastrelid = toast_pointer.va_toastrelid; + fixedsize = sizeof(toast_pointer); } else { varatt_external_oid toast_pointer; - VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr); + memcpy(&toast_pointer, ptr, sizeof(toast_pointer)); toast_ext_data->rawsize = toast_pointer.va_rawsize; toast_ext_data->extinfo = toast_pointer.va_extinfo; toast_ext_data->valueid = toast_pointer.va_valueid; toast_ext_data->toastrelid = toast_pointer.va_toastrelid; + fixedsize = sizeof(toast_pointer); } + /* + * Resolve the compression method: it is the two method bits of extinfo, + * except in the long form of a pointer, where those bits only flag the + * form and the method is in the trailing byte. + */ toast_ext_data->compress_method = (ToastCompressionId) - VARATT_EXTINFO_GET_COMPRESS_METHOD(toast_ext_data->extinfo); + (toast_ext_data->extinfo >> VARLENA_EXTSIZE_BITS); + if (VARTAG_IS_ONDISK_LONG(toast_ext_data->tag)) + { + uint8 cmid; + + Assert(toast_ext_data->compress_method == VARLENA_COMPRESS_METHOD_LONG); + memcpy(&cmid, ptr + fixedsize, sizeof(cmid)); + toast_ext_data->compress_method = (ToastCompressionId) cmid; + } } /* ---------- diff --git a/src/include/access/toast_compression.h b/src/include/access/toast_compression.h index 3265f10b734..e848d72805b 100644 --- a/src/include/access/toast_compression.h +++ b/src/include/access/toast_compression.h @@ -13,6 +13,8 @@ #ifndef TOAST_COMPRESSION_H #define TOAST_COMPRESSION_H +#include "varatt.h" + /* * GUC support. * @@ -23,24 +25,53 @@ extern PGDLLIMPORT int default_toast_compression; /* - * Built-in compression method ID. The toast compression header will store - * this in the first 2 bits of the raw length. These built-in compression - * method IDs are directly mapped to the built-in compression methods. + * Built-in compression method ID. These built-in compression method IDs are + * directly mapped to the built-in compression methods. + * + * A compressed varlena identifies its method in the two high-order bits of + * its tcinfo/extinfo word. Only pglz and lz4 are stored there directly; all + * other methods use the long form of the header, flagged by + * VARLENA_COMPRESS_METHOD_LONG in those bits, which stores the ID in a byte + * of its own (see varatt.h) and so leaves room for IDs up to 255. + * toast_compression_id_needs_cmid_byte() tells which form a given ID uses. + * + * TOAST_INVALID_COMPRESSION_ID is not a real compression method and is never + * stored on disk; it only serves to report "this value is not compressed". + * It is deliberately equal to VARLENA_COMPRESS_METHOD_LONG, so that code + * that mistakenly interprets the raw two-bit field of a long-form value as a + * method ID ends up with an invalid ID rather than a real method. * * 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. + * below. */ typedef enum ToastCompressionId { TOAST_PGLZ_COMPRESSION_ID = 0, TOAST_LZ4_COMPRESSION_ID = 1, - TOAST_INVALID_COMPRESSION_ID = 2, + TOAST_INVALID_COMPRESSION_ID = 3, } ToastCompressionId; +StaticAssertDecl(TOAST_INVALID_COMPRESSION_ID == VARLENA_COMPRESS_METHOD_LONG, + "TOAST_INVALID_COMPRESSION_ID must match VARLENA_COMPRESS_METHOD_LONG"); + +/* + * Does this compression method ID need a byte of its own? + * + * Only the two original methods fit in the two method bits of the header; + * every other method needs the long form of the compressed-in-line header + * and of the TOAST pointer, which carry the ID in a separate byte. Not + * meaningful for TOAST_INVALID_COMPRESSION_ID. + */ +static inline bool +toast_compression_id_needs_cmid_byte(ToastCompressionId cmid) +{ + Assert(cmid != TOAST_INVALID_COMPRESSION_ID); + return (cmid != TOAST_PGLZ_COMPRESSION_ID && + cmid != TOAST_LZ4_COMPRESSION_ID); +} + /* * Built-in compression methods. pg_attribute will store these in the * attcompression column. In attcompression, InvalidCompressionMethod diff --git a/src/include/access/toast_internals.h b/src/include/access/toast_internals.h index 033bf45f0d1..9f3dfa326cd 100644 --- a/src/include/access/toast_internals.h +++ b/src/include/access/toast_internals.h @@ -23,9 +23,10 @@ * Fill in the header of a compressed-in-line datum: the original data size * (excluding header) and the compression method. * - * The compression routine must already have laid out the datum with - * VARHDRSZ_COMPRESSED bytes of header, since the compressed data starts - * right after it. The varlena length word is not touched here. + * The compression routine must already have laid out the datum with the + * header size appropriate for its method (VARHDRSZ_COMPRESSED or + * VARHDRSZ_COMPRESSED_LONG), since the compressed data starts right after it. + * The varlena length word is not touched here. */ static inline void toast_compress_set_size_and_method(varlena *ptr, uint32 rawsize, @@ -37,8 +38,17 @@ toast_compress_set_size_and_method(varlena *ptr, uint32 rawsize, Assert(cmid == TOAST_PGLZ_COMPRESSION_ID || cmid == TOAST_LZ4_COMPRESSION_ID); - va->va_compressed.va_tcinfo = - rawsize | ((uint32) cmid << VARLENA_EXTSIZE_BITS); + if (toast_compression_id_needs_cmid_byte(cmid)) + { + varattrib_4b_long *va_long = (varattrib_4b_long *) ptr; + + va_long->va_tcinfo = + rawsize | ((uint32) VARLENA_COMPRESS_METHOD_LONG << VARLENA_EXTSIZE_BITS); + va_long->va_cmid = (uint8) cmid; + } + else + va->va_compressed.va_tcinfo = + rawsize | ((uint32) cmid << VARLENA_EXTSIZE_BITS); } extern Datum toast_compress_datum(Datum value, char cmethod); diff --git a/src/include/varatt.h b/src/include/varatt.h index f6a1f5d60ae..a6514f04d60 100644 --- a/src/include/varatt.h +++ b/src/include/varatt.h @@ -83,9 +83,27 @@ VARATT_EXTERNAL_OID8_SET_VALUEID(varatt_external_oid8 *toast_pointer, Oid8 id) /* * These macros define the "saved size" portion of va_extinfo. Its remaining * two high-order bits identify the compression method. + * + * Only the two original compression methods (pglz and lz4) are identified + * directly by those two bits. Any other method uses the "long" form of the + * header, which is flagged by the value VARLENA_COMPRESS_METHOD_LONG in those + * bits and carries the actual method ID in a byte of its own following the + * fixed part; see varattrib_4b_long and the VARTAG_ONDISK_*_LONG TOAST + * pointers below. */ #define VARLENA_EXTSIZE_BITS 30 #define VARLENA_EXTSIZE_MASK ((1U << VARLENA_EXTSIZE_BITS) - 1) +#define VARLENA_COMPRESS_METHOD_LONG 3 + +/* + * Size of the compression method ID byte that the VARTAG_ONDISK_*_LONG TOAST + * pointers append to a varatt_external_oid or varatt_external_oid8. No + * struct is declared for those pointers, because a struct containing that + * trailing byte would carry padding that we must not store on disk; decode + * the fixed part as usual and then read the extra byte (see + * toast_external_info_get()). + */ +#define VARATT_EXTERNAL_CMID_SIZE sizeof(uint8) /* * varatt_indirect is a "TOAST pointer" representing an out-of-line @@ -123,6 +141,13 @@ typedef struct varatt_expanded * value for VARTAG_ONDISK_OID comes from a requirement for on-disk * compatibility with a previous notion that the tag field was the pointer * datum's length. + * + * The _LONG variants of the on-disk tags denote the long form of a TOAST + * pointer, which appends a compression method ID byte to the corresponding + * plain form (see VARATT_EXTERNAL_CMID_SIZE). It is used only for values + * whose compression method does not fit in the two method bits of + * va_extinfo. Each _LONG tag is its plain counterpart with the low bit set, + * which the VARTAG_IS_ONDISK*() tests below rely on. */ typedef enum vartag_external { @@ -130,7 +155,9 @@ typedef enum vartag_external VARTAG_EXPANDED_RO = 2, VARTAG_EXPANDED_RW = 3, VARTAG_ONDISK_OID8 = 4, - VARTAG_ONDISK_OID = 18 + VARTAG_ONDISK_OID8_LONG = 5, + VARTAG_ONDISK_OID = 18, + VARTAG_ONDISK_OID_LONG = 19 } vartag_external; /* Is a TOAST pointer either type of expanded-object pointer? */ @@ -141,11 +168,36 @@ VARTAG_IS_EXPANDED(vartag_external tag) return ((tag & ~1) == VARTAG_EXPANDED_RO); } +/* + * Is a TOAST pointer an on-disk one with an Oid (resp. Oid8) value ID? + * These accept both the plain and the long form of the pointer; the tests + * rely on the specific tag values above. + */ +static inline bool +VARTAG_IS_ONDISK_OID(vartag_external tag) +{ + return ((tag & ~1) == VARTAG_ONDISK_OID); +} + +static inline bool +VARTAG_IS_ONDISK_OID8(vartag_external tag) +{ + return ((tag & ~1) == VARTAG_ONDISK_OID8); +} + /* Is a TOAST pointer any of the on-disk kinds? */ static inline bool VARTAG_IS_ONDISK(vartag_external tag) { - return (tag == VARTAG_ONDISK_OID || tag == VARTAG_ONDISK_OID8); + return VARTAG_IS_ONDISK_OID(tag) || VARTAG_IS_ONDISK_OID8(tag); +} + +/* Is a TOAST pointer the long form of an on-disk pointer? */ +/* this test relies on the specific tag values above */ +static inline bool +VARTAG_IS_ONDISK_LONG(vartag_external tag) +{ + return VARTAG_IS_ONDISK(tag) && (tag & 1) != 0; } /* Size of the data part of a "TOAST pointer" datum */ @@ -160,6 +212,10 @@ VARTAG_SIZE(vartag_external tag) return sizeof(varatt_external_oid); else if (tag == VARTAG_ONDISK_OID8) return sizeof(varatt_external_oid8); + else if (tag == VARTAG_ONDISK_OID_LONG) + return sizeof(varatt_external_oid) + VARATT_EXTERNAL_CMID_SIZE; + else if (tag == VARTAG_ONDISK_OID8_LONG) + return sizeof(varatt_external_oid8) + VARATT_EXTERNAL_CMID_SIZE; else { Assert(false); @@ -192,6 +248,33 @@ typedef union } va_compressed; } varattrib_4b; +/* + * Long form of the compressed-in-line format, used when the method bits of + * va_tcinfo hold VARLENA_COMPRESS_METHOD_LONG. It is the va_compressed + * layout above with the compression method ID inserted before the data. + * + * This is deliberately not a member of the varattrib_4b union: a member with + * the extra byte would pad to 12 bytes and so increase sizeof(varattrib_4b) + * from 8, and code all over the place inspects varlena headers of unknown or + * smaller size through pointers of that type. Only ever use offsetof() on + * this struct, never sizeof(), as it has trailing padding. + */ +typedef struct +{ + uint32 va_header; + uint32 va_tcinfo; /* As in va_compressed, with the method bits + * set to VARLENA_COMPRESS_METHOD_LONG */ + uint8 va_cmid; /* Compression method ID */ + char va_data[FLEXIBLE_ARRAY_MEMBER]; /* Compressed data */ +} varattrib_4b_long; + +StaticAssertDecl(offsetof(varattrib_4b_long, va_tcinfo) == + offsetof(varattrib_4b, va_compressed.va_tcinfo), + "varattrib_4b_long must extend va_compressed"); +StaticAssertDecl(offsetof(varattrib_4b_long, va_cmid) == + offsetof(varattrib_4b, va_compressed.va_data), + "varattrib_4b_long must extend va_compressed"); + typedef struct { uint8 va_header; @@ -328,6 +411,7 @@ typedef struct #define VARHDRSZ_EXTERNAL offsetof(varattrib_1b_e, va_data) #define VARHDRSZ_COMPRESSED offsetof(varattrib_4b, va_compressed.va_data) +#define VARHDRSZ_COMPRESSED_LONG offsetof(varattrib_4b_long, va_data) #define VARHDRSZ_SHORT offsetof(varattrib_1b, va_data) #define VARATT_SHORT_MAX 0x7F @@ -548,15 +632,28 @@ VARDATA_COMPRESSED_GET_EXTSIZE(const void *PTR) return ((const varattrib_4b *) PTR)->va_compressed.va_tcinfo & VARLENA_EXTSIZE_MASK; } -/* Compression method of a compressed-in-line varlena datum */ +/* + * Compression method of a compressed-in-line varlena datum. This handles + * both header forms, so it always returns the actual method ID. + */ static inline uint32 VARDATA_COMPRESSED_GET_COMPRESS_METHOD(const void *PTR) { - return ((const varattrib_4b *) PTR)->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS; + const varattrib_4b *va = (const varattrib_4b *) PTR; + uint32 method = va->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS; + + if (method == VARLENA_COMPRESS_METHOD_LONG) + method = ((const varattrib_4b_long *) PTR)->va_cmid; + return method; } /* - * Same for external Datums, saved into an va_extinfo. + * Same for the saved size of external Datums, stored in va_extinfo. + * + * There is deliberately no equivalent for the compression method: in the long + * form of a TOAST pointer it lives in a separate byte, so it cannot be + * derived from va_extinfo alone. Decode the pointer with + * toast_external_info_get() and use its compress_method field instead. */ static inline Size VARATT_EXTINFO_GET_EXTSIZE(uint32 extinfo) @@ -564,12 +661,6 @@ VARATT_EXTINFO_GET_EXTSIZE(uint32 extinfo) return extinfo & VARLENA_EXTSIZE_MASK; } -static inline uint32 -VARATT_EXTINFO_GET_COMPRESS_METHOD(uint32 extinfo) -{ - return extinfo >> VARLENA_EXTSIZE_BITS; -} - /* * Testing whether an externally-stored value is compressed requires comparing * the saved size stored in va_extinfo (the actual length of the external data) diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index b3992db9a59..fc1aabd783b 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -4418,6 +4418,7 @@ varatt_indirect varattrib_1b varattrib_1b_e varattrib_4b +varattrib_4b_long varlena vartag_external vbits -- 2.54.0 (Apple Git-157)