GIN pageinspect support for entry tree and posting tree
Hi!
I do frequently interact with gin indexes. I also frequently use
pageinspect to check how data is stored in indixies. The latter is
beneficial in both education and corruption fixing purposes.
I was always wondering on why GIN pageinspect module lacks support for
anything rather that Posting Tree leaf pages. So, I implemented
extended GIN support.
Patch includes two functions, gin_entrypage_items and gin_datapage_items.
gin_entrypage_items is a function to work with entry tree. It supports
both entry tree leaf and internal pages.
GIN entry tree stores an index tuple with a single indexed column in
single-index-column case.
Otherwise, each tuple on GIN entry tree page is tuple of (attrnum,
attrvalue), where the first item in tuple shows which indexes column
this tuple refers to. GIN internal pages also contains downlink to
other entry tree pages, while entry tree leaf page may contain
compressed TIDs of the posting list, or downlink to posting tree.
example of output on entry tree internal page of multi-column index:
```
reshke=# select * from gin_entrypage_items(get_raw_page('x_i_j_idx',
1), 'x_i_j_idx'::regclass);
itemoffset | downlink | tids | keys
------------+----------+------+------------------------------------
1 | (3,0) | {} | i=113
2 | (5,0) | {} | j=34173cb38f07f89ddbebc2ac9128303f
3 | (2,0) | {} | j=a0a080f42e6f13b3a2df133f073095dd
4 | (4,0) | {} | j=fc490ca45c00b1249bbe3554a4fdf6fb
(4 rows)
```
example of output on entry tree leaf page of multi-column index:
```
reshke=# select * from gin_entrypage_items(get_raw_page('x_i_j_idx',
2), 'x_i_j_idx'::regclass);
itemoffset | downlink | tids |
keys
------------+----------------+------------------------------+------------------------------------
1 | (2147483696,3) | {"(1,39)","(1,40)","(2,1)"} |
j=35f4a8d465e6e1edc05f3d8ab658c551
2 | (2147483696,3) | {"(4,10)","(4,11)","(4,12)"} |
j=3636638817772e42b59d74cff571fbb3
3 | (2147483696,3) | {"(5,1)","(5,2)","(5,3)"} |
j=3644a684f98ea8fe223c713b77189a77
4 | (2147483696,3) | {"(0,25)","(0,26)","(0,27)"} |
j=37693cfc748049e45d87b8c7d8b9aacd
5 | (2147483696,3) | {"(3,33)","(3,34)","(3,35)"} |
j=37a749d808e46495a8da1e5352d03cae
```
downlink on the leaf page has a different meaning than on the internal
page, but I didn't handle it any differently. In the example ouput,
(2147483696,3) = ((1<<31) + 48, 3), meaning the next 48 bytes is the
entry tree key, and after that there is 3 posting items,
varbyte-encoded.
I also tested this function on GIN index with nulls, it works. Let me
know if I'm wrong, I know that GIN handles NULLS very differently, but
I had trouble with it, which makes me think that I'm missing
something.
Also turns out this gin_entrypage_items actually works for fast lists
(GIN_LIST and GIN_LIST_FULLROW pages). But output is something
strange, I cannot validate is output is sane. For tids, I get values
like
```
1 | (1,19) |
{"(16782080,4096)","(16777216,14336)","(1280,0)","(469774336,0)","(65536,0)","(2734686208,57344)","(8389120,0)","(3372220424,65280)","(4294967295,65535)","(16711680,0)","(0,0)","(
33554688,0)","(614,0)","(67108864,1024)","(16777216,0)","(73793536,0)","(0,0)","(0,0)","(0,0)"}
```
gin_datapage_items is for posting trees, but not leaf pages. For leaf
pages, users are expected to still use the gin_leafpage_items
function.
Example output for gin_datapage_items:
```
reshke=# select * from gin_datapage_items(get_raw_page('x_i_j_idx',
43), 'x_i_j_idx'::regclass);
itemoffset | downlink | item_tid
------------+----------+----------
1 | 124 | (162,12)
2 | 123 | (314,37)
3 | 251 | (467,23)
4 | 373 | (0,0)
(4 rows)
```
Patch still is very raw, many things to improve.
Comments?
--
Best regards,
Kirill Reshke
Attachments:
v1-0001-GIN-pageinspect-support-for-entry-tree-and-postin.patchapplication/octet-stream; name=v1-0001-GIN-pageinspect-support-for-entry-tree-and-postin.patchDownload
From fec2708403f655260a14f148b6ff24c220219d0e Mon Sep 17 00:00:00 2001
From: reshke <reshke@double.cloud>
Date: Mon, 13 Oct 2025 20:14:26 +0000
Subject: [PATCH v1] GIN pageinspect support for entry tree and posting tree
non-leaf pages
---
contrib/pageinspect/Makefile | 2 +-
contrib/pageinspect/expected/gin.out | 44 ++-
contrib/pageinspect/ginfuncs.c | 310 +++++++++++++++++-
.../pageinspect/pageinspect--1.13--1.14.sql | 28 ++
contrib/pageinspect/pageinspect.control | 2 +-
contrib/pageinspect/sql/gin.sql | 9 +-
6 files changed, 388 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 9dee7653310..063c38b462c 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..03975d7854c 100644
--- a/contrib/pageinspect/expected/gin.out
+++ b/contrib/pageinspect/expected/gin.out
@@ -1,6 +1,7 @@
-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);
\x
SELECT * FROM gin_metapage_info(get_raw_page('test1_y_idx', 0));
-[ RECORD 1 ]----+-----------
@@ -27,6 +28,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',
diff --git a/contrib/pageinspect/ginfuncs.c b/contrib/pageinspect/ginfuncs.c
index 09a90957081..1f1cb6f2264 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,308 @@ 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_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..9d83875e18f 100644
--- a/contrib/pageinspect/sql/gin.sql
+++ b/contrib/pageinspect/sql/gin.sql
@@ -1,6 +1,7 @@
-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);
\x
@@ -11,6 +12,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
--
2.43.0
On Tue, 14 Oct 2025 at 01:43, Kirill Reshke <reshkekirill@gmail.com> wrote:
Hi!
I do frequently interact with gin indexes. I also frequently use
pageinspect to check how data is stored in indixies. The latter is
beneficial in both education and corruption fixing purposes.I was always wondering on why GIN pageinspect module lacks support for
anything rather that Posting Tree leaf pages. So, I implemented
extended GIN support.Patch includes two functions, gin_entrypage_items and gin_datapage_items.
gin_entrypage_items is a function to work with entry tree. It supports
both entry tree leaf and internal pages.GIN entry tree stores an index tuple with a single indexed column in
single-index-column case.
Otherwise, each tuple on GIN entry tree page is tuple of (attrnum,
attrvalue), where the first item in tuple shows which indexes column
this tuple refers to. GIN internal pages also contains downlink to
other entry tree pages, while entry tree leaf page may contain
compressed TIDs of the posting list, or downlink to posting tree.example of output on entry tree internal page of multi-column index:
```
reshke=# select * from gin_entrypage_items(get_raw_page('x_i_j_idx',
1), 'x_i_j_idx'::regclass);
itemoffset | downlink | tids | keys
------------+----------+------+------------------------------------
1 | (3,0) | {} | i=113
2 | (5,0) | {} | j=34173cb38f07f89ddbebc2ac9128303f
3 | (2,0) | {} | j=a0a080f42e6f13b3a2df133f073095dd
4 | (4,0) | {} | j=fc490ca45c00b1249bbe3554a4fdf6fb
(4 rows)```
example of output on entry tree leaf page of multi-column index:
```
reshke=# select * from gin_entrypage_items(get_raw_page('x_i_j_idx',
2), 'x_i_j_idx'::regclass);
itemoffset | downlink | tids |
keys
------------+----------------+------------------------------+------------------------------------
1 | (2147483696,3) | {"(1,39)","(1,40)","(2,1)"} |
j=35f4a8d465e6e1edc05f3d8ab658c551
2 | (2147483696,3) | {"(4,10)","(4,11)","(4,12)"} |
j=3636638817772e42b59d74cff571fbb3
3 | (2147483696,3) | {"(5,1)","(5,2)","(5,3)"} |
j=3644a684f98ea8fe223c713b77189a77
4 | (2147483696,3) | {"(0,25)","(0,26)","(0,27)"} |
j=37693cfc748049e45d87b8c7d8b9aacd
5 | (2147483696,3) | {"(3,33)","(3,34)","(3,35)"} |
j=37a749d808e46495a8da1e5352d03cae```
downlink on the leaf page has a different meaning than on the internal
page, but I didn't handle it any differently. In the example ouput,
(2147483696,3) = ((1<<31) + 48, 3), meaning the next 48 bytes is the
entry tree key, and after that there is 3 posting items,
varbyte-encoded.I also tested this function on GIN index with nulls, it works. Let me
know if I'm wrong, I know that GIN handles NULLS very differently, but
I had trouble with it, which makes me think that I'm missing
something.Also turns out this gin_entrypage_items actually works for fast lists
(GIN_LIST and GIN_LIST_FULLROW pages). But output is something
strange, I cannot validate is output is sane. For tids, I get values
like```
1 | (1,19) |
{"(16782080,4096)","(16777216,14336)","(1280,0)","(469774336,0)","(65536,0)","(2734686208,57344)","(8389120,0)","(3372220424,65280)","(4294967295,65535)","(16711680,0)","(0,0)","(
33554688,0)","(614,0)","(67108864,1024)","(16777216,0)","(73793536,0)","(0,0)","(0,0)","(0,0)"}
```gin_datapage_items is for posting trees, but not leaf pages. For leaf
pages, users are expected to still use the gin_leafpage_items
function.
Example output for gin_datapage_items:
In v2 I decided to simply reject these pages.
```
reshke=# select * from gin_datapage_items(get_raw_page('x_i_j_idx',
43), 'x_i_j_idx'::regclass);
itemoffset | downlink | item_tid
------------+----------+----------
1 | 124 | (162,12)
2 | 123 | (314,37)
3 | 251 | (467,23)
4 | 373 | (0,0)
(4 rows)```
Patch still is very raw, many things to improve.
Comments?--
Best regards,
Kirill Reshke
Attached v2 with minor fixes and new test cases in gin.sql.
--
Best regards,
Kirill Reshke
Attachments:
v2-0001-GIN-pageinspect-support-for-entry-tree-and-postin.patchapplication/octet-stream; name=v2-0001-GIN-pageinspect-support-for-entry-tree-and-postin.patchDownload
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
On 29 Dec 2025, at 17:51, Kirill Reshke <reshkekirill@gmail.com> wrote:
Attached v2
I've looked into the patch.
The functionality is useful and seems to reflect pageinspect style.
Patch still is very raw, many things to improve.
Yup, but let's work on it!
Please update the documentation here [0]https://www.postgresql.org/docs/current/pageinspect.html#PAGEINSPECT-GIN-FUNCS.
Other AM's seems to defend from each other: if (!IS_INDEX(rel) || !IS_BTREE(rel)) or if (!IS_GIST(indexRel)). I don't see such check in new functions. B-tree also protects from temp tables of other sessions: if (RELATION_IS_OTHER_TEMP(rel)).
gin_datapage_items() seem to ignore reloid, did you have some ideas how to use it?
In gin_entrypage_items() buf and tmpTupdesc seem to be recreated for every offset, can we reuse them?
gin_entrypage_items() errors out with "input page is not a valid GIN data leaf page", but function is for entry pages.
There's no tests for gin_datapage_items().
There's a typo "unsuppoerted" and "rejrect" in gin.sql.
gin_entrypage_items() emits elog(NOTICE, "page is deleted"), but gin_datapage_items() does not.
Also, note that corresponding GiST code does "OffsetNumber maxoff = InvalidOffsetNumber;", but gin_entrypage_items() has no this initialization.
I'd convert Assert(!isnull) into if+elog: inspected data might be corrupted.
Thanks!
Best regards, Andrey Borodin.
[0]: https://www.postgresql.org/docs/current/pageinspect.html#PAGEINSPECT-GIN-FUNCS
On Thu, 1 Jan 2026 at 19:00, Andrey Borodin <x4mmm@yandex-team.ru> wrote:
On 29 Dec 2025, at 17:51, Kirill Reshke <reshkekirill@gmail.com> wrote:
Attached v2
I've looked into the patch.
The functionality is useful and seems to reflect pageinspect style.
Thank you for taking a look.
Patch still is very raw, many things to improve.
Yup, but let's work on it!
Please update the documentation here [0].
Other AM's seems to defend from each other: if (!IS_INDEX(rel) || !IS_BTREE(rel)) or if (!IS_GIST(indexRel)). I don't see such check in new functions. B-tree also protects from temp tables of other sessions: if (RELATION_IS_OTHER_TEMP(rel)).
I have added protections for functions that accept regclass.
RELATION_IS_OTHER_TEMP is unnecessary here, because generic
get_raw_page functions already take care of that.
gin_datapage_items() seem to ignore reloid, did you have some ideas how to use it?
Yes, I removed that 'reloid'. It was of no use.
In gin_entrypage_items() buf and tmpTupdesc seem to be recreated for every offset, can we reuse them?
Well, I did not get it, how we can reuse them, but v3 now releases
memory after use.
For tmpTupdesc, the issue here is that GIN entry tree pages can have
tuples which need different tuple descriptors to access data, on the
same page. That's why I re-evaluate on every offset. I think caching
tuple descriptors here would be a cure worse than a disease.
Example from my first email here:
```
reshke=# select * from gin_entrypage_items(get_raw_page('x_i_j_idx',
1), 'x_i_j_idx'::regclass);
itemoffset | downlink | tids | keys
------------+----------+------+------------------------------------
1 | (3,0) | {} | i=113
2 | (5,0) | {} | j=34173cb38f07f89ddbebc2ac9128303f
3 | (2,0) | {} | j=a0a080f42e6f13b3a2df133f073095dd
4 | (4,0) | {} | j=fc490ca45c00b1249bbe3554a4fdf6fb
(4 rows)
```
To display that, we need different tupdescs for offset 1 & 2. for
offsets 3&4 we can reuse tupdesc from offset 2, but this would
(unnecessary?) increase patch complexity.
WDYT?
gin_entrypage_items() errors out with "input page is not a valid GIN data leaf page", but function is for entry pages.
I have added more sanity checks and regression tests for them.
There's no tests for gin_datapage_items().
I have added them in v3. My main concern here was that we have to
generate many tuples to trigger GIN to create an internal posting tree
page,
It is also block-size-dependent, and will not work for values other
than 8192, actually. Maybe we need to compute the number of inserted
tuples in-flight?
There's a typo "unsuppoerted" and "rejrect" in gin.sql.
Fxd
gin_entrypage_items() emits elog(NOTICE, "page is deleted"), but gin_datapage_items() does not.
This is stupid copy-paste from GiST pageinspect. My fault, removed.
(GiST deleted page do not store info to compute max offset correctly,
while GIN does store maxOffset in its Opaque info)
Also, note that corresponding GiST code does "OffsetNumber maxoff = InvalidOffsetNumber;", but gin_entrypage_items() has no this initialization.
It actually does but in a separate line, is this a problem?
I'd convert Assert(!isnull) into if+elog: inspected data might be corrupted.
I do not have a strong opinion here, but we are implementing
pageinspect here, not amcheck? I can only see one ereport with
corruption error code in pageinspect, and it is for HASH indexes.
Anyway, I changed assert to if-elog
Thanks!
Best regards, Andrey Borodin.
[0] https://www.postgresql.org/docs/current/pageinspect.html#PAGEINSPECT-GIN-FUNCS
PFA v3. I added you and Romka (cc-ed) as reviewers. Romka did a review
of v1 off-list which triggered me to actually bump this thread.
--
Best regards,
Kirill Reshke
Attachments:
v3-0001-GIN-pageinspect-support-for-entry-tree-and-postin.patchapplication/octet-stream; name=v3-0001-GIN-pageinspect-support-for-entry-tree-and-postin.patchDownload
From bb80ecd61a830bc24b297d486a99d419ad77fc5c Mon Sep 17 00:00:00 2001
From: reshke <reshke@double.cloud>
Date: Mon, 13 Oct 2025 20:14:26 +0000
Subject: [PATCH v3] GIN pageinspect support for entry tree and posting tree
internal pages
This patch provides new version for pageinspect contrib module including
two new functions:
* gin_entrypage_items.
* gin_datapage_items.
These two functions can be used to examine GIN entry tree and posting
tree pages. Namely, gin_entrypage_items can be used of both leaf and
non-leaf entry tree pages. gin_datapage_items is provided in pairs with
already-existing gin_leafpage_items to examine non-leaf posting tree
pages.
We keep the different functions here mainly because of different GIN
pages layoff.
Note that fast-list pages are out of scope of this patch.
Reviewed-by: Andrey Borodin x4mmm@yandex-team.ru
Reviewed-by: Roman Khapov rkhapov@yandex-team.ru
---
contrib/pageinspect/Makefile | 2 +-
contrib/pageinspect/expected/gin.out | 84 ++++-
contrib/pageinspect/ginfuncs.c | 337 ++++++++++++++++++
.../pageinspect/pageinspect--1.13--1.14.sql | 28 ++
contrib/pageinspect/pageinspect.control | 2 +-
contrib/pageinspect/sql/gin.sql | 34 +-
6 files changed, 481 insertions(+), 6 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..97057e37529 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',
@@ -35,6 +76,23 @@ FROM gin_leafpage_items(get_raw_page('test1_y_idx',
-[ RECORD 1 ]
?column? | t
+-- Now test posting tree non-leaf page.
+-- This requires inserting many tuples on a single leaf page to trigger page split.
+CREATE TABLE test_data_page(i INT[]);
+CREATE INDEX test_data_page_i_idx ON test_data_page USING gin(i) WITH (fastupdate = off);
+INSERT INTO test_data_page SELECT ARRAY[1] FROM generate_series(1, 10000);
+-- For this index, block 0 is metapage, block 1 is entry tree, block 2 is
+-- posting tree non-leaf page and block 3 & 4 are compressed data leaf pages.
+SELECT * FROM gin_datapage_items(get_raw_page('test_data_page_i_idx', 2));
+-[ RECORD 1 ]-------
+itemoffset | 1
+downlink | 4
+item_tid | (44,83)
+-[ RECORD 2 ]-------
+itemoffset | 2
+downlink | 3
+item_tid | (0,0)
+
-- Failure with various modes.
-- Suppress the DETAIL message, to allow the tests to work across various
-- page sizes and architectures.
@@ -54,12 +112,34 @@ 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 unsupported 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
+-- Check the error message for the internal posting tree page.
+SELECT * FROM gin_entrypage_items(get_raw_page('test_data_page_i_idx', 2), 'test_data_page_i_idx'::regclass);
+ERROR: input page is not a GIN entry tree page
+HINT: This appears to be a GIN posting tree page. Please use gin_datapage_items
+-- 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 the 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}
+
+-- reject 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'));
-[ RECORD 1 ]------+-
gin_leafpage_items |
+SELECT gin_datapage_items(decode(repeat('00', :block_size), 'hex'));
+(0 rows)
+
SELECT gin_metapage_info(decode(repeat('00', :block_size), 'hex'));
-[ RECORD 1 ]-----+-
gin_metapage_info |
diff --git a/contrib/pageinspect/ginfuncs.c b/contrib/pageinspect/ginfuncs.c
index ebcc2b3db5c..0fb98c0efd6 100644
--- a/contrib/pageinspect/ginfuncs.c
+++ b/contrib/pageinspect/ginfuncs.c
@@ -11,18 +11,27 @@
#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);
+#define IS_INDEX(r) ((r)->rd_rel->relkind == RELKIND_INDEX)
+#define IS_GIN(r) ((r)->rd_rel->relam == GIN_AM_OID)
Datum
gin_metapage_info(PG_FUNCTION_ARGS)
@@ -175,6 +184,334 @@ 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;
+ StringInfoData buf;
+
+ 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);
+
+ if (!IS_INDEX(indexRel) || !IS_GIN(indexRel))
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is not a %s index",
+ RelationGetRelationName(indexRel), "GIN")));
+
+ 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 entry tree 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"),
+ errhint("This appears to be a GIN posting tree page. Please use gin_datapage_items")));
+
+ maxoff = PageGetMaxOffsetNumber(page);
+
+ tupdesc = RelationGetDescr(indexRel);
+ oneCol = tupdesc->natts == 1;
+
+ for (offset = FirstOffsetNumber;
+ offset <= maxoff;
+ offset = OffsetNumberNext(offset))
+ {
+ 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);
+ if (isnull)
+ ereport(ERROR,
+ (errcode(ERRCODE_INDEX_CORRUPTED),
+ errmsg("invalud gin entry page tuple at offset %d", offset)));
+ }
+ 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 */
+ if (isnull)
+ ereport(ERROR,
+ (errcode(ERRCODE_INDEX_CORRUPTED),
+ errmsg("invalud gin entry page tuple at offset %d", offset)));
+
+ 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);
+
+ FreeTupleDesc(tmpTupdesc);
+ }
+
+ 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);
+ resetStringInfo(&buf);
+
+ 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..72f5e9bbea7
--- /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,
+ 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..3e2738c71b0 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
@@ -18,6 +24,18 @@ FROM gin_leafpage_items(get_raw_page('test1_y_idx',
(pg_relation_size('test1_y_idx') /
current_setting('block_size')::bigint)::int - 1));
+-- Now test posting tree non-leaf page.
+-- This requires inserting many tuples on a single leaf page to trigger page split.
+
+CREATE TABLE test_data_page(i INT[]);
+CREATE INDEX test_data_page_i_idx ON test_data_page USING gin(i) WITH (fastupdate = off);
+
+INSERT INTO test_data_page SELECT ARRAY[1] FROM generate_series(1, 10000);
+
+-- For this index, block 0 is metapage, block 1 is entry tree, block 2 is
+-- posting tree non-leaf page and block 3 & 4 are compressed data leaf pages.
+SELECT * FROM gin_datapage_items(get_raw_page('test_data_page_i_idx', 2));
+
-- Failure with various modes.
-- Suppress the DETAIL message, to allow the tests to work across various
-- page sizes and architectures.
@@ -32,9 +50,21 @@ 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 unsupported page types in gin_entrypage_items.
+SELECT * FROM gin_entrypage_items(get_raw_page('test2_y_z_idx', 0), 'test2_y_z_idx'::regclass);
+-- Check the error message for the internal posting tree page.
+SELECT * FROM gin_entrypage_items(get_raw_page('test_data_page_i_idx', 2), 'test_data_page_i_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 the new page is fast-list.
+SELECT * FROM gin_page_opaque_info(get_raw_page('test3_y_z_idx', 2));
+-- reject 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'));
+SELECT gin_datapage_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'));
--
2.43.0
Sorry for noise, new version with minor polishing.
On Sat, 3 Jan 2026 at 00:10, Kirill Reshke <reshkekirill@gmail.com> wrote:
It actually does but in a separate line, is this a problem?
I removed this cause we unconditionally assign maxoff =
PageGetMaxOffsetNumber(page);.
PFA v3.
PFA v4 with typo fixes.
--
Best regards,
Kirill Reshke
Attachments:
v4-0001-GIN-pageinspect-support-for-entry-tree-and-postin.patchapplication/octet-stream; name=v4-0001-GIN-pageinspect-support-for-entry-tree-and-postin.patchDownload
From 8be472bd81fef7dc4fa6baa5872cee3b1e0036a7 Mon Sep 17 00:00:00 2001
From: reshke <reshke@double.cloud>
Date: Mon, 13 Oct 2025 20:14:26 +0000
Subject: [PATCH v4] GIN pageinspect support for entry tree and posting tree
internal pages
This patch provides new version for pageinspect contrib module including
two new functions:
* gin_entrypage_items.
* gin_datapage_items.
These two functions can be used to examine GIN entry tree and posting
tree pages. Namely, gin_entrypage_items can be used of both leaf and
non-leaf entry tree pages. gin_datapage_items is provided in pairs with
already-existing gin_leafpage_items to examine non-leaf posting tree
pages.
We keep the different functions here mainly because of different GIN
pages layoff.
Note that fast-list pages are out of scope of this patch.
Reviewed-by: Andrey Borodin x4mmm@yandex-team.ru
Reviewed-by: Roman Khapov rkhapov@yandex-team.ru
---
contrib/pageinspect/Makefile | 2 +-
contrib/pageinspect/expected/gin.out | 84 ++++-
contrib/pageinspect/ginfuncs.c | 335 ++++++++++++++++++
.../pageinspect/pageinspect--1.13--1.14.sql | 28 ++
contrib/pageinspect/pageinspect.control | 2 +-
contrib/pageinspect/sql/gin.sql | 34 +-
6 files changed, 479 insertions(+), 6 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..97057e37529 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',
@@ -35,6 +76,23 @@ FROM gin_leafpage_items(get_raw_page('test1_y_idx',
-[ RECORD 1 ]
?column? | t
+-- Now test posting tree non-leaf page.
+-- This requires inserting many tuples on a single leaf page to trigger page split.
+CREATE TABLE test_data_page(i INT[]);
+CREATE INDEX test_data_page_i_idx ON test_data_page USING gin(i) WITH (fastupdate = off);
+INSERT INTO test_data_page SELECT ARRAY[1] FROM generate_series(1, 10000);
+-- For this index, block 0 is metapage, block 1 is entry tree, block 2 is
+-- posting tree non-leaf page and block 3 & 4 are compressed data leaf pages.
+SELECT * FROM gin_datapage_items(get_raw_page('test_data_page_i_idx', 2));
+-[ RECORD 1 ]-------
+itemoffset | 1
+downlink | 4
+item_tid | (44,83)
+-[ RECORD 2 ]-------
+itemoffset | 2
+downlink | 3
+item_tid | (0,0)
+
-- Failure with various modes.
-- Suppress the DETAIL message, to allow the tests to work across various
-- page sizes and architectures.
@@ -54,12 +112,34 @@ 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 unsupported 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
+-- Check the error message for the internal posting tree page.
+SELECT * FROM gin_entrypage_items(get_raw_page('test_data_page_i_idx', 2), 'test_data_page_i_idx'::regclass);
+ERROR: input page is not a GIN entry tree page
+HINT: This appears to be a GIN posting tree page. Please use gin_datapage_items
+-- 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 the 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}
+
+-- reject 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'));
-[ RECORD 1 ]------+-
gin_leafpage_items |
+SELECT gin_datapage_items(decode(repeat('00', :block_size), 'hex'));
+(0 rows)
+
SELECT gin_metapage_info(decode(repeat('00', :block_size), 'hex'));
-[ RECORD 1 ]-----+-
gin_metapage_info |
diff --git a/contrib/pageinspect/ginfuncs.c b/contrib/pageinspect/ginfuncs.c
index ebcc2b3db5c..059db497e93 100644
--- a/contrib/pageinspect/ginfuncs.c
+++ b/contrib/pageinspect/ginfuncs.c
@@ -11,18 +11,27 @@
#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);
+#define IS_INDEX(r) ((r)->rd_rel->relkind == RELKIND_INDEX)
+#define IS_GIN(r) ((r)->rd_rel->relam == GIN_AM_OID)
Datum
gin_metapage_info(PG_FUNCTION_ARGS)
@@ -175,6 +184,332 @@ 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;
+ StringInfoData buf;
+
+ 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_INDEX(indexRel) || !IS_GIN(indexRel))
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is not a %s index",
+ RelationGetRelationName(indexRel), "GIN")));
+
+ 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 entry tree 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"),
+ errhint("This appears to be a GIN posting tree page. Please use gin_datapage_items")));
+
+ maxoff = PageGetMaxOffsetNumber(page);
+
+ tupdesc = RelationGetDescr(indexRel);
+ oneCol = tupdesc->natts == 1;
+
+ for (offset = FirstOffsetNumber;
+ offset <= maxoff;
+ offset = OffsetNumberNext(offset))
+ {
+ 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);
+ if (isnull)
+ ereport(ERROR,
+ (errcode(ERRCODE_INDEX_CORRUPTED),
+ errmsg("invalid gin entry page tuple at offset %d", offset)));
+ }
+ 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 */
+ if (isnull)
+ ereport(ERROR,
+ (errcode(ERRCODE_INDEX_CORRUPTED),
+ errmsg("invalid gin entry page tuple at offset %d", offset)));
+
+ 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);
+
+ FreeTupleDesc(tmpTupdesc);
+ }
+
+ 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);
+ resetStringInfo(&buf);
+
+ 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..72f5e9bbea7
--- /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,
+ 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..3e2738c71b0 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
@@ -18,6 +24,18 @@ FROM gin_leafpage_items(get_raw_page('test1_y_idx',
(pg_relation_size('test1_y_idx') /
current_setting('block_size')::bigint)::int - 1));
+-- Now test posting tree non-leaf page.
+-- This requires inserting many tuples on a single leaf page to trigger page split.
+
+CREATE TABLE test_data_page(i INT[]);
+CREATE INDEX test_data_page_i_idx ON test_data_page USING gin(i) WITH (fastupdate = off);
+
+INSERT INTO test_data_page SELECT ARRAY[1] FROM generate_series(1, 10000);
+
+-- For this index, block 0 is metapage, block 1 is entry tree, block 2 is
+-- posting tree non-leaf page and block 3 & 4 are compressed data leaf pages.
+SELECT * FROM gin_datapage_items(get_raw_page('test_data_page_i_idx', 2));
+
-- Failure with various modes.
-- Suppress the DETAIL message, to allow the tests to work across various
-- page sizes and architectures.
@@ -32,9 +50,21 @@ 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 unsupported page types in gin_entrypage_items.
+SELECT * FROM gin_entrypage_items(get_raw_page('test2_y_z_idx', 0), 'test2_y_z_idx'::regclass);
+-- Check the error message for the internal posting tree page.
+SELECT * FROM gin_entrypage_items(get_raw_page('test_data_page_i_idx', 2), 'test_data_page_i_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 the new page is fast-list.
+SELECT * FROM gin_page_opaque_info(get_raw_page('test3_y_z_idx', 2));
+-- reject 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'));
+SELECT gin_datapage_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'));
--
2.43.0
On 3 Jan 2026, at 00:16, Kirill Reshke <reshkekirill@gmail.com> wrote:
PFA v4 with typo fixes.
Thanks!
I think block size dependency is not a problem. There are no pageinspect tests that pass with 1Kb pages.
I have some more nits:
Meson build needs to be updated.
Documentation is on a TODO list.
+ relation_close(indexRel, AccessShareLock);
Perhaps, index_close() is what you actually wanted?
StringInfoData buf is leaked still, you can init it once and reset in the loop.
tmpTupdesc recreation worth commenting on.
Thanks!
Best regards, Andrey Borodin.
On Sat, 3 Jan 2026 at 00:16, Kirill Reshke <reshkekirill@gmail.com> wrote:
PFA v3.
PFA v4 with typo fixes.
PFA v5 with pageinspect documentation updates.
--
Best regards,
Kirill Reshke
Attachments:
v5-0001-GIN-pageinspect-support-for-entry-tree-and-postin.patchapplication/octet-stream; name=v5-0001-GIN-pageinspect-support-for-entry-tree-and-postin.patchDownload
From 947687725296941bbf3773e399bda93bf0da8148 Mon Sep 17 00:00:00 2001
From: reshke <reshke@double.cloud>
Date: Mon, 13 Oct 2025 20:14:26 +0000
Subject: [PATCH v5] GIN pageinspect support for entry tree and posting tree
internal pages
This patch provides new version for pageinspect contrib module including
two new functions:
* gin_entrypage_items.
* gin_datapage_items.
These two functions can be used to examine GIN entry tree and posting
tree pages. Namely, gin_entrypage_items can be used of both leaf and
non-leaf entry tree pages. gin_datapage_items is provided in pairs with
already-existing gin_leafpage_items to examine non-leaf posting tree
pages.
We keep the different functions here mainly because of different GIN
pages layoff.
Note that fast-list pages are out of scope of this patch.
Reviewed-by: Andrey Borodin x4mmm@yandex-team.ru
Reviewed-by: Roman Khapov rkhapov@yandex-team.ru
---
contrib/pageinspect/Makefile | 2 +-
contrib/pageinspect/expected/gin.out | 84 ++++-
contrib/pageinspect/ginfuncs.c | 335 ++++++++++++++++++
.../pageinspect/pageinspect--1.13--1.14.sql | 28 ++
contrib/pageinspect/pageinspect.control | 2 +-
contrib/pageinspect/sql/gin.sql | 34 +-
doc/src/sgml/pageinspect.sgml | 54 +++
7 files changed, 533 insertions(+), 6 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..97057e37529 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',
@@ -35,6 +76,23 @@ FROM gin_leafpage_items(get_raw_page('test1_y_idx',
-[ RECORD 1 ]
?column? | t
+-- Now test posting tree non-leaf page.
+-- This requires inserting many tuples on a single leaf page to trigger page split.
+CREATE TABLE test_data_page(i INT[]);
+CREATE INDEX test_data_page_i_idx ON test_data_page USING gin(i) WITH (fastupdate = off);
+INSERT INTO test_data_page SELECT ARRAY[1] FROM generate_series(1, 10000);
+-- For this index, block 0 is metapage, block 1 is entry tree, block 2 is
+-- posting tree non-leaf page and block 3 & 4 are compressed data leaf pages.
+SELECT * FROM gin_datapage_items(get_raw_page('test_data_page_i_idx', 2));
+-[ RECORD 1 ]-------
+itemoffset | 1
+downlink | 4
+item_tid | (44,83)
+-[ RECORD 2 ]-------
+itemoffset | 2
+downlink | 3
+item_tid | (0,0)
+
-- Failure with various modes.
-- Suppress the DETAIL message, to allow the tests to work across various
-- page sizes and architectures.
@@ -54,12 +112,34 @@ 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 unsupported 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
+-- Check the error message for the internal posting tree page.
+SELECT * FROM gin_entrypage_items(get_raw_page('test_data_page_i_idx', 2), 'test_data_page_i_idx'::regclass);
+ERROR: input page is not a GIN entry tree page
+HINT: This appears to be a GIN posting tree page. Please use gin_datapage_items
+-- 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 the 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}
+
+-- reject 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'));
-[ RECORD 1 ]------+-
gin_leafpage_items |
+SELECT gin_datapage_items(decode(repeat('00', :block_size), 'hex'));
+(0 rows)
+
SELECT gin_metapage_info(decode(repeat('00', :block_size), 'hex'));
-[ RECORD 1 ]-----+-
gin_metapage_info |
diff --git a/contrib/pageinspect/ginfuncs.c b/contrib/pageinspect/ginfuncs.c
index ebcc2b3db5c..059db497e93 100644
--- a/contrib/pageinspect/ginfuncs.c
+++ b/contrib/pageinspect/ginfuncs.c
@@ -11,18 +11,27 @@
#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);
+#define IS_INDEX(r) ((r)->rd_rel->relkind == RELKIND_INDEX)
+#define IS_GIN(r) ((r)->rd_rel->relam == GIN_AM_OID)
Datum
gin_metapage_info(PG_FUNCTION_ARGS)
@@ -175,6 +184,332 @@ 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;
+ StringInfoData buf;
+
+ 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_INDEX(indexRel) || !IS_GIN(indexRel))
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is not a %s index",
+ RelationGetRelationName(indexRel), "GIN")));
+
+ 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 entry tree 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"),
+ errhint("This appears to be a GIN posting tree page. Please use gin_datapage_items")));
+
+ maxoff = PageGetMaxOffsetNumber(page);
+
+ tupdesc = RelationGetDescr(indexRel);
+ oneCol = tupdesc->natts == 1;
+
+ for (offset = FirstOffsetNumber;
+ offset <= maxoff;
+ offset = OffsetNumberNext(offset))
+ {
+ 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);
+ if (isnull)
+ ereport(ERROR,
+ (errcode(ERRCODE_INDEX_CORRUPTED),
+ errmsg("invalid gin entry page tuple at offset %d", offset)));
+ }
+ 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 */
+ if (isnull)
+ ereport(ERROR,
+ (errcode(ERRCODE_INDEX_CORRUPTED),
+ errmsg("invalid gin entry page tuple at offset %d", offset)));
+
+ 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);
+
+ FreeTupleDesc(tmpTupdesc);
+ }
+
+ 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);
+ resetStringInfo(&buf);
+
+ 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..72f5e9bbea7
--- /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,
+ 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..3e2738c71b0 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
@@ -18,6 +24,18 @@ FROM gin_leafpage_items(get_raw_page('test1_y_idx',
(pg_relation_size('test1_y_idx') /
current_setting('block_size')::bigint)::int - 1));
+-- Now test posting tree non-leaf page.
+-- This requires inserting many tuples on a single leaf page to trigger page split.
+
+CREATE TABLE test_data_page(i INT[]);
+CREATE INDEX test_data_page_i_idx ON test_data_page USING gin(i) WITH (fastupdate = off);
+
+INSERT INTO test_data_page SELECT ARRAY[1] FROM generate_series(1, 10000);
+
+-- For this index, block 0 is metapage, block 1 is entry tree, block 2 is
+-- posting tree non-leaf page and block 3 & 4 are compressed data leaf pages.
+SELECT * FROM gin_datapage_items(get_raw_page('test_data_page_i_idx', 2));
+
-- Failure with various modes.
-- Suppress the DETAIL message, to allow the tests to work across various
-- page sizes and architectures.
@@ -32,9 +50,21 @@ 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 unsupported page types in gin_entrypage_items.
+SELECT * FROM gin_entrypage_items(get_raw_page('test2_y_z_idx', 0), 'test2_y_z_idx'::regclass);
+-- Check the error message for the internal posting tree page.
+SELECT * FROM gin_entrypage_items(get_raw_page('test_data_page_i_idx', 2), 'test_data_page_i_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 the new page is fast-list.
+SELECT * FROM gin_page_opaque_info(get_raw_page('test3_y_z_idx', 2));
+-- reject 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'));
+SELECT gin_datapage_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'));
diff --git a/doc/src/sgml/pageinspect.sgml b/doc/src/sgml/pageinspect.sgml
index 3a113439e1d..4c1c7162b54 100644
--- a/doc/src/sgml/pageinspect.sgml
+++ b/doc/src/sgml/pageinspect.sgml
@@ -718,6 +718,60 @@ test=# SELECT first_tid, nbytes, tids[0:5] AS some_tids
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term>
+ <function>gin_entrypage_items(page bytea, reloid oid) returns setof record</function>
+ <indexterm>
+ <primary>gin_entrypage_items</primary>
+ </indexterm>
+ </term>
+
+ <listitem>
+ <para>
+ <function>gin_entrypage_items</function> returns information about
+ the data stored in a entry tree <acronym>GIN</acronym> page. For example:
+<screen>
+test=# select * from gin_entrypage_items(get_raw_page('gin_test_idx',
+1), 'gin_test_idx'::regclass);
+ itemoffset | downlink | tids | keys
+------------+----------+------+------------------------------------
+ 1 | (3,0) | {} | i=113
+ 2 | (5,0) | {} | j=34173cb38f07f89ddbebc2ac9128303f
+ 3 | (2,0) | {} | j=a0a080f42e6f13b3a2df133f073095dd
+ 4 | (4,0) | {} | j=fc490ca45c00b1249bbe3554a4fdf6fb
+(4 rows)
+</screen>
+ </para>
+ </listitem>
+
+ <varlistentry>
+ <term>
+ <function>gin_datapage_items(page bytea) returns setof record</function>
+ <indexterm>
+ <primary>gin_datapage_items</primary>
+ </indexterm>
+ </term>
+
+ <listitem>
+ <para>
+ <function>gin_datapage_items</function> returns information about
+ the data stored in a posting tree <acronym>GIN</acronym> internal page. For example:
+<screen>
+test=# select * from gin_datapage_items(get_raw_page('gin_test_idx',
+43));
+ itemoffset | downlink | item_tid
+------------+----------+----------
+ 1 | 124 | (162,12)
+ 2 | 123 | (314,37)
+ 3 | 251 | (467,23)
+ 4 | 373 | (0,0)
+(4 rows)
+</screen>
+ </para>
+ </listitem>
+
+ </varlistentry>
</variablelist>
</sect2>
--
2.43.0
On Sat, 3 Jan 2026 at 23:58, Andrey Borodin <x4mmm@yandex-team.ru> wrote:
On 3 Jan 2026, at 00:16, Kirill Reshke <reshkekirill@gmail.com> wrote:
PFA v4 with typo fixes.
Thanks!
I think block size dependency is not a problem. There are no pageinspect tests that pass with 1Kb pages.
I have some more nits:
Meson build needs to be updated.
Documentation is on a TODO list.
Done
+ relation_close(indexRel, AccessShareLock);
Perhaps, index_close() is what you actually wanted?
Ok
StringInfoData buf is leaked still, you can init it once and reset in the loop.
Ok
tmpTupdesc recreation worth commenting on.
Ok
Thanks!
Best regards, Andrey Borodin.
PFA v6
--
Best regards,
Kirill Reshke
Attachments:
v6-0001-GIN-pageinspect-support-for-entry-tree-and-postin.patchapplication/octet-stream; name=v6-0001-GIN-pageinspect-support-for-entry-tree-and-postin.patchDownload
From 46d09ed2638257a3f4dab2636f4c5faca52153a5 Mon Sep 17 00:00:00 2001
From: reshke <reshke@double.cloud>
Date: Mon, 13 Oct 2025 20:14:26 +0000
Subject: [PATCH v6] GIN pageinspect support for entry tree and posting tree
internal pages
This patch provides new version for pageinspect contrib module including
two new functions:
* gin_entrypage_items.
* gin_datapage_items.
These two functions can be used to examine GIN entry tree and posting
tree pages. Namely, gin_entrypage_items can be used of both leaf and
non-leaf entry tree pages. gin_datapage_items is provided in pairs with
already-existing gin_leafpage_items to examine non-leaf posting tree
pages.
We keep the different functions here mainly because of different GIN
pages layoff.
Note that fast-list pages are out of scope of this patch.
Reviewed-by: Andrey Borodin x4mmm@yandex-team.ru
Reviewed-by: Roman Khapov rkhapov@yandex-team.ru
---
contrib/pageinspect/Makefile | 2 +-
contrib/pageinspect/expected/gin.out | 84 ++++-
contrib/pageinspect/ginfuncs.c | 340 ++++++++++++++++++
contrib/pageinspect/meson.build | 1 +
.../pageinspect/pageinspect--1.13--1.14.sql | 28 ++
contrib/pageinspect/pageinspect.control | 2 +-
contrib/pageinspect/sql/gin.sql | 34 +-
doc/src/sgml/pageinspect.sgml | 54 +++
8 files changed, 539 insertions(+), 6 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..97057e37529 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',
@@ -35,6 +76,23 @@ FROM gin_leafpage_items(get_raw_page('test1_y_idx',
-[ RECORD 1 ]
?column? | t
+-- Now test posting tree non-leaf page.
+-- This requires inserting many tuples on a single leaf page to trigger page split.
+CREATE TABLE test_data_page(i INT[]);
+CREATE INDEX test_data_page_i_idx ON test_data_page USING gin(i) WITH (fastupdate = off);
+INSERT INTO test_data_page SELECT ARRAY[1] FROM generate_series(1, 10000);
+-- For this index, block 0 is metapage, block 1 is entry tree, block 2 is
+-- posting tree non-leaf page and block 3 & 4 are compressed data leaf pages.
+SELECT * FROM gin_datapage_items(get_raw_page('test_data_page_i_idx', 2));
+-[ RECORD 1 ]-------
+itemoffset | 1
+downlink | 4
+item_tid | (44,83)
+-[ RECORD 2 ]-------
+itemoffset | 2
+downlink | 3
+item_tid | (0,0)
+
-- Failure with various modes.
-- Suppress the DETAIL message, to allow the tests to work across various
-- page sizes and architectures.
@@ -54,12 +112,34 @@ 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 unsupported 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
+-- Check the error message for the internal posting tree page.
+SELECT * FROM gin_entrypage_items(get_raw_page('test_data_page_i_idx', 2), 'test_data_page_i_idx'::regclass);
+ERROR: input page is not a GIN entry tree page
+HINT: This appears to be a GIN posting tree page. Please use gin_datapage_items
+-- 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 the 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}
+
+-- reject 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'));
-[ RECORD 1 ]------+-
gin_leafpage_items |
+SELECT gin_datapage_items(decode(repeat('00', :block_size), 'hex'));
+(0 rows)
+
SELECT gin_metapage_info(decode(repeat('00', :block_size), 'hex'));
-[ RECORD 1 ]-----+-
gin_metapage_info |
diff --git a/contrib/pageinspect/ginfuncs.c b/contrib/pageinspect/ginfuncs.c
index ebcc2b3db5c..aa5e8478fc0 100644
--- a/contrib/pageinspect/ginfuncs.c
+++ b/contrib/pageinspect/ginfuncs.c
@@ -11,18 +11,27 @@
#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);
+#define IS_INDEX(r) ((r)->rd_rel->relkind == RELKIND_INDEX)
+#define IS_GIN(r) ((r)->rd_rel->relam == GIN_AM_OID)
Datum
gin_metapage_info(PG_FUNCTION_ARGS)
@@ -175,6 +184,337 @@ 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;
+ StringInfoData buf;
+
+ 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_INDEX(indexRel) || !IS_GIN(indexRel))
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is not a %s index",
+ RelationGetRelationName(indexRel), "GIN")));
+
+ 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 entry tree 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"),
+ errhint("This appears to be a GIN posting tree page. Please use gin_datapage_items")));
+
+ initStringInfo(&buf);
+ maxoff = PageGetMaxOffsetNumber(page);
+
+ tupdesc = RelationGetDescr(indexRel);
+ oneCol = tupdesc->natts == 1;
+
+ for (offset = FirstOffsetNumber;
+ offset <= maxoff;
+ offset = OffsetNumberNext(offset))
+ {
+ 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);
+ if (isnull)
+ ereport(ERROR,
+ (errcode(ERRCODE_INDEX_CORRUPTED),
+ errmsg("invalid gin entry page tuple at offset %d", offset)));
+ }
+ else
+ {
+ TupleDesc tmpTupdesc;
+ Datum res;
+ Form_pg_attribute attr;
+
+ /* Multi-column GIN indexes store 2-attribute tuple on each
+ * page item. First attribute is which heap attribute is stored
+ * as the second value in pair. To display value with proper output
+ * function we need to recreate tuple descriptor on each offset. */
+
+ /* orig tuple reuse is safe */
+
+ res = index_getattr(idxtuple, FirstOffsetNumber, tupdesc,
+ &isnull);
+
+ /* we do not expect null for first attr in multi-column GIN */
+ if (isnull)
+ ereport(ERROR,
+ (errcode(ERRCODE_INDEX_CORRUPTED),
+ errmsg("invalid gin entry page tuple at offset %d", offset)));
+
+ 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);
+
+ FreeTupleDesc(tmpTupdesc);
+ }
+
+ 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);
+ resetStringInfo(&buf);
+
+ 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);
+ }
+
+ index_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/meson.build b/contrib/pageinspect/meson.build
index c43ea400a4d..2f333635838 100644
--- a/contrib/pageinspect/meson.build
+++ b/contrib/pageinspect/meson.build
@@ -38,6 +38,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,
)
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..72f5e9bbea7
--- /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,
+ 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..3e2738c71b0 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
@@ -18,6 +24,18 @@ FROM gin_leafpage_items(get_raw_page('test1_y_idx',
(pg_relation_size('test1_y_idx') /
current_setting('block_size')::bigint)::int - 1));
+-- Now test posting tree non-leaf page.
+-- This requires inserting many tuples on a single leaf page to trigger page split.
+
+CREATE TABLE test_data_page(i INT[]);
+CREATE INDEX test_data_page_i_idx ON test_data_page USING gin(i) WITH (fastupdate = off);
+
+INSERT INTO test_data_page SELECT ARRAY[1] FROM generate_series(1, 10000);
+
+-- For this index, block 0 is metapage, block 1 is entry tree, block 2 is
+-- posting tree non-leaf page and block 3 & 4 are compressed data leaf pages.
+SELECT * FROM gin_datapage_items(get_raw_page('test_data_page_i_idx', 2));
+
-- Failure with various modes.
-- Suppress the DETAIL message, to allow the tests to work across various
-- page sizes and architectures.
@@ -32,9 +50,21 @@ 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 unsupported page types in gin_entrypage_items.
+SELECT * FROM gin_entrypage_items(get_raw_page('test2_y_z_idx', 0), 'test2_y_z_idx'::regclass);
+-- Check the error message for the internal posting tree page.
+SELECT * FROM gin_entrypage_items(get_raw_page('test_data_page_i_idx', 2), 'test_data_page_i_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 the new page is fast-list.
+SELECT * FROM gin_page_opaque_info(get_raw_page('test3_y_z_idx', 2));
+-- reject 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'));
+SELECT gin_datapage_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'));
diff --git a/doc/src/sgml/pageinspect.sgml b/doc/src/sgml/pageinspect.sgml
index 3a113439e1d..4c1c7162b54 100644
--- a/doc/src/sgml/pageinspect.sgml
+++ b/doc/src/sgml/pageinspect.sgml
@@ -718,6 +718,60 @@ test=# SELECT first_tid, nbytes, tids[0:5] AS some_tids
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term>
+ <function>gin_entrypage_items(page bytea, reloid oid) returns setof record</function>
+ <indexterm>
+ <primary>gin_entrypage_items</primary>
+ </indexterm>
+ </term>
+
+ <listitem>
+ <para>
+ <function>gin_entrypage_items</function> returns information about
+ the data stored in a entry tree <acronym>GIN</acronym> page. For example:
+<screen>
+test=# select * from gin_entrypage_items(get_raw_page('gin_test_idx',
+1), 'gin_test_idx'::regclass);
+ itemoffset | downlink | tids | keys
+------------+----------+------+------------------------------------
+ 1 | (3,0) | {} | i=113
+ 2 | (5,0) | {} | j=34173cb38f07f89ddbebc2ac9128303f
+ 3 | (2,0) | {} | j=a0a080f42e6f13b3a2df133f073095dd
+ 4 | (4,0) | {} | j=fc490ca45c00b1249bbe3554a4fdf6fb
+(4 rows)
+</screen>
+ </para>
+ </listitem>
+
+ <varlistentry>
+ <term>
+ <function>gin_datapage_items(page bytea) returns setof record</function>
+ <indexterm>
+ <primary>gin_datapage_items</primary>
+ </indexterm>
+ </term>
+
+ <listitem>
+ <para>
+ <function>gin_datapage_items</function> returns information about
+ the data stored in a posting tree <acronym>GIN</acronym> internal page. For example:
+<screen>
+test=# select * from gin_datapage_items(get_raw_page('gin_test_idx',
+43));
+ itemoffset | downlink | item_tid
+------------+----------+----------
+ 1 | 124 | (162,12)
+ 2 | 123 | (314,37)
+ 3 | 251 | (467,23)
+ 4 | 373 | (0,0)
+(4 rows)
+</screen>
+ </para>
+ </listitem>
+
+ </varlistentry>
</variablelist>
</sect2>
--
2.43.0
On 4 Jan 2026, at 00:25, Kirill Reshke <reshkekirill@gmail.com> wrote:
PFA v6
Would it be theoretically possible to unite functions for different GIN page types?
e.g. merge gin_entrypage_items + gin_datapage_items -> gin_tree_items? Or is it an awkward API?
The patch adds whitespace errors.
x4mmm@x4mmm-osx postgres % git am ~/Downloads/v6-0001-GIN-pageinspect-support-for-entry-tree-and-postin.patch
Applying: GIN pageinspect support for entry tree and posting tree internal pages
.git/rebase-apply/patch:236: trailing whitespace.
.git/rebase-apply/patch:242: trailing whitespace.
.git/rebase-apply/patch:243: trailing whitespace.
.git/rebase-apply/patch:249: trailing whitespace.
.git/rebase-apply/patch:284: trailing whitespace.
warning: squelched 8 whitespace errors
warning: 13 lines add whitespace errors.
Docs build fail [0]https://github.com/x4m/postgres_g/runs/59444268144:
[08:01:48.343] pageinspect.sgml:775: parser error : Opening and ending tag mismatch: varlistentry line 722 and variablelist
[08:01:48.343] </variablelist>
[08:01:48.343] ^
[08:01:48.343] pageinspect.sgml:776: parser error : Opening and ending tag mismatch: variablelist line 637 and sect2
[08:01:48.343] </sect2>
[08:01:48.343] ^
[08:01:48.343] pageinspect.sgml:1016: parser error : Opening and ending tag mismatch: sect2 line 634 and sect1
[08:01:48.343] </sect1>
[08:01:48.343] ^
[08:01:48.343] pageinspect.sgml:1017: parser error : Premature end of data in tag sect1 line 3
[08:01:48.343]
[08:01:48.343] ^
[08:01:48.343] pageinspect.sgml:1017: parser error : chunk is not well balanced
[08:01:48.343]
[08:01:48.343] ^
[08:01:48.343] contrib.sgml:152: parser error : Entity 'pageinspect' failed to parse
[08:01:48.343] &pageinspect;
[08:01:48.343] ^
[08:01:48.343] contrib.sgml:239: parser error : chunk is not well balanced
[08:01:48.343]
[08:01:48.343] ^
[08:01:48.344] postgres.sgml:279: parser error : Entity 'contrib' failed to parse
[08:01:48.344] &contrib;
And 32-bit build fail [1]https://cirrus-ci.com/task/6255878661210112:
SELECT * FROM gin_entrypage_items(get_raw_page('test1_y_idx', 1), 'test1_y_idx'::regclass);
-[ RECORD 1 ]--------------
itemoffset | 1
-downlink | (2147483664,1)
+downlink | (2147483660,1)
tids | {"(0,1)"}
keys | y=11
-[ RECORD 2 ]--------------
itemoffset | 2
-downlink | (2147483664,1)
+downlink | (2147483660,1)
tids | {"(0,1)"}
keys | y=111
Thanks!
Best regards, Andrey Borodin.
[0]: https://github.com/x4m/postgres_g/runs/59444268144
[1]: https://cirrus-ci.com/task/6255878661210112
On Mon, 5 Jan 2026 at 13:39, Andrey Borodin <x4mmm@yandex-team.ru> wrote:
On 4 Jan 2026, at 00:25, Kirill Reshke <reshkekirill@gmail.com> wrote:
PFA v6
Would it be theoretically possible to unite functions for different GIN page types?
e.g. merge gin_entrypage_items + gin_datapage_items -> gin_tree_items? Or is it an awkward API?The patch adds whitespace errors.
x4mmm@x4mmm-osx postgres % git am ~/Downloads/v6-0001-GIN-pageinspect-support-for-entry-tree-and-postin.patch
Applying: GIN pageinspect support for entry tree and posting tree internal pages
.git/rebase-apply/patch:236: trailing whitespace.
.git/rebase-apply/patch:242: trailing whitespace.
.git/rebase-apply/patch:243: trailing whitespace.
.git/rebase-apply/patch:249: trailing whitespace.
.git/rebase-apply/patch:284: trailing whitespace.
warning: squelched 8 whitespace errors
warning: 13 lines add whitespace errors.
Thanks
v7 should not suffer from this.
Docs build fail [0]:
[08:01:48.343] pageinspect.sgml:775: parser error : Opening and ending tag mismatch: varlistentry line 722 and variablelist
[08:01:48.343] </variablelist>
[08:01:48.343] ^
[08:01:48.343] pageinspect.sgml:776: parser error : Opening and ending tag mismatch: variablelist line 637 and sect2
[08:01:48.343] </sect2>
[08:01:48.343] ^
[08:01:48.343] pageinspect.sgml:1016: parser error : Opening and ending tag mismatch: sect2 line 634 and sect1
[08:01:48.343] </sect1>
[08:01:48.343] ^
[08:01:48.343] pageinspect.sgml:1017: parser error : Premature end of data in tag sect1 line 3
[08:01:48.343]
[08:01:48.343] ^
[08:01:48.343] pageinspect.sgml:1017: parser error : chunk is not well balanced
[08:01:48.343]
[08:01:48.343] ^
[08:01:48.343] contrib.sgml:152: parser error : Entity 'pageinspect' failed to parse
[08:01:48.343] &pageinspect;
[08:01:48.343] ^
[08:01:48.343] contrib.sgml:239: parser error : chunk is not well balanced
[08:01:48.343]
[08:01:48.343] ^
[08:01:48.344] postgres.sgml:279: parser error : Entity 'contrib' failed to parse
[08:01:48.344] &contrib;
In v7 this is fixed, I believe, (/varlistentry was missing)
And 32-bit build fail [1]:
SELECT * FROM gin_entrypage_items(get_raw_page('test1_y_idx', 1), 'test1_y_idx'::regclass);
-[ RECORD 1 ]--------------
itemoffset | 1
-downlink | (2147483664,1)
+downlink | (2147483660,1)
tids | {"(0,1)"}
keys | y=11
-[ RECORD 2 ]--------------
itemoffset | 2
-downlink | (2147483664,1)
+downlink | (2147483660,1)
tids | {"(0,1)"}
keys | y=111Thanks!
Best regards, Andrey Borodin.
[0] https://github.com/x4m/postgres_g/runs/59444268144
[1] https://cirrus-ci.com/task/6255878661210112
This is because GIN on-disk format is platform-dependent, see [0]https://github.com/postgres/postgres/blob/master/src/backend/access/gin/ginentrypage.c#L91. We
align the offset where to start the Compressed GIN tuples list, that's
where the difference comes from. So, GinSetPostingOffset(itup,
newsize) on line 93 sets offset to 12 | GIN_ITUP_COMPRESSED on 32 bit
and 16 | GIN_ITUP_COMPRESSED on 64-bit. I added alternative regression
output gin_1.out to v7.
PFA v7.
[0]: https://github.com/postgres/postgres/blob/master/src/backend/access/gin/ginentrypage.c#L91
--
Best regards,
Kirill Reshke
Attachments:
v7-0001-GIN-pageinspect-support-for-entry-tree-and-postin.patchapplication/octet-stream; name=v7-0001-GIN-pageinspect-support-for-entry-tree-and-postin.patchDownload
From 197d3a2e74b77c7c890f330b6ebc7056a6916279 Mon Sep 17 00:00:00 2001
From: reshke <reshke@double.cloud>
Date: Mon, 13 Oct 2025 20:14:26 +0000
Subject: [PATCH v7] GIN pageinspect support for entry tree and posting tree
internal pages
This patch provides new version for pageinspect contrib module including
two new functions:
* gin_entrypage_items.
* gin_datapage_items.
These two functions can be used to examine GIN entry tree and posting
tree pages. Namely, gin_entrypage_items can be used of both leaf and
non-leaf entry tree pages. gin_datapage_items is provided in pairs with
already-existing gin_leafpage_items to examine non-leaf posting tree
pages.
We keep the different functions here mainly because of different GIN
pages layoff.
Note that fast-list pages are out of scope of this patch.
Reviewed-by: Andrey Borodin x4mmm@yandex-team.ru
Reviewed-by: Roman Khapov rkhapov@yandex-team.ru
---
contrib/pageinspect/Makefile | 2 +-
contrib/pageinspect/expected/gin.out | 84 ++++-
contrib/pageinspect/expected/gin_1.out | 151 ++++++++
contrib/pageinspect/ginfuncs.c | 338 ++++++++++++++++++
contrib/pageinspect/meson.build | 1 +
.../pageinspect/pageinspect--1.13--1.14.sql | 27 ++
contrib/pageinspect/pageinspect.control | 2 +-
contrib/pageinspect/sql/gin.sql | 34 +-
doc/src/sgml/pageinspect.sgml | 54 +++
9 files changed, 687 insertions(+), 6 deletions(-)
create mode 100644 contrib/pageinspect/expected/gin_1.out
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..97057e37529 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',
@@ -35,6 +76,23 @@ FROM gin_leafpage_items(get_raw_page('test1_y_idx',
-[ RECORD 1 ]
?column? | t
+-- Now test posting tree non-leaf page.
+-- This requires inserting many tuples on a single leaf page to trigger page split.
+CREATE TABLE test_data_page(i INT[]);
+CREATE INDEX test_data_page_i_idx ON test_data_page USING gin(i) WITH (fastupdate = off);
+INSERT INTO test_data_page SELECT ARRAY[1] FROM generate_series(1, 10000);
+-- For this index, block 0 is metapage, block 1 is entry tree, block 2 is
+-- posting tree non-leaf page and block 3 & 4 are compressed data leaf pages.
+SELECT * FROM gin_datapage_items(get_raw_page('test_data_page_i_idx', 2));
+-[ RECORD 1 ]-------
+itemoffset | 1
+downlink | 4
+item_tid | (44,83)
+-[ RECORD 2 ]-------
+itemoffset | 2
+downlink | 3
+item_tid | (0,0)
+
-- Failure with various modes.
-- Suppress the DETAIL message, to allow the tests to work across various
-- page sizes and architectures.
@@ -54,12 +112,34 @@ 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 unsupported 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
+-- Check the error message for the internal posting tree page.
+SELECT * FROM gin_entrypage_items(get_raw_page('test_data_page_i_idx', 2), 'test_data_page_i_idx'::regclass);
+ERROR: input page is not a GIN entry tree page
+HINT: This appears to be a GIN posting tree page. Please use gin_datapage_items
+-- 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 the 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}
+
+-- reject 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'));
-[ RECORD 1 ]------+-
gin_leafpage_items |
+SELECT gin_datapage_items(decode(repeat('00', :block_size), 'hex'));
+(0 rows)
+
SELECT gin_metapage_info(decode(repeat('00', :block_size), 'hex'));
-[ RECORD 1 ]-----+-
gin_metapage_info |
diff --git a/contrib/pageinspect/expected/gin_1.out b/contrib/pageinspect/expected/gin_1.out
new file mode 100644
index 00000000000..eccc5707184
--- /dev/null
+++ b/contrib/pageinspect/expected/gin_1.out
@@ -0,0 +1,151 @@
+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 ]----+-----------
+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
+SELECT * FROM gin_entrypage_items(get_raw_page('test1_y_idx', 1), 'test1_y_idx'::regclass);
+-[ RECORD 1 ]--------------
+itemoffset | 1
+downlink | (2147483660,1)
+tids | {"(0,1)"}
+keys | y=11
+-[ RECORD 2 ]--------------
+itemoffset | 2
+downlink | (2147483660,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 | (2147483660,1)
+tids | {"(0,1)"}
+keys | y=11
+-[ RECORD 2 ]--------------
+itemoffset | 2
+downlink | (2147483660,1)
+tids | {"(0,1)"}
+keys | y=111
+-[ RECORD 3 ]--------------
+itemoffset | 3
+downlink | (2147483660,1)
+tids | {"(0,1)"}
+keys | z=a
+-[ RECORD 4 ]--------------
+itemoffset | 4
+downlink | (2147483660,1)
+tids | {"(0,1)"}
+keys | z=b
+-[ RECORD 5 ]--------------
+itemoffset | 5
+downlink | (2147483660,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',
+ (pg_relation_size('test1_y_idx') /
+ current_setting('block_size')::bigint)::int - 1));
+-[ RECORD 1 ]
+?column? | t
+
+-- Now test posting tree non-leaf page.
+-- This requires inserting many tuples on a single leaf page to trigger page split.
+CREATE TABLE test_data_page(i INT[]);
+CREATE INDEX test_data_page_i_idx ON test_data_page USING gin(i) WITH (fastupdate = off);
+INSERT INTO test_data_page SELECT ARRAY[1] FROM generate_series(1, 10000);
+-- For this index, block 0 is metapage, block 1 is entry tree, block 2 is
+-- posting tree non-leaf page and block 3 & 4 are compressed data leaf pages.
+SELECT * FROM gin_datapage_items(get_raw_page('test_data_page_i_idx', 2));
+-[ RECORD 1 ]-------
+itemoffset | 1
+downlink | 4
+item_tid | (44,83)
+-[ RECORD 2 ]-------
+itemoffset | 2
+downlink | 3
+item_tid | (0,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 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
+-- Reject unsupported 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
+-- Check the error message for the internal posting tree page.
+SELECT * FROM gin_entrypage_items(get_raw_page('test_data_page_i_idx', 2), 'test_data_page_i_idx'::regclass);
+ERROR: input page is not a GIN entry tree page
+HINT: This appears to be a GIN posting tree page. Please use gin_datapage_items
+-- 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 the 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}
+
+-- reject 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'));
+-[ RECORD 1 ]------+-
+gin_leafpage_items |
+
+SELECT gin_datapage_items(decode(repeat('00', :block_size), 'hex'));
+(0 rows)
+
+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/ginfuncs.c b/contrib/pageinspect/ginfuncs.c
index ebcc2b3db5c..3cd5e4c0e65 100644
--- a/contrib/pageinspect/ginfuncs.c
+++ b/contrib/pageinspect/ginfuncs.c
@@ -11,18 +11,27 @@
#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);
+#define IS_INDEX(r) ((r)->rd_rel->relkind == RELKIND_INDEX)
+#define IS_GIN(r) ((r)->rd_rel->relam == GIN_AM_OID)
Datum
gin_metapage_info(PG_FUNCTION_ARGS)
@@ -175,6 +184,335 @@ 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;
+ StringInfoData buf;
+
+ 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_INDEX(indexRel) || !IS_GIN(indexRel))
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is not a %s index",
+ RelationGetRelationName(indexRel), "GIN")));
+
+ 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 entry tree 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"),
+ errhint("This appears to be a GIN posting tree page. Please use gin_datapage_items")));
+
+ initStringInfo(&buf);
+ maxoff = PageGetMaxOffsetNumber(page);
+
+ tupdesc = RelationGetDescr(indexRel);
+ oneCol = tupdesc->natts == 1;
+
+ for (offset = FirstOffsetNumber;
+ offset <= maxoff;
+ offset = OffsetNumberNext(offset))
+ {
+ 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);
+ if (isnull)
+ ereport(ERROR,
+ (errcode(ERRCODE_INDEX_CORRUPTED),
+ errmsg("invalid gin entry page tuple at offset %d", offset)));
+ }
+ else
+ {
+ TupleDesc tmpTupdesc;
+ Datum res;
+ Form_pg_attribute attr;
+
+ /* Multi-column GIN indexes store 2-attribute tuple on each
+ * page item. First attribute is which heap attribute is stored
+ * as the second value in pair. To display value with proper output
+ * function we need to recreate tuple descriptor on each offset. */
+
+ /* orig tuple reuse is safe */
+
+ res = index_getattr(idxtuple, FirstOffsetNumber, tupdesc,
+ &isnull);
+
+ /* we do not expect null for first attr in multi-column GIN */
+ if (isnull)
+ ereport(ERROR,
+ (errcode(ERRCODE_INDEX_CORRUPTED),
+ errmsg("invalid gin entry page tuple at offset %d", offset)));
+
+ 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);
+
+ FreeTupleDesc(tmpTupdesc);
+ }
+
+ 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);
+ resetStringInfo(&buf);
+
+ 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);
+ }
+
+ index_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/meson.build b/contrib/pageinspect/meson.build
index c43ea400a4d..2f333635838 100644
--- a/contrib/pageinspect/meson.build
+++ b/contrib/pageinspect/meson.build
@@ -38,6 +38,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,
)
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..ef6fa87e0f4
--- /dev/null
+++ b/contrib/pageinspect/pageinspect--1.13--1.14.sql
@@ -0,0 +1,27 @@
+/* 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,
+ 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..1a9eaebeebc 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
@@ -18,6 +24,18 @@ FROM gin_leafpage_items(get_raw_page('test1_y_idx',
(pg_relation_size('test1_y_idx') /
current_setting('block_size')::bigint)::int - 1));
+-- Now test posting tree non-leaf page.
+-- This requires inserting many tuples on a single leaf page to trigger page split.
+
+CREATE TABLE test_data_page(i INT[]);
+CREATE INDEX test_data_page_i_idx ON test_data_page USING gin(i) WITH (fastupdate = off);
+
+INSERT INTO test_data_page SELECT ARRAY[1] FROM generate_series(1, 10000);
+
+-- For this index, block 0 is metapage, block 1 is entry tree, block 2 is
+-- posting tree non-leaf page and block 3 & 4 are compressed data leaf pages.
+SELECT * FROM gin_datapage_items(get_raw_page('test_data_page_i_idx', 2));
+
-- Failure with various modes.
-- Suppress the DETAIL message, to allow the tests to work across various
-- page sizes and architectures.
@@ -32,9 +50,21 @@ 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 unsupported page types in gin_entrypage_items.
+SELECT * FROM gin_entrypage_items(get_raw_page('test2_y_z_idx', 0), 'test2_y_z_idx'::regclass);
+-- Check the error message for the internal posting tree page.
+SELECT * FROM gin_entrypage_items(get_raw_page('test_data_page_i_idx', 2), 'test_data_page_i_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 the new page is fast-list.
+SELECT * FROM gin_page_opaque_info(get_raw_page('test3_y_z_idx', 2));
+-- reject 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'));
+SELECT gin_datapage_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'));
diff --git a/doc/src/sgml/pageinspect.sgml b/doc/src/sgml/pageinspect.sgml
index 3a113439e1d..4ed8826e92e 100644
--- a/doc/src/sgml/pageinspect.sgml
+++ b/doc/src/sgml/pageinspect.sgml
@@ -714,6 +714,60 @@ test=# SELECT first_tid, nbytes, tids[0:5] AS some_tids
(170,30) | 376 | {"(170,30)","(170,31)","(170,32)","(170,33)","(170,34)"}
(173,44) | 197 | {"(173,44)","(173,45)","(173,46)","(173,47)","(173,48)"}
(7 rows)
+</screen>
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>
+ <function>gin_entrypage_items(page bytea, reloid oid) returns setof record</function>
+ <indexterm>
+ <primary>gin_entrypage_items</primary>
+ </indexterm>
+ </term>
+
+ <listitem>
+ <para>
+ <function>gin_entrypage_items</function> returns information about
+ the data stored in a entry tree <acronym>GIN</acronym> page. For example:
+<screen>
+test=# select * from gin_entrypage_items(get_raw_page('gin_test_idx',
+1), 'gin_test_idx'::regclass);
+ itemoffset | downlink | tids | keys
+------------+----------+------+------------------------------------
+ 1 | (3,0) | {} | i=113
+ 2 | (5,0) | {} | j=34173cb38f07f89ddbebc2ac9128303f
+ 3 | (2,0) | {} | j=a0a080f42e6f13b3a2df133f073095dd
+ 4 | (4,0) | {} | j=fc490ca45c00b1249bbe3554a4fdf6fb
+(4 rows)
+</screen>
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>
+ <function>gin_datapage_items(page bytea) returns setof record</function>
+ <indexterm>
+ <primary>gin_datapage_items</primary>
+ </indexterm>
+ </term>
+
+ <listitem>
+ <para>
+ <function>gin_datapage_items</function> returns information about
+ the data stored in a posting tree <acronym>GIN</acronym> internal page. For example:
+<screen>
+test=# select * from gin_datapage_items(get_raw_page('gin_test_idx',
+43));
+ itemoffset | downlink | item_tid
+------------+----------+----------
+ 1 | 124 | (162,12)
+ 2 | 123 | (314,37)
+ 3 | 251 | (467,23)
+ 4 | 373 | (0,0)
+(4 rows)
</screen>
</para>
</listitem>
--
2.43.0
On Mon, 5 Jan 2026 at 13:39, Andrey Borodin <x4mmm@yandex-team.ru> wrote:
On 4 Jan 2026, at 00:25, Kirill Reshke <reshkekirill@gmail.com> wrote:
PFA v6
Would it be theoretically possible to unite functions for different GIN page types?
e.g. merge gin_entrypage_items + gin_datapage_items -> gin_tree_items? Or is it an awkward API?
For this, I borrowed this design from HASH and BRIN pageinspect
implementation. For them, we have one function-per-page-type. So,
maybe we can have dynamic schema here, but I don't see this as an
improvement to design.
--
Best regards,
Kirill Reshke
On Tue, 6 Jan 2026 at 21:28, Kirill Reshke <reshkekirill@gmail.com> wrote:
On Mon, 5 Jan 2026 at 13:39, Andrey Borodin <x4mmm@yandex-team.ru> wrote:
On 4 Jan 2026, at 00:25, Kirill Reshke <reshkekirill@gmail.com> wrote:
PFA v6
Would it be theoretically possible to unite functions for different GIN page types?
e.g. merge gin_entrypage_items + gin_datapage_items -> gin_tree_items? Or is it an awkward API?For this, I borrowed this design from HASH and BRIN pageinspect
implementation. For them, we have one function-per-page-type. So,
maybe we can have dynamic schema here, but I don't see this as an
improvement to design.--
Best regards,
Kirill Reshke
CF bot did like trailing whitespace in regression output files,
posting v8 with this issue fixed.
--
Best regards,
Kirill Reshke
Attachments:
v8-0001-GIN-pageinspect-support-for-entry-tree-and-postin.patchapplication/octet-stream; name=v8-0001-GIN-pageinspect-support-for-entry-tree-and-postin.patchDownload
From dc870e8c17a1971267dd20f5bc92fc6b33abe137 Mon Sep 17 00:00:00 2001
From: reshke <reshke@double.cloud>
Date: Mon, 13 Oct 2025 20:14:26 +0000
Subject: [PATCH v8] GIN pageinspect support for entry tree and posting tree
internal pages
This patch provides new version for pageinspect contrib module including
two new functions:
* gin_entrypage_items.
* gin_datapage_items.
These two functions can be used to examine GIN entry tree and posting
tree pages. Namely, gin_entrypage_items can be used of both leaf and
non-leaf entry tree pages. gin_datapage_items is provided in pairs with
already-existing gin_leafpage_items to examine non-leaf posting tree
pages.
We keep the different functions here mainly because of different GIN
pages layoff.
Note that fast-list pages are out of scope of this patch.
Reviewed-by: Andrey Borodin x4mmm@yandex-team.ru
Reviewed-by: Roman Khapov rkhapov@yandex-team.ru
---
contrib/pageinspect/Makefile | 2 +-
contrib/pageinspect/expected/gin.out | 84 ++++-
contrib/pageinspect/expected/gin_1.out | 151 ++++++++
contrib/pageinspect/ginfuncs.c | 338 ++++++++++++++++++
contrib/pageinspect/meson.build | 1 +
.../pageinspect/pageinspect--1.13--1.14.sql | 27 ++
contrib/pageinspect/pageinspect.control | 2 +-
contrib/pageinspect/sql/gin.sql | 34 +-
doc/src/sgml/pageinspect.sgml | 54 +++
9 files changed, 687 insertions(+), 6 deletions(-)
create mode 100644 contrib/pageinspect/expected/gin_1.out
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..702e9598594 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',
@@ -35,6 +76,23 @@ FROM gin_leafpage_items(get_raw_page('test1_y_idx',
-[ RECORD 1 ]
?column? | t
+-- Now test posting tree non-leaf page.
+-- This requires inserting many tuples on a single leaf page to trigger page split.
+CREATE TABLE test_data_page(i INT[]);
+CREATE INDEX test_data_page_i_idx ON test_data_page USING gin(i) WITH (fastupdate = off);
+INSERT INTO test_data_page SELECT ARRAY[1] FROM generate_series(1, 10000);
+-- For this index, block 0 is metapage, block 1 is entry tree, block 2 is
+-- posting tree non-leaf page and block 3 & 4 are compressed data leaf pages.
+SELECT * FROM gin_datapage_items(get_raw_page('test_data_page_i_idx', 2));
+-[ RECORD 1 ]-------
+itemoffset | 1
+downlink | 4
+item_tid | (44,83)
+-[ RECORD 2 ]-------
+itemoffset | 2
+downlink | 3
+item_tid | (0,0)
+
-- Failure with various modes.
-- Suppress the DETAIL message, to allow the tests to work across various
-- page sizes and architectures.
@@ -54,12 +112,34 @@ 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 unsupported 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
+-- Check the error message for the internal posting tree page.
+SELECT * FROM gin_entrypage_items(get_raw_page('test_data_page_i_idx', 2), 'test_data_page_i_idx'::regclass);
+ERROR: input page is not a GIN entry tree page
+HINT: This appears to be a GIN posting tree page. Please use gin_datapage_items
+-- 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 the 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}
+
+-- reject 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'));
-[ RECORD 1 ]------+-
gin_leafpage_items |
+SELECT gin_datapage_items(decode(repeat('00', :block_size), 'hex'));
+(0 rows)
+
SELECT gin_metapage_info(decode(repeat('00', :block_size), 'hex'));
-[ RECORD 1 ]-----+-
gin_metapage_info |
diff --git a/contrib/pageinspect/expected/gin_1.out b/contrib/pageinspect/expected/gin_1.out
new file mode 100644
index 00000000000..52ddd2924ae
--- /dev/null
+++ b/contrib/pageinspect/expected/gin_1.out
@@ -0,0 +1,151 @@
+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 ]----+-----------
+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
+SELECT * FROM gin_entrypage_items(get_raw_page('test1_y_idx', 1), 'test1_y_idx'::regclass);
+-[ RECORD 1 ]--------------
+itemoffset | 1
+downlink | (2147483660,1)
+tids | {"(0,1)"}
+keys | y=11
+-[ RECORD 2 ]--------------
+itemoffset | 2
+downlink | (2147483660,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 | (2147483660,1)
+tids | {"(0,1)"}
+keys | y=11
+-[ RECORD 2 ]--------------
+itemoffset | 2
+downlink | (2147483660,1)
+tids | {"(0,1)"}
+keys | y=111
+-[ RECORD 3 ]--------------
+itemoffset | 3
+downlink | (2147483660,1)
+tids | {"(0,1)"}
+keys | z=a
+-[ RECORD 4 ]--------------
+itemoffset | 4
+downlink | (2147483660,1)
+tids | {"(0,1)"}
+keys | z=b
+-[ RECORD 5 ]--------------
+itemoffset | 5
+downlink | (2147483660,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',
+ (pg_relation_size('test1_y_idx') /
+ current_setting('block_size')::bigint)::int - 1));
+-[ RECORD 1 ]
+?column? | t
+
+-- Now test posting tree non-leaf page.
+-- This requires inserting many tuples on a single leaf page to trigger page split.
+CREATE TABLE test_data_page(i INT[]);
+CREATE INDEX test_data_page_i_idx ON test_data_page USING gin(i) WITH (fastupdate = off);
+INSERT INTO test_data_page SELECT ARRAY[1] FROM generate_series(1, 10000);
+-- For this index, block 0 is metapage, block 1 is entry tree, block 2 is
+-- posting tree non-leaf page and block 3 & 4 are compressed data leaf pages.
+SELECT * FROM gin_datapage_items(get_raw_page('test_data_page_i_idx', 2));
+-[ RECORD 1 ]-------
+itemoffset | 1
+downlink | 4
+item_tid | (44,83)
+-[ RECORD 2 ]-------
+itemoffset | 2
+downlink | 3
+item_tid | (0,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 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
+-- Reject unsupported 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
+-- Check the error message for the internal posting tree page.
+SELECT * FROM gin_entrypage_items(get_raw_page('test_data_page_i_idx', 2), 'test_data_page_i_idx'::regclass);
+ERROR: input page is not a GIN entry tree page
+HINT: This appears to be a GIN posting tree page. Please use gin_datapage_items
+-- 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 the 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}
+
+-- reject 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'));
+-[ RECORD 1 ]------+-
+gin_leafpage_items |
+
+SELECT gin_datapage_items(decode(repeat('00', :block_size), 'hex'));
+(0 rows)
+
+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/ginfuncs.c b/contrib/pageinspect/ginfuncs.c
index ebcc2b3db5c..3cd5e4c0e65 100644
--- a/contrib/pageinspect/ginfuncs.c
+++ b/contrib/pageinspect/ginfuncs.c
@@ -11,18 +11,27 @@
#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);
+#define IS_INDEX(r) ((r)->rd_rel->relkind == RELKIND_INDEX)
+#define IS_GIN(r) ((r)->rd_rel->relam == GIN_AM_OID)
Datum
gin_metapage_info(PG_FUNCTION_ARGS)
@@ -175,6 +184,335 @@ 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;
+ StringInfoData buf;
+
+ 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_INDEX(indexRel) || !IS_GIN(indexRel))
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is not a %s index",
+ RelationGetRelationName(indexRel), "GIN")));
+
+ 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 entry tree 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"),
+ errhint("This appears to be a GIN posting tree page. Please use gin_datapage_items")));
+
+ initStringInfo(&buf);
+ maxoff = PageGetMaxOffsetNumber(page);
+
+ tupdesc = RelationGetDescr(indexRel);
+ oneCol = tupdesc->natts == 1;
+
+ for (offset = FirstOffsetNumber;
+ offset <= maxoff;
+ offset = OffsetNumberNext(offset))
+ {
+ 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);
+ if (isnull)
+ ereport(ERROR,
+ (errcode(ERRCODE_INDEX_CORRUPTED),
+ errmsg("invalid gin entry page tuple at offset %d", offset)));
+ }
+ else
+ {
+ TupleDesc tmpTupdesc;
+ Datum res;
+ Form_pg_attribute attr;
+
+ /* Multi-column GIN indexes store 2-attribute tuple on each
+ * page item. First attribute is which heap attribute is stored
+ * as the second value in pair. To display value with proper output
+ * function we need to recreate tuple descriptor on each offset. */
+
+ /* orig tuple reuse is safe */
+
+ res = index_getattr(idxtuple, FirstOffsetNumber, tupdesc,
+ &isnull);
+
+ /* we do not expect null for first attr in multi-column GIN */
+ if (isnull)
+ ereport(ERROR,
+ (errcode(ERRCODE_INDEX_CORRUPTED),
+ errmsg("invalid gin entry page tuple at offset %d", offset)));
+
+ 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);
+
+ FreeTupleDesc(tmpTupdesc);
+ }
+
+ 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);
+ resetStringInfo(&buf);
+
+ 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);
+ }
+
+ index_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/meson.build b/contrib/pageinspect/meson.build
index c43ea400a4d..2f333635838 100644
--- a/contrib/pageinspect/meson.build
+++ b/contrib/pageinspect/meson.build
@@ -38,6 +38,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,
)
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..ef6fa87e0f4
--- /dev/null
+++ b/contrib/pageinspect/pageinspect--1.13--1.14.sql
@@ -0,0 +1,27 @@
+/* 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,
+ 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..1a9eaebeebc 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
@@ -18,6 +24,18 @@ FROM gin_leafpage_items(get_raw_page('test1_y_idx',
(pg_relation_size('test1_y_idx') /
current_setting('block_size')::bigint)::int - 1));
+-- Now test posting tree non-leaf page.
+-- This requires inserting many tuples on a single leaf page to trigger page split.
+
+CREATE TABLE test_data_page(i INT[]);
+CREATE INDEX test_data_page_i_idx ON test_data_page USING gin(i) WITH (fastupdate = off);
+
+INSERT INTO test_data_page SELECT ARRAY[1] FROM generate_series(1, 10000);
+
+-- For this index, block 0 is metapage, block 1 is entry tree, block 2 is
+-- posting tree non-leaf page and block 3 & 4 are compressed data leaf pages.
+SELECT * FROM gin_datapage_items(get_raw_page('test_data_page_i_idx', 2));
+
-- Failure with various modes.
-- Suppress the DETAIL message, to allow the tests to work across various
-- page sizes and architectures.
@@ -32,9 +50,21 @@ 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 unsupported page types in gin_entrypage_items.
+SELECT * FROM gin_entrypage_items(get_raw_page('test2_y_z_idx', 0), 'test2_y_z_idx'::regclass);
+-- Check the error message for the internal posting tree page.
+SELECT * FROM gin_entrypage_items(get_raw_page('test_data_page_i_idx', 2), 'test_data_page_i_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 the new page is fast-list.
+SELECT * FROM gin_page_opaque_info(get_raw_page('test3_y_z_idx', 2));
+-- reject 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'));
+SELECT gin_datapage_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'));
diff --git a/doc/src/sgml/pageinspect.sgml b/doc/src/sgml/pageinspect.sgml
index 3a113439e1d..4ed8826e92e 100644
--- a/doc/src/sgml/pageinspect.sgml
+++ b/doc/src/sgml/pageinspect.sgml
@@ -714,6 +714,60 @@ test=# SELECT first_tid, nbytes, tids[0:5] AS some_tids
(170,30) | 376 | {"(170,30)","(170,31)","(170,32)","(170,33)","(170,34)"}
(173,44) | 197 | {"(173,44)","(173,45)","(173,46)","(173,47)","(173,48)"}
(7 rows)
+</screen>
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>
+ <function>gin_entrypage_items(page bytea, reloid oid) returns setof record</function>
+ <indexterm>
+ <primary>gin_entrypage_items</primary>
+ </indexterm>
+ </term>
+
+ <listitem>
+ <para>
+ <function>gin_entrypage_items</function> returns information about
+ the data stored in a entry tree <acronym>GIN</acronym> page. For example:
+<screen>
+test=# select * from gin_entrypage_items(get_raw_page('gin_test_idx',
+1), 'gin_test_idx'::regclass);
+ itemoffset | downlink | tids | keys
+------------+----------+------+------------------------------------
+ 1 | (3,0) | {} | i=113
+ 2 | (5,0) | {} | j=34173cb38f07f89ddbebc2ac9128303f
+ 3 | (2,0) | {} | j=a0a080f42e6f13b3a2df133f073095dd
+ 4 | (4,0) | {} | j=fc490ca45c00b1249bbe3554a4fdf6fb
+(4 rows)
+</screen>
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>
+ <function>gin_datapage_items(page bytea) returns setof record</function>
+ <indexterm>
+ <primary>gin_datapage_items</primary>
+ </indexterm>
+ </term>
+
+ <listitem>
+ <para>
+ <function>gin_datapage_items</function> returns information about
+ the data stored in a posting tree <acronym>GIN</acronym> internal page. For example:
+<screen>
+test=# select * from gin_datapage_items(get_raw_page('gin_test_idx',
+43));
+ itemoffset | downlink | item_tid
+------------+----------+----------
+ 1 | 124 | (162,12)
+ 2 | 123 | (314,37)
+ 3 | 251 | (467,23)
+ 4 | 373 | (0,0)
+(4 rows)
</screen>
</para>
</listitem>
--
2.43.0
On 06.01.26 17:44, Kirill Reshke wrote:
On Tue, 6 Jan 2026 at 21:28, Kirill Reshke <reshkekirill@gmail.com> wrote:
On Mon, 5 Jan 2026 at 13:39, Andrey Borodin <x4mmm@yandex-team.ru> wrote:
On 4 Jan 2026, at 00:25, Kirill Reshke <reshkekirill@gmail.com> wrote:
PFA v6
Would it be theoretically possible to unite functions for different GIN page types?
e.g. merge gin_entrypage_items + gin_datapage_items -> gin_tree_items? Or is it an awkward API?For this, I borrowed this design from HASH and BRIN pageinspect
implementation. For them, we have one function-per-page-type. So,
maybe we can have dynamic schema here, but I don't see this as an
improvement to design.--
Best regards,
Kirill ReshkeCF bot did like trailing whitespace in regression output files,
posting v8 with this issue fixed.
I noticed that the newly added C code could use a bit of modernization.
See attached patch for some suggestions. I only treated the new
gin_entrypage_items(); the new gin_datapage_items() could use mostly the
same changes. One of the main changes is to push many local variables
to smaller scopes. Also some changes in ereport parentheses style,
palloc style, some whitespace changes. (The whole patch also needs
pgindent treatment.)
Attachments:
0001-some-code-cleanup.patch.nocfbottext/plain; charset=UTF-8; name=0001-some-code-cleanup.patch.nocfbotDownload
From b424062035a43c894cf6d11b854a917be0cad6d8 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Wed, 7 Jan 2026 17:37:05 +0100
Subject: [PATCH] some code cleanup
---
contrib/pageinspect/ginfuncs.c | 95 ++++++++++++++++------------------
1 file changed, 44 insertions(+), 51 deletions(-)
diff --git a/contrib/pageinspect/ginfuncs.c b/contrib/pageinspect/ginfuncs.c
index 3cd5e4c0e65..4294b640a9a 100644
--- a/contrib/pageinspect/ginfuncs.c
+++ b/contrib/pageinspect/ginfuncs.c
@@ -191,17 +191,16 @@ gin_entrypage_items(PG_FUNCTION_ARGS)
Oid indexRelid = PG_GETARG_OID(1);
ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
Relation indexRel;
- OffsetNumber maxoff, offset;
+ OffsetNumber maxoff;
TupleDesc tupdesc;
- bool oneCol;
Page page;
GinPageOpaque opaq;
StringInfoData buf;
if (!superuser())
ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to use raw page functions")));
+ errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions"));
InitMaterializedSRF(fcinfo, 0);
@@ -210,9 +209,9 @@ gin_entrypage_items(PG_FUNCTION_ARGS)
if (!IS_INDEX(indexRel) || !IS_GIN(indexRel))
ereport(ERROR,
- (errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("\"%s\" is not a %s index",
- RelationGetRelationName(indexRel), "GIN")));
+ errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is not a %s index",
+ RelationGetRelationName(indexRel), "GIN"));
page = get_page_from_raw(raw_page);
@@ -224,58 +223,46 @@ gin_entrypage_items(PG_FUNCTION_ARGS)
if (PageGetSpecialSize(page) != MAXALIGN(sizeof(GinPageOpaqueData)))
ereport(ERROR,
- (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
- errmsg("input page is not a valid GIN entry tree page"),
- errdetail("Expected special size %d, got %d.",
- (int) MAXALIGN(sizeof(GinPageOpaqueData)),
- (int) PageGetSpecialSize(page))));
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("input page is not a valid GIN entry tree page"),
+ errdetail("Expected special size %d, got %d.",
+ (int) MAXALIGN(sizeof(GinPageOpaqueData)),
+ 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")));
+ 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")));
+ 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"),
- errhint("This appears to be a GIN posting tree page. Please use gin_datapage_items")));
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("input page is not a GIN entry tree page"),
+ errhint("This appears to be a GIN posting tree page. Please use gin_datapage_items."));
initStringInfo(&buf);
maxoff = PageGetMaxOffsetNumber(page);
tupdesc = RelationGetDescr(indexRel);
- oneCol = tupdesc->natts == 1;
- for (offset = FirstOffsetNumber;
+ for (OffsetNumber offset = FirstOffsetNumber;
offset <= maxoff;
offset = OffsetNumberNext(offset))
{
OffsetNumber indAtt;
Datum values[4];
- bool nulls[4];
- int ndecoded, i;
- Datum *tids_datum;
- ItemPointer items_orig;
- bool free_items_orig;
+ bool nulls[4] = {0};
Datum attrVal;
- Oid foutoid;
- bool typisvarlena;
- Oid typoid;
- char* value;
- bool nq;
- char* tmp;
bool isnull;
IndexTuple idxtuple;
ItemId iid = PageGetItemId(page, offset);
@@ -285,20 +272,17 @@ gin_entrypage_items(PG_FUNCTION_ARGS)
idxtuple = (IndexTuple) PageGetItem(page, iid);
- memset(nulls, 0, sizeof(nulls));
-
values[0] = UInt16GetDatum(offset);
- if (oneCol)
+ if (tupdesc->natts == 1)
{
indAtt = FirstOffsetNumber;
/* here we can safely reuse pg_class's tuple descriptor. */
- attrVal = index_getattr(idxtuple, FirstOffsetNumber, tupdesc,
- &isnull);
+ attrVal = index_getattr(idxtuple, FirstOffsetNumber, tupdesc, &isnull);
if (isnull)
ereport(ERROR,
- (errcode(ERRCODE_INDEX_CORRUPTED),
- errmsg("invalid gin entry page tuple at offset %d", offset)));
+ errcode(ERRCODE_INDEX_CORRUPTED),
+ errmsg("invalid gin entry page tuple at offset %d", offset));
}
else
{
@@ -313,14 +297,13 @@ gin_entrypage_items(PG_FUNCTION_ARGS)
/* orig tuple reuse is safe */
- res = index_getattr(idxtuple, FirstOffsetNumber, tupdesc,
- &isnull);
+ res = index_getattr(idxtuple, FirstOffsetNumber, tupdesc, &isnull);
/* we do not expect null for first attr in multi-column GIN */
if (isnull)
ereport(ERROR,
- (errcode(ERRCODE_INDEX_CORRUPTED),
- errmsg("invalid gin entry page tuple at offset %d", offset)));
+ errcode(ERRCODE_INDEX_CORRUPTED),
+ errmsg("invalid gin entry page tuple at offset %d", offset));
indAtt = DatumGetUInt16(res);
@@ -346,16 +329,22 @@ gin_entrypage_items(PG_FUNCTION_ARGS)
appendStringInfo(&buf, "%s=", quote_identifier(TupleDescAttr(tupdesc, indAtt - 1)->attname.data));
- if (!isnull) {
+ if (!isnull)
+ {
+ Oid foutoid;
+ bool typisvarlena;
+ Oid typoid;
+ char* value;
+ bool nq;
+
/* 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++)
+ for (const char *tmp = value; *tmp; tmp++)
{
char ch = *tmp;
@@ -371,7 +360,7 @@ gin_entrypage_items(PG_FUNCTION_ARGS)
/* And emit the string */
if (nq)
appendStringInfoCharMacro(&buf, '"');
- for (tmp = value; *tmp; tmp++)
+ for (const char *tmp = value; *tmp; tmp++)
{
char ch = *tmp;
@@ -387,7 +376,6 @@ gin_entrypage_items(PG_FUNCTION_ARGS)
appendStringInfo(&buf, "NULL");
}
-
values[3] = CStringGetTextDatum(buf.data);
resetStringInfo(&buf);
@@ -398,6 +386,11 @@ gin_entrypage_items(PG_FUNCTION_ARGS)
}
else
{
+ int ndecoded;
+ Datum *tids_datum;
+ ItemPointer items_orig;
+ bool free_items_orig;
+
values[1] = ItemPointerGetDatum(&idxtuple->t_tid);
/* Get list of item pointers from the tuple. */
if (GinItupIsCompressed(idxtuple))
@@ -412,8 +405,8 @@ gin_entrypage_items(PG_FUNCTION_ARGS)
free_items_orig = false;
}
- tids_datum = (Datum *) palloc(ndecoded * sizeof(Datum));
- for (i = 0; i < ndecoded; i++)
+ tids_datum = palloc_array(Datum, ndecoded);
+ for (int i = 0; i < ndecoded; i++)
tids_datum[i] = ItemPointerGetDatum(&items_orig[i]);
values[2] = PointerGetDatum(construct_array_builtin(tids_datum, ndecoded, TIDOID));
--
2.52.0
On Wed, 7 Jan 2026 at 21:41, Peter Eisentraut <peter@eisentraut.org> wrote:
On 06.01.26 17:44, Kirill Reshke wrote:
On Tue, 6 Jan 2026 at 21:28, Kirill Reshke <reshkekirill@gmail.com> wrote:
On Mon, 5 Jan 2026 at 13:39, Andrey Borodin <x4mmm@yandex-team.ru> wrote:
On 4 Jan 2026, at 00:25, Kirill Reshke <reshkekirill@gmail.com> wrote:
PFA v6
Would it be theoretically possible to unite functions for different GIN page types?
e.g. merge gin_entrypage_items + gin_datapage_items -> gin_tree_items? Or is it an awkward API?For this, I borrowed this design from HASH and BRIN pageinspect
implementation. For them, we have one function-per-page-type. So,
maybe we can have dynamic schema here, but I don't see this as an
improvement to design.--
Best regards,
Kirill ReshkeCF bot did like trailing whitespace in regression output files,
posting v8 with this issue fixed.I noticed that the newly added C code could use a bit of modernization.
See attached patch for some suggestions. I only treated the new
gin_entrypage_items(); the new gin_datapage_items() could use mostly the
same changes. One of the main changes is to push many local variables
to smaller scopes. Also some changes in ereport parentheses style,
palloc style, some whitespace changes. (The whole patch also needs
pgindent treatment.)
Hi!
Thank you. I included your changes in v9, and also did a similar
cleanup in gin_datapage_items().
For pg_ident run sake, and for general benefit, there is also v9-0001
which does cleanups similar to your changes for already-existing
functions
like gin_leafpage_items etc.
--
Best regards,
Kirill Reshke
Attachments:
v9-0002-GIN-pageinspect-support-for-entry-tree-and-postin.patchapplication/octet-stream; name=v9-0002-GIN-pageinspect-support-for-entry-tree-and-postin.patchDownload
From c34fc7c81214f16530881e9679c0a6281681c243 Mon Sep 17 00:00:00 2001
From: reshke <reshke@double.cloud>
Date: Mon, 13 Oct 2025 20:14:26 +0000
Subject: [PATCH v9 2/2] GIN pageinspect support for entry tree and posting
tree internal pages
This patch provides new version for pageinspect contrib module including
two new functions:
* gin_entrypage_items.
* gin_datapage_items.
These two functions can be used to examine GIN entry tree and posting
tree pages. Namely, gin_entrypage_items can be used of both leaf and
non-leaf entry tree pages. gin_datapage_items is provided in pairs with
already-existing gin_leafpage_items to examine non-leaf posting tree
pages.
We keep the different functions here mainly because of different GIN
pages layoff.
Note that fast-list pages are out of scope of this patch.
Co-authored-by: Peter Eisentraut <peter@eisentraut.org>
Reviewed-by: Andrey Borodin x4mmm@yandex-team.ru
Reviewed-by: Roman Khapov rkhapov@yandex-team.ru
some code cleanup
---
contrib/pageinspect/Makefile | 2 +-
contrib/pageinspect/expected/gin.out | 84 ++++-
contrib/pageinspect/expected/gin_1.out | 151 ++++++++
contrib/pageinspect/ginfuncs.c | 328 ++++++++++++++++++
contrib/pageinspect/meson.build | 1 +
.../pageinspect/pageinspect--1.13--1.14.sql | 27 ++
contrib/pageinspect/pageinspect.control | 2 +-
contrib/pageinspect/sql/gin.sql | 34 +-
doc/src/sgml/pageinspect.sgml | 54 +++
9 files changed, 677 insertions(+), 6 deletions(-)
create mode 100644 contrib/pageinspect/expected/gin_1.out
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..702e9598594 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',
@@ -35,6 +76,23 @@ FROM gin_leafpage_items(get_raw_page('test1_y_idx',
-[ RECORD 1 ]
?column? | t
+-- Now test posting tree non-leaf page.
+-- This requires inserting many tuples on a single leaf page to trigger page split.
+CREATE TABLE test_data_page(i INT[]);
+CREATE INDEX test_data_page_i_idx ON test_data_page USING gin(i) WITH (fastupdate = off);
+INSERT INTO test_data_page SELECT ARRAY[1] FROM generate_series(1, 10000);
+-- For this index, block 0 is metapage, block 1 is entry tree, block 2 is
+-- posting tree non-leaf page and block 3 & 4 are compressed data leaf pages.
+SELECT * FROM gin_datapage_items(get_raw_page('test_data_page_i_idx', 2));
+-[ RECORD 1 ]-------
+itemoffset | 1
+downlink | 4
+item_tid | (44,83)
+-[ RECORD 2 ]-------
+itemoffset | 2
+downlink | 3
+item_tid | (0,0)
+
-- Failure with various modes.
-- Suppress the DETAIL message, to allow the tests to work across various
-- page sizes and architectures.
@@ -54,12 +112,34 @@ 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 unsupported 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
+-- Check the error message for the internal posting tree page.
+SELECT * FROM gin_entrypage_items(get_raw_page('test_data_page_i_idx', 2), 'test_data_page_i_idx'::regclass);
+ERROR: input page is not a GIN entry tree page
+HINT: This appears to be a GIN posting tree page. Please use gin_datapage_items
+-- 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 the 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}
+
+-- reject 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'));
-[ RECORD 1 ]------+-
gin_leafpage_items |
+SELECT gin_datapage_items(decode(repeat('00', :block_size), 'hex'));
+(0 rows)
+
SELECT gin_metapage_info(decode(repeat('00', :block_size), 'hex'));
-[ RECORD 1 ]-----+-
gin_metapage_info |
diff --git a/contrib/pageinspect/expected/gin_1.out b/contrib/pageinspect/expected/gin_1.out
new file mode 100644
index 00000000000..b9257e1840a
--- /dev/null
+++ b/contrib/pageinspect/expected/gin_1.out
@@ -0,0 +1,151 @@
+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 ]----+-----------
+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
+SELECT * FROM gin_entrypage_items(get_raw_page('test1_y_idx', 1), 'test1_y_idx'::regclass);
+-[ RECORD 1 ]--------------
+itemoffset | 1
+downlink | (2147483660,1)
+tids | {"(0,1)"}
+keys | y=11
+-[ RECORD 2 ]--------------
+itemoffset | 2
+downlink | (2147483660,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 | (2147483660,1)
+tids | {"(0,1)"}
+keys | z=a
+-[ RECORD 4 ]--------------
+itemoffset | 4
+downlink | (2147483660,1)
+tids | {"(0,1)"}
+keys | z=b
+-[ RECORD 5 ]--------------
+itemoffset | 5
+downlink | (2147483660,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',
+ (pg_relation_size('test1_y_idx') /
+ current_setting('block_size')::bigint)::int - 1));
+-[ RECORD 1 ]
+?column? | t
+
+-- Now test posting tree non-leaf page.
+-- This requires inserting many tuples on a single leaf page to trigger page split.
+CREATE TABLE test_data_page(i INT[]);
+CREATE INDEX test_data_page_i_idx ON test_data_page USING gin(i) WITH (fastupdate = off);
+INSERT INTO test_data_page SELECT ARRAY[1] FROM generate_series(1, 10000);
+-- For this index, block 0 is metapage, block 1 is entry tree, block 2 is
+-- posting tree non-leaf page and block 3 & 4 are compressed data leaf pages.
+SELECT * FROM gin_datapage_items(get_raw_page('test_data_page_i_idx', 2));
+-[ RECORD 1 ]-------
+itemoffset | 1
+downlink | 4
+item_tid | (41,125)
+-[ RECORD 2 ]--------
+itemoffset | 2
+downlink | 3
+item_tid | (0,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 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
+-- Reject unsupported 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
+-- Check the error message for the internal posting tree page.
+SELECT * FROM gin_entrypage_items(get_raw_page('test_data_page_i_idx', 2), 'test_data_page_i_idx'::regclass);
+ERROR: input page is not a GIN entry tree page
+HINT: This appears to be a GIN posting tree page. Please use gin_datapage_items
+-- 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 the new page is fast-list.
+SELECT * FROM gin_page_opaque_info(get_raw_page('test3_y_z_idx', 2));
+-[ RECORD 1 ]------------------
+rightlink | 3
+maxoff | 136
+flags | {list,list_fullrow}
+
+-- reject 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'));
+-[ RECORD 1 ]------+-
+gin_leafpage_items |
+
+SELECT gin_datapage_items(decode(repeat('00', :block_size), 'hex'));
+(0 rows)
+
+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/ginfuncs.c b/contrib/pageinspect/ginfuncs.c
index ccd2a0582a1..e0621cd2c26 100644
--- a/contrib/pageinspect/ginfuncs.c
+++ b/contrib/pageinspect/ginfuncs.c
@@ -11,18 +11,27 @@
#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);
+#define IS_INDEX(r) ((r)->rd_rel->relkind == RELKIND_INDEX)
+#define IS_GIN(r) ((r)->rd_rel->relam == GIN_AM_OID)
Datum
gin_metapage_info(PG_FUNCTION_ARGS) {
@@ -171,6 +180,325 @@ typedef struct gin_leafpage_items_state {
GinPostingList *lastseg;
} gin_leafpage_items_state;
+/*
+ * gin_entrypage_items
+ *
+ * Allows inspection of contents of an entry tree page.
+ */
+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;
+ TupleDesc tupdesc;
+ Page page;
+ GinPageOpaque opaq;
+ StringInfoData buf;
+
+ 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_INDEX(indexRel) || !IS_GIN(indexRel))
+ ereport(ERROR,
+ errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is not a %s index",
+ RelationGetRelationName(indexRel), "GIN"));
+
+ 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 entry tree page"),
+ errdetail("Expected special size %d, got %d.",
+ (int) MAXALIGN(sizeof(GinPageOpaqueData)),
+ 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"),
+ errhint("This appears to be a GIN posting tree page. Please use gin_datapage_items."));
+
+ initStringInfo(&buf);
+ maxoff = PageGetMaxOffsetNumber(page);
+
+ tupdesc = RelationGetDescr(indexRel);
+
+ for (OffsetNumber offset = FirstOffsetNumber;
+ offset <= maxoff;
+ offset = OffsetNumberNext(offset)) {
+ OffsetNumber indAtt;
+ Datum values[4];
+ bool nulls[4] = {0};
+ Datum attrVal;
+ bool isnull;
+ IndexTuple idxtuple;
+ ItemId iid = PageGetItemId(page, offset);
+
+ if (!ItemIdIsValid(iid))
+ elog(ERROR, "invalid ItemId");
+
+ idxtuple = (IndexTuple) PageGetItem(page, iid);
+
+ values[0] = UInt16GetDatum(offset);
+
+ if (tupdesc->natts == 1) {
+ indAtt = FirstOffsetNumber;
+ /*
+ * here we can safely reuse pg_class's tuple
+ * descriptor.
+ */
+ attrVal = index_getattr(idxtuple, FirstOffsetNumber, tupdesc, &isnull);
+ if (isnull)
+ ereport(ERROR,
+ errcode(ERRCODE_INDEX_CORRUPTED),
+ errmsg("invalid gin entry page tuple at offset %d", offset));
+ } else {
+ TupleDesc tmpTupdesc;
+ Datum res;
+ Form_pg_attribute attr;
+
+ /*
+ * Multi-column GIN indexes store 2-attribute tuple on
+ * each page item. First attribute is which heap
+ * attribute is stored as the second value in pair. To
+ * display value with proper output function we need
+ * to recreate tuple descriptor on each offset.
+ */
+
+ /* orig tuple reuse is safe */
+
+ res = index_getattr(idxtuple, FirstOffsetNumber, tupdesc, &isnull);
+
+ /*
+ * we do not expect null for first attr in
+ * multi-column GIN
+ */
+ if (isnull)
+ ereport(ERROR,
+ errcode(ERRCODE_INDEX_CORRUPTED),
+ errmsg("invalid gin entry page tuple at offset %d", offset));
+
+ 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);
+
+ FreeTupleDesc(tmpTupdesc);
+ }
+
+ appendStringInfo(&buf, "%s=", quote_identifier(TupleDescAttr(tupdesc, indAtt - 1)->attname.data));
+
+ if (!isnull) {
+ Oid foutoid;
+ bool typisvarlena;
+ Oid typoid;
+ char *value;
+ bool nq;
+
+ /* 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 (const char *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 (const char *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);
+ resetStringInfo(&buf);
+
+ if (GinIsPostingTree(idxtuple)) {
+ values[1] = ItemPointerGetDatum(&idxtuple->t_tid);
+ nulls[2] = true;
+ } else {
+ int ndecoded;
+ Datum *tids_datum;
+ ItemPointer items_orig;
+ bool free_items_orig;
+
+ 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 = palloc_array(Datum, ndecoded);
+ for (int 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);
+ }
+
+ index_close(indexRel, AccessShareLock);
+
+ return (Datum) 0;
+}
+
+/*
+ * gin_datapage_items
+ *
+ * Allows inspection of contents of an posting tree non-leaf page.
+ */
+Datum
+gin_datapage_items(PG_FUNCTION_ARGS) {
+ bytea *raw_page = PG_GETARG_BYTEA_P(0);
+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ OffsetNumber maxoff;
+ 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"),
+ errhint("This appears to be a GIN posting leaf tree page. Please use gin_leafpage_items."));
+
+ maxoff = GinPageGetOpaque(page)->maxoff;
+
+ for (OffsetNumber 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) {
bytea *raw_page = PG_GETARG_BYTEA_P(0);
diff --git a/contrib/pageinspect/meson.build b/contrib/pageinspect/meson.build
index c43ea400a4d..2f333635838 100644
--- a/contrib/pageinspect/meson.build
+++ b/contrib/pageinspect/meson.build
@@ -38,6 +38,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,
)
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..ef6fa87e0f4
--- /dev/null
+++ b/contrib/pageinspect/pageinspect--1.13--1.14.sql
@@ -0,0 +1,27 @@
+/* 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,
+ 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..1a9eaebeebc 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
@@ -18,6 +24,18 @@ FROM gin_leafpage_items(get_raw_page('test1_y_idx',
(pg_relation_size('test1_y_idx') /
current_setting('block_size')::bigint)::int - 1));
+-- Now test posting tree non-leaf page.
+-- This requires inserting many tuples on a single leaf page to trigger page split.
+
+CREATE TABLE test_data_page(i INT[]);
+CREATE INDEX test_data_page_i_idx ON test_data_page USING gin(i) WITH (fastupdate = off);
+
+INSERT INTO test_data_page SELECT ARRAY[1] FROM generate_series(1, 10000);
+
+-- For this index, block 0 is metapage, block 1 is entry tree, block 2 is
+-- posting tree non-leaf page and block 3 & 4 are compressed data leaf pages.
+SELECT * FROM gin_datapage_items(get_raw_page('test_data_page_i_idx', 2));
+
-- Failure with various modes.
-- Suppress the DETAIL message, to allow the tests to work across various
-- page sizes and architectures.
@@ -32,9 +50,21 @@ 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 unsupported page types in gin_entrypage_items.
+SELECT * FROM gin_entrypage_items(get_raw_page('test2_y_z_idx', 0), 'test2_y_z_idx'::regclass);
+-- Check the error message for the internal posting tree page.
+SELECT * FROM gin_entrypage_items(get_raw_page('test_data_page_i_idx', 2), 'test_data_page_i_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 the new page is fast-list.
+SELECT * FROM gin_page_opaque_info(get_raw_page('test3_y_z_idx', 2));
+-- reject 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'));
+SELECT gin_datapage_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'));
diff --git a/doc/src/sgml/pageinspect.sgml b/doc/src/sgml/pageinspect.sgml
index 3a113439e1d..4ed8826e92e 100644
--- a/doc/src/sgml/pageinspect.sgml
+++ b/doc/src/sgml/pageinspect.sgml
@@ -714,6 +714,60 @@ test=# SELECT first_tid, nbytes, tids[0:5] AS some_tids
(170,30) | 376 | {"(170,30)","(170,31)","(170,32)","(170,33)","(170,34)"}
(173,44) | 197 | {"(173,44)","(173,45)","(173,46)","(173,47)","(173,48)"}
(7 rows)
+</screen>
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>
+ <function>gin_entrypage_items(page bytea, reloid oid) returns setof record</function>
+ <indexterm>
+ <primary>gin_entrypage_items</primary>
+ </indexterm>
+ </term>
+
+ <listitem>
+ <para>
+ <function>gin_entrypage_items</function> returns information about
+ the data stored in a entry tree <acronym>GIN</acronym> page. For example:
+<screen>
+test=# select * from gin_entrypage_items(get_raw_page('gin_test_idx',
+1), 'gin_test_idx'::regclass);
+ itemoffset | downlink | tids | keys
+------------+----------+------+------------------------------------
+ 1 | (3,0) | {} | i=113
+ 2 | (5,0) | {} | j=34173cb38f07f89ddbebc2ac9128303f
+ 3 | (2,0) | {} | j=a0a080f42e6f13b3a2df133f073095dd
+ 4 | (4,0) | {} | j=fc490ca45c00b1249bbe3554a4fdf6fb
+(4 rows)
+</screen>
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>
+ <function>gin_datapage_items(page bytea) returns setof record</function>
+ <indexterm>
+ <primary>gin_datapage_items</primary>
+ </indexterm>
+ </term>
+
+ <listitem>
+ <para>
+ <function>gin_datapage_items</function> returns information about
+ the data stored in a posting tree <acronym>GIN</acronym> internal page. For example:
+<screen>
+test=# select * from gin_datapage_items(get_raw_page('gin_test_idx',
+43));
+ itemoffset | downlink | item_tid
+------------+----------+----------
+ 1 | 124 | (162,12)
+ 2 | 123 | (314,37)
+ 3 | 251 | (467,23)
+ 4 | 373 | (0,0)
+(4 rows)
</screen>
</para>
</listitem>
--
2.43.0
v9-0001-Prelimitary-cleaunup.patchapplication/octet-stream; name=v9-0001-Prelimitary-cleaunup.patchDownload
From 41f52c04780435aa51bc6e90e959fb75105e9764 Mon Sep 17 00:00:00 2001
From: reshke <reshke@double.cloud>
Date: Wed, 7 Jan 2026 19:38:52 +0000
Subject: [PATCH v9 1/2] Prelimitary cleaunup
This patch fixes whitespace/tab issue enforcing single style
across existing ginfuncs.c code. This patch also switches
palloc to our newly-preferred palloc_array.
Per Peter Eisentraut's patch in thread.
Discussion: https://www.postgresql.org/message-id/CALdSSPiN13n7feQcY0WCmq8jzxjwqhNrt1E%3Dg%3Dg6aZANyE_OoQ%40mail.gmail.com
---
contrib/pageinspect/ginfuncs.c | 135 +++++++++++++++------------------
1 file changed, 63 insertions(+), 72 deletions(-)
diff --git a/contrib/pageinspect/ginfuncs.c b/contrib/pageinspect/ginfuncs.c
index ebcc2b3db5c..ccd2a0582a1 100644
--- a/contrib/pageinspect/ginfuncs.c
+++ b/contrib/pageinspect/ginfuncs.c
@@ -25,21 +25,20 @@ PG_FUNCTION_INFO_V1(gin_leafpage_items);
Datum
-gin_metapage_info(PG_FUNCTION_ARGS)
-{
+gin_metapage_info(PG_FUNCTION_ARGS) {
bytea *raw_page = PG_GETARG_BYTEA_P(0);
- TupleDesc tupdesc;
- Page page;
+ TupleDesc tupdesc;
+ Page page;
GinPageOpaque opaq;
GinMetaPageData *metadata;
- HeapTuple resultTuple;
- Datum values[10];
- bool nulls[10];
+ 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")));
+ errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions"));
page = get_page_from_raw(raw_page);
@@ -48,20 +47,20 @@ gin_metapage_info(PG_FUNCTION_ARGS)
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))));
+ 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)));
+ 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)
@@ -93,23 +92,22 @@ gin_metapage_info(PG_FUNCTION_ARGS)
Datum
-gin_page_opaque_info(PG_FUNCTION_ARGS)
-{
+gin_page_opaque_info(PG_FUNCTION_ARGS) {
bytea *raw_page = PG_GETARG_BYTEA_P(0);
- TupleDesc tupdesc;
- Page page;
+ TupleDesc tupdesc;
+ Page page;
GinPageOpaque opaq;
- HeapTuple resultTuple;
- Datum values[3];
- bool nulls[3];
- Datum flags[16];
- int nflags = 0;
- uint16 flagbits;
+ 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")));
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions")));
page = get_page_from_raw(raw_page);
@@ -118,11 +116,11 @@ gin_page_opaque_info(PG_FUNCTION_ARGS)
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))));
+ 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);
@@ -149,9 +147,8 @@ gin_page_opaque_info(PG_FUNCTION_ARGS)
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)
- {
+ 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));
}
@@ -168,30 +165,27 @@ gin_page_opaque_info(PG_FUNCTION_ARGS)
return HeapTupleGetDatum(resultTuple);
}
-typedef struct gin_leafpage_items_state
-{
- TupleDesc tupd;
+typedef struct gin_leafpage_items_state {
+ TupleDesc tupd;
GinPostingList *seg;
GinPostingList *lastseg;
-} gin_leafpage_items_state;
+} gin_leafpage_items_state;
Datum
-gin_leafpage_items(PG_FUNCTION_ARGS)
-{
+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")));
+ errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions"));
- if (SRF_IS_FIRSTCALL())
- {
- TupleDesc tupdesc;
+ if (SRF_IS_FIRSTCALL()) {
+ TupleDesc tupdesc;
MemoryContext mctx;
- Page page;
+ Page page;
GinPageOpaque opaq;
fctx = SRF_FIRSTCALL_INIT();
@@ -199,28 +193,27 @@ gin_leafpage_items(PG_FUNCTION_ARGS)
page = get_page_from_raw(raw_page);
- if (PageIsNew(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))));
+ 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))));
+ 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_object(gin_leafpage_items_state);
@@ -233,7 +226,7 @@ gin_leafpage_items(PG_FUNCTION_ARGS)
inter_call_data->seg = GinDataLeafPageGetPostingList(page);
inter_call_data->lastseg = (GinPostingList *)
(((char *) inter_call_data->seg) +
- GinDataLeafPageGetPostingListSize(page));
+ GinDataLeafPageGetPostingListSize(page));
fctx->user_fctx = inter_call_data;
@@ -243,15 +236,13 @@ gin_leafpage_items(PG_FUNCTION_ARGS)
fctx = SRF_PERCALL_SETUP();
inter_call_data = fctx->user_fctx;
- if (inter_call_data->seg != inter_call_data->lastseg)
- {
+ 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;
+ HeapTuple resultTuple;
+ Datum result;
+ Datum values[3];
+ bool nulls[3];
+ int ndecoded, i;
ItemPointer tids;
Datum *tids_datum;
@@ -262,7 +253,7 @@ gin_leafpage_items(PG_FUNCTION_ARGS)
/* build an array of decoded item pointers */
tids = ginPostingListDecode(cur, &ndecoded);
- tids_datum = (Datum *) palloc(ndecoded * sizeof(Datum));
+ tids_datum = palloc_array(Datum, ndecoded);
for (i = 0; i < ndecoded; i++)
tids_datum[i] = ItemPointerGetDatum(&tids[i]);
values[2] = PointerGetDatum(construct_array_builtin(tids_datum, ndecoded, TIDOID));
--
2.43.0
On Thu, 8 Jan 2026 at 01:13, Kirill Reshke <reshkekirill@gmail.com> wrote:
On Wed, 7 Jan 2026 at 21:41, Peter Eisentraut <peter@eisentraut.org> wrote:
On 06.01.26 17:44, Kirill Reshke wrote:
On Tue, 6 Jan 2026 at 21:28, Kirill Reshke <reshkekirill@gmail.com> wrote:
On Mon, 5 Jan 2026 at 13:39, Andrey Borodin <x4mmm@yandex-team.ru> wrote:
On 4 Jan 2026, at 00:25, Kirill Reshke <reshkekirill@gmail.com> wrote:
PFA v6
Would it be theoretically possible to unite functions for different GIN page types?
e.g. merge gin_entrypage_items + gin_datapage_items -> gin_tree_items? Or is it an awkward API?For this, I borrowed this design from HASH and BRIN pageinspect
implementation. For them, we have one function-per-page-type. So,
maybe we can have dynamic schema here, but I don't see this as an
improvement to design.--
Best regards,
Kirill ReshkeCF bot did like trailing whitespace in regression output files,
posting v8 with this issue fixed.I noticed that the newly added C code could use a bit of modernization.
See attached patch for some suggestions. I only treated the new
gin_entrypage_items(); the new gin_datapage_items() could use mostly the
same changes. One of the main changes is to push many local variables
to smaller scopes. Also some changes in ereport parentheses style,
palloc style, some whitespace changes. (The whole patch also needs
pgindent treatment.)Hi!
Thank you. I included your changes in v9, and also did a similar
cleanup in gin_datapage_items().
For pg_ident run sake, and for general benefit, there is also v9-0001
which does cleanups similar to your changes for already-existing
functions
like gin_leafpage_items etc.--
Best regards,
Kirill Reshke
PFA v10 with `make check` that passes.
Previous version missed dot at the end of errhint sentence
--
Best regards,
Kirill Reshke
Attachments:
v10-0001-Prelimitary-cleaunup.patchapplication/octet-stream; name=v10-0001-Prelimitary-cleaunup.patchDownload
From 41f52c04780435aa51bc6e90e959fb75105e9764 Mon Sep 17 00:00:00 2001
From: reshke <reshke@double.cloud>
Date: Wed, 7 Jan 2026 19:38:52 +0000
Subject: [PATCH v10 1/2] Prelimitary cleaunup
This patch fixes whitespace/tab issue enforcing single style
across existing ginfuncs.c code. This patch also switches
palloc to our newly-preferred palloc_array.
Per Peter Eisentraut's patch in thread.
Discussion: https://www.postgresql.org/message-id/CALdSSPiN13n7feQcY0WCmq8jzxjwqhNrt1E%3Dg%3Dg6aZANyE_OoQ%40mail.gmail.com
---
contrib/pageinspect/ginfuncs.c | 135 +++++++++++++++------------------
1 file changed, 63 insertions(+), 72 deletions(-)
diff --git a/contrib/pageinspect/ginfuncs.c b/contrib/pageinspect/ginfuncs.c
index ebcc2b3db5c..ccd2a0582a1 100644
--- a/contrib/pageinspect/ginfuncs.c
+++ b/contrib/pageinspect/ginfuncs.c
@@ -25,21 +25,20 @@ PG_FUNCTION_INFO_V1(gin_leafpage_items);
Datum
-gin_metapage_info(PG_FUNCTION_ARGS)
-{
+gin_metapage_info(PG_FUNCTION_ARGS) {
bytea *raw_page = PG_GETARG_BYTEA_P(0);
- TupleDesc tupdesc;
- Page page;
+ TupleDesc tupdesc;
+ Page page;
GinPageOpaque opaq;
GinMetaPageData *metadata;
- HeapTuple resultTuple;
- Datum values[10];
- bool nulls[10];
+ 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")));
+ errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions"));
page = get_page_from_raw(raw_page);
@@ -48,20 +47,20 @@ gin_metapage_info(PG_FUNCTION_ARGS)
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))));
+ 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)));
+ 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)
@@ -93,23 +92,22 @@ gin_metapage_info(PG_FUNCTION_ARGS)
Datum
-gin_page_opaque_info(PG_FUNCTION_ARGS)
-{
+gin_page_opaque_info(PG_FUNCTION_ARGS) {
bytea *raw_page = PG_GETARG_BYTEA_P(0);
- TupleDesc tupdesc;
- Page page;
+ TupleDesc tupdesc;
+ Page page;
GinPageOpaque opaq;
- HeapTuple resultTuple;
- Datum values[3];
- bool nulls[3];
- Datum flags[16];
- int nflags = 0;
- uint16 flagbits;
+ 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")));
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions")));
page = get_page_from_raw(raw_page);
@@ -118,11 +116,11 @@ gin_page_opaque_info(PG_FUNCTION_ARGS)
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))));
+ 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);
@@ -149,9 +147,8 @@ gin_page_opaque_info(PG_FUNCTION_ARGS)
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)
- {
+ 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));
}
@@ -168,30 +165,27 @@ gin_page_opaque_info(PG_FUNCTION_ARGS)
return HeapTupleGetDatum(resultTuple);
}
-typedef struct gin_leafpage_items_state
-{
- TupleDesc tupd;
+typedef struct gin_leafpage_items_state {
+ TupleDesc tupd;
GinPostingList *seg;
GinPostingList *lastseg;
-} gin_leafpage_items_state;
+} gin_leafpage_items_state;
Datum
-gin_leafpage_items(PG_FUNCTION_ARGS)
-{
+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")));
+ errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions"));
- if (SRF_IS_FIRSTCALL())
- {
- TupleDesc tupdesc;
+ if (SRF_IS_FIRSTCALL()) {
+ TupleDesc tupdesc;
MemoryContext mctx;
- Page page;
+ Page page;
GinPageOpaque opaq;
fctx = SRF_FIRSTCALL_INIT();
@@ -199,28 +193,27 @@ gin_leafpage_items(PG_FUNCTION_ARGS)
page = get_page_from_raw(raw_page);
- if (PageIsNew(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))));
+ 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))));
+ 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_object(gin_leafpage_items_state);
@@ -233,7 +226,7 @@ gin_leafpage_items(PG_FUNCTION_ARGS)
inter_call_data->seg = GinDataLeafPageGetPostingList(page);
inter_call_data->lastseg = (GinPostingList *)
(((char *) inter_call_data->seg) +
- GinDataLeafPageGetPostingListSize(page));
+ GinDataLeafPageGetPostingListSize(page));
fctx->user_fctx = inter_call_data;
@@ -243,15 +236,13 @@ gin_leafpage_items(PG_FUNCTION_ARGS)
fctx = SRF_PERCALL_SETUP();
inter_call_data = fctx->user_fctx;
- if (inter_call_data->seg != inter_call_data->lastseg)
- {
+ 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;
+ HeapTuple resultTuple;
+ Datum result;
+ Datum values[3];
+ bool nulls[3];
+ int ndecoded, i;
ItemPointer tids;
Datum *tids_datum;
@@ -262,7 +253,7 @@ gin_leafpage_items(PG_FUNCTION_ARGS)
/* build an array of decoded item pointers */
tids = ginPostingListDecode(cur, &ndecoded);
- tids_datum = (Datum *) palloc(ndecoded * sizeof(Datum));
+ tids_datum = palloc_array(Datum, ndecoded);
for (i = 0; i < ndecoded; i++)
tids_datum[i] = ItemPointerGetDatum(&tids[i]);
values[2] = PointerGetDatum(construct_array_builtin(tids_datum, ndecoded, TIDOID));
--
2.43.0
v10-0002-GIN-pageinspect-support-for-entry-tree-and-posti.patchapplication/octet-stream; name=v10-0002-GIN-pageinspect-support-for-entry-tree-and-posti.patchDownload
From f1b3a3ecfd9731ead65e515d5ea14516b7010c6f Mon Sep 17 00:00:00 2001
From: reshke <reshke@double.cloud>
Date: Mon, 13 Oct 2025 20:14:26 +0000
Subject: [PATCH v10 2/2] GIN pageinspect support for entry tree and posting
tree internal pages
This patch provides new version for pageinspect contrib module including
two new functions:
* gin_entrypage_items.
* gin_datapage_items.
These two functions can be used to examine GIN entry tree and posting
tree pages. Namely, gin_entrypage_items can be used of both leaf and
non-leaf entry tree pages. gin_datapage_items is provided in pairs with
already-existing gin_leafpage_items to examine non-leaf posting tree
pages.
We keep the different functions here mainly because of different GIN
pages layoff.
Note that fast-list pages are out of scope of this patch.
Co-authored-by: Peter Eisentraut <peter@eisentraut.org>
Reviewed-by: Andrey Borodin x4mmm@yandex-team.ru
Reviewed-by: Roman Khapov rkhapov@yandex-team.ru
some code cleanup
---
contrib/pageinspect/Makefile | 2 +-
contrib/pageinspect/expected/gin.out | 84 ++++-
contrib/pageinspect/expected/gin_1.out | 151 ++++++++
contrib/pageinspect/ginfuncs.c | 328 ++++++++++++++++++
contrib/pageinspect/meson.build | 1 +
.../pageinspect/pageinspect--1.13--1.14.sql | 27 ++
contrib/pageinspect/pageinspect.control | 2 +-
contrib/pageinspect/sql/gin.sql | 34 +-
doc/src/sgml/pageinspect.sgml | 54 +++
9 files changed, 677 insertions(+), 6 deletions(-)
create mode 100644 contrib/pageinspect/expected/gin_1.out
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..dee2894c2ac 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',
@@ -35,6 +76,23 @@ FROM gin_leafpage_items(get_raw_page('test1_y_idx',
-[ RECORD 1 ]
?column? | t
+-- Now test posting tree non-leaf page.
+-- This requires inserting many tuples on a single leaf page to trigger page split.
+CREATE TABLE test_data_page(i INT[]);
+CREATE INDEX test_data_page_i_idx ON test_data_page USING gin(i) WITH (fastupdate = off);
+INSERT INTO test_data_page SELECT ARRAY[1] FROM generate_series(1, 10000);
+-- For this index, block 0 is metapage, block 1 is entry tree, block 2 is
+-- posting tree non-leaf page and block 3 & 4 are compressed data leaf pages.
+SELECT * FROM gin_datapage_items(get_raw_page('test_data_page_i_idx', 2));
+-[ RECORD 1 ]-------
+itemoffset | 1
+downlink | 4
+item_tid | (44,83)
+-[ RECORD 2 ]-------
+itemoffset | 2
+downlink | 3
+item_tid | (0,0)
+
-- Failure with various modes.
-- Suppress the DETAIL message, to allow the tests to work across various
-- page sizes and architectures.
@@ -54,12 +112,34 @@ 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 unsupported 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
+-- Check the error message for the internal posting tree page.
+SELECT * FROM gin_entrypage_items(get_raw_page('test_data_page_i_idx', 2), 'test_data_page_i_idx'::regclass);
+ERROR: input page is not a GIN entry tree page
+HINT: This appears to be a GIN posting tree page. Please use gin_datapage_items.
+-- 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 the 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}
+
+-- reject 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'));
-[ RECORD 1 ]------+-
gin_leafpage_items |
+SELECT gin_datapage_items(decode(repeat('00', :block_size), 'hex'));
+(0 rows)
+
SELECT gin_metapage_info(decode(repeat('00', :block_size), 'hex'));
-[ RECORD 1 ]-----+-
gin_metapage_info |
diff --git a/contrib/pageinspect/expected/gin_1.out b/contrib/pageinspect/expected/gin_1.out
new file mode 100644
index 00000000000..1d504cb5557
--- /dev/null
+++ b/contrib/pageinspect/expected/gin_1.out
@@ -0,0 +1,151 @@
+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 ]----+-----------
+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
+SELECT * FROM gin_entrypage_items(get_raw_page('test1_y_idx', 1), 'test1_y_idx'::regclass);
+-[ RECORD 1 ]--------------
+itemoffset | 1
+downlink | (2147483660,1)
+tids | {"(0,1)"}
+keys | y=11
+-[ RECORD 2 ]--------------
+itemoffset | 2
+downlink | (2147483660,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 | (2147483660,1)
+tids | {"(0,1)"}
+keys | z=a
+-[ RECORD 4 ]--------------
+itemoffset | 4
+downlink | (2147483660,1)
+tids | {"(0,1)"}
+keys | z=b
+-[ RECORD 5 ]--------------
+itemoffset | 5
+downlink | (2147483660,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',
+ (pg_relation_size('test1_y_idx') /
+ current_setting('block_size')::bigint)::int - 1));
+-[ RECORD 1 ]
+?column? | t
+
+-- Now test posting tree non-leaf page.
+-- This requires inserting many tuples on a single leaf page to trigger page split.
+CREATE TABLE test_data_page(i INT[]);
+CREATE INDEX test_data_page_i_idx ON test_data_page USING gin(i) WITH (fastupdate = off);
+INSERT INTO test_data_page SELECT ARRAY[1] FROM generate_series(1, 10000);
+-- For this index, block 0 is metapage, block 1 is entry tree, block 2 is
+-- posting tree non-leaf page and block 3 & 4 are compressed data leaf pages.
+SELECT * FROM gin_datapage_items(get_raw_page('test_data_page_i_idx', 2));
+-[ RECORD 1 ]-------
+itemoffset | 1
+downlink | 4
+item_tid | (41,125)
+-[ RECORD 2 ]--------
+itemoffset | 2
+downlink | 3
+item_tid | (0,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 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
+-- Reject unsupported 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
+-- Check the error message for the internal posting tree page.
+SELECT * FROM gin_entrypage_items(get_raw_page('test_data_page_i_idx', 2), 'test_data_page_i_idx'::regclass);
+ERROR: input page is not a GIN entry tree page
+HINT: This appears to be a GIN posting tree page. Please use gin_datapage_items.
+-- 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 the new page is fast-list.
+SELECT * FROM gin_page_opaque_info(get_raw_page('test3_y_z_idx', 2));
+-[ RECORD 1 ]------------------
+rightlink | 3
+maxoff | 136
+flags | {list,list_fullrow}
+
+-- reject 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'));
+-[ RECORD 1 ]------+-
+gin_leafpage_items |
+
+SELECT gin_datapage_items(decode(repeat('00', :block_size), 'hex'));
+(0 rows)
+
+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/ginfuncs.c b/contrib/pageinspect/ginfuncs.c
index ccd2a0582a1..e0621cd2c26 100644
--- a/contrib/pageinspect/ginfuncs.c
+++ b/contrib/pageinspect/ginfuncs.c
@@ -11,18 +11,27 @@
#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);
+#define IS_INDEX(r) ((r)->rd_rel->relkind == RELKIND_INDEX)
+#define IS_GIN(r) ((r)->rd_rel->relam == GIN_AM_OID)
Datum
gin_metapage_info(PG_FUNCTION_ARGS) {
@@ -171,6 +180,325 @@ typedef struct gin_leafpage_items_state {
GinPostingList *lastseg;
} gin_leafpage_items_state;
+/*
+ * gin_entrypage_items
+ *
+ * Allows inspection of contents of an entry tree page.
+ */
+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;
+ TupleDesc tupdesc;
+ Page page;
+ GinPageOpaque opaq;
+ StringInfoData buf;
+
+ 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_INDEX(indexRel) || !IS_GIN(indexRel))
+ ereport(ERROR,
+ errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is not a %s index",
+ RelationGetRelationName(indexRel), "GIN"));
+
+ 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 entry tree page"),
+ errdetail("Expected special size %d, got %d.",
+ (int) MAXALIGN(sizeof(GinPageOpaqueData)),
+ 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"),
+ errhint("This appears to be a GIN posting tree page. Please use gin_datapage_items."));
+
+ initStringInfo(&buf);
+ maxoff = PageGetMaxOffsetNumber(page);
+
+ tupdesc = RelationGetDescr(indexRel);
+
+ for (OffsetNumber offset = FirstOffsetNumber;
+ offset <= maxoff;
+ offset = OffsetNumberNext(offset)) {
+ OffsetNumber indAtt;
+ Datum values[4];
+ bool nulls[4] = {0};
+ Datum attrVal;
+ bool isnull;
+ IndexTuple idxtuple;
+ ItemId iid = PageGetItemId(page, offset);
+
+ if (!ItemIdIsValid(iid))
+ elog(ERROR, "invalid ItemId");
+
+ idxtuple = (IndexTuple) PageGetItem(page, iid);
+
+ values[0] = UInt16GetDatum(offset);
+
+ if (tupdesc->natts == 1) {
+ indAtt = FirstOffsetNumber;
+ /*
+ * here we can safely reuse pg_class's tuple
+ * descriptor.
+ */
+ attrVal = index_getattr(idxtuple, FirstOffsetNumber, tupdesc, &isnull);
+ if (isnull)
+ ereport(ERROR,
+ errcode(ERRCODE_INDEX_CORRUPTED),
+ errmsg("invalid gin entry page tuple at offset %d", offset));
+ } else {
+ TupleDesc tmpTupdesc;
+ Datum res;
+ Form_pg_attribute attr;
+
+ /*
+ * Multi-column GIN indexes store 2-attribute tuple on
+ * each page item. First attribute is which heap
+ * attribute is stored as the second value in pair. To
+ * display value with proper output function we need
+ * to recreate tuple descriptor on each offset.
+ */
+
+ /* orig tuple reuse is safe */
+
+ res = index_getattr(idxtuple, FirstOffsetNumber, tupdesc, &isnull);
+
+ /*
+ * we do not expect null for first attr in
+ * multi-column GIN
+ */
+ if (isnull)
+ ereport(ERROR,
+ errcode(ERRCODE_INDEX_CORRUPTED),
+ errmsg("invalid gin entry page tuple at offset %d", offset));
+
+ 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);
+
+ FreeTupleDesc(tmpTupdesc);
+ }
+
+ appendStringInfo(&buf, "%s=", quote_identifier(TupleDescAttr(tupdesc, indAtt - 1)->attname.data));
+
+ if (!isnull) {
+ Oid foutoid;
+ bool typisvarlena;
+ Oid typoid;
+ char *value;
+ bool nq;
+
+ /* 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 (const char *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 (const char *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);
+ resetStringInfo(&buf);
+
+ if (GinIsPostingTree(idxtuple)) {
+ values[1] = ItemPointerGetDatum(&idxtuple->t_tid);
+ nulls[2] = true;
+ } else {
+ int ndecoded;
+ Datum *tids_datum;
+ ItemPointer items_orig;
+ bool free_items_orig;
+
+ 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 = palloc_array(Datum, ndecoded);
+ for (int 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);
+ }
+
+ index_close(indexRel, AccessShareLock);
+
+ return (Datum) 0;
+}
+
+/*
+ * gin_datapage_items
+ *
+ * Allows inspection of contents of an posting tree non-leaf page.
+ */
+Datum
+gin_datapage_items(PG_FUNCTION_ARGS) {
+ bytea *raw_page = PG_GETARG_BYTEA_P(0);
+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ OffsetNumber maxoff;
+ 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"),
+ errhint("This appears to be a GIN posting leaf tree page. Please use gin_leafpage_items."));
+
+ maxoff = GinPageGetOpaque(page)->maxoff;
+
+ for (OffsetNumber 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) {
bytea *raw_page = PG_GETARG_BYTEA_P(0);
diff --git a/contrib/pageinspect/meson.build b/contrib/pageinspect/meson.build
index c43ea400a4d..2f333635838 100644
--- a/contrib/pageinspect/meson.build
+++ b/contrib/pageinspect/meson.build
@@ -38,6 +38,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,
)
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..ef6fa87e0f4
--- /dev/null
+++ b/contrib/pageinspect/pageinspect--1.13--1.14.sql
@@ -0,0 +1,27 @@
+/* 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,
+ 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..1a9eaebeebc 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
@@ -18,6 +24,18 @@ FROM gin_leafpage_items(get_raw_page('test1_y_idx',
(pg_relation_size('test1_y_idx') /
current_setting('block_size')::bigint)::int - 1));
+-- Now test posting tree non-leaf page.
+-- This requires inserting many tuples on a single leaf page to trigger page split.
+
+CREATE TABLE test_data_page(i INT[]);
+CREATE INDEX test_data_page_i_idx ON test_data_page USING gin(i) WITH (fastupdate = off);
+
+INSERT INTO test_data_page SELECT ARRAY[1] FROM generate_series(1, 10000);
+
+-- For this index, block 0 is metapage, block 1 is entry tree, block 2 is
+-- posting tree non-leaf page and block 3 & 4 are compressed data leaf pages.
+SELECT * FROM gin_datapage_items(get_raw_page('test_data_page_i_idx', 2));
+
-- Failure with various modes.
-- Suppress the DETAIL message, to allow the tests to work across various
-- page sizes and architectures.
@@ -32,9 +50,21 @@ 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 unsupported page types in gin_entrypage_items.
+SELECT * FROM gin_entrypage_items(get_raw_page('test2_y_z_idx', 0), 'test2_y_z_idx'::regclass);
+-- Check the error message for the internal posting tree page.
+SELECT * FROM gin_entrypage_items(get_raw_page('test_data_page_i_idx', 2), 'test_data_page_i_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 the new page is fast-list.
+SELECT * FROM gin_page_opaque_info(get_raw_page('test3_y_z_idx', 2));
+-- reject 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'));
+SELECT gin_datapage_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'));
diff --git a/doc/src/sgml/pageinspect.sgml b/doc/src/sgml/pageinspect.sgml
index 3a113439e1d..4ed8826e92e 100644
--- a/doc/src/sgml/pageinspect.sgml
+++ b/doc/src/sgml/pageinspect.sgml
@@ -714,6 +714,60 @@ test=# SELECT first_tid, nbytes, tids[0:5] AS some_tids
(170,30) | 376 | {"(170,30)","(170,31)","(170,32)","(170,33)","(170,34)"}
(173,44) | 197 | {"(173,44)","(173,45)","(173,46)","(173,47)","(173,48)"}
(7 rows)
+</screen>
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>
+ <function>gin_entrypage_items(page bytea, reloid oid) returns setof record</function>
+ <indexterm>
+ <primary>gin_entrypage_items</primary>
+ </indexterm>
+ </term>
+
+ <listitem>
+ <para>
+ <function>gin_entrypage_items</function> returns information about
+ the data stored in a entry tree <acronym>GIN</acronym> page. For example:
+<screen>
+test=# select * from gin_entrypage_items(get_raw_page('gin_test_idx',
+1), 'gin_test_idx'::regclass);
+ itemoffset | downlink | tids | keys
+------------+----------+------+------------------------------------
+ 1 | (3,0) | {} | i=113
+ 2 | (5,0) | {} | j=34173cb38f07f89ddbebc2ac9128303f
+ 3 | (2,0) | {} | j=a0a080f42e6f13b3a2df133f073095dd
+ 4 | (4,0) | {} | j=fc490ca45c00b1249bbe3554a4fdf6fb
+(4 rows)
+</screen>
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>
+ <function>gin_datapage_items(page bytea) returns setof record</function>
+ <indexterm>
+ <primary>gin_datapage_items</primary>
+ </indexterm>
+ </term>
+
+ <listitem>
+ <para>
+ <function>gin_datapage_items</function> returns information about
+ the data stored in a posting tree <acronym>GIN</acronym> internal page. For example:
+<screen>
+test=# select * from gin_datapage_items(get_raw_page('gin_test_idx',
+43));
+ itemoffset | downlink | item_tid
+------------+----------+----------
+ 1 | 124 | (162,12)
+ 2 | 123 | (314,37)
+ 3 | 251 | (467,23)
+ 4 | 373 | (0,0)
+(4 rows)
</screen>
</para>
</listitem>
--
2.43.0
On 8 Jan 2026, at 01:57, Kirill Reshke <reshkekirill@gmail.com> wrote:
PFA v10
"Prelimitary cleaunup" has two typos. Also it seems that you used something that is not pgindent.
Looks like clang-format with default settings.
+ errdetail("Expected special size %d, got %d.",
+ (int) MAXALIGN(sizeof(GinPageOpaqueData)),
+ PageGetSpecialSize(page)));
I PageGetSpecialSize() returns uint16, maybe let's cast it to (int) too?
Besides this 2nd patch looks good to me.
Best regards, Andrey Borodin.
On Thu, 8 Jan 2026 at 21:49, Andrey Borodin <x4mmm@yandex-team.ru> wrote:
On 8 Jan 2026, at 01:57, Kirill Reshke <reshkekirill@gmail.com> wrote:
PFA v10
Also it seems that you used something that is not pgindent.
Why
Looks like clang-format with default settings.
I use "./src/tools/pg_bsd_indent/pg_bsd_indent -l79 -di12 -nfc1 -nlp
-sac ./contrib/pageinspect/ginfuncs.c"
--
Best regards,
Kirill Reshke
On Thu, 8 Jan 2026 at 22:11, Kirill Reshke <reshkekirill@gmail.com> wrote:
On Thu, 8 Jan 2026 at 21:49, Andrey Borodin <x4mmm@yandex-team.ru> wrote:
On 8 Jan 2026, at 01:57, Kirill Reshke <reshkekirill@gmail.com> wrote:
PFA v10
Also it seems that you used something that is not pgindent.
Why
Looks like clang-format with default settings.
I use "./src/tools/pg_bsd_indent/pg_bsd_indent -l79 -di12 -nfc1 -nlp
-sac ./contrib/pageinspect/ginfuncs.c"--
Best regards,
Kirill Reshke
Okay, after off-list discussion looks like my options to pg_bsd_indent
are bad, better is:
"./src/tools/pg_bsd_indent/pg_bsd_indent -bad -bap -bbb -bc -bl -cli1
-cp33 -cdb -nce -d0 -di12 -nfc1 -i4 -l79 -lp -lpl -nip -npro -sac -tpg
-ts4 ./contrib/pageinspect/ginfuncs.c"
And still this is not OK to use plain pg_bsd_indent and thus I used
" ./src/tools/pgindent/pgindent ./contrib/pageinspect/ginfuncs.c"
v11 with this and commit message polishing
--
Best regards,
Kirill Reshke
Attachments:
v11-0002-GIN-pageinspect-support-for-entry-tree-and-posti.patchapplication/octet-stream; name=v11-0002-GIN-pageinspect-support-for-entry-tree-and-posti.patchDownload
From 5c621d40155f9969ec7569293f91dc82c10cb324 Mon Sep 17 00:00:00 2001
From: reshke <reshke@double.cloud>
Date: Mon, 13 Oct 2025 20:14:26 +0000
Subject: [PATCH v11 2/2] GIN pageinspect support for entry tree and posting
tree internal pages
This patch provides new version for pageinspect contrib module including
two new functions:
* gin_entrypage_items.
* gin_datapage_items.
These two functions can be used to examine GIN entry tree and posting
tree pages. Namely, gin_entrypage_items can be used of both leaf and
non-leaf entry tree pages. gin_datapage_items is provided in pairs with
already-existing gin_leafpage_items to examine non-leaf posting tree
pages.
We keep the different functions here mainly because of different GIN
pages layoff.
Note that fast-list pages are out of scope of this patch.
Co-authored-by: Peter Eisentraut <peter@eisentraut.org>
Reviewed-by: Andrey Borodin x4mmm@yandex-team.ru
Reviewed-by: Roman Khapov rkhapov@yandex-team.ru
Discussion: https://postgr.es/m/CALdSSPiN13n7feQcY0WCmq8jzxjwqhNrt1E=g=g6aZANyE_OoQ@mail.gmail.com
---
contrib/pageinspect/Makefile | 2 +-
contrib/pageinspect/expected/gin.out | 84 ++++-
contrib/pageinspect/expected/gin_1.out | 151 ++++++++
contrib/pageinspect/ginfuncs.c | 346 ++++++++++++++++++
contrib/pageinspect/meson.build | 1 +
.../pageinspect/pageinspect--1.13--1.14.sql | 27 ++
contrib/pageinspect/pageinspect.control | 2 +-
contrib/pageinspect/sql/gin.sql | 34 +-
doc/src/sgml/pageinspect.sgml | 54 +++
9 files changed, 695 insertions(+), 6 deletions(-)
create mode 100644 contrib/pageinspect/expected/gin_1.out
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..dee2894c2ac 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',
@@ -35,6 +76,23 @@ FROM gin_leafpage_items(get_raw_page('test1_y_idx',
-[ RECORD 1 ]
?column? | t
+-- Now test posting tree non-leaf page.
+-- This requires inserting many tuples on a single leaf page to trigger page split.
+CREATE TABLE test_data_page(i INT[]);
+CREATE INDEX test_data_page_i_idx ON test_data_page USING gin(i) WITH (fastupdate = off);
+INSERT INTO test_data_page SELECT ARRAY[1] FROM generate_series(1, 10000);
+-- For this index, block 0 is metapage, block 1 is entry tree, block 2 is
+-- posting tree non-leaf page and block 3 & 4 are compressed data leaf pages.
+SELECT * FROM gin_datapage_items(get_raw_page('test_data_page_i_idx', 2));
+-[ RECORD 1 ]-------
+itemoffset | 1
+downlink | 4
+item_tid | (44,83)
+-[ RECORD 2 ]-------
+itemoffset | 2
+downlink | 3
+item_tid | (0,0)
+
-- Failure with various modes.
-- Suppress the DETAIL message, to allow the tests to work across various
-- page sizes and architectures.
@@ -54,12 +112,34 @@ 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 unsupported 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
+-- Check the error message for the internal posting tree page.
+SELECT * FROM gin_entrypage_items(get_raw_page('test_data_page_i_idx', 2), 'test_data_page_i_idx'::regclass);
+ERROR: input page is not a GIN entry tree page
+HINT: This appears to be a GIN posting tree page. Please use gin_datapage_items.
+-- 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 the 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}
+
+-- reject 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'));
-[ RECORD 1 ]------+-
gin_leafpage_items |
+SELECT gin_datapage_items(decode(repeat('00', :block_size), 'hex'));
+(0 rows)
+
SELECT gin_metapage_info(decode(repeat('00', :block_size), 'hex'));
-[ RECORD 1 ]-----+-
gin_metapage_info |
diff --git a/contrib/pageinspect/expected/gin_1.out b/contrib/pageinspect/expected/gin_1.out
new file mode 100644
index 00000000000..e37df702afd
--- /dev/null
+++ b/contrib/pageinspect/expected/gin_1.out
@@ -0,0 +1,151 @@
+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 ]----+-----------
+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
+SELECT * FROM gin_entrypage_items(get_raw_page('test1_y_idx', 1), 'test1_y_idx'::regclass);
+-[ RECORD 1 ]--------------
+itemoffset | 1
+downlink | (2147483660,1)
+tids | {"(0,1)"}
+keys | y=11
+-[ RECORD 2 ]--------------
+itemoffset | 2
+downlink | (2147483660,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 | (2147483660,1)
+tids | {"(0,1)"}
+keys | z=a
+-[ RECORD 4 ]--------------
+itemoffset | 4
+downlink | (2147483660,1)
+tids | {"(0,1)"}
+keys | z=b
+-[ RECORD 5 ]--------------
+itemoffset | 5
+downlink | (2147483660,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',
+ (pg_relation_size('test1_y_idx') /
+ current_setting('block_size')::bigint)::int - 1));
+-[ RECORD 1 ]
+?column? | t
+
+-- Now test posting tree non-leaf page.
+-- This requires inserting many tuples on a single leaf page to trigger page split.
+CREATE TABLE test_data_page(i INT[]);
+CREATE INDEX test_data_page_i_idx ON test_data_page USING gin(i) WITH (fastupdate = off);
+INSERT INTO test_data_page SELECT ARRAY[1] FROM generate_series(1, 10000);
+-- For this index, block 0 is metapage, block 1 is entry tree, block 2 is
+-- posting tree non-leaf page and block 3 & 4 are compressed data leaf pages.
+SELECT * FROM gin_datapage_items(get_raw_page('test_data_page_i_idx', 2));
+-[ RECORD 1 ]--------
+itemoffset | 1
+downlink | 4
+item_tid | (41,125)
+-[ RECORD 2 ]--------
+itemoffset | 2
+downlink | 3
+item_tid | (0,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 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
+-- Reject unsupported 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
+-- Check the error message for the internal posting tree page.
+SELECT * FROM gin_entrypage_items(get_raw_page('test_data_page_i_idx', 2), 'test_data_page_i_idx'::regclass);
+ERROR: input page is not a GIN entry tree page
+HINT: This appears to be a GIN posting tree page. Please use gin_datapage_items.
+-- 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 the new page is fast-list.
+SELECT * FROM gin_page_opaque_info(get_raw_page('test3_y_z_idx', 2));
+-[ RECORD 1 ]------------------
+rightlink | 3
+maxoff | 136
+flags | {list,list_fullrow}
+
+-- reject 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'));
+-[ RECORD 1 ]------+-
+gin_leafpage_items |
+
+SELECT gin_datapage_items(decode(repeat('00', :block_size), 'hex'));
+(0 rows)
+
+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/ginfuncs.c b/contrib/pageinspect/ginfuncs.c
index b7bd7a3f4cd..c825027781d 100644
--- a/contrib/pageinspect/ginfuncs.c
+++ b/contrib/pageinspect/ginfuncs.c
@@ -11,18 +11,27 @@
#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);
+#define IS_INDEX(r) ((r)->rd_rel->relkind == RELKIND_INDEX)
+#define IS_GIN(r) ((r)->rd_rel->relam == GIN_AM_OID)
Datum
gin_metapage_info(PG_FUNCTION_ARGS)
@@ -175,6 +184,343 @@ typedef struct gin_leafpage_items_state
GinPostingList *lastseg;
} gin_leafpage_items_state;
+/*
+ * gin_entrypage_items
+ *
+ * Allows inspection of contents of an entry tree page.
+ */
+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;
+ TupleDesc tupdesc;
+ Page page;
+ GinPageOpaque opaq;
+ StringInfoData buf;
+
+ 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_INDEX(indexRel) || !IS_GIN(indexRel))
+ ereport(ERROR,
+ errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is not a %s index",
+ RelationGetRelationName(indexRel), "GIN"));
+
+ 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 entry tree page"),
+ errdetail("Expected special size %d, got %d.",
+ (int) MAXALIGN(sizeof(GinPageOpaqueData)),
+ 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"),
+ errhint("This appears to be a GIN posting tree page. Please use gin_datapage_items."));
+
+ initStringInfo(&buf);
+ maxoff = PageGetMaxOffsetNumber(page);
+
+ tupdesc = RelationGetDescr(indexRel);
+
+ for (OffsetNumber offset = FirstOffsetNumber;
+ offset <= maxoff;
+ offset = OffsetNumberNext(offset))
+ {
+ OffsetNumber indAtt;
+ Datum values[4];
+ bool nulls[4] = {0};
+ Datum attrVal;
+ bool isnull;
+ IndexTuple idxtuple;
+ ItemId iid = PageGetItemId(page, offset);
+
+ if (!ItemIdIsValid(iid))
+ elog(ERROR, "invalid ItemId");
+
+ idxtuple = (IndexTuple) PageGetItem(page, iid);
+
+ values[0] = UInt16GetDatum(offset);
+
+ if (tupdesc->natts == 1)
+ {
+ indAtt = FirstOffsetNumber;
+
+ /*
+ * here we can safely reuse pg_class's tuple descriptor.
+ */
+ attrVal = index_getattr(idxtuple, FirstOffsetNumber, tupdesc, &isnull);
+ if (isnull)
+ ereport(ERROR,
+ errcode(ERRCODE_INDEX_CORRUPTED),
+ errmsg("invalid gin entry page tuple at offset %d", offset));
+ }
+ else
+ {
+ TupleDesc tmpTupdesc;
+ Datum res;
+ Form_pg_attribute attr;
+
+ /*
+ * Multi-column GIN indexes store 2-attribute tuple on each page
+ * item. First attribute is which heap attribute is stored as the
+ * second value in pair. To display value with proper output
+ * function we need to recreate tuple descriptor on each offset.
+ */
+
+ /* orig tuple reuse is safe */
+
+ res = index_getattr(idxtuple, FirstOffsetNumber, tupdesc, &isnull);
+
+ /*
+ * we do not expect null for first attr in multi-column GIN
+ */
+ if (isnull)
+ ereport(ERROR,
+ errcode(ERRCODE_INDEX_CORRUPTED),
+ errmsg("invalid gin entry page tuple at offset %d", offset));
+
+ 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);
+
+ FreeTupleDesc(tmpTupdesc);
+ }
+
+ appendStringInfo(&buf, "%s=", quote_identifier(TupleDescAttr(tupdesc, indAtt - 1)->attname.data));
+
+ if (!isnull)
+ {
+ Oid foutoid;
+ bool typisvarlena;
+ Oid typoid;
+ char *value;
+ bool nq;
+
+ /* 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 (const char *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 (const char *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);
+ resetStringInfo(&buf);
+
+ if (GinIsPostingTree(idxtuple))
+ {
+ values[1] = ItemPointerGetDatum(&idxtuple->t_tid);
+ nulls[2] = true;
+ }
+ else
+ {
+ int ndecoded;
+ Datum *tids_datum;
+ ItemPointer items_orig;
+ bool free_items_orig;
+
+ 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 = palloc_array(Datum, ndecoded);
+ for (int 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);
+ }
+
+ index_close(indexRel, AccessShareLock);
+
+ return (Datum) 0;
+}
+
+/*
+ * gin_datapage_items
+ *
+ * Allows inspection of contents of an posting tree non-leaf page.
+ */
+Datum
+gin_datapage_items(PG_FUNCTION_ARGS)
+{
+ bytea *raw_page = PG_GETARG_BYTEA_P(0);
+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ OffsetNumber maxoff;
+ 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"),
+ errhint("This appears to be a GIN posting leaf tree page. Please use gin_leafpage_items."));
+
+ maxoff = GinPageGetOpaque(page)->maxoff;
+
+ for (OffsetNumber 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/meson.build b/contrib/pageinspect/meson.build
index c43ea400a4d..2f333635838 100644
--- a/contrib/pageinspect/meson.build
+++ b/contrib/pageinspect/meson.build
@@ -38,6 +38,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,
)
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..ef6fa87e0f4
--- /dev/null
+++ b/contrib/pageinspect/pageinspect--1.13--1.14.sql
@@ -0,0 +1,27 @@
+/* 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,
+ 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..1a9eaebeebc 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
@@ -18,6 +24,18 @@ FROM gin_leafpage_items(get_raw_page('test1_y_idx',
(pg_relation_size('test1_y_idx') /
current_setting('block_size')::bigint)::int - 1));
+-- Now test posting tree non-leaf page.
+-- This requires inserting many tuples on a single leaf page to trigger page split.
+
+CREATE TABLE test_data_page(i INT[]);
+CREATE INDEX test_data_page_i_idx ON test_data_page USING gin(i) WITH (fastupdate = off);
+
+INSERT INTO test_data_page SELECT ARRAY[1] FROM generate_series(1, 10000);
+
+-- For this index, block 0 is metapage, block 1 is entry tree, block 2 is
+-- posting tree non-leaf page and block 3 & 4 are compressed data leaf pages.
+SELECT * FROM gin_datapage_items(get_raw_page('test_data_page_i_idx', 2));
+
-- Failure with various modes.
-- Suppress the DETAIL message, to allow the tests to work across various
-- page sizes and architectures.
@@ -32,9 +50,21 @@ 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 unsupported page types in gin_entrypage_items.
+SELECT * FROM gin_entrypage_items(get_raw_page('test2_y_z_idx', 0), 'test2_y_z_idx'::regclass);
+-- Check the error message for the internal posting tree page.
+SELECT * FROM gin_entrypage_items(get_raw_page('test_data_page_i_idx', 2), 'test_data_page_i_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 the new page is fast-list.
+SELECT * FROM gin_page_opaque_info(get_raw_page('test3_y_z_idx', 2));
+-- reject 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'));
+SELECT gin_datapage_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'));
diff --git a/doc/src/sgml/pageinspect.sgml b/doc/src/sgml/pageinspect.sgml
index 3a113439e1d..4ed8826e92e 100644
--- a/doc/src/sgml/pageinspect.sgml
+++ b/doc/src/sgml/pageinspect.sgml
@@ -714,6 +714,60 @@ test=# SELECT first_tid, nbytes, tids[0:5] AS some_tids
(170,30) | 376 | {"(170,30)","(170,31)","(170,32)","(170,33)","(170,34)"}
(173,44) | 197 | {"(173,44)","(173,45)","(173,46)","(173,47)","(173,48)"}
(7 rows)
+</screen>
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>
+ <function>gin_entrypage_items(page bytea, reloid oid) returns setof record</function>
+ <indexterm>
+ <primary>gin_entrypage_items</primary>
+ </indexterm>
+ </term>
+
+ <listitem>
+ <para>
+ <function>gin_entrypage_items</function> returns information about
+ the data stored in a entry tree <acronym>GIN</acronym> page. For example:
+<screen>
+test=# select * from gin_entrypage_items(get_raw_page('gin_test_idx',
+1), 'gin_test_idx'::regclass);
+ itemoffset | downlink | tids | keys
+------------+----------+------+------------------------------------
+ 1 | (3,0) | {} | i=113
+ 2 | (5,0) | {} | j=34173cb38f07f89ddbebc2ac9128303f
+ 3 | (2,0) | {} | j=a0a080f42e6f13b3a2df133f073095dd
+ 4 | (4,0) | {} | j=fc490ca45c00b1249bbe3554a4fdf6fb
+(4 rows)
+</screen>
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>
+ <function>gin_datapage_items(page bytea) returns setof record</function>
+ <indexterm>
+ <primary>gin_datapage_items</primary>
+ </indexterm>
+ </term>
+
+ <listitem>
+ <para>
+ <function>gin_datapage_items</function> returns information about
+ the data stored in a posting tree <acronym>GIN</acronym> internal page. For example:
+<screen>
+test=# select * from gin_datapage_items(get_raw_page('gin_test_idx',
+43));
+ itemoffset | downlink | item_tid
+------------+----------+----------
+ 1 | 124 | (162,12)
+ 2 | 123 | (314,37)
+ 3 | 251 | (467,23)
+ 4 | 373 | (0,0)
+(4 rows)
</screen>
</para>
</listitem>
--
2.43.0
v11-0001-Preliminary-cleanup.patchapplication/octet-stream; name=v11-0001-Preliminary-cleanup.patchDownload
From c33be44852aa4286e0c85ffd3457caf7b283866a Mon Sep 17 00:00:00 2001
From: reshke <reshke@double.cloud>
Date: Wed, 7 Jan 2026 19:38:52 +0000
Subject: [PATCH v11 1/2] Preliminary cleanup
This patch fixes whitespace/tab issues enforcing single style
across existing ginfuncs.c code. This patch also switches
palloc to our newly-preferred palloc_array.
Per Peter Eisentraut's patch in thread.
Reviewed-by: Andrey Borodin x4mmm@yandex-team.ru
Discussion: https://postgr.es/m/CALdSSPiN13n7feQcY0WCmq8jzxjwqhNrt1E=g=g6aZANyE_OoQ@mail.gmail.com
---
contrib/pageinspect/ginfuncs.c | 58 +++++++++++++++++-----------------
1 file changed, 29 insertions(+), 29 deletions(-)
diff --git a/contrib/pageinspect/ginfuncs.c b/contrib/pageinspect/ginfuncs.c
index ebcc2b3db5c..b7bd7a3f4cd 100644
--- a/contrib/pageinspect/ginfuncs.c
+++ b/contrib/pageinspect/ginfuncs.c
@@ -38,8 +38,8 @@ gin_metapage_info(PG_FUNCTION_ARGS)
if (!superuser())
ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to use raw page functions")));
+ errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions"));
page = get_page_from_raw(raw_page);
@@ -48,20 +48,20 @@ gin_metapage_info(PG_FUNCTION_ARGS)
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))));
+ 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)));
+ 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)
@@ -118,11 +118,11 @@ gin_page_opaque_info(PG_FUNCTION_ARGS)
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))));
+ 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);
@@ -184,8 +184,8 @@ gin_leafpage_items(PG_FUNCTION_ARGS)
if (!superuser())
ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to use raw page functions")));
+ errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions"));
if (SRF_IS_FIRSTCALL())
{
@@ -207,20 +207,20 @@ gin_leafpage_items(PG_FUNCTION_ARGS)
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))));
+ 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))));
+ 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_object(gin_leafpage_items_state);
@@ -262,7 +262,7 @@ gin_leafpage_items(PG_FUNCTION_ARGS)
/* build an array of decoded item pointers */
tids = ginPostingListDecode(cur, &ndecoded);
- tids_datum = (Datum *) palloc(ndecoded * sizeof(Datum));
+ tids_datum = palloc_array(Datum, ndecoded);
for (i = 0; i < ndecoded; i++)
tids_datum[i] = ItemPointerGetDatum(&tids[i]);
values[2] = PointerGetDatum(construct_array_builtin(tids_datum, ndecoded, TIDOID));
--
2.43.0
On 8 Jan 2026, at 23:00, Kirill Reshke <reshkekirill@gmail.com> wrote:
v11 with this and commit message polishing
LGTM, I switched CF item to RfC.
Thanks!
Best regards, Andrey Borodin.
On Jan 9, 2026, at 02:00, Kirill Reshke <reshkekirill@gmail.com> wrote:
On Thu, 8 Jan 2026 at 22:11, Kirill Reshke <reshkekirill@gmail.com> wrote:
On Thu, 8 Jan 2026 at 21:49, Andrey Borodin <x4mmm@yandex-team.ru> wrote:
On 8 Jan 2026, at 01:57, Kirill Reshke <reshkekirill@gmail.com> wrote:
PFA v10
Also it seems that you used something that is not pgindent.
Why
Looks like clang-format with default settings.
I use "./src/tools/pg_bsd_indent/pg_bsd_indent -l79 -di12 -nfc1 -nlp
-sac ./contrib/pageinspect/ginfuncs.c"--
Best regards,
Kirill ReshkeOkay, after off-list discussion looks like my options to pg_bsd_indent
are bad, better is:"./src/tools/pg_bsd_indent/pg_bsd_indent -bad -bap -bbb -bc -bl -cli1
-cp33 -cdb -nce -d0 -di12 -nfc1 -i4 -l79 -lp -lpl -nip -npro -sac -tpg
-ts4 ./contrib/pageinspect/ginfuncs.c"And still this is not OK to use plain pg_bsd_indent and thus I used
" ./src/tools/pgindent/pgindent ./contrib/pageinspect/ginfuncs.c"v11 with this and commit message polishing
--
Best regards,
Kirill Reshke
<v11-0002-GIN-pageinspect-support-for-entry-tree-and-posti.patch><v11-0001-Preliminary-cleanup.patch>
Hi Kirill,
Thanks for the patch. I have some small comments:
1 - 0001
```
Subject: [PATCH v11 1/2] Preliminary cleanup
```
0001 does some cleanup work, the commit subject "Preliminary cleanup” is a bit vague. Maybe: pageinspect: clean up ginfuncs.c formatting and allocations, or pageinspect: cleanup in ginfuncs.c
2 - 0001
```
Per Peter Eisentraut's patch in thread.
```
From what I have seen so far, instead of mention a name, it’s more common to mention a commit id.
3 - 0001
```
Reviewed-by: Andrey Borodin x4mmm@yandex-team.ru
```
I think the correct form is: Reviewed-by: Andrey Borodin <x4mmm@yandex-team.ru <mailto:x4mmm@yandex-team.ru>>
I believe the committer will fix that before pushing. But to make committer’s life easier, you’d better fix that rather than leaving to the committer.
Otherwise, code changes of 0001 is straightforward and LGTM.
4 - 0002
```
Reviewed-by: Andrey Borodin x4mmm@yandex-team.ru
Reviewed-by: Roman Khapov rkhapov@yandex-team.ru <mailto:rkhapov@yandex-team.ru>
```
Add <> to email addresses.
5 - 0002
```
+ if (!IS_INDEX(indexRel) || !IS_GIN(indexRel))
+ ereport(ERROR,
+ errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is not a %s index",
+ RelationGetRelationName(indexRel), "GIN"));
```
I don’t see a test covering this error case.
6 - 0002
```
+ /* 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"),
+ errhint("This appears to be a GIN posting tree page. Please use gin_datapage_items."));
```
I just feel the comment is unclear. It’s easy to read it as only for the immediate “if”, but it’s actually for the following 3 “if”. Maybe change to: Reject non-entry-tree GIN pages (meta, fast list, and data pages)
And the 3 error messages look in inconsistent forms. I would suggest change the first 2 error messages to:
* gin_entrypage_items does not support metapages
* gin_entrypage_items does not support fast list pages
7 - 0002
```
+ if (!ItemIdIsValid(iid))
+ elog(ERROR, "invalid ItemId”);
```
Why this is elog but ereport?
Also, the error message is too simple. Maybe change to:
```
errmsg("invalid ItemId at offset %u", offset))
```
8 - 0002
```
+ /*
+ * here we can safely reuse pg_class's tuple descriptor.
+ */
+ attrVal = index_getattr(idxtuple, FirstOffsetNumber, tupdesc, &isnull);
```
I think this comment is wrong. tupdesc is the index relation’s descriptor.
Also, this can be a one-line comment.
9 - 0002
```
+ ereport(ERROR,
+ errcode(ERRCODE_INDEX_CORRUPTED),
+ errmsg("invalid gin entry page tuple at offset %d", offset));
```
Offset is unsigned, so %u should be used.
10 - 0002
```
+ /* Most of this is copied from record_out(). */
+ typoid = TupleDescAttr(tupdesc, indAtt - 1)->atttypid;
```
This comment is confusing. I understand that you meant to say the following code piece is copied from record_out(). Maybe change to:
```
typoid = TupleDescAttr(tupdesc, indAtt - 1)->atttypid;
getTypeOutputInfo(typoid, &foutoid, &typisvarlena);
value = OidOutputFunctionCall(foutoid, attrVal);
/*
* The following value output and quoting logic is copied
* from record_out().
*/
```
11 - 0002
```
+ /* Get list of item pointers from the tuple. */
+ if (GinItupIsCompressed(idxtuple))
```
Nit: Get list -> Get a list
Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/
On Fri, 9 Jan 2026 at 14:43, Chao Li <li.evan.chao@gmail.com> wrote:
Hi Kirill,
Thanks for the patch. I have some small comments:
1 - 0001
```
Subject: [PATCH v11 1/2] Preliminary cleanup
```0001 does some cleanup work, the commit subject "Preliminary cleanup” is a bit vague. Maybe: pageinspect: clean up ginfuncs.c formatting and allocations, or pageinspect: cleanup in ginfuncs.c
Yes, the commit message is indeed vague. PFA v12 with reworked commit message.
2 - 0001
```
Per Peter Eisentraut's patch in thread.
```From what I have seen so far, instead of mention a name, it’s more common to mention a commit id.
In this case, I am referring to this patch, which is never committed
anywhere [0]/messages/by-id/921aa27c-e983-4577-b1dc-3adda3ce79da@eisentraut.org
3 - 0001
```
Reviewed-by: Andrey Borodin x4mmm@yandex-team.ru
```I think the correct form is: Reviewed-by: Andrey Borodin <x4mmm@yandex-team.ru <mailto:x4mmm@yandex-team.ru>>
I believe the committer will fix that before pushing. But to make committer’s life easier, you’d better fix that rather than leaving to the committer.
Otherwise, code changes of 0001 is straightforward and LGTM.
Yes, fixed.
4 - 0002
```
Reviewed-by: Andrey Borodin x4mmm@yandex-team.ru
Reviewed-by: Roman Khapov rkhapov@yandex-team.ru <mailto:rkhapov@yandex-team.ru>
```Add <> to email addresses.
Thanks, fixed.
5 - 0002 ``` + if (!IS_INDEX(indexRel) || !IS_GIN(indexRel)) + ereport(ERROR, + errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not a %s index", + RelationGetRelationName(indexRel), "GIN")); ```I don’t see a test covering this error case.
Hmm. I dont see any tests for this in nearby pageinspect modules (for
btree, hash, etc), so I leave it as-is for now. I am not sure the
value of testing this is worth testing cycles.
6 - 0002 ``` + /* 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"), + errhint("This appears to be a GIN posting tree page. Please use gin_datapage_items.")); ```I just feel the comment is unclear. It’s easy to read it as only for the immediate “if”, but it’s actually for the following 3 “if”. Maybe change to: Reject non-entry-tree GIN pages (meta, fast list, and data pages)
And the 3 error messages look in inconsistent forms. I would suggest change the first 2 error messages to:
* gin_entrypage_items does not support metapages
* gin_entrypage_items does not support fast list pages
Thank you, I changed all 3 error messages to this form and updated tests.
7 - 0002 ``` + if (!ItemIdIsValid(iid)) + elog(ERROR, "invalid ItemId”); ```Why this is elog but ereport?
I copied this from gistfuncs.c. However, this is user-facing change,
so this is indeed something where ereport should be used.
Also, the error message is too simple. Maybe change to:
```
errmsg("invalid ItemId at offset %u", offset))
```8 - 0002 ``` + /* + * here we can safely reuse pg_class's tuple descriptor. + */ + attrVal = index_getattr(idxtuple, FirstOffsetNumber, tupdesc, &isnull); ```I think this comment is wrong. tupdesc is the index relation’s descriptor.
Also, this can be a one-line comment.
Thank you, I have updated this comment.
9 - 0002 ``` + ereport(ERROR, + errcode(ERRCODE_INDEX_CORRUPTED), + errmsg("invalid gin entry page tuple at offset %d", offset)); ```Offset is unsigned, so %u should be used.
Fixed.
10 - 0002 ``` + /* Most of this is copied from record_out(). */ + typoid = TupleDescAttr(tupdesc, indAtt - 1)->atttypid; ```This comment is confusing. I understand that you meant to say the following code piece is copied from record_out(). Maybe change to:
```
typoid = TupleDescAttr(tupdesc, indAtt - 1)->atttypid;
getTypeOutputInfo(typoid, &foutoid, &typisvarlena);
value = OidOutputFunctionCall(foutoid, attrVal);/*
* The following value output and quoting logic is copied
* from record_out().
*/
```
Applied.
11 - 0002 ``` + /* Get list of item pointers from the tuple. */ + if (GinItupIsCompressed(idxtuple)) ```Nit: Get list -> Get a list
Applied.
Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/
PFA v12.
[0]: /messages/by-id/921aa27c-e983-4577-b1dc-3adda3ce79da@eisentraut.org
--
Best regards,
Kirill Reshke
Attachments:
v12-0002-GIN-pageinspect-support-for-entry-tree-and-posti.patchapplication/octet-stream; name=v12-0002-GIN-pageinspect-support-for-entry-tree-and-posti.patchDownload
From fe73b3197548d813ce232ce76a507fad8d87b7b2 Mon Sep 17 00:00:00 2001
From: reshke <reshke@double.cloud>
Date: Mon, 13 Oct 2025 20:14:26 +0000
Subject: [PATCH v12 2/2] GIN pageinspect support for entry tree and posting
tree internal pages
This patch provides a new version for the pageinspect contrib module, including
two new functions:
* gin_entrypage_items.
* gin_datapage_items.
These two functions can be used to examine the GIN entry tree and posting
tree pages. Namely, gin_entrypage_items can be used for both leaf and
non-leaf entry tree pages. gin_datapage_items is provided in pairs with
already-existing gin_leafpage_items to examine the non-leaf posting tree pages.
We keep the different functions here mainly because of different GIN.
pages layoff.
Note that fast-list pages are out of scope of this patch.
Co-authored-by: Peter Eisentraut <peter@eisentraut.org>
Reviewed-by: Andrey Borodin <x4mmm@yandex-team.ru>
Reviewed-by: Roman Khapov <rkhapov@yandex-team.ru>
Reviewed-by: Chao Li <li.evan.chao@gmail.com>
Discussion: https://postgr.es/m/CALdSSPiN13n7feQcY0WCmq8jzxjwqhNrt1E=g=g6aZANyE_OoQ@mail.gmail.com
---
contrib/pageinspect/Makefile | 2 +-
contrib/pageinspect/expected/gin.out | 84 ++++-
contrib/pageinspect/expected/gin_1.out | 151 ++++++++
contrib/pageinspect/ginfuncs.c | 350 ++++++++++++++++++
contrib/pageinspect/meson.build | 1 +
.../pageinspect/pageinspect--1.13--1.14.sql | 27 ++
contrib/pageinspect/pageinspect.control | 2 +-
contrib/pageinspect/sql/gin.sql | 34 +-
doc/src/sgml/pageinspect.sgml | 54 +++
9 files changed, 699 insertions(+), 6 deletions(-)
create mode 100644 contrib/pageinspect/expected/gin_1.out
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..5018ef76aa7 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',
@@ -35,6 +76,23 @@ FROM gin_leafpage_items(get_raw_page('test1_y_idx',
-[ RECORD 1 ]
?column? | t
+-- Now test posting tree non-leaf page.
+-- This requires inserting many tuples on a single leaf page to trigger page split.
+CREATE TABLE test_data_page(i INT[]);
+CREATE INDEX test_data_page_i_idx ON test_data_page USING gin(i) WITH (fastupdate = off);
+INSERT INTO test_data_page SELECT ARRAY[1] FROM generate_series(1, 10000);
+-- For this index, block 0 is metapage, block 1 is entry tree, block 2 is
+-- posting tree non-leaf page and block 3 & 4 are compressed data leaf pages.
+SELECT * FROM gin_datapage_items(get_raw_page('test_data_page_i_idx', 2));
+-[ RECORD 1 ]-------
+itemoffset | 1
+downlink | 4
+item_tid | (44,83)
+-[ RECORD 2 ]-------
+itemoffset | 2
+downlink | 3
+item_tid | (0,0)
+
-- Failure with various modes.
-- Suppress the DETAIL message, to allow the tests to work across various
-- page sizes and architectures.
@@ -54,12 +112,34 @@ 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 unsupported 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 does not support metapages
+-- Check the error message for the internal posting tree page.
+SELECT * FROM gin_entrypage_items(get_raw_page('test_data_page_i_idx', 2), 'test_data_page_i_idx'::regclass);
+ERROR: gin_entrypage_items does not support posting tree pages
+HINT: This appears to be a GIN posting tree page. Please use gin_datapage_items.
+-- 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 the 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}
+
+-- reject 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 does not support fast list pages
-- 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_datapage_items(decode(repeat('00', :block_size), 'hex'));
+(0 rows)
+
SELECT gin_metapage_info(decode(repeat('00', :block_size), 'hex'));
-[ RECORD 1 ]-----+-
gin_metapage_info |
diff --git a/contrib/pageinspect/expected/gin_1.out b/contrib/pageinspect/expected/gin_1.out
new file mode 100644
index 00000000000..afb52500111
--- /dev/null
+++ b/contrib/pageinspect/expected/gin_1.out
@@ -0,0 +1,151 @@
+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 ]----+-----------
+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
+SELECT * FROM gin_entrypage_items(get_raw_page('test1_y_idx', 1), 'test1_y_idx'::regclass);
+-[ RECORD 1 ]--------------
+itemoffset | 1
+downlink | (2147483660,1)
+tids | {"(0,1)"}
+keys | y=11
+-[ RECORD 2 ]--------------
+itemoffset | 2
+downlink | (2147483660,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 | (2147483660,1)
+tids | {"(0,1)"}
+keys | z=a
+-[ RECORD 4 ]--------------
+itemoffset | 4
+downlink | (2147483660,1)
+tids | {"(0,1)"}
+keys | z=b
+-[ RECORD 5 ]--------------
+itemoffset | 5
+downlink | (2147483660,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',
+ (pg_relation_size('test1_y_idx') /
+ current_setting('block_size')::bigint)::int - 1));
+-[ RECORD 1 ]
+?column? | t
+
+-- Now test posting tree non-leaf page.
+-- This requires inserting many tuples on a single leaf page to trigger page split.
+CREATE TABLE test_data_page(i INT[]);
+CREATE INDEX test_data_page_i_idx ON test_data_page USING gin(i) WITH (fastupdate = off);
+INSERT INTO test_data_page SELECT ARRAY[1] FROM generate_series(1, 10000);
+-- For this index, block 0 is metapage, block 1 is entry tree, block 2 is
+-- posting tree non-leaf page and block 3 & 4 are compressed data leaf pages.
+SELECT * FROM gin_datapage_items(get_raw_page('test_data_page_i_idx', 2));
+-[ RECORD 1 ]--------
+itemoffset | 1
+downlink | 4
+item_tid | (41,125)
+-[ RECORD 2 ]--------
+itemoffset | 2
+downlink | 3
+item_tid | (0,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 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
+-- Reject unsupported 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 does not support metapages
+-- Check the error message for the internal posting tree page.
+SELECT * FROM gin_entrypage_items(get_raw_page('test_data_page_i_idx', 2), 'test_data_page_i_idx'::regclass);
+ERROR: gin_entrypage_items does not support posting tree pages
+HINT: This appears to be a GIN posting tree page. Please use gin_datapage_items.
+-- 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 the new page is fast-list.
+SELECT * FROM gin_page_opaque_info(get_raw_page('test3_y_z_idx', 2));
+-[ RECORD 1 ]------------------
+rightlink | 3
+maxoff | 136
+flags | {list,list_fullrow}
+
+-- reject 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 does not support fast list pages
+-- 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_datapage_items(decode(repeat('00', :block_size), 'hex'));
+(0 rows)
+
+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/ginfuncs.c b/contrib/pageinspect/ginfuncs.c
index b7bd7a3f4cd..baaf0b21436 100644
--- a/contrib/pageinspect/ginfuncs.c
+++ b/contrib/pageinspect/ginfuncs.c
@@ -11,18 +11,27 @@
#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);
+#define IS_INDEX(r) ((r)->rd_rel->relkind == RELKIND_INDEX)
+#define IS_GIN(r) ((r)->rd_rel->relam == GIN_AM_OID)
Datum
gin_metapage_info(PG_FUNCTION_ARGS)
@@ -175,6 +184,347 @@ typedef struct gin_leafpage_items_state
GinPostingList *lastseg;
} gin_leafpage_items_state;
+/*
+ * gin_entrypage_items
+ *
+ * Allows inspection of contents of an entry tree page.
+ */
+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;
+ TupleDesc tupdesc;
+ Page page;
+ GinPageOpaque opaq;
+ StringInfoData buf;
+
+ 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_INDEX(indexRel) || !IS_GIN(indexRel))
+ ereport(ERROR,
+ errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is not a %s index",
+ RelationGetRelationName(indexRel), "GIN"));
+
+ 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 entry tree page"),
+ errdetail("Expected special size %d, got %d.",
+ (int) MAXALIGN(sizeof(GinPageOpaqueData)),
+ 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 does not support metapages"));
+
+
+ if (opaq->flags & (GIN_LIST | GIN_LIST_FULLROW))
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("gin_entrypage_items does not support fast list pages"));
+
+
+ if (opaq->flags & GIN_DATA)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("gin_entrypage_items does not support posting tree pages"),
+ errhint("This appears to be a GIN posting tree page. Please use gin_datapage_items."));
+
+ initStringInfo(&buf);
+ maxoff = PageGetMaxOffsetNumber(page);
+
+ tupdesc = RelationGetDescr(indexRel);
+
+ for (OffsetNumber offset = FirstOffsetNumber;
+ offset <= maxoff;
+ offset = OffsetNumberNext(offset))
+ {
+ OffsetNumber indAtt;
+ Datum values[4];
+ bool nulls[4] = {0};
+ Datum attrVal;
+ bool isnull;
+ IndexTuple idxtuple;
+ ItemId iid = PageGetItemId(page, offset);
+
+ if (!ItemIdIsValid(iid))
+ ereport(ERROR, errcode(ERRCODE_INDEX_CORRUPTED), errmsg("invalid ItemId at offset %u", offset));
+
+ idxtuple = (IndexTuple) PageGetItem(page, iid);
+
+ values[0] = UInt16GetDatum(offset);
+
+ if (tupdesc->natts == 1)
+ {
+ indAtt = FirstOffsetNumber;
+
+ /* Here we can safely reuse any tuple descriptor. */
+ attrVal = index_getattr(idxtuple, FirstOffsetNumber, tupdesc, &isnull);
+ if (isnull)
+ ereport(ERROR,
+ errcode(ERRCODE_INDEX_CORRUPTED),
+ errmsg("invalid gin entry page tuple at offset %u", offset));
+ }
+ else
+ {
+ TupleDesc tmpTupdesc;
+ Datum res;
+ Form_pg_attribute attr;
+
+ /*
+ * Multi-column GIN indexes store 2-attribute tuple on each page
+ * item. First attribute is which heap attribute is stored as the
+ * second value in pair. To display value with proper output
+ * function we need to recreate tuple descriptor on each offset.
+ */
+
+ /* orig tuple reuse is safe */
+
+ res = index_getattr(idxtuple, FirstOffsetNumber, tupdesc, &isnull);
+
+ /*
+ * we do not expect null for first attr in multi-column GIN
+ */
+ if (isnull)
+ ereport(ERROR,
+ errcode(ERRCODE_INDEX_CORRUPTED),
+ errmsg("invalid gin entry page tuple at offset %u", offset));
+
+ 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);
+
+ FreeTupleDesc(tmpTupdesc);
+ }
+
+ appendStringInfo(&buf, "%s=", quote_identifier(TupleDescAttr(tupdesc, indAtt - 1)->attname.data));
+
+ if (!isnull)
+ {
+ Oid foutoid;
+ bool typisvarlena;
+ Oid typoid;
+ char *value;
+ bool nq;
+
+ /*
+ * The following value output and quoting logic 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 (const char *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 (const char *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);
+ resetStringInfo(&buf);
+
+ if (GinIsPostingTree(idxtuple))
+ {
+ values[1] = ItemPointerGetDatum(&idxtuple->t_tid);
+ nulls[2] = true;
+ }
+ else
+ {
+ int ndecoded;
+ Datum *tids_datum;
+ ItemPointer items_orig;
+ bool free_items_orig;
+
+ 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 = palloc_array(Datum, ndecoded);
+ for (int 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);
+ }
+
+ index_close(indexRel, AccessShareLock);
+
+ return (Datum) 0;
+}
+
+/*
+ * gin_datapage_items
+ *
+ * Allows inspection of contents of an posting tree non-leaf page.
+ */
+Datum
+gin_datapage_items(PG_FUNCTION_ARGS)
+{
+ bytea *raw_page = PG_GETARG_BYTEA_P(0);
+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ OffsetNumber maxoff;
+ 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);
+
+ /*
+ * Reject non-entry-tree GIN pages, which are metapage, fastlist pages,
+ * and posting tree pages.
+ */
+
+ 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"),
+ errhint("This appears to be a GIN posting leaf tree page. Please use gin_leafpage_items."));
+
+ maxoff = GinPageGetOpaque(page)->maxoff;
+
+ for (OffsetNumber 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/meson.build b/contrib/pageinspect/meson.build
index c43ea400a4d..2f333635838 100644
--- a/contrib/pageinspect/meson.build
+++ b/contrib/pageinspect/meson.build
@@ -38,6 +38,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,
)
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..ef6fa87e0f4
--- /dev/null
+++ b/contrib/pageinspect/pageinspect--1.13--1.14.sql
@@ -0,0 +1,27 @@
+/* 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,
+ 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..1a9eaebeebc 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
@@ -18,6 +24,18 @@ FROM gin_leafpage_items(get_raw_page('test1_y_idx',
(pg_relation_size('test1_y_idx') /
current_setting('block_size')::bigint)::int - 1));
+-- Now test posting tree non-leaf page.
+-- This requires inserting many tuples on a single leaf page to trigger page split.
+
+CREATE TABLE test_data_page(i INT[]);
+CREATE INDEX test_data_page_i_idx ON test_data_page USING gin(i) WITH (fastupdate = off);
+
+INSERT INTO test_data_page SELECT ARRAY[1] FROM generate_series(1, 10000);
+
+-- For this index, block 0 is metapage, block 1 is entry tree, block 2 is
+-- posting tree non-leaf page and block 3 & 4 are compressed data leaf pages.
+SELECT * FROM gin_datapage_items(get_raw_page('test_data_page_i_idx', 2));
+
-- Failure with various modes.
-- Suppress the DETAIL message, to allow the tests to work across various
-- page sizes and architectures.
@@ -32,9 +50,21 @@ 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 unsupported page types in gin_entrypage_items.
+SELECT * FROM gin_entrypage_items(get_raw_page('test2_y_z_idx', 0), 'test2_y_z_idx'::regclass);
+-- Check the error message for the internal posting tree page.
+SELECT * FROM gin_entrypage_items(get_raw_page('test_data_page_i_idx', 2), 'test_data_page_i_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 the new page is fast-list.
+SELECT * FROM gin_page_opaque_info(get_raw_page('test3_y_z_idx', 2));
+-- reject 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'));
+SELECT gin_datapage_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'));
diff --git a/doc/src/sgml/pageinspect.sgml b/doc/src/sgml/pageinspect.sgml
index 3a113439e1d..4ed8826e92e 100644
--- a/doc/src/sgml/pageinspect.sgml
+++ b/doc/src/sgml/pageinspect.sgml
@@ -714,6 +714,60 @@ test=# SELECT first_tid, nbytes, tids[0:5] AS some_tids
(170,30) | 376 | {"(170,30)","(170,31)","(170,32)","(170,33)","(170,34)"}
(173,44) | 197 | {"(173,44)","(173,45)","(173,46)","(173,47)","(173,48)"}
(7 rows)
+</screen>
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>
+ <function>gin_entrypage_items(page bytea, reloid oid) returns setof record</function>
+ <indexterm>
+ <primary>gin_entrypage_items</primary>
+ </indexterm>
+ </term>
+
+ <listitem>
+ <para>
+ <function>gin_entrypage_items</function> returns information about
+ the data stored in a entry tree <acronym>GIN</acronym> page. For example:
+<screen>
+test=# select * from gin_entrypage_items(get_raw_page('gin_test_idx',
+1), 'gin_test_idx'::regclass);
+ itemoffset | downlink | tids | keys
+------------+----------+------+------------------------------------
+ 1 | (3,0) | {} | i=113
+ 2 | (5,0) | {} | j=34173cb38f07f89ddbebc2ac9128303f
+ 3 | (2,0) | {} | j=a0a080f42e6f13b3a2df133f073095dd
+ 4 | (4,0) | {} | j=fc490ca45c00b1249bbe3554a4fdf6fb
+(4 rows)
+</screen>
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>
+ <function>gin_datapage_items(page bytea) returns setof record</function>
+ <indexterm>
+ <primary>gin_datapage_items</primary>
+ </indexterm>
+ </term>
+
+ <listitem>
+ <para>
+ <function>gin_datapage_items</function> returns information about
+ the data stored in a posting tree <acronym>GIN</acronym> internal page. For example:
+<screen>
+test=# select * from gin_datapage_items(get_raw_page('gin_test_idx',
+43));
+ itemoffset | downlink | item_tid
+------------+----------+----------
+ 1 | 124 | (162,12)
+ 2 | 123 | (314,37)
+ 3 | 251 | (467,23)
+ 4 | 373 | (0,0)
+(4 rows)
</screen>
</para>
</listitem>
--
2.43.0
v12-0001-Modernize-coding-in-GIN-pageinspect-functions.patchapplication/octet-stream; name=v12-0001-Modernize-coding-in-GIN-pageinspect-functions.patchDownload
From cca4b1eaecb36cbb28ef79c1d7d560d5740e15f6 Mon Sep 17 00:00:00 2001
From: reshke <reshke@double.cloud>
Date: Wed, 7 Jan 2026 19:38:52 +0000
Subject: [PATCH v12 1/2] Modernize coding in GIN pageinspect functions.
This patch switches palloc to our newly preferred palloc_array and modernizes
ereport calls, switching from list-style to polymorphic ereport
calls. This patch also fixes whitespace/tab issues, enforcing a single style.
across existing ginfuncs.c code.
Inspired by Peter Eisentraut's patch in the thread.
Reviewed-by: Andrey Borodin <x4mmm@yandex-team.ru>
Reviewed-by: Chao Li <li.evan.chao@gmail.com>
Discussion: https://postgr.es/m/CALdSSPiN13n7feQcY0WCmq8jzxjwqhNrt1E=g=g6aZANyE_OoQ@mail.gmail.com
---
contrib/pageinspect/ginfuncs.c | 58 +++++++++++++++++-----------------
1 file changed, 29 insertions(+), 29 deletions(-)
diff --git a/contrib/pageinspect/ginfuncs.c b/contrib/pageinspect/ginfuncs.c
index ebcc2b3db5c..b7bd7a3f4cd 100644
--- a/contrib/pageinspect/ginfuncs.c
+++ b/contrib/pageinspect/ginfuncs.c
@@ -38,8 +38,8 @@ gin_metapage_info(PG_FUNCTION_ARGS)
if (!superuser())
ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to use raw page functions")));
+ errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions"));
page = get_page_from_raw(raw_page);
@@ -48,20 +48,20 @@ gin_metapage_info(PG_FUNCTION_ARGS)
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))));
+ 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)));
+ 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)
@@ -118,11 +118,11 @@ gin_page_opaque_info(PG_FUNCTION_ARGS)
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))));
+ 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);
@@ -184,8 +184,8 @@ gin_leafpage_items(PG_FUNCTION_ARGS)
if (!superuser())
ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to use raw page functions")));
+ errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions"));
if (SRF_IS_FIRSTCALL())
{
@@ -207,20 +207,20 @@ gin_leafpage_items(PG_FUNCTION_ARGS)
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))));
+ 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))));
+ 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_object(gin_leafpage_items_state);
@@ -262,7 +262,7 @@ gin_leafpage_items(PG_FUNCTION_ARGS)
/* build an array of decoded item pointers */
tids = ginPostingListDecode(cur, &ndecoded);
- tids_datum = (Datum *) palloc(ndecoded * sizeof(Datum));
+ tids_datum = palloc_array(Datum, ndecoded);
for (i = 0; i < ndecoded; i++)
tids_datum[i] = ItemPointerGetDatum(&tids[i]);
values[2] = PointerGetDatum(construct_array_builtin(tids_datum, ndecoded, TIDOID));
--
2.43.0
On Fri, 09 Jan 2026 at 21:39, Kirill Reshke <reshkekirill@gmail.com> wrote:
On Fri, 9 Jan 2026 at 14:43, Chao Li <li.evan.chao@gmail.com> wrote:
Hi Kirill,
Thanks for the patch. I have some small comments:
1 - 0001
```
Subject: [PATCH v11 1/2] Preliminary cleanup
```0001 does some cleanup work, the commit subject "Preliminary cleanup” is a bit vague. Maybe: pageinspect: clean up ginfuncs.c formatting and allocations, or pageinspect: cleanup in ginfuncs.c
Yes, the commit message is indeed vague. PFA v12 with reworked commit message.
2 - 0001
```
Per Peter Eisentraut's patch in thread.
```From what I have seen so far, instead of mention a name, it’s more common to mention a commit id.
In this case, I am referring to this patch, which is never committed
anywhere [0]3 - 0001
```
Reviewed-by: Andrey Borodin x4mmm@yandex-team.ru
```I think the correct form is: Reviewed-by: Andrey Borodin <x4mmm@yandex-team.ru <mailto:x4mmm@yandex-team.ru>>
I believe the committer will fix that before pushing. But to make committer’s life easier, you’d better fix that rather than leaving to the committer.
Otherwise, code changes of 0001 is straightforward and LGTM.
Yes, fixed.
4 - 0002
```
Reviewed-by: Andrey Borodin x4mmm@yandex-team.ru
Reviewed-by: Roman Khapov rkhapov@yandex-team.ru <mailto:rkhapov@yandex-team.ru>
```Add <> to email addresses.
Thanks, fixed.
5 - 0002 ``` + if (!IS_INDEX(indexRel) || !IS_GIN(indexRel)) + ereport(ERROR, + errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not a %s index", + RelationGetRelationName(indexRel), "GIN")); ```I don’t see a test covering this error case.
Hmm. I dont see any tests for this in nearby pageinspect modules (for
btree, hash, etc), so I leave it as-is for now. I am not sure the
value of testing this is worth testing cycles.6 - 0002 ``` + /* 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"), + errhint("This appears to be a GIN posting tree page. Please use gin_datapage_items.")); ```I just feel the comment is unclear. It’s easy to read it as only for
the immediate “if”, but it’s actually for the following 3
“if”. Maybe change to: Reject non-entry-tree GIN pages (meta, fast
list, and data pages)And the 3 error messages look in inconsistent forms. I would suggest change the first 2 error messages to:
* gin_entrypage_items does not support metapages
* gin_entrypage_items does not support fast list pagesThank you, I changed all 3 error messages to this form and updated tests.
7 - 0002 ``` + if (!ItemIdIsValid(iid)) + elog(ERROR, "invalid ItemId”); ```Why this is elog but ereport?
I copied this from gistfuncs.c. However, this is user-facing change,
so this is indeed something where ereport should be used.Also, the error message is too simple. Maybe change to:
```
errmsg("invalid ItemId at offset %u", offset))
```8 - 0002 ``` + /* + * here we can safely reuse pg_class's tuple descriptor. + */ + attrVal = index_getattr(idxtuple, FirstOffsetNumber, tupdesc, &isnull); ```I think this comment is wrong. tupdesc is the index relation’s descriptor.
Also, this can be a one-line comment.
Thank you, I have updated this comment.
9 - 0002 ``` + ereport(ERROR, + errcode(ERRCODE_INDEX_CORRUPTED), + errmsg("invalid gin entry page tuple at offset %d", offset)); ```Offset is unsigned, so %u should be used.
Fixed.
10 - 0002 ``` + /* Most of this is copied from record_out(). */ + typoid = TupleDescAttr(tupdesc, indAtt - 1)->atttypid; ```This comment is confusing. I understand that you meant to say the following code piece is copied from record_out(). Maybe change to:
```
typoid = TupleDescAttr(tupdesc, indAtt - 1)->atttypid;
getTypeOutputInfo(typoid, &foutoid, &typisvarlena);
value = OidOutputFunctionCall(foutoid, attrVal);/*
* The following value output and quoting logic is copied
* from record_out().
*/
```Applied.
11 - 0002 ``` + /* Get list of item pointers from the tuple. */ + if (GinItupIsCompressed(idxtuple)) ```Nit: Get list -> Get a list
Applied.
Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/PFA v12.
[0] /messages/by-id/921aa27c-e983-4577-b1dc-3adda3ce79da@eisentraut.org
--
Best regards,
Kirill Reshke[2. text/x-diff; v12-0001-Modernize-coding-in-GIN-pageinspect-functions.patch]...
[3. text/x-diff; v12-0002-GIN-pageinspect-support-for-entry-tree-and-posti.patch]...
Just a few small nitpicks on v12:
1.
+ /* Open the relation */
+ indexRel = index_open(indexRelid, AccessShareLock);
"relation" is technically correct, but "index" feels more precise here.
2.
+ if (PageGetSpecialSize(page) != MAXALIGN(sizeof(GinPageOpaqueData)))
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("input page is not a valid GIN entry tree page"),
+ errdetail("Expected special size %d, got %d.",
+ (int) MAXALIGN(sizeof(GinPageOpaqueData)),
+ PageGetSpecialSize(page)));
Cast the second PageGetSpecialSize(page) to int as well, for consistency.
3.
+ /* orig tuple reuse is safe */
+
+ res = index_getattr(idxtuple, FirstOffsetNumber, tupdesc, &isnull);
Does "orig tuple" refer to `idxtuple` here? If so, maybe the comment could be
slightly clearer, e.g.:
/* Safe to reuse the original index tuple */
4.
+
+ return (Datum) 0;
Since this appears to be a void-returning function, consider using_RETURN_VOID()
instead — it's the conventional way and slightly more self-documenting.
--
Regards,
Japin Li
ChengDu WenWu Information Technology Co., Ltd.
On Sat, 10 Jan 2026 at 07:20, Japin Li <japinli@hotmail.com> wrote:
PFA v12.
Just a few small nitpicks on v12:
Hi!
Thank you for your review.
1. + /* Open the relation */ + indexRel = index_open(indexRelid, AccessShareLock);"relation" is technically correct, but "index" feels more precise here.
I have updated this comment, thanks
2. + if (PageGetSpecialSize(page) != MAXALIGN(sizeof(GinPageOpaqueData))) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("input page is not a valid GIN entry tree page"), + errdetail("Expected special size %d, got %d.", + (int) MAXALIGN(sizeof(GinPageOpaqueData)), + PageGetSpecialSize(page)));Cast the second PageGetSpecialSize(page) to int as well, for consistency.
Fixed.
3. + /* orig tuple reuse is safe */ + + res = index_getattr(idxtuple, FirstOffsetNumber, tupdesc, &isnull);Does "orig tuple" refer to `idxtuple` here? If so, maybe the comment could be
slightly clearer, e.g.:/* Safe to reuse the original index tuple */
I have rephrased this comment.
4. + + return (Datum) 0;Since this appears to be a void-returning function, consider using_RETURN_VOID()
instead — it's the conventional way and slightly more self-documenting.
No, this function returns SET OF RECORD. So, `return (Datum) 0;` is
not exactly VOID or NULL, it is rather the end of the output marker.
You can also see `
gist_page_items`
Your comments refer to v12-0002. For the record, did you review 0001,
if yes, do you think it is good? I have included you as a reviewer to
v12-0002 commit message.
--
Best regards,
Kirill Reshke
Attachments:
v13-0001-Modernize-coding-in-GIN-pageinspect-functions.patchapplication/octet-stream; name=v13-0001-Modernize-coding-in-GIN-pageinspect-functions.patchDownload
From cca4b1eaecb36cbb28ef79c1d7d560d5740e15f6 Mon Sep 17 00:00:00 2001
From: reshke <reshke@double.cloud>
Date: Wed, 7 Jan 2026 19:38:52 +0000
Subject: [PATCH v13 1/2] Modernize coding in GIN pageinspect functions.
This patch switches palloc to our newly preferred palloc_array and modernizes
ereport calls, switching from list-style to polymorphic ereport
calls. This patch also fixes whitespace/tab issues, enforcing a single style.
across existing ginfuncs.c code.
Inspired by Peter Eisentraut's patch in the thread.
Reviewed-by: Andrey Borodin <x4mmm@yandex-team.ru>
Reviewed-by: Chao Li <li.evan.chao@gmail.com>
Discussion: https://postgr.es/m/CALdSSPiN13n7feQcY0WCmq8jzxjwqhNrt1E=g=g6aZANyE_OoQ@mail.gmail.com
---
contrib/pageinspect/ginfuncs.c | 58 +++++++++++++++++-----------------
1 file changed, 29 insertions(+), 29 deletions(-)
diff --git a/contrib/pageinspect/ginfuncs.c b/contrib/pageinspect/ginfuncs.c
index ebcc2b3db5c..b7bd7a3f4cd 100644
--- a/contrib/pageinspect/ginfuncs.c
+++ b/contrib/pageinspect/ginfuncs.c
@@ -38,8 +38,8 @@ gin_metapage_info(PG_FUNCTION_ARGS)
if (!superuser())
ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to use raw page functions")));
+ errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions"));
page = get_page_from_raw(raw_page);
@@ -48,20 +48,20 @@ gin_metapage_info(PG_FUNCTION_ARGS)
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))));
+ 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)));
+ 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)
@@ -118,11 +118,11 @@ gin_page_opaque_info(PG_FUNCTION_ARGS)
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))));
+ 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);
@@ -184,8 +184,8 @@ gin_leafpage_items(PG_FUNCTION_ARGS)
if (!superuser())
ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to use raw page functions")));
+ errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions"));
if (SRF_IS_FIRSTCALL())
{
@@ -207,20 +207,20 @@ gin_leafpage_items(PG_FUNCTION_ARGS)
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))));
+ 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))));
+ 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_object(gin_leafpage_items_state);
@@ -262,7 +262,7 @@ gin_leafpage_items(PG_FUNCTION_ARGS)
/* build an array of decoded item pointers */
tids = ginPostingListDecode(cur, &ndecoded);
- tids_datum = (Datum *) palloc(ndecoded * sizeof(Datum));
+ tids_datum = palloc_array(Datum, ndecoded);
for (i = 0; i < ndecoded; i++)
tids_datum[i] = ItemPointerGetDatum(&tids[i]);
values[2] = PointerGetDatum(construct_array_builtin(tids_datum, ndecoded, TIDOID));
--
2.43.0
v13-0002-GIN-pageinspect-support-for-entry-tree-and-posti.patchapplication/octet-stream; name=v13-0002-GIN-pageinspect-support-for-entry-tree-and-posti.patchDownload
From 1c0ea0cca057af45cdc8c44b197ce76de5bf6732 Mon Sep 17 00:00:00 2001
From: reshke <reshke@double.cloud>
Date: Mon, 13 Oct 2025 20:14:26 +0000
Subject: [PATCH v13 2/2] GIN pageinspect support for entry tree and posting
tree internal pages
This patch provides a new version for the pageinspect contrib module, including
two new functions:
* gin_entrypage_items.
* gin_datapage_items.
These two functions can be used to examine the GIN entry tree and posting
tree pages. Namely, gin_entrypage_items can be used for both leaf and
non-leaf entry tree pages. gin_datapage_items is provided in pairs with
already-existing gin_leafpage_items to examine the non-leaf posting tree pages.
We keep the different functions here mainly because of different GIN.
pages layoff.
Note that fast-list pages are out of scope of this patch.
Co-authored-by: Peter Eisentraut <peter@eisentraut.org>
Reviewed-by: Andrey Borodin <x4mmm@yandex-team.ru>
Reviewed-by: Roman Khapov <rkhapov@yandex-team.ru>
Reviewed-by: Chao Li <li.evan.chao@gmail.com>
Reviewed-by: Japin Li <japinli@hotmail.com>
Discussion: https://postgr.es/m/CALdSSPiN13n7feQcY0WCmq8jzxjwqhNrt1E=g=g6aZANyE_OoQ@mail.gmail.com
---
contrib/pageinspect/Makefile | 2 +-
contrib/pageinspect/expected/gin.out | 84 ++++-
contrib/pageinspect/expected/gin_1.out | 151 ++++++++
contrib/pageinspect/ginfuncs.c | 350 ++++++++++++++++++
contrib/pageinspect/meson.build | 1 +
.../pageinspect/pageinspect--1.13--1.14.sql | 27 ++
contrib/pageinspect/pageinspect.control | 2 +-
contrib/pageinspect/sql/gin.sql | 34 +-
doc/src/sgml/pageinspect.sgml | 54 +++
9 files changed, 699 insertions(+), 6 deletions(-)
create mode 100644 contrib/pageinspect/expected/gin_1.out
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..5018ef76aa7 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',
@@ -35,6 +76,23 @@ FROM gin_leafpage_items(get_raw_page('test1_y_idx',
-[ RECORD 1 ]
?column? | t
+-- Now test posting tree non-leaf page.
+-- This requires inserting many tuples on a single leaf page to trigger page split.
+CREATE TABLE test_data_page(i INT[]);
+CREATE INDEX test_data_page_i_idx ON test_data_page USING gin(i) WITH (fastupdate = off);
+INSERT INTO test_data_page SELECT ARRAY[1] FROM generate_series(1, 10000);
+-- For this index, block 0 is metapage, block 1 is entry tree, block 2 is
+-- posting tree non-leaf page and block 3 & 4 are compressed data leaf pages.
+SELECT * FROM gin_datapage_items(get_raw_page('test_data_page_i_idx', 2));
+-[ RECORD 1 ]-------
+itemoffset | 1
+downlink | 4
+item_tid | (44,83)
+-[ RECORD 2 ]-------
+itemoffset | 2
+downlink | 3
+item_tid | (0,0)
+
-- Failure with various modes.
-- Suppress the DETAIL message, to allow the tests to work across various
-- page sizes and architectures.
@@ -54,12 +112,34 @@ 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 unsupported 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 does not support metapages
+-- Check the error message for the internal posting tree page.
+SELECT * FROM gin_entrypage_items(get_raw_page('test_data_page_i_idx', 2), 'test_data_page_i_idx'::regclass);
+ERROR: gin_entrypage_items does not support posting tree pages
+HINT: This appears to be a GIN posting tree page. Please use gin_datapage_items.
+-- 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 the 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}
+
+-- reject 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 does not support fast list pages
-- 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_datapage_items(decode(repeat('00', :block_size), 'hex'));
+(0 rows)
+
SELECT gin_metapage_info(decode(repeat('00', :block_size), 'hex'));
-[ RECORD 1 ]-----+-
gin_metapage_info |
diff --git a/contrib/pageinspect/expected/gin_1.out b/contrib/pageinspect/expected/gin_1.out
new file mode 100644
index 00000000000..afb52500111
--- /dev/null
+++ b/contrib/pageinspect/expected/gin_1.out
@@ -0,0 +1,151 @@
+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 ]----+-----------
+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
+SELECT * FROM gin_entrypage_items(get_raw_page('test1_y_idx', 1), 'test1_y_idx'::regclass);
+-[ RECORD 1 ]--------------
+itemoffset | 1
+downlink | (2147483660,1)
+tids | {"(0,1)"}
+keys | y=11
+-[ RECORD 2 ]--------------
+itemoffset | 2
+downlink | (2147483660,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 | (2147483660,1)
+tids | {"(0,1)"}
+keys | z=a
+-[ RECORD 4 ]--------------
+itemoffset | 4
+downlink | (2147483660,1)
+tids | {"(0,1)"}
+keys | z=b
+-[ RECORD 5 ]--------------
+itemoffset | 5
+downlink | (2147483660,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',
+ (pg_relation_size('test1_y_idx') /
+ current_setting('block_size')::bigint)::int - 1));
+-[ RECORD 1 ]
+?column? | t
+
+-- Now test posting tree non-leaf page.
+-- This requires inserting many tuples on a single leaf page to trigger page split.
+CREATE TABLE test_data_page(i INT[]);
+CREATE INDEX test_data_page_i_idx ON test_data_page USING gin(i) WITH (fastupdate = off);
+INSERT INTO test_data_page SELECT ARRAY[1] FROM generate_series(1, 10000);
+-- For this index, block 0 is metapage, block 1 is entry tree, block 2 is
+-- posting tree non-leaf page and block 3 & 4 are compressed data leaf pages.
+SELECT * FROM gin_datapage_items(get_raw_page('test_data_page_i_idx', 2));
+-[ RECORD 1 ]--------
+itemoffset | 1
+downlink | 4
+item_tid | (41,125)
+-[ RECORD 2 ]--------
+itemoffset | 2
+downlink | 3
+item_tid | (0,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 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
+-- Reject unsupported 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 does not support metapages
+-- Check the error message for the internal posting tree page.
+SELECT * FROM gin_entrypage_items(get_raw_page('test_data_page_i_idx', 2), 'test_data_page_i_idx'::regclass);
+ERROR: gin_entrypage_items does not support posting tree pages
+HINT: This appears to be a GIN posting tree page. Please use gin_datapage_items.
+-- 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 the new page is fast-list.
+SELECT * FROM gin_page_opaque_info(get_raw_page('test3_y_z_idx', 2));
+-[ RECORD 1 ]------------------
+rightlink | 3
+maxoff | 136
+flags | {list,list_fullrow}
+
+-- reject 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 does not support fast list pages
+-- 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_datapage_items(decode(repeat('00', :block_size), 'hex'));
+(0 rows)
+
+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/ginfuncs.c b/contrib/pageinspect/ginfuncs.c
index b7bd7a3f4cd..c9cf08872ac 100644
--- a/contrib/pageinspect/ginfuncs.c
+++ b/contrib/pageinspect/ginfuncs.c
@@ -11,18 +11,27 @@
#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);
+#define IS_INDEX(r) ((r)->rd_rel->relkind == RELKIND_INDEX)
+#define IS_GIN(r) ((r)->rd_rel->relam == GIN_AM_OID)
Datum
gin_metapage_info(PG_FUNCTION_ARGS)
@@ -175,6 +184,347 @@ typedef struct gin_leafpage_items_state
GinPostingList *lastseg;
} gin_leafpage_items_state;
+/*
+ * gin_entrypage_items
+ *
+ * Allows inspection of contents of an entry tree page.
+ */
+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;
+ TupleDesc tupdesc;
+ Page page;
+ GinPageOpaque opaq;
+ StringInfoData buf;
+
+ 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_INDEX(indexRel) || !IS_GIN(indexRel))
+ ereport(ERROR,
+ errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is not a %s index",
+ RelationGetRelationName(indexRel), "GIN"));
+
+ 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 entry tree 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 does not support metapages"));
+
+
+ if (opaq->flags & (GIN_LIST | GIN_LIST_FULLROW))
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("gin_entrypage_items does not support fast list pages"));
+
+
+ if (opaq->flags & GIN_DATA)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("gin_entrypage_items does not support posting tree pages"),
+ errhint("This appears to be a GIN posting tree page. Please use gin_datapage_items."));
+
+ initStringInfo(&buf);
+ maxoff = PageGetMaxOffsetNumber(page);
+
+ tupdesc = RelationGetDescr(indexRel);
+
+ for (OffsetNumber offset = FirstOffsetNumber;
+ offset <= maxoff;
+ offset = OffsetNumberNext(offset))
+ {
+ OffsetNumber indAtt;
+ Datum values[4];
+ bool nulls[4] = {0};
+ Datum attrVal;
+ bool isnull;
+ IndexTuple idxtuple;
+ ItemId iid = PageGetItemId(page, offset);
+
+ if (!ItemIdIsValid(iid))
+ ereport(ERROR, errcode(ERRCODE_INDEX_CORRUPTED), errmsg("invalid ItemId at offset %u", offset));
+
+ idxtuple = (IndexTuple) PageGetItem(page, iid);
+
+ values[0] = UInt16GetDatum(offset);
+
+ if (tupdesc->natts == 1)
+ {
+ indAtt = FirstOffsetNumber;
+
+ /* Here we can safely reuse any tuple descriptor. */
+ attrVal = index_getattr(idxtuple, FirstOffsetNumber, tupdesc, &isnull);
+ if (isnull)
+ ereport(ERROR,
+ errcode(ERRCODE_INDEX_CORRUPTED),
+ errmsg("invalid gin entry page tuple at offset %u", offset));
+ }
+ else
+ {
+ TupleDesc tmpTupdesc;
+ Datum res;
+ Form_pg_attribute attr;
+
+ /*
+ * Multi-column GIN indexes store 2-attribute tuple on each page
+ * item. First attribute is which heap attribute is stored as the
+ * second value in pair. To display value with proper output
+ * function we need to recreate tuple descriptor on each offset.
+ * NB: It is safe to reuse the original index tuple. See also
+ * gintuple_get_attrnum.
+ */
+
+ res = index_getattr(idxtuple, FirstOffsetNumber, tupdesc, &isnull);
+
+ /*
+ * we do not expect null for first attr in multi-column GIN
+ */
+ if (isnull)
+ ereport(ERROR,
+ errcode(ERRCODE_INDEX_CORRUPTED),
+ errmsg("invalid gin entry page tuple at offset %u", offset));
+
+ 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);
+
+ FreeTupleDesc(tmpTupdesc);
+ }
+
+ appendStringInfo(&buf, "%s=", quote_identifier(TupleDescAttr(tupdesc, indAtt - 1)->attname.data));
+
+ if (!isnull)
+ {
+ Oid foutoid;
+ bool typisvarlena;
+ Oid typoid;
+ char *value;
+ bool nq;
+
+ /*
+ * The following value output and quoting logic 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 (const char *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 (const char *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);
+ resetStringInfo(&buf);
+
+ if (GinIsPostingTree(idxtuple))
+ {
+ values[1] = ItemPointerGetDatum(&idxtuple->t_tid);
+ nulls[2] = true;
+ }
+ else
+ {
+ int ndecoded;
+ Datum *tids_datum;
+ ItemPointer items_orig;
+ bool free_items_orig;
+
+ 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 = palloc_array(Datum, ndecoded);
+ for (int 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);
+ }
+
+ index_close(indexRel, AccessShareLock);
+
+ return (Datum) 0;
+}
+
+/*
+ * gin_datapage_items
+ *
+ * Allows inspection of contents of an posting tree non-leaf page.
+ */
+Datum
+gin_datapage_items(PG_FUNCTION_ARGS)
+{
+ bytea *raw_page = PG_GETARG_BYTEA_P(0);
+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ OffsetNumber maxoff;
+ 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);
+
+ /*
+ * Reject non-entry-tree GIN pages, which are metapage, fastlist pages,
+ * and posting tree pages.
+ */
+
+ 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"),
+ errhint("This appears to be a GIN posting leaf tree page. Please use gin_leafpage_items."));
+
+ maxoff = GinPageGetOpaque(page)->maxoff;
+
+ for (OffsetNumber 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/meson.build b/contrib/pageinspect/meson.build
index c43ea400a4d..2f333635838 100644
--- a/contrib/pageinspect/meson.build
+++ b/contrib/pageinspect/meson.build
@@ -38,6 +38,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,
)
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..ef6fa87e0f4
--- /dev/null
+++ b/contrib/pageinspect/pageinspect--1.13--1.14.sql
@@ -0,0 +1,27 @@
+/* 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,
+ 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..1a9eaebeebc 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
@@ -18,6 +24,18 @@ FROM gin_leafpage_items(get_raw_page('test1_y_idx',
(pg_relation_size('test1_y_idx') /
current_setting('block_size')::bigint)::int - 1));
+-- Now test posting tree non-leaf page.
+-- This requires inserting many tuples on a single leaf page to trigger page split.
+
+CREATE TABLE test_data_page(i INT[]);
+CREATE INDEX test_data_page_i_idx ON test_data_page USING gin(i) WITH (fastupdate = off);
+
+INSERT INTO test_data_page SELECT ARRAY[1] FROM generate_series(1, 10000);
+
+-- For this index, block 0 is metapage, block 1 is entry tree, block 2 is
+-- posting tree non-leaf page and block 3 & 4 are compressed data leaf pages.
+SELECT * FROM gin_datapage_items(get_raw_page('test_data_page_i_idx', 2));
+
-- Failure with various modes.
-- Suppress the DETAIL message, to allow the tests to work across various
-- page sizes and architectures.
@@ -32,9 +50,21 @@ 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 unsupported page types in gin_entrypage_items.
+SELECT * FROM gin_entrypage_items(get_raw_page('test2_y_z_idx', 0), 'test2_y_z_idx'::regclass);
+-- Check the error message for the internal posting tree page.
+SELECT * FROM gin_entrypage_items(get_raw_page('test_data_page_i_idx', 2), 'test_data_page_i_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 the new page is fast-list.
+SELECT * FROM gin_page_opaque_info(get_raw_page('test3_y_z_idx', 2));
+-- reject 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'));
+SELECT gin_datapage_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'));
diff --git a/doc/src/sgml/pageinspect.sgml b/doc/src/sgml/pageinspect.sgml
index 3a113439e1d..4ed8826e92e 100644
--- a/doc/src/sgml/pageinspect.sgml
+++ b/doc/src/sgml/pageinspect.sgml
@@ -714,6 +714,60 @@ test=# SELECT first_tid, nbytes, tids[0:5] AS some_tids
(170,30) | 376 | {"(170,30)","(170,31)","(170,32)","(170,33)","(170,34)"}
(173,44) | 197 | {"(173,44)","(173,45)","(173,46)","(173,47)","(173,48)"}
(7 rows)
+</screen>
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>
+ <function>gin_entrypage_items(page bytea, reloid oid) returns setof record</function>
+ <indexterm>
+ <primary>gin_entrypage_items</primary>
+ </indexterm>
+ </term>
+
+ <listitem>
+ <para>
+ <function>gin_entrypage_items</function> returns information about
+ the data stored in a entry tree <acronym>GIN</acronym> page. For example:
+<screen>
+test=# select * from gin_entrypage_items(get_raw_page('gin_test_idx',
+1), 'gin_test_idx'::regclass);
+ itemoffset | downlink | tids | keys
+------------+----------+------+------------------------------------
+ 1 | (3,0) | {} | i=113
+ 2 | (5,0) | {} | j=34173cb38f07f89ddbebc2ac9128303f
+ 3 | (2,0) | {} | j=a0a080f42e6f13b3a2df133f073095dd
+ 4 | (4,0) | {} | j=fc490ca45c00b1249bbe3554a4fdf6fb
+(4 rows)
+</screen>
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>
+ <function>gin_datapage_items(page bytea) returns setof record</function>
+ <indexterm>
+ <primary>gin_datapage_items</primary>
+ </indexterm>
+ </term>
+
+ <listitem>
+ <para>
+ <function>gin_datapage_items</function> returns information about
+ the data stored in a posting tree <acronym>GIN</acronym> internal page. For example:
+<screen>
+test=# select * from gin_datapage_items(get_raw_page('gin_test_idx',
+43));
+ itemoffset | downlink | item_tid
+------------+----------+----------
+ 1 | 124 | (162,12)
+ 2 | 123 | (314,37)
+ 3 | 251 | (467,23)
+ 4 | 373 | (0,0)
+(4 rows)
</screen>
</para>
</listitem>
--
2.43.0
On Sat, 10 Jan 2026 at 12:41, Kirill Reshke <reshkekirill@gmail.com> wrote:
On Sat, 10 Jan 2026 at 07:20, Japin Li <japinli@hotmail.com> wrote:
PFA v12.
Just a few small nitpicks on v12:
Hi!
Thank you for your review.1. + /* Open the relation */ + indexRel = index_open(indexRelid, AccessShareLock);"relation" is technically correct, but "index" feels more precise here.
I have updated this comment, thanks
2. + if (PageGetSpecialSize(page) != MAXALIGN(sizeof(GinPageOpaqueData))) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("input page is not a valid GIN entry tree page"), + errdetail("Expected special size %d, got %d.", + (int) MAXALIGN(sizeof(GinPageOpaqueData)), + PageGetSpecialSize(page)));Cast the second PageGetSpecialSize(page) to int as well, for consistency.
Fixed.
3. + /* orig tuple reuse is safe */ + + res = index_getattr(idxtuple, FirstOffsetNumber, tupdesc, &isnull);Does "orig tuple" refer to `idxtuple` here? If so, maybe the comment could be
slightly clearer, e.g.:/* Safe to reuse the original index tuple */
I have rephrased this comment.
Thanks for updating the patches.
4. + + return (Datum) 0;Since this appears to be a void-returning function, consider using_RETURN_VOID()
instead — it's the conventional way and slightly more self-documenting.No, this function returns SET OF RECORD. So, `return (Datum) 0;` is
not exactly VOID or NULL, it is rather the end of the output marker.
You can also see `
gist_page_items`
Thanks for pointing that out!
Your comments refer to v12-0002. For the record, did you review 0001,
if yes, do you think it is good? I have included you as a reviewer to
v12-0002 commit message.
Yeah, patch 0001 looks good to me.
I noticed that the IS_INDEX macro is currently defined in btreefunc.c,
hashfuncs.c, and now also in ginfuncs.c. Would it be possible to move it
to a shared header file so all these modules can include it from one place?
--
Regards,
Japin Li
ChengDu WenWu Information Technology Co., Ltd.
On Sat, 10 Jan 2026 at 17:15, Japin Li <japinli@hotmail.com> wrote:
Your comments refer to v12-0002. For the record, did you review 0001,
if yes, do you think it is good? I have included you as a reviewer to
v12-0002 commit message.Yeah, patch 0001 looks good to me.
Thank you. I have updated 0001 commit message.
I noticed that the IS_INDEX macro is currently defined in btreefunc.c,
hashfuncs.c, and now also in ginfuncs.c. Would it be possible to move it
to a shared header file so all these modules can include it from one place?
This is something I had thought about also. I do find this idea good,
but 0002 patch is already big, and I don't want to overload it. So,
v14-0004 with this change attached.
--
Best regards,
Kirill Reshke
Attachments:
v14-0001-Modernize-coding-in-GIN-pageinspect-functions.patchapplication/octet-stream; name=v14-0001-Modernize-coding-in-GIN-pageinspect-functions.patchDownload
From e294f67635175d86c9c0a864eabc52b353eace61 Mon Sep 17 00:00:00 2001
From: reshke <reshke@double.cloud>
Date: Wed, 7 Jan 2026 19:38:52 +0000
Subject: [PATCH v14 1/3] Modernize coding in GIN pageinspect functions.
This patch switches palloc to our newly preferred palloc_array and modernizes
ereport calls, switching from list-style to polymorphic ereport
calls. This patch also fixes whitespace/tab issues, enforcing a single style.
across existing ginfuncs.c code.
Inspired by Peter Eisentraut's patch in the thread.
Reviewed-by: Andrey Borodin <x4mmm@yandex-team.ru>
Reviewed-by: Chao Li <li.evan.chao@gmail.com>
Reviewed-by: Japin Li <japinli@hotmail.com>
Discussion: https://postgr.es/m/CALdSSPiN13n7feQcY0WCmq8jzxjwqhNrt1E=g=g6aZANyE_OoQ@mail.gmail.com
---
contrib/pageinspect/ginfuncs.c | 58 +++++++++++++++++-----------------
1 file changed, 29 insertions(+), 29 deletions(-)
diff --git a/contrib/pageinspect/ginfuncs.c b/contrib/pageinspect/ginfuncs.c
index ebcc2b3db5c..b7bd7a3f4cd 100644
--- a/contrib/pageinspect/ginfuncs.c
+++ b/contrib/pageinspect/ginfuncs.c
@@ -38,8 +38,8 @@ gin_metapage_info(PG_FUNCTION_ARGS)
if (!superuser())
ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to use raw page functions")));
+ errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions"));
page = get_page_from_raw(raw_page);
@@ -48,20 +48,20 @@ gin_metapage_info(PG_FUNCTION_ARGS)
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))));
+ 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)));
+ 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)
@@ -118,11 +118,11 @@ gin_page_opaque_info(PG_FUNCTION_ARGS)
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))));
+ 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);
@@ -184,8 +184,8 @@ gin_leafpage_items(PG_FUNCTION_ARGS)
if (!superuser())
ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to use raw page functions")));
+ errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to use raw page functions"));
if (SRF_IS_FIRSTCALL())
{
@@ -207,20 +207,20 @@ gin_leafpage_items(PG_FUNCTION_ARGS)
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))));
+ 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))));
+ 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_object(gin_leafpage_items_state);
@@ -262,7 +262,7 @@ gin_leafpage_items(PG_FUNCTION_ARGS)
/* build an array of decoded item pointers */
tids = ginPostingListDecode(cur, &ndecoded);
- tids_datum = (Datum *) palloc(ndecoded * sizeof(Datum));
+ tids_datum = palloc_array(Datum, ndecoded);
for (i = 0; i < ndecoded; i++)
tids_datum[i] = ItemPointerGetDatum(&tids[i]);
values[2] = PointerGetDatum(construct_array_builtin(tids_datum, ndecoded, TIDOID));
--
2.43.0
v14-0002-GIN-pageinspect-support-for-entry-tree-and-posti.patchapplication/octet-stream; name=v14-0002-GIN-pageinspect-support-for-entry-tree-and-posti.patchDownload
From f65c8c9bd59936aa05415574ed5183c72622994e Mon Sep 17 00:00:00 2001
From: reshke <reshke@double.cloud>
Date: Mon, 13 Oct 2025 20:14:26 +0000
Subject: [PATCH v14 2/3] GIN pageinspect support for entry tree and posting
tree internal pages
This patch provides a new version for the pageinspect contrib module, including
two new functions:
* gin_entrypage_items.
* gin_datapage_items.
These two functions can be used to examine the GIN entry tree and posting
tree pages. Namely, gin_entrypage_items can be used for both leaf and
non-leaf entry tree pages. gin_datapage_items is provided in pairs with
already-existing gin_leafpage_items to examine the non-leaf posting tree pages.
We keep the different functions here mainly because of different GIN.
pages layoff.
Note that fast-list pages are out of scope of this patch.
Co-authored-by: Peter Eisentraut <peter@eisentraut.org>
Reviewed-by: Andrey Borodin <x4mmm@yandex-team.ru>
Reviewed-by: Roman Khapov <rkhapov@yandex-team.ru>
Reviewed-by: Chao Li <li.evan.chao@gmail.com>
Reviewed-by: Japin Li <japinli@hotmail.com>
Discussion: https://postgr.es/m/CALdSSPiN13n7feQcY0WCmq8jzxjwqhNrt1E=g=g6aZANyE_OoQ@mail.gmail.com
---
contrib/pageinspect/Makefile | 2 +-
contrib/pageinspect/expected/gin.out | 84 ++++-
contrib/pageinspect/expected/gin_1.out | 151 ++++++++
contrib/pageinspect/ginfuncs.c | 350 ++++++++++++++++++
contrib/pageinspect/meson.build | 1 +
.../pageinspect/pageinspect--1.13--1.14.sql | 27 ++
contrib/pageinspect/pageinspect.control | 2 +-
contrib/pageinspect/sql/gin.sql | 34 +-
doc/src/sgml/pageinspect.sgml | 54 +++
9 files changed, 699 insertions(+), 6 deletions(-)
create mode 100644 contrib/pageinspect/expected/gin_1.out
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..5018ef76aa7 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',
@@ -35,6 +76,23 @@ FROM gin_leafpage_items(get_raw_page('test1_y_idx',
-[ RECORD 1 ]
?column? | t
+-- Now test posting tree non-leaf page.
+-- This requires inserting many tuples on a single leaf page to trigger page split.
+CREATE TABLE test_data_page(i INT[]);
+CREATE INDEX test_data_page_i_idx ON test_data_page USING gin(i) WITH (fastupdate = off);
+INSERT INTO test_data_page SELECT ARRAY[1] FROM generate_series(1, 10000);
+-- For this index, block 0 is metapage, block 1 is entry tree, block 2 is
+-- posting tree non-leaf page and block 3 & 4 are compressed data leaf pages.
+SELECT * FROM gin_datapage_items(get_raw_page('test_data_page_i_idx', 2));
+-[ RECORD 1 ]-------
+itemoffset | 1
+downlink | 4
+item_tid | (44,83)
+-[ RECORD 2 ]-------
+itemoffset | 2
+downlink | 3
+item_tid | (0,0)
+
-- Failure with various modes.
-- Suppress the DETAIL message, to allow the tests to work across various
-- page sizes and architectures.
@@ -54,12 +112,34 @@ 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 unsupported 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 does not support metapages
+-- Check the error message for the internal posting tree page.
+SELECT * FROM gin_entrypage_items(get_raw_page('test_data_page_i_idx', 2), 'test_data_page_i_idx'::regclass);
+ERROR: gin_entrypage_items does not support posting tree pages
+HINT: This appears to be a GIN posting tree page. Please use gin_datapage_items.
+-- 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 the 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}
+
+-- reject 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 does not support fast list pages
-- 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_datapage_items(decode(repeat('00', :block_size), 'hex'));
+(0 rows)
+
SELECT gin_metapage_info(decode(repeat('00', :block_size), 'hex'));
-[ RECORD 1 ]-----+-
gin_metapage_info |
diff --git a/contrib/pageinspect/expected/gin_1.out b/contrib/pageinspect/expected/gin_1.out
new file mode 100644
index 00000000000..afb52500111
--- /dev/null
+++ b/contrib/pageinspect/expected/gin_1.out
@@ -0,0 +1,151 @@
+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 ]----+-----------
+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
+SELECT * FROM gin_entrypage_items(get_raw_page('test1_y_idx', 1), 'test1_y_idx'::regclass);
+-[ RECORD 1 ]--------------
+itemoffset | 1
+downlink | (2147483660,1)
+tids | {"(0,1)"}
+keys | y=11
+-[ RECORD 2 ]--------------
+itemoffset | 2
+downlink | (2147483660,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 | (2147483660,1)
+tids | {"(0,1)"}
+keys | z=a
+-[ RECORD 4 ]--------------
+itemoffset | 4
+downlink | (2147483660,1)
+tids | {"(0,1)"}
+keys | z=b
+-[ RECORD 5 ]--------------
+itemoffset | 5
+downlink | (2147483660,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',
+ (pg_relation_size('test1_y_idx') /
+ current_setting('block_size')::bigint)::int - 1));
+-[ RECORD 1 ]
+?column? | t
+
+-- Now test posting tree non-leaf page.
+-- This requires inserting many tuples on a single leaf page to trigger page split.
+CREATE TABLE test_data_page(i INT[]);
+CREATE INDEX test_data_page_i_idx ON test_data_page USING gin(i) WITH (fastupdate = off);
+INSERT INTO test_data_page SELECT ARRAY[1] FROM generate_series(1, 10000);
+-- For this index, block 0 is metapage, block 1 is entry tree, block 2 is
+-- posting tree non-leaf page and block 3 & 4 are compressed data leaf pages.
+SELECT * FROM gin_datapage_items(get_raw_page('test_data_page_i_idx', 2));
+-[ RECORD 1 ]--------
+itemoffset | 1
+downlink | 4
+item_tid | (41,125)
+-[ RECORD 2 ]--------
+itemoffset | 2
+downlink | 3
+item_tid | (0,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 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
+-- Reject unsupported 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 does not support metapages
+-- Check the error message for the internal posting tree page.
+SELECT * FROM gin_entrypage_items(get_raw_page('test_data_page_i_idx', 2), 'test_data_page_i_idx'::regclass);
+ERROR: gin_entrypage_items does not support posting tree pages
+HINT: This appears to be a GIN posting tree page. Please use gin_datapage_items.
+-- 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 the new page is fast-list.
+SELECT * FROM gin_page_opaque_info(get_raw_page('test3_y_z_idx', 2));
+-[ RECORD 1 ]------------------
+rightlink | 3
+maxoff | 136
+flags | {list,list_fullrow}
+
+-- reject 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 does not support fast list pages
+-- 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_datapage_items(decode(repeat('00', :block_size), 'hex'));
+(0 rows)
+
+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/ginfuncs.c b/contrib/pageinspect/ginfuncs.c
index b7bd7a3f4cd..c9cf08872ac 100644
--- a/contrib/pageinspect/ginfuncs.c
+++ b/contrib/pageinspect/ginfuncs.c
@@ -11,18 +11,27 @@
#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);
+#define IS_INDEX(r) ((r)->rd_rel->relkind == RELKIND_INDEX)
+#define IS_GIN(r) ((r)->rd_rel->relam == GIN_AM_OID)
Datum
gin_metapage_info(PG_FUNCTION_ARGS)
@@ -175,6 +184,347 @@ typedef struct gin_leafpage_items_state
GinPostingList *lastseg;
} gin_leafpage_items_state;
+/*
+ * gin_entrypage_items
+ *
+ * Allows inspection of contents of an entry tree page.
+ */
+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;
+ TupleDesc tupdesc;
+ Page page;
+ GinPageOpaque opaq;
+ StringInfoData buf;
+
+ 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_INDEX(indexRel) || !IS_GIN(indexRel))
+ ereport(ERROR,
+ errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is not a %s index",
+ RelationGetRelationName(indexRel), "GIN"));
+
+ 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 entry tree 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 does not support metapages"));
+
+
+ if (opaq->flags & (GIN_LIST | GIN_LIST_FULLROW))
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("gin_entrypage_items does not support fast list pages"));
+
+
+ if (opaq->flags & GIN_DATA)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("gin_entrypage_items does not support posting tree pages"),
+ errhint("This appears to be a GIN posting tree page. Please use gin_datapage_items."));
+
+ initStringInfo(&buf);
+ maxoff = PageGetMaxOffsetNumber(page);
+
+ tupdesc = RelationGetDescr(indexRel);
+
+ for (OffsetNumber offset = FirstOffsetNumber;
+ offset <= maxoff;
+ offset = OffsetNumberNext(offset))
+ {
+ OffsetNumber indAtt;
+ Datum values[4];
+ bool nulls[4] = {0};
+ Datum attrVal;
+ bool isnull;
+ IndexTuple idxtuple;
+ ItemId iid = PageGetItemId(page, offset);
+
+ if (!ItemIdIsValid(iid))
+ ereport(ERROR, errcode(ERRCODE_INDEX_CORRUPTED), errmsg("invalid ItemId at offset %u", offset));
+
+ idxtuple = (IndexTuple) PageGetItem(page, iid);
+
+ values[0] = UInt16GetDatum(offset);
+
+ if (tupdesc->natts == 1)
+ {
+ indAtt = FirstOffsetNumber;
+
+ /* Here we can safely reuse any tuple descriptor. */
+ attrVal = index_getattr(idxtuple, FirstOffsetNumber, tupdesc, &isnull);
+ if (isnull)
+ ereport(ERROR,
+ errcode(ERRCODE_INDEX_CORRUPTED),
+ errmsg("invalid gin entry page tuple at offset %u", offset));
+ }
+ else
+ {
+ TupleDesc tmpTupdesc;
+ Datum res;
+ Form_pg_attribute attr;
+
+ /*
+ * Multi-column GIN indexes store 2-attribute tuple on each page
+ * item. First attribute is which heap attribute is stored as the
+ * second value in pair. To display value with proper output
+ * function we need to recreate tuple descriptor on each offset.
+ * NB: It is safe to reuse the original index tuple. See also
+ * gintuple_get_attrnum.
+ */
+
+ res = index_getattr(idxtuple, FirstOffsetNumber, tupdesc, &isnull);
+
+ /*
+ * we do not expect null for first attr in multi-column GIN
+ */
+ if (isnull)
+ ereport(ERROR,
+ errcode(ERRCODE_INDEX_CORRUPTED),
+ errmsg("invalid gin entry page tuple at offset %u", offset));
+
+ 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);
+
+ FreeTupleDesc(tmpTupdesc);
+ }
+
+ appendStringInfo(&buf, "%s=", quote_identifier(TupleDescAttr(tupdesc, indAtt - 1)->attname.data));
+
+ if (!isnull)
+ {
+ Oid foutoid;
+ bool typisvarlena;
+ Oid typoid;
+ char *value;
+ bool nq;
+
+ /*
+ * The following value output and quoting logic 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 (const char *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 (const char *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);
+ resetStringInfo(&buf);
+
+ if (GinIsPostingTree(idxtuple))
+ {
+ values[1] = ItemPointerGetDatum(&idxtuple->t_tid);
+ nulls[2] = true;
+ }
+ else
+ {
+ int ndecoded;
+ Datum *tids_datum;
+ ItemPointer items_orig;
+ bool free_items_orig;
+
+ 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 = palloc_array(Datum, ndecoded);
+ for (int 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);
+ }
+
+ index_close(indexRel, AccessShareLock);
+
+ return (Datum) 0;
+}
+
+/*
+ * gin_datapage_items
+ *
+ * Allows inspection of contents of an posting tree non-leaf page.
+ */
+Datum
+gin_datapage_items(PG_FUNCTION_ARGS)
+{
+ bytea *raw_page = PG_GETARG_BYTEA_P(0);
+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ OffsetNumber maxoff;
+ 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);
+
+ /*
+ * Reject non-entry-tree GIN pages, which are metapage, fastlist pages,
+ * and posting tree pages.
+ */
+
+ 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"),
+ errhint("This appears to be a GIN posting leaf tree page. Please use gin_leafpage_items."));
+
+ maxoff = GinPageGetOpaque(page)->maxoff;
+
+ for (OffsetNumber 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/meson.build b/contrib/pageinspect/meson.build
index c43ea400a4d..2f333635838 100644
--- a/contrib/pageinspect/meson.build
+++ b/contrib/pageinspect/meson.build
@@ -38,6 +38,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,
)
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..ef6fa87e0f4
--- /dev/null
+++ b/contrib/pageinspect/pageinspect--1.13--1.14.sql
@@ -0,0 +1,27 @@
+/* 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,
+ 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..1a9eaebeebc 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
@@ -18,6 +24,18 @@ FROM gin_leafpage_items(get_raw_page('test1_y_idx',
(pg_relation_size('test1_y_idx') /
current_setting('block_size')::bigint)::int - 1));
+-- Now test posting tree non-leaf page.
+-- This requires inserting many tuples on a single leaf page to trigger page split.
+
+CREATE TABLE test_data_page(i INT[]);
+CREATE INDEX test_data_page_i_idx ON test_data_page USING gin(i) WITH (fastupdate = off);
+
+INSERT INTO test_data_page SELECT ARRAY[1] FROM generate_series(1, 10000);
+
+-- For this index, block 0 is metapage, block 1 is entry tree, block 2 is
+-- posting tree non-leaf page and block 3 & 4 are compressed data leaf pages.
+SELECT * FROM gin_datapage_items(get_raw_page('test_data_page_i_idx', 2));
+
-- Failure with various modes.
-- Suppress the DETAIL message, to allow the tests to work across various
-- page sizes and architectures.
@@ -32,9 +50,21 @@ 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 unsupported page types in gin_entrypage_items.
+SELECT * FROM gin_entrypage_items(get_raw_page('test2_y_z_idx', 0), 'test2_y_z_idx'::regclass);
+-- Check the error message for the internal posting tree page.
+SELECT * FROM gin_entrypage_items(get_raw_page('test_data_page_i_idx', 2), 'test_data_page_i_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 the new page is fast-list.
+SELECT * FROM gin_page_opaque_info(get_raw_page('test3_y_z_idx', 2));
+-- reject 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'));
+SELECT gin_datapage_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'));
diff --git a/doc/src/sgml/pageinspect.sgml b/doc/src/sgml/pageinspect.sgml
index 3a113439e1d..4ed8826e92e 100644
--- a/doc/src/sgml/pageinspect.sgml
+++ b/doc/src/sgml/pageinspect.sgml
@@ -714,6 +714,60 @@ test=# SELECT first_tid, nbytes, tids[0:5] AS some_tids
(170,30) | 376 | {"(170,30)","(170,31)","(170,32)","(170,33)","(170,34)"}
(173,44) | 197 | {"(173,44)","(173,45)","(173,46)","(173,47)","(173,48)"}
(7 rows)
+</screen>
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>
+ <function>gin_entrypage_items(page bytea, reloid oid) returns setof record</function>
+ <indexterm>
+ <primary>gin_entrypage_items</primary>
+ </indexterm>
+ </term>
+
+ <listitem>
+ <para>
+ <function>gin_entrypage_items</function> returns information about
+ the data stored in a entry tree <acronym>GIN</acronym> page. For example:
+<screen>
+test=# select * from gin_entrypage_items(get_raw_page('gin_test_idx',
+1), 'gin_test_idx'::regclass);
+ itemoffset | downlink | tids | keys
+------------+----------+------+------------------------------------
+ 1 | (3,0) | {} | i=113
+ 2 | (5,0) | {} | j=34173cb38f07f89ddbebc2ac9128303f
+ 3 | (2,0) | {} | j=a0a080f42e6f13b3a2df133f073095dd
+ 4 | (4,0) | {} | j=fc490ca45c00b1249bbe3554a4fdf6fb
+(4 rows)
+</screen>
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>
+ <function>gin_datapage_items(page bytea) returns setof record</function>
+ <indexterm>
+ <primary>gin_datapage_items</primary>
+ </indexterm>
+ </term>
+
+ <listitem>
+ <para>
+ <function>gin_datapage_items</function> returns information about
+ the data stored in a posting tree <acronym>GIN</acronym> internal page. For example:
+<screen>
+test=# select * from gin_datapage_items(get_raw_page('gin_test_idx',
+43));
+ itemoffset | downlink | item_tid
+------------+----------+----------
+ 1 | 124 | (162,12)
+ 2 | 123 | (314,37)
+ 3 | 251 | (467,23)
+ 4 | 373 | (0,0)
+(4 rows)
</screen>
</para>
</listitem>
--
2.43.0
v14-0003-Move-IS_INDEX-macro-to-pageinspect.h.patchapplication/octet-stream; name=v14-0003-Move-IS_INDEX-macro-to-pageinspect.h.patchDownload
From f90043e70b6816727c1dac80250349b14b019b8a Mon Sep 17 00:00:00 2001
From: reshke <reshke@double.cloud>
Date: Sat, 10 Jan 2026 14:11:37 +0000
Subject: [PATCH v14 3/3] Move IS_INDEX macro to pageinspect.h
Suggested-by: Japin Li <japinli@hotmail.com>
Discussion: https://postgr.es/m/CALdSSPiN13n7feQcY0WCmq8jzxjwqhNrt1E=g=g6aZANyE_OoQ@mail.gmail.com
---
contrib/pageinspect/btreefuncs.c | 1 -
contrib/pageinspect/ginfuncs.c | 1 -
contrib/pageinspect/hashfuncs.c | 1 -
contrib/pageinspect/pageinspect.h | 2 ++
4 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/contrib/pageinspect/btreefuncs.c b/contrib/pageinspect/btreefuncs.c
index 62c905c6e7c..b8a0428ed19 100644
--- a/contrib/pageinspect/btreefuncs.c
+++ b/contrib/pageinspect/btreefuncs.c
@@ -49,7 +49,6 @@ 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)
/* ------------------------------------------------
diff --git a/contrib/pageinspect/ginfuncs.c b/contrib/pageinspect/ginfuncs.c
index c9cf08872ac..dff77068804 100644
--- a/contrib/pageinspect/ginfuncs.c
+++ b/contrib/pageinspect/ginfuncs.c
@@ -30,7 +30,6 @@ PG_FUNCTION_INFO_V1(gin_entrypage_items);
PG_FUNCTION_INFO_V1(gin_leafpage_items);
PG_FUNCTION_INFO_V1(gin_datapage_items);
-#define IS_INDEX(r) ((r)->rd_rel->relkind == RELKIND_INDEX)
#define IS_GIN(r) ((r)->rd_rel->relam == GIN_AM_OID)
Datum
diff --git a/contrib/pageinspect/hashfuncs.c b/contrib/pageinspect/hashfuncs.c
index 7fc97d043ce..6a0d59befc1 100644
--- a/contrib/pageinspect/hashfuncs.c
+++ b/contrib/pageinspect/hashfuncs.c
@@ -28,7 +28,6 @@ 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)
/* ------------------------------------------------
diff --git a/contrib/pageinspect/pageinspect.h b/contrib/pageinspect/pageinspect.h
index b241fdc97b2..7e5d28eeb4d 100644
--- a/contrib/pageinspect/pageinspect.h
+++ b/contrib/pageinspect/pageinspect.h
@@ -24,6 +24,8 @@ enum pageinspect_version
PAGEINSPECT_V1_9,
};
+#define IS_INDEX(r) ((r)->rd_rel->relkind == RELKIND_INDEX)
+
/* in rawpage.c */
extern Page get_page_from_raw(bytea *raw_page);
--
2.43.0
On 10 Jan 2026, at 19:17, Kirill Reshke <reshkekirill@gmail.com> wrote:
So,
v14-0004 with this change attached.
Did you mean 0003?
The change makes sense, but I'd note that macro is always used like this:
if (!IS_INDEX(indexRel) || !IS_HASH(indexRel))
If we are refactoring this, maybe put IS_INDEX inside of corresponding IS_BTREE(),IS_GIN() and IS_HASH()?
Best regards, Andrey Borodin.