From 74ea54df1cf522933349b67aaf10613966b40335 Mon Sep 17 00:00:00 2001 From: Peter Geoghegan Date: Mon, 20 Apr 2026 22:05:16 -0400 Subject: [PATCH 2/3] amcheck: verify prefix-compressed nbtree leaf pages. bt_target_page_check now routes every item on a prefix-compressed leaf page through _bt_prefix_reconstruct before any of the existing per-tuple checks run, so the ordering, high key, and heapallindexed fingerprint checks all operate on the same plain tuples that an index scan would see. Plain tuples refresh the current anchor as they pass through, which keeps followers from the next group from being reconstructed against the previous group's anchor. BTreeTupleGetHeapTIDCareful decodes a follower's real heap TID into scratch space, so bt_index_parent_check's cross-page invariant checks get a usable TID. Report corruption when the first data item on a prefix-compressed leaf page is not a plain anchor, when a compressed tuple appears on a leaf page without BTP_HAS_PREFIX_COMPRESSION, and when the metapage has both deduplication and prefix compression enabled. --- contrib/amcheck/verify_nbtree.c | 92 +++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c index 96a4d1f02..264bc62ef 100644 --- a/contrib/amcheck/verify_nbtree.c +++ b/contrib/amcheck/verify_nbtree.c @@ -115,6 +115,14 @@ typedef struct BtreeCheckState */ IndexTuple lowkey; + /* + * Scratch ItemPointerData for decoding a prefix-compressed follower's + * heap TID on demand (BTreeTupleGetHeapTID returns NULL for such tuples; + * callers that want a usable pointer use BTreeCompressedTupleGetHeapTID + * into this buffer). Only the last caller's decode is valid. + */ + ItemPointerData compressed_htid_scratch; + /* * The rightlink and incomplete split flag of block one level down to the * target page, which was visited last time via downlink from target page. @@ -328,6 +336,11 @@ bt_index_check_callback(Relation indrel, Relation heaprel, void *state, bool rea (errcode(ERRCODE_INDEX_CORRUPTED), errmsg("index \"%s\" metapage has equalimage field set on unsupported nbtree version", RelationGetRelationName(indrel)))); + if (allequalimage && prefix_compression) + ereport(ERROR, + (errcode(ERRCODE_INDEX_CORRUPTED), + errmsg("index \"%s\" metapage has both deduplication and prefix compression enabled", + RelationGetRelationName(indrel)))); if (allequalimage && !_bt_allequalimage(indrel, false)) ereport(ERROR, (errcode(ERRCODE_INDEX_CORRUPTED), @@ -1229,6 +1242,9 @@ bt_target_page_check(BtreeCheckState *state) OffsetNumber offset; OffsetNumber max; BTPageOpaque topaque; + bool prefix_page; + BTPrefixPageState prefix_state; + char *rbuf = NULL; /* Last visible entry info for checking indexes with unique constraint */ BtreeLastVisibleEntry lVis = {InvalidBlockNumber, InvalidOffsetNumber, -1, NULL}; @@ -1236,6 +1252,19 @@ bt_target_page_check(BtreeCheckState *state) topaque = BTPageGetOpaque(state->target); max = PageGetMaxOffsetNumber(state->target); + /* + * For prefix-compressed leaf pages, maintain a reconstruction state so + * compressed followers are materialized before any downstream checks + * look at their attrs, heap TID, or byte image. + */ + prefix_page = P_ISLEAF(topaque) && P_HAS_PREFIX_COMPRESSION(topaque); + if (prefix_page) + { + memset(&prefix_state, 0, sizeof(prefix_state)); + _bt_prefix_state_init(&prefix_state, state->target); + rbuf = palloc(BTMaxItemSize); + } + elog(DEBUG2, "verifying %u items on %s block %u", max, P_ISLEAF(topaque) ? "leaf" : "internal", state->targetblock); @@ -1297,6 +1326,51 @@ bt_target_page_check(BtreeCheckState *state) itup = (IndexTuple) PageGetItem(state->target, itemid); tupsize = IndexTupleSize(itup); + /* + * Prefix-compressed leaf invariants + reconstruction. + * + * The leftmost data item on a BTP_HAS_PREFIX_COMPRESSION leaf must + * be a plain anchor. A compressed tuple anywhere else is fine, but + * a compressed tuple at P_FIRSTDATAKEY would leave every follower + * below it without a governing anchor. Raise a corruption error. + * + * After the invariant check, materialize any compressed tuple into + * scratch storage so all downstream amcheck probes (natts check, + * scankey building, root-descend search, heap-TID fetch, posting + * checks) operate on a plain IndexTuple that index_getattr and + * friends can deform correctly. + */ + if (prefix_page && offset == P_FIRSTDATAKEY(topaque) && + BTreeTupleIsCompressed(itup)) + ereport(ERROR, + (errcode(ERRCODE_INDEX_CORRUPTED), + errmsg("leftmost data item on prefix-compressed leaf page is not a plain anchor in index \"%s\"", + RelationGetRelationName(state->rel)), + errdetail_internal("Index block=%u offset=%u page lsn=%X/%08X.", + state->targetblock, offset, + LSN_FORMAT_ARGS(state->targetlsn)))); + + if (!prefix_page && BTreeTupleIsCompressed(itup)) + ereport(ERROR, + (errcode(ERRCODE_INDEX_CORRUPTED), + errmsg("prefix-compressed tuple found on leaf page without BTP_HAS_PREFIX_COMPRESSION in index \"%s\"", + RelationGetRelationName(state->rel)), + errdetail_internal("Index block=%u offset=%u page lsn=%X/%08X.", + state->targetblock, offset, + LSN_FORMAT_ARGS(state->targetlsn)))); + + /* + * Always route leaf items through _bt_prefix_reconstruct on prefix- + * compressed pages. For plain tuples (anchors) it returns the raw + * pointer and refreshes the state's current anchor, so subsequent + * compressed followers reconstruct from the right group -- previously + * we only updated state on compressed tuples and would feed stale + * anchor attrs into the bloom filter for the next group's followers. + */ + if (prefix_page) + itup = _bt_prefix_reconstruct(state->rel, &prefix_state, + offset, rbuf); + /* * lp_len should match the IndexTuple reported length exactly, since * lp_len is completely redundant in indexes, and both sources of @@ -1829,6 +1903,12 @@ bt_target_page_check(BtreeCheckState *state) bt_child_highkey_check(state, InvalidOffsetNumber, NULL, topaque->btpo_level); } + + if (prefix_page) + { + _bt_prefix_state_cleanup(&prefix_state); + pfree(rbuf); + } } /* @@ -3542,6 +3622,18 @@ BTreeTupleGetHeapTIDCareful(BtreeCheckState *state, IndexTuple itup, state->targetblock, RelationGetRelationName(state->rel)))); + /* + * Compressed followers carry their heap TID packed into t_tid's upper + * nibble (status) + low 12 bits (offset). Decode into the state's + * scratch buffer so the caller gets a usable ItemPointer that agrees + * with what regular scans would return. + */ + if (BTreeTupleIsCompressed(itup)) + { + BTreeCompressedTupleGetHeapTID(itup, &state->compressed_htid_scratch); + return &state->compressed_htid_scratch; + } + htid = BTreeTupleGetHeapTID(itup); if (!ItemPointerIsValid(htid) && nonpivot) ereport(ERROR, -- 2.55.0