From 316abf87ff67bfbbe9c43291a864f5860f5b0043 Mon Sep 17 00:00:00 2001
From: Ubuntu <ubuntu@ip-172-31-46-230.ec2.internal>
Date: Sat, 6 Dec 2025 00:54:06 +0000
Subject: [PATCH v3 1/1] Move custom stats tests from injection_points to
 dedicated module

Extract custom statistics testing code from injection_points module
into new test_custom_stats module with separate variable and fixed
statistics components. This improves test organization and removes
statistics functionality that was not core to injection points.

Discussion: https://www.postgresql.org/message-id/CAA5RZ0sJgO6GAwgFxmzg9MVP%3DrM7Us8KKcWpuqxe-f5qxmpE0g%40mail.gmail.com
---
 src/test/modules/Makefile                     |   1 +
 src/test/modules/injection_points/Makefile    |   4 -
 .../injection_points--1.0.sql                 |  43 ---
 .../injection_points/injection_points.c       |  48 ---
 .../injection_points/injection_stats.c        | 228 -------------
 .../injection_points/injection_stats.h        |  35 --
 .../injection_points/injection_stats_fixed.c  | 214 -------------
 src/test/modules/injection_points/meson.build |  12 -
 .../modules/injection_points/t/001_stats.pl   | 103 ------
 src/test/modules/meson.build                  |   1 +
 src/test/modules/test_custom_stats/.gitignore |   4 +
 src/test/modules/test_custom_stats/Makefile   |  27 ++
 .../modules/test_custom_stats/meson.build     |  55 ++++
 .../test_custom_stats/t/001_custom_stats.pl   | 115 +++++++
 .../test_custom_fixed_stats--1.0.sql          |  20 ++
 .../test_custom_fixed_stats.c                 | 224 +++++++++++++
 .../test_custom_fixed_stats.control           |   4 +
 .../test_custom_var_stats--1.0.sql            |  25 ++
 .../test_custom_stats/test_custom_var_stats.c | 302 ++++++++++++++++++
 .../test_custom_var_stats.control             |   4 +
 20 files changed, 782 insertions(+), 687 deletions(-)
 delete mode 100644 src/test/modules/injection_points/injection_stats.c
 delete mode 100644 src/test/modules/injection_points/injection_stats.h
 delete mode 100644 src/test/modules/injection_points/injection_stats_fixed.c
 delete mode 100644 src/test/modules/injection_points/t/001_stats.pl
 create mode 100644 src/test/modules/test_custom_stats/.gitignore
 create mode 100644 src/test/modules/test_custom_stats/Makefile
 create mode 100644 src/test/modules/test_custom_stats/meson.build
 create mode 100644 src/test/modules/test_custom_stats/t/001_custom_stats.pl
 create mode 100644 src/test/modules/test_custom_stats/test_custom_fixed_stats--1.0.sql
 create mode 100644 src/test/modules/test_custom_stats/test_custom_fixed_stats.c
 create mode 100644 src/test/modules/test_custom_stats/test_custom_fixed_stats.control
 create mode 100644 src/test/modules/test_custom_stats/test_custom_var_stats--1.0.sql
 create mode 100644 src/test/modules/test_custom_stats/test_custom_var_stats.c
 create mode 100644 src/test/modules/test_custom_stats/test_custom_var_stats.control

diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index d079b91b1a2..4a109ccbf6c 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -21,6 +21,7 @@ SUBDIRS = \
 		  test_bloomfilter \
 		  test_copy_callbacks \
 		  test_custom_rmgrs \
+		  test_custom_stats \
 		  test_ddl_deparse \
 		  test_dsa \
 		  test_dsm_registry \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index a618e6a9899..c85034eb8cc 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -4,8 +4,6 @@ MODULE_big = injection_points
 OBJS = \
 	$(WIN32RES) \
 	injection_points.o \
-	injection_stats.o \
-	injection_stats_fixed.o \
 	regress_injection.o
 EXTENSION = injection_points
 DATA = injection_points--1.0.sql
@@ -23,8 +21,6 @@ ISOLATION = basic \
 	    reindex-concurrently-upsert-on-constraint \
 	    reindex-concurrently-upsert-partitioned
 
-TAP_TESTS = 1
-
 # The injection points are cluster-wide, so disable installcheck
 NO_INSTALLCHECK = 1
 
diff --git a/src/test/modules/injection_points/injection_points--1.0.sql b/src/test/modules/injection_points/injection_points--1.0.sql
index a51ff538684..861c7355d4e 100644
--- a/src/test/modules/injection_points/injection_points--1.0.sql
+++ b/src/test/modules/injection_points/injection_points--1.0.sql
@@ -101,49 +101,6 @@ RETURNS SETOF record
 AS 'MODULE_PATHNAME', 'injection_points_list'
 LANGUAGE C STRICT VOLATILE PARALLEL RESTRICTED;
 
---
--- injection_points_stats_numcalls()
---
--- Reports statistics, if any, related to the given injection point.
---
-CREATE FUNCTION injection_points_stats_numcalls(IN point_name TEXT)
-RETURNS bigint
-AS 'MODULE_PATHNAME', 'injection_points_stats_numcalls'
-LANGUAGE C STRICT;
-
---
--- injection_points_stats_count()
---
--- Return the number of entries stored in the pgstats hash table.
---
-CREATE FUNCTION injection_points_stats_count()
-RETURNS bigint
-AS 'MODULE_PATHNAME', 'injection_points_stats_count'
-LANGUAGE C STRICT;
-
---
--- injection_points_stats_drop()
---
--- Drop all statistics of injection points.
---
-CREATE FUNCTION injection_points_stats_drop()
-RETURNS void
-AS 'MODULE_PATHNAME', 'injection_points_stats_drop'
-LANGUAGE C STRICT;
-
---
--- injection_points_stats_fixed()
---
--- Reports fixed-numbered statistics for injection points.
-CREATE FUNCTION injection_points_stats_fixed(OUT numattach int8,
-   OUT numdetach int8,
-   OUT numrun int8,
-   OUT numcached int8,
-   OUT numloaded int8)
-RETURNS record
-AS 'MODULE_PATHNAME', 'injection_points_stats_fixed'
-LANGUAGE C STRICT;
-
 --
 -- regress_injection.c functions
 --
diff --git a/src/test/modules/injection_points/injection_points.c b/src/test/modules/injection_points/injection_points.c
index b7c1c58ea56..417b61f31c5 100644
--- a/src/test/modules/injection_points/injection_points.c
+++ b/src/test/modules/injection_points/injection_points.c
@@ -19,7 +19,6 @@
 
 #include "fmgr.h"
 #include "funcapi.h"
-#include "injection_stats.h"
 #include "miscadmin.h"
 #include "nodes/pg_list.h"
 #include "nodes/value.h"
@@ -107,15 +106,6 @@ extern PGDLLEXPORT void injection_wait(const char *name,
 /* track if injection points attached in this process are linked to it */
 static bool injection_point_local = false;
 
-/*
- * GUC variable
- *
- * This GUC is useful to control if statistics should be enabled or not
- * during a test with injection points, like for example if a test relies
- * on a callback run in a critical section where no allocation should happen.
- */
-bool		inj_stats_enabled = false;
-
 /* Shared memory init callbacks */
 static shmem_request_hook_type prev_shmem_request_hook = NULL;
 static shmem_startup_hook_type prev_shmem_startup_hook = NULL;
@@ -235,9 +225,6 @@ injection_points_cleanup(int code, Datum arg)
 		char	   *name = strVal(lfirst(lc));
 
 		(void) InjectionPointDetach(name);
-
-		/* Remove stats entry */
-		pgstat_drop_inj(name);
 	}
 }
 
@@ -251,8 +238,6 @@ injection_error(const char *name, const void *private_data, void *arg)
 	if (!injection_point_allowed(condition))
 		return;
 
-	pgstat_report_inj(name);
-
 	if (argstr)
 		elog(ERROR, "error triggered for injection point %s (%s)",
 			 name, argstr);
@@ -269,8 +254,6 @@ injection_notice(const char *name, const void *private_data, void *arg)
 	if (!injection_point_allowed(condition))
 		return;
 
-	pgstat_report_inj(name);
-
 	if (argstr)
 		elog(NOTICE, "notice triggered for injection point %s (%s)",
 			 name, argstr);
@@ -293,8 +276,6 @@ injection_wait(const char *name, const void *private_data, void *arg)
 	if (!injection_point_allowed(condition))
 		return;
 
