On Sun, 11 Jan 2026 at 21:43, Andrey Borodin <[email protected]> wrote: >> On 10 Jan 2026, at 19:17, Kirill Reshke <[email protected]> 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()? >
+1 Attach the v15 patch. v15-0001 and v15-0002 unchanged and v15-0003 changed as above. -- Regards, Japin Li ChengDu WenWu Information Technology Co., Ltd.
>From 573a478e9bd54df41c6ab1b96201196a3f8e4d67 Mon Sep 17 00:00:00 2001 From: reshke <[email protected]> Date: Wed, 7 Jan 2026 19:38:52 +0000 Subject: [PATCH v15 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 <[email protected]> Reviewed-by: Chao Li <[email protected]> Reviewed-by: Japin Li <[email protected]> Discussion: https://postgr.es/m/CALdSSPiN13n7feQcY0WCmq8jzxjwqhNrt1E=g=g6azanye_...@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
>From bac3dcf6f108a953e54f371f001e8d379d784898 Mon Sep 17 00:00:00 2001 From: reshke <[email protected]> Date: Mon, 13 Oct 2025 20:14:26 +0000 Subject: [PATCH v15 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 <[email protected]> Reviewed-by: Andrey Borodin <[email protected]> Reviewed-by: Roman Khapov <[email protected]> Reviewed-by: Chao Li <[email protected]> Reviewed-by: Japin Li <[email protected]> Discussion: https://postgr.es/m/CALdSSPiN13n7feQcY0WCmq8jzxjwqhNrt1E=g=g6azanye_...@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
>From fc55c9db508cda94401166b0a90ec41ddca7300f Mon Sep 17 00:00:00 2001 From: reshke <[email protected]> Date: Sat, 10 Jan 2026 14:11:37 +0000 Subject: [PATCH v15 3/3] Move IS_INDEX macro to pageinspect.h Suggested-by: Japin Li <[email protected]>, Andrey Borodin <[email protected]> Discussion: https://postgr.es/m/CALdSSPiN13n7feQcY0WCmq8jzxjwqhNrt1E=g=g6azanye_...@mail.gmail.com --- contrib/pageinspect/btreefuncs.c | 7 +++---- contrib/pageinspect/ginfuncs.c | 5 ++--- contrib/pageinspect/hashfuncs.c | 5 ++--- contrib/pageinspect/pageinspect.h | 2 ++ 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/contrib/pageinspect/btreefuncs.c b/contrib/pageinspect/btreefuncs.c index 62c905c6e7c..3f3332f9730 100644 --- a/contrib/pageinspect/btreefuncs.c +++ b/contrib/pageinspect/btreefuncs.c @@ -49,8 +49,7 @@ 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) +#define IS_BTREE(r) (IS_INDEX(r) && (r)->rd_rel->relam == BTREE_AM_OID) /* ------------------------------------------------ * structure for single btree page statistics @@ -225,7 +224,7 @@ check_relation_block_range(Relation rel, int64 blkno) static void bt_index_block_validate(Relation rel, int64 blkno) { - if (!IS_INDEX(rel) || !IS_BTREE(rel)) + if (!IS_BTREE(rel)) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("\"%s\" is not a %s index", @@ -858,7 +857,7 @@ bt_metap(PG_FUNCTION_ARGS) relrv = makeRangeVarFromNameList(textToQualifiedNameList(relname)); rel = relation_openrv(relrv, AccessShareLock); - if (!IS_INDEX(rel) || !IS_BTREE(rel)) + if (!IS_BTREE(rel)) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("\"%s\" is not a %s index", diff --git a/contrib/pageinspect/ginfuncs.c b/contrib/pageinspect/ginfuncs.c index c9cf08872ac..4c0bac683ea 100644 --- a/contrib/pageinspect/ginfuncs.c +++ b/contrib/pageinspect/ginfuncs.c @@ -30,8 +30,7 @@ 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) +#define IS_GIN(r) (IS_INDEX(r) && (r)->rd_rel->relam == GIN_AM_OID) Datum gin_metapage_info(PG_FUNCTION_ARGS) @@ -212,7 +211,7 @@ gin_entrypage_items(PG_FUNCTION_ARGS) /* Open the index relation */ indexRel = index_open(indexRelid, AccessShareLock); - if (!IS_INDEX(indexRel) || !IS_GIN(indexRel)) + if (!IS_GIN(indexRel)) ereport(ERROR, errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("\"%s\" is not a %s index", diff --git a/contrib/pageinspect/hashfuncs.c b/contrib/pageinspect/hashfuncs.c index 7fc97d043ce..86e73b36b41 100644 --- a/contrib/pageinspect/hashfuncs.c +++ b/contrib/pageinspect/hashfuncs.c @@ -28,8 +28,7 @@ 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) +#define IS_HASH(r) (IS_INDEX(r) && (r)->rd_rel->relam == HASH_AM_OID) /* ------------------------------------------------ * structure for single hash page statistics @@ -421,7 +420,7 @@ hash_bitmap_info(PG_FUNCTION_ARGS) */ indexRel = relation_open(indexRelid, AccessShareLock); - if (!IS_INDEX(indexRel) || !IS_HASH(indexRel)) + if (!IS_HASH(indexRel)) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("\"%s\" is not a %s index", 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
