v4-0001-Move-custom-stats-tests-from-injection_points-to-.patch
application/octet-stream
Filename: v4-0001-Move-custom-stats-tests-from-injection_points-to-.patch
Type: application/octet-stream
Part: 1
Patch
Same data as JSON:
GET /api/v1/attachments/:id/patch
the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes.
API reference →
Format: format-patch
Series: patch v4-0001
Subject: Move custom stats tests from injection_points to dedicated module
| File | + | − |
|---|---|---|
| src/test/modules/injection_points/injection_points--1.0.sql | 0 | 43 |
| src/test/modules/injection_points/injection_points.c | 0 | 48 |
| src/test/modules/injection_points/injection_stats.c | 0 | 228 |
| src/test/modules/injection_points/injection_stats_fixed.c | 0 | 214 |
| src/test/modules/injection_points/injection_stats.h | 0 | 35 |
| src/test/modules/injection_points/Makefile | 0 | 4 |
| src/test/modules/injection_points/meson.build | 0 | 12 |
| src/test/modules/injection_points/t/001_stats.pl | 0 | 103 |
| src/test/modules/Makefile | 1 | 0 |
| src/test/modules/meson.build | 1 | 0 |
| src/test/modules/test_custom_stats/.gitignore | 4 | 0 |
| src/test/modules/test_custom_stats/Makefile | 27 | 0 |
| src/test/modules/test_custom_stats/meson.build | 55 | 0 |
| src/test/modules/test_custom_stats/t/001_custom_stats.pl | 115 | 0 |
| src/test/modules/test_custom_stats/test_custom_fixed_stats--1.0.sql | 20 | 0 |
| src/test/modules/test_custom_stats/test_custom_fixed_stats.c | 224 | 0 |
| src/test/modules/test_custom_stats/test_custom_fixed_stats.control | 4 | 0 |
| src/test/modules/test_custom_stats/test_custom_var_stats--1.0.sql | 25 | 0 |
| src/test/modules/test_custom_stats/test_custom_var_stats.c | 302 | 0 |
| src/test/modules/test_custom_stats/test_custom_var_stats.control | 4 | 0 |
From 2bcde3ae93c6f45a71d399ebdd831398361a821d 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 v4 1/2] 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