-	pgstat_report_inj(name);
-
 	/*
 	 * Use the injection point name for this custom wait event.  Note that
 	 * this custom wait event name is not released, but we don't care much for
@@ -371,7 +352,6 @@ injection_points_attach(PG_FUNCTION_ARGS)
 		condition.pid = MyProcPid;
 	}
 
-	pgstat_report_inj_fixed(1, 0, 0, 0, 0);
 	InjectionPointAttach(name, "injection_points", function, &condition,
 						 sizeof(InjectionPointCondition));
 
@@ -385,9 +365,6 @@ injection_points_attach(PG_FUNCTION_ARGS)
 		MemoryContextSwitchTo(oldctx);
 	}
 
-	/* Add entry for stats */
-	pgstat_create_inj(name);
-
 	PG_RETURN_VOID();
 }
 
@@ -422,7 +399,6 @@ injection_points_attach_func(PG_FUNCTION_ARGS)
 		private_data_size = VARSIZE_ANY_EXHDR(private_data);
 	}
 
-	pgstat_report_inj_fixed(1, 0, 0, 0, 0);
 	if (private_data != NULL)
 		InjectionPointAttach(name, lib_name, function, VARDATA_ANY(private_data),
 							 private_data_size);
@@ -444,7 +420,6 @@ injection_points_load(PG_FUNCTION_ARGS)
 	if (inj_state == NULL)
 		injection_init_shmem();
 
-	pgstat_report_inj_fixed(0, 0, 0, 0, 1);
 	INJECTION_POINT_LOAD(name);
 
 	PG_RETURN_VOID();
@@ -467,7 +442,6 @@ injection_points_run(PG_FUNCTION_ARGS)
 	if (!PG_ARGISNULL(1))
 		arg = text_to_cstring(PG_GETARG_TEXT_PP(1));
 
-	pgstat_report_inj_fixed(0, 0, 1, 0, 0);
 	INJECTION_POINT(name, arg);
 
 	PG_RETURN_VOID();
@@ -490,7 +464,6 @@ injection_points_cached(PG_FUNCTION_ARGS)
 	if (!PG_ARGISNULL(1))
 		arg = text_to_cstring(PG_GETARG_TEXT_PP(1));
 
-	pgstat_report_inj_fixed(0, 0, 0, 1, 0);
 	INJECTION_POINT_CACHED(name, arg);
 
 	PG_RETURN_VOID();
@@ -567,7 +540,6 @@ injection_points_detach(PG_FUNCTION_ARGS)
 {
 	char	   *name = text_to_cstring(PG_GETARG_TEXT_PP(0));
 
-	pgstat_report_inj_fixed(0, 1, 0, 0, 0);
 	if (!InjectionPointDetach(name))
 		elog(ERROR, "could not detach injection point \"%s\"", name);
 
@@ -581,9 +553,6 @@ injection_points_detach(PG_FUNCTION_ARGS)
 		MemoryContextSwitchTo(oldctx);
 	}
 
-	/* Remove stats entry */
-	pgstat_drop_inj(name);
-
 	PG_RETURN_VOID();
 }
 
@@ -625,32 +594,15 @@ injection_points_list(PG_FUNCTION_ARGS)
 #undef NUM_INJECTION_POINTS_LIST
 }
 
-
 void
 _PG_init(void)
 {
 	if (!process_shared_preload_libraries_in_progress)
 		return;
 
-	DefineCustomBoolVariable("injection_points.stats",
-							 "Enables statistics for injection points.",
-							 NULL,
-							 &inj_stats_enabled,
-							 false,
-							 PGC_POSTMASTER,
-							 0,
-							 NULL,
-							 NULL,
-							 NULL);
-
-	MarkGUCPrefixReserved("injection_points");
-
 	/* Shared memory initialization */
 	prev_shmem_request_hook = shmem_request_hook;
 	shmem_request_hook = injection_shmem_request;
 	prev_shmem_startup_hook = shmem_startup_hook;
 	shmem_startup_hook = injection_shmem_startup;
-
-	pgstat_register_inj();
-	pgstat_register_inj_fixed();
 }
