--- /dev/null
+/*-------------------------------------------------------------------------
+ *
+ * verify_gin.c
+ *     Verifies the integrity of GIN indexes based on invariants.
+ *
+ *
+ * GIN index verification checks a number of invariants:
+ *
+ * - consistency: Paths in GIN graph have to contain consistent keys: tuples
+ *   on parent pages consistently include tuples from children pages.
+ *
+ * - graph invariants: Each internal page must have at least one downlink, and
+ *   can reference either only leaf pages or only internal pages.
+ *
+ *
+ * Copyright (c) 2016-2025, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *   contrib/amcheck/verify_gin.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/gin_private.h"
+#include "access/nbtree.h"
+#include "catalog/pg_am.h"
+#include "utils/memutils.h"
+#include "utils/rel.h"
+#include "verify_common.h"
+#include "string.h"
+
+/*
+ * GinScanItem represents one item of depth-first scan of the index.
+ */
+typedef struct GinScanItem
+{
+   int         depth;
+   IndexTuple  parenttup;
+   BlockNumber parentblk;
+   XLogRecPtr  parentlsn;
+   BlockNumber blkno;
+   struct GinScanItem *next;
+} GinScanItem;
+
+/*
+ * GinPostingTreeScanItem represents one item of a depth-first posting tree scan.
+ */
+typedef struct GinPostingTreeScanItem
+{
+   int         depth;
+   ItemPointerData parentkey;
+   BlockNumber parentblk;
+   BlockNumber blkno;
+   struct GinPostingTreeScanItem *next;
+} GinPostingTreeScanItem;
+
+
+PG_FUNCTION_INFO_V1(gin_index_check);
+
+static void gin_check_parent_keys_consistency(Relation rel,
+                                             Relation heaprel,
+                                             void *callback_state, bool readonly);
+static void check_index_page(Relation rel, Buffer buffer, BlockNumber blockNo);
+static IndexTuple gin_refind_parent(Relation rel,
+                                   BlockNumber parentblkno,
+                                   BlockNumber childblkno,
+                                   BufferAccessStrategy strategy);
+static ItemId PageGetItemIdCareful(Relation rel, BlockNumber block, Page page,
+                                  OffsetNumber offset);
+
+/*
+ * gin_index_check(index regclass)
+ *
+ * Verify integrity of GIN index.
+ *
+ * Acquires AccessShareLock on heap & index relations.
+ */
+Datum
+gin_index_check(PG_FUNCTION_ARGS)
+{
+   Oid         indrelid = PG_GETARG_OID(0);
+
+   amcheck_lock_relation_and_check(indrelid,
+                                   GIN_AM_OID,
+                                   gin_check_parent_keys_consistency,
+                                   AccessShareLock,
+                                   NULL);
+
+   PG_RETURN_VOID();
+}
+
+/*
+ * Read item pointers from leaf entry tuple.
+ *
+ * Returns a palloc'd array of ItemPointers. The number of items is returned
+ * in *nitems.
+ */
+static ItemPointer
+ginReadTupleWithoutState(IndexTuple itup, int *nitems)
+{
+   Pointer     ptr = GinGetPosting(itup);
+   int         nipd = GinGetNPosting(itup);
+   ItemPointer ipd;
+   int         ndecoded;
+
+   if (GinItupIsCompressed(itup))
+   {
+       if (nipd > 0)
+       {
+           ipd = ginPostingListDecode((GinPostingList *) ptr, &ndecoded);
+           if (nipd != ndecoded)
+               elog(ERROR, "number of items mismatch in GIN entry tuple, %d in tuple header, %d decoded",
+                    nipd, ndecoded);
+       }
+       else
+           ipd = palloc(0);
+   }
+   else
+   {
+       ipd = (ItemPointer) palloc(sizeof(ItemPointerData) * nipd);
+       memcpy(ipd, ptr, sizeof(ItemPointerData) * nipd);
+   }
+   *nitems = nipd;
+   return ipd;
+}
+
+/*
+ * Scans through a posting tree (given by the root), and verifies that the keys
+ * on a child keys are consistent with the parent.
+ *
+ * Allocates a separate memory context and scans through posting tree graph.
+ */
+static void
+gin_check_posting_tree_parent_keys_consistency(Relation rel, BlockNumber posting_tree_root)
+{
+   BufferAccessStrategy strategy = GetAccessStrategy(BAS_BULKREAD);
+   GinPostingTreeScanItem *stack;
+   MemoryContext mctx;
+   MemoryContext oldcontext;
+
+   int         leafdepth;
+
+   mctx = AllocSetContextCreate(CurrentMemoryContext,
+                                "posting tree check context",
+                                ALLOCSET_DEFAULT_SIZES);
+   oldcontext = MemoryContextSwitchTo(mctx);
+
+   /*
+    * We don't know the height of the tree yet, but as soon as we encounter a
+    * leaf page, we will set 'leafdepth' to its depth.
+    */
+   leafdepth = -1;
+
+   /* Start the scan at the root page */
+   stack = (GinPostingTreeScanItem *) palloc0(sizeof(GinPostingTreeScanItem));
+   stack->depth = 0;
+   ItemPointerSetInvalid(&stack->parentkey);
+   stack->parentblk = InvalidBlockNumber;
+   stack->blkno = posting_tree_root;
+
+   elog(DEBUG3, "processing posting tree at blk %u", posting_tree_root);
+
+   while (stack)
+   {
+       GinPostingTreeScanItem *stack_next;
+       Buffer      buffer;
+       Page        page;
+       OffsetNumber i,
+                   maxoff;
+       BlockNumber rightlink;
+
+       CHECK_FOR_INTERRUPTS();
+
+       buffer = ReadBufferExtended(rel, MAIN_FORKNUM, stack->blkno,
+                                   RBM_NORMAL, strategy);
+       LockBuffer(buffer, GIN_SHARE);
+       page = (Page) BufferGetPage(buffer);
+
+       Assert(GinPageIsData(page));
+
+       /* Check that the tree has the same height in all branches */
+       if (GinPageIsLeaf(page))
+       {
+           ItemPointerData minItem;
+           int         nlist;
+           ItemPointerData *list;
+           char        tidrange_buf[MAXPGPATH];
+
+           ItemPointerSetMin(&minItem);
+
+           elog(DEBUG1, "page blk: %u, type leaf", stack->blkno);
+
+           if (leafdepth == -1)
+               leafdepth = stack->depth;
+           else if (stack->depth != leafdepth)
+               ereport(ERROR,
+                       (errcode(ERRCODE_INDEX_CORRUPTED),
+                        errmsg("index \"%s\": internal pages traversal encountered leaf page unexpectedly on block %u",
+                               RelationGetRelationName(rel), stack->blkno)));
+           list = GinDataLeafPageGetItems(page, &nlist, minItem);
+
+           if (nlist > 0)
+               snprintf(tidrange_buf, sizeof(tidrange_buf),
+                        "%d tids (%u, %u) - (%u, %u)",
+                        nlist,
+                        ItemPointerGetBlockNumberNoCheck(&list[0]),
+                        ItemPointerGetOffsetNumberNoCheck(&list[0]),
+                        ItemPointerGetBlockNumberNoCheck(&list[nlist - 1]),
+                        ItemPointerGetOffsetNumberNoCheck(&list[nlist - 1]));
+           else
+               snprintf(tidrange_buf, sizeof(tidrange_buf), "0 tids");
+
+           if (stack->parentblk != InvalidBlockNumber)
+               elog(DEBUG3, "blk %u: parent %u highkey (%u, %u), %s",
+                    stack->blkno,
+                    stack->parentblk,
+                    ItemPointerGetBlockNumberNoCheck(&stack->parentkey),
+                    ItemPointerGetOffsetNumberNoCheck(&stack->parentkey),
+                    tidrange_buf);
+           else
+               elog(DEBUG3, "blk %u: root leaf, %s",
+                    stack->blkno,
+                    tidrange_buf);
+
+           if (stack->parentblk != InvalidBlockNumber &&
+               ItemPointerGetOffsetNumberNoCheck(&stack->parentkey) != InvalidOffsetNumber &&
+               nlist > 0 && ItemPointerCompare(&stack->parentkey, &list[nlist - 1]) < 0)
+               ereport(ERROR,
+                       (errcode(ERRCODE_INDEX_CORRUPTED),
+                        errmsg("index \"%s\": tid exceeds parent's high key in postingTree leaf on block %u",
+                               RelationGetRelationName(rel), stack->blkno)));
+       }
+       else
+       {
+           LocationIndex pd_lower;
+           ItemPointerData bound;
+           int         lowersize;
+
+           /*
+            * Check that tuples in each page are properly ordered and
+            * consistent with parent high key
+            */
+           maxoff = GinPageGetOpaque(page)->maxoff;
+           rightlink = GinPageGetOpaque(page)->rightlink;
+
+           elog(DEBUG1, "page blk: %u, type data, maxoff %d", stack->blkno, maxoff);
+
+           if (stack->parentblk != InvalidBlockNumber)
+               elog(DEBUG3, "blk %u: internal posting tree page with %u items, parent %u highkey (%u, %u)",
+                    stack->blkno, maxoff, stack->parentblk,
+                    ItemPointerGetBlockNumberNoCheck(&stack->parentkey),
+                    ItemPointerGetOffsetNumberNoCheck(&stack->parentkey));
+           else
+               elog(DEBUG3, "blk %u: root internal posting tree page with %u items",
+                    stack->blkno, maxoff);
+
+           /*
+            * A GIN posting tree internal page stores PostingItems in the
+            * 'lower' part of the page. The 'upper' part is unused. The
+            * number of elements is stored in the opaque area (maxoff). Make
+            * sure the size of the 'lower' part agrees with 'maxoff'
+            *
+            * We didn't set pd_lower until PostgreSQL version 9.4, so if this
+            * check fails, it could also be because the index was
+            * binary-upgraded from an earlier version. That was a long time
+            * ago, though, so let's warn if it doesn't match.
+            */
+           pd_lower = ((PageHeader) page)->pd_lower;
+           lowersize = pd_lower - MAXALIGN(SizeOfPageHeaderData);
+           if ((lowersize - MAXALIGN(sizeof(ItemPointerData))) / sizeof(PostingItem) != maxoff)
+               ereport(ERROR,
+                       (errcode(ERRCODE_INDEX_CORRUPTED),
+                        errmsg("index \"%s\" has unexpected pd_lower %u in posting tree block %u with maxoff %u)",
+                               RelationGetRelationName(rel), pd_lower, stack->blkno, maxoff)));
+
+           /*
+            * Before the PostingItems, there's one ItemPointerData in the
+            * 'lower' part that stores the page's high key.
+            */
+           bound = *GinDataPageGetRightBound(page);
+
+           /*
+            * Gin page right bound has a sane value only when not a highkey on
+            * the rightmost page (at a given level). For the rightmost page does
+            * not store the highkey explicitly, and the value is infinity.
+            */
+           if (ItemPointerIsValid(&stack->parentkey) &&
+               rightlink != InvalidBlockNumber &&
+               !ItemPointerEquals(&stack->parentkey, &bound))
+               ereport(ERROR,
+                       (errcode(ERRCODE_INDEX_CORRUPTED),
+                        errmsg("index \"%s\": posting tree page's high key (%u, %u) doesn't match the downlink on block %u (parent blk %u, key (%u, %u))",
+                               RelationGetRelationName(rel),
+                               ItemPointerGetBlockNumberNoCheck(&bound),
+                               ItemPointerGetOffsetNumberNoCheck(&bound),
+                               stack->blkno, stack->parentblk,
+                               ItemPointerGetBlockNumberNoCheck(&stack->parentkey),
+                               ItemPointerGetOffsetNumberNoCheck(&stack->parentkey))));
+
+           for (i = FirstOffsetNumber; i <= maxoff; i = OffsetNumberNext(i))
+           {
+               GinPostingTreeScanItem *ptr;
+               PostingItem *posting_item = GinDataPageGetPostingItem(page, i);
+
+               /* ItemPointerGetOffsetNumber expects a valid pointer */
+               if (!(i == maxoff &&
+                     rightlink == InvalidBlockNumber))
+                   elog(DEBUG3, "key (%u, %u) -> %u",
+                        ItemPointerGetBlockNumber(&posting_item->key),
+                        ItemPointerGetOffsetNumber(&posting_item->key),
+                        BlockIdGetBlockNumber(&posting_item->child_blkno));
+               else
+                   elog(DEBUG3, "key (%u, %u) -> %u",
+                        0, 0, BlockIdGetBlockNumber(&posting_item->child_blkno));
+
+               if (i == maxoff && rightlink == InvalidBlockNumber)
+               {
+                   /*
+                    * The rightmost item in the tree level has (0, 0) as the
+                    * key
+                    */
+                   if (ItemPointerGetBlockNumberNoCheck(&posting_item->key) != 0 ||
+                       ItemPointerGetOffsetNumberNoCheck(&posting_item->key) != 0)
+                       ereport(ERROR,
+                               (errcode(ERRCODE_INDEX_CORRUPTED),
+                                errmsg("index \"%s\": rightmost posting tree page (blk %u) has unexpected last key (%u, %u)",
+                                       RelationGetRelationName(rel),
+                                       stack->blkno,
+                                       ItemPointerGetBlockNumberNoCheck(&posting_item->key),
+                                       ItemPointerGetOffsetNumberNoCheck(&posting_item->key))));
+               }
+               else if (i != FirstOffsetNumber)
+               {
+                   PostingItem *previous_posting_item = GinDataPageGetPostingItem(page, i - 1);
+
+                   if (ItemPointerCompare(&posting_item->key, &previous_posting_item->key) < 0)
+                       ereport(ERROR,
+                               (errcode(ERRCODE_INDEX_CORRUPTED),
+                                errmsg("index \"%s\" has wrong tuple order in posting tree, block %u, offset %u",
+                                       RelationGetRelationName(rel), stack->blkno, i)));
+               }
+
+               /*
+                * Check if this tuple is consistent with the downlink in the
+                * parent.
+                */
+               if (stack->parentblk != InvalidBlockNumber && i == maxoff &&
+                   ItemPointerCompare(&stack->parentkey, &posting_item->key) < 0)
+                   ereport(ERROR,
+                           (errcode(ERRCODE_INDEX_CORRUPTED),
+                            errmsg("index \"%s\": posting item exceeds parent's high key in postingTree internal page on block %u offset %u",
+                                   RelationGetRelationName(rel),
+                                   stack->blkno, i)));
+
+               /* This is an internal page, recurse into the child. */
+               ptr = (GinPostingTreeScanItem *) palloc(sizeof(GinPostingTreeScanItem));
+               ptr->depth = stack->depth + 1;
+
+               /*
+                * Set rightmost parent key to invalid iterm pointer. Its
+                * value is 'Infinity' and not explicitly stored.
+                */
+               if (rightlink == InvalidBlockNumber)
+                   ItemPointerSetInvalid(&ptr->parentkey);
+               else
+                   ptr->parentkey = posting_item->key;
+
+               ptr->parentblk = stack->blkno;
+               ptr->blkno = BlockIdGetBlockNumber(&posting_item->child_blkno);
+               ptr->next = stack->next;
+               stack->next = ptr;
+           }
+       }
+       LockBuffer(buffer, GIN_UNLOCK);
+       ReleaseBuffer(buffer);
+
+       /* Step to next item in the queue */
+       stack_next = stack->next;
+       pfree(stack);
+       stack = stack_next;
+   }
+
+   MemoryContextSwitchTo(oldcontext);
+   MemoryContextDelete(mctx);
+}
+
+/*
+ * Main entry point for GIN checks.
+ *
+ * Allocates memory context and scans through the whole GIN graph.
+ */
+static void
+gin_check_parent_keys_consistency(Relation rel,
+                                 Relation heaprel,
+                                 void *callback_state,
+                                 bool readonly)
+{
+   BufferAccessStrategy strategy = GetAccessStrategy(BAS_BULKREAD);
+   GinScanItem *stack;
+   MemoryContext mctx;
+   MemoryContext oldcontext;
+   GinState    state;
+   int         leafdepth;
+
+   mctx = AllocSetContextCreate(CurrentMemoryContext,
+                                "amcheck consistency check context",
+                                ALLOCSET_DEFAULT_SIZES);
+   oldcontext = MemoryContextSwitchTo(mctx);
+   initGinState(&state, rel);
+
+   /*
+    * We don't know the height of the tree yet, but as soon as we encounter a
+    * leaf page, we will set 'leafdepth' to its depth.
+    */
+   leafdepth = -1;
+
+   /* Start the scan at the root page */
+   stack = (GinScanItem *) palloc0(sizeof(GinScanItem));
+   stack->depth = 0;
+   stack->parenttup = NULL;
+   stack->parentblk = InvalidBlockNumber;
+   stack->parentlsn = InvalidXLogRecPtr;
+   stack->blkno = GIN_ROOT_BLKNO;
+
+   while (stack)
+   {
+       GinScanItem *stack_next;
+       Buffer      buffer;
+       Page        page;
+       OffsetNumber i,
+                   maxoff,
+                   prev_attnum;
+       XLogRecPtr  lsn;
+       IndexTuple  prev_tuple;
+       BlockNumber rightlink;
+
+       CHECK_FOR_INTERRUPTS();
+
+       buffer = ReadBufferExtended(rel, MAIN_FORKNUM, stack->blkno,
+                                   RBM_NORMAL, strategy);
+       LockBuffer(buffer, GIN_SHARE);
+       page = (Page) BufferGetPage(buffer);
+       lsn = BufferGetLSNAtomic(buffer);
+       maxoff = PageGetMaxOffsetNumber(page);
+       rightlink = GinPageGetOpaque(page)->rightlink;
+
+       /* Do basic sanity checks on the page headers */
+       check_index_page(rel, buffer, stack->blkno);
+
+       elog(DEBUG3, "processing entry tree page at blk %u, maxoff: %u", stack->blkno, maxoff);
+
+       /*
+        * It's possible that the page was split since we looked at the
+        * parent, so that we didn't missed the downlink of the right sibling
+        * when we scanned the parent.  If so, add the right sibling to the
+        * stack now.
+        */
+       if (stack->parenttup != NULL)
+       {
+           GinNullCategory parent_key_category;
+           Datum       parent_key = gintuple_get_key(&state,
+                                                     stack->parenttup,
+                                                     &parent_key_category);
+           ItemId      iid = PageGetItemIdCareful(rel, stack->blkno,
+                                                  page, maxoff);
+           IndexTuple  idxtuple = (IndexTuple) PageGetItem(page, iid);
+           OffsetNumber attnum = gintuple_get_attrnum(&state, idxtuple);
+           GinNullCategory page_max_key_category;
+           Datum       page_max_key = gintuple_get_key(&state, idxtuple, &page_max_key_category);
+
+           if (rightlink != InvalidBlockNumber &&
+               ginCompareEntries(&state, attnum, page_max_key,
+                                 page_max_key_category, parent_key,
+                                 parent_key_category) > 0)
+           {
+               /* split page detected, install right link to the stack */
+               GinScanItem *ptr;
+
+               elog(DEBUG3, "split detected for blk: %u, parent blk: %u", stack->blkno, stack->parentblk);
+
+               ptr = (GinScanItem *) palloc(sizeof(GinScanItem));
+               ptr->depth = stack->depth;
+               ptr->parenttup = CopyIndexTuple(stack->parenttup);
+               ptr->parentblk = stack->parentblk;
+               ptr->parentlsn = stack->parentlsn;
+               ptr->blkno = rightlink;
+               ptr->next = stack->next;
+               stack->next = ptr;
+           }
+       }
+
+       /* Check that the tree has the same height in all branches */
+       if (GinPageIsLeaf(page))
+       {
+           if (leafdepth == -1)
+               leafdepth = stack->depth;
+           else if (stack->depth != leafdepth)
+               ereport(ERROR,
+                       (errcode(ERRCODE_INDEX_CORRUPTED),
+                        errmsg("index \"%s\": internal pages traversal encountered leaf page unexpectedly on block %u",
+                               RelationGetRelationName(rel), stack->blkno)));
+       }
+
+       /*
+        * Check that tuples in each page are properly ordered and consistent
+        * with parent high key
+        */
+       prev_tuple = NULL;
+       prev_attnum = InvalidAttrNumber;
+       for (i = FirstOffsetNumber; i <= maxoff; i = OffsetNumberNext(i))
+       {
+           ItemId      iid = PageGetItemIdCareful(rel, stack->blkno, page, i);
+           IndexTuple  idxtuple = (IndexTuple) PageGetItem(page, iid);
+           OffsetNumber attnum = gintuple_get_attrnum(&state, idxtuple);
+           GinNullCategory prev_key_category;
+           Datum       prev_key;
+           GinNullCategory current_key_category;
+           Datum       current_key;
+
+           if (MAXALIGN(ItemIdGetLength(iid)) != MAXALIGN(IndexTupleSize(idxtuple)))
+               ereport(ERROR,
+                       (errcode(ERRCODE_INDEX_CORRUPTED),
+                        errmsg("index \"%s\" has inconsistent tuple sizes, block %u, offset %u",
+                               RelationGetRelationName(rel), stack->blkno, i)));
+
+           current_key = gintuple_get_key(&state, idxtuple, ¤t_key_category);
+
+           /*
+            * First block is metadata, skip order check. Also, never check
+            * for high key on rightmost page, as this key is not really
+            * stored explicitly.
+            *
+            * Also make sure to not compare entries for different attnums, which
+            * may be stored on the same page.
+            */
+           if (i != FirstOffsetNumber && attnum == prev_attnum && stack->blkno != GIN_ROOT_BLKNO &&
+               !(i == maxoff && rightlink == InvalidBlockNumber))
+           {
+               prev_key = gintuple_get_key(&state, prev_tuple, &prev_key_category);
+               if (ginCompareEntries(&state, attnum, prev_key,
+                                     prev_key_category, current_key,
+                                     current_key_category) >= 0)
+                   ereport(ERROR,
+                           (errcode(ERRCODE_INDEX_CORRUPTED),
+                            errmsg("index \"%s\" has wrong tuple order on entry tree page, block %u, offset %u, rightlink %u",
+                                   RelationGetRelationName(rel), stack->blkno, i, rightlink)));
+           }
+
+           /*
+            * Check if this tuple is consistent with the downlink in the
+            * parent.
+            */
+           if (stack->parenttup &&
+               i == maxoff)
+           {
+               GinNullCategory parent_key_category;
+               Datum       parent_key = gintuple_get_key(&state,
+                                                         stack->parenttup,
+                                                         &parent_key_category);
+
+               if (ginCompareEntries(&state, attnum, current_key,
+                                     current_key_category, parent_key,
+                                     parent_key_category) > 0)
+               {
+                   /*
+                    * There was a discrepancy between parent and child
+                    * tuples. We need to verify it is not a result of
+                    * concurrent call of gistplacetopage(). So, lock parent
+                    * and try to find downlink for current page. It may be
+                    * missing due to concurrent page split, this is OK.
+                    */
+                   pfree(stack->parenttup);
+                   stack->parenttup = gin_refind_parent(rel, stack->parentblk,
+                                                        stack->blkno, strategy);
+
+                   /* We found it - make a final check before failing */
+                   if (!stack->parenttup)
+                       elog(NOTICE, "Unable to find parent tuple for block %u on block %u due to concurrent split",
+                            stack->blkno, stack->parentblk);
+                   else
+                   {
+                       parent_key = gintuple_get_key(&state,
+                                                     stack->parenttup,
+                                                     &parent_key_category);
+
+                       /*
+                        * Check if it is properly adjusted. If succeed,
+                        * procced to the next key.
+                        */
+                       if (ginCompareEntries(&state, attnum, current_key,
+                                             current_key_category, parent_key,
+                                             parent_key_category) > 0)
+                           ereport(ERROR,
+                                   (errcode(ERRCODE_INDEX_CORRUPTED),
+                                    errmsg("index \"%s\" has inconsistent records on page %u offset %u",
+                                           RelationGetRelationName(rel), stack->blkno, i)));
+                   }
+               }
+           }
+
+           /* If this is an internal page, recurse into the child */
+           if (!GinPageIsLeaf(page))
+           {
+               GinScanItem *ptr;
+
+               ptr = (GinScanItem *) palloc(sizeof(GinScanItem));
+               ptr->depth = stack->depth + 1;
+               /* last tuple in layer has no high key */
+               if (i != maxoff && !GinPageGetOpaque(page)->rightlink)
+                   ptr->parenttup = CopyIndexTuple(idxtuple);
+               else
+                   ptr->parenttup = NULL;
+               ptr->parentblk = stack->blkno;
+               ptr->blkno = GinGetDownlink(idxtuple);
+               ptr->parentlsn = lsn;
+               ptr->next = stack->next;
+               stack->next = ptr;
+           }
+           /* If this item is a pointer to a posting tree, recurse into it */
+           else if (GinIsPostingTree(idxtuple))
+           {
+               BlockNumber rootPostingTree = GinGetPostingTree(idxtuple);
+
+               gin_check_posting_tree_parent_keys_consistency(rel, rootPostingTree);
+           }
+           else
+           {
+               ItemPointer ipd;
+               int         nipd;
+
+               ipd = ginReadTupleWithoutState(idxtuple, &nipd);
+
+               for (int j = 0; j < nipd; j++)
+               {
+                   if (!OffsetNumberIsValid(ItemPointerGetOffsetNumber(&ipd[j])))
+                       ereport(ERROR,
+                               (errcode(ERRCODE_INDEX_CORRUPTED),
+                                errmsg("index \"%s\": posting list contains invalid heap pointer on block %u",
+                                       RelationGetRelationName(rel), stack->blkno)));
+               }
+               pfree(ipd);
+           }
+
+           prev_tuple = CopyIndexTuple(idxtuple);
+           prev_attnum = attnum;
+       }
+
+       LockBuffer(buffer, GIN_UNLOCK);
+       ReleaseBuffer(buffer);
+
+       /* Step to next item in the queue */
+       stack_next = stack->next;
+       if (stack->parenttup)
+           pfree(stack->parenttup);
+       pfree(stack);
+       stack = stack_next;
+   }
+
+   MemoryContextSwitchTo(oldcontext);
+   MemoryContextDelete(mctx);
+}
+
+/*
+ * Verify that a freshly-read page looks sane.
+ */
+static void
+check_index_page(Relation rel, Buffer buffer, BlockNumber blockNo)
+{
+   Page        page = BufferGetPage(buffer);
+
+   /*
+    * ReadBuffer verifies that every newly-read page passes
+    * PageHeaderIsValid, which means it either contains a reasonably sane
+    * page header or is all-zero.  We have to defend against the all-zero
+    * case, however.
+    */
+   if (PageIsNew(page))
+       ereport(ERROR,
+               (errcode(ERRCODE_INDEX_CORRUPTED),
+                errmsg("index \"%s\" contains unexpected zero page at block %u",
+                       RelationGetRelationName(rel),
+                       BufferGetBlockNumber(buffer)),
+                errhint("Please REINDEX it.")));
+
+   /*
+    * Additionally check that the special area looks sane.
+    */
+   if (PageGetSpecialSize(page) != MAXALIGN(sizeof(GinPageOpaqueData)))
+       ereport(ERROR,
+               (errcode(ERRCODE_INDEX_CORRUPTED),
+                errmsg("index \"%s\" contains corrupted page at block %u",
+                       RelationGetRelationName(rel),
+                       BufferGetBlockNumber(buffer)),
+                errhint("Please REINDEX it.")));
+
+   if (GinPageIsDeleted(page))
+   {
+       if (!GinPageIsLeaf(page))
+           ereport(ERROR,
+                   (errcode(ERRCODE_INDEX_CORRUPTED),
+                    errmsg("index \"%s\" has deleted internal page %d",
+                           RelationGetRelationName(rel), blockNo)));
+       if (PageGetMaxOffsetNumber(page) > InvalidOffsetNumber)
+           ereport(ERROR,
+                   (errcode(ERRCODE_INDEX_CORRUPTED),
+                    errmsg("index \"%s\" has deleted page %d with tuples",
+                           RelationGetRelationName(rel), blockNo)));
+   }
+   else if (PageGetMaxOffsetNumber(page) > MaxIndexTuplesPerPage)
+       ereport(ERROR,
+               (errcode(ERRCODE_INDEX_CORRUPTED),
+                errmsg("index \"%s\" has page %d with exceeding count of tuples",
+                       RelationGetRelationName(rel), blockNo)));
+}
+
+/*
+ * Try to re-find downlink pointing to 'blkno', in 'parentblkno'.
+ *
+ * If found, returns a palloc'd copy of the downlink tuple. Otherwise,
+ * returns NULL.
+ */
+static IndexTuple
+gin_refind_parent(Relation rel, BlockNumber parentblkno,
+                 BlockNumber childblkno, BufferAccessStrategy strategy)
+{
+   Buffer      parentbuf;
+   Page        parentpage;
+   OffsetNumber o,
+               parent_maxoff;
+   IndexTuple  result = NULL;
+
+   parentbuf = ReadBufferExtended(rel, MAIN_FORKNUM, parentblkno, RBM_NORMAL,
+                                  strategy);
+
+   LockBuffer(parentbuf, GIN_SHARE);
+   parentpage = BufferGetPage(parentbuf);
+
+   if (GinPageIsLeaf(parentpage))
+   {
+       UnlockReleaseBuffer(parentbuf);
+       return result;
+   }
+
+   parent_maxoff = PageGetMaxOffsetNumber(parentpage);
+   for (o = FirstOffsetNumber; o <= parent_maxoff; o = OffsetNumberNext(o))
+   {
+       ItemId      p_iid = PageGetItemIdCareful(rel, parentblkno, parentpage, o);
+       IndexTuple  itup = (IndexTuple) PageGetItem(parentpage, p_iid);
+
+       if (ItemPointerGetBlockNumber(&(itup->t_tid)) == childblkno)
+       {
+           /* Found it! Make copy and return it */
+           result = CopyIndexTuple(itup);
+           break;
+       }
+   }
+
+   UnlockReleaseBuffer(parentbuf);
+
+   return result;
+}
+
+static ItemId
+PageGetItemIdCareful(Relation rel, BlockNumber block, Page page,
+                    OffsetNumber offset)
+{
+   ItemId      itemid = PageGetItemId(page, offset);
+
+   if (ItemIdGetOffset(itemid) + ItemIdGetLength(itemid) >
+       BLCKSZ - MAXALIGN(sizeof(GinPageOpaqueData)))
+       ereport(ERROR,
+               (errcode(ERRCODE_INDEX_CORRUPTED),
+                errmsg("line pointer points past end of tuple space in index \"%s\"",
+                       RelationGetRelationName(rel)),
+                errdetail_internal("Index tid=(%u,%u) lp_off=%u, lp_len=%u lp_flags=%u.",
+                                   block, offset, ItemIdGetOffset(itemid),
+                                   ItemIdGetLength(itemid),
+                                   ItemIdGetFlags(itemid))));
+
+   /*
+    * Verify that line pointer isn't LP_REDIRECT or LP_UNUSED or LP_DEAD,
+    * since GIN never uses all three.  Verify that line pointer has storage,
+    * too.
+    */
+   if (ItemIdIsRedirected(itemid) || !ItemIdIsUsed(itemid) ||
+       ItemIdIsDead(itemid) || ItemIdGetLength(itemid) == 0)
+       ereport(ERROR,
+               (errcode(ERRCODE_INDEX_CORRUPTED),
+                errmsg("invalid line pointer storage in index \"%s\"",
+                       RelationGetRelationName(rel)),
+                errdetail_internal("Index tid=(%u,%u) lp_off=%u, lp_len=%u lp_flags=%u.",
+                                   block, offset, ItemIdGetOffset(itemid),
+                                   ItemIdGetLength(itemid),
+                                   ItemIdGetFlags(itemid))));
+
+   return itemid;
+}