v2-0001-GIN-pageinspect-support-for-entry-tree-and-postin.patch
application/octet-stream
Filename: v2-0001-GIN-pageinspect-support-for-entry-tree-and-postin.patch
Type: application/octet-stream
Part: 0
From be8abce940ea621f34a6b1949effad7d93bfa3c2 Mon Sep 17 00:00:00 2001
From: reshke <reshke@double.cloud>
Date: Mon, 13 Oct 2025 20:14:26 +0000
Subject: [PATCH v2] GIN pageinspect support for entry tree and posting tree
non-leaf pages
---
contrib/pageinspect/Makefile | 2 +-
contrib/pageinspect/expected/gin.out | 60 +++-
contrib/pageinspect/ginfuncs.c | 322 +++++++++++++++++-
.../pageinspect/pageinspect--1.13--1.14.sql | 28 ++
contrib/pageinspect/pageinspect.control | 2 +-
contrib/pageinspect/sql/gin.sql | 19 +-
6 files changed, 426 insertions(+), 7 deletions(-)
create mode 100644 contrib/pageinspect/pageinspect--1.13--1.14.sql
diff --git a/contrib/pageinspect/Makefile b/contrib/pageinspect/Makefile
index eae989569d0..09774fd340c 100644
--- a/contrib/pageinspect/Makefile
+++ b/contrib/pageinspect/Makefile
@@ -13,7 +13,7 @@ OBJS = \
rawpage.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 \
diff --git a/contrib/pageinspect/expected/gin.out b/contrib/pageinspect/expected/gin.out
index ff1da6a5a17..d108067c044 100644
--- a/contrib/pageinspect/expected/gin.out
+++ b/contrib/pageinspect/expected/gin.out
@@ -1,6 +1,8 @@
-CREATE TABLE test1 (x int, y int[]);
-INSERT INTO test1 VALUES (1, ARRAY[11, 111]);
+CREATE TABLE test1 (x int, y int[], z text[]);
+INSERT INTO test1 VALUES (1, ARRAY[11, 111], ARRAY['a', 'b', 'c']);
CREATE INDEX test1_y_idx ON test1 USING gin (y) WITH (fastupdate = off);
+CREATE INDEX test2_y_z_idx ON test1 USING gin (y, z) WITH (fastupdate = off);
+CREATE INDEX test3_y_z_idx ON test1 USING gin (y, z) WITH (fastupdate = on);
\x
SELECT * FROM gin_metapage_info(get_raw_page('test1_y_idx', 0));
-[ RECORD 1 ]----+-----------
@@ -27,6 +29,45 @@ 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
+SELECT * FROM gin_entrypage_items(get_raw_page('test1_y_idx', 1), 'test1_y_idx'::regclass);
+-[ RECORD 1 ]--------------
+itemoffset | 1
+downlink | (2147483664,1)
+tids | {"(0,1)"}
+keys | y=11
+-[ RECORD 2 ]--------------
+itemoffset | 2
+downlink | (2147483664,1)
+tids | {"(0,1)"}
+keys | y=111
+
+SELECT * FROM gin_entrypage_items(get_raw_page('test2_y_z_idx', 1), 'test2_y_z_idx'::regclass);
+-[ RECORD 1 ]--------------
+itemoffset | 1
+downlink | (2147483664,1)
+tids | {"(0,1)"}
+keys | y=11
+-[ RECORD 2 ]--------------
+itemoffset | 2
+downlink | (2147483664,1)
+tids | {"(0,1)"}
+keys | y=111
+-[ RECORD 3 ]--------------
+itemoffset | 3
+downlink | (2147483664,1)
+tids | {"(0,1)"}
+keys | z=a
+-[ RECORD 4 ]--------------
+itemoffset | 4
+downlink | (2147483664,1)
+tids | {"(0,1)"}
+keys | z=b
+-[ RECORD 5 ]--------------
+itemoffset | 5
+downlink | (2147483664,1)
+tids | {"(0,1)"}
+keys | z=c
+
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',
@@ -54,6 +95,21 @@ 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
+-- Reject unsuppoerted page types in gin_entrypage_items.
+SELECT * FROM gin_entrypage_items(get_raw_page('test2_y_z_idx', 0), 'test2_y_z_idx'::regclass);
+ERROR: gin_entrypage_items is unsupported for metapage
+-- insert new row to trigger new (fast-list) page allocation.
+INSERT INTO test1 VALUES (1, ARRAY[11, 111], ARRAY['a', 'b', 'c']);
+-- double check that new page is fast-list.
+SELECT * FROM gin_page_opaque_info(get_raw_page('test3_y_z_idx', 2));
+-[ RECORD 1 ]------------------
+rightlink | 3
+maxoff | 120
+flags | {list,list_fullrow}
+
+-- rejrect fast-list pages.
+SELECT * FROM gin_entrypage_items(get_raw_page('test3_y_z_idx', 3), 'test3_y_z_idx'::regclass);
+ERROR: gin_entrypage_items is unsupported for fast list pages
-- Tests with all-zero pages.
SHOW block_size \gset
SELECT gin_leafpage_items(decode(repeat('00', :block_size), 'hex'));
diff --git a/contrib/pageinspect/ginfuncs.c b/contrib/pageinspect/ginfuncs.c
index f6168d8e895..84de7a9f7d5 100644
--- a/contrib/pageinspect/ginfuncs.c
+++ b/contrib/pageinspect/ginfuncs.c
@@ -11,18 +11,24 @@
#include "access/gin_private.h"
#include "access/htup_details.h"
+#include "access/relation.h"
+#include "access/tupdesc.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/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/ruleutils.h"
PG_FUNCTION_INFO_V1(gin_metapage_info);
PG_FUNCTION_INFO_V1(gin_page_opaque_info);
+PG_FUNCTION_INFO_V1(gin_entrypage_items);
PG_FUNCTION_INFO_V1(gin_leafpage_items);
-
+PG_FUNCTION_INFO_V1(gin_datapage_items);
Datum
gin_metapage_info(PG_FUNCTION_ARGS)
@@ -175,6 +181,320 @@ typedef struct gin_leafpage_items_state
GinPostingList *lastseg;
} gin_leafpage_items_state;
+Datum
+gin_entrypage_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;
+ OffsetNumber maxoff, offset;
+ TupleDesc tupdesc;
+ bool oneCol;
+ Page page;
+ GinPageOpaque opaq;
+
+ if (!superuser())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions")));
+
+ maxoff = InvalidOffsetNumber;
+
+ InitMaterializedSRF(fcinfo, 0);
+
+ /* Open the relation */
+ indexRel = index_open(indexRelid, AccessShareLock);
+
+ page = get_page_from_raw(raw_page);
+
+ if (PageIsNew(page))
+ {
+ index_close(indexRel, AccessShareLock);
+ 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);
+
+
+ /* we only support entry tree in this function, check that */
+ if (opaq->flags & GIN_META)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("gin_entrypage_items is unsupported for metapage")));
+
+
+ if (opaq->flags & (GIN_LIST | GIN_LIST_FULLROW))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("gin_entrypage_items is unsupported for fast list pages")));
+
+
+ if (opaq->flags & GIN_DATA)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("input page is not a GIN entry tree page")));
+
+ /* Avoid bogus PageGetMaxOffsetNumber() call with deleted pages */
+ if (GinPageIsDeleted(page))
+ elog(NOTICE, "page is deleted");
+ else
+ maxoff = PageGetMaxOffsetNumber(page);
+ tupdesc = RelationGetDescr(indexRel);
+ oneCol = tupdesc->natts == 1;
+
+ for (offset = FirstOffsetNumber;
+ offset <= maxoff;
+ offset = OffsetNumberNext(offset))
+ {
+ StringInfoData buf;
+ OffsetNumber indAtt;
+ Datum values[4];
+ bool nulls[4];
+ int ndecoded, i;
+ Datum *tids_datum;
+ ItemPointer items_orig;
+ bool free_items_orig;
+ Datum attrVal;
+ Oid foutoid;
+ bool typisvarlena;
+ Oid typoid;
+ char* value;
+ bool nq;
+ char* tmp;
+ bool isnull;
+ IndexTuple idxtuple;
+ ItemId iid = PageGetItemId(page, offset);
+
+ if (!ItemIdIsValid(iid))
+ elog(ERROR, "invalid ItemId");
+ idxtuple = (IndexTuple) PageGetItem(page, iid);
+
+ memset(nulls, 0, sizeof(nulls));
+
+ values[0] = UInt16GetDatum(offset);
+
+ if (oneCol)
+ {
+ indAtt = FirstOffsetNumber;
+ /* here we can safely reuse pg_class's tuple descriptor. */
+ attrVal = index_getattr(idxtuple, FirstOffsetNumber, tupdesc,
+ &isnull);
+ Assert(!isnull);
+ }
+ else
+ {
+ TupleDesc tmpTupdesc;
+ Datum res;
+ Form_pg_attribute attr;
+
+ /* orig tuple reuse is safe */
+
+ res = index_getattr(idxtuple, FirstOffsetNumber, tupdesc,
+ &isnull);
+
+ /* we do not expect null for first attr in multi-column GIN */
+ Assert(!isnull);
+
+ indAtt = DatumGetUInt16(res);
+
+ attr = TupleDescAttr(tupdesc, indAtt - 1);
+
+ tmpTupdesc = CreateTemplateTupleDesc(2);
+
+ TupleDescInitEntry(tmpTupdesc, (AttrNumber) 1, NULL,
+ INT2OID, -1, 0);
+ TupleDescInitEntry(tmpTupdesc, (AttrNumber) 2, NULL,
+ attr->atttypid,
+ attr->atttypmod,
+ attr->attndims);
+ TupleDescInitEntryCollation(tmpTupdesc, (AttrNumber) 2,
+ attr->attcollation);
+
+ attrVal = index_getattr(idxtuple, OffsetNumberNext(FirstOffsetNumber),
+ tmpTupdesc,
+ &isnull);
+ }
+
+ initStringInfo(&buf);
+ appendStringInfo(&buf, "%s=", quote_identifier(TupleDescAttr(tupdesc, indAtt - 1)->attname.data));
+
+ if (!isnull) {
+ /* Most of this is copied from record_out(). */
+ typoid = TupleDescAttr(tupdesc, indAtt - 1)->atttypid;
+ getTypeOutputInfo(typoid, &foutoid, &typisvarlena);
+ value = OidOutputFunctionCall(foutoid, attrVal);
+
+
+ /* 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, '"');
+ }
+ else
+ {
+ appendStringInfo(&buf, "NULL");
+ }
+
+
+ values[3] = CStringGetTextDatum(buf.data);
+
+ if (GinIsPostingTree(idxtuple))
+ {
+ values[1] = ItemPointerGetDatum(&idxtuple->t_tid);
+ nulls[2] = true;
+ }
+ else
+ {
+ values[1] = ItemPointerGetDatum(&idxtuple->t_tid);
+ /* Get list of item pointers from the tuple. */
+ if (GinItupIsCompressed(idxtuple))
+ {
+ items_orig = ginPostingListDecode((GinPostingList *) GinGetPosting(idxtuple), &ndecoded);
+ free_items_orig = true;
+ }
+ else
+ {
+ items_orig = (ItemPointer) GinGetPosting(idxtuple);
+ ndecoded = GinGetNPosting(idxtuple);
+ free_items_orig = false;
+ }
+
+ tids_datum = (Datum *) palloc(ndecoded * sizeof(Datum));
+ for (i = 0; i < ndecoded; i++)
+ tids_datum[i] = ItemPointerGetDatum(&items_orig[i]);
+ values[2] = PointerGetDatum(construct_array_builtin(tids_datum, ndecoded, TIDOID));
+
+ pfree(tids_datum);
+
+ if (free_items_orig)
+ pfree(items_orig);
+ }
+
+ /* Build and return the result tuple. */
+ tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
+ }
+
+ relation_close(indexRel, AccessShareLock);
+
+ return (Datum) 0;
+}
+
+
+Datum
+gin_datapage_items(PG_FUNCTION_ARGS)
+{
+ bytea *raw_page = PG_GETARG_BYTEA_P(0);
+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ OffsetNumber maxoff, offset;
+ Page page;
+ GinPageOpaque opaq;
+
+
+ if (!superuser())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions")));
+
+
+ InitMaterializedSRF(fcinfo, 0);
+ 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);
+
+
+ /* we only support posting tree non-leaf in this function, check that */
+
+ if (opaq->flags & (GIN_META))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("gin_datapage_items is unsupported for metapage")));
+
+ if (opaq->flags & (GIN_LIST | GIN_LIST_FULLROW))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("gin_datapage_items is unsupported for GIN fast update list")));
+
+ if (!(opaq->flags & GIN_DATA))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("input page is not a GIN data tree page")));
+
+ if (opaq->flags & GIN_LEAF)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("input page is a GIN data leaf tree page")));
+
+ maxoff = GinPageGetOpaque(page)->maxoff;
+
+ for (offset = FirstOffsetNumber;
+ offset <= maxoff;
+ offset = OffsetNumberNext(offset))
+ {
+ Datum values[3];
+ bool nulls[3];
+ PostingItem* item = GinDataPageGetPostingItem(page, offset);
+
+ memset(nulls, 0, sizeof(nulls));
+
+
+ values[0] = UInt16GetDatum(offset);
+
+ values[1] = UInt32GetDatum(BlockIdGetBlockNumber(&item->child_blkno));
+ values[2] = ItemPointerGetDatum(&item->key);
+
+ /* Build and return the result tuple. */
+ tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
+ }
+
+ return (Datum) 0;
+}
+
Datum
gin_leafpage_items(PG_FUNCTION_ARGS)
{
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..71bc088ad12
--- /dev/null
+++ b/contrib/pageinspect/pageinspect--1.13--1.14.sql
@@ -0,0 +1,28 @@
+/* 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
+
+--
+-- gin_entrypage_items()
+--
+CREATE FUNCTION gin_entrypage_items(IN page bytea, IN reloid OID,
+ OUT itemoffset smallint,
+ OUT downlink tid,
+ OUT tids tid[],
+ OUT keys text)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'gin_entrypage_items'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+--
+-- gin_datapage_items()
+--
+CREATE FUNCTION gin_datapage_items(IN page bytea, IN reloid OID,
+ OUT itemoffset smallint,
+ OUT downlink int,
+ OUT item_tid tid)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'gin_datapage_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/sql/gin.sql b/contrib/pageinspect/sql/gin.sql
index b57466d7ebf..0848ac41523 100644
--- a/contrib/pageinspect/sql/gin.sql
+++ b/contrib/pageinspect/sql/gin.sql
@@ -1,6 +1,8 @@
-CREATE TABLE test1 (x int, y int[]);
-INSERT INTO test1 VALUES (1, ARRAY[11, 111]);
+CREATE TABLE test1 (x int, y int[], z text[]);
+INSERT INTO test1 VALUES (1, ARRAY[11, 111], ARRAY['a', 'b', 'c']);
CREATE INDEX test1_y_idx ON test1 USING gin (y) WITH (fastupdate = off);
+CREATE INDEX test2_y_z_idx ON test1 USING gin (y, z) WITH (fastupdate = off);
+CREATE INDEX test3_y_z_idx ON test1 USING gin (y, z) WITH (fastupdate = on);
\x
@@ -11,6 +13,10 @@ 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));
+SELECT * FROM gin_entrypage_items(get_raw_page('test1_y_idx', 1), 'test1_y_idx'::regclass);
+
+SELECT * FROM gin_entrypage_items(get_raw_page('test2_y_z_idx', 1), 'test2_y_z_idx'::regclass);
+
INSERT INTO test1 SELECT x, ARRAY[1,10] FROM generate_series(2,10000) x;
SELECT COUNT(*) > 0
@@ -32,6 +38,15 @@ SELECT * FROM gin_page_opaque_info(get_raw_page('test1', 0));
SELECT * FROM gin_leafpage_items(get_raw_page('test1', 0));
\set VERBOSITY default
+-- Reject unsuppoerted page types in gin_entrypage_items.
+SELECT * FROM gin_entrypage_items(get_raw_page('test2_y_z_idx', 0), 'test2_y_z_idx'::regclass);
+-- insert new row to trigger new (fast-list) page allocation.
+INSERT INTO test1 VALUES (1, ARRAY[11, 111], ARRAY['a', 'b', 'c']);
+-- double check that new page is fast-list.
+SELECT * FROM gin_page_opaque_info(get_raw_page('test3_y_z_idx', 2));
+-- rejrect fast-list pages.
+SELECT * FROM gin_entrypage_items(get_raw_page('test3_y_z_idx', 3), 'test3_y_z_idx'::regclass);
+
-- Tests with all-zero pages.
SHOW block_size \gset
SELECT gin_leafpage_items(decode(repeat('00', :block_size), 'hex'));
--
2.43.0