While working on the "Combine Prune and Freeze records emitted by
vacuum" patch [1], I wished we would have an easier way to test pruning.
There's a lot of logic with following HOT chains etc., and it's very
hard to construct all those scenarios just by INSERT/UPDATE/DELETE
commands. In principle though, pruning should be very amenable for good
test coverage. The input is one heap page and some parameters, the
output is one heap page and a few other fields that are already packaged
neatly in the PruneFreezeResult struct.
Back then, I started to work on a little tool for that to verify the
correctness of pruning refactoring, but I never got around to polish it
or write proper repeatable tests with it. I did use it for some ad hoc
testing, though.
I don't know when I'll find the time to polish it, so here is the very
rough work-in-progress version I've got now.
One thing I used this for was to test that we still handle
HEAP_MOVED_IN/OFF correctly. Yes, it still works. But what surprised me
is that when a HEAP_MOVED_IN tuple is frozen, we replace xvac with
FrozenTransactondId, and leave the HEAP_MOVED_IN flag in place. I
assumed that we would clear the HEAP_MOVED_IN flag instead.
[1]
https://www.postgresql.org/message-id/CAAKRu_azf-zH%3DDgVbquZ3tFWjMY1w5pO8m-TXJaMdri8z3933g%40mail.gmail.com
--
Heikki Linnakangas
Neon (https://neon.tech)
From 9dcd528dd42f689e207d808bee388fb233b2e25e Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <heikki.linnakan...@iki.fi>
Date: Sun, 14 Apr 2024 22:50:10 +0300
Subject: [PATCH 1/2] Expose conflict_xid to caller, for tests
---
src/backend/access/heap/pruneheap.c | 20 +++++++++++---------
src/include/access/heapam.h | 5 +++++
2 files changed, 16 insertions(+), 9 deletions(-)
diff --git a/src/backend/access/heap/pruneheap.c b/src/backend/access/heap/pruneheap.c
index d2eecaf7ebc..8f4d17f7d08 100644
--- a/src/backend/access/heap/pruneheap.c
+++ b/src/backend/access/heap/pruneheap.c
@@ -366,6 +366,7 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer,
bool do_hint;
bool hint_bit_fpi;
int64 fpi_before = pgWalUsage.wal_fpi;
+ TransactionId conflict_xid = InvalidTransactionId;
/* Copy parameters to prstate */
prstate.vistest = vistest;
@@ -794,7 +795,6 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer,
/*
* Emit a WAL XLOG_HEAP2_PRUNE_FREEZE record showing what we did
*/
- if (RelationNeedsWAL(relation))
{
/*
* The snapshotConflictHorizon for the whole record should be the
@@ -807,7 +807,6 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer,
* record will freeze will conflict.
*/
TransactionId frz_conflict_horizon = InvalidTransactionId;
- TransactionId conflict_xid;
/*
* We can use the visibility_cutoff_xid as our cutoff for
@@ -832,13 +831,14 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer,
else
conflict_xid = prstate.latest_xid_removed;
- log_heap_prune_and_freeze(relation, buffer,
- conflict_xid,
- true, reason,
- prstate.frozen, prstate.nfrozen,
- prstate.redirected, prstate.nredirected,
- prstate.nowdead, prstate.ndead,
- prstate.nowunused, prstate.nunused);
+ if (RelationNeedsWAL(relation))
+ log_heap_prune_and_freeze(relation, buffer,
+ conflict_xid,
+ true, reason,
+ prstate.frozen, prstate.nfrozen,
+ prstate.redirected, prstate.nredirected,
+ prstate.nowdead, prstate.ndead,
+ prstate.nowunused, prstate.nunused);
}
}
@@ -876,6 +876,8 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer,
presult->hastup = prstate.hastup;
+ presult->conflict_xid = conflict_xid;
+
/*
* For callers planning to update the visibility map, the conflict horizon
* for that record must be the newest xmin on the page. However, if the
diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h
index 735662dc9df..934b5bfbe50 100644
--- a/src/include/access/heapam.h
+++ b/src/include/access/heapam.h
@@ -255,6 +255,11 @@ typedef struct PruneFreezeResult
*/
bool hastup;
+ /*
+ * Recovery conflict XID, if any. This is returned just for white-box tests.
+ */
+ TransactionId conflict_xid;
+
/*
* LP_DEAD items on the page after pruning. Includes existing LP_DEAD
* items.
--
2.39.2
From 9af964cfddf8634e796af5facfd2476401f8ef78 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <heikki.linnakan...@iki.fi>
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