From a6453e15ae8f8730e99b1b6508271aeaea00ae06 Mon Sep 17 00:00:00 2001 From: Jingtang Zhang Date: Mon, 29 Jun 2026 17:08:52 +0800 Subject: [PATCH 2/2] test_fsm: Cover FSM range endpoint propagation Add a small test module for free space map behavior. The regression test sets free space on two blocks at an FSM leaf boundary, vacuums a non-empty range that either excludes or includes the last block, consumes the first block, and checks whether the last block becomes searchable. This demonstrates that passing an inclusive last block as the exclusive range endpoint leaves the last block hidden from FSM searches. --- src/test/modules/Makefile | 1 + src/test/modules/meson.build | 1 + src/test/modules/test_fsm/.gitignore | 4 + src/test/modules/test_fsm/Makefile | 23 +++++ .../modules/test_fsm/expected/test_fsm.out | 21 +++++ src/test/modules/test_fsm/meson.build | 33 +++++++ src/test/modules/test_fsm/sql/test_fsm.sql | 16 ++++ src/test/modules/test_fsm/test_fsm--1.0.sql | 10 ++ src/test/modules/test_fsm/test_fsm.c | 94 +++++++++++++++++++ src/test/modules/test_fsm/test_fsm.control | 4 + 10 files changed, 207 insertions(+) create mode 100644 src/test/modules/test_fsm/.gitignore create mode 100644 src/test/modules/test_fsm/Makefile create mode 100644 src/test/modules/test_fsm/expected/test_fsm.out create mode 100644 src/test/modules/test_fsm/meson.build create mode 100644 src/test/modules/test_fsm/sql/test_fsm.sql create mode 100644 src/test/modules/test_fsm/test_fsm--1.0.sql create mode 100644 src/test/modules/test_fsm/test_fsm.c create mode 100644 src/test/modules/test_fsm/test_fsm.control diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile index 0a74ab5c86f..711ec7b795e 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -31,6 +31,7 @@ SUBDIRS = \ test_dsm_registry \ test_escape \ test_extensions \ + test_fsm \ test_ginpostinglist \ test_int128 \ test_integerset \ diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build index 4bca42bb370..41864137231 100644 --- a/src/test/modules/meson.build +++ b/src/test/modules/meson.build @@ -32,6 +32,7 @@ subdir('test_dsa') subdir('test_dsm_registry') subdir('test_escape') subdir('test_extensions') +subdir('test_fsm') subdir('test_ginpostinglist') subdir('test_int128') subdir('test_integerset') diff --git a/src/test/modules/test_fsm/.gitignore b/src/test/modules/test_fsm/.gitignore new file mode 100644 index 00000000000..5dcb3ff9723 --- /dev/null +++ b/src/test/modules/test_fsm/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/test_fsm/Makefile b/src/test/modules/test_fsm/Makefile new file mode 100644 index 00000000000..e54d219aab6 --- /dev/null +++ b/src/test/modules/test_fsm/Makefile @@ -0,0 +1,23 @@ +# src/test/modules/test_fsm/Makefile + +MODULE_big = test_fsm +OBJS = \ + $(WIN32RES) \ + test_fsm.o +PGFILEDESC = "test_fsm - test code for free space map" + +EXTENSION = test_fsm +DATA = test_fsm--1.0.sql + +REGRESS = test_fsm + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_fsm +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_fsm/expected/test_fsm.out b/src/test/modules/test_fsm/expected/test_fsm.out new file mode 100644 index 00000000000..55414cc3f7d --- /dev/null +++ b/src/test/modules/test_fsm/expected/test_fsm.out @@ -0,0 +1,21 @@ +CREATE EXTENSION test_fsm; +CREATE TABLE test_fsm_wrong_range (a int); +-- FreeSpaceMapVacuumRange() takes an exclusive end block. Passing last_block +-- as end updates a non-empty range but leaves last_block's upper FSM slot +-- unchanged when last_block starts the next FSM leaf page. +SELECT test_fsm_range_endpoint('test_fsm_wrong_range', false); + test_fsm_range_endpoint +------------------------- + f +(1 row) + +CREATE TABLE test_fsm_right_range (a int); +-- Passing last_block + 1 includes the last block and makes it searchable after +-- the earlier block in the updated range is consumed. +SELECT test_fsm_range_endpoint('test_fsm_right_range', true); + test_fsm_range_endpoint +------------------------- + t +(1 row) + +DROP TABLE test_fsm_wrong_range, test_fsm_right_range; diff --git a/src/test/modules/test_fsm/meson.build b/src/test/modules/test_fsm/meson.build new file mode 100644 index 00000000000..211abd5e8e6 --- /dev/null +++ b/src/test/modules/test_fsm/meson.build @@ -0,0 +1,33 @@ +# Copyright (c) 2022-2026, PostgreSQL Global Development Group + +test_fsm_sources = files( + 'test_fsm.c', +) + +if host_system == 'windows' + test_fsm_sources += rc_lib_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'test_fsm', + '--FILEDESC', 'test_fsm - test code for free space map',]) +endif + +test_fsm = shared_module('test_fsm', + test_fsm_sources, + kwargs: pg_test_mod_args, +) +test_install_libs += test_fsm + +test_install_data += files( + 'test_fsm.control', + 'test_fsm--1.0.sql', +) + +tests += { + 'name': 'test_fsm', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'regress': { + 'sql': [ + 'test_fsm', + ], + }, +} diff --git a/src/test/modules/test_fsm/sql/test_fsm.sql b/src/test/modules/test_fsm/sql/test_fsm.sql new file mode 100644 index 00000000000..faa867d0ca4 --- /dev/null +++ b/src/test/modules/test_fsm/sql/test_fsm.sql @@ -0,0 +1,16 @@ +CREATE EXTENSION test_fsm; + +CREATE TABLE test_fsm_wrong_range (a int); + +-- FreeSpaceMapVacuumRange() takes an exclusive end block. Passing last_block +-- as end updates a non-empty range but leaves last_block's upper FSM slot +-- unchanged when last_block starts the next FSM leaf page. +SELECT test_fsm_range_endpoint('test_fsm_wrong_range', false); + +CREATE TABLE test_fsm_right_range (a int); + +-- Passing last_block + 1 includes the last block and makes it searchable after +-- the earlier block in the updated range is consumed. +SELECT test_fsm_range_endpoint('test_fsm_right_range', true); + +DROP TABLE test_fsm_wrong_range, test_fsm_right_range; diff --git a/src/test/modules/test_fsm/test_fsm--1.0.sql b/src/test/modules/test_fsm/test_fsm--1.0.sql new file mode 100644 index 00000000000..860f21ece35 --- /dev/null +++ b/src/test/modules/test_fsm/test_fsm--1.0.sql @@ -0,0 +1,10 @@ +/* src/test/modules/test_fsm/test_fsm--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_fsm" to load this file. \quit + +CREATE FUNCTION test_fsm_range_endpoint( + rel pg_catalog.regclass, + include_end pg_catalog.bool) +RETURNS pg_catalog.bool +AS 'MODULE_PATHNAME' LANGUAGE C STRICT; diff --git a/src/test/modules/test_fsm/test_fsm.c b/src/test/modules/test_fsm/test_fsm.c new file mode 100644 index 00000000000..9f41f0ee329 --- /dev/null +++ b/src/test/modules/test_fsm/test_fsm.c @@ -0,0 +1,94 @@ +/*-------------------------------------------------------------------------- + * + * test_fsm.c + * Test support for free space map behavior. + * + * Copyright (c) 2026, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_fsm/test_fsm.c + * + * ------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "access/relation.h" +#include "fmgr.h" +#include "storage/bufmgr.h" +#include "storage/freespace.h" +#include "storage/fsm_internals.h" +#include "utils/rel.h" + +PG_MODULE_MAGIC; + +PG_FUNCTION_INFO_V1(test_fsm_range_endpoint); + +Datum +test_fsm_range_endpoint(PG_FUNCTION_ARGS) +{ + Oid relid = PG_GETARG_OID(0); + bool include_end = PG_GETARG_BOOL(1); + BlockNumber nblocks; + BlockNumber start; + BlockNumber last; + BlockNumber found; + Buffer buffer; + Relation rel; + Size space_avail = BLCKSZ / 2; + Size space_needed = BLCKSZ / 4; + + rel = relation_open(relid, AccessExclusiveLock); + + if (!RELKIND_HAS_STORAGE(rel->rd_rel->relkind)) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("relation \"%s\" does not have storage", + RelationGetRelationName(rel)), + errdetail_relkind_not_supported(rel->rd_rel->relkind))); + + /* + * Use a real non-empty range that crosses an FSM leaf page boundary. This + * makes the last block depend on its own upper-level FSM slot; updating the + * range [start, last) cannot accidentally propagate the last block's leaf + * value. + */ + start = SlotsPerFSMPage - 1; + last = SlotsPerFSMPage; + + nblocks = RelationGetNumberOfBlocks(rel); + if (nblocks <= last) + { + buffer = ExtendBufferedRelTo(BMR_REL(rel), MAIN_FORKNUM, NULL, + 0, + last + 1, + RBM_ZERO_AND_LOCK); + UnlockReleaseBuffer(buffer); + } + + /* + * Model RelationAddBlocks() after it has recorded free space for a range of + * newly extended blocks. The include_end=false case matches passing the + * inclusive last block as FreeSpaceMapVacuumRange()'s exclusive end. + */ + RecordPageWithFreeSpace(rel, start, space_avail); + RecordPageWithFreeSpace(rel, last, space_avail); + FreeSpaceMapVacuumRange(rel, start, include_end ? last + 1 : last); + + /* + * Consume the first block made visible by the non-empty range. After this, + * a correct range update can still find the last block, while an update that + * excluded the last block cannot. + */ + found = GetPageWithFreeSpace(rel, space_needed); + if (found != start) + elog(ERROR, "expected to find block %u, found %u", start, found); + + RecordPageWithFreeSpace(rel, found, 0); + FreeSpaceMapVacuumRange(rel, found, found + 1); + found = GetPageWithFreeSpace(rel, space_needed); + + relation_close(rel, AccessExclusiveLock); + + PG_RETURN_BOOL(found == last); +} diff --git a/src/test/modules/test_fsm/test_fsm.control b/src/test/modules/test_fsm/test_fsm.control new file mode 100644 index 00000000000..3f408f8954e --- /dev/null +++ b/src/test/modules/test_fsm/test_fsm.control @@ -0,0 +1,4 @@ +comment = 'Test code for free space map' +default_version = '1.0' +module_pathname = '$libdir/test_fsm' +relocatable = true -- 2.43.7