summaryrefslogtreecommitdiffstats
path: root/contrib/pageinspect
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 13:44:03 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 13:44:03 +0000
commit293913568e6a7a86fd1479e1cff8e2ecb58d6568 (patch)
treefc3b469a3ec5ab71b36ea97cc7aaddb838423a0c /contrib/pageinspect
parentInitial commit. (diff)
downloadpostgresql-16-293913568e6a7a86fd1479e1cff8e2ecb58d6568.tar.xz
postgresql-16-293913568e6a7a86fd1479e1cff8e2ecb58d6568.zip
Adding upstream version 16.2.upstream/16.2
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--contrib/pageinspect/.gitignore4
-rw-r--r--contrib/pageinspect/Makefile36
-rw-r--r--contrib/pageinspect/brinfuncs.c422
-rw-r--r--contrib/pageinspect/btreefuncs.c939
-rw-r--r--contrib/pageinspect/expected/brin.out92
-rw-r--r--contrib/pageinspect/expected/btree.out221
-rw-r--r--contrib/pageinspect/expected/checksum.out40
-rw-r--r--contrib/pageinspect/expected/checksum_1.out40
-rw-r--r--contrib/pageinspect/expected/gin.out71
-rw-r--r--contrib/pageinspect/expected/gist.out133
-rw-r--r--contrib/pageinspect/expected/hash.out210
-rw-r--r--contrib/pageinspect/expected/oldextversions.out56
-rw-r--r--contrib/pageinspect/expected/page.out241
-rw-r--r--contrib/pageinspect/fsmfuncs.c65
-rw-r--r--contrib/pageinspect/ginfuncs.c285
-rw-r--r--contrib/pageinspect/gistfuncs.c369
-rw-r--r--contrib/pageinspect/hashfuncs.c570
-rw-r--r--contrib/pageinspect/heapfuncs.c618
-rw-r--r--contrib/pageinspect/meson.build60
-rw-r--r--contrib/pageinspect/pageinspect--1.0--1.1.sql18
-rw-r--r--contrib/pageinspect/pageinspect--1.1--1.2.sql18
-rw-r--r--contrib/pageinspect/pageinspect--1.10--1.11.sql28
-rw-r--r--contrib/pageinspect/pageinspect--1.11--1.12.sql40
-rw-r--r--contrib/pageinspect/pageinspect--1.2--1.3.sql82
-rw-r--r--contrib/pageinspect/pageinspect--1.3--1.4.sql118
-rw-r--r--contrib/pageinspect/pageinspect--1.4--1.5.sql24
-rw-r--r--contrib/pageinspect/pageinspect--1.5--1.6.sql99
-rw-r--r--contrib/pageinspect/pageinspect--1.5.sql279
-rw-r--r--contrib/pageinspect/pageinspect--1.6--1.7.sql26
-rw-r--r--contrib/pageinspect/pageinspect--1.7--1.8.sql69
-rw-r--r--contrib/pageinspect/pageinspect--1.8--1.9.sql137
-rw-r--r--contrib/pageinspect/pageinspect--1.9--1.10.sql21
-rw-r--r--contrib/pageinspect/pageinspect.control5
-rw-r--r--contrib/pageinspect/pageinspect.h30
-rw-r--r--contrib/pageinspect/rawpage.c376
-rw-r--r--contrib/pageinspect/sql/brin.sql39
-rw-r--r--contrib/pageinspect/sql/btree.sql62
-rw-r--r--contrib/pageinspect/sql/checksum.sql31
-rw-r--r--contrib/pageinspect/sql/gin.sql41
-rw-r--r--contrib/pageinspect/sql/gist.sql69
-rw-r--r--contrib/pageinspect/sql/hash.sql113
-rw-r--r--contrib/pageinspect/sql/oldextversions.sql26
-rw-r--r--contrib/pageinspect/sql/page.sql100
43 files changed, 6323 insertions, 0 deletions
diff --git a/contrib/pageinspect/.gitignore b/contrib/pageinspect/.gitignore
new file mode 100644
index 0000000..5dcb3ff
--- /dev/null
+++ b/contrib/pageinspect/.gitignore
@@ -0,0 +1,4 @@
+# Generated subdirectories
+/log/
+/results/
+/tmp_check/
diff --git a/contrib/pageinspect/Makefile b/contrib/pageinspect/Makefile
new file mode 100644
index 0000000..95e030b
--- /dev/null
+++ b/contrib/pageinspect/Makefile
@@ -0,0 +1,36 @@
+# contrib/pageinspect/Makefile
+
+MODULE_big = pageinspect
+OBJS = \
+ $(WIN32RES) \
+ brinfuncs.o \
+ btreefuncs.o \
+ fsmfuncs.o \
+ ginfuncs.o \
+ gistfuncs.o \
+ hashfuncs.o \
+ heapfuncs.o \
+ rawpage.o
+
+EXTENSION = pageinspect
+DATA = pageinspect--1.11--1.12.sql pageinspect--1.10--1.11.sql \
+ pageinspect--1.9--1.10.sql pageinspect--1.8--1.9.sql \
+ pageinspect--1.7--1.8.sql pageinspect--1.6--1.7.sql \
+ pageinspect--1.5.sql pageinspect--1.5--1.6.sql \
+ pageinspect--1.4--1.5.sql pageinspect--1.3--1.4.sql \
+ pageinspect--1.2--1.3.sql pageinspect--1.1--1.2.sql \
+ pageinspect--1.0--1.1.sql
+PGFILEDESC = "pageinspect - functions to inspect contents of database pages"
+
+REGRESS = page btree brin gin gist hash checksum oldextversions
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = contrib/pageinspect
+top_builddir = ../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/contrib/pageinspect/brinfuncs.c b/contrib/pageinspect/brinfuncs.c
new file mode 100644
index 0000000..a781f26
--- /dev/null
+++ b/contrib/pageinspect/brinfuncs.c
@@ -0,0 +1,422 @@
+/*
+ * brinfuncs.c
+ * Functions to investigate BRIN indexes
+ *
+ * Copyright (c) 2014-2023, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pageinspect/brinfuncs.c
+ */
+#include "postgres.h"
+
+#include "access/brin.h"
+#include "access/brin_internal.h"
+#include "access/brin_page.h"
+#include "access/brin_revmap.h"
+#include "access/brin_tuple.h"
+#include "access/htup_details.h"
+#include "catalog/index.h"
+#include "catalog/pg_am_d.h"
+#include "catalog/pg_type.h"
+#include "funcapi.h"
+#include "lib/stringinfo.h"
+#include "miscadmin.h"
+#include "pageinspect.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+
+PG_FUNCTION_INFO_V1(brin_page_type);
+PG_FUNCTION_INFO_V1(brin_page_items);
+PG_FUNCTION_INFO_V1(brin_metapage_info);
+PG_FUNCTION_INFO_V1(brin_revmap_data);
+
+#define IS_BRIN(r) ((r)->rd_rel->relam == BRIN_AM_OID)
+
+typedef struct brin_column_state
+{
+ int nstored;
+ FmgrInfo outputFn[FLEXIBLE_ARRAY_MEMBER];
+} brin_column_state;
+
+
+static Page verify_brin_page(bytea *raw_page, uint16 type,
+ const char *strtype);
+
+Datum
+brin_page_type(PG_FUNCTION_ARGS)
+{
+ bytea *raw_page = PG_GETARG_BYTEA_P(0);
+ Page page;
+ char *type;
+
+ if (!superuser())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions")));
+
+ page = get_page_from_raw(raw_page);
+
+ if (PageIsNew(page))
+ PG_RETURN_NULL();
+
+ /* verify the special space has the expected size */
+ if (PageGetSpecialSize(page) != MAXALIGN(sizeof(BrinSpecialSpace)))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("input page is not a valid %s page", "BRIN"),
+ errdetail("Expected special size %d, got %d.",
+ (int) MAXALIGN(sizeof(BrinSpecialSpace)),
+ (int) PageGetSpecialSize(page))));
+
+ switch (BrinPageType(page))
+ {
+ case BRIN_PAGETYPE_META:
+ type = "meta";
+ break;
+ case BRIN_PAGETYPE_REVMAP:
+ type = "revmap";
+ break;
+ case BRIN_PAGETYPE_REGULAR:
+ type = "regular";
+ break;
+ default:
+ type = psprintf("unknown (%02x)", BrinPageType(page));
+ break;
+ }
+
+ PG_RETURN_TEXT_P(cstring_to_text(type));
+}
+
+/*
+ * Verify that the given bytea contains a BRIN page of the indicated page
+ * type, or die in the attempt. A pointer to the page is returned.
+ */
+static Page
+verify_brin_page(bytea *raw_page, uint16 type, const char *strtype)
+{
+ Page page = get_page_from_raw(raw_page);
+
+ if (PageIsNew(page))
+ return page;
+
+ /* verify the special space has the expected size */
+ if (PageGetSpecialSize(page) != MAXALIGN(sizeof(BrinSpecialSpace)))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("input page is not a valid %s page", "BRIN"),
+ errdetail("Expected special size %d, got %d.",
+ (int) MAXALIGN(sizeof(BrinSpecialSpace)),
+ (int) PageGetSpecialSize(page))));
+
+ /* verify the special space says this page is what we want */
+ if (BrinPageType(page) != type)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("page is not a BRIN page of type \"%s\"", strtype),
+ errdetail("Expected special type %08x, got %08x.",
+ type, BrinPageType(page))));
+
+ return page;
+}
+
+
+/*
+ * Extract all item values from a BRIN index page
+ *
+ * Usage: SELECT * FROM brin_page_items(get_raw_page('idx', 1), 'idx'::regclass);
+ */
+Datum
+brin_page_items(PG_FUNCTION_ARGS)
+{
+ bytea *raw_page = PG_GETARG_BYTEA_P(0);
+ Oid indexRelid = PG_GETARG_OID(1);
+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ Relation indexRel;
+ brin_column_state **columns;
+ BrinDesc *bdesc;
+ BrinMemTuple *dtup;
+ Page page;
+ OffsetNumber offset;
+ AttrNumber attno;
+ bool unusedItem;
+
+ if (!superuser())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions")));
+
+ InitMaterializedSRF(fcinfo, 0);
+
+ indexRel = index_open(indexRelid, AccessShareLock);
+
+ if (!IS_BRIN(indexRel))
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is not a %s index",
+ RelationGetRelationName(indexRel), "BRIN")));
+
+ bdesc = brin_build_desc(indexRel);
+
+ /* minimally verify the page we got */
+ page = verify_brin_page(raw_page, BRIN_PAGETYPE_REGULAR, "regular");
+
+ if (PageIsNew(page))
+ {
+ brin_free_desc(bdesc);
+ index_close(indexRel, AccessShareLock);
+ PG_RETURN_NULL();
+ }
+
+ /*
+ * Initialize output functions for all indexed datatypes; simplifies
+ * calling them later.
+ */
+ columns = palloc(sizeof(brin_column_state *) * RelationGetDescr(indexRel)->natts);
+ for (attno = 1; attno <= bdesc->bd_tupdesc->natts; attno++)
+ {
+ Oid output;
+ bool isVarlena;
+ BrinOpcInfo *opcinfo;
+ int i;
+ brin_column_state *column;
+
+ opcinfo = bdesc->bd_info[attno - 1];
+ column = palloc(offsetof(brin_column_state, outputFn) +
+ sizeof(FmgrInfo) * opcinfo->oi_nstored);
+
+ column->nstored = opcinfo->oi_nstored;
+ for (i = 0; i < opcinfo->oi_nstored; i++)
+ {
+ getTypeOutputInfo(opcinfo->oi_typcache[i]->type_id, &output, &isVarlena);
+ fmgr_info(output, &column->outputFn[i]);
+ }
+
+ columns[attno - 1] = column;
+ }
+
+ offset = FirstOffsetNumber;
+ unusedItem = false;
+ dtup = NULL;
+ for (;;)
+ {
+ Datum values[8];
+ bool nulls[8] = {0};
+
+ /*
+ * This loop is called once for every attribute of every tuple in the
+ * page. At the start of a tuple, we get a NULL dtup; that's our
+ * signal for obtaining and decoding the next one. If that's not the
+ * case, we output the next attribute.
+ */
+ if (dtup == NULL)
+ {
+ ItemId itemId;
+
+ /* verify item status: if there's no data, we can't decode */
+ itemId = PageGetItemId(page, offset);
+ if (ItemIdIsUsed(itemId))
+ {
+ dtup = brin_deform_tuple(bdesc,
+ (BrinTuple *) PageGetItem(page, itemId),
+ NULL);
+ attno = 1;
+ unusedItem = false;
+ }
+ else
+ unusedItem = true;
+ }
+ else
+ attno++;
+
+ if (unusedItem)
+ {
+ values[0] = UInt16GetDatum(offset);
+ nulls[1] = true;
+ nulls[2] = true;
+ nulls[3] = true;
+ nulls[4] = true;
+ nulls[5] = true;
+ nulls[6] = true;
+ nulls[7] = true;
+ }
+ else
+ {
+ int att = attno - 1;
+
+ values[0] = UInt16GetDatum(offset);
+ switch (TupleDescAttr(rsinfo->setDesc, 1)->atttypid)
+ {
+ case INT8OID:
+ values[1] = Int64GetDatum((int64) dtup->bt_blkno);
+ break;
+ case INT4OID:
+ /* support for old extension version */
+ values[1] = UInt32GetDatum(dtup->bt_blkno);
+ break;
+ default:
+ elog(ERROR, "incorrect output types");
+ }
+ values[2] = UInt16GetDatum(attno);
+ values[3] = BoolGetDatum(dtup->bt_columns[att].bv_allnulls);
+ values[4] = BoolGetDatum(dtup->bt_columns[att].bv_hasnulls);
+ values[5] = BoolGetDatum(dtup->bt_placeholder);
+ values[6] = BoolGetDatum(dtup->bt_empty_range);
+ if (!dtup->bt_columns[att].bv_allnulls)
+ {
+ BrinValues *bvalues = &dtup->bt_columns[att];
+ StringInfoData s;
+ bool first;
+ int i;
+
+ initStringInfo(&s);
+ appendStringInfoChar(&s, '{');
+
+ first = true;
+ for (i = 0; i < columns[att]->nstored; i++)
+ {
+ char *val;
+
+ if (!first)
+ appendStringInfoString(&s, " .. ");
+ first = false;
+ val = OutputFunctionCall(&columns[att]->outputFn[i],
+ bvalues->bv_values[i]);
+ appendStringInfoString(&s, val);
+ pfree(val);
+ }
+ appendStringInfoChar(&s, '}');
+
+ values[7] = CStringGetTextDatum(s.data);
+ pfree(s.data);
+ }
+ else
+ {
+ nulls[7] = true;
+ }
+ }
+
+ tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
+
+ /*
+ * If the item was unused, jump straight to the next one; otherwise,
+ * the only cleanup needed here is to set our signal to go to the next
+ * tuple in the following iteration, by freeing the current one.
+ */
+ if (unusedItem)
+ offset = OffsetNumberNext(offset);
+ else if (attno >= bdesc->bd_tupdesc->natts)
+ {
+ pfree(dtup);
+ dtup = NULL;
+ offset = OffsetNumberNext(offset);
+ }
+
+ /*
+ * If we're beyond the end of the page, we're done.
+ */
+ if (offset > PageGetMaxOffsetNumber(page))
+ break;
+ }
+
+ brin_free_desc(bdesc);
+ index_close(indexRel, AccessShareLock);
+
+ return (Datum) 0;
+}
+
+Datum
+brin_metapage_info(PG_FUNCTION_ARGS)
+{
+ bytea *raw_page = PG_GETARG_BYTEA_P(0);
+ Page page;
+ BrinMetaPageData *meta;
+ TupleDesc tupdesc;
+ Datum values[4];
+ bool nulls[4] = {0};
+ HeapTuple htup;
+
+ if (!superuser())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions")));
+
+ page = verify_brin_page(raw_page, BRIN_PAGETYPE_META, "metapage");
+
+ if (PageIsNew(page))
+ PG_RETURN_NULL();
+
+ /* Build a tuple descriptor for our result type */
+ if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+ elog(ERROR, "return type must be a row type");
+ tupdesc = BlessTupleDesc(tupdesc);
+
+ /* Extract values from the metapage */
+ meta = (BrinMetaPageData *) PageGetContents(page);
+ values[0] = CStringGetTextDatum(psprintf("0x%08X", meta->brinMagic));
+ values[1] = Int32GetDatum(meta->brinVersion);
+ values[2] = Int32GetDatum(meta->pagesPerRange);
+ values[3] = Int64GetDatum(meta->lastRevmapPage);
+
+ htup = heap_form_tuple(tupdesc, values, nulls);
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(htup));
+}
+
+/*
+ * Return the TID array stored in a BRIN revmap page
+ */
+Datum
+brin_revmap_data(PG_FUNCTION_ARGS)
+{
+ struct
+ {
+ ItemPointerData *tids;
+ int idx;
+ } *state;
+ FuncCallContext *fctx;
+
+ if (!superuser())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions")));
+
+ if (SRF_IS_FIRSTCALL())
+ {
+ bytea *raw_page = PG_GETARG_BYTEA_P(0);
+ MemoryContext mctx;
+ Page page;
+
+ /* create a function context for cross-call persistence */
+ fctx = SRF_FIRSTCALL_INIT();
+
+ /* switch to memory context appropriate for multiple function calls */
+ mctx = MemoryContextSwitchTo(fctx->multi_call_memory_ctx);
+
+ /* minimally verify the page we got */
+ page = verify_brin_page(raw_page, BRIN_PAGETYPE_REVMAP, "revmap");
+
+ if (PageIsNew(page))
+ {
+ MemoryContextSwitchTo(mctx);
+ PG_RETURN_NULL();
+ }
+
+ state = palloc(sizeof(*state));
+ state->tids = ((RevmapContents *) PageGetContents(page))->rm_tids;
+ state->idx = 0;
+
+ fctx->user_fctx = state;
+
+ MemoryContextSwitchTo(mctx);
+ }
+
+ fctx = SRF_PERCALL_SETUP();
+ state = fctx->user_fctx;
+
+ if (state->idx < REVMAP_PAGE_MAXITEMS)
+ SRF_RETURN_NEXT(fctx, PointerGetDatum(&state->tids[state->idx++]));
+
+ SRF_RETURN_DONE(fctx);
+}
diff --git a/contrib/pageinspect/btreefuncs.c b/contrib/pageinspect/btreefuncs.c
new file mode 100644
index 0000000..9cdc8e1
--- /dev/null
+++ b/contrib/pageinspect/btreefuncs.c
@@ -0,0 +1,939 @@
+/*
+ * contrib/pageinspect/btreefuncs.c
+ *
+ *
+ * btreefuncs.c
+ *
+ * Copyright (c) 2006 Satoshi Nagayasu <nagayasus@nttdata.co.jp>
+ *
+ * Permission to use, copy, modify, and distribute this software and
+ * its documentation for any purpose, without fee, and without a
+ * written agreement is hereby granted, provided that the above
+ * copyright notice and this paragraph and the following two
+ * paragraphs appear in all copies.
+ *
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT,
+ * INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING
+ * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS
+ * DOCUMENTATION, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS
+ * IS" BASIS, AND THE AUTHOR HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE,
+ * SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+ */
+
+#include "postgres.h"
+
+#include "access/nbtree.h"
+#include "access/relation.h"
+#include "catalog/namespace.h"
+#include "catalog/pg_am.h"
+#include "catalog/pg_type.h"
+#include "funcapi.h"
+#include "miscadmin.h"
+#include "pageinspect.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/rel.h"
+#include "utils/varlena.h"
+
+PG_FUNCTION_INFO_V1(bt_metap);
+PG_FUNCTION_INFO_V1(bt_page_items_1_9);
+PG_FUNCTION_INFO_V1(bt_page_items);
+PG_FUNCTION_INFO_V1(bt_page_items_bytea);
+PG_FUNCTION_INFO_V1(bt_page_stats_1_9);
+PG_FUNCTION_INFO_V1(bt_page_stats);
+PG_FUNCTION_INFO_V1(bt_multi_page_stats);
+
+#define IS_INDEX(r) ((r)->rd_rel->relkind == RELKIND_INDEX)
+#define IS_BTREE(r) ((r)->rd_rel->relam == BTREE_AM_OID)
+
+/* ------------------------------------------------
+ * structure for single btree page statistics
+ * ------------------------------------------------
+ */
+typedef struct BTPageStat
+{
+ uint32 blkno;
+ uint32 live_items;
+ uint32 dead_items;
+ uint32 page_size;
+ uint32 max_avail;
+ uint32 free_size;
+ uint32 avg_item_size;
+ char type;
+
+ /* opaque data */
+ BlockNumber btpo_prev;
+ BlockNumber btpo_next;
+ uint32 btpo_level;
+ uint16 btpo_flags;
+ BTCycleId btpo_cycleid;
+} BTPageStat;
+
+/*
+ * cross-call data structure for SRF for page stats
+ */
+typedef struct ua_page_stats
+{
+ Oid relid;
+ int64 blkno;
+ int64 blk_count;
+ bool allpages;
+} ua_page_stats;
+
+/*
+ * cross-call data structure for SRF for page items
+ */
+typedef struct ua_page_items
+{
+ Page page;
+ OffsetNumber offset;
+ bool leafpage;
+ bool rightmost;
+ TupleDesc tupd;
+} ua_page_items;
+
+
+/* -------------------------------------------------
+ * GetBTPageStatistics()
+ *
+ * Collect statistics of single b-tree page
+ * -------------------------------------------------
+ */
+static void
+GetBTPageStatistics(BlockNumber blkno, Buffer buffer, BTPageStat *stat)
+{
+ Page page = BufferGetPage(buffer);
+ PageHeader phdr = (PageHeader) page;
+ OffsetNumber maxoff = PageGetMaxOffsetNumber(page);
+ BTPageOpaque opaque = BTPageGetOpaque(page);
+ int item_size = 0;
+ int off;
+
+ stat->blkno = blkno;
+
+ stat->max_avail = BLCKSZ - (BLCKSZ - phdr->pd_special + SizeOfPageHeaderData);
+
+ stat->dead_items = stat->live_items = 0;
+
+ stat->page_size = PageGetPageSize(page);
+
+ /* page type (flags) */
+ if (P_ISDELETED(opaque))
+ {
+ /* We divide deleted pages into leaf ('d') or internal ('D') */
+ if (P_ISLEAF(opaque) || !P_HAS_FULLXID(opaque))
+ stat->type = 'd';
+ else
+ stat->type = 'D';
+
+ /*
+ * Report safexid in a deleted page.
+ *
+ * Handle pg_upgrade'd deleted pages that used the previous safexid
+ * representation in btpo_level field (this used to be a union type
+ * called "bpto").
+ */
+ if (P_HAS_FULLXID(opaque))
+ {
+ FullTransactionId safexid = BTPageGetDeleteXid(page);
+
+ elog(DEBUG2, "deleted page from block %u has safexid %u:%u",
+ blkno, EpochFromFullTransactionId(safexid),
+ XidFromFullTransactionId(safexid));
+ }
+ else
+ elog(DEBUG2, "deleted page from block %u has safexid %u",
+ blkno, opaque->btpo_level);
+
+ /* Don't interpret BTDeletedPageData as index tuples */
+ maxoff = InvalidOffsetNumber;
+ }
+ else if (P_IGNORE(opaque))
+ stat->type = 'e';
+ else if (P_ISLEAF(opaque))
+ stat->type = 'l';
+ else if (P_ISROOT(opaque))
+ stat->type = 'r';
+ else
+ stat->type = 'i';
+
+ /* btpage opaque data */
+ stat->btpo_prev = opaque->btpo_prev;
+ stat->btpo_next = opaque->btpo_next;
+ stat->btpo_level = opaque->btpo_level;
+ stat->btpo_flags = opaque->btpo_flags;
+ stat->btpo_cycleid = opaque->btpo_cycleid;
+
+ /* count live and dead tuples, and free space */
+ for (off = FirstOffsetNumber; off <= maxoff; off++)
+ {
+ IndexTuple itup;
+
+ ItemId id = PageGetItemId(page, off);
+
+ itup = (IndexTuple) PageGetItem(page, id);
+
+ item_size += IndexTupleSize(itup);
+
+ if (!ItemIdIsDead(id))
+ stat->live_items++;
+ else
+ stat->dead_items++;
+ }
+ stat->free_size = PageGetFreeSpace(page);
+
+ if ((stat->live_items + stat->dead_items) > 0)
+ stat->avg_item_size = item_size / (stat->live_items + stat->dead_items);
+ else
+ stat->avg_item_size = 0;
+}
+
+/* -----------------------------------------------
+ * check_relation_block_range()
+ *
+ * Verify that a block number (given as int64) is valid for the relation.
+ * -----------------------------------------------
+ */
+static void
+check_relation_block_range(Relation rel, int64 blkno)
+{
+ /* Ensure we can cast to BlockNumber */
+ if (blkno < 0 || blkno > MaxBlockNumber)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("invalid block number %lld",
+ (long long) blkno)));
+
+ if ((BlockNumber) (blkno) >= RelationGetNumberOfBlocks(rel))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("block number %lld is out of range",
+ (long long) blkno)));
+}
+
+/* -----------------------------------------------
+ * bt_index_block_validate()
+ *
+ * Validate index type is btree and block number
+ * is valid (and not the metapage).
+ * -----------------------------------------------
+ */
+static void
+bt_index_block_validate(Relation rel, int64 blkno)
+{
+ if (!IS_INDEX(rel) || !IS_BTREE(rel))
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is not a %s index",
+ RelationGetRelationName(rel), "btree")));
+
+ /*
+ * Reject attempts to read non-local temporary relations; we would be
+ * likely to get wrong data since we have no visibility into the owning
+ * session's local buffers.
+ */
+ if (RELATION_IS_OTHER_TEMP(rel))
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot access temporary tables of other sessions")));
+
+ if (blkno == 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("block 0 is a meta page")));
+
+ check_relation_block_range(rel, blkno);
+}
+
+/* -----------------------------------------------
+ * bt_page_stats()
+ *
+ * Usage: SELECT * FROM bt_page_stats('t1_pkey', 1);
+ * Arguments are index relation name and block number
+ * -----------------------------------------------
+ */
+static Datum
+bt_page_stats_internal(PG_FUNCTION_ARGS, enum pageinspect_version ext_version)
+{
+ text *relname = PG_GETARG_TEXT_PP(0);
+ int64 blkno = (ext_version == PAGEINSPECT_V1_8 ? PG_GETARG_UINT32(1) : PG_GETARG_INT64(1));
+ Buffer buffer;
+ Relation rel;
+ RangeVar *relrv;
+ Datum result;
+ HeapTuple tuple;
+ TupleDesc tupleDesc;
+ int j;
+ char *values[11];
+ BTPageStat stat;
+
+ if (!superuser())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use pageinspect functions")));
+
+ relrv = makeRangeVarFromNameList(textToQualifiedNameList(relname));
+ rel = relation_openrv(relrv, AccessShareLock);
+
+ bt_index_block_validate(rel, blkno);
+
+ buffer = ReadBuffer(rel, blkno);
+ LockBuffer(buffer, BUFFER_LOCK_SHARE);
+
+ /* keep compiler quiet */
+ stat.btpo_prev = stat.btpo_next = InvalidBlockNumber;
+ stat.btpo_flags = stat.free_size = stat.avg_item_size = 0;
+
+ GetBTPageStatistics(blkno, buffer, &stat);
+
+ UnlockReleaseBuffer(buffer);
+ relation_close(rel, AccessShareLock);
+
+ /* Build a tuple descriptor for our result type */
+ if (get_call_result_type(fcinfo, NULL, &tupleDesc) != TYPEFUNC_COMPOSITE)
+ elog(ERROR, "return type must be a row type");
+
+ j = 0;
+ values[j++] = psprintf("%u", stat.blkno);
+ values[j++] = psprintf("%c", stat.type);
+ values[j++] = psprintf("%u", stat.live_items);
+ values[j++] = psprintf("%u", stat.dead_items);
+ values[j++] = psprintf("%u", stat.avg_item_size);
+ values[j++] = psprintf("%u", stat.page_size);
+ values[j++] = psprintf("%u", stat.free_size);
+ values[j++] = psprintf("%u", stat.btpo_prev);
+ values[j++] = psprintf("%u", stat.btpo_next);
+ values[j++] = psprintf("%u", stat.btpo_level);
+ values[j++] = psprintf("%d", stat.btpo_flags);
+
+ tuple = BuildTupleFromCStrings(TupleDescGetAttInMetadata(tupleDesc),
+ values);
+
+ result = HeapTupleGetDatum(tuple);
+
+ PG_RETURN_DATUM(result);
+}
+
+Datum
+bt_page_stats_1_9(PG_FUNCTION_ARGS)
+{
+ return bt_page_stats_internal(fcinfo, PAGEINSPECT_V1_9);
+}
+
+/* entry point for old extension version */
+Datum
+bt_page_stats(PG_FUNCTION_ARGS)
+{
+ return bt_page_stats_internal(fcinfo, PAGEINSPECT_V1_8);
+}
+
+
+/* -----------------------------------------------
+ * bt_multi_page_stats()
+ *
+ * Usage: SELECT * FROM bt_page_stats('t1_pkey', 1, 2);
+ * Arguments are index relation name, first block number, number of blocks
+ * (but number of blocks can be negative to mean "read all the rest")
+ * -----------------------------------------------
+ */
+Datum
+bt_multi_page_stats(PG_FUNCTION_ARGS)
+{
+ Relation rel;
+ ua_page_stats *uargs;
+ FuncCallContext *fctx;
+ MemoryContext mctx;
+
+ if (!superuser())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use pageinspect functions")));
+
+ if (SRF_IS_FIRSTCALL())
+ {
+ text *relname = PG_GETARG_TEXT_PP(0);
+ int64 blkno = PG_GETARG_INT64(1);
+ int64 blk_count = PG_GETARG_INT64(2);
+ RangeVar *relrv;
+
+ fctx = SRF_FIRSTCALL_INIT();
+
+ relrv = makeRangeVarFromNameList(textToQualifiedNameList(relname));
+ rel = relation_openrv(relrv, AccessShareLock);
+
+ /* Check that rel is a valid btree index and 1st block number is OK */
+ bt_index_block_validate(rel, blkno);
+
+ /*
+ * Check if upper bound of the specified range is valid. If only one
+ * page is requested, skip as we've already validated the page. (Also,
+ * it's important to skip this if blk_count is negative.)
+ */
+ if (blk_count > 1)
+ check_relation_block_range(rel, blkno + blk_count - 1);
+
+ /* Save arguments for reuse */
+ mctx = MemoryContextSwitchTo(fctx->multi_call_memory_ctx);
+
+ uargs = palloc(sizeof(ua_page_stats));
+
+ uargs->relid = RelationGetRelid(rel);
+ uargs->blkno = blkno;
+ uargs->blk_count = blk_count;
+ uargs->allpages = (blk_count < 0);
+
+ fctx->user_fctx = uargs;
+
+ MemoryContextSwitchTo(mctx);
+
+ /*
+ * To avoid possibly leaking a relcache reference if the SRF isn't run
+ * to completion, we close and re-open the index rel each time
+ * through, using the index's OID for re-opens to ensure we get the
+ * same rel. Keep the AccessShareLock though, to ensure it doesn't go
+ * away underneath us.
+ */
+ relation_close(rel, NoLock);
+ }
+
+ fctx = SRF_PERCALL_SETUP();
+ uargs = fctx->user_fctx;
+
+ /* We should have lock already */
+ rel = relation_open(uargs->relid, NoLock);
+
+ /* In all-pages mode, recheck the index length each time */
+ if (uargs->allpages)
+ uargs->blk_count = RelationGetNumberOfBlocks(rel) - uargs->blkno;
+
+ if (uargs->blk_count > 0)
+ {
+ /* We need to fetch next block statistics */
+ Buffer buffer;
+ Datum result;
+ HeapTuple tuple;
+ int j;
+ char *values[11];
+ BTPageStat stat;
+ TupleDesc tupleDesc;
+
+ buffer = ReadBuffer(rel, uargs->blkno);
+ LockBuffer(buffer, BUFFER_LOCK_SHARE);
+
+ /* keep compiler quiet */
+ stat.btpo_prev = stat.btpo_next = InvalidBlockNumber;
+ stat.btpo_flags = stat.free_size = stat.avg_item_size = 0;
+
+ GetBTPageStatistics(uargs->blkno, buffer, &stat);
+
+ UnlockReleaseBuffer(buffer);
+ relation_close(rel, NoLock);
+
+ /* Build a tuple descriptor for our result type */
+ if (get_call_result_type(fcinfo, NULL, &tupleDesc) != TYPEFUNC_COMPOSITE)
+ elog(ERROR, "return type must be a row type");
+
+ j = 0;
+ values[j++] = psprintf("%u", stat.blkno);
+ values[j++] = psprintf("%c", stat.type);
+ values[j++] = psprintf("%u", stat.live_items);
+ values[j++] = psprintf("%u", stat.dead_items);
+ values[j++] = psprintf("%u", stat.avg_item_size);
+ values[j++] = psprintf("%u", stat.page_size);
+ values[j++] = psprintf("%u", stat.free_size);
+ values[j++] = psprintf("%u", stat.btpo_prev);
+ values[j++] = psprintf("%u", stat.btpo_next);
+ values[j++] = psprintf("%u", stat.btpo_level);
+ values[j++] = psprintf("%d", stat.btpo_flags);
+
+ /* Construct tuple to be returned */
+ tuple = BuildTupleFromCStrings(TupleDescGetAttInMetadata(tupleDesc),
+ values);
+
+ result = HeapTupleGetDatum(tuple);
+
+ /*
+ * Move to the next block number and decrement the number of blocks
+ * still to be fetched
+ */
+ uargs->blkno++;
+ uargs->blk_count--;
+
+ SRF_RETURN_NEXT(fctx, result);
+ }
+
+ /* Done, so finally we can release the index lock */
+ relation_close(rel, AccessShareLock);
+ SRF_RETURN_DONE(fctx);
+}
+
+/*-------------------------------------------------------
+ * bt_page_print_tuples()
+ *
+ * Form a tuple describing index tuple at a given offset
+ * ------------------------------------------------------
+ */
+static Datum
+bt_page_print_tuples(ua_page_items *uargs)
+{
+ Page page = uargs->page;
+ OffsetNumber offset = uargs->offset;
+ bool leafpage = uargs->leafpage;
+ bool rightmost = uargs->rightmost;
+ bool ispivottuple;
+ Datum values[9];
+ bool nulls[9];
+ HeapTuple tuple;
+ ItemId id;
+ IndexTuple itup;
+ int j;
+ int off;
+ int dlen;
+ char *dump,
+ *datacstring;
+ char *ptr;
+ ItemPointer htid;
+
+ id = PageGetItemId(page, offset);
+
+ if (!ItemIdIsValid(id))
+ elog(ERROR, "invalid ItemId");
+
+ itup = (IndexTuple) PageGetItem(page, id);
+
+ j = 0;
+ memset(nulls, 0, sizeof(nulls));
+ values[j++] = DatumGetInt16(offset);
+ values[j++] = ItemPointerGetDatum(&itup->t_tid);
+ values[j++] = Int32GetDatum((int) IndexTupleSize(itup));
+ values[j++] = BoolGetDatum(IndexTupleHasNulls(itup));
+ values[j++] = BoolGetDatum(IndexTupleHasVarwidths(itup));
+
+ ptr = (char *) itup + IndexInfoFindDataOffset(itup->t_info);
+ dlen = IndexTupleSize(itup) - IndexInfoFindDataOffset(itup->t_info);
+
+ /*
+ * Make sure that "data" column does not include posting list or pivot
+ * tuple representation of heap TID(s).
+ *
+ * Note: BTreeTupleIsPivot() won't work reliably on !heapkeyspace indexes
+ * (those built before BTREE_VERSION 4), but we have no way of determining
+ * if this page came from a !heapkeyspace index. We may only have a bytea
+ * nbtree page image to go on, so in general there is no metapage that we
+ * can check.
+ *
+ * That's okay here because BTreeTupleIsPivot() can only return false for
+ * a !heapkeyspace pivot, never true for a !heapkeyspace non-pivot. Since
+ * heap TID isn't part of the keyspace in a !heapkeyspace index anyway,
+ * there cannot possibly be a pivot tuple heap TID representation that we
+ * fail to make an adjustment for. A !heapkeyspace index can have
+ * BTreeTupleIsPivot() return true (due to things like suffix truncation
+ * for INCLUDE indexes in Postgres v11), but when that happens
+ * BTreeTupleGetHeapTID() can be trusted to work reliably (i.e. return
+ * NULL).
+ *
+ * Note: BTreeTupleIsPosting() always works reliably, even with
+ * !heapkeyspace indexes.
+ */
+ if (BTreeTupleIsPosting(itup))
+ dlen -= IndexTupleSize(itup) - BTreeTupleGetPostingOffset(itup);
+ else if (BTreeTupleIsPivot(itup) && BTreeTupleGetHeapTID(itup) != NULL)
+ dlen -= MAXALIGN(sizeof(ItemPointerData));
+
+ if (dlen < 0 || dlen > INDEX_SIZE_MASK)
+ elog(ERROR, "invalid tuple length %d for tuple at offset number %u",
+ dlen, offset);
+ dump = palloc0(dlen * 3 + 1);
+ datacstring = dump;
+ for (off = 0; off < dlen; off++)
+ {
+ if (off > 0)
+ *dump++ = ' ';
+ sprintf(dump, "%02x", *(ptr + off) & 0xff);
+ dump += 2;
+ }
+ values[j++] = CStringGetTextDatum(datacstring);
+ pfree(datacstring);
+
+ /*
+ * We need to work around the BTreeTupleIsPivot() !heapkeyspace limitation
+ * again. Deduce whether or not tuple must be a pivot tuple based on
+ * whether or not the page is a leaf page, as well as the page offset
+ * number of the tuple.
+ */
+ ispivottuple = (!leafpage || (!rightmost && offset == P_HIKEY));
+
+ /* LP_DEAD bit can never be set for pivot tuples, so show a NULL there */
+ if (!ispivottuple)
+ values[j++] = BoolGetDatum(ItemIdIsDead(id));
+ else
+ {
+ Assert(!ItemIdIsDead(id));
+ nulls[j++] = true;
+ }
+
+ htid = BTreeTupleGetHeapTID(itup);
+ if (ispivottuple && !BTreeTupleIsPivot(itup))
+ {
+ /* Don't show bogus heap TID in !heapkeyspace pivot tuple */
+ htid = NULL;
+ }
+
+ if (htid)
+ values[j++] = ItemPointerGetDatum(htid);
+ else
+ nulls[j++] = true;
+
+ if (BTreeTupleIsPosting(itup))
+ {
+ /* Build an array of item pointers */
+ ItemPointer tids;
+ Datum *tids_datum;
+ int nposting;
+
+ tids = BTreeTupleGetPosting(itup);
+ nposting = BTreeTupleGetNPosting(itup);
+ tids_datum = (Datum *) palloc(nposting * sizeof(Datum));
+ for (int i = 0; i < nposting; i++)
+ tids_datum[i] = ItemPointerGetDatum(&tids[i]);
+ values[j++] = PointerGetDatum(construct_array_builtin(tids_datum, nposting, TIDOID));
+ pfree(tids_datum);
+ }
+ else
+ nulls[j++] = true;
+
+ /* Build and return the result tuple */
+ tuple = heap_form_tuple(uargs->tupd, values, nulls);
+
+ return HeapTupleGetDatum(tuple);
+}
+
+/*-------------------------------------------------------
+ * bt_page_items()
+ *
+ * Get IndexTupleData set in a btree page
+ *
+ * Usage: SELECT * FROM bt_page_items('t1_pkey', 1);
+ *-------------------------------------------------------
+ */
+static Datum
+bt_page_items_internal(PG_FUNCTION_ARGS, enum pageinspect_version ext_version)
+{
+ text *relname = PG_GETARG_TEXT_PP(0);
+ int64 blkno = (ext_version == PAGEINSPECT_V1_8 ? PG_GETARG_UINT32(1) : PG_GETARG_INT64(1));
+ Datum result;
+ FuncCallContext *fctx;
+ MemoryContext mctx;
+ ua_page_items *uargs;
+
+ if (!superuser())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use pageinspect functions")));
+
+ if (SRF_IS_FIRSTCALL())
+ {
+ RangeVar *relrv;
+ Relation rel;
+ Buffer buffer;
+ BTPageOpaque opaque;
+ TupleDesc tupleDesc;
+
+ fctx = SRF_FIRSTCALL_INIT();
+
+ relrv = makeRangeVarFromNameList(textToQualifiedNameList(relname));
+ rel = relation_openrv(relrv, AccessShareLock);
+
+ bt_index_block_validate(rel, blkno);
+
+ buffer = ReadBuffer(rel, blkno);
+ LockBuffer(buffer, BUFFER_LOCK_SHARE);
+
+ /*
+ * We copy the page into local storage to avoid holding pin on the
+ * buffer longer than we must, and possibly failing to release it at
+ * all if the calling query doesn't fetch all rows.
+ */
+ mctx = MemoryContextSwitchTo(fctx->multi_call_memory_ctx);
+
+ uargs = palloc(sizeof(ua_page_items));
+
+ uargs->page = palloc(BLCKSZ);
+ memcpy(uargs->page, BufferGetPage(buffer), BLCKSZ);
+
+ UnlockReleaseBuffer(buffer);
+ relation_close(rel, AccessShareLock);
+
+ uargs->offset = FirstOffsetNumber;
+
+ opaque = BTPageGetOpaque(uargs->page);
+
+ if (!P_ISDELETED(opaque))
+ fctx->max_calls = PageGetMaxOffsetNumber(uargs->page);
+ else
+ {
+ /* Don't interpret BTDeletedPageData as index tuples */
+ elog(NOTICE, "page from block " INT64_FORMAT " is deleted", blkno);
+ fctx->max_calls = 0;
+ }
+ uargs->leafpage = P_ISLEAF(opaque);
+ uargs->rightmost = P_RIGHTMOST(opaque);
+
+ /* Build a tuple descriptor for our result type */
+ if (get_call_result_type(fcinfo, NULL, &tupleDesc) != TYPEFUNC_COMPOSITE)
+ elog(ERROR, "return type must be a row type");
+ tupleDesc = BlessTupleDesc(tupleDesc);
+
+ uargs->tupd = tupleDesc;
+
+ fctx->user_fctx = uargs;
+
+ MemoryContextSwitchTo(mctx);
+ }
+
+ fctx = SRF_PERCALL_SETUP();
+ uargs = fctx->user_fctx;
+
+ if (fctx->call_cntr < fctx->max_calls)
+ {
+ result = bt_page_print_tuples(uargs);
+ uargs->offset++;
+ SRF_RETURN_NEXT(fctx, result);
+ }
+
+ SRF_RETURN_DONE(fctx);
+}
+
+Datum
+bt_page_items_1_9(PG_FUNCTION_ARGS)
+{
+ return bt_page_items_internal(fcinfo, PAGEINSPECT_V1_9);
+}
+
+/* entry point for old extension version */
+Datum
+bt_page_items(PG_FUNCTION_ARGS)
+{
+ return bt_page_items_internal(fcinfo, PAGEINSPECT_V1_8);
+}
+
+/*-------------------------------------------------------
+ * bt_page_items_bytea()
+ *
+ * Get IndexTupleData set in a btree page
+ *
+ * Usage: SELECT * FROM bt_page_items(get_raw_page('t1_pkey', 1));
+ *-------------------------------------------------------
+ */
+
+Datum
+bt_page_items_bytea(PG_FUNCTION_ARGS)
+{
+ bytea *raw_page = PG_GETARG_BYTEA_P(0);
+ Datum result;
+ FuncCallContext *fctx;
+ ua_page_items *uargs;
+
+ if (!superuser())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions")));
+
+ if (SRF_IS_FIRSTCALL())
+ {
+ BTPageOpaque opaque;
+ MemoryContext mctx;
+ TupleDesc tupleDesc;
+
+ fctx = SRF_FIRSTCALL_INIT();
+ mctx = MemoryContextSwitchTo(fctx->multi_call_memory_ctx);
+
+ uargs = palloc(sizeof(ua_page_items));
+
+ uargs->page = get_page_from_raw(raw_page);
+
+ if (PageIsNew(uargs->page))
+ {
+ MemoryContextSwitchTo(mctx);
+ PG_RETURN_NULL();
+ }
+
+ uargs->offset = FirstOffsetNumber;
+
+ /* verify the special space has the expected size */
+ if (PageGetSpecialSize(uargs->page) != MAXALIGN(sizeof(BTPageOpaqueData)))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("input page is not a valid %s page", "btree"),
+ errdetail("Expected special size %d, got %d.",
+ (int) MAXALIGN(sizeof(BTPageOpaqueData)),
+ (int) PageGetSpecialSize(uargs->page))));
+
+ opaque = BTPageGetOpaque(uargs->page);
+
+ if (P_ISMETA(opaque))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("block is a meta page")));
+
+ if (P_ISLEAF(opaque) && opaque->btpo_level != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("block is not a valid btree leaf page")));
+
+ if (P_ISDELETED(opaque))
+ elog(NOTICE, "page is deleted");
+
+ if (!P_ISDELETED(opaque))
+ fctx->max_calls = PageGetMaxOffsetNumber(uargs->page);
+ else
+ {
+ /* Don't interpret BTDeletedPageData as index tuples */
+ elog(NOTICE, "page from block is deleted");
+ fctx->max_calls = 0;
+ }
+ uargs->leafpage = P_ISLEAF(opaque);
+ uargs->rightmost = P_RIGHTMOST(opaque);
+
+ /* Build a tuple descriptor for our result type */
+ if (get_call_result_type(fcinfo, NULL, &tupleDesc) != TYPEFUNC_COMPOSITE)
+ elog(ERROR, "return type must be a row type");
+ tupleDesc = BlessTupleDesc(tupleDesc);
+
+ uargs->tupd = tupleDesc;
+
+ fctx->user_fctx = uargs;
+
+ MemoryContextSwitchTo(mctx);
+ }
+
+ fctx = SRF_PERCALL_SETUP();
+ uargs = fctx->user_fctx;
+
+ if (fctx->call_cntr < fctx->max_calls)
+ {
+ result = bt_page_print_tuples(uargs);
+ uargs->offset++;
+ SRF_RETURN_NEXT(fctx, result);
+ }
+
+ SRF_RETURN_DONE(fctx);
+}
+
+/* Number of output arguments (columns) for bt_metap() */
+#define BT_METAP_COLS_V1_8 9
+
+/* ------------------------------------------------
+ * bt_metap()
+ *
+ * Get a btree's meta-page information
+ *
+ * Usage: SELECT * FROM bt_metap('t1_pkey')
+ * ------------------------------------------------
+ */
+Datum
+bt_metap(PG_FUNCTION_ARGS)
+{
+ text *relname = PG_GETARG_TEXT_PP(0);
+ Datum result;
+ Relation rel;
+ RangeVar *relrv;
+ BTMetaPageData *metad;
+ TupleDesc tupleDesc;
+ int j;
+ char *values[9];
+ Buffer buffer;
+ Page page;
+ HeapTuple tuple;
+
+ if (!superuser())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use pageinspect functions")));
+
+ relrv = makeRangeVarFromNameList(textToQualifiedNameList(relname));
+ rel = relation_openrv(relrv, AccessShareLock);
+
+ if (!IS_INDEX(rel) || !IS_BTREE(rel))
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is not a %s index",
+ RelationGetRelationName(rel), "btree")));
+
+ /*
+ * Reject attempts to read non-local temporary relations; we would be
+ * likely to get wrong data since we have no visibility into the owning
+ * session's local buffers.
+ */
+ if (RELATION_IS_OTHER_TEMP(rel))
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot access temporary tables of other sessions")));
+
+ buffer = ReadBuffer(rel, 0);
+ LockBuffer(buffer, BUFFER_LOCK_SHARE);
+
+ page = BufferGetPage(buffer);
+ metad = BTPageGetMeta(page);
+
+ /* Build a tuple descriptor for our result type */
+ if (get_call_result_type(fcinfo, NULL, &tupleDesc) != TYPEFUNC_COMPOSITE)
+ elog(ERROR, "return type must be a row type");
+
+ /*
+ * We need a kluge here to detect API versions prior to 1.8. Earlier
+ * versions incorrectly used int4 for certain columns.
+ *
+ * There is no way to reliably avoid the problems created by the old
+ * function definition at this point, so insist that the user update the
+ * extension.
+ */
+ if (tupleDesc->natts < BT_METAP_COLS_V1_8)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
+ errmsg("function has wrong number of declared columns"),
+ errhint("To resolve the problem, update the \"pageinspect\" extension to the latest version.")));
+
+ j = 0;
+ values[j++] = psprintf("%d", metad->btm_magic);
+ values[j++] = psprintf("%d", metad->btm_version);
+ values[j++] = psprintf(INT64_FORMAT, (int64) metad->btm_root);
+ values[j++] = psprintf(INT64_FORMAT, (int64) metad->btm_level);
+ values[j++] = psprintf(INT64_FORMAT, (int64) metad->btm_fastroot);
+ values[j++] = psprintf(INT64_FORMAT, (int64) metad->btm_fastlevel);
+
+ /*
+ * Get values of extended metadata if available, use default values
+ * otherwise. Note that we rely on the assumption that btm_allequalimage
+ * is initialized to zero with indexes that were built on versions prior
+ * to Postgres 13 (just like _bt_metaversion()).
+ */
+ if (metad->btm_version >= BTREE_NOVAC_VERSION)
+ {
+ values[j++] = psprintf(INT64_FORMAT,
+ (int64) metad->btm_last_cleanup_num_delpages);
+ values[j++] = psprintf("%f", metad->btm_last_cleanup_num_heap_tuples);
+ values[j++] = metad->btm_allequalimage ? "t" : "f";
+ }
+ else
+ {
+ values[j++] = "0";
+ values[j++] = "-1";
+ values[j++] = "f";
+ }
+
+ tuple = BuildTupleFromCStrings(TupleDescGetAttInMetadata(tupleDesc),
+ values);
+
+ result = HeapTupleGetDatum(tuple);
+
+ UnlockReleaseBuffer(buffer);
+ relation_close(rel, AccessShareLock);
+
+ PG_RETURN_DATUM(result);
+}
diff --git a/contrib/pageinspect/expected/brin.out b/contrib/pageinspect/expected/brin.out
new file mode 100644
index 0000000..098ddc2
--- /dev/null
+++ b/contrib/pageinspect/expected/brin.out
@@ -0,0 +1,92 @@
+CREATE TABLE test1 (a int, b text);
+INSERT INTO test1 VALUES (1, 'one');
+CREATE INDEX test1_a_idx ON test1 USING brin (a);
+SELECT brin_page_type(get_raw_page('test1_a_idx', 0));
+ brin_page_type
+----------------
+ meta
+(1 row)
+
+SELECT brin_page_type(get_raw_page('test1_a_idx', 1));
+ brin_page_type
+----------------
+ revmap
+(1 row)
+
+SELECT brin_page_type(get_raw_page('test1_a_idx', 2));
+ brin_page_type
+----------------
+ regular
+(1 row)
+
+SELECT * FROM brin_metapage_info(get_raw_page('test1_a_idx', 0));
+ magic | version | pagesperrange | lastrevmappage
+------------+---------+---------------+----------------
+ 0xA8109CFA | 1 | 128 | 1
+(1 row)
+
+SELECT * FROM brin_metapage_info(get_raw_page('test1_a_idx', 1));
+ERROR: page is not a BRIN page of type "metapage"
+DETAIL: Expected special type 0000f091, got 0000f092.
+SELECT * FROM brin_revmap_data(get_raw_page('test1_a_idx', 0)) LIMIT 5;
+ERROR: page is not a BRIN page of type "revmap"
+DETAIL: Expected special type 0000f092, got 0000f091.
+SELECT * FROM brin_revmap_data(get_raw_page('test1_a_idx', 1)) LIMIT 5;
+ pages
+-------
+ (2,1)
+ (0,0)
+ (0,0)
+ (0,0)
+ (0,0)
+(5 rows)
+
+SELECT * FROM brin_page_items(get_raw_page('test1_a_idx', 2), 'test1_a_idx')
+ ORDER BY blknum, attnum LIMIT 5;
+ itemoffset | blknum | attnum | allnulls | hasnulls | placeholder | empty | value
+------------+--------+--------+----------+----------+-------------+-------+----------
+ 1 | 0 | 1 | f | f | f | f | {1 .. 1}
+(1 row)
+
+-- Mask DETAIL messages as these are not portable across architectures.
+\set VERBOSITY terse
+-- Failures for non-BRIN index.
+CREATE INDEX test1_a_btree ON test1 (a);
+SELECT brin_page_items(get_raw_page('test1_a_btree', 0), 'test1_a_btree');
+ERROR: "test1_a_btree" is not a BRIN index
+SELECT brin_page_items(get_raw_page('test1_a_btree', 0), 'test1_a_idx');
+ERROR: input page is not a valid BRIN page
+-- Invalid special area size
+SELECT brin_page_type(get_raw_page('test1', 0));
+ERROR: input page is not a valid BRIN page
+SELECT * FROM brin_metapage_info(get_raw_page('test1', 0));
+ERROR: input page is not a valid BRIN page
+SELECT * FROM brin_revmap_data(get_raw_page('test1', 0));
+ERROR: input page is not a valid BRIN page
+\set VERBOSITY default
+-- Tests with all-zero pages.
+SHOW block_size \gset
+SELECT brin_page_type(decode(repeat('00', :block_size), 'hex'));
+ brin_page_type
+----------------
+
+(1 row)
+
+SELECT brin_page_items(decode(repeat('00', :block_size), 'hex'), 'test1_a_idx');
+ brin_page_items
+-----------------
+(0 rows)
+
+SELECT brin_metapage_info(decode(repeat('00', :block_size), 'hex'));
+ brin_metapage_info
+--------------------
+
+(1 row)
+
+SELECT brin_revmap_data(decode(repeat('00', :block_size), 'hex'));
+ brin_revmap_data
+------------------
+
+(1 row)
+
+DROP TABLE test1;
diff --git a/contrib/pageinspect/expected/btree.out b/contrib/pageinspect/expected/btree.out
new file mode 100644
index 0000000..0aa5d73
--- /dev/null
+++ b/contrib/pageinspect/expected/btree.out
@@ -0,0 +1,221 @@
+CREATE TABLE test1 (a int8, b int4range);
+INSERT INTO test1 VALUES (72057594037927937, '[0,1)');
+CREATE INDEX test1_a_idx ON test1 USING btree (a);
+\x
+SELECT * FROM bt_metap('test1_a_idx');
+-[ RECORD 1 ]-------------+-------
+magic | 340322
+version | 4
+root | 1
+level | 0
+fastroot | 1
+fastlevel | 0
+last_cleanup_num_delpages | 0
+last_cleanup_num_tuples | -1
+allequalimage | t
+
+SELECT * FROM bt_page_stats('test1_a_idx', -1);
+ERROR: invalid block number -1
+SELECT * FROM bt_page_stats('test1_a_idx', 0);
+ERROR: block 0 is a meta page
+SELECT * FROM bt_page_stats('test1_a_idx', 1);
+-[ RECORD 1 ]-+-----
+blkno | 1
+type | l
+live_items | 1
+dead_items | 0
+avg_item_size | 16
+page_size | 8192
+free_size | 8128
+btpo_prev | 0
+btpo_next | 0
+btpo_level | 0
+btpo_flags | 3
+
+SELECT * FROM bt_page_stats('test1_a_idx', 2);
+ERROR: block number 2 is out of range
+-- bt_multi_page_stats() function returns a set of records of page statistics.
+CREATE TABLE test2 AS (SELECT generate_series(1, 1000)::int8 AS col1);
+CREATE INDEX test2_col1_idx ON test2(col1);
+SELECT * FROM bt_multi_page_stats('test2_col1_idx', 0, 1);
+ERROR: block 0 is a meta page
+SELECT * FROM bt_multi_page_stats('test2_col1_idx', 1, -1);
+-[ RECORD 1 ]-+-----
+blkno | 1
+type | l
+live_items | 367
+dead_items | 0
+avg_item_size | 16
+page_size | 8192
+free_size | 808
+btpo_prev | 0
+btpo_next | 2
+btpo_level | 0
+btpo_flags | 1
+-[ RECORD 2 ]-+-----
+blkno | 2
+type | l
+live_items | 367
+dead_items | 0
+avg_item_size | 16
+page_size | 8192
+free_size | 808
+btpo_prev | 1
+btpo_next | 4
+btpo_level | 0
+btpo_flags | 1
+-[ RECORD 3 ]-+-----
+blkno | 3
+type | r
+live_items | 3
+dead_items | 0
+avg_item_size | 13
+page_size | 8192
+free_size | 8096
+btpo_prev | 0
+btpo_next | 0
+btpo_level | 1
+btpo_flags | 2
+-[ RECORD 4 ]-+-----
+blkno | 4
+type | l
+live_items | 268
+dead_items | 0
+avg_item_size | 16
+page_size | 8192
+free_size | 2788
+btpo_prev | 2
+btpo_next | 0
+btpo_level | 0
+btpo_flags | 1
+
+SELECT * FROM bt_multi_page_stats('test2_col1_idx', 1, 0);
+(0 rows)
+
+SELECT * FROM bt_multi_page_stats('test2_col1_idx', 1, 2);
+-[ RECORD 1 ]-+-----
+blkno | 1
+type | l
+live_items | 367
+dead_items | 0
+avg_item_size | 16
+page_size | 8192
+free_size | 808
+btpo_prev | 0
+btpo_next | 2
+btpo_level | 0
+btpo_flags | 1
+-[ RECORD 2 ]-+-----
+blkno | 2
+type | l
+live_items | 367
+dead_items | 0
+avg_item_size | 16
+page_size | 8192
+free_size | 808
+btpo_prev | 1
+btpo_next | 4
+btpo_level | 0
+btpo_flags | 1
+
+SELECT * FROM bt_multi_page_stats('test2_col1_idx', 3, 2);
+-[ RECORD 1 ]-+-----
+blkno | 3
+type | r
+live_items | 3
+dead_items | 0
+avg_item_size | 13
+page_size | 8192
+free_size | 8096
+btpo_prev | 0
+btpo_next | 0
+btpo_level | 1
+btpo_flags | 2
+-[ RECORD 2 ]-+-----
+blkno | 4
+type | l
+live_items | 268
+dead_items | 0
+avg_item_size | 16
+page_size | 8192
+free_size | 2788
+btpo_prev | 2
+btpo_next | 0
+btpo_level | 0
+btpo_flags | 1
+
+SELECT * FROM bt_multi_page_stats('test2_col1_idx', 7, 2);
+ERROR: block number 7 is out of range
+DROP TABLE test2;
+SELECT * FROM bt_page_items('test1_a_idx', -1);
+ERROR: invalid block number -1
+SELECT * FROM bt_page_items('test1_a_idx', 0);
+ERROR: block 0 is a meta page
+SELECT * FROM bt_page_items('test1_a_idx', 1);
+-[ RECORD 1 ]-----------------------
+itemoffset | 1
+ctid | (0,1)
+itemlen | 16
+nulls | f
+vars | f
+data | 01 00 00 00 00 00 00 01
+dead | f
+htid | (0,1)
+tids |
+
+SELECT * FROM bt_page_items('test1_a_idx', 2);
+ERROR: block number 2 is out of range
+SELECT * FROM bt_page_items(get_raw_page('test1_a_idx', -1));
+ERROR: invalid block number
+SELECT * FROM bt_page_items(get_raw_page('test1_a_idx', 0));
+ERROR: block is a meta page
+SELECT * FROM bt_page_items(get_raw_page('test1_a_idx', 1));
+-[ RECORD 1 ]-----------------------
+itemoffset | 1
+ctid | (0,1)
+itemlen | 16
+nulls | f
+vars | f
+data | 01 00 00 00 00 00 00 01
+dead | f
+htid | (0,1)
+tids |
+
+SELECT * FROM bt_page_items(get_raw_page('test1_a_idx', 2));
+ERROR: block number 2 is out of range for relation "test1_a_idx"
+-- Failure when using a non-btree index.
+CREATE INDEX test1_a_hash ON test1 USING hash(a);
+SELECT bt_metap('test1_a_hash');
+ERROR: "test1_a_hash" is not a btree index
+SELECT bt_page_stats('test1_a_hash', 0);
+ERROR: "test1_a_hash" is not a btree index
+SELECT bt_page_items('test1_a_hash', 0);
+ERROR: "test1_a_hash" is not a btree index
+SELECT bt_page_items(get_raw_page('test1_a_hash', 0));
+ERROR: block is a meta page
+CREATE INDEX test1_b_gist ON test1 USING gist(b);
+-- Special area of GiST is the same as btree, this complains about inconsistent
+-- leaf data on the page.
+SELECT bt_page_items(get_raw_page('test1_b_gist', 0));
+ERROR: block is not a valid btree leaf page
+-- Several failure modes.
+-- Suppress the DETAIL message, to allow the tests to work across various
+-- page sizes and architectures.
+\set VERBOSITY terse
+-- invalid page size
+SELECT bt_page_items('aaa'::bytea);
+ERROR: invalid page size
+-- invalid special area size
+CREATE INDEX test1_a_brin ON test1 USING brin(a);
+SELECT bt_page_items(get_raw_page('test1', 0));
+ERROR: input page is not a valid btree page
+SELECT bt_page_items(get_raw_page('test1_a_brin', 0));
+ERROR: input page is not a valid btree page
+\set VERBOSITY default
+-- Tests with all-zero pages.
+SHOW block_size \gset
+SELECT bt_page_items(decode(repeat('00', :block_size), 'hex'));
+-[ RECORD 1 ]-+-
+bt_page_items |
+
+DROP TABLE test1;
diff --git a/contrib/pageinspect/expected/checksum.out b/contrib/pageinspect/expected/checksum.out
new file mode 100644
index 0000000..a85388e
--- /dev/null
+++ b/contrib/pageinspect/expected/checksum.out
@@ -0,0 +1,40 @@
+--
+-- Verify correct calculation of checksums
+--
+-- Postgres' checksum algorithm produces different answers on little-endian
+-- and big-endian machines. The results of this test also vary depending
+-- on the configured block size. This test has several different expected
+-- results files to handle the following possibilities:
+--
+-- BLCKSZ end file
+-- 8K LE checksum.out
+-- 8K BE checksum_1.out
+--
+-- In future we might provide additional expected-results files for other
+-- block sizes, but there seems little point as long as so many other
+-- test scripts also show false failures for non-default block sizes.
+--
+-- This is to label the results files with blocksize:
+SHOW block_size;
+ block_size
+------------
+ 8192
+(1 row)
+
+SHOW block_size \gset
+-- Apply page_checksum() to some different data patterns and block numbers
+SELECT blkno,
+ page_checksum(decode(repeat('01', :block_size), 'hex'), blkno) AS checksum_01,
+ page_checksum(decode(repeat('04', :block_size), 'hex'), blkno) AS checksum_04,
+ page_checksum(decode(repeat('ff', :block_size), 'hex'), blkno) AS checksum_ff,
+ page_checksum(decode(repeat('abcd', :block_size / 2), 'hex'), blkno) AS checksum_abcd,
+ page_checksum(decode(repeat('e6d6', :block_size / 2), 'hex'), blkno) AS checksum_e6d6,
+ page_checksum(decode(repeat('4a5e', :block_size / 2), 'hex'), blkno) AS checksum_4a5e
+ FROM generate_series(0, 100, 50) AS a (blkno);
+ blkno | checksum_01 | checksum_04 | checksum_ff | checksum_abcd | checksum_e6d6 | checksum_4a5e
+-------+-------------+-------------+-------------+---------------+---------------+---------------
+ 0 | 1175 | 28338 | 3612 | -30781 | -16269 | -27377
+ 50 | 1225 | 28352 | 3598 | -30795 | -16251 | -27391
+ 100 | 1139 | 28438 | 3648 | -30881 | -16305 | -27349
+(3 rows)
+
diff --git a/contrib/pageinspect/expected/checksum_1.out b/contrib/pageinspect/expected/checksum_1.out
new file mode 100644
index 0000000..6fb1b1b
--- /dev/null
+++ b/contrib/pageinspect/expected/checksum_1.out
@@ -0,0 +1,40 @@
+--
+-- Verify correct calculation of checksums
+--
+-- Postgres' checksum algorithm produces different answers on little-endian
+-- and big-endian machines. The results of this test also vary depending
+-- on the configured block size. This test has several different expected
+-- results files to handle the following possibilities:
+--
+-- BLCKSZ end file
+-- 8K LE checksum.out
+-- 8K BE checksum_1.out
+--
+-- In future we might provide additional expected-results files for other
+-- block sizes, but there seems little point as long as so many other
+-- test scripts also show false failures for non-default block sizes.
+--
+-- This is to label the results files with blocksize:
+SHOW block_size;
+ block_size
+------------
+ 8192
+(1 row)
+
+SHOW block_size \gset
+-- Apply page_checksum() to some different data patterns and block numbers
+SELECT blkno,
+ page_checksum(decode(repeat('01', :block_size), 'hex'), blkno) AS checksum_01,
+ page_checksum(decode(repeat('04', :block_size), 'hex'), blkno) AS checksum_04,
+ page_checksum(decode(repeat('ff', :block_size), 'hex'), blkno) AS checksum_ff,
+ page_checksum(decode(repeat('abcd', :block_size / 2), 'hex'), blkno) AS checksum_abcd,
+ page_checksum(decode(repeat('e6d6', :block_size / 2), 'hex'), blkno) AS checksum_e6d6,
+ page_checksum(decode(repeat('4a5e', :block_size / 2), 'hex'), blkno) AS checksum_4a5e
+ FROM generate_series(0, 100, 50) AS a (blkno);
+ blkno | checksum_01 | checksum_04 | checksum_ff | checksum_abcd | checksum_e6d6 | checksum_4a5e
+-------+-------------+-------------+-------------+---------------+---------------+---------------
+ 0 | -16327 | 8766 | -2722 | 13757 | -11485 | -31426
+ 50 | -16281 | 8780 | -2708 | 13771 | -11503 | -31440
+ 100 | -16235 | 8866 | -2758 | 13721 | -11577 | -31518
+(3 rows)
+
diff --git a/contrib/pageinspect/expected/gin.out b/contrib/pageinspect/expected/gin.out
new file mode 100644
index 0000000..ff1da6a
--- /dev/null
+++ b/contrib/pageinspect/expected/gin.out
@@ -0,0 +1,71 @@
+CREATE TABLE test1 (x int, y int[]);
+INSERT INTO test1 VALUES (1, ARRAY[11, 111]);
+CREATE INDEX test1_y_idx ON test1 USING gin (y) WITH (fastupdate = off);
+\x
+SELECT * FROM gin_metapage_info(get_raw_page('test1_y_idx', 0));
+-[ RECORD 1 ]----+-----------
+pending_head | 4294967295
+pending_tail | 4294967295
+tail_free_size | 0
+n_pending_pages | 0
+n_pending_tuples | 0
+n_total_pages | 2
+n_entry_pages | 1
+n_data_pages | 0
+n_entries | 2
+version | 2
+
+SELECT * FROM gin_metapage_info(get_raw_page('test1_y_idx', 1));
+ERROR: input page is not a GIN metapage
+DETAIL: Flags 0002, expected 0008
+SELECT * FROM gin_page_opaque_info(get_raw_page('test1_y_idx', 1));
+-[ RECORD 1 ]---------
+rightlink | 4294967295
+maxoff | 0
+flags | {leaf}
+
+SELECT * FROM gin_leafpage_items(get_raw_page('test1_y_idx', 1));
+ERROR: input page is not a compressed GIN data leaf page
+DETAIL: Flags 0002, expected 0083
+INSERT INTO test1 SELECT x, ARRAY[1,10] FROM generate_series(2,10000) x;
+SELECT COUNT(*) > 0
+FROM gin_leafpage_items(get_raw_page('test1_y_idx',
+ (pg_relation_size('test1_y_idx') /
+ current_setting('block_size')::bigint)::int - 1));
+-[ RECORD 1 ]
+?column? | t
+
+-- Failure with various modes.
+-- Suppress the DETAIL message, to allow the tests to work across various
+-- page sizes and architectures.
+\set VERBOSITY terse
+-- invalid page size
+SELECT gin_leafpage_items('aaa'::bytea);
+ERROR: invalid page size
+SELECT gin_metapage_info('bbb'::bytea);
+ERROR: invalid page size
+SELECT gin_page_opaque_info('ccc'::bytea);
+ERROR: invalid page size
+-- invalid special area size
+SELECT * FROM gin_metapage_info(get_raw_page('test1', 0));
+ERROR: input page is not a valid GIN metapage
+SELECT * FROM gin_page_opaque_info(get_raw_page('test1', 0));
+ERROR: input page is not a valid GIN data leaf page
+SELECT * FROM gin_leafpage_items(get_raw_page('test1', 0));
+ERROR: input page is not a valid GIN data leaf page
+\set VERBOSITY default
+-- Tests with all-zero pages.
+SHOW block_size \gset
+SELECT gin_leafpage_items(decode(repeat('00', :block_size), 'hex'));
+-[ RECORD 1 ]------+-
+gin_leafpage_items |
+
+SELECT gin_metapage_info(decode(repeat('00', :block_size), 'hex'));
+-[ RECORD 1 ]-----+-
+gin_metapage_info |
+
+SELECT gin_page_opaque_info(decode(repeat('00', :block_size), 'hex'));
+-[ RECORD 1 ]--------+-
+gin_page_opaque_info |
+
+DROP TABLE test1;
diff --git a/contrib/pageinspect/expected/gist.out b/contrib/pageinspect/expected/gist.out
new file mode 100644
index 0000000..d1adbab
--- /dev/null
+++ b/contrib/pageinspect/expected/gist.out
@@ -0,0 +1,133 @@
+-- The gist_page_opaque_info() function prints the page's LSN. Normally,
+-- that's constant 1 (GistBuildLSN) on every page of a freshly built GiST
+-- index. But with wal_level=minimal, the whole relation is dumped to WAL at
+-- the end of the transaction if it's smaller than wal_skip_threshold, which
+-- updates the LSNs. Wrap the tests on gist_page_opaque_info() in the
+-- same transaction with the CREATE INDEX so that we see the LSNs before
+-- they are possibly overwritten at end of transaction.
+BEGIN;
+-- Create a test table and GiST index.
+CREATE TABLE test_gist AS SELECT point(i,i) p, i::text t FROM
+ generate_series(1,1000) i;
+CREATE INDEX test_gist_idx ON test_gist USING gist (p);
+-- Page 0 is the root, the rest are leaf pages
+SELECT * FROM gist_page_opaque_info(get_raw_page('test_gist_idx', 0));
+ lsn | nsn | rightlink | flags
+-----+-----+------------+-------
+ 0/1 | 0/0 | 4294967295 | {}
+(1 row)
+
+SELECT * FROM gist_page_opaque_info(get_raw_page('test_gist_idx', 1));
+ lsn | nsn | rightlink | flags
+-----+-----+------------+--------
+ 0/1 | 0/0 | 4294967295 | {leaf}
+(1 row)
+
+SELECT * FROM gist_page_opaque_info(get_raw_page('test_gist_idx', 2));
+ lsn | nsn | rightlink | flags
+-----+-----+-----------+--------
+ 0/1 | 0/0 | 1 | {leaf}
+(1 row)
+
+COMMIT;
+SELECT * FROM gist_page_items(get_raw_page('test_gist_idx', 0), 'test_gist_idx');
+ itemoffset | ctid | itemlen | dead | keys
+------------+-----------+---------+------+-------------------------------
+ 1 | (1,65535) | 40 | f | (p)=("(185,185),(1,1)")
+ 2 | (2,65535) | 40 | f | (p)=("(370,370),(186,186)")
+ 3 | (3,65535) | 40 | f | (p)=("(555,555),(371,371)")
+ 4 | (4,65535) | 40 | f | (p)=("(740,740),(556,556)")
+ 5 | (5,65535) | 40 | f | (p)=("(870,870),(741,741)")
+ 6 | (6,65535) | 40 | f | (p)=("(1000,1000),(871,871)")
+(6 rows)
+
+SELECT * FROM gist_page_items(get_raw_page('test_gist_idx', 1), 'test_gist_idx') LIMIT 5;
+ itemoffset | ctid | itemlen | dead | keys
+------------+-------+---------+------+---------------------
+ 1 | (0,1) | 40 | f | (p)=("(1,1),(1,1)")
+ 2 | (0,2) | 40 | f | (p)=("(2,2),(2,2)")
+ 3 | (0,3) | 40 | f | (p)=("(3,3),(3,3)")
+ 4 | (0,4) | 40 | f | (p)=("(4,4),(4,4)")
+ 5 | (0,5) | 40 | f | (p)=("(5,5),(5,5)")
+(5 rows)
+
+-- gist_page_items_bytea prints the raw key data as a bytea. The output of that is
+-- platform-dependent (endianness), so omit the actual key data from the output.
+SELECT itemoffset, ctid, itemlen FROM gist_page_items_bytea(get_raw_page('test_gist_idx', 0));
+ itemoffset | ctid | itemlen
+------------+-----------+---------
+ 1 | (1,65535) | 40
+ 2 | (2,65535) | 40
+ 3 | (3,65535) | 40
+ 4 | (4,65535) | 40
+ 5 | (5,65535) | 40
+ 6 | (6,65535) | 40
+(6 rows)
+
+-- Suppress the DETAIL message, to allow the tests to work across various
+-- page sizes and architectures.
+\set VERBOSITY terse
+-- Failures with non-GiST index.
+CREATE INDEX test_gist_btree on test_gist(t);
+SELECT gist_page_items(get_raw_page('test_gist_btree', 0), 'test_gist_btree');
+ERROR: "test_gist_btree" is not a GiST index
+SELECT gist_page_items(get_raw_page('test_gist_btree', 0), 'test_gist_idx');
+ERROR: input page is not a valid GiST page
+-- Failure with various modes.
+-- invalid page size
+SELECT gist_page_items_bytea('aaa'::bytea);
+ERROR: invalid page size
+SELECT gist_page_items('aaa'::bytea, 'test_gist_idx'::regclass);
+ERROR: invalid page size
+SELECT gist_page_opaque_info('aaa'::bytea);
+ERROR: invalid page size
+-- invalid special area size
+SELECT * FROM gist_page_opaque_info(get_raw_page('test_gist', 0));
+ERROR: input page is not a valid GiST page
+SELECT gist_page_items_bytea(get_raw_page('test_gist', 0));
+ERROR: input page is not a valid GiST page
+SELECT gist_page_items_bytea(get_raw_page('test_gist_btree', 0));
+ERROR: input page is not a valid GiST page
+\set VERBOSITY default
+-- Tests with all-zero pages.
+SHOW block_size \gset
+SELECT gist_page_items_bytea(decode(repeat('00', :block_size), 'hex'));
+ gist_page_items_bytea
+-----------------------
+(0 rows)
+
+SELECT gist_page_items(decode(repeat('00', :block_size), 'hex'), 'test_gist_idx'::regclass);
+ gist_page_items
+-----------------
+(0 rows)
+
+SELECT gist_page_opaque_info(decode(repeat('00', :block_size), 'hex'));
+ gist_page_opaque_info
+-----------------------
+
+(1 row)
+
+-- Test gist_page_items with included columns.
+-- Non-leaf pages contain only the key attributes, and leaf pages contain
+-- the included attributes.
+ALTER TABLE test_gist ADD COLUMN i int DEFAULT NULL;
+CREATE INDEX test_gist_idx_inc ON test_gist
+ USING gist (p) INCLUDE (t, i);
+-- Mask the value of the key attribute to avoid alignment issues.
+SELECT regexp_replace(keys, '\(p\)=\("(.*?)"\)', '(p)=("<val>")') AS keys_nonleaf_1
+ FROM gist_page_items(get_raw_page('test_gist_idx_inc', 0), 'test_gist_idx_inc')
+ WHERE itemoffset = 1;
+ keys_nonleaf_1
+----------------
+ (p)=("<val>")
+(1 row)
+
+SELECT keys AS keys_leaf_1
+ FROM gist_page_items(get_raw_page('test_gist_idx_inc', 1), 'test_gist_idx_inc')
+ WHERE itemoffset = 1;
+ keys_leaf_1
+------------------------------------------------------
+ (p) INCLUDE (t, i)=("(1,1),(1,1)") INCLUDE (1, null)
+(1 row)
+
+DROP TABLE test_gist;
diff --git a/contrib/pageinspect/expected/hash.out b/contrib/pageinspect/expected/hash.out
new file mode 100644
index 0000000..ea387a6
--- /dev/null
+++ b/contrib/pageinspect/expected/hash.out
@@ -0,0 +1,210 @@
+CREATE TABLE test_hash (a int, b text);
+INSERT INTO test_hash VALUES (1, 'one');
+CREATE INDEX test_hash_a_idx ON test_hash USING hash (a);
+CREATE TABLE test_hash_part (a int, b int) PARTITION BY RANGE (a);
+CREATE INDEX test_hash_part_idx ON test_hash_part USING hash(b);
+\x
+SELECT hash_page_type(get_raw_page('test_hash_a_idx', 0));
+-[ RECORD 1 ]--+---------
+hash_page_type | metapage
+
+SELECT hash_page_type(get_raw_page('test_hash_a_idx', 1));
+-[ RECORD 1 ]--+-------
+hash_page_type | bucket
+
+SELECT hash_page_type(get_raw_page('test_hash_a_idx', 2));
+-[ RECORD 1 ]--+-------
+hash_page_type | bucket
+
+SELECT hash_page_type(get_raw_page('test_hash_a_idx', 3));
+-[ RECORD 1 ]--+-------
+hash_page_type | bucket
+
+SELECT hash_page_type(get_raw_page('test_hash_a_idx', 4));
+-[ RECORD 1 ]--+-------
+hash_page_type | bucket
+
+SELECT hash_page_type(get_raw_page('test_hash_a_idx', 5));
+-[ RECORD 1 ]--+-------
+hash_page_type | bitmap
+
+SELECT hash_page_type(get_raw_page('test_hash_a_idx', 6));
+ERROR: block number 6 is out of range for relation "test_hash_a_idx"
+SELECT * FROM hash_bitmap_info('test_hash_a_idx', -1);
+ERROR: invalid block number
+SELECT * FROM hash_bitmap_info('test_hash_a_idx', 0);
+ERROR: invalid overflow block number 0
+SELECT * FROM hash_bitmap_info('test_hash_a_idx', 1);
+ERROR: invalid overflow block number 1
+SELECT * FROM hash_bitmap_info('test_hash_a_idx', 2);
+ERROR: invalid overflow block number 2
+SELECT * FROM hash_bitmap_info('test_hash_a_idx', 3);
+ERROR: invalid overflow block number 3
+SELECT * FROM hash_bitmap_info('test_hash_a_idx', 4);
+ERROR: invalid overflow block number 4
+SELECT * FROM hash_bitmap_info('test_hash_a_idx', 5);
+ERROR: invalid overflow block number 5
+SELECT * FROM hash_bitmap_info('test_hash_a_idx', 6);
+ERROR: block number 6 is out of range for relation "test_hash_a_idx"
+SELECT * FROM hash_bitmap_info('test_hash_part_idx', 1); -- error
+ERROR: "test_hash_part_idx" is not a hash index
+SELECT magic, version, ntuples, bsize, bmsize, bmshift, maxbucket, highmask,
+lowmask, ovflpoint, firstfree, nmaps, procid, spares, mapp FROM
+hash_metapage_info(get_raw_page('test_hash_a_idx', 0));
+-[ RECORD 1 ]--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+magic | 105121344
+version | 4
+ntuples | 1
+bsize | 8152
+bmsize | 4096
+bmshift | 15
+maxbucket | 3
+highmask | 7
+lowmask | 3
+ovflpoint | 2
+firstfree | 0
+nmaps | 1
+procid | 450
+spares | {0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}
+mapp | {5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}
+
+SELECT magic, version, ntuples, bsize, bmsize, bmshift, maxbucket, highmask,
+lowmask, ovflpoint, firstfree, nmaps, procid, spares, mapp FROM
+hash_metapage_info(get_raw_page('test_hash_a_idx', 1));
+ERROR: page is not a hash meta page
+SELECT magic, version, ntuples, bsize, bmsize, bmshift, maxbucket, highmask,
+lowmask, ovflpoint, firstfree, nmaps, procid, spares, mapp FROM
+hash_metapage_info(get_raw_page('test_hash_a_idx', 2));
+ERROR: page is not a hash meta page
+SELECT magic, version, ntuples, bsize, bmsize, bmshift, maxbucket, highmask,
+lowmask, ovflpoint, firstfree, nmaps, procid, spares, mapp FROM
+hash_metapage_info(get_raw_page('test_hash_a_idx', 3));
+ERROR: page is not a hash meta page
+SELECT magic, version, ntuples, bsize, bmsize, bmshift, maxbucket, highmask,
+lowmask, ovflpoint, firstfree, nmaps, procid, spares, mapp FROM
+hash_metapage_info(get_raw_page('test_hash_a_idx', 4));
+ERROR: page is not a hash meta page
+SELECT magic, version, ntuples, bsize, bmsize, bmshift, maxbucket, highmask,
+lowmask, ovflpoint, firstfree, nmaps, procid, spares, mapp FROM
+hash_metapage_info(get_raw_page('test_hash_a_idx', 5));
+ERROR: page is not a hash meta page
+SELECT live_items, dead_items, page_size, hasho_prevblkno, hasho_nextblkno,
+hasho_bucket, hasho_flag, hasho_page_id FROM
+hash_page_stats(get_raw_page('test_hash_a_idx', 0));
+ERROR: page is not a hash bucket or overflow page
+SELECT live_items, dead_items, page_size, hasho_prevblkno, hasho_nextblkno,
+hasho_bucket, hasho_flag, hasho_page_id FROM
+hash_page_stats(get_raw_page('test_hash_a_idx', 1));
+-[ RECORD 1 ]---+-----------
+live_items | 0
+dead_items | 0
+page_size | 8192
+hasho_prevblkno | 3
+hasho_nextblkno | 4294967295
+hasho_bucket | 0
+hasho_flag | 2
+hasho_page_id | 65408
+
+SELECT live_items, dead_items, page_size, hasho_prevblkno, hasho_nextblkno,
+hasho_bucket, hasho_flag, hasho_page_id FROM
+hash_page_stats(get_raw_page('test_hash_a_idx', 2));
+-[ RECORD 1 ]---+-----------
+live_items | 0
+dead_items | 0
+page_size | 8192
+hasho_prevblkno | 3
+hasho_nextblkno | 4294967295
+hasho_bucket | 1
+hasho_flag | 2
+hasho_page_id | 65408
+
+SELECT live_items, dead_items, page_size, hasho_prevblkno, hasho_nextblkno,
+hasho_bucket, hasho_flag, hasho_page_id FROM
+hash_page_stats(get_raw_page('test_hash_a_idx', 3));
+-[ RECORD 1 ]---+-----------
+live_items | 1
+dead_items | 0
+page_size | 8192
+hasho_prevblkno | 3
+hasho_nextblkno | 4294967295
+hasho_bucket | 2
+hasho_flag | 2
+hasho_page_id | 65408
+
+SELECT live_items, dead_items, page_size, hasho_prevblkno, hasho_nextblkno,
+hasho_bucket, hasho_flag, hasho_page_id FROM
+hash_page_stats(get_raw_page('test_hash_a_idx', 4));
+-[ RECORD 1 ]---+-----------
+live_items | 0
+dead_items | 0
+page_size | 8192
+hasho_prevblkno | 3
+hasho_nextblkno | 4294967295
+hasho_bucket | 3
+hasho_flag | 2
+hasho_page_id | 65408
+
+SELECT live_items, dead_items, page_size, hasho_prevblkno, hasho_nextblkno,
+hasho_bucket, hasho_flag, hasho_page_id FROM
+hash_page_stats(get_raw_page('test_hash_a_idx', 5));
+ERROR: page is not a hash bucket or overflow page
+SELECT * FROM hash_page_items(get_raw_page('test_hash_a_idx', 0));
+ERROR: page is not a hash bucket or overflow page
+SELECT * FROM hash_page_items(get_raw_page('test_hash_a_idx', 1));
+(0 rows)
+
+SELECT * FROM hash_page_items(get_raw_page('test_hash_a_idx', 2));
+(0 rows)
+
+SELECT * FROM hash_page_items(get_raw_page('test_hash_a_idx', 3));
+-[ RECORD 1 ]----------
+itemoffset | 1
+ctid | (0,1)
+data | 2389907270
+
+SELECT * FROM hash_page_items(get_raw_page('test_hash_a_idx', 4));
+(0 rows)
+
+SELECT * FROM hash_page_items(get_raw_page('test_hash_a_idx', 5));
+ERROR: page is not a hash bucket or overflow page
+-- Failure with non-hash index
+CREATE INDEX test_hash_a_btree ON test_hash USING btree (a);
+SELECT hash_bitmap_info('test_hash_a_btree', 0);
+ERROR: "test_hash_a_btree" is not a hash index
+-- Failure with various modes.
+-- Suppress the DETAIL message, to allow the tests to work across various
+-- page sizes and architectures.
+\set VERBOSITY terse
+-- invalid page size
+SELECT hash_metapage_info('aaa'::bytea);
+ERROR: invalid page size
+SELECT hash_page_items('bbb'::bytea);
+ERROR: invalid page size
+SELECT hash_page_stats('ccc'::bytea);
+ERROR: invalid page size
+SELECT hash_page_type('ddd'::bytea);
+ERROR: invalid page size
+-- invalid special area size
+SELECT hash_metapage_info(get_raw_page('test_hash', 0));
+ERROR: input page is not a valid hash page
+SELECT hash_page_items(get_raw_page('test_hash', 0));
+ERROR: input page is not a valid hash page
+SELECT hash_page_stats(get_raw_page('test_hash', 0));
+ERROR: input page is not a valid hash page
+SELECT hash_page_type(get_raw_page('test_hash', 0));
+ERROR: input page is not a valid hash page
+\set VERBOSITY default
+-- Tests with all-zero pages.
+SHOW block_size \gset
+SELECT hash_metapage_info(decode(repeat('00', :block_size), 'hex'));
+ERROR: page is not a hash meta page
+SELECT hash_page_items(decode(repeat('00', :block_size), 'hex'));
+ERROR: page is not a hash bucket or overflow page
+SELECT hash_page_stats(decode(repeat('00', :block_size), 'hex'));
+ERROR: page is not a hash bucket or overflow page
+SELECT hash_page_type(decode(repeat('00', :block_size), 'hex'));
+-[ RECORD 1 ]--+-------
+hash_page_type | unused
+
+DROP TABLE test_hash;
+DROP TABLE test_hash_part;
diff --git a/contrib/pageinspect/expected/oldextversions.out b/contrib/pageinspect/expected/oldextversions.out
new file mode 100644
index 0000000..f5c4b61
--- /dev/null
+++ b/contrib/pageinspect/expected/oldextversions.out
@@ -0,0 +1,56 @@
+-- test old extension version entry points
+DROP EXTENSION pageinspect;
+CREATE EXTENSION pageinspect VERSION '1.8';
+CREATE TABLE test1 (a int8, b text);
+INSERT INTO test1 VALUES (72057594037927937, 'text');
+CREATE INDEX test1_a_idx ON test1 USING btree (a);
+-- from page.sql
+SELECT octet_length(get_raw_page('test1', 0)) AS main_0;
+ main_0
+--------
+ 8192
+(1 row)
+
+SELECT octet_length(get_raw_page('test1', 'main', 0)) AS main_0;
+ main_0
+--------
+ 8192
+(1 row)
+
+SELECT page_checksum(get_raw_page('test1', 0), 0) IS NOT NULL AS silly_checksum_test;
+ silly_checksum_test
+---------------------
+ t
+(1 row)
+
+-- from btree.sql
+SELECT * FROM bt_page_stats('test1_a_idx', 1);
+ blkno | type | live_items | dead_items | avg_item_size | page_size | free_size | btpo_prev | btpo_next | btpo | btpo_flags
+-------+------+------------+------------+---------------+-----------+-----------+-----------+-----------+------+------------
+ 1 | l | 1 | 0 | 16 | 8192 | 8128 | 0 | 0 | 0 | 3
+(1 row)
+
+SELECT * FROM bt_page_items('test1_a_idx', 1);
+ itemoffset | ctid | itemlen | nulls | vars | data | dead | htid | tids
+------------+-------+---------+-------+------+-------------------------+------+-------+------
+ 1 | (0,1) | 16 | f | f | 01 00 00 00 00 00 00 01 | f | (0,1) |
+(1 row)
+
+-- page_header() uses int instead of smallint for lower, upper, special and
+-- pagesize in pageinspect >= 1.10.
+ALTER EXTENSION pageinspect UPDATE TO '1.9';
+\df page_header
+ List of functions
+ Schema | Name | Result data type | Argument data types | Type
+--------+-------------+------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------
+ public | page_header | record | page bytea, OUT lsn pg_lsn, OUT checksum smallint, OUT flags smallint, OUT lower smallint, OUT upper smallint, OUT special smallint, OUT pagesize smallint, OUT version smallint, OUT prune_xid xid | func
+(1 row)
+
+SELECT pagesize, version FROM page_header(get_raw_page('test1', 0));
+ pagesize | version
+----------+---------
+ 8192 | 4
+(1 row)
+
+DROP TABLE test1;
+DROP EXTENSION pageinspect;
diff --git a/contrib/pageinspect/expected/page.out b/contrib/pageinspect/expected/page.out
new file mode 100644
index 0000000..80ddb45
--- /dev/null
+++ b/contrib/pageinspect/expected/page.out
@@ -0,0 +1,241 @@
+CREATE EXTENSION pageinspect;
+-- Use a temp table so that effects of VACUUM are predictable
+CREATE TEMP TABLE test1 (a int, b int);
+INSERT INTO test1 VALUES (16777217, 131584);
+VACUUM (DISABLE_PAGE_SKIPPING) test1; -- set up FSM
+-- The page contents can vary, so just test that it can be read
+-- successfully, but don't keep the output.
+SELECT octet_length(get_raw_page('test1', 'main', 0)) AS main_0;
+ main_0
+--------
+ 8192
+(1 row)
+
+SELECT octet_length(get_raw_page('test1', 'main', 1)) AS main_1;
+ERROR: block number 1 is out of range for relation "test1"
+SELECT octet_length(get_raw_page('test1', 'fsm', 0)) AS fsm_0;
+ fsm_0
+-------
+ 8192
+(1 row)
+
+SELECT octet_length(get_raw_page('test1', 'fsm', 1)) AS fsm_1;
+ fsm_1
+-------
+ 8192
+(1 row)
+
+SELECT octet_length(get_raw_page('test1', 'vm', 0)) AS vm_0;
+ vm_0
+------
+ 8192
+(1 row)
+
+SELECT octet_length(get_raw_page('test1', 'vm', 1)) AS vm_1;
+ERROR: block number 1 is out of range for relation "test1"
+SELECT octet_length(get_raw_page('test1', 'main', -1));
+ERROR: invalid block number
+SELECT octet_length(get_raw_page('xxx', 'main', 0));
+ERROR: relation "xxx" does not exist
+SELECT octet_length(get_raw_page('test1', 'xxx', 0));
+ERROR: invalid fork name
+HINT: Valid fork names are "main", "fsm", "vm", and "init".
+SELECT get_raw_page('test1', 0) = get_raw_page('test1', 'main', 0);
+ ?column?
+----------
+ t
+(1 row)
+
+SELECT pagesize, version FROM page_header(get_raw_page('test1', 0));
+ pagesize | version
+----------+---------
+ 8192 | 4
+(1 row)
+
+SELECT page_checksum(get_raw_page('test1', 0), 0) IS NOT NULL AS silly_checksum_test;
+ silly_checksum_test
+---------------------
+ t
+(1 row)
+
+SELECT page_checksum(get_raw_page('test1', 0), -1);
+ERROR: invalid block number
+SELECT tuple_data_split('test1'::regclass, t_data, t_infomask, t_infomask2, t_bits)
+ FROM heap_page_items(get_raw_page('test1', 0));
+ tuple_data_split
+-------------------------------
+ {"\\x01000001","\\x00020200"}
+(1 row)
+
+SELECT * FROM fsm_page_contents(get_raw_page('test1', 'fsm', 0));
+ fsm_page_contents
+-------------------
+ 0: 254 +
+ 1: 254 +
+ 3: 254 +
+ 7: 254 +
+ 15: 254 +
+ 31: 254 +
+ 63: 254 +
+ 127: 254 +
+ 255: 254 +
+ 511: 254 +
+ 1023: 254 +
+ 2047: 254 +
+ 4095: 254 +
+ fp_next_slot: 0 +
+
+(1 row)
+
+-- If we freeze the only tuple on test1, the infomask should
+-- always be the same in all test runs.
+VACUUM (FREEZE, DISABLE_PAGE_SKIPPING) test1;
+SELECT t_infomask, t_infomask2, raw_flags, combined_flags
+FROM heap_page_items(get_raw_page('test1', 0)),
+ LATERAL heap_tuple_infomask_flags(t_infomask, t_infomask2);
+ t_infomask | t_infomask2 | raw_flags | combined_flags
+------------+-------------+-----------------------------------------------------------+--------------------
+ 2816 | 2 | {HEAP_XMIN_COMMITTED,HEAP_XMIN_INVALID,HEAP_XMAX_INVALID} | {HEAP_XMIN_FROZEN}
+(1 row)
+
+-- tests for decoding of combined flags
+-- HEAP_XMAX_SHR_LOCK = (HEAP_XMAX_EXCL_LOCK | HEAP_XMAX_KEYSHR_LOCK)
+SELECT * FROM heap_tuple_infomask_flags(x'0050'::int, 0);
+ raw_flags | combined_flags
+---------------------------------------------+----------------------
+ {HEAP_XMAX_KEYSHR_LOCK,HEAP_XMAX_EXCL_LOCK} | {HEAP_XMAX_SHR_LOCK}
+(1 row)
+
+-- HEAP_XMIN_FROZEN = (HEAP_XMIN_COMMITTED | HEAP_XMIN_INVALID)
+SELECT * FROM heap_tuple_infomask_flags(x'0300'::int, 0);
+ raw_flags | combined_flags
+-----------------------------------------+--------------------
+ {HEAP_XMIN_COMMITTED,HEAP_XMIN_INVALID} | {HEAP_XMIN_FROZEN}
+(1 row)
+
+-- HEAP_MOVED = (HEAP_MOVED_IN | HEAP_MOVED_OFF)
+SELECT * FROM heap_tuple_infomask_flags(x'C000'::int, 0);
+ raw_flags | combined_flags
+--------------------------------+----------------
+ {HEAP_MOVED_OFF,HEAP_MOVED_IN} | {HEAP_MOVED}
+(1 row)
+
+SELECT * FROM heap_tuple_infomask_flags(x'C000'::int, 0);
+ raw_flags | combined_flags
+--------------------------------+----------------
+ {HEAP_MOVED_OFF,HEAP_MOVED_IN} | {HEAP_MOVED}
+(1 row)
+
+-- test all flags of t_infomask and t_infomask2
+SELECT unnest(raw_flags)
+ FROM heap_tuple_infomask_flags(x'FFFF'::int, x'FFFF'::int) ORDER BY 1;
+ unnest
+-----------------------
+ HEAP_COMBOCID
+ HEAP_HASEXTERNAL
+ HEAP_HASNULL
+ HEAP_HASOID_OLD
+ HEAP_HASVARWIDTH
+ HEAP_HOT_UPDATED
+ HEAP_KEYS_UPDATED
+ HEAP_MOVED_IN
+ HEAP_MOVED_OFF
+ HEAP_ONLY_TUPLE
+ HEAP_UPDATED
+ HEAP_XMAX_COMMITTED
+ HEAP_XMAX_EXCL_LOCK
+ HEAP_XMAX_INVALID
+ HEAP_XMAX_IS_MULTI
+ HEAP_XMAX_KEYSHR_LOCK
+ HEAP_XMAX_LOCK_ONLY
+ HEAP_XMIN_COMMITTED
+ HEAP_XMIN_INVALID
+(19 rows)
+
+SELECT unnest(combined_flags)
+ FROM heap_tuple_infomask_flags(x'FFFF'::int, x'FFFF'::int) ORDER BY 1;
+ unnest
+--------------------
+ HEAP_MOVED
+ HEAP_XMAX_SHR_LOCK
+ HEAP_XMIN_FROZEN
+(3 rows)
+
+-- no flags at all
+SELECT * FROM heap_tuple_infomask_flags(0, 0);
+ raw_flags | combined_flags
+-----------+----------------
+ {} | {}
+(1 row)
+
+-- no combined flags
+SELECT * FROM heap_tuple_infomask_flags(x'0010'::int, 0);
+ raw_flags | combined_flags
+-------------------------+----------------
+ {HEAP_XMAX_KEYSHR_LOCK} | {}
+(1 row)
+
+DROP TABLE test1;
+-- check that using any of these functions with a partitioned table or index
+-- would fail
+create table test_partitioned (a int) partition by range (a);
+create index test_partitioned_index on test_partitioned (a);
+select get_raw_page('test_partitioned', 0); -- error about partitioned table
+ERROR: cannot get raw page from relation "test_partitioned"
+DETAIL: This operation is not supported for partitioned tables.
+select get_raw_page('test_partitioned_index', 0); -- error about partitioned index
+ERROR: cannot get raw page from relation "test_partitioned_index"
+DETAIL: This operation is not supported for partitioned indexes.
+-- a regular table which is a member of a partition set should work though
+create table test_part1 partition of test_partitioned for values from ( 1 ) to (100);
+select get_raw_page('test_part1', 0); -- get farther and error about empty table
+ERROR: block number 0 is out of range for relation "test_part1"
+drop table test_partitioned;
+-- check null bitmap alignment for table whose number of attributes is multiple of 8
+create table test8 (f1 int, f2 int, f3 int, f4 int, f5 int, f6 int, f7 int, f8 int);
+insert into test8(f1, f8) values (x'7f00007f'::int, 0);
+select t_bits, t_data from heap_page_items(get_raw_page('test8', 0));
+ t_bits | t_data
+----------+--------------------
+ 10000001 | \x7f00007f00000000
+(1 row)
+
+select tuple_data_split('test8'::regclass, t_data, t_infomask, t_infomask2, t_bits)
+ from heap_page_items(get_raw_page('test8', 0));
+ tuple_data_split
+-------------------------------------------------------------
+ {"\\x7f00007f",NULL,NULL,NULL,NULL,NULL,NULL,"\\x00000000"}
+(1 row)
+
+drop table test8;
+-- Failure with incorrect page size
+-- Suppress the DETAIL message, to allow the tests to work across various
+-- page sizes.
+\set VERBOSITY terse
+SELECT fsm_page_contents('aaa'::bytea);
+ERROR: invalid page size
+SELECT page_checksum('bbb'::bytea, 0);
+ERROR: invalid page size
+SELECT page_header('ccc'::bytea);
+ERROR: invalid page size
+\set VERBOSITY default
+-- Tests with all-zero pages.
+SHOW block_size \gset
+SELECT fsm_page_contents(decode(repeat('00', :block_size), 'hex'));
+ fsm_page_contents
+-------------------
+
+(1 row)
+
+SELECT page_header(decode(repeat('00', :block_size), 'hex'));
+ page_header
+-----------------------
+ (0/0,0,0,0,0,0,0,0,0)
+(1 row)
+
+SELECT page_checksum(decode(repeat('00', :block_size), 'hex'), 1);
+ page_checksum
+---------------
+
+(1 row)
+
diff --git a/contrib/pageinspect/fsmfuncs.c b/contrib/pageinspect/fsmfuncs.c
new file mode 100644
index 0000000..23b192b
--- /dev/null
+++ b/contrib/pageinspect/fsmfuncs.c
@@ -0,0 +1,65 @@
+/*-------------------------------------------------------------------------
+ *
+ * fsmfuncs.c
+ * Functions to investigate FSM pages
+ *
+ * These functions are restricted to superusers for the fear of introducing
+ * security holes if the input checking isn't as water-tight as it should.
+ * You'd need to be superuser to obtain a raw page image anyway, so
+ * there's hardly any use case for using these without superuser-rights
+ * anyway.
+ *
+ * Copyright (c) 2007-2023, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pageinspect/fsmfuncs.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "funcapi.h"
+#include "lib/stringinfo.h"
+#include "miscadmin.h"
+#include "pageinspect.h"
+#include "storage/fsm_internals.h"
+#include "utils/builtins.h"
+
+/*
+ * Dumps the contents of a FSM page.
+ */
+PG_FUNCTION_INFO_V1(fsm_page_contents);
+
+Datum
+fsm_page_contents(PG_FUNCTION_ARGS)
+{
+ bytea *raw_page = PG_GETARG_BYTEA_P(0);
+ StringInfoData sinfo;
+ Page page;
+ FSMPage fsmpage;
+ int i;
+
+ if (!superuser())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions")));
+
+ page = get_page_from_raw(raw_page);
+
+ if (PageIsNew(page))
+ PG_RETURN_NULL();
+
+ fsmpage = (FSMPage) PageGetContents(page);
+
+ initStringInfo(&sinfo);
+
+ for (i = 0; i < NodesPerPage; i++)
+ {
+ if (fsmpage->fp_nodes[i] != 0)
+ appendStringInfo(&sinfo, "%d: %d\n", i, fsmpage->fp_nodes[i]);
+ }
+ appendStringInfo(&sinfo, "fp_next_slot: %d\n", fsmpage->fp_next_slot);
+
+ PG_RETURN_TEXT_P(cstring_to_text_with_len(sinfo.data, sinfo.len));
+}
diff --git a/contrib/pageinspect/ginfuncs.c b/contrib/pageinspect/ginfuncs.c
new file mode 100644
index 0000000..0f84698
--- /dev/null
+++ b/contrib/pageinspect/ginfuncs.c
@@ -0,0 +1,285 @@
+/*
+ * ginfuncs.c
+ * Functions to investigate the content of GIN indexes
+ *
+ * Copyright (c) 2014-2023, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pageinspect/ginfuncs.c
+ */
+#include "postgres.h"
+
+#include "access/gin.h"
+#include "access/gin_private.h"
+#include "access/htup_details.h"
+#include "catalog/namespace.h"
+#include "catalog/pg_type.h"
+#include "funcapi.h"
+#include "miscadmin.h"
+#include "pageinspect.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/rel.h"
+
+
+PG_FUNCTION_INFO_V1(gin_metapage_info);
+PG_FUNCTION_INFO_V1(gin_page_opaque_info);
+PG_FUNCTION_INFO_V1(gin_leafpage_items);
+
+
+Datum
+gin_metapage_info(PG_FUNCTION_ARGS)
+{
+ bytea *raw_page = PG_GETARG_BYTEA_P(0);
+ TupleDesc tupdesc;
+ Page page;
+ GinPageOpaque opaq;
+ GinMetaPageData *metadata;
+ HeapTuple resultTuple;
+ Datum values[10];
+ bool nulls[10];
+
+ if (!superuser())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions")));
+
+ page = get_page_from_raw(raw_page);
+
+ if (PageIsNew(page))
+ PG_RETURN_NULL();
+
+ if (PageGetSpecialSize(page) != MAXALIGN(sizeof(GinPageOpaqueData)))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("input page is not a valid GIN metapage"),
+ errdetail("Expected special size %d, got %d.",
+ (int) MAXALIGN(sizeof(GinPageOpaqueData)),
+ (int) PageGetSpecialSize(page))));
+
+ opaq = GinPageGetOpaque(page);
+
+ if (opaq->flags != GIN_META)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("input page is not a GIN metapage"),
+ errdetail("Flags %04X, expected %04X",
+ opaq->flags, GIN_META)));
+
+ /* Build a tuple descriptor for our result type */
+ if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+ elog(ERROR, "return type must be a row type");
+
+ metadata = GinPageGetMeta(page);
+
+ memset(nulls, 0, sizeof(nulls));
+
+ values[0] = Int64GetDatum(metadata->head);
+ values[1] = Int64GetDatum(metadata->tail);
+ values[2] = Int32GetDatum(metadata->tailFreeSize);
+ values[3] = Int64GetDatum(metadata->nPendingPages);
+ values[4] = Int64GetDatum(metadata->nPendingHeapTuples);
+
+ /* statistics, updated by VACUUM */
+ values[5] = Int64GetDatum(metadata->nTotalPages);
+ values[6] = Int64GetDatum(metadata->nEntryPages);
+ values[7] = Int64GetDatum(metadata->nDataPages);
+ values[8] = Int64GetDatum(metadata->nEntries);
+
+ values[9] = Int32GetDatum(metadata->ginVersion);
+
+ /* Build and return the result tuple. */
+ resultTuple = heap_form_tuple(tupdesc, values, nulls);
+
+ return HeapTupleGetDatum(resultTuple);
+}
+
+
+Datum
+gin_page_opaque_info(PG_FUNCTION_ARGS)
+{
+ bytea *raw_page = PG_GETARG_BYTEA_P(0);
+ TupleDesc tupdesc;
+ Page page;
+ GinPageOpaque opaq;
+ HeapTuple resultTuple;
+ Datum values[3];
+ bool nulls[3];
+ Datum flags[16];
+ int nflags = 0;
+ uint16 flagbits;
+
+ if (!superuser())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions")));
+
+ page = get_page_from_raw(raw_page);
+
+ if (PageIsNew(page))
+ PG_RETURN_NULL();
+
+ if (PageGetSpecialSize(page) != MAXALIGN(sizeof(GinPageOpaqueData)))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("input page is not a valid GIN data leaf page"),
+ errdetail("Expected special size %d, got %d.",
+ (int) MAXALIGN(sizeof(GinPageOpaqueData)),
+ (int) PageGetSpecialSize(page))));
+
+ opaq = GinPageGetOpaque(page);
+
+ /* Build a tuple descriptor for our result type */
+ if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+ elog(ERROR, "return type must be a row type");
+
+ /* Convert the flags bitmask to an array of human-readable names */
+ flagbits = opaq->flags;
+ if (flagbits & GIN_DATA)
+ flags[nflags++] = CStringGetTextDatum("data");
+ if (flagbits & GIN_LEAF)
+ flags[nflags++] = CStringGetTextDatum("leaf");
+ if (flagbits & GIN_DELETED)
+ flags[nflags++] = CStringGetTextDatum("deleted");
+ if (flagbits & GIN_META)
+ flags[nflags++] = CStringGetTextDatum("meta");
+ if (flagbits & GIN_LIST)
+ flags[nflags++] = CStringGetTextDatum("list");
+ if (flagbits & GIN_LIST_FULLROW)
+ flags[nflags++] = CStringGetTextDatum("list_fullrow");
+ if (flagbits & GIN_INCOMPLETE_SPLIT)
+ flags[nflags++] = CStringGetTextDatum("incomplete_split");
+ if (flagbits & GIN_COMPRESSED)
+ flags[nflags++] = CStringGetTextDatum("compressed");
+ flagbits &= ~(GIN_DATA | GIN_LEAF | GIN_DELETED | GIN_META | GIN_LIST |
+ GIN_LIST_FULLROW | GIN_INCOMPLETE_SPLIT | GIN_COMPRESSED);
+ if (flagbits)
+ {
+ /* any flags we don't recognize are printed in hex */
+ flags[nflags++] = DirectFunctionCall1(to_hex32, Int32GetDatum(flagbits));
+ }
+
+ memset(nulls, 0, sizeof(nulls));
+
+ values[0] = Int64GetDatum(opaq->rightlink);
+ values[1] = Int32GetDatum(opaq->maxoff);
+ values[2] = PointerGetDatum(construct_array_builtin(flags, nflags, TEXTOID));
+
+ /* Build and return the result tuple. */
+ resultTuple = heap_form_tuple(tupdesc, values, nulls);
+
+ return HeapTupleGetDatum(resultTuple);
+}
+
+typedef struct gin_leafpage_items_state
+{
+ TupleDesc tupd;
+ GinPostingList *seg;
+ GinPostingList *lastseg;
+} gin_leafpage_items_state;
+
+Datum
+gin_leafpage_items(PG_FUNCTION_ARGS)
+{
+ bytea *raw_page = PG_GETARG_BYTEA_P(0);
+ FuncCallContext *fctx;
+ gin_leafpage_items_state *inter_call_data;
+
+ if (!superuser())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions")));
+
+ if (SRF_IS_FIRSTCALL())
+ {
+ TupleDesc tupdesc;
+ MemoryContext mctx;
+ Page page;
+ GinPageOpaque opaq;
+
+ fctx = SRF_FIRSTCALL_INIT();
+ mctx = MemoryContextSwitchTo(fctx->multi_call_memory_ctx);
+
+ page = get_page_from_raw(raw_page);
+
+ if (PageIsNew(page))
+ {
+ MemoryContextSwitchTo(mctx);
+ PG_RETURN_NULL();
+ }
+
+ if (PageGetSpecialSize(page) != MAXALIGN(sizeof(GinPageOpaqueData)))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("input page is not a valid GIN data leaf page"),
+ errdetail("Expected special size %d, got %d.",
+ (int) MAXALIGN(sizeof(GinPageOpaqueData)),
+ (int) PageGetSpecialSize(page))));
+
+ opaq = GinPageGetOpaque(page);
+ if (opaq->flags != (GIN_DATA | GIN_LEAF | GIN_COMPRESSED))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("input page is not a compressed GIN data leaf page"),
+ errdetail("Flags %04X, expected %04X",
+ opaq->flags,
+ (GIN_DATA | GIN_LEAF | GIN_COMPRESSED))));
+
+ inter_call_data = palloc(sizeof(gin_leafpage_items_state));
+
+ /* Build a tuple descriptor for our result type */
+ if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+ elog(ERROR, "return type must be a row type");
+
+ inter_call_data->tupd = tupdesc;
+
+ inter_call_data->seg = GinDataLeafPageGetPostingList(page);
+ inter_call_data->lastseg = (GinPostingList *)
+ (((char *) inter_call_data->seg) +
+ GinDataLeafPageGetPostingListSize(page));
+
+ fctx->user_fctx = inter_call_data;
+
+ MemoryContextSwitchTo(mctx);
+ }
+
+ fctx = SRF_PERCALL_SETUP();
+ inter_call_data = fctx->user_fctx;
+
+ if (inter_call_data->seg != inter_call_data->lastseg)
+ {
+ GinPostingList *cur = inter_call_data->seg;
+ HeapTuple resultTuple;
+ Datum result;
+ Datum values[3];
+ bool nulls[3];
+ int ndecoded,
+ i;
+ ItemPointer tids;
+ Datum *tids_datum;
+
+ memset(nulls, 0, sizeof(nulls));
+
+ values[0] = ItemPointerGetDatum(&cur->first);
+ values[1] = UInt16GetDatum(cur->nbytes);
+
+ /* build an array of decoded item pointers */
+ tids = ginPostingListDecode(cur, &ndecoded);
+ tids_datum = (Datum *) palloc(ndecoded * sizeof(Datum));
+ for (i = 0; i < ndecoded; i++)
+ tids_datum[i] = ItemPointerGetDatum(&tids[i]);
+ values[2] = PointerGetDatum(construct_array_builtin(tids_datum, ndecoded, TIDOID));
+ pfree(tids_datum);
+ pfree(tids);
+
+ /* Build and return the result tuple. */
+ resultTuple = heap_form_tuple(inter_call_data->tupd, values, nulls);
+ result = HeapTupleGetDatum(resultTuple);
+
+ inter_call_data->seg = GinNextPostingListSegment(cur);
+
+ SRF_RETURN_NEXT(fctx, result);
+ }
+
+ SRF_RETURN_DONE(fctx);
+}
diff --git a/contrib/pageinspect/gistfuncs.c b/contrib/pageinspect/gistfuncs.c
new file mode 100644
index 0000000..5512b00
--- /dev/null
+++ b/contrib/pageinspect/gistfuncs.c
@@ -0,0 +1,369 @@
+/*
+ * gistfuncs.c
+ * Functions to investigate the content of GiST indexes
+ *
+ * Copyright (c) 2014-2023, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pageinspect/gistfuncs.c
+ */
+#include "postgres.h"
+
+#include "access/gist.h"
+#include "access/gist_private.h"
+#include "access/htup.h"
+#include "access/relation.h"
+#include "catalog/namespace.h"
+#include "catalog/pg_am_d.h"
+#include "funcapi.h"
+#include "miscadmin.h"
+#include "pageinspect.h"
+#include "storage/itemptr.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/pg_lsn.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/ruleutils.h"
+#include "utils/varlena.h"
+
+PG_FUNCTION_INFO_V1(gist_page_opaque_info);
+PG_FUNCTION_INFO_V1(gist_page_items);
+PG_FUNCTION_INFO_V1(gist_page_items_bytea);
+
+#define IS_GIST(r) ((r)->rd_rel->relam == GIST_AM_OID)
+
+
+static Page verify_gist_page(bytea *raw_page);
+
+/*
+ * Verify that the given bytea contains a GIST page or die in the attempt.
+ * A pointer to the page is returned.
+ */
+static Page
+verify_gist_page(bytea *raw_page)
+{
+ Page page = get_page_from_raw(raw_page);
+ GISTPageOpaque opaq;
+
+ if (PageIsNew(page))
+ return page;
+
+ /* verify the special space has the expected size */
+ if (PageGetSpecialSize(page) != MAXALIGN(sizeof(GISTPageOpaqueData)))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("input page is not a valid %s page", "GiST"),
+ errdetail("Expected special size %d, got %d.",
+ (int) MAXALIGN(sizeof(GISTPageOpaqueData)),
+ (int) PageGetSpecialSize(page))));
+
+ opaq = GistPageGetOpaque(page);
+ if (opaq->gist_page_id != GIST_PAGE_ID)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("input page is not a valid %s page", "GiST"),
+ errdetail("Expected %08x, got %08x.",
+ GIST_PAGE_ID,
+ opaq->gist_page_id)));
+
+ return page;
+}
+
+Datum
+gist_page_opaque_info(PG_FUNCTION_ARGS)
+{
+ bytea *raw_page = PG_GETARG_BYTEA_P(0);
+ TupleDesc tupdesc;
+ Page page;
+ HeapTuple resultTuple;
+ Datum values[4];
+ bool nulls[4];
+ Datum flags[16];
+ int nflags = 0;
+ uint16 flagbits;
+
+ if (!superuser())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions")));
+
+ page = verify_gist_page(raw_page);
+
+ if (PageIsNew(page))
+ PG_RETURN_NULL();
+
+ /* Build a tuple descriptor for our result type */
+ if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+ elog(ERROR, "return type must be a row type");
+
+ /* Convert the flags bitmask to an array of human-readable names */
+ flagbits = GistPageGetOpaque(page)->flags;
+ if (flagbits & F_LEAF)
+ flags[nflags++] = CStringGetTextDatum("leaf");
+ if (flagbits & F_DELETED)
+ flags[nflags++] = CStringGetTextDatum("deleted");
+ if (flagbits & F_TUPLES_DELETED)
+ flags[nflags++] = CStringGetTextDatum("tuples_deleted");
+ if (flagbits & F_FOLLOW_RIGHT)
+ flags[nflags++] = CStringGetTextDatum("follow_right");
+ if (flagbits & F_HAS_GARBAGE)
+ flags[nflags++] = CStringGetTextDatum("has_garbage");
+ flagbits &= ~(F_LEAF | F_DELETED | F_TUPLES_DELETED | F_FOLLOW_RIGHT | F_HAS_GARBAGE);
+ if (flagbits)
+ {
+ /* any flags we don't recognize are printed in hex */
+ flags[nflags++] = DirectFunctionCall1(to_hex32, Int32GetDatum(flagbits));
+ }
+
+ memset(nulls, 0, sizeof(nulls));
+
+ values[0] = LSNGetDatum(PageGetLSN(page));
+ values[1] = LSNGetDatum(GistPageGetNSN(page));
+ values[2] = Int64GetDatum(GistPageGetOpaque(page)->rightlink);
+ values[3] = PointerGetDatum(construct_array_builtin(flags, nflags, TEXTOID));
+
+ /* Build and return the result tuple. */
+ resultTuple = heap_form_tuple(tupdesc, values, nulls);
+
+ return HeapTupleGetDatum(resultTuple);
+}
+
+Datum
+gist_page_items_bytea(PG_FUNCTION_ARGS)
+{
+ bytea *raw_page = PG_GETARG_BYTEA_P(0);
+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ Page page;
+ OffsetNumber offset;
+ OffsetNumber maxoff = InvalidOffsetNumber;
+
+ if (!superuser())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions")));
+
+ InitMaterializedSRF(fcinfo, 0);
+
+ page = verify_gist_page(raw_page);
+
+ if (PageIsNew(page))
+ PG_RETURN_NULL();
+
+ /* Avoid bogus PageGetMaxOffsetNumber() call with deleted pages */
+ if (GistPageIsDeleted(page))
+ elog(NOTICE, "page is deleted");
+ else
+ maxoff = PageGetMaxOffsetNumber(page);
+
+ for (offset = FirstOffsetNumber;
+ offset <= maxoff;
+ offset++)
+ {
+ Datum values[5];
+ bool nulls[5];
+ ItemId id;
+ IndexTuple itup;
+ bytea *tuple_bytea;
+ int tuple_len;
+
+ id = PageGetItemId(page, offset);
+
+ if (!ItemIdIsValid(id))
+ elog(ERROR, "invalid ItemId");
+
+ itup = (IndexTuple) PageGetItem(page, id);
+ tuple_len = IndexTupleSize(itup);
+
+ memset(nulls, 0, sizeof(nulls));
+
+ values[0] = DatumGetInt16(offset);
+ values[1] = ItemPointerGetDatum(&itup->t_tid);
+ values[2] = Int32GetDatum((int) IndexTupleSize(itup));
+
+ tuple_bytea = (bytea *) palloc(tuple_len + VARHDRSZ);
+ SET_VARSIZE(tuple_bytea, tuple_len + VARHDRSZ);
+ memcpy(VARDATA(tuple_bytea), itup, tuple_len);
+ values[3] = BoolGetDatum(ItemIdIsDead(id));
+ values[4] = PointerGetDatum(tuple_bytea);
+
+ tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
+ }
+
+ return (Datum) 0;
+}
+
+Datum
+gist_page_items(PG_FUNCTION_ARGS)
+{
+ bytea *raw_page = PG_GETARG_BYTEA_P(0);
+ Oid indexRelid = PG_GETARG_OID(1);
+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ Relation indexRel;
+ TupleDesc tupdesc;
+ Page page;
+ uint16 flagbits;
+ bits16 printflags = 0;
+ OffsetNumber offset;
+ OffsetNumber maxoff = InvalidOffsetNumber;
+ char *index_columns;
+
+ if (!superuser())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions")));
+
+ InitMaterializedSRF(fcinfo, 0);
+
+ /* Open the relation */
+ indexRel = index_open(indexRelid, AccessShareLock);
+
+ if (!IS_GIST(indexRel))
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is not a %s index",
+ RelationGetRelationName(indexRel), "GiST")));
+
+ page = verify_gist_page(raw_page);
+
+ if (PageIsNew(page))
+ {
+ index_close(indexRel, AccessShareLock);
+ PG_RETURN_NULL();
+ }
+
+ flagbits = GistPageGetOpaque(page)->flags;
+
+ /*
+ * Included attributes are added when dealing with leaf pages, discarded
+ * for non-leaf pages as these include only data for key attributes.
+ */
+ printflags |= RULE_INDEXDEF_PRETTY;
+ if (flagbits & F_LEAF)
+ {
+ tupdesc = RelationGetDescr(indexRel);
+ }
+ else
+ {
+ tupdesc = CreateTupleDescCopy(RelationGetDescr(indexRel));
+ tupdesc->natts = IndexRelationGetNumberOfKeyAttributes(indexRel);
+ printflags |= RULE_INDEXDEF_KEYS_ONLY;
+ }
+
+ index_columns = pg_get_indexdef_columns_extended(indexRelid,
+ printflags);
+
+ /* Avoid bogus PageGetMaxOffsetNumber() call with deleted pages */
+ if (GistPageIsDeleted(page))
+ elog(NOTICE, "page is deleted");
+ else
+ maxoff = PageGetMaxOffsetNumber(page);
+
+ for (offset = FirstOffsetNumber;
+ offset <= maxoff;
+ offset++)
+ {
+ Datum values[5];
+ bool nulls[5];
+ ItemId id;
+ IndexTuple itup;
+ Datum itup_values[INDEX_MAX_KEYS];
+ bool itup_isnull[INDEX_MAX_KEYS];
+ StringInfoData buf;
+ int i;
+
+ id = PageGetItemId(page, offset);
+
+ if (!ItemIdIsValid(id))
+ elog(ERROR, "invalid ItemId");
+
+ itup = (IndexTuple) PageGetItem(page, id);
+
+ index_deform_tuple(itup, tupdesc,
+ itup_values, itup_isnull);
+
+ memset(nulls, 0, sizeof(nulls));
+
+ values[0] = DatumGetInt16(offset);
+ values[1] = ItemPointerGetDatum(&itup->t_tid);
+ values[2] = Int32GetDatum((int) IndexTupleSize(itup));
+ values[3] = BoolGetDatum(ItemIdIsDead(id));
+
+ if (index_columns)
+ {
+ initStringInfo(&buf);
+ appendStringInfo(&buf, "(%s)=(", index_columns);
+
+ /* Most of this is copied from record_out(). */
+ for (i = 0; i < tupdesc->natts; i++)
+ {
+ char *value;
+ char *tmp;
+ bool nq = false;
+
+ if (itup_isnull[i])
+ value = "null";
+ else
+ {
+ Oid foutoid;
+ bool typisvarlena;
+ Oid typoid;
+
+ typoid = tupdesc->attrs[i].atttypid;
+ getTypeOutputInfo(typoid, &foutoid, &typisvarlena);
+ value = OidOutputFunctionCall(foutoid, itup_values[i]);
+ }
+
+ if (i == IndexRelationGetNumberOfKeyAttributes(indexRel))
+ appendStringInfoString(&buf, ") INCLUDE (");
+ else if (i > 0)
+ appendStringInfoString(&buf, ", ");
+
+ /* Check whether we need double quotes for this value */
+ nq = (value[0] == '\0'); /* force quotes for empty string */
+ for (tmp = value; *tmp; tmp++)
+ {
+ char ch = *tmp;
+
+ if (ch == '"' || ch == '\\' ||
+ ch == '(' || ch == ')' || ch == ',' ||
+ isspace((unsigned char) ch))
+ {
+ nq = true;
+ break;
+ }
+ }
+
+ /* And emit the string */
+ if (nq)
+ appendStringInfoCharMacro(&buf, '"');
+ for (tmp = value; *tmp; tmp++)
+ {
+ char ch = *tmp;
+
+ if (ch == '"' || ch == '\\')
+ appendStringInfoCharMacro(&buf, ch);
+ appendStringInfoCharMacro(&buf, ch);
+ }
+ if (nq)
+ appendStringInfoCharMacro(&buf, '"');
+ }
+
+ appendStringInfoChar(&buf, ')');
+
+ values[4] = CStringGetTextDatum(buf.data);
+ nulls[4] = false;
+ }
+ else
+ {
+ values[4] = (Datum) 0;
+ nulls[4] = true;
+ }
+
+ tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
+ }
+
+ relation_close(indexRel, AccessShareLock);
+
+ return (Datum) 0;
+}
diff --git a/contrib/pageinspect/hashfuncs.c b/contrib/pageinspect/hashfuncs.c
new file mode 100644
index 0000000..5c38ec1
--- /dev/null
+++ b/contrib/pageinspect/hashfuncs.c
@@ -0,0 +1,570 @@
+/*
+ * hashfuncs.c
+ * Functions to investigate the content of HASH indexes
+ *
+ * Copyright (c) 2017-2023, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pageinspect/hashfuncs.c
+ */
+
+#include "postgres.h"
+
+#include "access/hash.h"
+#include "access/htup_details.h"
+#include "access/relation.h"
+#include "catalog/pg_am.h"
+#include "catalog/pg_type.h"
+#include "funcapi.h"
+#include "miscadmin.h"
+#include "pageinspect.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/rel.h"
+
+PG_FUNCTION_INFO_V1(hash_page_type);
+PG_FUNCTION_INFO_V1(hash_page_stats);
+PG_FUNCTION_INFO_V1(hash_page_items);
+PG_FUNCTION_INFO_V1(hash_bitmap_info);
+PG_FUNCTION_INFO_V1(hash_metapage_info);
+
+#define IS_INDEX(r) ((r)->rd_rel->relkind == RELKIND_INDEX)
+#define IS_HASH(r) ((r)->rd_rel->relam == HASH_AM_OID)
+
+/* ------------------------------------------------
+ * structure for single hash page statistics
+ * ------------------------------------------------
+ */
+typedef struct HashPageStat
+{
+ int live_items;
+ int dead_items;
+ int page_size;
+ int free_size;
+
+ /* opaque data */
+ BlockNumber hasho_prevblkno;
+ BlockNumber hasho_nextblkno;
+ Bucket hasho_bucket;
+ uint16 hasho_flag;
+ uint16 hasho_page_id;
+} HashPageStat;
+
+
+/*
+ * Verify that the given bytea contains a HASH page, or die in the attempt.
+ * A pointer to a palloc'd, properly aligned copy of the page is returned.
+ */
+static Page
+verify_hash_page(bytea *raw_page, int flags)
+{
+ Page page = get_page_from_raw(raw_page);
+ int pagetype = LH_UNUSED_PAGE;
+
+ /* Treat new pages as unused. */
+ if (!PageIsNew(page))
+ {
+ HashPageOpaque pageopaque;
+
+ if (PageGetSpecialSize(page) != MAXALIGN(sizeof(HashPageOpaqueData)))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("input page is not a valid %s page", "hash"),
+ errdetail("Expected special size %d, got %d.",
+ (int) MAXALIGN(sizeof(HashPageOpaqueData)),
+ (int) PageGetSpecialSize(page))));
+
+ pageopaque = HashPageGetOpaque(page);
+ if (pageopaque->hasho_page_id != HASHO_PAGE_ID)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("input page is not a valid %s page", "hash"),
+ errdetail("Expected %08x, got %08x.",
+ HASHO_PAGE_ID, pageopaque->hasho_page_id)));
+
+ pagetype = pageopaque->hasho_flag & LH_PAGE_TYPE;
+ }
+
+ /* Check that page type is sane. */
+ if (pagetype != LH_OVERFLOW_PAGE && pagetype != LH_BUCKET_PAGE &&
+ pagetype != LH_BITMAP_PAGE && pagetype != LH_META_PAGE &&
+ pagetype != LH_UNUSED_PAGE)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("invalid hash page type %08x", pagetype)));
+
+ /* If requested, verify page type. */
+ if (flags != 0 && (pagetype & flags) == 0)
+ {
+ switch (flags)
+ {
+ case LH_META_PAGE:
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("page is not a hash meta page")));
+ break;
+ case LH_BUCKET_PAGE | LH_OVERFLOW_PAGE:
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("page is not a hash bucket or overflow page")));
+ break;
+ case LH_OVERFLOW_PAGE:
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("page is not a hash overflow page")));
+ break;
+ default:
+ elog(ERROR,
+ "hash page of type %08x not in mask %08x",
+ pagetype, flags);
+ break;
+ }
+ }
+
+ /*
+ * If it is the metapage, also verify magic number and version.
+ */
+ if (pagetype == LH_META_PAGE)
+ {
+ HashMetaPage metap = HashPageGetMeta(page);
+
+ if (metap->hashm_magic != HASH_MAGIC)
+ ereport(ERROR,
+ (errcode(ERRCODE_INDEX_CORRUPTED),
+ errmsg("invalid magic number for metadata"),
+ errdetail("Expected 0x%08x, got 0x%08x.",
+ HASH_MAGIC, metap->hashm_magic)));
+
+ if (metap->hashm_version != HASH_VERSION)
+ ereport(ERROR,
+ (errcode(ERRCODE_INDEX_CORRUPTED),
+ errmsg("invalid version for metadata"),
+ errdetail("Expected %d, got %d.",
+ HASH_VERSION, metap->hashm_version)));
+ }
+
+ return page;
+}
+
+/* -------------------------------------------------
+ * GetHashPageStatistics()
+ *
+ * Collect statistics of single hash page
+ * -------------------------------------------------
+ */
+static void
+GetHashPageStatistics(Page page, HashPageStat *stat)
+{
+ OffsetNumber maxoff = PageGetMaxOffsetNumber(page);
+ HashPageOpaque opaque = HashPageGetOpaque(page);
+ int off;
+
+ stat->dead_items = stat->live_items = 0;
+ stat->page_size = PageGetPageSize(page);
+
+ /* hash page opaque data */
+ stat->hasho_prevblkno = opaque->hasho_prevblkno;
+ stat->hasho_nextblkno = opaque->hasho_nextblkno;
+ stat->hasho_bucket = opaque->hasho_bucket;
+ stat->hasho_flag = opaque->hasho_flag;
+ stat->hasho_page_id = opaque->hasho_page_id;
+
+ /* count live and dead tuples, and free space */
+ for (off = FirstOffsetNumber; off <= maxoff; off++)
+ {
+ ItemId id = PageGetItemId(page, off);
+
+ if (!ItemIdIsDead(id))
+ stat->live_items++;
+ else
+ stat->dead_items++;
+ }
+ stat->free_size = PageGetFreeSpace(page);
+}
+
+/* ---------------------------------------------------
+ * hash_page_type()
+ *
+ * Usage: SELECT hash_page_type(get_raw_page('con_hash_index', 1));
+ * ---------------------------------------------------
+ */
+Datum
+hash_page_type(PG_FUNCTION_ARGS)
+{
+ bytea *raw_page = PG_GETARG_BYTEA_P(0);
+ Page page;
+ HashPageOpaque opaque;
+ int pagetype;
+ const char *type;
+
+ if (!superuser())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions")));
+
+ page = verify_hash_page(raw_page, 0);
+
+ if (PageIsNew(page))
+ type = "unused";
+ else
+ {
+ opaque = HashPageGetOpaque(page);
+
+ /* page type (flags) */
+ pagetype = opaque->hasho_flag & LH_PAGE_TYPE;
+ if (pagetype == LH_META_PAGE)
+ type = "metapage";
+ else if (pagetype == LH_OVERFLOW_PAGE)
+ type = "overflow";
+ else if (pagetype == LH_BUCKET_PAGE)
+ type = "bucket";
+ else if (pagetype == LH_BITMAP_PAGE)
+ type = "bitmap";
+ else
+ type = "unused";
+ }
+
+ PG_RETURN_TEXT_P(cstring_to_text(type));
+}
+
+/* ---------------------------------------------------
+ * hash_page_stats()
+ *
+ * Usage: SELECT * FROM hash_page_stats(get_raw_page('con_hash_index', 1));
+ * ---------------------------------------------------
+ */
+Datum
+hash_page_stats(PG_FUNCTION_ARGS)
+{
+ bytea *raw_page = PG_GETARG_BYTEA_P(0);
+ Page page;
+ int j;
+ Datum values[9];
+ bool nulls[9] = {0};
+ HashPageStat stat;
+ HeapTuple tuple;
+ TupleDesc tupleDesc;
+
+ if (!superuser())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions")));
+
+ page = verify_hash_page(raw_page, LH_BUCKET_PAGE | LH_OVERFLOW_PAGE);
+
+ /* keep compiler quiet */
+ stat.hasho_prevblkno = stat.hasho_nextblkno = InvalidBlockNumber;
+ stat.hasho_flag = stat.hasho_page_id = stat.free_size = 0;
+
+ GetHashPageStatistics(page, &stat);
+
+ /* Build a tuple descriptor for our result type */
+ if (get_call_result_type(fcinfo, NULL, &tupleDesc) != TYPEFUNC_COMPOSITE)
+ elog(ERROR, "return type must be a row type");
+ tupleDesc = BlessTupleDesc(tupleDesc);
+
+ j = 0;
+ values[j++] = Int32GetDatum(stat.live_items);
+ values[j++] = Int32GetDatum(stat.dead_items);
+ values[j++] = Int32GetDatum(stat.page_size);
+ values[j++] = Int32GetDatum(stat.free_size);
+ values[j++] = Int64GetDatum((int64) stat.hasho_prevblkno);
+ values[j++] = Int64GetDatum((int64) stat.hasho_nextblkno);
+ values[j++] = Int64GetDatum((int64) stat.hasho_bucket);
+ values[j++] = Int32GetDatum((int32) stat.hasho_flag);
+ values[j++] = Int32GetDatum((int32) stat.hasho_page_id);
+
+ tuple = heap_form_tuple(tupleDesc, values, nulls);
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(tuple));
+}
+
+/*
+ * cross-call data structure for SRF
+ */
+struct user_args
+{
+ Page page;
+ OffsetNumber offset;
+};
+
+/*-------------------------------------------------------
+ * hash_page_items()
+ *
+ * Get IndexTupleData set in a hash page
+ *
+ * Usage: SELECT * FROM hash_page_items(get_raw_page('con_hash_index', 1));
+ *-------------------------------------------------------
+ */
+Datum
+hash_page_items(PG_FUNCTION_ARGS)
+{
+ bytea *raw_page = PG_GETARG_BYTEA_P(0);
+ Page page;
+ Datum result;
+ Datum values[3];
+ bool nulls[3] = {0};
+ uint32 hashkey;
+ HeapTuple tuple;
+ FuncCallContext *fctx;
+ MemoryContext mctx;
+ struct user_args *uargs;
+
+ if (!superuser())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions")));
+
+ if (SRF_IS_FIRSTCALL())
+ {
+ TupleDesc tupleDesc;
+
+ fctx = SRF_FIRSTCALL_INIT();
+
+ mctx = MemoryContextSwitchTo(fctx->multi_call_memory_ctx);
+
+ page = verify_hash_page(raw_page, LH_BUCKET_PAGE | LH_OVERFLOW_PAGE);
+
+ uargs = palloc(sizeof(struct user_args));
+
+ uargs->page = page;
+
+ uargs->offset = FirstOffsetNumber;
+
+ fctx->max_calls = PageGetMaxOffsetNumber(uargs->page);
+
+ /* Build a tuple descriptor for our result type */
+ if (get_call_result_type(fcinfo, NULL, &tupleDesc) != TYPEFUNC_COMPOSITE)
+ elog(ERROR, "return type must be a row type");
+ tupleDesc = BlessTupleDesc(tupleDesc);
+
+ fctx->attinmeta = TupleDescGetAttInMetadata(tupleDesc);
+
+ fctx->user_fctx = uargs;
+
+ MemoryContextSwitchTo(mctx);
+ }
+
+ fctx = SRF_PERCALL_SETUP();
+ uargs = fctx->user_fctx;
+
+ if (fctx->call_cntr < fctx->max_calls)
+ {
+ ItemId id;
+ IndexTuple itup;
+ int j;
+
+ id = PageGetItemId(uargs->page, uargs->offset);
+
+ if (!ItemIdIsValid(id))
+ elog(ERROR, "invalid ItemId");
+
+ itup = (IndexTuple) PageGetItem(uargs->page, id);
+
+ j = 0;
+ values[j++] = Int32GetDatum((int32) uargs->offset);
+ values[j++] = PointerGetDatum(&itup->t_tid);
+
+ hashkey = _hash_get_indextuple_hashkey(itup);
+ values[j] = Int64GetDatum((int64) hashkey);
+
+ tuple = heap_form_tuple(fctx->attinmeta->tupdesc, values, nulls);
+ result = HeapTupleGetDatum(tuple);
+
+ uargs->offset = uargs->offset + 1;
+
+ SRF_RETURN_NEXT(fctx, result);
+ }
+
+ SRF_RETURN_DONE(fctx);
+}
+
+/* ------------------------------------------------
+ * hash_bitmap_info()
+ *
+ * Get bitmap information for a particular overflow page
+ *
+ * Usage: SELECT * FROM hash_bitmap_info('con_hash_index'::regclass, 5);
+ * ------------------------------------------------
+ */
+Datum
+hash_bitmap_info(PG_FUNCTION_ARGS)
+{
+ Oid indexRelid = PG_GETARG_OID(0);
+ int64 ovflblkno = PG_GETARG_INT64(1);
+ HashMetaPage metap;
+ Buffer metabuf,
+ mapbuf;
+ BlockNumber bitmapblkno;
+ Page mappage;
+ bool bit = false;
+ TupleDesc tupleDesc;
+ Relation indexRel;
+ uint32 ovflbitno;
+ int32 bitmappage,
+ bitmapbit;
+ HeapTuple tuple;
+ int i,
+ j;
+ Datum values[3];
+ bool nulls[3] = {0};
+ uint32 *freep;
+
+ if (!superuser())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions")));
+
+ indexRel = relation_open(indexRelid, AccessShareLock);
+
+ if (!IS_INDEX(indexRel) || !IS_HASH(indexRel))
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is not a %s index",
+ RelationGetRelationName(indexRel), "hash")));
+
+ if (RELATION_IS_OTHER_TEMP(indexRel))
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot access temporary tables of other sessions")));
+
+ if (ovflblkno < 0 || ovflblkno > MaxBlockNumber)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("invalid block number")));
+
+ if (ovflblkno >= RelationGetNumberOfBlocks(indexRel))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("block number %lld is out of range for relation \"%s\"",
+ (long long int) ovflblkno, RelationGetRelationName(indexRel))));
+
+ /* Read the metapage so we can determine which bitmap page to use */
+ metabuf = _hash_getbuf(indexRel, HASH_METAPAGE, HASH_READ, LH_META_PAGE);
+ metap = HashPageGetMeta(BufferGetPage(metabuf));
+
+ /*
+ * Reject attempt to read the bit for a metapage or bitmap page; this is
+ * only meaningful for overflow pages.
+ */
+ if (ovflblkno == 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("invalid overflow block number %u",
+ (BlockNumber) ovflblkno)));
+ for (i = 0; i < metap->hashm_nmaps; i++)
+ if (metap->hashm_mapp[i] == ovflblkno)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("invalid overflow block number %u",
+ (BlockNumber) ovflblkno)));
+
+ /*
+ * Identify overflow bit number. This will error out for primary bucket
+ * pages, and we've already rejected the metapage and bitmap pages above.
+ */
+ ovflbitno = _hash_ovflblkno_to_bitno(metap, (BlockNumber) ovflblkno);
+
+ bitmappage = ovflbitno >> BMPG_SHIFT(metap);
+ bitmapbit = ovflbitno & BMPG_MASK(metap);
+
+ if (bitmappage >= metap->hashm_nmaps)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("invalid overflow block number %u",
+ (BlockNumber) ovflblkno)));
+
+ bitmapblkno = metap->hashm_mapp[bitmappage];
+
+ _hash_relbuf(indexRel, metabuf);
+
+ /* Check the status of bitmap bit for overflow page */
+ mapbuf = _hash_getbuf(indexRel, bitmapblkno, HASH_READ, LH_BITMAP_PAGE);
+ mappage = BufferGetPage(mapbuf);
+ freep = HashPageGetBitmap(mappage);
+
+ bit = ISSET(freep, bitmapbit) != 0;
+
+ _hash_relbuf(indexRel, mapbuf);
+ index_close(indexRel, AccessShareLock);
+
+ /* Build a tuple descriptor for our result type */
+ if (get_call_result_type(fcinfo, NULL, &tupleDesc) != TYPEFUNC_COMPOSITE)
+ elog(ERROR, "return type must be a row type");
+ tupleDesc = BlessTupleDesc(tupleDesc);
+
+ j = 0;
+ values[j++] = Int64GetDatum((int64) bitmapblkno);
+ values[j++] = Int32GetDatum(bitmapbit);
+ values[j++] = BoolGetDatum(bit);
+
+ tuple = heap_form_tuple(tupleDesc, values, nulls);
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(tuple));
+}
+
+/* ------------------------------------------------
+ * hash_metapage_info()
+ *
+ * Get the meta-page information for a hash index
+ *
+ * Usage: SELECT * FROM hash_metapage_info(get_raw_page('con_hash_index', 0))
+ * ------------------------------------------------
+ */
+Datum
+hash_metapage_info(PG_FUNCTION_ARGS)
+{
+ bytea *raw_page = PG_GETARG_BYTEA_P(0);
+ Page page;
+ HashMetaPageData *metad;
+ TupleDesc tupleDesc;
+ HeapTuple tuple;
+ int i,
+ j;
+ Datum values[16];
+ bool nulls[16] = {0};
+ Datum spares[HASH_MAX_SPLITPOINTS];
+ Datum mapp[HASH_MAX_BITMAPS];
+
+ if (!superuser())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions")));
+
+ page = verify_hash_page(raw_page, LH_META_PAGE);
+
+ /* Build a tuple descriptor for our result type */
+ if (get_call_result_type(fcinfo, NULL, &tupleDesc) != TYPEFUNC_COMPOSITE)
+ elog(ERROR, "return type must be a row type");
+ tupleDesc = BlessTupleDesc(tupleDesc);
+
+ metad = HashPageGetMeta(page);
+
+ j = 0;
+ values[j++] = Int64GetDatum((int64) metad->hashm_magic);
+ values[j++] = Int64GetDatum((int64) metad->hashm_version);
+ values[j++] = Float8GetDatum(metad->hashm_ntuples);
+ values[j++] = Int32GetDatum((int32) metad->hashm_ffactor);
+ values[j++] = Int32GetDatum((int32) metad->hashm_bsize);
+ values[j++] = Int32GetDatum((int32) metad->hashm_bmsize);
+ values[j++] = Int32GetDatum((int32) metad->hashm_bmshift);
+ values[j++] = Int64GetDatum((int64) metad->hashm_maxbucket);
+ values[j++] = Int64GetDatum((int64) metad->hashm_highmask);
+ values[j++] = Int64GetDatum((int64) metad->hashm_lowmask);
+ values[j++] = Int64GetDatum((int64) metad->hashm_ovflpoint);
+ values[j++] = Int64GetDatum((int64) metad->hashm_firstfree);
+ values[j++] = Int64GetDatum((int64) metad->hashm_nmaps);
+ values[j++] = ObjectIdGetDatum((Oid) metad->hashm_procid);
+
+ for (i = 0; i < HASH_MAX_SPLITPOINTS; i++)
+ spares[i] = Int64GetDatum((int64) metad->hashm_spares[i]);
+ values[j++] = PointerGetDatum(construct_array_builtin(spares, HASH_MAX_SPLITPOINTS, INT8OID));
+
+ for (i = 0; i < HASH_MAX_BITMAPS; i++)
+ mapp[i] = Int64GetDatum((int64) metad->hashm_mapp[i]);
+ values[j++] = PointerGetDatum(construct_array_builtin(mapp, HASH_MAX_BITMAPS, INT8OID));
+
+ tuple = heap_form_tuple(tupleDesc, values, nulls);
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(tuple));
+}
diff --git a/contrib/pageinspect/heapfuncs.c b/contrib/pageinspect/heapfuncs.c
new file mode 100644
index 0000000..0f02525
--- /dev/null
+++ b/contrib/pageinspect/heapfuncs.c
@@ -0,0 +1,618 @@
+/*-------------------------------------------------------------------------
+ *
+ * heapfuncs.c
+ * Functions to investigate heap pages
+ *
+ * We check the input to these functions for corrupt pointers etc. that
+ * might cause crashes, but at the same time we try to print out as much
+ * information as possible, even if it's nonsense. That's because if a
+ * page is corrupt, we don't know why and how exactly it is corrupt, so we
+ * let the user judge it.
+ *
+ * These functions are restricted to superusers for the fear of introducing
+ * security holes if the input checking isn't as water-tight as it should be.
+ * You'd need to be superuser to obtain a raw page image anyway, so
+ * there's hardly any use case for using these without superuser-rights
+ * anyway.
+ *
+ * Copyright (c) 2007-2023, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pageinspect/heapfuncs.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/relation.h"
+#include "catalog/pg_am_d.h"
+#include "catalog/pg_type.h"
+#include "funcapi.h"
+#include "mb/pg_wchar.h"
+#include "miscadmin.h"
+#include "pageinspect.h"
+#include "port/pg_bitutils.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/rel.h"
+
+/*
+ * It's not supported to create tuples with oids anymore, but when pg_upgrade
+ * was used to upgrade from an older version, tuples might still have an
+ * oid. Seems worthwhile to display that.
+ */
+#define HeapTupleHeaderGetOidOld(tup) \
+( \
+ ((tup)->t_infomask & HEAP_HASOID_OLD) ? \
+ *((Oid *) ((char *)(tup) + (tup)->t_hoff - sizeof(Oid))) \
+ : \
+ InvalidOid \
+)
+
+
+/*
+ * bits_to_text
+ *
+ * Converts a bits8-array of 'len' bits to a human-readable
+ * c-string representation.
+ */
+static char *
+bits_to_text(bits8 *bits, int len)
+{
+ int i;
+ char *str;
+
+ str = palloc(len + 1);
+
+ for (i = 0; i < len; i++)
+ str[i] = (bits[(i / 8)] & (1 << (i % 8))) ? '1' : '0';
+
+ str[i] = '\0';
+
+ return str;
+}
+
+
+/*
+ * text_to_bits
+ *
+ * Converts a c-string representation of bits into a bits8-array. This is
+ * the reverse operation of previous routine.
+ */
+static bits8 *
+text_to_bits(char *str, int len)
+{
+ bits8 *bits;
+ int off = 0;
+ char byte = 0;
+
+ bits = palloc(len + 1);
+
+ while (off < len)
+ {
+ if (off % 8 == 0)
+ byte = 0;
+
+ if ((str[off] == '0') || (str[off] == '1'))
+ byte = byte | ((str[off] - '0') << off % 8);
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("invalid character \"%.*s\" in t_bits string",
+ pg_mblen(str + off), str + off)));
+
+ if (off % 8 == 7)
+ bits[off / 8] = byte;
+
+ off++;
+ }
+
+ return bits;
+}
+
+/*
+ * heap_page_items
+ *
+ * Allows inspection of line pointers and tuple headers of a heap page.
+ */
+PG_FUNCTION_INFO_V1(heap_page_items);
+
+typedef struct heap_page_items_state
+{
+ TupleDesc tupd;
+ Page page;
+ uint16 offset;
+} heap_page_items_state;
+
+Datum
+heap_page_items(PG_FUNCTION_ARGS)
+{
+ bytea *raw_page = PG_GETARG_BYTEA_P(0);
+ heap_page_items_state *inter_call_data = NULL;
+ FuncCallContext *fctx;
+ int raw_page_size;
+
+ if (!superuser())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions")));
+
+ raw_page_size = VARSIZE(raw_page) - VARHDRSZ;
+
+ if (SRF_IS_FIRSTCALL())
+ {
+ TupleDesc tupdesc;
+ MemoryContext mctx;
+
+ if (raw_page_size < SizeOfPageHeaderData)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("input page too small (%d bytes)", raw_page_size)));
+
+ fctx = SRF_FIRSTCALL_INIT();
+ mctx = MemoryContextSwitchTo(fctx->multi_call_memory_ctx);
+
+ inter_call_data = palloc(sizeof(heap_page_items_state));
+
+ /* Build a tuple descriptor for our result type */
+ if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+ elog(ERROR, "return type must be a row type");
+
+ inter_call_data->tupd = tupdesc;
+
+ inter_call_data->offset = FirstOffsetNumber;
+ inter_call_data->page = VARDATA(raw_page);
+
+ fctx->max_calls = PageGetMaxOffsetNumber(inter_call_data->page);
+ fctx->user_fctx = inter_call_data;
+
+ MemoryContextSwitchTo(mctx);
+ }
+
+ fctx = SRF_PERCALL_SETUP();
+ inter_call_data = fctx->user_fctx;
+
+ if (fctx->call_cntr < fctx->max_calls)
+ {
+ Page page = inter_call_data->page;
+ HeapTuple resultTuple;
+ Datum result;
+ ItemId id;
+ Datum values[14];
+ bool nulls[14];
+ uint16 lp_offset;
+ uint16 lp_flags;
+ uint16 lp_len;
+
+ memset(nulls, 0, sizeof(nulls));
+
+ /* Extract information from the line pointer */
+
+ id = PageGetItemId(page, inter_call_data->offset);
+
+ lp_offset = ItemIdGetOffset(id);
+ lp_flags = ItemIdGetFlags(id);
+ lp_len = ItemIdGetLength(id);
+
+ values[0] = UInt16GetDatum(inter_call_data->offset);
+ values[1] = UInt16GetDatum(lp_offset);
+ values[2] = UInt16GetDatum(lp_flags);
+ values[3] = UInt16GetDatum(lp_len);
+
+ /*
+ * We do just enough validity checking to make sure we don't reference
+ * data outside the page passed to us. The page could be corrupt in
+ * many other ways, but at least we won't crash.
+ */
+ if (ItemIdHasStorage(id) &&
+ lp_len >= MinHeapTupleSize &&
+ lp_offset == MAXALIGN(lp_offset) &&
+ lp_offset + lp_len <= raw_page_size)
+ {
+ HeapTupleHeader tuphdr;
+ bytea *tuple_data_bytea;
+ int tuple_data_len;
+
+ /* Extract information from the tuple header */
+
+ tuphdr = (HeapTupleHeader) PageGetItem(page, id);
+
+ values[4] = UInt32GetDatum(HeapTupleHeaderGetRawXmin(tuphdr));
+ values[5] = UInt32GetDatum(HeapTupleHeaderGetRawXmax(tuphdr));
+ /* shared with xvac */
+ values[6] = UInt32GetDatum(HeapTupleHeaderGetRawCommandId(tuphdr));
+ values[7] = PointerGetDatum(&tuphdr->t_ctid);
+ values[8] = UInt32GetDatum(tuphdr->t_infomask2);
+ values[9] = UInt32GetDatum(tuphdr->t_infomask);
+ values[10] = UInt8GetDatum(tuphdr->t_hoff);
+
+ /* Copy raw tuple data into bytea attribute */
+ tuple_data_len = lp_len - tuphdr->t_hoff;
+ tuple_data_bytea = (bytea *) palloc(tuple_data_len + VARHDRSZ);
+ SET_VARSIZE(tuple_data_bytea, tuple_data_len + VARHDRSZ);
+ memcpy(VARDATA(tuple_data_bytea), (char *) tuphdr + tuphdr->t_hoff,
+ tuple_data_len);
+ values[13] = PointerGetDatum(tuple_data_bytea);
+
+ /*
+ * We already checked that the item is completely within the raw
+ * page passed to us, with the length given in the line pointer.
+ * Let's check that t_hoff doesn't point over lp_len, before using
+ * it to access t_bits and oid.
+ */
+ if (tuphdr->t_hoff >= SizeofHeapTupleHeader &&
+ tuphdr->t_hoff <= lp_len &&
+ tuphdr->t_hoff == MAXALIGN(tuphdr->t_hoff))
+ {
+ if (tuphdr->t_infomask & HEAP_HASNULL)
+ {
+ int bits_len;
+
+ bits_len =
+ BITMAPLEN(HeapTupleHeaderGetNatts(tuphdr)) * BITS_PER_BYTE;
+ values[11] = CStringGetTextDatum(bits_to_text(tuphdr->t_bits, bits_len));
+ }
+ else
+ nulls[11] = true;
+
+ if (tuphdr->t_infomask & HEAP_HASOID_OLD)
+ values[12] = HeapTupleHeaderGetOidOld(tuphdr);
+ else
+ nulls[12] = true;
+ }
+ else
+ {
+ nulls[11] = true;
+ nulls[12] = true;
+ }
+ }
+ else
+ {
+ /*
+ * The line pointer is not used, or it's invalid. Set the rest of
+ * the fields to NULL
+ */
+ int i;
+
+ for (i = 4; i <= 13; i++)
+ nulls[i] = true;
+ }
+
+ /* Build and return the result tuple. */
+ resultTuple = heap_form_tuple(inter_call_data->tupd, values, nulls);
+ result = HeapTupleGetDatum(resultTuple);
+
+ inter_call_data->offset++;
+
+ SRF_RETURN_NEXT(fctx, result);
+ }
+ else
+ SRF_RETURN_DONE(fctx);
+}
+
+/*
+ * tuple_data_split_internal
+ *
+ * Split raw tuple data taken directly from a page into an array of bytea
+ * elements. This routine does a lookup on NULL values and creates array
+ * elements accordingly. This is a reimplementation of nocachegetattr()
+ * in heaptuple.c simplified for educational purposes.
+ */
+static Datum
+tuple_data_split_internal(Oid relid, char *tupdata,
+ uint16 tupdata_len, uint16 t_infomask,
+ uint16 t_infomask2, bits8 *t_bits,
+ bool do_detoast)
+{
+ ArrayBuildState *raw_attrs;
+ int nattrs;
+ int i;
+ int off = 0;
+ Relation rel;
+ TupleDesc tupdesc;
+
+ /* Get tuple descriptor from relation OID */
+ rel = relation_open(relid, AccessShareLock);
+ tupdesc = RelationGetDescr(rel);
+
+ raw_attrs = initArrayResult(BYTEAOID, CurrentMemoryContext, false);
+ nattrs = tupdesc->natts;
+
+ if (rel->rd_rel->relam != HEAP_TABLE_AM_OID)
+ ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("only heap AM is supported")));
+
+ if (nattrs < (t_infomask2 & HEAP_NATTS_MASK))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("number of attributes in tuple header is greater than number of attributes in tuple descriptor")));
+
+ for (i = 0; i < nattrs; i++)
+ {
+ Form_pg_attribute attr;
+ bool is_null;
+ bytea *attr_data = NULL;
+
+ attr = TupleDescAttr(tupdesc, i);
+
+ /*
+ * Tuple header can specify fewer attributes than tuple descriptor as
+ * ALTER TABLE ADD COLUMN without DEFAULT keyword does not actually
+ * change tuples in pages, so attributes with numbers greater than
+ * (t_infomask2 & HEAP_NATTS_MASK) should be treated as NULL.
+ */
+ if (i >= (t_infomask2 & HEAP_NATTS_MASK))
+ is_null = true;
+ else
+ is_null = (t_infomask & HEAP_HASNULL) && att_isnull(i, t_bits);
+
+ if (!is_null)
+ {
+ int len;
+
+ if (attr->attlen == -1)
+ {
+ off = att_align_pointer(off, attr->attalign, -1,
+ tupdata + off);
+
+ /*
+ * As VARSIZE_ANY throws an exception if it can't properly
+ * detect the type of external storage in macros VARTAG_SIZE,
+ * this check is repeated to have a nicer error handling.
+ */
+ if (VARATT_IS_EXTERNAL(tupdata + off) &&
+ !VARATT_IS_EXTERNAL_ONDISK(tupdata + off) &&
+ !VARATT_IS_EXTERNAL_INDIRECT(tupdata + off))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("first byte of varlena attribute is incorrect for attribute %d", i)));
+
+ len = VARSIZE_ANY(tupdata + off);
+ }
+ else
+ {
+ off = att_align_nominal(off, attr->attalign);
+ len = attr->attlen;
+ }
+
+ if (tupdata_len < off + len)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("unexpected end of tuple data")));
+
+ if (attr->attlen == -1 && do_detoast)
+ attr_data = pg_detoast_datum_copy((struct varlena *) (tupdata + off));
+ else
+ {
+ attr_data = (bytea *) palloc(len + VARHDRSZ);
+ SET_VARSIZE(attr_data, len + VARHDRSZ);
+ memcpy(VARDATA(attr_data), tupdata + off, len);
+ }
+
+ off = att_addlength_pointer(off, attr->attlen,
+ tupdata + off);
+ }
+
+ raw_attrs = accumArrayResult(raw_attrs, PointerGetDatum(attr_data),
+ is_null, BYTEAOID, CurrentMemoryContext);
+ if (attr_data)
+ pfree(attr_data);
+ }
+
+ if (tupdata_len != off)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("end of tuple reached without looking at all its data")));
+
+ relation_close(rel, AccessShareLock);
+
+ return makeArrayResult(raw_attrs, CurrentMemoryContext);
+}
+
+/*
+ * tuple_data_split
+ *
+ * Split raw tuple data taken directly from page into distinct elements
+ * taking into account null values.
+ */
+PG_FUNCTION_INFO_V1(tuple_data_split);
+
+Datum
+tuple_data_split(PG_FUNCTION_ARGS)
+{
+ Oid relid;
+ bytea *raw_data;
+ uint16 t_infomask;
+ uint16 t_infomask2;
+ char *t_bits_str;
+ bool do_detoast = false;
+ bits8 *t_bits = NULL;
+ Datum res;
+
+ relid = PG_GETARG_OID(0);
+ raw_data = PG_ARGISNULL(1) ? NULL : PG_GETARG_BYTEA_P(1);
+ t_infomask = PG_GETARG_INT16(2);
+ t_infomask2 = PG_GETARG_INT16(3);
+ t_bits_str = PG_ARGISNULL(4) ? NULL :
+ text_to_cstring(PG_GETARG_TEXT_PP(4));
+
+ if (PG_NARGS() >= 6)
+ do_detoast = PG_GETARG_BOOL(5);
+
+ if (!superuser())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions")));
+
+ if (!raw_data)
+ PG_RETURN_NULL();
+
+ /*
+ * Convert t_bits string back to the bits8 array as represented in the
+ * tuple header.
+ */
+ if (t_infomask & HEAP_HASNULL)
+ {
+ size_t bits_str_len;
+ size_t bits_len;
+
+ bits_len = BITMAPLEN(t_infomask2 & HEAP_NATTS_MASK) * BITS_PER_BYTE;
+ if (!t_bits_str)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("t_bits string must not be NULL")));
+
+ bits_str_len = strlen(t_bits_str);
+ if (bits_len != bits_str_len)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("unexpected length of t_bits string: %zu, expected %zu",
+ bits_str_len, bits_len)));
+
+ /* do the conversion */
+ t_bits = text_to_bits(t_bits_str, bits_str_len);
+ }
+ else
+ {
+ if (t_bits_str)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("t_bits string is expected to be NULL, but instead it is %zu bytes long",
+ strlen(t_bits_str))));
+ }
+
+ /* Split tuple data */
+ res = tuple_data_split_internal(relid, (char *) raw_data + VARHDRSZ,
+ VARSIZE(raw_data) - VARHDRSZ,
+ t_infomask, t_infomask2, t_bits,
+ do_detoast);
+
+ if (t_bits)
+ pfree(t_bits);
+
+ PG_RETURN_DATUM(res);
+}
+
+/*
+ * heap_tuple_infomask_flags
+ *
+ * Decode into a human-readable format t_infomask and t_infomask2 associated
+ * to a tuple. All the flags are described in access/htup_details.h.
+ */
+PG_FUNCTION_INFO_V1(heap_tuple_infomask_flags);
+
+Datum
+heap_tuple_infomask_flags(PG_FUNCTION_ARGS)
+{
+#define HEAP_TUPLE_INFOMASK_COLS 2
+ Datum values[HEAP_TUPLE_INFOMASK_COLS] = {0};
+ bool nulls[HEAP_TUPLE_INFOMASK_COLS] = {0};
+ uint16 t_infomask = PG_GETARG_INT16(0);
+ uint16 t_infomask2 = PG_GETARG_INT16(1);
+ int cnt = 0;
+ ArrayType *a;
+ int bitcnt;
+ Datum *flags;
+ TupleDesc tupdesc;
+ HeapTuple tuple;
+
+ if (!superuser())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions")));
+
+ /* Build a tuple descriptor for our result type */
+ if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+ elog(ERROR, "return type must be a row type");
+
+ bitcnt = pg_popcount((const char *) &t_infomask, sizeof(uint16)) +
+ pg_popcount((const char *) &t_infomask2, sizeof(uint16));
+
+ /* If no flags, return a set of empty arrays */
+ if (bitcnt <= 0)
+ {
+ values[0] = PointerGetDatum(construct_empty_array(TEXTOID));
+ values[1] = PointerGetDatum(construct_empty_array(TEXTOID));
+ tuple = heap_form_tuple(tupdesc, values, nulls);
+ PG_RETURN_DATUM(HeapTupleGetDatum(tuple));
+ }
+
+ /* build set of raw flags */
+ flags = (Datum *) palloc0(sizeof(Datum) * bitcnt);
+
+ /* decode t_infomask */
+ if ((t_infomask & HEAP_HASNULL) != 0)
+ flags[cnt++] = CStringGetTextDatum("HEAP_HASNULL");
+ if ((t_infomask & HEAP_HASVARWIDTH) != 0)
+ flags[cnt++] = CStringGetTextDatum("HEAP_HASVARWIDTH");
+ if ((t_infomask & HEAP_HASEXTERNAL) != 0)
+ flags[cnt++] = CStringGetTextDatum("HEAP_HASEXTERNAL");
+ if ((t_infomask & HEAP_HASOID_OLD) != 0)
+ flags[cnt++] = CStringGetTextDatum("HEAP_HASOID_OLD");
+ if ((t_infomask & HEAP_XMAX_KEYSHR_LOCK) != 0)
+ flags[cnt++] = CStringGetTextDatum("HEAP_XMAX_KEYSHR_LOCK");
+ if ((t_infomask & HEAP_COMBOCID) != 0)
+ flags[cnt++] = CStringGetTextDatum("HEAP_COMBOCID");
+ if ((t_infomask & HEAP_XMAX_EXCL_LOCK) != 0)
+ flags[cnt++] = CStringGetTextDatum("HEAP_XMAX_EXCL_LOCK");
+ if ((t_infomask & HEAP_XMAX_LOCK_ONLY) != 0)
+ flags[cnt++] = CStringGetTextDatum("HEAP_XMAX_LOCK_ONLY");
+ if ((t_infomask & HEAP_XMIN_COMMITTED) != 0)
+ flags[cnt++] = CStringGetTextDatum("HEAP_XMIN_COMMITTED");
+ if ((t_infomask & HEAP_XMIN_INVALID) != 0)
+ flags[cnt++] = CStringGetTextDatum("HEAP_XMIN_INVALID");
+ if ((t_infomask & HEAP_XMAX_COMMITTED) != 0)
+ flags[cnt++] = CStringGetTextDatum("HEAP_XMAX_COMMITTED");
+ if ((t_infomask & HEAP_XMAX_INVALID) != 0)
+ flags[cnt++] = CStringGetTextDatum("HEAP_XMAX_INVALID");
+ if ((t_infomask & HEAP_XMAX_IS_MULTI) != 0)
+ flags[cnt++] = CStringGetTextDatum("HEAP_XMAX_IS_MULTI");
+ if ((t_infomask & HEAP_UPDATED) != 0)
+ flags[cnt++] = CStringGetTextDatum("HEAP_UPDATED");
+ if ((t_infomask & HEAP_MOVED_OFF) != 0)
+ flags[cnt++] = CStringGetTextDatum("HEAP_MOVED_OFF");
+ if ((t_infomask & HEAP_MOVED_IN) != 0)
+ flags[cnt++] = CStringGetTextDatum("HEAP_MOVED_IN");
+
+ /* decode t_infomask2 */
+ if ((t_infomask2 & HEAP_KEYS_UPDATED) != 0)
+ flags[cnt++] = CStringGetTextDatum("HEAP_KEYS_UPDATED");
+ if ((t_infomask2 & HEAP_HOT_UPDATED) != 0)
+ flags[cnt++] = CStringGetTextDatum("HEAP_HOT_UPDATED");
+ if ((t_infomask2 & HEAP_ONLY_TUPLE) != 0)
+ flags[cnt++] = CStringGetTextDatum("HEAP_ONLY_TUPLE");
+
+ /* build value */
+ Assert(cnt <= bitcnt);
+ a = construct_array_builtin(flags, cnt, TEXTOID);
+ values[0] = PointerGetDatum(a);
+
+ /*
+ * Build set of combined flags. Use the same array as previously, this
+ * keeps the code simple.
+ */
+ cnt = 0;
+ MemSet(flags, 0, sizeof(Datum) * bitcnt);
+
+ /* decode combined masks of t_infomask */
+ if ((t_infomask & HEAP_XMAX_SHR_LOCK) == HEAP_XMAX_SHR_LOCK)
+ flags[cnt++] = CStringGetTextDatum("HEAP_XMAX_SHR_LOCK");
+ if ((t_infomask & HEAP_XMIN_FROZEN) == HEAP_XMIN_FROZEN)
+ flags[cnt++] = CStringGetTextDatum("HEAP_XMIN_FROZEN");
+ if ((t_infomask & HEAP_MOVED) == HEAP_MOVED)
+ flags[cnt++] = CStringGetTextDatum("HEAP_MOVED");
+
+ /* Build an empty array if there are no combined flags */
+ if (cnt == 0)
+ a = construct_empty_array(TEXTOID);
+ else
+ a = construct_array_builtin(flags, cnt, TEXTOID);
+ pfree(flags);
+ values[1] = PointerGetDatum(a);
+
+ /* Returns the record as Datum */
+ tuple = heap_form_tuple(tupdesc, values, nulls);
+ PG_RETURN_DATUM(HeapTupleGetDatum(tuple));
+}
diff --git a/contrib/pageinspect/meson.build b/contrib/pageinspect/meson.build
new file mode 100644
index 0000000..4dde4e7
--- /dev/null
+++ b/contrib/pageinspect/meson.build
@@ -0,0 +1,60 @@
+# Copyright (c) 2022-2023, PostgreSQL Global Development Group
+
+pageinspect_sources = files(
+ 'brinfuncs.c',
+ 'btreefuncs.c',
+ 'fsmfuncs.c',
+ 'ginfuncs.c',
+ 'gistfuncs.c',
+ 'hashfuncs.c',
+ 'heapfuncs.c',
+ 'rawpage.c',
+)
+
+if host_system == 'windows'
+ pageinspect_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+ '--NAME', 'pageinspect',
+ '--FILEDESC', 'pageinspect - functions to inspect contents of database pages',])
+endif
+
+pageinspect = shared_module('pageinspect',
+ pageinspect_sources,
+ kwargs: contrib_mod_args,
+)
+contrib_targets += pageinspect
+
+install_data(
+ 'pageinspect--1.0--1.1.sql',
+ 'pageinspect--1.1--1.2.sql',
+ 'pageinspect--1.2--1.3.sql',
+ 'pageinspect--1.3--1.4.sql',
+ 'pageinspect--1.4--1.5.sql',
+ 'pageinspect--1.5--1.6.sql',
+ 'pageinspect--1.5.sql',
+ 'pageinspect--1.6--1.7.sql',
+ 'pageinspect--1.7--1.8.sql',
+ 'pageinspect--1.8--1.9.sql',
+ 'pageinspect--1.9--1.10.sql',
+ 'pageinspect--1.10--1.11.sql',
+ 'pageinspect--1.11--1.12.sql',
+ 'pageinspect.control',
+ kwargs: contrib_data_args,
+)
+
+tests += {
+ 'name': 'pageinspect',
+ 'sd': meson.current_source_dir(),
+ 'bd': meson.current_build_dir(),
+ 'regress': {
+ 'sql': [
+ 'page',
+ 'btree',
+ 'brin',
+ 'gin',
+ 'gist',
+ 'hash',
+ 'checksum',
+ 'oldextversions',
+ ],
+ },
+}
diff --git a/contrib/pageinspect/pageinspect--1.0--1.1.sql b/contrib/pageinspect/pageinspect--1.0--1.1.sql
new file mode 100644
index 0000000..0e2c3f4
--- /dev/null
+++ b/contrib/pageinspect/pageinspect--1.0--1.1.sql
@@ -0,0 +1,18 @@
+/* contrib/pageinspect/pageinspect--1.0--1.1.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION pageinspect UPDATE TO '1.1'" to load this file. \quit
+
+DROP FUNCTION page_header(bytea);
+CREATE FUNCTION page_header(IN page bytea,
+ OUT lsn text,
+ OUT checksum smallint,
+ OUT flags smallint,
+ OUT lower smallint,
+ OUT upper smallint,
+ OUT special smallint,
+ OUT pagesize smallint,
+ OUT version smallint,
+ OUT prune_xid xid)
+AS 'MODULE_PATHNAME', 'page_header'
+LANGUAGE C STRICT;
diff --git a/contrib/pageinspect/pageinspect--1.1--1.2.sql b/contrib/pageinspect/pageinspect--1.1--1.2.sql
new file mode 100644
index 0000000..31ffbb0
--- /dev/null
+++ b/contrib/pageinspect/pageinspect--1.1--1.2.sql
@@ -0,0 +1,18 @@
+/* contrib/pageinspect/pageinspect--1.1--1.2.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION pageinspect UPDATE TO '1.2'" to load this file. \quit
+
+DROP FUNCTION page_header(bytea);
+CREATE FUNCTION page_header(IN page bytea,
+ OUT lsn pg_lsn,
+ OUT checksum smallint,
+ OUT flags smallint,
+ OUT lower smallint,
+ OUT upper smallint,
+ OUT special smallint,
+ OUT pagesize smallint,
+ OUT version smallint,
+ OUT prune_xid xid)
+AS 'MODULE_PATHNAME', 'page_header'
+LANGUAGE C STRICT;
diff --git a/contrib/pageinspect/pageinspect--1.10--1.11.sql b/contrib/pageinspect/pageinspect--1.10--1.11.sql
new file mode 100644
index 0000000..8fa5e10
--- /dev/null
+++ b/contrib/pageinspect/pageinspect--1.10--1.11.sql
@@ -0,0 +1,28 @@
+/* contrib/pageinspect/pageinspect--1.10--1.11.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION pageinspect UPDATE TO '1.11'" to load this file. \quit
+
+--
+-- Functions that fetch relation pages must be PARALLEL RESTRICTED,
+-- not PARALLEL SAFE, otherwise they will fail when run on a
+-- temporary table in a parallel worker process.
+--
+
+ALTER FUNCTION get_raw_page(text, int8) PARALLEL RESTRICTED;
+ALTER FUNCTION get_raw_page(text, text, int8) PARALLEL RESTRICTED;
+-- tuple_data_split must be restricted because it may fetch TOAST data.
+ALTER FUNCTION tuple_data_split(oid, bytea, integer, integer, text) PARALLEL RESTRICTED;
+ALTER FUNCTION tuple_data_split(oid, bytea, integer, integer, text, bool) PARALLEL RESTRICTED;
+-- heap_page_item_attrs must be restricted because it calls tuple_data_split.
+ALTER FUNCTION heap_page_item_attrs(bytea, regclass, bool) PARALLEL RESTRICTED;
+ALTER FUNCTION heap_page_item_attrs(bytea, regclass) PARALLEL RESTRICTED;
+ALTER FUNCTION bt_metap(text) PARALLEL RESTRICTED;
+ALTER FUNCTION bt_page_stats(text, int8) PARALLEL RESTRICTED;
+ALTER FUNCTION bt_page_items(text, int8) PARALLEL RESTRICTED;
+ALTER FUNCTION hash_bitmap_info(regclass, int8) PARALLEL RESTRICTED;
+-- brin_page_items might be parallel safe, because it seems to touch
+-- only index metadata, but I don't think there's a point in risking it.
+-- Likewise for gist_page_items.
+ALTER FUNCTION brin_page_items(bytea, regclass) PARALLEL RESTRICTED;
+ALTER FUNCTION gist_page_items(bytea, regclass) PARALLEL RESTRICTED;
diff --git a/contrib/pageinspect/pageinspect--1.11--1.12.sql b/contrib/pageinspect/pageinspect--1.11--1.12.sql
new file mode 100644
index 0000000..a20d67a
--- /dev/null
+++ b/contrib/pageinspect/pageinspect--1.11--1.12.sql
@@ -0,0 +1,40 @@
+/* contrib/pageinspect/pageinspect--1.11--1.12.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION pageinspect UPDATE TO '1.12'" to load this file. \quit
+
+--
+-- bt_multi_page_stats()
+--
+CREATE FUNCTION bt_multi_page_stats(IN relname text, IN blkno int8, IN blk_count int8,
+ OUT blkno int8,
+ OUT type "char",
+ OUT live_items int4,
+ OUT dead_items int4,
+ OUT avg_item_size int4,
+ OUT page_size int4,
+ OUT free_size int4,
+ OUT btpo_prev int8,
+ OUT btpo_next int8,
+ OUT btpo_level int8,
+ OUT btpo_flags int4)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'bt_multi_page_stats'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+
+--
+-- add information about BRIN empty ranges
+--
+DROP FUNCTION brin_page_items(IN page bytea, IN index_oid regclass);
+CREATE FUNCTION brin_page_items(IN page bytea, IN index_oid regclass,
+ OUT itemoffset int,
+ OUT blknum int8,
+ OUT attnum int,
+ OUT allnulls bool,
+ OUT hasnulls bool,
+ OUT placeholder bool,
+ OUT empty bool,
+ OUT value text)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'brin_page_items'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
diff --git a/contrib/pageinspect/pageinspect--1.2--1.3.sql b/contrib/pageinspect/pageinspect--1.2--1.3.sql
new file mode 100644
index 0000000..9c55a6e
--- /dev/null
+++ b/contrib/pageinspect/pageinspect--1.2--1.3.sql
@@ -0,0 +1,82 @@
+/* contrib/pageinspect/pageinspect--1.2--1.3.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION pageinspect UPDATE TO '1.3'" to load this file. \quit
+
+--
+-- brin_page_type()
+--
+CREATE FUNCTION brin_page_type(IN page bytea)
+RETURNS text
+AS 'MODULE_PATHNAME', 'brin_page_type'
+LANGUAGE C STRICT;
+
+--
+-- brin_metapage_info()
+--
+CREATE FUNCTION brin_metapage_info(IN page bytea, OUT magic text,
+ OUT version integer, OUT pagesperrange integer, OUT lastrevmappage bigint)
+AS 'MODULE_PATHNAME', 'brin_metapage_info'
+LANGUAGE C STRICT;
+
+--
+-- brin_revmap_data()
+--
+CREATE FUNCTION brin_revmap_data(IN page bytea,
+ OUT pages tid)
+RETURNS SETOF tid
+AS 'MODULE_PATHNAME', 'brin_revmap_data'
+LANGUAGE C STRICT;
+
+--
+-- brin_page_items()
+--
+CREATE FUNCTION brin_page_items(IN page bytea, IN index_oid regclass,
+ OUT itemoffset int,
+ OUT blknum int,
+ OUT attnum int,
+ OUT allnulls bool,
+ OUT hasnulls bool,
+ OUT placeholder bool,
+ OUT value text)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'brin_page_items'
+LANGUAGE C STRICT;
+
+--
+-- gin_metapage_info()
+--
+CREATE FUNCTION gin_metapage_info(IN page bytea,
+ OUT pending_head bigint,
+ OUT pending_tail bigint,
+ OUT tail_free_size int4,
+ OUT n_pending_pages bigint,
+ OUT n_pending_tuples bigint,
+ OUT n_total_pages bigint,
+ OUT n_entry_pages bigint,
+ OUT n_data_pages bigint,
+ OUT n_entries bigint,
+ OUT version int4)
+AS 'MODULE_PATHNAME', 'gin_metapage_info'
+LANGUAGE C STRICT;
+
+--
+-- gin_page_opaque_info()
+--
+CREATE FUNCTION gin_page_opaque_info(IN page bytea,
+ OUT rightlink bigint,
+ OUT maxoff int4,
+ OUT flags text[])
+AS 'MODULE_PATHNAME', 'gin_page_opaque_info'
+LANGUAGE C STRICT;
+
+--
+-- gin_leafpage_items()
+--
+CREATE FUNCTION gin_leafpage_items(IN page bytea,
+ OUT first_tid tid,
+ OUT nbytes int2,
+ OUT tids tid[])
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'gin_leafpage_items'
+LANGUAGE C STRICT;
diff --git a/contrib/pageinspect/pageinspect--1.3--1.4.sql b/contrib/pageinspect/pageinspect--1.3--1.4.sql
new file mode 100644
index 0000000..86e0dfa
--- /dev/null
+++ b/contrib/pageinspect/pageinspect--1.3--1.4.sql
@@ -0,0 +1,118 @@
+/* contrib/pageinspect/pageinspect--1.3--1.4.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION pageinspect UPDATE TO '1.4'" to load this file. \quit
+
+--
+-- heap_page_items()
+--
+DROP FUNCTION heap_page_items(bytea);
+CREATE FUNCTION heap_page_items(IN page bytea,
+ OUT lp smallint,
+ OUT lp_off smallint,
+ OUT lp_flags smallint,
+ OUT lp_len smallint,
+ OUT t_xmin xid,
+ OUT t_xmax xid,
+ OUT t_field3 int4,
+ OUT t_ctid tid,
+ OUT t_infomask2 integer,
+ OUT t_infomask integer,
+ OUT t_hoff smallint,
+ OUT t_bits text,
+ OUT t_oid oid,
+ OUT t_data bytea)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'heap_page_items'
+LANGUAGE C STRICT;
+
+--
+-- tuple_data_split()
+--
+CREATE FUNCTION tuple_data_split(rel_oid oid,
+ t_data bytea,
+ t_infomask integer,
+ t_infomask2 integer,
+ t_bits text)
+RETURNS bytea[]
+AS 'MODULE_PATHNAME','tuple_data_split'
+LANGUAGE C;
+
+CREATE FUNCTION tuple_data_split(rel_oid oid,
+ t_data bytea,
+ t_infomask integer,
+ t_infomask2 integer,
+ t_bits text,
+ do_detoast bool)
+RETURNS bytea[]
+AS 'MODULE_PATHNAME','tuple_data_split'
+LANGUAGE C;
+
+--
+-- heap_page_item_attrs()
+--
+CREATE FUNCTION heap_page_item_attrs(
+ IN page bytea,
+ IN rel_oid regclass,
+ IN do_detoast bool,
+ OUT lp smallint,
+ OUT lp_off smallint,
+ OUT lp_flags smallint,
+ OUT lp_len smallint,
+ OUT t_xmin xid,
+ OUT t_xmax xid,
+ OUT t_field3 int4,
+ OUT t_ctid tid,
+ OUT t_infomask2 integer,
+ OUT t_infomask integer,
+ OUT t_hoff smallint,
+ OUT t_bits text,
+ OUT t_oid oid,
+ OUT t_attrs bytea[]
+ )
+RETURNS SETOF record AS $$
+SELECT lp,
+ lp_off,
+ lp_flags,
+ lp_len,
+ t_xmin,
+ t_xmax,
+ t_field3,
+ t_ctid,
+ t_infomask2,
+ t_infomask,
+ t_hoff,
+ t_bits,
+ t_oid,
+ tuple_data_split(
+ rel_oid,
+ t_data,
+ t_infomask,
+ t_infomask2,
+ t_bits,
+ do_detoast)
+ AS t_attrs
+ FROM heap_page_items(page);
+$$ LANGUAGE SQL;
+
+CREATE FUNCTION heap_page_item_attrs(
+ IN page bytea,
+ IN rel_oid regclass,
+ OUT lp smallint,
+ OUT lp_off smallint,
+ OUT lp_flags smallint,
+ OUT lp_len smallint,
+ OUT t_xmin xid,
+ OUT t_xmax xid,
+ OUT t_field3 int4,
+ OUT t_ctid tid,
+ OUT t_infomask2 integer,
+ OUT t_infomask integer,
+ OUT t_hoff smallint,
+ OUT t_bits text,
+ OUT t_oid oid,
+ OUT t_attrs bytea[]
+ )
+RETURNS SETOF record AS $$
+SELECT * from heap_page_item_attrs(page, rel_oid, false);
+$$ LANGUAGE SQL;
diff --git a/contrib/pageinspect/pageinspect--1.4--1.5.sql b/contrib/pageinspect/pageinspect--1.4--1.5.sql
new file mode 100644
index 0000000..7ed7b54
--- /dev/null
+++ b/contrib/pageinspect/pageinspect--1.4--1.5.sql
@@ -0,0 +1,24 @@
+/* contrib/pageinspect/pageinspect--1.4--1.5.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION pageinspect UPDATE TO '1.5'" to load this file. \quit
+
+ALTER FUNCTION get_raw_page(text, int4) PARALLEL SAFE;
+ALTER FUNCTION get_raw_page(text, text, int4) PARALLEL SAFE;
+ALTER FUNCTION page_header(bytea) PARALLEL SAFE;
+ALTER FUNCTION heap_page_items(bytea) PARALLEL SAFE;
+ALTER FUNCTION tuple_data_split(oid, bytea, integer, integer, text) PARALLEL SAFE;
+ALTER FUNCTION tuple_data_split(oid, bytea, integer, integer, text, bool) PARALLEL SAFE;
+ALTER FUNCTION heap_page_item_attrs(bytea, regclass, bool) PARALLEL SAFE;
+ALTER FUNCTION heap_page_item_attrs(bytea, regclass) PARALLEL SAFE;
+ALTER FUNCTION bt_metap(text) PARALLEL SAFE;
+ALTER FUNCTION bt_page_stats(text, int4) PARALLEL SAFE;
+ALTER FUNCTION bt_page_items(text, int4) PARALLEL SAFE;
+ALTER FUNCTION brin_page_type(bytea) PARALLEL SAFE;
+ALTER FUNCTION brin_metapage_info(bytea) PARALLEL SAFE;
+ALTER FUNCTION brin_revmap_data(bytea) PARALLEL SAFE;
+ALTER FUNCTION brin_page_items(bytea, regclass) PARALLEL SAFE;
+ALTER FUNCTION fsm_page_contents(bytea) PARALLEL SAFE;
+ALTER FUNCTION gin_metapage_info(bytea) PARALLEL SAFE;
+ALTER FUNCTION gin_page_opaque_info(bytea) PARALLEL SAFE;
+ALTER FUNCTION gin_leafpage_items(bytea) PARALLEL SAFE;
diff --git a/contrib/pageinspect/pageinspect--1.5--1.6.sql b/contrib/pageinspect/pageinspect--1.5--1.6.sql
new file mode 100644
index 0000000..c435628
--- /dev/null
+++ b/contrib/pageinspect/pageinspect--1.5--1.6.sql
@@ -0,0 +1,99 @@
+/* contrib/pageinspect/pageinspect--1.5--1.6.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION pageinspect UPDATE TO '1.6'" to load this file. \quit
+
+--
+-- HASH functions
+--
+
+--
+-- hash_page_type()
+--
+CREATE FUNCTION hash_page_type(IN page bytea)
+RETURNS text
+AS 'MODULE_PATHNAME', 'hash_page_type'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+--
+-- hash_page_stats()
+--
+CREATE FUNCTION hash_page_stats(IN page bytea,
+ OUT live_items int4,
+ OUT dead_items int4,
+ OUT page_size int4,
+ OUT free_size int4,
+ OUT hasho_prevblkno int8,
+ OUT hasho_nextblkno int8,
+ OUT hasho_bucket int8,
+ OUT hasho_flag int4,
+ OUT hasho_page_id int4)
+AS 'MODULE_PATHNAME', 'hash_page_stats'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+--
+-- hash_page_items()
+--
+CREATE FUNCTION hash_page_items(IN page bytea,
+ OUT itemoffset int4,
+ OUT ctid tid,
+ OUT data int8)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'hash_page_items'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+--
+-- hash_bitmap_info()
+--
+CREATE FUNCTION hash_bitmap_info(IN index_oid regclass, IN blkno int8,
+ OUT bitmapblkno int8,
+ OUT bitmapbit int4,
+ OUT bitstatus bool)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'hash_bitmap_info'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+--
+-- hash_metapage_info()
+--
+CREATE FUNCTION hash_metapage_info(IN page bytea,
+ OUT magic int8,
+ OUT version int8,
+ OUT ntuples double precision,
+ OUT ffactor int4,
+ OUT bsize int4,
+ OUT bmsize int4,
+ OUT bmshift int4,
+ OUT maxbucket int8,
+ OUT highmask int8,
+ OUT lowmask int8,
+ OUT ovflpoint int8,
+ OUT firstfree int8,
+ OUT nmaps int8,
+ OUT procid oid,
+ OUT spares int8[],
+ OUT mapp int8[])
+AS 'MODULE_PATHNAME', 'hash_metapage_info'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+--
+-- page_checksum()
+--
+CREATE FUNCTION page_checksum(IN page bytea, IN blkno int4)
+RETURNS smallint
+AS 'MODULE_PATHNAME', 'page_checksum'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+--
+-- bt_page_items_bytea()
+--
+CREATE FUNCTION bt_page_items(IN page bytea,
+ OUT itemoffset smallint,
+ OUT ctid tid,
+ OUT itemlen smallint,
+ OUT nulls bool,
+ OUT vars bool,
+ OUT data text)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'bt_page_items_bytea'
+LANGUAGE C STRICT PARALLEL SAFE;
diff --git a/contrib/pageinspect/pageinspect--1.5.sql b/contrib/pageinspect/pageinspect--1.5.sql
new file mode 100644
index 0000000..1e40c3c
--- /dev/null
+++ b/contrib/pageinspect/pageinspect--1.5.sql
@@ -0,0 +1,279 @@
+/* contrib/pageinspect/pageinspect--1.5.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION pageinspect" to load this file. \quit
+
+--
+-- get_raw_page()
+--
+CREATE FUNCTION get_raw_page(text, int4)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'get_raw_page'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+CREATE FUNCTION get_raw_page(text, text, int4)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'get_raw_page_fork'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+--
+-- page_header()
+--
+CREATE FUNCTION page_header(IN page bytea,
+ OUT lsn pg_lsn,
+ OUT checksum smallint,
+ OUT flags smallint,
+ OUT lower smallint,
+ OUT upper smallint,
+ OUT special smallint,
+ OUT pagesize smallint,
+ OUT version smallint,
+ OUT prune_xid xid)
+AS 'MODULE_PATHNAME', 'page_header'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+--
+-- heap_page_items()
+--
+CREATE FUNCTION heap_page_items(IN page bytea,
+ OUT lp smallint,
+ OUT lp_off smallint,
+ OUT lp_flags smallint,
+ OUT lp_len smallint,
+ OUT t_xmin xid,
+ OUT t_xmax xid,
+ OUT t_field3 int4,
+ OUT t_ctid tid,
+ OUT t_infomask2 integer,
+ OUT t_infomask integer,
+ OUT t_hoff smallint,
+ OUT t_bits text,
+ OUT t_oid oid,
+ OUT t_data bytea)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'heap_page_items'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+--
+-- tuple_data_split()
+--
+CREATE FUNCTION tuple_data_split(rel_oid oid,
+ t_data bytea,
+ t_infomask integer,
+ t_infomask2 integer,
+ t_bits text)
+RETURNS bytea[]
+AS 'MODULE_PATHNAME','tuple_data_split'
+LANGUAGE C PARALLEL SAFE;
+
+CREATE FUNCTION tuple_data_split(rel_oid oid,
+ t_data bytea,
+ t_infomask integer,
+ t_infomask2 integer,
+ t_bits text,
+ do_detoast bool)
+RETURNS bytea[]
+AS 'MODULE_PATHNAME','tuple_data_split'
+LANGUAGE C PARALLEL SAFE;
+
+--
+-- heap_page_item_attrs()
+--
+CREATE FUNCTION heap_page_item_attrs(
+ IN page bytea,
+ IN rel_oid regclass,
+ IN do_detoast bool,
+ OUT lp smallint,
+ OUT lp_off smallint,
+ OUT lp_flags smallint,
+ OUT lp_len smallint,
+ OUT t_xmin xid,
+ OUT t_xmax xid,
+ OUT t_field3 int4,
+ OUT t_ctid tid,
+ OUT t_infomask2 integer,
+ OUT t_infomask integer,
+ OUT t_hoff smallint,
+ OUT t_bits text,
+ OUT t_oid oid,
+ OUT t_attrs bytea[]
+ )
+RETURNS SETOF record AS $$
+SELECT lp,
+ lp_off,
+ lp_flags,
+ lp_len,
+ t_xmin,
+ t_xmax,
+ t_field3,
+ t_ctid,
+ t_infomask2,
+ t_infomask,
+ t_hoff,
+ t_bits,
+ t_oid,
+ tuple_data_split(
+ rel_oid,
+ t_data,
+ t_infomask,
+ t_infomask2,
+ t_bits,
+ do_detoast)
+ AS t_attrs
+ FROM heap_page_items(page);
+$$ LANGUAGE SQL PARALLEL SAFE;
+
+CREATE FUNCTION heap_page_item_attrs(IN page bytea, IN rel_oid regclass,
+ OUT lp smallint,
+ OUT lp_off smallint,
+ OUT lp_flags smallint,
+ OUT lp_len smallint,
+ OUT t_xmin xid,
+ OUT t_xmax xid,
+ OUT t_field3 int4,
+ OUT t_ctid tid,
+ OUT t_infomask2 integer,
+ OUT t_infomask integer,
+ OUT t_hoff smallint,
+ OUT t_bits text,
+ OUT t_oid oid,
+ OUT t_attrs bytea[]
+ )
+RETURNS SETOF record AS $$
+SELECT * from heap_page_item_attrs(page, rel_oid, false);
+$$ LANGUAGE SQL PARALLEL SAFE;
+
+--
+-- bt_metap()
+--
+CREATE FUNCTION bt_metap(IN relname text,
+ OUT magic int4,
+ OUT version int4,
+ OUT root int4,
+ OUT level int4,
+ OUT fastroot int4,
+ OUT fastlevel int4)
+AS 'MODULE_PATHNAME', 'bt_metap'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+--
+-- bt_page_stats()
+--
+CREATE FUNCTION bt_page_stats(IN relname text, IN blkno int4,
+ OUT blkno int4,
+ OUT type "char",
+ OUT live_items int4,
+ OUT dead_items int4,
+ OUT avg_item_size int4,
+ OUT page_size int4,
+ OUT free_size int4,
+ OUT btpo_prev int4,
+ OUT btpo_next int4,
+ OUT btpo int4,
+ OUT btpo_flags int4)
+AS 'MODULE_PATHNAME', 'bt_page_stats'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+--
+-- bt_page_items()
+--
+CREATE FUNCTION bt_page_items(IN relname text, IN blkno int4,
+ OUT itemoffset smallint,
+ OUT ctid tid,
+ OUT itemlen smallint,
+ OUT nulls bool,
+ OUT vars bool,
+ OUT data text)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'bt_page_items'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+--
+-- brin_page_type()
+--
+CREATE FUNCTION brin_page_type(IN page bytea)
+RETURNS text
+AS 'MODULE_PATHNAME', 'brin_page_type'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+--
+-- brin_metapage_info()
+--
+CREATE FUNCTION brin_metapage_info(IN page bytea, OUT magic text,
+ OUT version integer, OUT pagesperrange integer, OUT lastrevmappage bigint)
+AS 'MODULE_PATHNAME', 'brin_metapage_info'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+--
+-- brin_revmap_data()
+--
+CREATE FUNCTION brin_revmap_data(IN page bytea,
+ OUT pages tid)
+RETURNS SETOF tid
+AS 'MODULE_PATHNAME', 'brin_revmap_data'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+--
+-- brin_page_items()
+--
+CREATE FUNCTION brin_page_items(IN page bytea, IN index_oid regclass,
+ OUT itemoffset int,
+ OUT blknum int,
+ OUT attnum int,
+ OUT allnulls bool,
+ OUT hasnulls bool,
+ OUT placeholder bool,
+ OUT value text)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'brin_page_items'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+--
+-- fsm_page_contents()
+--
+CREATE FUNCTION fsm_page_contents(IN page bytea)
+RETURNS text
+AS 'MODULE_PATHNAME', 'fsm_page_contents'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+--
+-- GIN functions
+--
+
+--
+-- gin_metapage_info()
+--
+CREATE FUNCTION gin_metapage_info(IN page bytea,
+ OUT pending_head bigint,
+ OUT pending_tail bigint,
+ OUT tail_free_size int4,
+ OUT n_pending_pages bigint,
+ OUT n_pending_tuples bigint,
+ OUT n_total_pages bigint,
+ OUT n_entry_pages bigint,
+ OUT n_data_pages bigint,
+ OUT n_entries bigint,
+ OUT version int4)
+AS 'MODULE_PATHNAME', 'gin_metapage_info'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+--
+-- gin_page_opaque_info()
+--
+CREATE FUNCTION gin_page_opaque_info(IN page bytea,
+ OUT rightlink bigint,
+ OUT maxoff int4,
+ OUT flags text[])
+AS 'MODULE_PATHNAME', 'gin_page_opaque_info'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+--
+-- gin_leafpage_items()
+--
+CREATE FUNCTION gin_leafpage_items(IN page bytea,
+ OUT first_tid tid,
+ OUT nbytes int2,
+ OUT tids tid[])
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'gin_leafpage_items'
+LANGUAGE C STRICT PARALLEL SAFE;
diff --git a/contrib/pageinspect/pageinspect--1.6--1.7.sql b/contrib/pageinspect/pageinspect--1.6--1.7.sql
new file mode 100644
index 0000000..2433a21
--- /dev/null
+++ b/contrib/pageinspect/pageinspect--1.6--1.7.sql
@@ -0,0 +1,26 @@
+/* contrib/pageinspect/pageinspect--1.6--1.7.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION pageinspect UPDATE TO '1.7'" to load this file. \quit
+
+--
+-- bt_metap()
+--
+DROP FUNCTION bt_metap(IN relname text,
+ OUT magic int4,
+ OUT version int4,
+ OUT root int4,
+ OUT level int4,
+ OUT fastroot int4,
+ OUT fastlevel int4);
+CREATE FUNCTION bt_metap(IN relname text,
+ OUT magic int4,
+ OUT version int4,
+ OUT root int4,
+ OUT level int4,
+ OUT fastroot int4,
+ OUT fastlevel int4,
+ OUT oldest_xact int4,
+ OUT last_cleanup_num_tuples real)
+AS 'MODULE_PATHNAME', 'bt_metap'
+LANGUAGE C STRICT PARALLEL SAFE;
diff --git a/contrib/pageinspect/pageinspect--1.7--1.8.sql b/contrib/pageinspect/pageinspect--1.7--1.8.sql
new file mode 100644
index 0000000..45031a0
--- /dev/null
+++ b/contrib/pageinspect/pageinspect--1.7--1.8.sql
@@ -0,0 +1,69 @@
+/* contrib/pageinspect/pageinspect--1.7--1.8.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION pageinspect UPDATE TO '1.8'" to load this file. \quit
+
+--
+-- heap_tuple_infomask_flags()
+--
+CREATE FUNCTION heap_tuple_infomask_flags(
+ t_infomask integer,
+ t_infomask2 integer,
+ raw_flags OUT text[],
+ combined_flags OUT text[])
+RETURNS record
+AS 'MODULE_PATHNAME', 'heap_tuple_infomask_flags'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+--
+-- bt_metap()
+--
+DROP FUNCTION bt_metap(text);
+CREATE FUNCTION bt_metap(IN relname text,
+ OUT magic int4,
+ OUT version int4,
+ OUT root int8,
+ OUT level int8,
+ OUT fastroot int8,
+ OUT fastlevel int8,
+ OUT oldest_xact xid,
+ OUT last_cleanup_num_tuples float8,
+ OUT allequalimage boolean)
+AS 'MODULE_PATHNAME', 'bt_metap'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+--
+-- bt_page_items(text, int4)
+--
+DROP FUNCTION bt_page_items(text, int4);
+CREATE FUNCTION bt_page_items(IN relname text, IN blkno int4,
+ OUT itemoffset smallint,
+ OUT ctid tid,
+ OUT itemlen smallint,
+ OUT nulls bool,
+ OUT vars bool,
+ OUT data text,
+ OUT dead boolean,
+ OUT htid tid,
+ OUT tids tid[])
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'bt_page_items'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+--
+-- bt_page_items(bytea)
+--
+DROP FUNCTION bt_page_items(bytea);
+CREATE FUNCTION bt_page_items(IN page bytea,
+ OUT itemoffset smallint,
+ OUT ctid tid,
+ OUT itemlen smallint,
+ OUT nulls bool,
+ OUT vars bool,
+ OUT data text,
+ OUT dead boolean,
+ OUT htid tid,
+ OUT tids tid[])
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'bt_page_items_bytea'
+LANGUAGE C STRICT PARALLEL SAFE;
diff --git a/contrib/pageinspect/pageinspect--1.8--1.9.sql b/contrib/pageinspect/pageinspect--1.8--1.9.sql
new file mode 100644
index 0000000..be89a64
--- /dev/null
+++ b/contrib/pageinspect/pageinspect--1.8--1.9.sql
@@ -0,0 +1,137 @@
+/* contrib/pageinspect/pageinspect--1.8--1.9.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION pageinspect UPDATE TO '1.9'" to load this file. \quit
+
+--
+-- gist_page_opaque_info()
+--
+CREATE FUNCTION gist_page_opaque_info(IN page bytea,
+ OUT lsn pg_lsn,
+ OUT nsn pg_lsn,
+ OUT rightlink bigint,
+ OUT flags text[])
+AS 'MODULE_PATHNAME', 'gist_page_opaque_info'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+
+--
+-- gist_page_items_bytea()
+--
+CREATE FUNCTION gist_page_items_bytea(IN page bytea,
+ OUT itemoffset smallint,
+ OUT ctid tid,
+ OUT itemlen smallint,
+ OUT dead boolean,
+ OUT key_data bytea)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'gist_page_items_bytea'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+--
+-- gist_page_items()
+--
+CREATE FUNCTION gist_page_items(IN page bytea,
+ IN index_oid regclass,
+ OUT itemoffset smallint,
+ OUT ctid tid,
+ OUT itemlen smallint,
+ OUT dead boolean,
+ OUT keys text)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'gist_page_items'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+--
+-- get_raw_page()
+--
+DROP FUNCTION get_raw_page(text, int4);
+CREATE FUNCTION get_raw_page(text, int8)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'get_raw_page_1_9'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+DROP FUNCTION get_raw_page(text, text, int4);
+CREATE FUNCTION get_raw_page(text, text, int8)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'get_raw_page_fork_1_9'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+--
+-- page_checksum()
+--
+DROP FUNCTION page_checksum(IN page bytea, IN blkno int4);
+CREATE FUNCTION page_checksum(IN page bytea, IN blkno int8)
+RETURNS smallint
+AS 'MODULE_PATHNAME', 'page_checksum_1_9'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+--
+-- bt_metap()
+--
+DROP FUNCTION bt_metap(text);
+CREATE FUNCTION bt_metap(IN relname text,
+ OUT magic int4,
+ OUT version int4,
+ OUT root int8,
+ OUT level int8,
+ OUT fastroot int8,
+ OUT fastlevel int8,
+ OUT last_cleanup_num_delpages int8,
+ OUT last_cleanup_num_tuples float8,
+ OUT allequalimage boolean)
+AS 'MODULE_PATHNAME', 'bt_metap'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+--
+-- bt_page_stats()
+--
+DROP FUNCTION bt_page_stats(text, int4);
+CREATE FUNCTION bt_page_stats(IN relname text, IN blkno int8,
+ OUT blkno int8,
+ OUT type "char",
+ OUT live_items int4,
+ OUT dead_items int4,
+ OUT avg_item_size int4,
+ OUT page_size int4,
+ OUT free_size int4,
+ OUT btpo_prev int8,
+ OUT btpo_next int8,
+ OUT btpo_level int8,
+ OUT btpo_flags int4)
+AS 'MODULE_PATHNAME', 'bt_page_stats_1_9'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+--
+-- bt_page_items()
+--
+DROP FUNCTION bt_page_items(text, int4);
+CREATE FUNCTION bt_page_items(IN relname text, IN blkno int8,
+ OUT itemoffset smallint,
+ OUT ctid tid,
+ OUT itemlen smallint,
+ OUT nulls bool,
+ OUT vars bool,
+ OUT data text,
+ OUT dead boolean,
+ OUT htid tid,
+ OUT tids tid[])
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'bt_page_items_1_9'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+--
+-- brin_page_items()
+--
+DROP FUNCTION brin_page_items(IN page bytea, IN index_oid regclass);
+CREATE FUNCTION brin_page_items(IN page bytea, IN index_oid regclass,
+ OUT itemoffset int,
+ OUT blknum int8,
+ OUT attnum int,
+ OUT allnulls bool,
+ OUT hasnulls bool,
+ OUT placeholder bool,
+ OUT value text)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'brin_page_items'
+LANGUAGE C STRICT PARALLEL SAFE;
diff --git a/contrib/pageinspect/pageinspect--1.9--1.10.sql b/contrib/pageinspect/pageinspect--1.9--1.10.sql
new file mode 100644
index 0000000..8dd9a27
--- /dev/null
+++ b/contrib/pageinspect/pageinspect--1.9--1.10.sql
@@ -0,0 +1,21 @@
+/* contrib/pageinspect/pageinspect--1.9--1.10.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION pageinspect UPDATE TO '1.10'" to load this file. \quit
+
+--
+-- page_header()
+--
+DROP FUNCTION page_header(IN page bytea);
+CREATE FUNCTION page_header(IN page bytea,
+ OUT lsn pg_lsn,
+ OUT checksum smallint,
+ OUT flags smallint,
+ OUT lower int,
+ OUT upper int,
+ OUT special int,
+ OUT pagesize int,
+ OUT version smallint,
+ OUT prune_xid xid)
+AS 'MODULE_PATHNAME', 'page_header'
+LANGUAGE C STRICT PARALLEL SAFE;
diff --git a/contrib/pageinspect/pageinspect.control b/contrib/pageinspect/pageinspect.control
new file mode 100644
index 0000000..b2804e9
--- /dev/null
+++ b/contrib/pageinspect/pageinspect.control
@@ -0,0 +1,5 @@
+# pageinspect extension
+comment = 'inspect the contents of database pages at a low level'
+default_version = '1.12'
+module_pathname = '$libdir/pageinspect'
+relocatable = true
diff --git a/contrib/pageinspect/pageinspect.h b/contrib/pageinspect/pageinspect.h
new file mode 100644
index 0000000..3f0fe7e
--- /dev/null
+++ b/contrib/pageinspect/pageinspect.h
@@ -0,0 +1,30 @@
+/*-------------------------------------------------------------------------
+ *
+ * pageinspect.h
+ * Common functions for pageinspect.
+ *
+ * Copyright (c) 2017-2023, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pageinspect/pageinspect.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef _PAGEINSPECT_H_
+#define _PAGEINSPECT_H_
+
+#include "storage/bufpage.h"
+
+/*
+ * Extension version number, for supporting older extension versions' objects
+ */
+enum pageinspect_version
+{
+ PAGEINSPECT_V1_8,
+ PAGEINSPECT_V1_9,
+};
+
+/* in rawpage.c */
+extern Page get_page_from_raw(bytea *raw_page);
+
+#endif /* _PAGEINSPECT_H_ */
diff --git a/contrib/pageinspect/rawpage.c b/contrib/pageinspect/rawpage.c
new file mode 100644
index 0000000..b25a63c
--- /dev/null
+++ b/contrib/pageinspect/rawpage.c
@@ -0,0 +1,376 @@
+/*-------------------------------------------------------------------------
+ *
+ * rawpage.c
+ * Functions to extract a raw page as bytea and inspect it
+ *
+ * Access-method specific inspection functions are in separate files.
+ *
+ * Copyright (c) 2007-2023, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pageinspect/rawpage.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/relation.h"
+#include "catalog/namespace.h"
+#include "catalog/pg_type.h"
+#include "funcapi.h"
+#include "miscadmin.h"
+#include "pageinspect.h"
+#include "storage/bufmgr.h"
+#include "storage/checksum.h"
+#include "utils/builtins.h"
+#include "utils/pg_lsn.h"
+#include "utils/rel.h"
+#include "utils/varlena.h"
+
+PG_MODULE_MAGIC;
+
+static bytea *get_raw_page_internal(text *relname, ForkNumber forknum,
+ BlockNumber blkno);
+
+
+/*
+ * get_raw_page
+ *
+ * Returns a copy of a page from shared buffers as a bytea
+ */
+PG_FUNCTION_INFO_V1(get_raw_page_1_9);
+
+Datum
+get_raw_page_1_9(PG_FUNCTION_ARGS)
+{
+ text *relname = PG_GETARG_TEXT_PP(0);
+ int64 blkno = PG_GETARG_INT64(1);
+ bytea *raw_page;
+
+ if (blkno < 0 || blkno > MaxBlockNumber)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("invalid block number")));
+
+ raw_page = get_raw_page_internal(relname, MAIN_FORKNUM, blkno);
+
+ PG_RETURN_BYTEA_P(raw_page);
+}
+
+/*
+ * entry point for old extension version
+ */
+PG_FUNCTION_INFO_V1(get_raw_page);
+
+Datum
+get_raw_page(PG_FUNCTION_ARGS)
+{
+ text *relname = PG_GETARG_TEXT_PP(0);
+ uint32 blkno = PG_GETARG_UINT32(1);
+ bytea *raw_page;
+
+ /*
+ * We don't normally bother to check the number of arguments to a C
+ * function, but here it's needed for safety because early 8.4 beta
+ * releases mistakenly redefined get_raw_page() as taking three arguments.
+ */
+ if (PG_NARGS() != 2)
+ ereport(ERROR,
+ (errmsg("wrong number of arguments to get_raw_page()"),
+ errhint("Run the updated pageinspect.sql script.")));
+
+ raw_page = get_raw_page_internal(relname, MAIN_FORKNUM, blkno);
+
+ PG_RETURN_BYTEA_P(raw_page);
+}
+
+/*
+ * get_raw_page_fork
+ *
+ * Same, for any fork
+ */
+PG_FUNCTION_INFO_V1(get_raw_page_fork_1_9);
+
+Datum
+get_raw_page_fork_1_9(PG_FUNCTION_ARGS)
+{
+ text *relname = PG_GETARG_TEXT_PP(0);
+ text *forkname = PG_GETARG_TEXT_PP(1);
+ int64 blkno = PG_GETARG_INT64(2);
+ bytea *raw_page;
+ ForkNumber forknum;
+
+ forknum = forkname_to_number(text_to_cstring(forkname));
+
+ if (blkno < 0 || blkno > MaxBlockNumber)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("invalid block number")));
+
+ raw_page = get_raw_page_internal(relname, forknum, blkno);
+
+ PG_RETURN_BYTEA_P(raw_page);
+}
+
+/*
+ * Entry point for old extension version
+ */
+PG_FUNCTION_INFO_V1(get_raw_page_fork);
+
+Datum
+get_raw_page_fork(PG_FUNCTION_ARGS)
+{
+ text *relname = PG_GETARG_TEXT_PP(0);
+ text *forkname = PG_GETARG_TEXT_PP(1);
+ uint32 blkno = PG_GETARG_UINT32(2);
+ bytea *raw_page;
+ ForkNumber forknum;
+
+ forknum = forkname_to_number(text_to_cstring(forkname));
+
+ raw_page = get_raw_page_internal(relname, forknum, blkno);
+
+ PG_RETURN_BYTEA_P(raw_page);
+}
+
+/*
+ * workhorse
+ */
+static bytea *
+get_raw_page_internal(text *relname, ForkNumber forknum, BlockNumber blkno)
+{
+ bytea *raw_page;
+ RangeVar *relrv;
+ Relation rel;
+ char *raw_page_data;
+ Buffer buf;
+
+ if (!superuser())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions")));
+
+ relrv = makeRangeVarFromNameList(textToQualifiedNameList(relname));
+ rel = relation_openrv(relrv, AccessShareLock);
+
+ if (!RELKIND_HAS_STORAGE(rel->rd_rel->relkind))
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot get raw page from relation \"%s\"",
+ RelationGetRelationName(rel)),
+ errdetail_relkind_not_supported(rel->rd_rel->relkind)));
+
+ /*
+ * Reject attempts to read non-local temporary relations; we would be
+ * likely to get wrong data since we have no visibility into the owning
+ * session's local buffers.
+ */
+ if (RELATION_IS_OTHER_TEMP(rel))
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot access temporary tables of other sessions")));
+
+ if (blkno >= RelationGetNumberOfBlocksInFork(rel, forknum))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("block number %u is out of range for relation \"%s\"",
+ blkno, RelationGetRelationName(rel))));
+
+ /* Initialize buffer to copy to */
+ raw_page = (bytea *) palloc(BLCKSZ + VARHDRSZ);
+ SET_VARSIZE(raw_page, BLCKSZ + VARHDRSZ);
+ raw_page_data = VARDATA(raw_page);
+
+ /* Take a verbatim copy of the page */
+
+ buf = ReadBufferExtended(rel, forknum, blkno, RBM_NORMAL, NULL);
+ LockBuffer(buf, BUFFER_LOCK_SHARE);
+
+ memcpy(raw_page_data, BufferGetPage(buf), BLCKSZ);
+
+ LockBuffer(buf, BUFFER_LOCK_UNLOCK);
+ ReleaseBuffer(buf);
+
+ relation_close(rel, AccessShareLock);
+
+ return raw_page;
+}
+
+
+/*
+ * get_page_from_raw
+ *
+ * Get a palloc'd, maxalign'ed page image from the result of get_raw_page()
+ *
+ * On machines with MAXALIGN = 8, the payload of a bytea is not maxaligned,
+ * since it will start 4 bytes into a palloc'd value. On alignment-picky
+ * machines, this will cause failures in accesses to 8-byte-wide values
+ * within the page. We don't need to worry if accessing only 4-byte or
+ * smaller fields, but when examining a struct that contains 8-byte fields,
+ * use this function for safety.
+ */
+Page
+get_page_from_raw(bytea *raw_page)
+{
+ Page page;
+ int raw_page_size;
+
+ raw_page_size = VARSIZE_ANY_EXHDR(raw_page);
+
+ if (raw_page_size != BLCKSZ)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("invalid page size"),
+ errdetail("Expected %d bytes, got %d.",
+ BLCKSZ, raw_page_size)));
+
+ page = palloc(raw_page_size);
+
+ memcpy(page, VARDATA_ANY(raw_page), raw_page_size);
+
+ return page;
+}
+
+
+/*
+ * page_header
+ *
+ * Allows inspection of page header fields of a raw page
+ */
+
+PG_FUNCTION_INFO_V1(page_header);
+
+Datum
+page_header(PG_FUNCTION_ARGS)
+{
+ bytea *raw_page = PG_GETARG_BYTEA_P(0);
+
+ TupleDesc tupdesc;
+
+ Datum result;
+ HeapTuple tuple;
+ Datum values[9];
+ bool nulls[9];
+
+ Page page;
+ PageHeader pageheader;
+ XLogRecPtr lsn;
+
+ if (!superuser())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions")));
+
+ page = get_page_from_raw(raw_page);
+ pageheader = (PageHeader) page;
+
+ /* Build a tuple descriptor for our result type */
+ if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+ elog(ERROR, "return type must be a row type");
+
+ /* Extract information from the page header */
+
+ lsn = PageGetLSN(page);
+
+ /* pageinspect >= 1.2 uses pg_lsn instead of text for the LSN field. */
+ if (TupleDescAttr(tupdesc, 0)->atttypid == TEXTOID)
+ {
+ char lsnchar[64];
+
+ snprintf(lsnchar, sizeof(lsnchar), "%X/%X", LSN_FORMAT_ARGS(lsn));
+ values[0] = CStringGetTextDatum(lsnchar);
+ }
+ else
+ values[0] = LSNGetDatum(lsn);
+ values[1] = UInt16GetDatum(pageheader->pd_checksum);
+ values[2] = UInt16GetDatum(pageheader->pd_flags);
+
+ /* pageinspect >= 1.10 uses int4 instead of int2 for those fields */
+ switch (TupleDescAttr(tupdesc, 3)->atttypid)
+ {
+ case INT2OID:
+ Assert(TupleDescAttr(tupdesc, 4)->atttypid == INT2OID &&
+ TupleDescAttr(tupdesc, 5)->atttypid == INT2OID &&
+ TupleDescAttr(tupdesc, 6)->atttypid == INT2OID);
+ values[3] = UInt16GetDatum(pageheader->pd_lower);
+ values[4] = UInt16GetDatum(pageheader->pd_upper);
+ values[5] = UInt16GetDatum(pageheader->pd_special);
+ values[6] = UInt16GetDatum(PageGetPageSize(page));
+ break;
+ case INT4OID:
+ Assert(TupleDescAttr(tupdesc, 4)->atttypid == INT4OID &&
+ TupleDescAttr(tupdesc, 5)->atttypid == INT4OID &&
+ TupleDescAttr(tupdesc, 6)->atttypid == INT4OID);
+ values[3] = Int32GetDatum(pageheader->pd_lower);
+ values[4] = Int32GetDatum(pageheader->pd_upper);
+ values[5] = Int32GetDatum(pageheader->pd_special);
+ values[6] = Int32GetDatum(PageGetPageSize(page));
+ break;
+ default:
+ elog(ERROR, "incorrect output types");
+ break;
+ }
+
+ values[7] = UInt16GetDatum(PageGetPageLayoutVersion(page));
+ values[8] = TransactionIdGetDatum(pageheader->pd_prune_xid);
+
+ /* Build and return the tuple. */
+
+ memset(nulls, 0, sizeof(nulls));
+
+ tuple = heap_form_tuple(tupdesc, values, nulls);
+ result = HeapTupleGetDatum(tuple);
+
+ PG_RETURN_DATUM(result);
+}
+
+/*
+ * page_checksum
+ *
+ * Compute checksum of a raw page
+ */
+
+PG_FUNCTION_INFO_V1(page_checksum_1_9);
+PG_FUNCTION_INFO_V1(page_checksum);
+
+static Datum
+page_checksum_internal(PG_FUNCTION_ARGS, enum pageinspect_version ext_version)
+{
+ bytea *raw_page = PG_GETARG_BYTEA_P(0);
+ int64 blkno = (ext_version == PAGEINSPECT_V1_8 ? PG_GETARG_UINT32(1) : PG_GETARG_INT64(1));
+ Page page;
+
+ if (!superuser())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions")));
+
+ if (blkno < 0 || blkno > MaxBlockNumber)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("invalid block number")));
+
+ page = get_page_from_raw(raw_page);
+
+ if (PageIsNew(page))
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT16(pg_checksum_page((char *) page, blkno));
+}
+
+Datum
+page_checksum_1_9(PG_FUNCTION_ARGS)
+{
+ return page_checksum_internal(fcinfo, PAGEINSPECT_V1_9);
+}
+
+/*
+ * Entry point for old extension version
+ */
+Datum
+page_checksum(PG_FUNCTION_ARGS)
+{
+ return page_checksum_internal(fcinfo, PAGEINSPECT_V1_8);
+}
diff --git a/contrib/pageinspect/sql/brin.sql b/contrib/pageinspect/sql/brin.sql
new file mode 100644
index 0000000..96b4645
--- /dev/null
+++ b/contrib/pageinspect/sql/brin.sql
@@ -0,0 +1,39 @@
+CREATE TABLE test1 (a int, b text);
+INSERT INTO test1 VALUES (1, 'one');
+CREATE INDEX test1_a_idx ON test1 USING brin (a);
+
+SELECT brin_page_type(get_raw_page('test1_a_idx', 0));
+SELECT brin_page_type(get_raw_page('test1_a_idx', 1));
+SELECT brin_page_type(get_raw_page('test1_a_idx', 2));
+
+SELECT * FROM brin_metapage_info(get_raw_page('test1_a_idx', 0));
+SELECT * FROM brin_metapage_info(get_raw_page('test1_a_idx', 1));
+
+SELECT * FROM brin_revmap_data(get_raw_page('test1_a_idx', 0)) LIMIT 5;
+SELECT * FROM brin_revmap_data(get_raw_page('test1_a_idx', 1)) LIMIT 5;
+
+SELECT * FROM brin_page_items(get_raw_page('test1_a_idx', 2), 'test1_a_idx')
+ ORDER BY blknum, attnum LIMIT 5;
+
+-- Mask DETAIL messages as these are not portable across architectures.
+\set VERBOSITY terse
+
+-- Failures for non-BRIN index.
+CREATE INDEX test1_a_btree ON test1 (a);
+SELECT brin_page_items(get_raw_page('test1_a_btree', 0), 'test1_a_btree');
+SELECT brin_page_items(get_raw_page('test1_a_btree', 0), 'test1_a_idx');
+
+-- Invalid special area size
+SELECT brin_page_type(get_raw_page('test1', 0));
+SELECT * FROM brin_metapage_info(get_raw_page('test1', 0));
+SELECT * FROM brin_revmap_data(get_raw_page('test1', 0));
+\set VERBOSITY default
+
+-- Tests with all-zero pages.
+SHOW block_size \gset
+SELECT brin_page_type(decode(repeat('00', :block_size), 'hex'));
+SELECT brin_page_items(decode(repeat('00', :block_size), 'hex'), 'test1_a_idx');
+SELECT brin_metapage_info(decode(repeat('00', :block_size), 'hex'));
+SELECT brin_revmap_data(decode(repeat('00', :block_size), 'hex'));
+
+DROP TABLE test1;
diff --git a/contrib/pageinspect/sql/btree.sql b/contrib/pageinspect/sql/btree.sql
new file mode 100644
index 0000000..102ebde
--- /dev/null
+++ b/contrib/pageinspect/sql/btree.sql
@@ -0,0 +1,62 @@
+CREATE TABLE test1 (a int8, b int4range);
+INSERT INTO test1 VALUES (72057594037927937, '[0,1)');
+CREATE INDEX test1_a_idx ON test1 USING btree (a);
+
+\x
+
+SELECT * FROM bt_metap('test1_a_idx');
+
+SELECT * FROM bt_page_stats('test1_a_idx', -1);
+SELECT * FROM bt_page_stats('test1_a_idx', 0);
+SELECT * FROM bt_page_stats('test1_a_idx', 1);
+SELECT * FROM bt_page_stats('test1_a_idx', 2);
+
+-- bt_multi_page_stats() function returns a set of records of page statistics.
+CREATE TABLE test2 AS (SELECT generate_series(1, 1000)::int8 AS col1);
+CREATE INDEX test2_col1_idx ON test2(col1);
+SELECT * FROM bt_multi_page_stats('test2_col1_idx', 0, 1);
+SELECT * FROM bt_multi_page_stats('test2_col1_idx', 1, -1);
+SELECT * FROM bt_multi_page_stats('test2_col1_idx', 1, 0);
+SELECT * FROM bt_multi_page_stats('test2_col1_idx', 1, 2);
+SELECT * FROM bt_multi_page_stats('test2_col1_idx', 3, 2);
+SELECT * FROM bt_multi_page_stats('test2_col1_idx', 7, 2);
+DROP TABLE test2;
+
+SELECT * FROM bt_page_items('test1_a_idx', -1);
+SELECT * FROM bt_page_items('test1_a_idx', 0);
+SELECT * FROM bt_page_items('test1_a_idx', 1);
+SELECT * FROM bt_page_items('test1_a_idx', 2);
+
+SELECT * FROM bt_page_items(get_raw_page('test1_a_idx', -1));
+SELECT * FROM bt_page_items(get_raw_page('test1_a_idx', 0));
+SELECT * FROM bt_page_items(get_raw_page('test1_a_idx', 1));
+SELECT * FROM bt_page_items(get_raw_page('test1_a_idx', 2));
+
+-- Failure when using a non-btree index.
+CREATE INDEX test1_a_hash ON test1 USING hash(a);
+SELECT bt_metap('test1_a_hash');
+SELECT bt_page_stats('test1_a_hash', 0);
+SELECT bt_page_items('test1_a_hash', 0);
+SELECT bt_page_items(get_raw_page('test1_a_hash', 0));
+CREATE INDEX test1_b_gist ON test1 USING gist(b);
+-- Special area of GiST is the same as btree, this complains about inconsistent
+-- leaf data on the page.
+SELECT bt_page_items(get_raw_page('test1_b_gist', 0));
+
+-- Several failure modes.
+-- Suppress the DETAIL message, to allow the tests to work across various
+-- page sizes and architectures.
+\set VERBOSITY terse
+-- invalid page size
+SELECT bt_page_items('aaa'::bytea);
+-- invalid special area size
+CREATE INDEX test1_a_brin ON test1 USING brin(a);
+SELECT bt_page_items(get_raw_page('test1', 0));
+SELECT bt_page_items(get_raw_page('test1_a_brin', 0));
+\set VERBOSITY default
+
+-- Tests with all-zero pages.
+SHOW block_size \gset
+SELECT bt_page_items(decode(repeat('00', :block_size), 'hex'));
+
+DROP TABLE test1;
diff --git a/contrib/pageinspect/sql/checksum.sql b/contrib/pageinspect/sql/checksum.sql
new file mode 100644
index 0000000..b877db0
--- /dev/null
+++ b/contrib/pageinspect/sql/checksum.sql
@@ -0,0 +1,31 @@
+--
+-- Verify correct calculation of checksums
+--
+-- Postgres' checksum algorithm produces different answers on little-endian
+-- and big-endian machines. The results of this test also vary depending
+-- on the configured block size. This test has several different expected
+-- results files to handle the following possibilities:
+--
+-- BLCKSZ end file
+-- 8K LE checksum.out
+-- 8K BE checksum_1.out
+--
+-- In future we might provide additional expected-results files for other
+-- block sizes, but there seems little point as long as so many other
+-- test scripts also show false failures for non-default block sizes.
+--
+
+-- This is to label the results files with blocksize:
+SHOW block_size;
+
+SHOW block_size \gset
+
+-- Apply page_checksum() to some different data patterns and block numbers
+SELECT blkno,
+ page_checksum(decode(repeat('01', :block_size), 'hex'), blkno) AS checksum_01,
+ page_checksum(decode(repeat('04', :block_size), 'hex'), blkno) AS checksum_04,
+ page_checksum(decode(repeat('ff', :block_size), 'hex'), blkno) AS checksum_ff,
+ page_checksum(decode(repeat('abcd', :block_size / 2), 'hex'), blkno) AS checksum_abcd,
+ page_checksum(decode(repeat('e6d6', :block_size / 2), 'hex'), blkno) AS checksum_e6d6,
+ page_checksum(decode(repeat('4a5e', :block_size / 2), 'hex'), blkno) AS checksum_4a5e
+ FROM generate_series(0, 100, 50) AS a (blkno);
diff --git a/contrib/pageinspect/sql/gin.sql b/contrib/pageinspect/sql/gin.sql
new file mode 100644
index 0000000..b57466d
--- /dev/null
+++ b/contrib/pageinspect/sql/gin.sql
@@ -0,0 +1,41 @@
+CREATE TABLE test1 (x int, y int[]);
+INSERT INTO test1 VALUES (1, ARRAY[11, 111]);
+CREATE INDEX test1_y_idx ON test1 USING gin (y) WITH (fastupdate = off);
+
+\x
+
+SELECT * FROM gin_metapage_info(get_raw_page('test1_y_idx', 0));
+SELECT * FROM gin_metapage_info(get_raw_page('test1_y_idx', 1));
+
+SELECT * FROM gin_page_opaque_info(get_raw_page('test1_y_idx', 1));
+
+SELECT * FROM gin_leafpage_items(get_raw_page('test1_y_idx', 1));
+
+INSERT INTO test1 SELECT x, ARRAY[1,10] FROM generate_series(2,10000) x;
+
+SELECT COUNT(*) > 0
+FROM gin_leafpage_items(get_raw_page('test1_y_idx',
+ (pg_relation_size('test1_y_idx') /
+ current_setting('block_size')::bigint)::int - 1));
+
+-- Failure with various modes.
+-- Suppress the DETAIL message, to allow the tests to work across various
+-- page sizes and architectures.
+\set VERBOSITY terse
+-- invalid page size
+SELECT gin_leafpage_items('aaa'::bytea);
+SELECT gin_metapage_info('bbb'::bytea);
+SELECT gin_page_opaque_info('ccc'::bytea);
+-- invalid special area size
+SELECT * FROM gin_metapage_info(get_raw_page('test1', 0));
+SELECT * FROM gin_page_opaque_info(get_raw_page('test1', 0));
+SELECT * FROM gin_leafpage_items(get_raw_page('test1', 0));
+\set VERBOSITY default
+
+-- Tests with all-zero pages.
+SHOW block_size \gset
+SELECT gin_leafpage_items(decode(repeat('00', :block_size), 'hex'));
+SELECT gin_metapage_info(decode(repeat('00', :block_size), 'hex'));
+SELECT gin_page_opaque_info(decode(repeat('00', :block_size), 'hex'));
+
+DROP TABLE test1;
diff --git a/contrib/pageinspect/sql/gist.sql b/contrib/pageinspect/sql/gist.sql
new file mode 100644
index 0000000..d263542
--- /dev/null
+++ b/contrib/pageinspect/sql/gist.sql
@@ -0,0 +1,69 @@
+-- The gist_page_opaque_info() function prints the page's LSN. Normally,
+-- that's constant 1 (GistBuildLSN) on every page of a freshly built GiST
+-- index. But with wal_level=minimal, the whole relation is dumped to WAL at
+-- the end of the transaction if it's smaller than wal_skip_threshold, which
+-- updates the LSNs. Wrap the tests on gist_page_opaque_info() in the
+-- same transaction with the CREATE INDEX so that we see the LSNs before
+-- they are possibly overwritten at end of transaction.
+BEGIN;
+
+-- Create a test table and GiST index.
+CREATE TABLE test_gist AS SELECT point(i,i) p, i::text t FROM
+ generate_series(1,1000) i;
+CREATE INDEX test_gist_idx ON test_gist USING gist (p);
+
+-- Page 0 is the root, the rest are leaf pages
+SELECT * FROM gist_page_opaque_info(get_raw_page('test_gist_idx', 0));
+SELECT * FROM gist_page_opaque_info(get_raw_page('test_gist_idx', 1));
+SELECT * FROM gist_page_opaque_info(get_raw_page('test_gist_idx', 2));
+
+COMMIT;
+
+SELECT * FROM gist_page_items(get_raw_page('test_gist_idx', 0), 'test_gist_idx');
+SELECT * FROM gist_page_items(get_raw_page('test_gist_idx', 1), 'test_gist_idx') LIMIT 5;
+
+-- gist_page_items_bytea prints the raw key data as a bytea. The output of that is
+-- platform-dependent (endianness), so omit the actual key data from the output.
+SELECT itemoffset, ctid, itemlen FROM gist_page_items_bytea(get_raw_page('test_gist_idx', 0));
+
+-- Suppress the DETAIL message, to allow the tests to work across various
+-- page sizes and architectures.
+\set VERBOSITY terse
+
+-- Failures with non-GiST index.
+CREATE INDEX test_gist_btree on test_gist(t);
+SELECT gist_page_items(get_raw_page('test_gist_btree', 0), 'test_gist_btree');
+SELECT gist_page_items(get_raw_page('test_gist_btree', 0), 'test_gist_idx');
+
+-- Failure with various modes.
+-- invalid page size
+SELECT gist_page_items_bytea('aaa'::bytea);
+SELECT gist_page_items('aaa'::bytea, 'test_gist_idx'::regclass);
+SELECT gist_page_opaque_info('aaa'::bytea);
+-- invalid special area size
+SELECT * FROM gist_page_opaque_info(get_raw_page('test_gist', 0));
+SELECT gist_page_items_bytea(get_raw_page('test_gist', 0));
+SELECT gist_page_items_bytea(get_raw_page('test_gist_btree', 0));
+\set VERBOSITY default
+
+-- Tests with all-zero pages.
+SHOW block_size \gset
+SELECT gist_page_items_bytea(decode(repeat('00', :block_size), 'hex'));
+SELECT gist_page_items(decode(repeat('00', :block_size), 'hex'), 'test_gist_idx'::regclass);
+SELECT gist_page_opaque_info(decode(repeat('00', :block_size), 'hex'));
+
+-- Test gist_page_items with included columns.
+-- Non-leaf pages contain only the key attributes, and leaf pages contain
+-- the included attributes.
+ALTER TABLE test_gist ADD COLUMN i int DEFAULT NULL;
+CREATE INDEX test_gist_idx_inc ON test_gist
+ USING gist (p) INCLUDE (t, i);
+-- Mask the value of the key attribute to avoid alignment issues.
+SELECT regexp_replace(keys, '\(p\)=\("(.*?)"\)', '(p)=("<val>")') AS keys_nonleaf_1
+ FROM gist_page_items(get_raw_page('test_gist_idx_inc', 0), 'test_gist_idx_inc')
+ WHERE itemoffset = 1;
+SELECT keys AS keys_leaf_1
+ FROM gist_page_items(get_raw_page('test_gist_idx_inc', 1), 'test_gist_idx_inc')
+ WHERE itemoffset = 1;
+
+DROP TABLE test_gist;
diff --git a/contrib/pageinspect/sql/hash.sql b/contrib/pageinspect/sql/hash.sql
new file mode 100644
index 0000000..e4b9e97
--- /dev/null
+++ b/contrib/pageinspect/sql/hash.sql
@@ -0,0 +1,113 @@
+CREATE TABLE test_hash (a int, b text);
+INSERT INTO test_hash VALUES (1, 'one');
+CREATE INDEX test_hash_a_idx ON test_hash USING hash (a);
+
+CREATE TABLE test_hash_part (a int, b int) PARTITION BY RANGE (a);
+CREATE INDEX test_hash_part_idx ON test_hash_part USING hash(b);
+
+\x
+
+SELECT hash_page_type(get_raw_page('test_hash_a_idx', 0));
+SELECT hash_page_type(get_raw_page('test_hash_a_idx', 1));
+SELECT hash_page_type(get_raw_page('test_hash_a_idx', 2));
+SELECT hash_page_type(get_raw_page('test_hash_a_idx', 3));
+SELECT hash_page_type(get_raw_page('test_hash_a_idx', 4));
+SELECT hash_page_type(get_raw_page('test_hash_a_idx', 5));
+SELECT hash_page_type(get_raw_page('test_hash_a_idx', 6));
+
+
+SELECT * FROM hash_bitmap_info('test_hash_a_idx', -1);
+SELECT * FROM hash_bitmap_info('test_hash_a_idx', 0);
+SELECT * FROM hash_bitmap_info('test_hash_a_idx', 1);
+SELECT * FROM hash_bitmap_info('test_hash_a_idx', 2);
+SELECT * FROM hash_bitmap_info('test_hash_a_idx', 3);
+SELECT * FROM hash_bitmap_info('test_hash_a_idx', 4);
+SELECT * FROM hash_bitmap_info('test_hash_a_idx', 5);
+SELECT * FROM hash_bitmap_info('test_hash_a_idx', 6);
+SELECT * FROM hash_bitmap_info('test_hash_part_idx', 1); -- error
+
+
+SELECT magic, version, ntuples, bsize, bmsize, bmshift, maxbucket, highmask,
+lowmask, ovflpoint, firstfree, nmaps, procid, spares, mapp FROM
+hash_metapage_info(get_raw_page('test_hash_a_idx', 0));
+
+SELECT magic, version, ntuples, bsize, bmsize, bmshift, maxbucket, highmask,
+lowmask, ovflpoint, firstfree, nmaps, procid, spares, mapp FROM
+hash_metapage_info(get_raw_page('test_hash_a_idx', 1));
+
+SELECT magic, version, ntuples, bsize, bmsize, bmshift, maxbucket, highmask,
+lowmask, ovflpoint, firstfree, nmaps, procid, spares, mapp FROM
+hash_metapage_info(get_raw_page('test_hash_a_idx', 2));
+
+SELECT magic, version, ntuples, bsize, bmsize, bmshift, maxbucket, highmask,
+lowmask, ovflpoint, firstfree, nmaps, procid, spares, mapp FROM
+hash_metapage_info(get_raw_page('test_hash_a_idx', 3));
+
+SELECT magic, version, ntuples, bsize, bmsize, bmshift, maxbucket, highmask,
+lowmask, ovflpoint, firstfree, nmaps, procid, spares, mapp FROM
+hash_metapage_info(get_raw_page('test_hash_a_idx', 4));
+
+SELECT magic, version, ntuples, bsize, bmsize, bmshift, maxbucket, highmask,
+lowmask, ovflpoint, firstfree, nmaps, procid, spares, mapp FROM
+hash_metapage_info(get_raw_page('test_hash_a_idx', 5));
+
+SELECT live_items, dead_items, page_size, hasho_prevblkno, hasho_nextblkno,
+hasho_bucket, hasho_flag, hasho_page_id FROM
+hash_page_stats(get_raw_page('test_hash_a_idx', 0));
+
+SELECT live_items, dead_items, page_size, hasho_prevblkno, hasho_nextblkno,
+hasho_bucket, hasho_flag, hasho_page_id FROM
+hash_page_stats(get_raw_page('test_hash_a_idx', 1));
+
+SELECT live_items, dead_items, page_size, hasho_prevblkno, hasho_nextblkno,
+hasho_bucket, hasho_flag, hasho_page_id FROM
+hash_page_stats(get_raw_page('test_hash_a_idx', 2));
+
+SELECT live_items, dead_items, page_size, hasho_prevblkno, hasho_nextblkno,
+hasho_bucket, hasho_flag, hasho_page_id FROM
+hash_page_stats(get_raw_page('test_hash_a_idx', 3));
+
+SELECT live_items, dead_items, page_size, hasho_prevblkno, hasho_nextblkno,
+hasho_bucket, hasho_flag, hasho_page_id FROM
+hash_page_stats(get_raw_page('test_hash_a_idx', 4));
+
+SELECT live_items, dead_items, page_size, hasho_prevblkno, hasho_nextblkno,
+hasho_bucket, hasho_flag, hasho_page_id FROM
+hash_page_stats(get_raw_page('test_hash_a_idx', 5));
+
+SELECT * FROM hash_page_items(get_raw_page('test_hash_a_idx', 0));
+SELECT * FROM hash_page_items(get_raw_page('test_hash_a_idx', 1));
+SELECT * FROM hash_page_items(get_raw_page('test_hash_a_idx', 2));
+SELECT * FROM hash_page_items(get_raw_page('test_hash_a_idx', 3));
+SELECT * FROM hash_page_items(get_raw_page('test_hash_a_idx', 4));
+SELECT * FROM hash_page_items(get_raw_page('test_hash_a_idx', 5));
+
+-- Failure with non-hash index
+CREATE INDEX test_hash_a_btree ON test_hash USING btree (a);
+SELECT hash_bitmap_info('test_hash_a_btree', 0);
+
+-- Failure with various modes.
+-- Suppress the DETAIL message, to allow the tests to work across various
+-- page sizes and architectures.
+\set VERBOSITY terse
+-- invalid page size
+SELECT hash_metapage_info('aaa'::bytea);
+SELECT hash_page_items('bbb'::bytea);
+SELECT hash_page_stats('ccc'::bytea);
+SELECT hash_page_type('ddd'::bytea);
+-- invalid special area size
+SELECT hash_metapage_info(get_raw_page('test_hash', 0));
+SELECT hash_page_items(get_raw_page('test_hash', 0));
+SELECT hash_page_stats(get_raw_page('test_hash', 0));
+SELECT hash_page_type(get_raw_page('test_hash', 0));
+\set VERBOSITY default
+
+-- Tests with all-zero pages.
+SHOW block_size \gset
+SELECT hash_metapage_info(decode(repeat('00', :block_size), 'hex'));
+SELECT hash_page_items(decode(repeat('00', :block_size), 'hex'));
+SELECT hash_page_stats(decode(repeat('00', :block_size), 'hex'));
+SELECT hash_page_type(decode(repeat('00', :block_size), 'hex'));
+
+DROP TABLE test_hash;
+DROP TABLE test_hash_part;
diff --git a/contrib/pageinspect/sql/oldextversions.sql b/contrib/pageinspect/sql/oldextversions.sql
new file mode 100644
index 0000000..9f95349
--- /dev/null
+++ b/contrib/pageinspect/sql/oldextversions.sql
@@ -0,0 +1,26 @@
+-- test old extension version entry points
+
+DROP EXTENSION pageinspect;
+CREATE EXTENSION pageinspect VERSION '1.8';
+
+CREATE TABLE test1 (a int8, b text);
+INSERT INTO test1 VALUES (72057594037927937, 'text');
+CREATE INDEX test1_a_idx ON test1 USING btree (a);
+
+-- from page.sql
+SELECT octet_length(get_raw_page('test1', 0)) AS main_0;
+SELECT octet_length(get_raw_page('test1', 'main', 0)) AS main_0;
+SELECT page_checksum(get_raw_page('test1', 0), 0) IS NOT NULL AS silly_checksum_test;
+
+-- from btree.sql
+SELECT * FROM bt_page_stats('test1_a_idx', 1);
+SELECT * FROM bt_page_items('test1_a_idx', 1);
+
+-- page_header() uses int instead of smallint for lower, upper, special and
+-- pagesize in pageinspect >= 1.10.
+ALTER EXTENSION pageinspect UPDATE TO '1.9';
+\df page_header
+SELECT pagesize, version FROM page_header(get_raw_page('test1', 0));
+
+DROP TABLE test1;
+DROP EXTENSION pageinspect;
diff --git a/contrib/pageinspect/sql/page.sql b/contrib/pageinspect/sql/page.sql
new file mode 100644
index 0000000..5bff568
--- /dev/null
+++ b/contrib/pageinspect/sql/page.sql
@@ -0,0 +1,100 @@
+CREATE EXTENSION pageinspect;
+
+-- Use a temp table so that effects of VACUUM are predictable
+CREATE TEMP TABLE test1 (a int, b int);
+INSERT INTO test1 VALUES (16777217, 131584);
+
+VACUUM (DISABLE_PAGE_SKIPPING) test1; -- set up FSM
+
+-- The page contents can vary, so just test that it can be read
+-- successfully, but don't keep the output.
+
+SELECT octet_length(get_raw_page('test1', 'main', 0)) AS main_0;
+SELECT octet_length(get_raw_page('test1', 'main', 1)) AS main_1;
+
+SELECT octet_length(get_raw_page('test1', 'fsm', 0)) AS fsm_0;
+SELECT octet_length(get_raw_page('test1', 'fsm', 1)) AS fsm_1;
+
+SELECT octet_length(get_raw_page('test1', 'vm', 0)) AS vm_0;
+SELECT octet_length(get_raw_page('test1', 'vm', 1)) AS vm_1;
+
+SELECT octet_length(get_raw_page('test1', 'main', -1));
+SELECT octet_length(get_raw_page('xxx', 'main', 0));
+SELECT octet_length(get_raw_page('test1', 'xxx', 0));
+
+SELECT get_raw_page('test1', 0) = get_raw_page('test1', 'main', 0);
+
+SELECT pagesize, version FROM page_header(get_raw_page('test1', 0));
+
+SELECT page_checksum(get_raw_page('test1', 0), 0) IS NOT NULL AS silly_checksum_test;
+SELECT page_checksum(get_raw_page('test1', 0), -1);
+
+SELECT tuple_data_split('test1'::regclass, t_data, t_infomask, t_infomask2, t_bits)
+ FROM heap_page_items(get_raw_page('test1', 0));
+
+SELECT * FROM fsm_page_contents(get_raw_page('test1', 'fsm', 0));
+
+-- If we freeze the only tuple on test1, the infomask should
+-- always be the same in all test runs.
+VACUUM (FREEZE, DISABLE_PAGE_SKIPPING) test1;
+
+SELECT t_infomask, t_infomask2, raw_flags, combined_flags
+FROM heap_page_items(get_raw_page('test1', 0)),
+ LATERAL heap_tuple_infomask_flags(t_infomask, t_infomask2);
+
+-- tests for decoding of combined flags
+-- HEAP_XMAX_SHR_LOCK = (HEAP_XMAX_EXCL_LOCK | HEAP_XMAX_KEYSHR_LOCK)
+SELECT * FROM heap_tuple_infomask_flags(x'0050'::int, 0);
+-- HEAP_XMIN_FROZEN = (HEAP_XMIN_COMMITTED | HEAP_XMIN_INVALID)
+SELECT * FROM heap_tuple_infomask_flags(x'0300'::int, 0);
+-- HEAP_MOVED = (HEAP_MOVED_IN | HEAP_MOVED_OFF)
+SELECT * FROM heap_tuple_infomask_flags(x'C000'::int, 0);
+SELECT * FROM heap_tuple_infomask_flags(x'C000'::int, 0);
+
+-- test all flags of t_infomask and t_infomask2
+SELECT unnest(raw_flags)
+ FROM heap_tuple_infomask_flags(x'FFFF'::int, x'FFFF'::int) ORDER BY 1;
+SELECT unnest(combined_flags)
+ FROM heap_tuple_infomask_flags(x'FFFF'::int, x'FFFF'::int) ORDER BY 1;
+
+-- no flags at all
+SELECT * FROM heap_tuple_infomask_flags(0, 0);
+-- no combined flags
+SELECT * FROM heap_tuple_infomask_flags(x'0010'::int, 0);
+
+DROP TABLE test1;
+
+-- check that using any of these functions with a partitioned table or index
+-- would fail
+create table test_partitioned (a int) partition by range (a);
+create index test_partitioned_index on test_partitioned (a);
+select get_raw_page('test_partitioned', 0); -- error about partitioned table
+select get_raw_page('test_partitioned_index', 0); -- error about partitioned index
+
+-- a regular table which is a member of a partition set should work though
+create table test_part1 partition of test_partitioned for values from ( 1 ) to (100);
+select get_raw_page('test_part1', 0); -- get farther and error about empty table
+drop table test_partitioned;
+
+-- check null bitmap alignment for table whose number of attributes is multiple of 8
+create table test8 (f1 int, f2 int, f3 int, f4 int, f5 int, f6 int, f7 int, f8 int);
+insert into test8(f1, f8) values (x'7f00007f'::int, 0);
+select t_bits, t_data from heap_page_items(get_raw_page('test8', 0));
+select tuple_data_split('test8'::regclass, t_data, t_infomask, t_infomask2, t_bits)
+ from heap_page_items(get_raw_page('test8', 0));
+drop table test8;
+
+-- Failure with incorrect page size
+-- Suppress the DETAIL message, to allow the tests to work across various
+-- page sizes.
+\set VERBOSITY terse
+SELECT fsm_page_contents('aaa'::bytea);
+SELECT page_checksum('bbb'::bytea, 0);
+SELECT page_header('ccc'::bytea);
+\set VERBOSITY default
+
+-- Tests with all-zero pages.
+SHOW block_size \gset
+SELECT fsm_page_contents(decode(repeat('00', :block_size), 'hex'));
+SELECT page_header(decode(repeat('00', :block_size), 'hex'));
+SELECT page_checksum(decode(repeat('00', :block_size), 'hex'), 1);