pageinspect support for SpGiST
Hi hackers!
I am currently working for amcheck & pageinspect support for SpGiST indices.
Here is a patch implementing the pageinspect part.
My current design is two functions, which are
* spgist_page_opaque_info - akin to gin/gist opaque info functions.
* spgist_page_items - function that returns information about tuples
on the SpGiST page. Currently this works only for LEAF pages, Internal
pages to be supported.
Following "Desirability -> Design -> Implement -> Test -> Review ->
Commit" developing process, posting v1 patch with PoC and looking for
Desirability/Design feedback.
PFA (WIP) v1.
--
Best regards,
Kirill Reshke
Attachments:
v1-0001-Add-pageinspect-support-for-SpGiST-indexes.patchapplication/octet-stream; name=v1-0001-Add-pageinspect-support-for-SpGiST-indexes.patchDownload
From 513fd8b8957b7d3d066233f3a529aa45b4d29024 Mon Sep 17 00:00:00 2001
From: reshke <reshke@double.cloud>
Date: Sat, 10 Jan 2026 17:29:57 +0000
Subject: [PATCH v1] Add pageinspect support for SpGiST indexes.
---
contrib/pageinspect/Makefile | 1 +
contrib/pageinspect/expected/spgist.out | 44 +++
contrib/pageinspect/meson.build | 3 +
.../pageinspect/pageinspect--1.13--1.14.sql | 32 ++
contrib/pageinspect/pageinspect.control | 2 +-
contrib/pageinspect/spgistfuncs.c | 338 ++++++++++++++++++
contrib/pageinspect/sql/spgist.sql | 27 ++
7 files changed, 446 insertions(+), 1 deletion(-)
create mode 100644 contrib/pageinspect/expected/spgist.out
create mode 100644 contrib/pageinspect/pageinspect--1.13--1.14.sql
create mode 100644 contrib/pageinspect/spgistfuncs.c
create mode 100644 contrib/pageinspect/sql/spgist.sql
diff --git a/contrib/pageinspect/Makefile b/contrib/pageinspect/Makefile
index eae989569d0..7462118fa4d 100644
--- a/contrib/pageinspect/Makefile
+++ b/contrib/pageinspect/Makefile
@@ -32,6 +32,7 @@ REGRESS = \
gin \
gist \
hash \
+ spgist \
oldextversions
ifdef USE_PGXS
diff --git a/contrib/pageinspect/expected/spgist.out b/contrib/pageinspect/expected/spgist.out
new file mode 100644
index 00000000000..339878b5bbb
--- /dev/null
+++ b/contrib/pageinspect/expected/spgist.out
@@ -0,0 +1,44 @@
+-- The gist_page_opaque_info() function prints the page's LSN.
+-- Use an unlogged index, so that the LSN is predictable.
+CREATE UNLOGGED TABLE test_gist AS SELECT point(i,i) p, i::text t FROM
+ generate_series(1,1000) i;
+CREATE INDEX test_spgist_idx ON test_gist USING spgist (p);
+-- Page 0 is the root, the rest are leaf pages
+SELECT * FROM spgist_page_opaque_info(get_raw_page('test_spgist_idx', 0));
+ lsn | nplaceholder | nredirection | flags
+------------+--------------+--------------+--------
+ 0/00000000 | 0 | 0 | {meta}
+(1 row)
+
+SELECT * FROM spgist_page_opaque_info(get_raw_page('test_spgist_idx', 1));
+ lsn | nplaceholder | nredirection | flags
+------------+--------------+--------------+-------
+ 0/00000000 | 0 | 0 | {}
+(1 row)
+
+SELECT * FROM spgist_page_opaque_info(get_raw_page('test_spgist_idx', 2));
+ lsn | nplaceholder | nredirection | flags
+------------+--------------+--------------+--------------
+ 0/00000000 | 0 | 0 | {leaf,nulls}
+(1 row)
+
+-- Suppress the DETAIL message, to allow the tests to work across various
+-- page sizes and architectures.
+\set VERBOSITY terse
+-- Failure with various modes.
+-- invalid page size
+SELECT spgist_page_opaque_info('aaa'::bytea);
+ERROR: invalid page size
+-- invalid special area size
+SELECT * FROM spgist_page_opaque_info(get_raw_page('test_gist', 0));
+ERROR: input page is not a valid SpGiST page
+\set VERBOSITY default
+-- Tests with all-zero pages.
+SHOW block_size \gset
+SELECT spgist_page_opaque_info(decode(repeat('00', :block_size), 'hex'));
+ spgist_page_opaque_info
+-------------------------
+
+(1 row)
+
+DROP TABLE test_gist;
diff --git a/contrib/pageinspect/meson.build b/contrib/pageinspect/meson.build
index c43ea400a4d..13408d0491a 100644
--- a/contrib/pageinspect/meson.build
+++ b/contrib/pageinspect/meson.build
@@ -9,6 +9,7 @@ pageinspect_sources = files(
'hashfuncs.c',
'heapfuncs.c',
'rawpage.c',
+ 'spgistfuncs.c',
)
if host_system == 'windows'
@@ -38,6 +39,7 @@ install_data(
'pageinspect--1.10--1.11.sql',
'pageinspect--1.11--1.12.sql',
'pageinspect--1.12--1.13.sql',
+ 'pageinspect--1.13--1.14.sql',
'pageinspect.control',
kwargs: contrib_data_args,
)
@@ -56,6 +58,7 @@ tests += {
'hash',
'checksum',
'oldextversions',
+ 'spgist',
],
},
}
diff --git a/contrib/pageinspect/pageinspect--1.13--1.14.sql b/contrib/pageinspect/pageinspect--1.13--1.14.sql
new file mode 100644
index 00000000000..581aa308b59
--- /dev/null
+++ b/contrib/pageinspect/pageinspect--1.13--1.14.sql
@@ -0,0 +1,32 @@
+/* contrib/pageinspect/pageinspect--1.14--1.15.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION pageinspect UPDATE TO '1.14'" to load this file. \quit
+
+--
+-- spgist_page_opaque_info()
+--
+CREATE FUNCTION spgist_page_opaque_info(IN page bytea,
+ OUT lsn pg_lsn,
+ OUT nPlaceholder SMALLINT,
+ OUT nRedirection SMALLINT,
+ OUT flags text[])
+AS 'MODULE_PATHNAME', 'spgist_page_opaque_info'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+--
+-- spgist_page_items()
+--
+CREATE FUNCTION spgist_page_items(IN page bytea,
+ IN index_oid regclass,
+ OUT itemoffset smallint,
+ OUT ctid tid,
+ OUT size smallint,
+ OUT hasnullmask boolean,
+ OUT nextoffset smallint,
+ OUT state TEXT,
+ OUT xid xid,
+ OUT keys text)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'spgist_page_items'
+LANGUAGE C STRICT PARALLEL SAFE;
diff --git a/contrib/pageinspect/pageinspect.control b/contrib/pageinspect/pageinspect.control
index cfc87feac03..aee3f598a9e 100644
--- a/contrib/pageinspect/pageinspect.control
+++ b/contrib/pageinspect/pageinspect.control
@@ -1,5 +1,5 @@
# pageinspect extension
comment = 'inspect the contents of database pages at a low level'
-default_version = '1.13'
+default_version = '1.14'
module_pathname = '$libdir/pageinspect'
relocatable = true
diff --git a/contrib/pageinspect/spgistfuncs.c b/contrib/pageinspect/spgistfuncs.c
new file mode 100644
index 00000000000..193a0bdc5dc
--- /dev/null
+++ b/contrib/pageinspect/spgistfuncs.c
@@ -0,0 +1,338 @@
+/*
+ * gistfuncs.c
+ * Functions to investigate the content of GiST indexes
+ *
+ * Copyright (c) 2014-2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pageinspect/gistfuncs.c
+ */
+#include "postgres.h"
+
+#include "access/relation.h"
+#include "access/spgist.h"
+#include "access/spgist_private.h"
+#include "access/htup.h"
+#include "access/htup_details.h"
+#include "catalog/pg_am_d.h"
+#include "funcapi.h"
+#include "miscadmin.h"
+#include "pageinspect.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/lsyscache.h"
+#include "utils/pg_lsn.h"
+#include "utils/rel.h"
+#include "utils/ruleutils.h"
+
+PG_FUNCTION_INFO_V1(spgist_page_opaque_info);
+PG_FUNCTION_INFO_V1(spgist_page_items);
+
+#define IS_SPGIST(r) ((r)->rd_rel->relam == SPGIST_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);
+ SpGistPageOpaque opaq;
+
+ if (PageIsNew(page))
+ return page;
+
+ /* verify the special space has the expected size */
+ if (PageGetSpecialSize(page) != MAXALIGN(sizeof(SpGistPageOpaque)))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("input page is not a valid %s page", "SpGiST"),
+ errdetail("Expected special size %d, got %d.",
+ (int) MAXALIGN(sizeof(SpGistPageOpaque)),
+ (int) PageGetSpecialSize(page))));
+
+ opaq = SpGistPageGetOpaque(page);
+ if (opaq->spgist_page_id != SPGIST_PAGE_ID)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("input page is not a valid %s page", "GiST"),
+ errdetail("Expected %08x, got %08x.",
+ SPGIST_PAGE_ID,
+ opaq->spgist_page_id)));
+
+ return page;
+}
+
+Datum
+spgist_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 = SpGistPageGetOpaque(page)->flags;
+ if (flagbits & SPGIST_META)
+ flags[nflags++] = CStringGetTextDatum("meta");
+ if (flagbits & SPGIST_DELETED)
+ flags[nflags++] = CStringGetTextDatum("deleted");
+ if (flagbits & SPGIST_LEAF)
+ flags[nflags++] = CStringGetTextDatum("leaf");
+ if (flagbits & SPGIST_NULLS)
+ flags[nflags++] = CStringGetTextDatum("nulls");
+ flagbits &= ~(SPGIST_META | SPGIST_DELETED | SPGIST_LEAF | SPGIST_NULLS);
+ 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] = Int16GetDatum(SpGistPageGetOpaque(page)->nPlaceholder);
+ values[2] = Int16GetDatum(SpGistPageGetOpaque(page)->nRedirection);
+ 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
+spgist_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_SPGIST(indexRel))
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is not a %s index",
+ RelationGetRelationName(indexRel), "SpGiST")));
+
+ page = verify_gist_page(raw_page);
+
+ if (PageIsNew(page))
+ {
+ index_close(indexRel, AccessShareLock);
+ PG_RETURN_NULL();
+ }
+
+ flagbits = SpGistPageGetOpaque(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 & SPGIST_LEAF)
+ {
+ tupdesc = RelationGetDescr(indexRel);
+ }
+ else
+ {
+ tupdesc = CreateTupleDescTruncatedCopy(RelationGetDescr(indexRel),
+ IndexRelationGetNumberOfKeyAttributes(indexRel));
+ printflags |= RULE_INDEXDEF_KEYS_ONLY;
+ }
+
+ index_columns = pg_get_indexdef_columns_extended(indexRelid,
+ printflags);
+
+ /* Avoid bogus PageGetMaxOffsetNumber() call with deleted pages */
+ if (SpGistPageIsDeleted(page))
+ elog(NOTICE, "page is deleted");
+ else
+ maxoff = PageGetMaxOffsetNumber(page);
+
+ for (offset = FirstOffsetNumber;
+ offset <= maxoff;
+ offset++)
+ {
+ Datum values[8];
+ bool nulls[8];
+ ItemId id;
+ IndexTuple itup;
+ Datum itup_values[INDEX_MAX_KEYS];
+ bool itup_isnull[INDEX_MAX_KEYS];
+ StringInfoData buf;
+ SpGistLeafTuple leafTuple;
+ int i;
+ bool hasNullsMask;
+ bool has_datums;
+ char *tp;
+ bits8 *bp;
+
+ id = PageGetItemId(page, offset);
+
+ if (!ItemIdIsValid(id))
+ elog(ERROR, "invalid ItemId");
+
+ itup = (IndexTuple) PageGetItem(page, id);
+ leafTuple = (SpGistLeafTuple) itup;
+ hasNullsMask = SGLT_GET_HASNULLMASK(leafTuple);
+
+ tp = (char *) leafTuple + SGLTHDRSZ(hasNullsMask);
+ bp = (bits8 *) ((char *) leafTuple + sizeof(SpGistLeafTupleData));
+ has_datums = false;
+
+ index_deform_tuple_internal(tupdesc,
+ itup_values, itup_isnull,
+ tp, bp, hasNullsMask);
+
+ memset(nulls, 0, sizeof(nulls));
+
+ values[0] = Int16GetDatum(offset);
+ values[1] = ItemPointerGetDatum(&leafTuple->heapPtr);
+ values[2] = Int32GetDatum(leafTuple->size);
+ values[3] = BoolGetDatum(hasNullsMask);
+ values[4] = Int16GetDatum(SGLT_GET_NEXTOFFSET(leafTuple));
+
+ values[6] = InvalidTransactionId;
+
+ switch (leafTuple->tupstate)
+ {
+ case SPGIST_LIVE:
+ values[5] = CStringGetTextDatum("LIVE");
+ has_datums = true;
+ break;
+ case SPGIST_REDIRECT:
+ values[5] = CStringGetTextDatum("REDIRECT");
+ values[6] = ((SpGistDeadTuple) leafTuple)->xid;
+ break;
+ case SPGIST_DEAD:
+ values[5] = CStringGetTextDatum("DEAD");
+ values[6] = ((SpGistDeadTuple) leafTuple)->xid;
+ break;
+ case SPGIST_PLACEHOLDER:
+ values[5] = CStringGetTextDatum("PLACEHOLDER");
+ values[6] = ((SpGistDeadTuple) leafTuple)->xid;
+ break;
+ default:
+ ereport(ERROR, errcode(ERRCODE_INDEX_CORRUPTED), errmsg("malformed SpGist leaf tuple state %d", leafTuple->tupstate));
+ }
+
+ if (has_datums && 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 = TupleDescAttr(tupdesc, 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[7] = CStringGetTextDatum(buf.data);
+ }
+ else
+ {
+ values[7] = (Datum) 0;
+ nulls[7] = true;
+ }
+
+ tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
+ }
+
+ index_close(indexRel, AccessShareLock);
+
+ return (Datum) 0;
+}
diff --git a/contrib/pageinspect/sql/spgist.sql b/contrib/pageinspect/sql/spgist.sql
new file mode 100644
index 00000000000..780b820989a
--- /dev/null
+++ b/contrib/pageinspect/sql/spgist.sql
@@ -0,0 +1,27 @@
+-- The gist_page_opaque_info() function prints the page's LSN.
+-- Use an unlogged index, so that the LSN is predictable.
+CREATE UNLOGGED TABLE test_gist AS SELECT point(i,i) p, i::text t FROM
+ generate_series(1,1000) i;
+CREATE INDEX test_spgist_idx ON test_gist USING spgist (p);
+
+-- Page 0 is the root, the rest are leaf pages
+SELECT * FROM spgist_page_opaque_info(get_raw_page('test_spgist_idx', 0));
+SELECT * FROM spgist_page_opaque_info(get_raw_page('test_spgist_idx', 1));
+SELECT * FROM spgist_page_opaque_info(get_raw_page('test_spgist_idx', 2));
+
+-- Suppress the DETAIL message, to allow the tests to work across various
+-- page sizes and architectures.
+\set VERBOSITY terse
+
+-- Failure with various modes.
+-- invalid page size
+SELECT spgist_page_opaque_info('aaa'::bytea);
+-- invalid special area size
+SELECT * FROM spgist_page_opaque_info(get_raw_page('test_gist', 0));
+\set VERBOSITY default
+
+-- Tests with all-zero pages.
+SHOW block_size \gset
+SELECT spgist_page_opaque_info(decode(repeat('00', :block_size), 'hex'));
+
+DROP TABLE test_gist;
--
2.43.0
On Sat, 10 Jan 2026 at 23:05, I wrote:
Currently this works only for LEAF pages, Internal pages to be supported.
I have added the spgist_innerpage_items function to address that.
spgist_page_items renamed to spgist_leafpage_items
PFA v2
--
Best regards,
Kirill Reshke
Attachments:
v2-0001-Add-pageinspect-support-for-SpGiST-indexes.patchapplication/octet-stream; name=v2-0001-Add-pageinspect-support-for-SpGiST-indexes.patchDownload
From c251ce2bb79b3d242a3c76b9fa5477bda1c06562 Mon Sep 17 00:00:00 2001
From: reshke <reshke@double.cloud>
Date: Sat, 10 Jan 2026 17:29:57 +0000
Subject: [PATCH v2] Add pageinspect support for SpGiST indexes.
---
contrib/pageinspect/Makefile | 6 +-
contrib/pageinspect/expected/spgist.out | 44 ++
contrib/pageinspect/meson.build | 3 +
.../pageinspect/pageinspect--1.13--1.14.sql | 60 ++
contrib/pageinspect/pageinspect.control | 2 +-
contrib/pageinspect/spgistfuncs.c | 526 ++++++++++++++++++
contrib/pageinspect/sql/spgist.sql | 27 +
7 files changed, 665 insertions(+), 3 deletions(-)
create mode 100644 contrib/pageinspect/expected/spgist.out
create mode 100644 contrib/pageinspect/pageinspect--1.13--1.14.sql
create mode 100644 contrib/pageinspect/spgistfuncs.c
create mode 100644 contrib/pageinspect/sql/spgist.sql
diff --git a/contrib/pageinspect/Makefile b/contrib/pageinspect/Makefile
index eae989569d0..c565c0d9b23 100644
--- a/contrib/pageinspect/Makefile
+++ b/contrib/pageinspect/Makefile
@@ -10,10 +10,11 @@ OBJS = \
gistfuncs.o \
hashfuncs.o \
heapfuncs.o \
- rawpage.o
+ rawpage.o \
+ spgistfuncs.o
EXTENSION = pageinspect
-DATA = pageinspect--1.12--1.13.sql \
+DATA = pageinspect--1.13--1.14.sql pageinspect--1.12--1.13.sql \
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 \
@@ -32,6 +33,7 @@ REGRESS = \
gin \
gist \
hash \
+ spgist \
oldextversions
ifdef USE_PGXS
diff --git a/contrib/pageinspect/expected/spgist.out b/contrib/pageinspect/expected/spgist.out
new file mode 100644
index 00000000000..339878b5bbb
--- /dev/null
+++ b/contrib/pageinspect/expected/spgist.out
@@ -0,0 +1,44 @@
+-- The gist_page_opaque_info() function prints the page's LSN.
+-- Use an unlogged index, so that the LSN is predictable.
+CREATE UNLOGGED TABLE test_gist AS SELECT point(i,i) p, i::text t FROM
+ generate_series(1,1000) i;
+CREATE INDEX test_spgist_idx ON test_gist USING spgist (p);
+-- Page 0 is the root, the rest are leaf pages
+SELECT * FROM spgist_page_opaque_info(get_raw_page('test_spgist_idx', 0));
+ lsn | nplaceholder | nredirection | flags
+------------+--------------+--------------+--------
+ 0/00000000 | 0 | 0 | {meta}
+(1 row)
+
+SELECT * FROM spgist_page_opaque_info(get_raw_page('test_spgist_idx', 1));
+ lsn | nplaceholder | nredirection | flags
+------------+--------------+--------------+-------
+ 0/00000000 | 0 | 0 | {}
+(1 row)
+
+SELECT * FROM spgist_page_opaque_info(get_raw_page('test_spgist_idx', 2));
+ lsn | nplaceholder | nredirection | flags
+------------+--------------+--------------+--------------
+ 0/00000000 | 0 | 0 | {leaf,nulls}
+(1 row)
+
+-- Suppress the DETAIL message, to allow the tests to work across various
+-- page sizes and architectures.
+\set VERBOSITY terse
+-- Failure with various modes.
+-- invalid page size
+SELECT spgist_page_opaque_info('aaa'::bytea);
+ERROR: invalid page size
+-- invalid special area size
+SELECT * FROM spgist_page_opaque_info(get_raw_page('test_gist', 0));
+ERROR: input page is not a valid SpGiST page
+\set VERBOSITY default
+-- Tests with all-zero pages.
+SHOW block_size \gset
+SELECT spgist_page_opaque_info(decode(repeat('00', :block_size), 'hex'));
+ spgist_page_opaque_info
+-------------------------
+
+(1 row)
+
+DROP TABLE test_gist;
diff --git a/contrib/pageinspect/meson.build b/contrib/pageinspect/meson.build
index c43ea400a4d..13408d0491a 100644
--- a/contrib/pageinspect/meson.build
+++ b/contrib/pageinspect/meson.build
@@ -9,6 +9,7 @@ pageinspect_sources = files(
'hashfuncs.c',
'heapfuncs.c',
'rawpage.c',
+ 'spgistfuncs.c',
)
if host_system == 'windows'
@@ -38,6 +39,7 @@ install_data(
'pageinspect--1.10--1.11.sql',
'pageinspect--1.11--1.12.sql',
'pageinspect--1.12--1.13.sql',
+ 'pageinspect--1.13--1.14.sql',
'pageinspect.control',
kwargs: contrib_data_args,
)
@@ -56,6 +58,7 @@ tests += {
'hash',
'checksum',
'oldextversions',
+ 'spgist',
],
},
}
diff --git a/contrib/pageinspect/pageinspect--1.13--1.14.sql b/contrib/pageinspect/pageinspect--1.13--1.14.sql
new file mode 100644
index 00000000000..c8b42e467a1
--- /dev/null
+++ b/contrib/pageinspect/pageinspect--1.13--1.14.sql
@@ -0,0 +1,60 @@
+/* contrib/pageinspect/pageinspect--1.13--1.14.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION pageinspect UPDATE TO '1.14'" to load this file. \quit
+
+--
+-- spgist_page_opaque_info()
+--
+CREATE FUNCTION spgist_page_opaque_info(IN page bytea,
+ OUT lsn pg_lsn,
+ OUT nPlaceholder SMALLINT,
+ OUT nRedirection SMALLINT,
+ OUT flags text[])
+AS 'MODULE_PATHNAME', 'spgist_page_opaque_info'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+--
+-- spgist_leafpage_items()
+--
+CREATE FUNCTION spgist_leafpage_items(IN page bytea,
+ IN index_oid regclass,
+ OUT itemoffset smallint,
+ OUT ctid tid,
+ OUT size smallint,
+ OUT hasnullmask boolean,
+ OUT nextoffset smallint,
+ OUT state TEXT,
+ OUT xid xid,
+ OUT keys text)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'spgist_leafpage_items'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+
+--
+-- spgist_innerpage_items()
+--
+CREATE FUNCTION spgist_innerpage_items(IN page bytea,
+ IN index_oid regclass,
+ OUT itemoffset smallint,
+ OUT allTheSame INT,
+ OUT nNodes INT,
+ OUT prefixSize INT,
+ OUT size smallint,
+ OUT state TEXT)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'spgist_innerpage_items'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+
+--
+-- spgist_metapage_items()
+--
+CREATE FUNCTION spgist_metapage_items(IN page bytea,
+ OUT itemoffset int,
+ OUT blkno smallint,
+ OUT freespace int)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'spgist_metapage_items'
+LANGUAGE C STRICT PARALLEL SAFE;
diff --git a/contrib/pageinspect/pageinspect.control b/contrib/pageinspect/pageinspect.control
index cfc87feac03..aee3f598a9e 100644
--- a/contrib/pageinspect/pageinspect.control
+++ b/contrib/pageinspect/pageinspect.control
@@ -1,5 +1,5 @@
# pageinspect extension
comment = 'inspect the contents of database pages at a low level'
-default_version = '1.13'
+default_version = '1.14'
module_pathname = '$libdir/pageinspect'
relocatable = true
diff --git a/contrib/pageinspect/spgistfuncs.c b/contrib/pageinspect/spgistfuncs.c
new file mode 100644
index 00000000000..175b0ee2ec9
--- /dev/null
+++ b/contrib/pageinspect/spgistfuncs.c
@@ -0,0 +1,526 @@
+/*
+ * gistfuncs.c
+ * Functions to investigate the content of GiST indexes
+ *
+ * Copyright (c) 2014-2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * contrib/pageinspect/gistfuncs.c
+ */
+#include "postgres.h"
+
+#include "access/relation.h"
+#include "access/spgist.h"
+#include "access/spgist_private.h"
+#include "access/htup.h"
+#include "access/htup_details.h"
+#include "catalog/pg_am_d.h"
+#include "funcapi.h"
+#include "miscadmin.h"
+#include "pageinspect.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/lsyscache.h"
+#include "utils/pg_lsn.h"
+#include "utils/rel.h"
+#include "utils/ruleutils.h"
+
+PG_FUNCTION_INFO_V1(spgist_page_opaque_info);
+PG_FUNCTION_INFO_V1(spgist_leafpage_items);
+PG_FUNCTION_INFO_V1(spgist_innerpage_items);
+PG_FUNCTION_INFO_V1(spgist_metapage_items);
+
+#define IS_SPGIST(r) ((r)->rd_rel->relam == SPGIST_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);
+ SpGistPageOpaque opaq;
+
+ if (PageIsNew(page))
+ return page;
+
+ /* verify the special space has the expected size */
+ if (PageGetSpecialSize(page) != MAXALIGN(sizeof(SpGistPageOpaque)))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("input page is not a valid %s page", "SpGiST"),
+ errdetail("Expected special size %d, got %d.",
+ (int) MAXALIGN(sizeof(SpGistPageOpaque)),
+ (int) PageGetSpecialSize(page))));
+
+ opaq = SpGistPageGetOpaque(page);
+ if (opaq->spgist_page_id != SPGIST_PAGE_ID)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("input page is not a valid %s page", "GiST"),
+ errdetail("Expected %08x, got %08x.",
+ SPGIST_PAGE_ID,
+ opaq->spgist_page_id)));
+
+ return page;
+}
+
+Datum
+spgist_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 = SpGistPageGetOpaque(page)->flags;
+ if (flagbits & SPGIST_META)
+ flags[nflags++] = CStringGetTextDatum("meta");
+ if (flagbits & SPGIST_DELETED)
+ flags[nflags++] = CStringGetTextDatum("deleted");
+ if (flagbits & SPGIST_LEAF)
+ flags[nflags++] = CStringGetTextDatum("leaf");
+ if (flagbits & SPGIST_NULLS)
+ flags[nflags++] = CStringGetTextDatum("nulls");
+ flagbits &= ~(SPGIST_META | SPGIST_DELETED | SPGIST_LEAF | SPGIST_NULLS);
+ 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] = Int16GetDatum(SpGistPageGetOpaque(page)->nPlaceholder);
+ values[2] = Int16GetDatum(SpGistPageGetOpaque(page)->nRedirection);
+ 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
+spgist_metapage_items(PG_FUNCTION_ARGS)
+{
+ bytea *raw_page = PG_GETARG_BYTEA_P(0);
+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ Page metapage;
+ uint16 flagbits;
+ SpGistMetaPageData *metadata;
+
+ if (!superuser())
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions"));
+
+ InitMaterializedSRF(fcinfo, 0);
+
+
+ metapage = verify_gist_page(raw_page);
+
+ flagbits = SpGistPageGetOpaque(metapage)->flags;
+
+ if (!(flagbits & SPGIST_META))
+ ereport(ERROR,
+ errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("input page is not a %s meta page", "SpGiST"));
+
+ metadata = SpGistPageGetMeta(metapage);
+
+ if (metadata->magicNumber != SPGIST_MAGIC_NUMBER)
+ ereport(ERROR,
+ errcode(ERRCODE_INDEX_CORRUPTED),
+ errmsg("input page is not a valid SpGiST metapage"),
+ errdetail("Expected special size %d, got %d.",
+ (int) SPGIST_MAGIC_NUMBER,
+ (int) metadata->magicNumber));
+
+ for (int i = 0; i < SPGIST_CACHED_PAGES; i++)
+ {
+ Datum values[3];
+ bool nulls[3];
+
+ memset(nulls, 0, sizeof(nulls));
+
+ values[0] = Int32GetDatum(i);
+ values[1] = Int16GetDatum(metadata->lastUsedPages.cachedPage[i].blkno);
+ values[2] = Int32GetDatum(metadata->lastUsedPages.cachedPage[i].freeSpace);
+
+ tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
+ }
+
+ return (Datum) 0;
+}
+
+Datum
+spgist_leafpage_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 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 index relation */
+ indexRel = index_open(indexRelid, AccessShareLock);
+
+ if (!IS_SPGIST(indexRel))
+ ereport(ERROR,
+ errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is not a %s index",
+ RelationGetRelationName(indexRel), "SpGiST"));
+
+ page = verify_gist_page(raw_page);
+
+ if (PageIsNew(page))
+ {
+ index_close(indexRel, AccessShareLock);
+ PG_RETURN_NULL();
+ }
+
+ flagbits = SpGistPageGetOpaque(page)->flags;
+
+ if (flagbits & SPGIST_META)
+ ereport(ERROR,
+ errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("input page is not a %s leaf page", "SpGiST"),
+ errhint("this appears to be %s metapage. Please use spgist_metapage_items.", "SpGiST"));
+
+ /*
+ * 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 & SPGIST_LEAF)
+ tupdesc = RelationGetDescr(indexRel);
+ else
+ ereport(ERROR,
+ errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("input page is not a %s leaf page", "SpGiST"),
+ errhint("this appears to be %s inner page. Please use spgist_innerpage_items.", "SpGiST"));
+
+ index_columns = pg_get_indexdef_columns_extended(indexRelid,
+ printflags);
+
+ /* Avoid bogus PageGetMaxOffsetNumber() call with deleted pages */
+ if (SpGistPageIsDeleted(page))
+ elog(NOTICE, "page is deleted");
+ else
+ maxoff = PageGetMaxOffsetNumber(page);
+
+ for (OffsetNumber offset = FirstOffsetNumber;
+ offset <= maxoff;
+ offset++)
+ {
+ Datum values[8];
+ bool nulls[8];
+ ItemId id;
+ IndexTuple itup;
+ Datum itup_values[INDEX_MAX_KEYS];
+ bool itup_isnull[INDEX_MAX_KEYS];
+ SpGistLeafTuple leafTuple;
+ int i;
+ bool hasNullsMask;
+ bool has_datums;
+ char *tp;
+ bits8 *bp;
+
+ id = PageGetItemId(page, offset);
+
+ if (!ItemIdIsValid(id))
+ elog(ERROR, "invalid ItemId");
+
+ itup = (IndexTuple) PageGetItem(page, id);
+ leafTuple = (SpGistLeafTuple) itup;
+ hasNullsMask = SGLT_GET_HASNULLMASK(leafTuple);
+
+ tp = (char *) leafTuple + SGLTHDRSZ(hasNullsMask);
+ bp = (bits8 *) ((char *) leafTuple + sizeof(SpGistLeafTupleData));
+ has_datums = false;
+
+ index_deform_tuple_internal(tupdesc,
+ itup_values, itup_isnull,
+ tp, bp, hasNullsMask);
+
+ memset(nulls, 0, sizeof(nulls));
+
+ values[0] = UInt16GetDatum(offset);
+ values[1] = ItemPointerGetDatum(&leafTuple->heapPtr);
+ values[2] = UInt32GetDatum(leafTuple->size);
+ values[3] = BoolGetDatum(hasNullsMask);
+ values[4] = UInt16GetDatum(SGLT_GET_NEXTOFFSET(leafTuple));
+
+ values[6] = InvalidTransactionId;
+
+ switch (leafTuple->tupstate)
+ {
+ case SPGIST_LIVE:
+ values[5] = CStringGetTextDatum("LIVE");
+ has_datums = true;
+ break;
+ case SPGIST_REDIRECT:
+ values[5] = CStringGetTextDatum("REDIRECT");
+ values[6] = ((SpGistDeadTuple) leafTuple)->xid;
+ break;
+ case SPGIST_DEAD:
+ values[5] = CStringGetTextDatum("DEAD");
+ values[6] = ((SpGistDeadTuple) leafTuple)->xid;
+ break;
+ case SPGIST_PLACEHOLDER:
+ values[5] = CStringGetTextDatum("PLACEHOLDER");
+ values[6] = ((SpGistDeadTuple) leafTuple)->xid;
+ break;
+ default:
+ ereport(ERROR, errcode(ERRCODE_INDEX_CORRUPTED), errmsg("malformed SpGist leaf tuple state %d", leafTuple->tupstate));
+ }
+
+ if (has_datums && index_columns)
+ {
+ StringInfoData buf;
+
+ 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 = TupleDescAttr(tupdesc, 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[7] = CStringGetTextDatum(buf.data);
+ }
+ else
+ {
+ values[7] = (Datum) 0;
+ nulls[7] = true;
+ }
+
+ tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
+ }
+
+ index_close(indexRel, AccessShareLock);
+
+ return (Datum) 0;
+}
+
+
+Datum
+spgist_innerpage_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 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 index relation */
+ indexRel = index_open(indexRelid, AccessShareLock);
+
+ if (!IS_SPGIST(indexRel))
+ ereport(ERROR,
+ errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is not a %s index",
+ RelationGetRelationName(indexRel), "SpGiST"));
+
+ page = verify_gist_page(raw_page);
+
+ if (PageIsNew(page))
+ {
+ index_close(indexRel, AccessShareLock);
+ PG_RETURN_NULL();
+ }
+
+ flagbits = SpGistPageGetOpaque(page)->flags;
+
+ if (flagbits & SPGIST_META)
+ ereport(ERROR,
+ errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("input page is not a %s inner page", "SpGiST"),
+ errhint("this appears to be %s metapage. Please use spgist_metapage_items.", "SpGiST"));
+
+ /*
+ * 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 & SPGIST_LEAF)
+ ereport(ERROR,
+ errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("input page is not a %s inner page", "SpGiST"),
+ errhint("this appears to be %s leaf page. Please use spgist_leafpage_items.", "SpGiST"));
+
+ tupdesc = CreateTupleDescTruncatedCopy(RelationGetDescr(indexRel),
+ IndexRelationGetNumberOfKeyAttributes(indexRel));
+ printflags |= RULE_INDEXDEF_KEYS_ONLY;
+
+ index_columns = pg_get_indexdef_columns_extended(indexRelid,
+ printflags);
+
+ /* Avoid bogus PageGetMaxOffsetNumber() call with deleted pages */
+ if (SpGistPageIsDeleted(page))
+ elog(NOTICE, "page is deleted");
+ else
+ maxoff = PageGetMaxOffsetNumber(page);
+
+ for (OffsetNumber offset = FirstOffsetNumber;
+ offset <= maxoff;
+ offset++)
+ {
+ Datum values[8];
+ bool nulls[8];
+ ItemId id;
+ IndexTuple itup;
+ Datum itup_values[INDEX_MAX_KEYS];
+ bool itup_isnull[INDEX_MAX_KEYS];
+ SpGistInnerTuple innerTuple;
+ int i;
+ bool hasNullsMask;
+ bool has_datums;
+ char *tp;
+ bits8 *bp;
+
+ id = PageGetItemId(page, offset);
+
+ if (!ItemIdIsValid(id))
+ elog(ERROR, "invalid ItemId");
+
+ itup = (IndexTuple) PageGetItem(page, id);
+ innerTuple = (SpGistInnerTuple) itup;
+
+ memset(nulls, 0, sizeof(nulls));
+
+ values[0] = UInt16GetDatum(offset);
+ values[1] = UInt32GetDatum(innerTuple->allTheSame);
+ values[2] = UInt32GetDatum(innerTuple->nNodes);
+ values[3] = UInt32GetDatum(innerTuple->prefixSize);
+ values[4] = UInt16GetDatum(innerTuple->size);
+
+ switch (innerTuple->tupstate)
+ {
+ case SPGIST_LIVE:
+ values[5] = CStringGetTextDatum("LIVE");
+ has_datums = true;
+ break;
+ case SPGIST_REDIRECT:
+ values[5] = CStringGetTextDatum("REDIRECT");
+ break;
+ case SPGIST_DEAD:
+ values[5] = CStringGetTextDatum("DEAD");
+ break;
+ case SPGIST_PLACEHOLDER:
+ values[5] = CStringGetTextDatum("PLACEHOLDER");
+ break;
+ default:
+ ereport(ERROR, errcode(ERRCODE_INDEX_CORRUPTED),
+ errmsg("malformed SpGist leaf tuple state %d", innerTuple->tupstate));
+ }
+
+ tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
+ }
+
+ index_close(indexRel, AccessShareLock);
+
+ return (Datum) 0;
+}
diff --git a/contrib/pageinspect/sql/spgist.sql b/contrib/pageinspect/sql/spgist.sql
new file mode 100644
index 00000000000..780b820989a
--- /dev/null
+++ b/contrib/pageinspect/sql/spgist.sql
@@ -0,0 +1,27 @@
+-- The gist_page_opaque_info() function prints the page's LSN.
+-- Use an unlogged index, so that the LSN is predictable.
+CREATE UNLOGGED TABLE test_gist AS SELECT point(i,i) p, i::text t FROM
+ generate_series(1,1000) i;
+CREATE INDEX test_spgist_idx ON test_gist USING spgist (p);
+
+-- Page 0 is the root, the rest are leaf pages
+SELECT * FROM spgist_page_opaque_info(get_raw_page('test_spgist_idx', 0));
+SELECT * FROM spgist_page_opaque_info(get_raw_page('test_spgist_idx', 1));
+SELECT * FROM spgist_page_opaque_info(get_raw_page('test_spgist_idx', 2));
+
+-- Suppress the DETAIL message, to allow the tests to work across various
+-- page sizes and architectures.
+\set VERBOSITY terse
+
+-- Failure with various modes.
+-- invalid page size
+SELECT spgist_page_opaque_info('aaa'::bytea);
+-- invalid special area size
+SELECT * FROM spgist_page_opaque_info(get_raw_page('test_gist', 0));
+\set VERBOSITY default
+
+-- Tests with all-zero pages.
+SHOW block_size \gset
+SELECT spgist_page_opaque_info(decode(repeat('00', :block_size), 'hex'));
+
+DROP TABLE test_gist;
--
2.43.0