diff --git a/src/test/modules/injection_points/injection_stats.c b/src/test/modules/injection_points/injection_stats.c
deleted file mode 100644
index 158e1631af9..00000000000
--- a/src/test/modules/injection_points/injection_stats.c
+++ /dev/null
@@ -1,228 +0,0 @@
-/*--------------------------------------------------------------------------
- *
- * injection_stats.c
- *		Code for statistics of injection points.
- *
- * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
- * Portions Copyright (c) 1994, Regents of the University of California
- *
- * IDENTIFICATION
- *		src/test/modules/injection_points/injection_stats.c
- *
- * -------------------------------------------------------------------------
- */
-
-#include "postgres.h"
-
-#include "fmgr.h"
-
-#include "common/hashfn.h"
-#include "injection_stats.h"
-#include "pgstat.h"
-#include "utils/builtins.h"
-#include "utils/pgstat_internal.h"
-
-/* Structures for statistics of injection points */
-typedef struct PgStat_StatInjEntry
-{
-	PgStat_Counter numcalls;	/* number of times point has been run */
-} PgStat_StatInjEntry;
-
-typedef struct PgStatShared_InjectionPoint
-{
-	PgStatShared_Common header;
-	PgStat_StatInjEntry stats;
-} PgStatShared_InjectionPoint;
-
-static bool injection_stats_flush_cb(PgStat_EntryRef *entry_ref, bool nowait);
-
-static const PgStat_KindInfo injection_stats = {
-	.name = "injection_points",
-	.fixed_amount = false,		/* Bounded by the number of points */
-	.write_to_file = true,
-	.track_entry_count = true,
-
-	/* Injection points are system-wide */
-	.accessed_across_databases = true,
-
-	.shared_size = sizeof(PgStatShared_InjectionPoint),
-	.shared_data_off = offsetof(PgStatShared_InjectionPoint, stats),
-	.shared_data_len = sizeof(((PgStatShared_InjectionPoint *) 0)->stats),
-	.pending_size = sizeof(PgStat_StatInjEntry),
-	.flush_pending_cb = injection_stats_flush_cb,
-};
-
-/*
- * Compute stats entry idx from point name with an 8-byte hash.
- */
-#define PGSTAT_INJ_IDX(name) hash_bytes_extended((const unsigned char *) name, strlen(name), 0)
-
-/*
- * Kind ID reserved for statistics of injection points.
- */
-#define PGSTAT_KIND_INJECTION	25
-
-/* Track if stats are loaded */
-static bool inj_stats_loaded = false;
-
-/*
- * Callback for stats handling
- */
-static bool
-injection_stats_flush_cb(PgStat_EntryRef *entry_ref, bool nowait)
-{
-	PgStat_StatInjEntry *localent;
-	PgStatShared_InjectionPoint *shfuncent;
-
-	localent = (PgStat_StatInjEntry *) entry_ref->pending;
-	shfuncent = (PgStatShared_InjectionPoint *) entry_ref->shared_stats;
-
-	if (!pgstat_lock_entry(entry_ref, nowait))
-		return false;
-
-	shfuncent->stats.numcalls += localent->numcalls;
-
-	pgstat_unlock_entry(entry_ref);
-
-	return true;
-}
-
-/*
- * Support function for the SQL-callable pgstat* functions.  Returns
- * a pointer to the injection point statistics struct.
- */
-static PgStat_StatInjEntry *
-pgstat_fetch_stat_injentry(const char *name)
-{
-	PgStat_StatInjEntry *entry = NULL;
-
-	if (!inj_stats_loaded || !inj_stats_enabled)
-		return NULL;
-
-	/* Compile the lookup key as a hash of the point name */
-	entry = (PgStat_StatInjEntry *) pgstat_fetch_entry(PGSTAT_KIND_INJECTION,
-													   InvalidOid,
-													   PGSTAT_INJ_IDX(name));
-	return entry;
-}
-
-/*
- * Workhorse to do the registration work, called in _PG_init().
- */
-void
-pgstat_register_inj(void)
-{
-	pgstat_register_kind(PGSTAT_KIND_INJECTION, &injection_stats);
-
-	/* mark stats as loaded */
-	inj_stats_loaded = true;
-}
-
-/*
- * Report injection point creation.
- */
-void
-pgstat_create_inj(const char *name)
-{
-	PgStat_EntryRef *entry_ref;
-	PgStatShared_InjectionPoint *shstatent;
-
-	/* leave if disabled */
-	if (!inj_stats_loaded || !inj_stats_enabled)
-		return;
-
-	entry_ref = pgstat_prep_pending_entry(PGSTAT_KIND_INJECTION, InvalidOid,
-										  PGSTAT_INJ_IDX(name), NULL);
-
-	shstatent = (PgStatShared_InjectionPoint *) entry_ref->shared_stats;
-
-	/* initialize shared memory data */
-	memset(&shstatent->stats, 0, sizeof(shstatent->stats));
-}
-
-/*
- * Report injection point drop.
- */
-void
-pgstat_drop_inj(const char *name)
-{
-	/* leave if disabled */
-	if (!inj_stats_loaded || !inj_stats_enabled)
-		return;
-
-	if (!pgstat_drop_entry(PGSTAT_KIND_INJECTION, InvalidOid,
-						   PGSTAT_INJ_IDX(name)))
-		pgstat_request_entry_refs_gc();
-}
-
-/*
- * Report statistics for injection point.
- *
- * This is simple because the set of stats to report currently is simple:
- * track the number of times a point has been run.
- */
-void
-pgstat_report_inj(const char *name)
-{
-	PgStat_EntryRef *entry_ref;
-	PgStat_StatInjEntry *pending;
-
-	/* leave if disabled */
-	if (!inj_stats_loaded || !inj_stats_enabled)
-		return;
-
-	entry_ref = pgstat_prep_pending_entry(PGSTAT_KIND_INJECTION, InvalidOid,
-										  PGSTAT_INJ_IDX(name), NULL);
-
-	pending = (PgStat_StatInjEntry *) entry_ref->pending;
-
-	/* Update the injection point pending statistics */
-	pending->numcalls++;
-}
-
-/*
- * SQL function returning the number of times an injection point
- * has been called.
- */
-PG_FUNCTION_INFO_V1(injection_points_stats_numcalls);
-Datum
-injection_points_stats_numcalls(PG_FUNCTION_ARGS)
-{
-	char	   *name = text_to_cstring(PG_GETARG_TEXT_PP(0));
-	PgStat_StatInjEntry *entry = pgstat_fetch_stat_injentry(name);
-
-	if (entry == NULL)
-		PG_RETURN_NULL();
-
-	PG_RETURN_INT64(entry->numcalls);
-}
-
-/*
- * SQL function returning the number of entries allocated for injection
- * points in the shared hashtable of pgstats.
- */
-PG_FUNCTION_INFO_V1(injection_points_stats_count);
-Datum
-injection_points_stats_count(PG_FUNCTION_ARGS)
-{
-	PG_RETURN_INT64(pgstat_get_entry_count(PGSTAT_KIND_INJECTION));
-}
-
-/* Only used by injection_points_stats_drop() */
-static bool
-match_inj_entries(PgStatShared_HashEntry *entry, Datum match_data)
-{
-	return entry->key.kind == PGSTAT_KIND_INJECTION;
-}
-
-/*
- * SQL function that drops all injection point statistics.
- */
-PG_FUNCTION_INFO_V1(injection_points_stats_drop);
-Datum
-injection_points_stats_drop(PG_FUNCTION_ARGS)
-{
-	pgstat_drop_matching_entries(match_inj_entries, 0);
-
-	PG_RETURN_VOID();
-}
diff --git a/src/test/modules/injection_points/injection_stats.h b/src/test/modules/injection_points/injection_stats.h
deleted file mode 100644
index ba310c52c7f..00000000000
--- a/src/test/modules/injection_points/injection_stats.h
+++ /dev/null
@@ -1,35 +0,0 @@
-/*--------------------------------------------------------------------------
- *
- * injection_stats.h
- *		Definitions for statistics of injection points.
- *
- * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
- * Portions Copyright (c) 1994, Regents of the University of California
- *
- * IDENTIFICATION
- *		src/test/modules/injection_points/injection_stats.h
- *
- * -------------------------------------------------------------------------
- */
-
-#ifndef INJECTION_STATS
-#define INJECTION_STATS
-
-/* GUC variable */
-extern bool inj_stats_enabled;
-
-/* injection_stats.c */
-extern void pgstat_register_inj(void);
-extern void pgstat_create_inj(const char *name);
-extern void pgstat_drop_inj(const char *name);
-extern void pgstat_report_inj(const char *name);
-
-/* injection_stats_fixed.c */
-extern void pgstat_register_inj_fixed(void);
-extern void pgstat_report_inj_fixed(uint32 numattach,
-									uint32 numdetach,
-									uint32 numrun,
-									uint32 numcached,
-									uint32 numloaded);
-
-#endif
diff --git a/src/test/modules/injection_points/injection_stats_fixed.c b/src/test/modules/injection_points/injection_stats_fixed.c
deleted file mode 100644
index b493e8f77a3..00000000000
--- a/src/test/modules/injection_points/injection_stats_fixed.c
+++ /dev/null
@@ -1,214 +0,0 @@
-/*--------------------------------------------------------------------------
- *
- * injection_stats_fixed.c
- *		Code for fixed-numbered statistics of injection points.
- *
- * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
- * Portions Copyright (c) 1994, Regents of the University of California
- *
- * IDENTIFICATION
- *		src/test/modules/injection_points/injection_stats_fixed.c
- *
- * -------------------------------------------------------------------------
- */
-
-#include "postgres.h"
-
-#include "fmgr.h"
-
-#include "access/htup_details.h"
-#include "common/hashfn.h"
-#include "funcapi.h"
-#include "injection_stats.h"
-#include "pgstat.h"
-#include "utils/builtins.h"
-#include "utils/pgstat_internal.h"
-
-/* Structures for statistics of injection points, fixed-size */
-typedef struct PgStat_StatInjFixedEntry
-{
-	PgStat_Counter numattach;	/* number of points attached */
-	PgStat_Counter numdetach;	/* number of points detached */
-	PgStat_Counter numrun;		/* number of points run */
-	PgStat_Counter numcached;	/* number of points cached */
-	PgStat_Counter numloaded;	/* number of points loaded */
-	TimestampTz stat_reset_timestamp;
-} PgStat_StatInjFixedEntry;
-
-typedef struct PgStatShared_InjectionPointFixed
-{
-	LWLock		lock;			/* protects all the counters */
-	uint32		changecount;
-	PgStat_StatInjFixedEntry stats;
-	PgStat_StatInjFixedEntry reset_offset;
-} PgStatShared_InjectionPointFixed;
-
-/* Callbacks for fixed-numbered stats */
-static void injection_stats_fixed_init_shmem_cb(void *stats);
-static void injection_stats_fixed_reset_all_cb(TimestampTz ts);
-static void injection_stats_fixed_snapshot_cb(void);
-
-static const PgStat_KindInfo injection_stats_fixed = {
-	.name = "injection_points_fixed",
-	.fixed_amount = true,
-	.write_to_file = true,
-
-	.shared_size = sizeof(PgStat_StatInjFixedEntry),
-	.shared_data_off = offsetof(PgStatShared_InjectionPointFixed, stats),
-	.shared_data_len = sizeof(((PgStatShared_InjectionPointFixed *) 0)->stats),
-
-	.init_shmem_cb = injection_stats_fixed_init_shmem_cb,
-	.reset_all_cb = injection_stats_fixed_reset_all_cb,
-	.snapshot_cb = injection_stats_fixed_snapshot_cb,
-};
-
-/*
- * Kind ID reserved for statistics of injection points.
- */
-#define PGSTAT_KIND_INJECTION_FIXED	26
-
-/* Track if fixed-numbered stats are loaded */
-static bool inj_fixed_loaded = false;
-
-static void
-injection_stats_fixed_init_shmem_cb(void *stats)
-{
-	PgStatShared_InjectionPointFixed *stats_shmem =
-		(PgStatShared_InjectionPointFixed *) stats;
-
-	LWLockInitialize(&stats_shmem->lock, LWTRANCHE_PGSTATS_DATA);
-}
-
-static void
-injection_stats_fixed_reset_all_cb(TimestampTz ts)
-{
-	PgStatShared_InjectionPointFixed *stats_shmem =
-		pgstat_get_custom_shmem_data(PGSTAT_KIND_INJECTION_FIXED);
-
-	LWLockAcquire(&stats_shmem->lock, LW_EXCLUSIVE);
-	pgstat_copy_changecounted_stats(&stats_shmem->reset_offset,
-									&stats_shmem->stats,
-									sizeof(stats_shmem->stats),
-									&stats_shmem->changecount);
-	stats_shmem->stats.stat_reset_timestamp = ts;
-	LWLockRelease(&stats_shmem->lock);
-}
-
-static void
-injection_stats_fixed_snapshot_cb(void)
-{
-	PgStatShared_InjectionPointFixed *stats_shmem =
-		pgstat_get_custom_shmem_data(PGSTAT_KIND_INJECTION_FIXED);
-	PgStat_StatInjFixedEntry *stat_snap =
-		pgstat_get_custom_snapshot_data(PGSTAT_KIND_INJECTION_FIXED);
-	PgStat_StatInjFixedEntry *reset_offset = &stats_shmem->reset_offset;
-	PgStat_StatInjFixedEntry reset;
-
-	pgstat_copy_changecounted_stats(stat_snap,
-									&stats_shmem->stats,
-									sizeof(stats_shmem->stats),
-									&stats_shmem->changecount);
-
-	LWLockAcquire(&stats_shmem->lock, LW_SHARED);
-	memcpy(&reset, reset_offset, sizeof(stats_shmem->stats));
-	LWLockRelease(&stats_shmem->lock);
-
-	/* compensate by reset offsets */
-#define FIXED_COMP(fld) stat_snap->fld -= reset.fld;
-	FIXED_COMP(numattach);
-	FIXED_COMP(numdetach);
-	FIXED_COMP(numrun);
-	FIXED_COMP(numcached);
-	FIXED_COMP(numloaded);
-#undef FIXED_COMP
-}
-
-/*
- * Workhorse to do the registration work, called in _PG_init().
- */
-void
-pgstat_register_inj_fixed(void)
-{
-	pgstat_register_kind(PGSTAT_KIND_INJECTION_FIXED, &injection_stats_fixed);
-
-	/* mark stats as loaded */
-	inj_fixed_loaded = true;
-}
-
-/*
- * Report fixed number of statistics for an injection point.
- */
-void
-pgstat_report_inj_fixed(uint32 numattach,
-						uint32 numdetach,
-						uint32 numrun,
-						uint32 numcached,
-						uint32 numloaded)
-{
-	PgStatShared_InjectionPointFixed *stats_shmem;
-
-	/* leave if disabled */
-	if (!inj_fixed_loaded || !inj_stats_enabled)
-		return;
-
-	stats_shmem = pgstat_get_custom_shmem_data(PGSTAT_KIND_INJECTION_FIXED);
-
-	LWLockAcquire(&stats_shmem->lock, LW_EXCLUSIVE);
-
-	pgstat_begin_changecount_write(&stats_shmem->changecount);
-	stats_shmem->stats.numattach += numattach;
-	stats_shmem->stats.numdetach += numdetach;
-	stats_shmem->stats.numrun += numrun;
-	stats_shmem->stats.numcached += numcached;
-	stats_shmem->stats.numloaded += numloaded;
-	pgstat_end_changecount_write(&stats_shmem->changecount);
-
-	LWLockRelease(&stats_shmem->lock);
-}
-
-/*
- * SQL function returning fixed-numbered statistics for injection points.
- */
-PG_FUNCTION_INFO_V1(injection_points_stats_fixed);
-Datum
-injection_points_stats_fixed(PG_FUNCTION_ARGS)
-{
-	TupleDesc	tupdesc;
-	Datum		values[5] = {0};
-	bool		nulls[5] = {0};
-	PgStat_StatInjFixedEntry *stats;
-
-	if (!inj_fixed_loaded || !inj_stats_enabled)
-		PG_RETURN_NULL();
-
-	pgstat_snapshot_fixed(PGSTAT_KIND_INJECTION_FIXED);
-	stats = pgstat_get_custom_snapshot_data(PGSTAT_KIND_INJECTION_FIXED);
-
-	/* Initialise attributes information in the tuple descriptor */
-	tupdesc = CreateTemplateTupleDesc(5);
-	TupleDescInitEntry(tupdesc, (AttrNumber) 1, "numattach",
-					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) 2, "numdetach",
-					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) 3, "numrun",
-					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) 4, "numcached",
-					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) 5, "numloaded",
-					   INT8OID, -1, 0);
-	BlessTupleDesc(tupdesc);
-
-	values[0] = Int64GetDatum(stats->numattach);
-	values[1] = Int64GetDatum(stats->numdetach);
-	values[2] = Int64GetDatum(stats->numrun);
-	values[3] = Int64GetDatum(stats->numcached);
-	values[4] = Int64GetDatum(stats->numloaded);
-	nulls[0] = false;
-	nulls[1] = false;
-	nulls[2] = false;
-	nulls[3] = false;
-	nulls[4] = false;
-
-	/* Returns the record as Datum */
-	PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
-}
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index 1a2af8a26c4..8d6f662040d 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -6,8 +6,6 @@ endif
 
 injection_points_sources = files(
   'injection_points.c',
-  'injection_stats.c',
-  'injection_stats_fixed.c',
   'regress_injection.c',
 )
 
