From fec2708403f655260a14f148b6ff24c220219d0e Mon Sep 17 00:00:00 2001 From: reshke 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