From 9af964cfddf8634e796af5facfd2476401f8ef78 Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Sun, 14 Apr 2024 23:29:35 +0300 Subject: [PATCH 2/2] XXX: Add test_heapam heappage_craft.c contains SQL-callable functions for constructing a heap page from scratch, with the exact line pointers, XIDs, flags etc. --- src/test/modules/Makefile | 1 + src/test/modules/meson.build | 1 + src/test/modules/test_heapam/Makefile | 24 ++ .../test_heapam/expected/move_in_out.out | 69 ++++ .../modules/test_heapam/expected/pruning.out | 317 ++++++++++++++++++ src/test/modules/test_heapam/heappage_craft.c | 271 +++++++++++++++ src/test/modules/test_heapam/meson.build | 35 ++ .../modules/test_heapam/sql/move_in_out.sql | 37 ++ src/test/modules/test_heapam/sql/pruning.sql | 76 +++++ .../modules/test_heapam/test_heapam--1.0.sql | 42 +++ src/test/modules/test_heapam/test_heapam.c | 114 +++++++ .../modules/test_heapam/test_heapam.control | 4 + 12 files changed, 991 insertions(+) create mode 100644 src/test/modules/test_heapam/Makefile create mode 100644 src/test/modules/test_heapam/expected/move_in_out.out create mode 100644 src/test/modules/test_heapam/expected/pruning.out create mode 100644 src/test/modules/test_heapam/heappage_craft.c create mode 100644 src/test/modules/test_heapam/meson.build create mode 100644 src/test/modules/test_heapam/sql/move_in_out.sql create mode 100644 src/test/modules/test_heapam/sql/pruning.sql create mode 100644 src/test/modules/test_heapam/test_heapam--1.0.sql create mode 100644 src/test/modules/test_heapam/test_heapam.c create mode 100644 src/test/modules/test_heapam/test_heapam.control diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile index 256799f520a..fa9e1c471b3 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -21,6 +21,7 @@ SUBDIRS = \ test_dsm_registry \ test_extensions \ test_ginpostinglist \ + test_heapam \ test_integerset \ test_json_parser \ test_lfind \ diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build index d8fe059d236..946d1157a6f 100644 --- a/src/test/modules/meson.build +++ b/src/test/modules/meson.build @@ -20,6 +20,7 @@ subdir('test_dsa') subdir('test_dsm_registry') subdir('test_extensions') subdir('test_ginpostinglist') +subdir('test_heapam') subdir('test_integerset') subdir('test_json_parser') subdir('test_lfind') diff --git a/src/test/modules/test_heapam/Makefile b/src/test/modules/test_heapam/Makefile new file mode 100644 index 00000000000..1ba90fef276 --- /dev/null +++ b/src/test/modules/test_heapam/Makefile @@ -0,0 +1,24 @@ +# src/test/modules/test_heapam/Makefile + +MODULE_big = test_heapam +OBJS = \ + $(WIN32RES) \ + heappage_craft.o \ + test_heapam.o +PGFILEDESC = "test_heapam - test code for heap AM" + +EXTENSION = test_heapam +DATA = test_heapam--1.0.sql + +REGRESS = move_in_out pruning + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_heapam +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_heapam/expected/move_in_out.out b/src/test/modules/test_heapam/expected/move_in_out.out new file mode 100644 index 00000000000..70090caac3d --- /dev/null +++ b/src/test/modules/test_heapam/expected/move_in_out.out @@ -0,0 +1,69 @@ +CREATE EXTENSION pageinspect; +CREATE EXTENSION test_heapam; +CREATE TEMP TABLE prunetest (data text); +select pg_current_xact_id() as committed_xid1 +\gset +create temporary view dump_items as + SELECT lp, case lp_flags + when 0 then 'LP_UNUSED' + when 1 then 'LP_NORMAL' + when 2 then 'LP_REDIRECT to ' || lp_off + when 3 then 'LP_DEAD' END as lp_flags, + t_xmin, t_xmax, t_field3, + (heap_tuple_infomask_flags(t_infomask, t_infomask2)).*, + convert_from(substring(t_data, 2), 'utf8') as data + FROM heap_page_items(get_raw_page('prunetest', 0)); +select heappage_craft_new(); + heappage_craft_new +-------------------- + +(1 row) + +-- lp xmin xmax xvac/cid ctid flags data +select heappage_craft_add_tuple('1', '2', '0', :'committed_xid1', '(0, 1)', array['HEAP_MOVED_OFF']::text[], 'normal'); + heappage_craft_add_tuple +-------------------------- + +(1 row) + +select heappage_craft_add_tuple('2', '2', '0', :'committed_xid1', '(0, 2)', array['HEAP_MOVED_IN']::text[], 'normal'); + heappage_craft_add_tuple +-------------------------- + +(1 row) + +select heappage_craft_install('prunetest'::regclass, 0); + heappage_craft_install +------------------------ + +(1 row) + +select * from dump_items; + lp | lp_flags | t_xmin | t_xmax | t_field3 | raw_flags | combined_flags | data +----+-----------+--------+--------+----------+-----------------------------------+----------------+-------- + 1 | LP_NORMAL | 2 | 0 | 743 | {HEAP_HASVARWIDTH,HEAP_MOVED_OFF} | {} | normal + 2 | LP_NORMAL | 2 | 0 | 743 | {HEAP_HASVARWIDTH,HEAP_MOVED_IN} | {} | normal +(2 rows) + +select xmin, xmax, ctid, * from prunetest; + xmin | xmax | ctid | data +------+------+-------+-------- + 2 | 0 | (0,2) | normal +(1 row) + +select * from dump_items; + lp | lp_flags | t_xmin | t_xmax | t_field3 | raw_flags | combined_flags | data +----+-----------+--------+--------+----------+------------------------------------------------------------------------+----------------+-------- + 1 | LP_NORMAL | 2 | 0 | 743 | {HEAP_HASVARWIDTH,HEAP_XMIN_INVALID,HEAP_MOVED_OFF} | {} | normal + 2 | LP_NORMAL | 2 | 0 | 743 | {HEAP_HASVARWIDTH,HEAP_XMIN_COMMITTED,HEAP_XMAX_INVALID,HEAP_MOVED_IN} | {} | normal +(2 rows) + +--SELECT *, (heap_tuple_infomask_flags(t_infomask, t_infomask2)).* FROM heap_page_items(get_raw_page('prunetest', 0)); +vacuum freeze prunetest; +select * from dump_items; + lp | lp_flags | t_xmin | t_xmax | t_field3 | raw_flags | combined_flags | data +----+-----------+--------+--------+----------+------------------------------------------------------------------------+----------------+-------- + 1 | LP_UNUSED | | | | | | + 2 | LP_NORMAL | 2 | 0 | 2 | {HEAP_HASVARWIDTH,HEAP_XMIN_COMMITTED,HEAP_XMAX_INVALID,HEAP_MOVED_IN} | {} | normal +(2 rows) + diff --git a/src/test/modules/test_heapam/expected/pruning.out b/src/test/modules/test_heapam/expected/pruning.out new file mode 100644 index 00000000000..56f7254332c --- /dev/null +++ b/src/test/modules/test_heapam/expected/pruning.out @@ -0,0 +1,317 @@ +CREATE EXTENSION pageinspect; +ERROR: extension "pageinspect" already exists +CREATE EXTENSION test_heapam; +ERROR: extension "test_heapam" already exists +CREATE TABLE prunetest (data text) WITH (autovacuum_enabled=false); +select pg_current_xact_id() as committed_xid +\gset +begin; +select pg_current_xact_id() as aborted_xid; + aborted_xid +------------- + 749 +(1 row) + +\gset +rollback; +create temporary view dump_items as + SELECT lp, case when lp_flags = 2 then lp_off else null end as redirect_off, t_xmin, t_xmax, + (heap_tuple_infomask_flags(t_infomask, t_infomask2)).*, + convert_from(substring(t_data, 2), 'utf8') as data + FROM heap_page_items(get_raw_page('prunetest', 0)); +select heappage_craft_new(); + heappage_craft_new +-------------------- + +(1 row) + +select heappage_craft_add_lp_unused('1'); + heappage_craft_add_lp_unused +------------------------------ + +(1 row) + +select heappage_craft_add_tuple('1', :'committed_xid', '0', '0', '(0, 1)', array[]::text[], 'normal'); + heappage_craft_add_tuple +-------------------------- + +(1 row) + +select heappage_craft_install('prunetest'::regclass, 0); + heappage_craft_install +------------------------ + +(1 row) + +select * from dump_items; + lp | redirect_off | t_xmin | t_xmax | raw_flags | combined_flags | data +----+--------------+--------+--------+--------------------+----------------+-------- + 1 | | 748 | 0 | {HEAP_HASVARWIDTH} | {} | normal +(1 row) + +--SELECT *, (heap_tuple_infomask_flags(t_infomask, t_infomask2)).* FROM heap_page_items(get_raw_page('prunetest', 0)); +SELECT heappage_prune_and_freeze('prunetest', 0); +NOTICE: prune results: + ndeleted: 0 + nnewlpdead: 0 + nfrozen: 1 + live_tuples: 1 + recently_dead_tuples: 0 + all_visible: 1 + all_frozen: 1 + vm_conflict_horizon: 0 + hastup: 1 + conflict_xid: 748 + deadoffsets: [] + new_relfrozen_xid: 752 + new_relmin_mxid: 1 + + heappage_prune_and_freeze +--------------------------- + +(1 row) + +select * from dump_items; + lp | redirect_off | t_xmin | t_xmax | raw_flags | combined_flags | data +----+--------------+--------+--------+----------------------------------------------------------------------------+--------------------+-------- + 1 | | 748 | 0 | {HEAP_HASVARWIDTH,HEAP_XMIN_COMMITTED,HEAP_XMIN_INVALID,HEAP_XMAX_INVALID} | {HEAP_XMIN_FROZEN} | normal +(1 row) + +-- Page has two LP_DEAD items, one LP_UNUSED, nothing else. +select heappage_craft_new(); + heappage_craft_new +-------------------- + +(1 row) + +select heappage_craft_add_lp_dead('1'); + heappage_craft_add_lp_dead +---------------------------- + +(1 row) + +select heappage_craft_add_lp_unused('2'); + heappage_craft_add_lp_unused +------------------------------ + +(1 row) + +select heappage_craft_add_lp_dead('3'); + heappage_craft_add_lp_dead +---------------------------- + +(1 row) + +select heappage_craft_install('prunetest'::regclass, 0); + heappage_craft_install +------------------------ + +(1 row) + +select heappage_prune_and_freeze('prunetest', 0); +NOTICE: prune results: + ndeleted: 0 + nnewlpdead: 0 + nfrozen: 0 + live_tuples: 0 + recently_dead_tuples: 0 + all_visible: 0 + all_frozen: 0 + vm_conflict_horizon: 0 + hastup: 0 + conflict_xid: 0 + deadoffsets: [3, 1] + new_relfrozen_xid: 754 + new_relmin_mxid: 1 + + heappage_prune_and_freeze +--------------------------- + +(1 row) + +-- One aborted item, nothing else. +select heappage_craft_new(); + heappage_craft_new +-------------------- + +(1 row) + +select heappage_craft_add_tuple('1', :'aborted_xid', '0', '0', '(0, 1)', array[]::text[], 'aborted'); + heappage_craft_add_tuple +-------------------------- + +(1 row) + +select heappage_craft_install('prunetest'::regclass, 0); + heappage_craft_install +------------------------ + +(1 row) + +select heappage_prune_and_freeze('prunetest', 0); +NOTICE: prune results: + ndeleted: 1 + nnewlpdead: 1 + nfrozen: 0 + live_tuples: 0 + recently_dead_tuples: 0 + all_visible: 0 + all_frozen: 0 + vm_conflict_horizon: 0 + hastup: 0 + conflict_xid: 0 + deadoffsets: [1] + new_relfrozen_xid: 756 + new_relmin_mxid: 1 + + heappage_prune_and_freeze +--------------------------- + +(1 row) + +-- One already-frozen item, nothing else. +select heappage_craft_new(); + heappage_craft_new +-------------------- + +(1 row) + +select heappage_craft_add_tuple('1', '2', '0', '0', '(0, 1)', array[]::text[], 'normal'); + heappage_craft_add_tuple +-------------------------- + +(1 row) + +select heappage_craft_install('prunetest'::regclass, 0); + heappage_craft_install +------------------------ + +(1 row) + +select heappage_prune_and_freeze('prunetest', 0); +NOTICE: prune results: + ndeleted: 0 + nnewlpdead: 0 + nfrozen: 0 + live_tuples: 1 + recently_dead_tuples: 0 + all_visible: 1 + all_frozen: 1 + vm_conflict_horizon: 0 + hastup: 1 + conflict_xid: 0 + deadoffsets: [] + new_relfrozen_xid: 758 + new_relmin_mxid: 1 + + heappage_prune_and_freeze +--------------------------- + +(1 row) + +-- One committed item, nothing else. +select pg_current_xact_id() as committed_xid +\gset +select heappage_craft_new(); + heappage_craft_new +-------------------- + +(1 row) + +select heappage_craft_add_tuple('1', :'committed_xid', '0', '0', '(0, 1)', array[]::text[], 'normal'); + heappage_craft_add_tuple +-------------------------- + +(1 row) + +select heappage_craft_install('prunetest'::regclass, 0); + heappage_craft_install +------------------------ + +(1 row) + +select heappage_prune_and_freeze('prunetest', 0); +NOTICE: prune results: + ndeleted: 0 + nnewlpdead: 0 + nfrozen: 1 + live_tuples: 1 + recently_dead_tuples: 0 + all_visible: 1 + all_frozen: 1 + vm_conflict_horizon: 0 + hastup: 1 + conflict_xid: 759 + deadoffsets: [] + new_relfrozen_xid: 761 + new_relmin_mxid: 1 + + heappage_prune_and_freeze +--------------------------- + +(1 row) + +-- One visible item, one deleted item +select pg_current_xact_id() as committed_xid1 +\gset +select pg_current_xact_id() as committed_xid2 +\gset +select heappage_craft_new(); + heappage_craft_new +-------------------- + +(1 row) + +select heappage_craft_add_tuple('1', :'committed_xid1', '0', '0', '(0, 1)', array[]::text[], 'normal'); + heappage_craft_add_tuple +-------------------------- + +(1 row) + +select heappage_craft_add_tuple('2', '2', :'committed_xid2', '0', '(0, 1)', array[]::text[], 'deleted'); + heappage_craft_add_tuple +-------------------------- + +(1 row) + +select heappage_craft_install('prunetest'::regclass, 0); + heappage_craft_install +------------------------ + +(1 row) + +select * from dump_items; + lp | redirect_off | t_xmin | t_xmax | raw_flags | combined_flags | data +----+--------------+--------+--------+--------------------+----------------+--------- + 1 | | 762 | 0 | {HEAP_HASVARWIDTH} | {} | normal + 2 | | 2 | 763 | {HEAP_HASVARWIDTH} | {} | deleted +(2 rows) + +select heappage_prune_and_freeze('prunetest', 0); +NOTICE: prune results: + ndeleted: 1 + nnewlpdead: 1 + nfrozen: 1 + live_tuples: 1 + recently_dead_tuples: 0 + all_visible: 0 + all_frozen: 0 + vm_conflict_horizon: 762 + hastup: 1 + conflict_xid: 763 + deadoffsets: [2] + new_relfrozen_xid: 765 + new_relmin_mxid: 1 + + heappage_prune_and_freeze +--------------------------- + +(1 row) + +select * from dump_items; + lp | redirect_off | t_xmin | t_xmax | raw_flags | combined_flags | data +----+--------------+--------+--------+----------------------------------------------------------------------------+--------------------+-------- + 1 | | 762 | 0 | {HEAP_HASVARWIDTH,HEAP_XMIN_COMMITTED,HEAP_XMIN_INVALID,HEAP_XMAX_INVALID} | {HEAP_XMIN_FROZEN} | normal + 2 | | | | | | +(2 rows) + diff --git a/src/test/modules/test_heapam/heappage_craft.c b/src/test/modules/test_heapam/heappage_craft.c new file mode 100644 index 00000000000..78f99a47829 --- /dev/null +++ b/src/test/modules/test_heapam/heappage_craft.c @@ -0,0 +1,271 @@ +/*-------------------------------------------------------------------------- + * + * heappage_craft.c + * Functions to craft heap pages with specific contents + * + * Copyright (c) 2022-2024, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_heapam/heappage_craft.c + * + * ------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/htup_details.h" +#include "access/table.h" +#include "fmgr.h" +#include "funcapi.h" +#include "miscadmin.h" +#include "storage/bufmgr.h" +#include "storage/bufpage.h" +#include "utils/array.h" +#include "utils/builtins.h" +#include "utils/rel.h" + +/* Temporary Page image we're currently crafting */ +static Page crafted_page = NULL; + +static OffsetNumber heappage_craft_add_line_pointer(OffsetNumber offnum, ItemIdData itemid); + +/* Initialize a new empty page. Must be called before the other functions */ +PG_FUNCTION_INFO_V1(heappage_craft_new); +Datum +heappage_craft_new(PG_FUNCTION_ARGS) +{ + if (!superuser()) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("must be superuser to use test_heapam functions"))); + + if (crafted_page == NULL) + crafted_page = MemoryContextAlloc(TopMemoryContext, BLCKSZ); + PageInit(crafted_page, BLCKSZ, 0); + + PG_RETURN_VOID(); +} + +/* Install the currently crafted page into a table */ +PG_FUNCTION_INFO_V1(heappage_craft_install); +Datum +heappage_craft_install(PG_FUNCTION_ARGS) +{ + Oid table_oid = PG_GETARG_OID(0); + BlockNumber blkno = PG_GETARG_UINT32(1); + Relation rel; + BlockNumber numblocks; + Buffer buf; + + if (!superuser()) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("must be superuser to use test_heapam functions"))); + + rel = table_open(table_oid, AccessExclusiveLock); + + numblocks = RelationGetNumberOfBlocksInFork(rel, MAIN_FORKNUM); + if (blkno == numblocks) + { + buf = ReadBufferExtended(rel, MAIN_FORKNUM, P_NEW, RBM_ZERO_AND_LOCK, NULL); + } + else + { + if (blkno >= RelationGetNumberOfBlocksInFork(rel, MAIN_FORKNUM)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("block number %u is out of range for relation \"%s\"", + blkno, RelationGetRelationName(rel)))); + buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_ZERO_AND_LOCK, NULL); + } + + memcpy(BufferGetPage(buf), crafted_page, BLCKSZ); + + LockBuffer(buf, BUFFER_LOCK_UNLOCK); + ReleaseBuffer(buf); + + table_close(rel, AccessExclusiveLock); + + PG_RETURN_VOID(); +} + +static void +flags_to_infomask(ArrayType *flags, uint16 *infomask, uint16 *infomask2) +{ + Datum *elems; + int nelems; + + deconstruct_array(flags, TEXTOID, -1, false, TYPALIGN_INT, + &elems, NULL, &nelems); + for (int i = 0; i < nelems; i++) + { + char *flag = TextDatumGetCString(elems[i]); + + if (strcmp(flag, "HEAP_HASNULL") == 0) + (*infomask) |= HEAP_HASNULL; + else if (strcmp(flag, "HEAP_HASVARWIDTH") == 0) + (*infomask) |= HEAP_HASVARWIDTH; + else if (strcmp(flag, "HEAP_HASEXTERNAL") == 0) + (*infomask) |= HEAP_HASEXTERNAL; + else if (strcmp(flag, "HEAP_HASOID_OLD") == 0) + (*infomask) |= HEAP_HASOID_OLD; + else if (strcmp(flag, "HEAP_XMAX_KEYSHR_LOCK") == 0) + (*infomask) |= HEAP_XMAX_KEYSHR_LOCK; + else if (strcmp(flag, "HEAP_COMBOCID") == 0) + (*infomask) |= HEAP_COMBOCID; + else if (strcmp(flag, "HEAP_XMAX_EXCL_LOCK") == 0) + (*infomask) |= HEAP_XMAX_EXCL_LOCK; + else if (strcmp(flag, "HEAP_XMAX_LOCK_ONLY") == 0) + (*infomask) |= HEAP_XMAX_LOCK_ONLY; + else if (strcmp(flag, "HEAP_XMIN_COMMITTED") == 0) + (*infomask) |= HEAP_XMIN_COMMITTED; + else if (strcmp(flag, "HEAP_XMIN_INVALID") == 0) + (*infomask) |= HEAP_XMIN_INVALID; + else if (strcmp(flag, "HEAP_XMAX_COMMITTED") == 0) + (*infomask) |= HEAP_XMAX_COMMITTED; + else if (strcmp(flag, "HEAP_XMAX_INVALID") == 0) + (*infomask) |= HEAP_XMAX_INVALID; + else if (strcmp(flag, "HEAP_XMAX_IS_MULTI") == 0) + (*infomask) |= HEAP_XMAX_IS_MULTI; + else if (strcmp(flag, "HEAP_UPDATED") == 0) + (*infomask) |= HEAP_UPDATED; + else if (strcmp(flag, "HEAP_MOVED_OFF") == 0) + (*infomask) |= HEAP_MOVED_OFF; + else if (strcmp(flag, "HEAP_MOVED_IN") == 0) + (*infomask) |= HEAP_MOVED_IN; + + else if (strcmp(flag, " HEAP_KEYS_UPDATED") == 0) + (*infomask2) |= HEAP_KEYS_UPDATED; + else if (strcmp(flag, " HEAP_HOT_UPDATED") == 0) + (*infomask2) |= HEAP_HOT_UPDATED; + else if (strcmp(flag, " HEAP_ONLY_TUPLE") == 0) + (*infomask2) |= HEAP_ONLY_TUPLE; + else + elog(ERROR, "unknown heap tuple flag %s", flag); + + pfree(flag); + } +} + +/* Add a tuple to page being crafted */ +PG_FUNCTION_INFO_V1(heappage_craft_add_tuple); +Datum +heappage_craft_add_tuple(PG_FUNCTION_ARGS) +{ + OffsetNumber offnum = PG_GETARG_UINT16(0); + TransactionId xmin = PG_GETARG_TRANSACTIONID(1); + TransactionId xmax = PG_GETARG_TRANSACTIONID(2); + CommandId cid = PG_GETARG_UINT32(3); + ItemPointer ctid = PG_GETARG_ITEMPOINTER(4); + ArrayType *infoflags = PG_GETARG_ARRAYTYPE_P(5); + Datum data = PG_GETARG_DATUM(6); + bool isnull = false; + HeapTuple htup; + TupleDesc tupdesc; + + if (!superuser()) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("must be superuser to use test_heapam functions"))); + + tupdesc = CreateTemplateTupleDesc(1); + TupleDescInitEntry(tupdesc, (AttrNumber) 1, "data", + TEXTOID, -1, 0); + tupdesc = BlessTupleDesc(tupdesc); + + htup = heap_form_tuple(tupdesc, &data, &isnull); + + htup->t_data->t_choice.t_heap.t_xmin = xmin; + htup->t_data->t_choice.t_heap.t_xmax = xmax; + htup->t_data->t_choice.t_heap.t_field3.t_cid = cid; + htup->t_data->t_ctid = *ctid; + flags_to_infomask(infoflags, + &htup->t_data->t_infomask, + &htup->t_data->t_infomask2); + + if (PageAddItemExtended(crafted_page, (Item) htup->t_data, htup->t_len, offnum, + PAI_OVERWRITE | PAI_IS_HEAP) == InvalidOffsetNumber) + elog(ERROR, "failed to add tuple"); + + PG_RETURN_VOID(); +} + +PG_FUNCTION_INFO_V1(heappage_craft_add_lp_unused); +Datum +heappage_craft_add_lp_unused(PG_FUNCTION_ARGS) +{ + OffsetNumber offnum = PG_GETARG_UINT16(0); + ItemIdData iid; + + if (!superuser()) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("must be superuser to use test_heapam functions"))); + + ItemIdSetUnused(&iid); + heappage_craft_add_line_pointer(offnum, iid); + + PG_RETURN_VOID(); +} + +PG_FUNCTION_INFO_V1(heappage_craft_add_lp_redirect); +Datum +heappage_craft_add_lp_redirect(PG_FUNCTION_ARGS) +{ + OffsetNumber offnum = PG_GETARG_UINT16(0); + OffsetNumber redirect_offnum = PG_GETARG_UINT16(1); + ItemIdData iid; + + if (!superuser()) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("must be superuser to use test_heapam functions"))); + + ItemIdSetRedirect(&iid, redirect_offnum); + heappage_craft_add_line_pointer(offnum, iid); + + PG_RETURN_VOID(); +} + +PG_FUNCTION_INFO_V1(heappage_craft_add_lp_dead); +Datum +heappage_craft_add_lp_dead(PG_FUNCTION_ARGS) +{ + OffsetNumber offnum = PG_GETARG_UINT16(0); + ItemIdData iid; + + if (!superuser()) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("must be superuser to use test_heapam functions"))); + + ItemIdSetDead(&iid); + heappage_craft_add_line_pointer(offnum, iid); + + PG_RETURN_VOID(); +} + +static OffsetNumber +heappage_craft_add_line_pointer(OffsetNumber offnum, ItemIdData itemid) +{ + PageHeader phdr = (PageHeader) crafted_page; + OffsetNumber limit; + + /* Reject placing items beyond the first unused line pointer */ + limit = OffsetNumberNext(PageGetMaxOffsetNumber(crafted_page)); + if (offnum > limit) + { + elog(WARNING, "specified item offset is too large"); + return InvalidOffsetNumber; + } + if (offnum == limit) + { + uint16 new_lower = phdr->pd_lower + sizeof(ItemIdData); + + if (new_lower > phdr->pd_upper) + return InvalidOffsetNumber; + phdr->pd_lower = new_lower; + } + phdr->pd_linp[offnum - 1] = itemid; + + return offnum; +} diff --git a/src/test/modules/test_heapam/meson.build b/src/test/modules/test_heapam/meson.build new file mode 100644 index 00000000000..f0fe701bf24 --- /dev/null +++ b/src/test/modules/test_heapam/meson.build @@ -0,0 +1,35 @@ +# Copyright (c) 2022-2024, PostgreSQL Global Development Group + +test_heapam_sources = files( + 'heappage_craft.c', + 'test_heapam.c', +) + +if host_system == 'windows' + test_heapam_sources += rc_lib_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'test_heapam', + '--FILEDESC', 'test_heapam - test code for heap AM',]) +endif + +test_heapam = shared_module('test_heapam', + test_heapam_sources, + kwargs: pg_test_mod_args, +) +test_install_libs += test_heapam + +test_install_data += files( + 'test_heapam.control', + 'test_heapam--1.0.sql', +) + +tests += { + 'name': 'test_heapam', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'regress': { + 'sql': [ + 'move_in_out', + 'pruning', + ], + }, +} diff --git a/src/test/modules/test_heapam/sql/move_in_out.sql b/src/test/modules/test_heapam/sql/move_in_out.sql new file mode 100644 index 00000000000..15a5a9686bb --- /dev/null +++ b/src/test/modules/test_heapam/sql/move_in_out.sql @@ -0,0 +1,37 @@ +CREATE EXTENSION pageinspect; +CREATE EXTENSION test_heapam; + +CREATE TEMP TABLE prunetest (data text); + +select pg_current_xact_id() as committed_xid1 +\gset + +create temporary view dump_items as + SELECT lp, case lp_flags + when 0 then 'LP_UNUSED' + when 1 then 'LP_NORMAL' + when 2 then 'LP_REDIRECT to ' || lp_off + when 3 then 'LP_DEAD' END as lp_flags, + t_xmin, t_xmax, t_field3, + (heap_tuple_infomask_flags(t_infomask, t_infomask2)).*, + convert_from(substring(t_data, 2), 'utf8') as data + FROM heap_page_items(get_raw_page('prunetest', 0)); + +select heappage_craft_new(); + +-- lp xmin xmax xvac/cid ctid flags data +select heappage_craft_add_tuple('1', '2', '0', :'committed_xid1', '(0, 1)', array['HEAP_MOVED_OFF']::text[], 'normal'); +select heappage_craft_add_tuple('2', '2', '0', :'committed_xid1', '(0, 2)', array['HEAP_MOVED_IN']::text[], 'normal'); + +select heappage_craft_install('prunetest'::regclass, 0); + +select * from dump_items; + +select xmin, xmax, ctid, * from prunetest; + +select * from dump_items; +--SELECT *, (heap_tuple_infomask_flags(t_infomask, t_infomask2)).* FROM heap_page_items(get_raw_page('prunetest', 0)); + +vacuum freeze prunetest; + +select * from dump_items; diff --git a/src/test/modules/test_heapam/sql/pruning.sql b/src/test/modules/test_heapam/sql/pruning.sql new file mode 100644 index 00000000000..360b6c6b052 --- /dev/null +++ b/src/test/modules/test_heapam/sql/pruning.sql @@ -0,0 +1,76 @@ +CREATE EXTENSION pageinspect; +CREATE EXTENSION test_heapam; + +CREATE TABLE prunetest (data text) WITH (autovacuum_enabled=false); + +select pg_current_xact_id() as committed_xid +\gset + +begin; +select pg_current_xact_id() as aborted_xid; +\gset +rollback; + +create temporary view dump_items as + SELECT lp, case when lp_flags = 2 then lp_off else null end as redirect_off, t_xmin, t_xmax, + (heap_tuple_infomask_flags(t_infomask, t_infomask2)).*, + convert_from(substring(t_data, 2), 'utf8') as data + FROM heap_page_items(get_raw_page('prunetest', 0)); + +select heappage_craft_new(); +select heappage_craft_add_lp_unused('1'); +select heappage_craft_add_tuple('1', :'committed_xid', '0', '0', '(0, 1)', array[]::text[], 'normal'); +select heappage_craft_install('prunetest'::regclass, 0); + +select * from dump_items; +--SELECT *, (heap_tuple_infomask_flags(t_infomask, t_infomask2)).* FROM heap_page_items(get_raw_page('prunetest', 0)); + +SELECT heappage_prune_and_freeze('prunetest', 0); + +select * from dump_items; + + +-- Page has two LP_DEAD items, one LP_UNUSED, nothing else. +select heappage_craft_new(); +select heappage_craft_add_lp_dead('1'); +select heappage_craft_add_lp_unused('2'); +select heappage_craft_add_lp_dead('3'); +select heappage_craft_install('prunetest'::regclass, 0); + +select heappage_prune_and_freeze('prunetest', 0); + +-- One aborted item, nothing else. +select heappage_craft_new(); +select heappage_craft_add_tuple('1', :'aborted_xid', '0', '0', '(0, 1)', array[]::text[], 'aborted'); +select heappage_craft_install('prunetest'::regclass, 0); +select heappage_prune_and_freeze('prunetest', 0); + +-- One already-frozen item, nothing else. +select heappage_craft_new(); +select heappage_craft_add_tuple('1', '2', '0', '0', '(0, 1)', array[]::text[], 'normal'); +select heappage_craft_install('prunetest'::regclass, 0); +select heappage_prune_and_freeze('prunetest', 0); + +-- One committed item, nothing else. +select pg_current_xact_id() as committed_xid +\gset +select heappage_craft_new(); +select heappage_craft_add_tuple('1', :'committed_xid', '0', '0', '(0, 1)', array[]::text[], 'normal'); +select heappage_craft_install('prunetest'::regclass, 0); +select heappage_prune_and_freeze('prunetest', 0); + + +-- One visible item, one deleted item +select pg_current_xact_id() as committed_xid1 +\gset + +select pg_current_xact_id() as committed_xid2 +\gset + +select heappage_craft_new(); +select heappage_craft_add_tuple('1', :'committed_xid1', '0', '0', '(0, 1)', array[]::text[], 'normal'); +select heappage_craft_add_tuple('2', '2', :'committed_xid2', '0', '(0, 1)', array[]::text[], 'deleted'); +select heappage_craft_install('prunetest'::regclass, 0); +select * from dump_items; +select heappage_prune_and_freeze('prunetest', 0); +select * from dump_items; diff --git a/src/test/modules/test_heapam/test_heapam--1.0.sql b/src/test/modules/test_heapam/test_heapam--1.0.sql new file mode 100644 index 00000000000..54141fd93f0 --- /dev/null +++ b/src/test/modules/test_heapam/test_heapam--1.0.sql @@ -0,0 +1,42 @@ +/* src/test/modules/test_heapam/test_heapam--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_heapam" to load this file. \quit + + +CREATE FUNCTION heappage_prune_and_freeze(rel regclass, blkno int4) + RETURNS void + AS 'MODULE_PATHNAME' LANGUAGE C; + + +CREATE FUNCTION heappage_craft_new() + RETURNS void + AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION heappage_craft_install(rel regclass, blkno int4) + RETURNS void + AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION heappage_craft_add_tuple( + offnum int2, + xmin xid, + xmax xid, + cid cid, + ctid tid, + infoflags text[], + data text +) + RETURNS void + AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION heappage_craft_add_lp_unused(offnum int2) + RETURNS void + AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION heappage_craft_add_lp_redirect(offnum int2, redirect_offnum int2) + RETURNS void + AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION heappage_craft_add_lp_dead(offnum int2) + RETURNS void + AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/test_heapam/test_heapam.c b/src/test/modules/test_heapam/test_heapam.c new file mode 100644 index 00000000000..311d39f09bf --- /dev/null +++ b/src/test/modules/test_heapam/test_heapam.c @@ -0,0 +1,114 @@ +/*-------------------------------------------------------------------------- + * + * test_heapam.c + * Heap AM test functinos + * + * Copyright (c) 2022-2024, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_heapam/test_heapam.c + * + * ------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/heapam.h" +#include "access/htup_details.h" +#include "access/table.h" +#include "commands/vacuum.h" +#include "fmgr.h" +#include "funcapi.h" +#include "miscadmin.h" +#include "storage/bufmgr.h" +#include "storage/bufpage.h" +#include "utils/array.h" +#include "utils/builtins.h" +#include "utils/rel.h" + +PG_MODULE_MAGIC; + +PG_FUNCTION_INFO_V1(heappage_prune_and_freeze); +Datum +heappage_prune_and_freeze(PG_FUNCTION_ARGS) +{ + Oid table_oid = PG_GETARG_OID(0); + BlockNumber blkno = PG_GETARG_UINT32(1); + Relation rel; + BlockNumber numblocks; + Buffer buf; + VacuumParams vacuum_params; + struct GlobalVisState *vistest; + struct VacuumCutoffs cutoffs; + PruneFreezeResult presult; + OffsetNumber off_loc; + TransactionId new_relfrozen_xid; + MultiXactId new_relmin_mxid; + StringInfoData sinfo; + + if (!superuser()) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("must be superuser to use test_heapam functions"))); + + rel = table_open(table_oid, AccessExclusiveLock); + + numblocks = RelationGetNumberOfBlocksInFork(rel, MAIN_FORKNUM); + if (blkno >= numblocks) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("block number %u is out of range for relation \"%s\"", + blkno, RelationGetRelationName(rel)))); + + buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL, NULL); + LockBufferForCleanup(buf); + + memset(&vacuum_params, 0, sizeof(VacuumParams)); + vacuum_params.options = VACOPT_VACUUM | VACOPT_PROCESS_MAIN | VACOPT_FREEZE; + vacuum_params.freeze_min_age = 0; + vacuum_params.multixact_freeze_min_age = 0; + + (void) vacuum_get_cutoffs(rel, &vacuum_params, &cutoffs); + vistest = GlobalVisTestFor(rel); + + new_relfrozen_xid = cutoffs.OldestXmin; + new_relmin_mxid = cutoffs.OldestMxact; + heap_page_prune_and_freeze(rel, buf, vistest, HEAP_PAGE_PRUNE_FREEZE, + &cutoffs, &presult, + PRUNE_VACUUM_SCAN, &off_loc, + &new_relfrozen_xid, &new_relmin_mxid); + + initStringInfo(&sinfo); + appendStringInfo(&sinfo, "prune results:\n"); + appendStringInfo(&sinfo, " ndeleted: %d\n", presult.ndeleted); + appendStringInfo(&sinfo, " nnewlpdead: %d\n", presult.nnewlpdead); + appendStringInfo(&sinfo, " nfrozen: %d\n", presult.nfrozen); + appendStringInfo(&sinfo, " live_tuples: %d\n", presult.live_tuples); + appendStringInfo(&sinfo, " recently_dead_tuples: %d\n", presult.recently_dead_tuples); + + appendStringInfo(&sinfo, " all_visible: %d\n", presult.all_visible); + appendStringInfo(&sinfo, " all_frozen: %d\n", presult.all_frozen); + appendStringInfo(&sinfo, " vm_conflict_horizon: %u\n", presult.vm_conflict_horizon); + appendStringInfo(&sinfo, " hastup: %d\n", presult.hastup); + appendStringInfo(&sinfo, " conflict_xid: %u\n", presult.conflict_xid); + + appendStringInfoString(&sinfo, " deadoffsets: ["); + for (int i = 0; i < presult.lpdead_items; i++) + { + if (i > 0) + appendStringInfoString(&sinfo, ", "); + appendStringInfo(&sinfo, "%d", presult.deadoffsets[i]); + } + appendStringInfoString(&sinfo, "]\n"); + + appendStringInfo(&sinfo, " new_relfrozen_xid: %u\n", new_relfrozen_xid); + appendStringInfo(&sinfo, " new_relmin_mxid: %u\n", new_relmin_mxid); + + LockBuffer(buf, BUFFER_LOCK_UNLOCK); + ReleaseBuffer(buf); + + table_close(rel, AccessExclusiveLock); + + elog(NOTICE, "%s", sinfo.data); + + PG_RETURN_VOID(); +} diff --git a/src/test/modules/test_heapam/test_heapam.control b/src/test/modules/test_heapam/test_heapam.control new file mode 100644 index 00000000000..4dd825e6b6e --- /dev/null +++ b/src/test/modules/test_heapam/test_heapam.control @@ -0,0 +1,4 @@ +comment = 'Test code for heap AM' +default_version = '1.0' +module_pathname = '$libdir/test_heapam' +relocatable = true -- 2.39.2