@@ -58,14 +56,4 @@ tests += {
     # Some tests wait for all snapshots, so avoid parallel execution
     'runningcheck-parallel': false,
   },
-  'tap': {
-    'env': {
-      'enable_injection_points': get_option('injection_points') ? 'yes' : 'no',
-    },
-    'tests': [
-      't/001_stats.pl',
-    ],
-    # The injection points are cluster-wide, so disable installcheck
-    'runningcheck': false,
-  },
 }
diff --git a/src/test/modules/injection_points/t/001_stats.pl b/src/test/modules/injection_points/t/001_stats.pl
deleted file mode 100644
index 47ab58d0e9b..00000000000
--- a/src/test/modules/injection_points/t/001_stats.pl
+++ /dev/null
@@ -1,103 +0,0 @@
-
-# Copyright (c) 2024-2025, PostgreSQL Global Development Group
-
-# Tests for Custom Cumulative Statistics.
-
-use strict;
-use warnings FATAL => 'all';
-use locale;
-
-use PostgreSQL::Test::Cluster;
-use PostgreSQL::Test::Utils;
-use Test::More;
-
-# Test persistency of statistics generated for injection points.
-if ($ENV{enable_injection_points} ne 'yes')
-{
-	plan skip_all => 'Injection points not supported by this build';
-}
-
-# Node initialization
-my $node = PostgreSQL::Test::Cluster->new('master');
-$node->init;
-$node->append_conf(
-	'postgresql.conf', qq(
-shared_preload_libraries = 'injection_points'
-injection_points.stats = true
-));
-$node->start;
-$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;');
-
-# This should count for two calls.
-$node->safe_psql('postgres',
-	"SELECT injection_points_attach('stats-notice', 'notice');");
-$node->safe_psql('postgres', "SELECT injection_points_run('stats-notice');");
-$node->safe_psql('postgres', "SELECT injection_points_run('stats-notice');");
-my $numcalls = $node->safe_psql('postgres',
-	"SELECT injection_points_stats_numcalls('stats-notice');");
-is($numcalls, '2', 'number of stats calls');
-my $entrycount =
-  $node->safe_psql('postgres', "SELECT injection_points_stats_count();");
-is($entrycount, '1', 'number of entries');
-my $fixedstats = $node->safe_psql('postgres',
-	"SELECT * FROM injection_points_stats_fixed();");
-is($fixedstats, '1|0|2|0|0', 'fixed stats after some calls');
-
-# Loading and caching.
-$node->safe_psql(
-	'postgres', "
-SELECT injection_points_load('stats-notice');
-SELECT injection_points_cached('stats-notice');
-");
-$fixedstats = $node->safe_psql('postgres',
-	"SELECT * FROM injection_points_stats_fixed();");
-is($fixedstats, '1|0|2|1|1', 'fixed stats after loading and caching');
-
-# Restart the node cleanly, stats should still be around.
-$node->restart;
-$numcalls = $node->safe_psql('postgres',
-	"SELECT injection_points_stats_numcalls('stats-notice');");
-is($numcalls, '3', 'number of stats after clean restart');
-$entrycount =
-  $node->safe_psql('postgres', "SELECT injection_points_stats_count();");
-is($entrycount, '1', 'number of entries after clean restart');
-$fixedstats = $node->safe_psql('postgres',
-	"SELECT * FROM injection_points_stats_fixed();");
-is($fixedstats, '1|0|2|1|1', 'fixed stats after clean restart');
-
-# On crash the stats are gone.
-$node->stop('immediate');
-$node->start;
-$numcalls = $node->safe_psql('postgres',
-	"SELECT injection_points_stats_numcalls('stats-notice');");
-is($numcalls, '', 'number of stats after crash');
-$entrycount =
-  $node->safe_psql('postgres', "SELECT injection_points_stats_count();");
-is($entrycount, '0', 'number of entries after crash');
-$fixedstats = $node->safe_psql('postgres',
-	"SELECT * FROM injection_points_stats_fixed();");
-is($fixedstats, '0|0|0|0|0', 'fixed stats after crash');
-
-# On drop all stats are gone
-$node->safe_psql('postgres',
-	"SELECT injection_points_attach('stats-notice', 'notice');");
-$node->safe_psql('postgres', "SELECT injection_points_run('stats-notice');");
-$node->safe_psql('postgres', "SELECT injection_points_run('stats-notice');");
-$numcalls = $node->safe_psql('postgres',
-	"SELECT injection_points_stats_numcalls('stats-notice');");
-is($numcalls, '2', 'number of stats calls');
-$node->safe_psql('postgres', "SELECT injection_points_stats_drop();");
-$numcalls = $node->safe_psql('postgres',
-	"SELECT injection_points_stats_numcalls('stats-notice');");
-is($numcalls, '', 'no stats after drop via SQL function');
-$entrycount =
-  $node->safe_psql('postgres', "SELECT injection_points_stats_count();");
-is($entrycount, '0', 'number of entries after drop via SQL function');
-
-# Stop the server, disable the module, then restart.  The server
-# should be able to come up.
-$node->stop;
-$node->adjust_conf('postgresql.conf', 'shared_preload_libraries', "''");
-$node->start;
-
-done_testing();
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index cc57461e59a..2806db485d3 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -21,6 +21,7 @@ subdir('test_bitmapset')
 subdir('test_bloomfilter')
 subdir('test_copy_callbacks')
 subdir('test_custom_rmgrs')
+subdir('test_custom_stats')
 subdir('test_ddl_deparse')
 subdir('test_dsa')
 subdir('test_dsm_registry')
diff --git a/src/test/modules/test_custom_stats/.gitignore b/src/test/modules/test_custom_stats/.gitignore
new file mode 100644
index 00000000000..5dcb3ff9723
--- /dev/null
+++ b/src/test/modules/test_custom_stats/.gitignore
@@ -0,0 +1,4 @@
+# Generated subdirectories
+/log/
+/results/
+/tmp_check/
diff --git a/src/test/modules/test_custom_stats/Makefile b/src/test/modules/test_custom_stats/Makefile
new file mode 100644
index 00000000000..5b065a1cd42
--- /dev/null
+++ b/src/test/modules/test_custom_stats/Makefile
@@ -0,0 +1,27 @@
+# src/test/modules/test_custom_stats/Makefile
+
+MODULES = test_custom_var_stats test_custom_fixed_stats
+
+EXTENSION = test_custom_var_stats test_custom_fixed_stats
+
+OBJS = \
+	$(WIN32RES) \
+	test_custom_var_stats.o \
+	test_custom_fixed_stats.o
+PGFILEDESC = "test_custom_stats - test code for custom stat kinds"
+
+DATA = test_custom_var_stats--1.0.sql \
+       test_custom_fixed_stats--1.0.sql
+
+TAP_TESTS = 1
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_custom_stats
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_custom_stats/meson.build b/src/test/modules/test_custom_stats/meson.build
new file mode 100644
index 00000000000..a734467e169
--- /dev/null
+++ b/src/test/modules/test_custom_stats/meson.build
@@ -0,0 +1,55 @@
+# Copyright (c) 2025, PostgreSQL Global Development Group
+
+test_custom_var_stats_sources = files(
+  'test_custom_var_stats.c',
+)
+
+if host_system == 'windows'
+  test_custom_var_stats_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_custom_var_stats',
+    '--FILEDESC', 'test_custom_var_stats - test code for variable custom stat kinds',])
+endif
+
+test_custom_var_stats = shared_module('test_custom_var_stats',
+  test_custom_var_stats_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_custom_var_stats
+
+test_install_data += files(
+  'test_custom_var_stats.control',
+  'test_custom_var_stats--1.0.sql',
+)
+
+test_custom_fixed_stats_sources = files(
+  'test_custom_fixed_stats.c',
+)
+
+if host_system == 'windows'
+  test_custom_fixed_stats_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_custom_fixed_stats',
+    '--FILEDESC', 'test_custom_fixed_stats - test code for fixed custom stat kinds',])
+endif
+
+test_custom_fixed_stats = shared_module('test_custom_fixed_stats',
+  test_custom_fixed_stats_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_custom_fixed_stats
+
+test_install_data += files(
+  'test_custom_fixed_stats.control',
+  'test_custom_fixed_stats--1.0.sql',
+)
+
+tests += {
+  'name': 'test_custom_stats',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'tests': [
+      't/001_custom_stats.pl',
+    ],
+    'runningcheck': false,
+  },
+}
diff --git a/src/test/modules/test_custom_stats/t/001_custom_stats.pl b/src/test/modules/test_custom_stats/t/001_custom_stats.pl
new file mode 100644
index 00000000000..c4fceb7d267
--- /dev/null
+++ b/src/test/modules/test_custom_stats/t/001_custom_stats.pl
@@ -0,0 +1,115 @@
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+# Test custom statistics functionality
+#
+# Tests both variable-amount and fixed-amount custom statistics:
+# - Creation, updates, and reporting
+# - Persistence across clean restarts
+# - Loss after crash recovery
+# - Reset functionality for fixed stats
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+use File::Copy;
+
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+$node->append_conf('postgresql.conf',
+	"shared_preload_libraries = 'test_custom_var_stats, test_custom_fixed_stats'");
+$node->start;
+
+$node->safe_psql('postgres', q(CREATE EXTENSION test_custom_var_stats));
+$node->safe_psql('postgres', q(CREATE EXTENSION test_custom_fixed_stats));
+
+# Create variable statistics entries
+$node->safe_psql('postgres', q(select pgstat_create_custom_var_stats('entry1')));
+$node->safe_psql('postgres', q(select pgstat_create_custom_var_stats('entry2')));
+$node->safe_psql('postgres', q(select pgstat_create_custom_var_stats('entry3')));
+$node->safe_psql('postgres', q(select pgstat_create_custom_var_stats('entry4')));
+
+# Update counters: entry1=2, entry2=3, entry3=2, entry4=3, fixed=3
+$node->safe_psql('postgres', q(select pgstat_update_custom_var_stats('entry1')));
+$node->safe_psql('postgres', q(select pgstat_update_custom_var_stats('entry1')));
+$node->safe_psql('postgres', q(select pgstat_update_custom_var_stats('entry2')));
+$node->safe_psql('postgres', q(select pgstat_update_custom_var_stats('entry2')));
+$node->safe_psql('postgres', q(select pgstat_update_custom_var_stats('entry2')));
+$node->safe_psql('postgres', q(select pgstat_update_custom_var_stats('entry3')));
+$node->safe_psql('postgres', q(select pgstat_update_custom_var_stats('entry3')));
+$node->safe_psql('postgres', q(select pgstat_update_custom_var_stats('entry4')));
+$node->safe_psql('postgres', q(select pgstat_update_custom_var_stats('entry4')));
+$node->safe_psql('postgres', q(select pgstat_update_custom_var_stats('entry4')));
+$node->safe_psql('postgres', q(select pgstat_update_custom_fixed_stats()));
+$node->safe_psql('postgres', q(select pgstat_update_custom_fixed_stats()));
+$node->safe_psql('postgres', q(select pgstat_update_custom_fixed_stats()));
+
+# Test variable statistics reporting
+my $result = $node->safe_psql('postgres', q(select * from pgstat_report_custom_var_stats('entry1')));
+is($result, "entry1|2", "var stats entry1 reports correct calls");
+
+$result = $node->safe_psql('postgres', q(select * from pgstat_report_custom_var_stats('entry2')));
+is($result, "entry2|3", "var stats entry2 reports correct calls");
+
+$result = $node->safe_psql('postgres', q(select * from pgstat_report_custom_var_stats('entry3')));
+is($result, "entry3|2", "var stats entry3 reports correct calls");
+
+$result = $node->safe_psql('postgres', q(select * from pgstat_report_custom_var_stats('entry4')));
+is($result, "entry4|3", "var stats entry4 reports correct calls");
+
+$result = $node->safe_psql('postgres', q(select * from pgstat_report_custom_fixed_stats()));
+is($result, "3|", "fixed stats reports correct calls");
+
+# Test variable statistics drop functionality
+$result = $node->safe_psql('postgres', q(select * from pgstat_drop_custom_var_stats('entry3')));
+$result = $node->safe_psql('postgres', q(select * from pgstat_report_custom_var_stats('entry3')));
+is($result, "", "entry3 not found after drop");
+
+$result = $node->safe_psql('postgres', q(select * from pgstat_drop_custom_var_stats('entry4')));
+$result = $node->safe_psql('postgres', q(select * from pgstat_report_custom_var_stats('entry4')));
+is($result, "", "entry4 not found after drop");
+
+# Test persistence across clean restart
+$node->stop();
+$node->start();
+
+$result = $node->safe_psql('postgres', q(select * from pgstat_report_custom_var_stats('entry1')));
+is($result, "entry1|2", "var stats entry1 persists after clean restart");
+
+$result = $node->safe_psql('postgres', q(select * from pgstat_report_custom_var_stats('entry2')));
+is($result, "entry2|3", "var stats entry2 persists after clean restart");
+
+$result = $node->safe_psql('postgres', q(select * from pgstat_report_custom_fixed_stats()));
+is($result, "3|", "fixed stats persists after clean restart");
+
+# Test crash recovery behavior
+$node->stop('immediate');
+$node->start;
+
+$result = $node->safe_psql('postgres', q(select * from pgstat_report_custom_var_stats('entry1')));
+is($result, "", "var stats entry1 lost after crash recovery");
+
+$result = $node->safe_psql('postgres', q(select * from pgstat_report_custom_var_stats('entry2')));
+is($result, "", "var stats entry2 lost after crash recovery");
+
+# crash recovery sets the reset timestamp
+$result = $node->safe_psql('postgres', q(select numcalls from pgstat_report_custom_fixed_stats() where stats_reset is not null));
+is($result, "0", "fixed stats reset after crash recovery");
+
+# Test fixed statistics reset functionality
+$node->safe_psql('postgres', q(select pgstat_update_custom_fixed_stats()));
+$node->safe_psql('postgres', q(select pgstat_update_custom_fixed_stats()));
+$node->safe_psql('postgres', q(select pgstat_update_custom_fixed_stats()));
+
+$result = $node->safe_psql('postgres', q(select numcalls from pgstat_report_custom_fixed_stats()));
+is($result, "3", "fixed stats shows calls before manual reset");
+
+$node->safe_psql('postgres', q(select pgstat_reset_custom_fixed_stats()));
+
+$result = $node->safe_psql('postgres', q(select numcalls from pgstat_report_custom_fixed_stats() where stats_reset is not null));
+is($result, "0", "fixed stats reset after manual reset");
+
+# Test completed successfully
+done_testing();
\ No newline at end of file
diff --git a/src/test/modules/test_custom_stats/test_custom_fixed_stats--1.0.sql b/src/test/modules/test_custom_stats/test_custom_fixed_stats--1.0.sql
new file mode 100644
index 00000000000..e4c39749398
--- /dev/null
+++ b/src/test/modules/test_custom_stats/test_custom_fixed_stats--1.0.sql
@@ -0,0 +1,20 @@
+/* src/test/modules/test_custom_stats/test_custom_fixed_stats--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_custom_fixed_stats" to load this file. \quit
+
+CREATE FUNCTION pgstat_update_custom_fixed_stats()
+RETURNS void
+AS 'MODULE_PATHNAME', 'pgstat_update_custom_fixed_stats'
+LANGUAGE C STRICT PARALLEL UNSAFE;
+
+CREATE FUNCTION pgstat_report_custom_fixed_stats(OUT numcalls bigint,
+    OUT stats_reset timestamptz)
+RETURNS record
+AS 'MODULE_PATHNAME', 'pgstat_report_custom_fixed_stats'
+LANGUAGE C STRICT PARALLEL UNSAFE;
+
+CREATE FUNCTION pgstat_reset_custom_fixed_stats()
+RETURNS void
+AS 'MODULE_PATHNAME', 'pgstat_reset_custom_fixed_stats'
+LANGUAGE C STRICT PARALLEL UNSAFE;
\ No newline at end of file
diff --git a/src/test/modules/test_custom_stats/test_custom_fixed_stats.c b/src/test/modules/test_custom_stats/test_custom_fixed_stats.c
new file mode 100644
index 00000000000..f0821b4848a
--- /dev/null
+++ b/src/test/modules/test_custom_stats/test_custom_fixed_stats.c
@@ -0,0 +1,224 @@
+/*--------------------------------------------------------------------------
+ *
+ * test_custom_fixed_stats.c
+ *		Test module for fixed-amount custom statistics
+ *
+ * Copyright (c) 2024-2025, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/test/modules/test_custom_stats/test_custom_fixed_stats.c
+ *
+ * -------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "funcapi.h"
+#include "pgstat.h"
+#include "utils/builtins.h"
+#include "utils/pgstat_internal.h"
+
+PG_MODULE_MAGIC_EXT(
+					.name = "test_custom_fixed_stats",
+					.version = PG_VERSION
+);
+
+/* Fixed-amount custom statistics entry */
+typedef struct PgStat_StatCustomFixedEntry
+{
+	PgStat_Counter numcalls;	/* # of times update function called */
+	TimestampTz stat_reset_timestamp;
+}			PgStat_StatCustomFixedEntry;
+
+typedef struct PgStatShared_CustomFixedEntry
+{
+	LWLock		lock;			/* protects counters */
+	uint32		changecount;	/* for atomic reads */
+	PgStat_StatCustomFixedEntry stats;	/* current counters */
+	PgStat_StatCustomFixedEntry reset_offset;	/* reset baseline */
+}			PgStatShared_CustomFixedEntry;
+
+/* Callbacks for fixed-amount statistics */
+static void pgstat_custom_fixed_init_shmem_cb(void *stats);
+static void pgstat_custom_fixed_reset_all_cb(TimestampTz ts);
+static void pgstat_custom_fixed_snapshot_cb(void);
+
+static const PgStat_KindInfo custom_stats = {
+	.name = "test_custom_fixed_stats",
+	.fixed_amount = true,		/* exactly one entry */
+	.write_to_file = true,		/* persist to stats file */
+
+	.shared_size = sizeof(PgStat_StatCustomFixedEntry),
+	.shared_data_off = offsetof(PgStatShared_CustomFixedEntry, stats),
+	.shared_data_len = sizeof(((PgStatShared_CustomFixedEntry *) 0)->stats),
+
+	.init_shmem_cb = pgstat_custom_fixed_init_shmem_cb,
+	.reset_all_cb = pgstat_custom_fixed_reset_all_cb,
+	.snapshot_cb = pgstat_custom_fixed_snapshot_cb,
+};
+
+/*
+ * Kind ID for test_custom_fixed_stats.
+ */
+#define PGSTAT_KIND_TEST_CUSTOM_FIXED_STATS 26
+
+/*--------------------------------------------------------------------------
+ * Module initialization
+ *--------------------------------------------------------------------------
+ */
+
+void
+_PG_init(void)
+{
+	/* Must be loaded via shared_preload_libraries */
+	if (!process_shared_preload_libraries_in_progress)
+		return;
+
+	/* Register custom statistics kind */
+	pgstat_register_kind(PGSTAT_KIND_TEST_CUSTOM_FIXED_STATS, &custom_stats);
+}
+
+/*
+ * pgstat_custom_fixed_init_shmem_cb
+ *		Initialize shared memory structure
+ */
+static void
+pgstat_custom_fixed_init_shmem_cb(void *stats)
+{
+	PgStatShared_CustomFixedEntry *stats_shmem =
+		(PgStatShared_CustomFixedEntry *) stats;
+
+	LWLockInitialize(&stats_shmem->lock, LWTRANCHE_PGSTATS_DATA);
+}
+
+/*
+ * pgstat_custom_fixed_reset_all_cb
+ *		Reset the fixed stats
+ */
+static void
+pgstat_custom_fixed_reset_all_cb(TimestampTz ts)
+{
+	PgStatShared_CustomFixedEntry *stats_shmem =
+		pgstat_get_custom_shmem_data(PGSTAT_KIND_TEST_CUSTOM_FIXED_STATS);
+
+	/* see explanation above PgStatShared_Archiver for the reset protocol */
+	LWLockAcquire(&stats_shmem->lock, LW_EXCLUSIVE);
+	pgstat_copy_changecounted_stats(&stats_shmem->reset_offset,
+									&stats_shmem->stats,
+									sizeof(stats_shmem->stats),
+									&stats_shmem->changecount);
+	stats_shmem->stats.stat_reset_timestamp = ts;
+	LWLockRelease(&stats_shmem->lock);
+}
+
+/*
+ * pgstat_custom_fixed_snapshot_cb
+ *		Copy current stats to snapshot area
+ */
+static void
+pgstat_custom_fixed_snapshot_cb(void)
+{
+	PgStatShared_CustomFixedEntry *stats_shmem =
+		pgstat_get_custom_shmem_data(PGSTAT_KIND_TEST_CUSTOM_FIXED_STATS);
+	PgStat_StatCustomFixedEntry *stat_snap =
+		pgstat_get_custom_snapshot_data(PGSTAT_KIND_TEST_CUSTOM_FIXED_STATS);
+	PgStat_StatCustomFixedEntry *reset_offset = &stats_shmem->reset_offset;
+	PgStat_StatCustomFixedEntry reset;
+
+	pgstat_copy_changecounted_stats(stat_snap,
+									&stats_shmem->stats,
+									sizeof(stats_shmem->stats),
+									&stats_shmem->changecount);
+
+	LWLockAcquire(&stats_shmem->lock, LW_SHARED);
+	memcpy(&reset, reset_offset, sizeof(stats_shmem->stats));
+	LWLockRelease(&stats_shmem->lock);
+
+	/* Apply reset offsets */
+#define FIXED_COMP(fld) stat_snap->fld -= reset.fld;
+	FIXED_COMP(numcalls);
+#undef FIXED_COMP
+}
+
+/*--------------------------------------------------------------------------
+ * SQL-callable functions
+ *--------------------------------------------------------------------------
+ */
+
+/*
+ * pgstat_update_custom_fixed_stats
+ *		Increment call counter
+ */
+PG_FUNCTION_INFO_V1(pgstat_update_custom_fixed_stats);
+Datum
+pgstat_update_custom_fixed_stats(PG_FUNCTION_ARGS)
+{
+	PgStatShared_CustomFixedEntry *stats_shmem;
+
+	stats_shmem = pgstat_get_custom_shmem_data(PGSTAT_KIND_TEST_CUSTOM_FIXED_STATS);
+
+	LWLockAcquire(&stats_shmem->lock, LW_EXCLUSIVE);
+
+	pgstat_begin_changecount_write(&stats_shmem->changecount);
+	stats_shmem->stats.numcalls++;
+	pgstat_end_changecount_write(&stats_shmem->changecount);
+
+	LWLockRelease(&stats_shmem->lock);
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * pgstat_reset_custom_fixed_stats
+ *		Reset statistics by calling pgstat system
+ */
+PG_FUNCTION_INFO_V1(pgstat_reset_custom_fixed_stats);
+Datum
+pgstat_reset_custom_fixed_stats(PG_FUNCTION_ARGS)
+{
+	pgstat_reset_of_kind(PGSTAT_KIND_TEST_CUSTOM_FIXED_STATS);
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * pgstat_report_custom_fixed_stats
+ *		Return current counter values
+ */
+PG_FUNCTION_INFO_V1(pgstat_report_custom_fixed_stats);
+Datum
+pgstat_report_custom_fixed_stats(PG_FUNCTION_ARGS)
+{
+	TupleDesc	tupdesc;
+	Datum		values[2] = {0};
+	bool		nulls[2] = {false};
+	PgStat_StatCustomFixedEntry *stats;
+
+	/* Take snapshot (applies reset offsets) */
+	pgstat_snapshot_fixed(PGSTAT_KIND_TEST_CUSTOM_FIXED_STATS);
+	stats = pgstat_get_custom_snapshot_data(PGSTAT_KIND_TEST_CUSTOM_FIXED_STATS);
+
+	/* Build return tuple */
+	tupdesc = CreateTemplateTupleDesc(2);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 1, "numcalls",
+					   INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 2, "stats_reset",
+					   TIMESTAMPTZOID, -1, 0);
+	BlessTupleDesc(tupdesc);
+
+	values[0] = Int64GetDatum(stats->numcalls);
+
+	/* Handle uninitialized timestamp (no reset yet) */
+	if (stats->stat_reset_timestamp == 0)
+	{
+		nulls[1] = true;
+	}
+	else
+	{
+		values[1] = TimestampTzGetDatum(stats->stat_reset_timestamp);
+	}
+
+	/* Return as tuple */
+	PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
diff --git a/src/test/modules/test_custom_stats/test_custom_fixed_stats.control b/src/test/modules/test_custom_stats/test_custom_fixed_stats.control
new file mode 100644
index 00000000000..b96e2aa18fb
--- /dev/null
+++ b/src/test/modules/test_custom_stats/test_custom_fixed_stats.control
@@ -0,0 +1,4 @@
+comment = 'Test code for fixed custom stat kinds'
+default_version = '1.0'
+module_pathname = '$libdir/test_custom_fixed_stats'
+relocatable = true
diff --git a/src/test/modules/test_custom_stats/test_custom_var_stats--1.0.sql b/src/test/modules/test_custom_stats/test_custom_var_stats--1.0.sql
new file mode 100644
index 00000000000..84ae2bf5666
--- /dev/null
+++ b/src/test/modules/test_custom_stats/test_custom_var_stats--1.0.sql
@@ -0,0 +1,25 @@
+/* src/test/modules/test_custom_var_stats/test_custom_var_stats--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_custom_var_stats" to load this file. \quit
+
+CREATE FUNCTION pgstat_create_custom_var_stats(IN name TEXT)
+RETURNS void
+AS 'MODULE_PATHNAME', 'pgstat_create_custom_var_stats'
+LANGUAGE C STRICT PARALLEL UNSAFE;
+
+CREATE FUNCTION pgstat_update_custom_var_stats(IN name TEXT)
+RETURNS void
+AS 'MODULE_PATHNAME', 'pgstat_update_custom_var_stats'
+LANGUAGE C STRICT PARALLEL UNSAFE;
+
+CREATE FUNCTION pgstat_drop_custom_var_stats(IN name TEXT)
+RETURNS void
+AS 'MODULE_PATHNAME', 'pgstat_drop_custom_var_stats'
+LANGUAGE C STRICT PARALLEL UNSAFE;
+
+
+CREATE FUNCTION pgstat_report_custom_var_stats(INOUT name TEXT, OUT calls BIGINT)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'pgstat_report_custom_var_stats'
+LANGUAGE C STRICT PARALLEL UNSAFE;
diff --git a/src/test/modules/test_custom_stats/test_custom_var_stats.c b/src/test/modules/test_custom_stats/test_custom_var_stats.c
new file mode 100644
index 00000000000..6320eaf2cae
--- /dev/null
+++ b/src/test/modules/test_custom_stats/test_custom_var_stats.c
@@ -0,0 +1,302 @@
+/*------------------------------------------------------------------------------------
+ *
+ * test_custom_var_stats.c
+ *		Test module for custom PostgreSQL variable-numbered custom statistic kinds
+ *
+ * Copyright (c) 2024-2025, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/test/modules/test_custom_var_stats/test_custom_var_stats.c
+ *
+ * ------------------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "common/hashfn.h"
+#include "funcapi.h"
+#include "utils/builtins.h"
+#include "utils/pgstat_internal.h"
+
+PG_MODULE_MAGIC_EXT(
+					.name = "test_custom_var_stats",
+					.version = PG_VERSION
+);
+
+/*--------------------------------------------------------------------------
+ * Macros and constants
+ *--------------------------------------------------------------------------
+ */
+
+/*
+ * Kind ID for test_custom_var_stats statistics.
+ * Reuses the same ID as injection points to avoid reserving a new kind ID.
+ */
+#define PGSTAT_KIND_TEST_CUSTOM_VAR_STATS 25
+
+/*
+ * Hash statistic name to generate entry index for pgstat lookup.
+ */
+#define PGSTAT_CUSTOM_VAR_STATS_IDX(name) hash_bytes_extended((const unsigned char *) name, strlen(name), 0)
+
+/*--------------------------------------------------------------------------
+ * Type definitions
+ *--------------------------------------------------------------------------
+ */
+
+/* Backend-local pending statistics before flush to shared memory */
+typedef struct PgStat_StatCustomEntry
+{
+	PgStat_Counter numcalls;	/* times statistic was incremented */
+}			PgStat_StatCustomEntry;
+
+/* Shared memory statistics entry visible to all backends */
+typedef struct PgStatShared_CustomEntry
+{
+	PgStatShared_Common header; /* standard pgstat entry header */
+	PgStat_StatCustomEntry stats;	/* custom statistics data */
+}			PgStatShared_CustomEntry;
+
+/*--------------------------------------------------------------------------
+ * Function prototypes
+ *--------------------------------------------------------------------------
+ */
+
+/* Flush callback: merge pending stats into shared memory */
+static bool pgstat_custom_entry_flush_cb(PgStat_EntryRef *entry_ref, bool nowait);
+
+/*--------------------------------------------------------------------------
+ * Custom kind configuration
+ *--------------------------------------------------------------------------
+ */
+
+static const PgStat_KindInfo custom_stats = {
+	.name = "test_custom_var_stats",
+	.fixed_amount = false,		/* variable number of entries */
+	.write_to_file = true,		/* persist across restarts */
+	.track_entry_count = true,	/* count active entries */
+	.accessed_across_databases = true,	/* global statistics */
+	.shared_size = sizeof(PgStatShared_CustomEntry),
+	.shared_data_off = offsetof(PgStatShared_CustomEntry, stats),
+	.shared_data_len = sizeof(((PgStatShared_CustomEntry *) 0)->stats),
+	.pending_size = sizeof(PgStat_StatCustomEntry),
+	.flush_pending_cb = pgstat_custom_entry_flush_cb,
+};
+
+/*--------------------------------------------------------------------------
+ * Module initialization
+ *--------------------------------------------------------------------------
+ */
+
+void
+_PG_init(void)
+{
+	/* Must be loaded via shared_preload_libraries */
+	if (!process_shared_preload_libraries_in_progress)
+		return;
+
+	/* Register custom statistics kind */
+	pgstat_register_kind(PGSTAT_KIND_TEST_CUSTOM_VAR_STATS, &custom_stats);
+}
+
+/*--------------------------------------------------------------------------
+ * Statistics callback functions
+ *--------------------------------------------------------------------------
+ */
+
+/*
+ * pgstat_custom_entry_flush_cb
+ *		Merge pending backend statistics into shared memory
+ *
+ * Called by pgstat collector to flush accumulated local statistics
+ * to shared memory where other backends can read them.
+ *
+ * Returns false only if nowait=true and lock acquisition fails.
+ */
+static bool
+pgstat_custom_entry_flush_cb(PgStat_EntryRef *entry_ref, bool nowait)
+{
+	PgStat_StatCustomEntry *pending_entry;
+	PgStatShared_CustomEntry *shared_entry;
+
+	pending_entry = (PgStat_StatCustomEntry *) entry_ref->pending;
+	shared_entry = (PgStatShared_CustomEntry *) entry_ref->shared_stats;
+
+	if (!pgstat_lock_entry(entry_ref, nowait))
+		return false;
+
+	/* Add pending counts to shared totals */
+	shared_entry->stats.numcalls += pending_entry->numcalls;
+
+	pgstat_unlock_entry(entry_ref);
+
+	return true;
+}
+
+/*--------------------------------------------------------------------------
+ * Helper functions
+ *--------------------------------------------------------------------------
+ */
+
+/*
+ * pgstat_fetch_custom_entry
+ *		Look up custom statistic by name
+ *
+ * Returns statistics entry from shared memory, or NULL if not found.
+ */
+static PgStat_StatCustomEntry *
+pgstat_fetch_custom_entry(const char *stat_name)
+{
+	/* Fetch entry by hashed name */
+	return (PgStat_StatCustomEntry *) pgstat_fetch_entry(PGSTAT_KIND_TEST_CUSTOM_VAR_STATS,
+														 InvalidOid,
+														 PGSTAT_CUSTOM_VAR_STATS_IDX(stat_name));
+}
+
+/*--------------------------------------------------------------------------
+ * SQL-callable functions
+ *--------------------------------------------------------------------------
+ */
+
+/*
+ * pgstat_create_custom_var_stats
+ *		Create new custom statistic entry
+ *
+ * Initializes a zero-valued statistics entry in shared memory.
+ * Validates name length against NAMEDATALEN limit.
+ */
+PG_FUNCTION_INFO_V1(pgstat_create_custom_var_stats);
+Datum
+pgstat_create_custom_var_stats(PG_FUNCTION_ARGS)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_CustomEntry *shared_entry;
+	char	   *stat_name = text_to_cstring(PG_GETARG_TEXT_PP(0));
+
+	/* Validate name length first */
+	if (strlen(stat_name) >= NAMEDATALEN)
+		ereport(ERROR,
+				(errcode(ERRCODE_NAME_TOO_LONG),
+				 errmsg("custom statistic name \"%s\" is too long", stat_name),
+				 errdetail("Name must be less than %d characters.", NAMEDATALEN)));
+
+	/* Create or get existing entry */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_TEST_CUSTOM_VAR_STATS, InvalidOid,
+											PGSTAT_CUSTOM_VAR_STATS_IDX(stat_name), true);
+
+	if (!entry_ref)
+		PG_RETURN_VOID();
+
+	shared_entry = (PgStatShared_CustomEntry *) entry_ref->shared_stats;
+
+	/* Zero-initialize statistics */
+	memset(&shared_entry->stats, 0, sizeof(shared_entry->stats));
+
+	pgstat_unlock_entry(entry_ref);
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * pgstat_update_custom_var_stats
+ *		Increment custom statistic counter
+ *
+ * Increments call count in backend-local memory. Changes are flushed
+ * to shared memory by the statistics collector.
+ */
+PG_FUNCTION_INFO_V1(pgstat_update_custom_var_stats);
+Datum
+pgstat_update_custom_var_stats(PG_FUNCTION_ARGS)
+{
+	char	   *stat_name = text_to_cstring(PG_GETARG_TEXT_PP(0));
+	PgStat_EntryRef *entry_ref;
+	PgStat_StatCustomEntry *pending_entry;
+
+	/* Get pending entry in local memory */
+	entry_ref = pgstat_prep_pending_entry(PGSTAT_KIND_TEST_CUSTOM_VAR_STATS, InvalidOid,
+										  PGSTAT_CUSTOM_VAR_STATS_IDX(stat_name), NULL);
+
+	pending_entry = (PgStat_StatCustomEntry *) entry_ref->pending;
+	pending_entry->numcalls++;
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * pgstat_drop_custom_var_stats
+ *		Remove custom statistic entry
+ *
+ * Drops the named statistic from shared memory and requests
+ * garbage collection if needed.
+ */
+PG_FUNCTION_INFO_V1(pgstat_drop_custom_var_stats);
+Datum
+pgstat_drop_custom_var_stats(PG_FUNCTION_ARGS)
+{
+	char	   *stat_name = text_to_cstring(PG_GETARG_TEXT_PP(0));
+
+	/* Drop entry and request GC if the entry could not be freed */
+	if (!pgstat_drop_entry(PGSTAT_KIND_TEST_CUSTOM_VAR_STATS, InvalidOid,
+						   PGSTAT_CUSTOM_VAR_STATS_IDX(stat_name)))
+		pgstat_request_entry_refs_gc();
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * pgstat_report_custom_var_stats
+ *		Retrieve custom statistic values
+ *
+ * Returns single row with statistic name and call count if the
+ * statistic exists, otherwise returns no rows.
+ */
+PG_FUNCTION_INFO_V1(pgstat_report_custom_var_stats);
+Datum
+pgstat_report_custom_var_stats(PG_FUNCTION_ARGS)
+{
+	FuncCallContext *funcctx;
+	char	   *stat_name;
+	PgStat_StatCustomEntry *stat_entry;
+
+	if (SRF_IS_FIRSTCALL())
+	{
+		TupleDesc	tupdesc;
+		MemoryContext oldcontext;
+
+		/* Initialize SRF context */
+		funcctx = SRF_FIRSTCALL_INIT();
+		oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+		/* Get composite return type */
+		if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+			elog(ERROR, "pgstat_report_custom_var_stats: return type is not composite");
+
+		funcctx->tuple_desc = BlessTupleDesc(tupdesc);
+		funcctx->max_calls = 1; /* single row result */
+
+		MemoryContextSwitchTo(oldcontext);
+	}
+
+	funcctx = SRF_PERCALL_SETUP();
+
+	if (funcctx->call_cntr < funcctx->max_calls)
+	{
+		Datum		values[2];
+		bool		nulls[2] = {false, false};
+		HeapTuple	tuple;
+
+		stat_name = text_to_cstring(PG_GETARG_TEXT_PP(0));
+		stat_entry = pgstat_fetch_custom_entry(stat_name);
+
+		/* Return row only if entry exists */
+		if (stat_entry)
+		{
+			values[0] = PointerGetDatum(cstring_to_text(stat_name));
+			values[1] = Int64GetDatum(stat_entry->numcalls);
+
+			tuple = heap_form_tuple(funcctx->tuple_desc, values, nulls);
+			SRF_RETURN_NEXT(funcctx, HeapTupleGetDatum(tuple));
+		}
+	}
+
+	SRF_RETURN_DONE(funcctx);
+}
diff --git a/src/test/modules/test_custom_stats/test_custom_var_stats.control b/src/test/modules/test_custom_stats/test_custom_var_stats.control
new file mode 100644
index 00000000000..43a2783f965
--- /dev/null
+++ b/src/test/modules/test_custom_stats/test_custom_var_stats.control
@@ -0,0 +1,4 @@
+comment = 'Test code for variable custom stat kinds'
+default_version = '1.0'
+module_pathname = '$libdir/test_custom_var_stats'
+relocatable = true
-- 
2.43.0

