public inbox for [email protected]  
help / color / mirror / Atom feed
From: Sami Imseih <[email protected]>
To: Michael Paquier <[email protected]>
Cc: Chao Li <[email protected]>
Cc: PostgreSQL Hackers <[email protected]>
Subject: Re: [Proposal] Adding callback support for custom statistics kinds
Date: Sun, 7 Dec 2025 11:42:38 -0600
Message-ID: <CAA5RZ0s90-gSt9EpgA3FJHQBwhLeYk=LyaynffaaV-yNJMnu2w@mail.gmail.com> (raw)
In-Reply-To: <CAA5RZ0sG2RUKg=OLY+6-e4q=X9rsLfK3pKn03d=RZQppEDR=Bg@mail.gmail.com>
References: <CAA5RZ0tH6ih3anQ=Lcwttm+8BURJ8VkBoy_9V8Ty73ZA1r384Q@mail.gmail.com>
	<[email protected]>
	<CAA5RZ0tvDvHYYLnu-T0DRK0xv1qv2_EK1yHjth5uAqDWz_GvfA@mail.gmail.com>
	<CAA5RZ0thcqk_r_SCB3TdXP9hVV5mG_29yc9GaGr4ujPQmcLDEQ@mail.gmail.com>
	<[email protected]>
	<CAA5RZ0ufds2r3H7QUro_yJDrnUmsQ4ocufqgcsRGoZvxQKeMrA@mail.gmail.com>
	<[email protected]>
	<[email protected]>
	<[email protected]>
	<CAA5RZ0sJgO6GAwgFxmzg9MVP=rM7Us8KKcWpuqxe-f5qxmpE0g@mail.gmail.com>
	<[email protected]>
	<CAA5RZ0sG2RUKg=OLY+6-e4q=X9rsLfK3pKn03d=RZQppEDR=Bg@mail.gmail.com>

I took a look at the earlier comments.

> +    /* Read and verify the hash key */
> +    if (!pgstat_read_chunk(fd, (void *) key, sizeof(PgStat_HashKey)))
> +        return;
> [...]
> +    /* Write the hash key to identify this entry */
> +    pgstat_write_chunk(fd, (void *) key, sizeof(PgStat_HashKey));

> I am puzzled by this part of 0002.  Why are you overwriting the key
> once after loading it from the main pgstats file?

Yes, this is not necessary. I removed it.

> I would have done it in a slightly different way, I guess, with the data
> stored on disk in the main pgstats file including an offset to know
> where to search in the secondary file.

that's a much better approach and the pattern we would want to use
going forward. Since this does not require we read the entires
back in the same order as written, so it's much more flexible.
I not did this in the test module.

> Perhaps the callback in the module for end_extra_stats should use a
> switch based on PgStat_StatsFileOp.  Minor point.

Agree. Done.

>> * In the current path, pgstat performs its own write, then call
>> * callbacks. What about if a callback fails? Will that leave pgstat
>> * in a stale state?

> For the write state, end_extra_stats() would take care of that.  It
> depends on what kind of errors you would need to deal with, but as
> proposed the patch would offer the same level of protection for the
> writes of the stats, where we'd check for an error on the fd saved by
> an extension for an extra file.

> I think that you have a fair point about the stats read path though,
> shouldn't we make the callback from_serialized_extra_stats() return a
> state to be able to trigger a full-scale cleanup, at least?

In this case, even if the callback does not return a state, the cleanup
will eventually occur at the end of the read, see

```
done:
    /* First, cleanup the main stats file, PGSTAT_STAT_PERMANENT_FILENAME */
    FreeFile(fpin);

    elog(DEBUG2, "removing permanent stats file \"%s\"", statfile);
    unlink(statfile);

    /* Let each stats kind run its cleanup callback, if it provides one */
    for (PgStat_Kind kind = PGSTAT_KIND_MIN; kind <= PGSTAT_KIND_MAX; kind++)
    {
        const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);

        if (kind_info && kind_info->end_extra_stats)
            kind_info->end_extra_stats(STATS_READ);
    }
```

However, this could also mean some entries could be read back correctly, and
others not, so maybe it's not such a good idea. So, I did what is suggested
and allow the callback to return a bool which will raise an error and trigger
the cleanup code.

> +     if (pgstat_is_kind_custom(key.kind) && kind_info->from_serialized_extra_stats)
> +         kind_info->from_serialized_extra_stats(&key, header, fpin);
> [...]
> +     if (pgstat_is_kind_custom(ps->key.kind) && kind_info->to_serialized_extra_stats)
> +         kind_info->to_serialized_extra_stats(&ps->key, shstats, fpout);

> These restrictions based on custom kinds do not seem mandatory.
> Why not allowing built-in kinds the same set of operations?

No good reason not to. In fact, maybe a follow-up will be to move the
replslot to this infrastructure and remove reliance on PGSTAT_FILE_ENTRY_NAME.

attached is the v4 patch set which includes:

0001 - which is just moving the tests out of injection points into a new
test module. This is similar to v3 [0].

0002 - Is the code changes to implement the callbacks and the necessary
tests in the new test module.

[0] https://www.postgresql.org/message-id/CAA5RZ0sG2RUKg%3DOLY%2B6-e4q%3DX9rsLfK3pKn03d%3DRZQppEDR%3DBg%...

--
Sami Imseih
Amazon Web Services (AWS)


Attachments:

  [application/octet-stream] v4-0002-Allow-cumulative-statistics-to-serialize-auxiliar.patch (28.0K, ../CAA5RZ0s90-gSt9EpgA3FJHQBwhLeYk=LyaynffaaV-yNJMnu2w@mail.gmail.com/2-v4-0002-Allow-cumulative-statistics-to-serialize-auxiliar.patch)
  download | inline diff:
From 87d34f2c618798efb37f7e225a753d1e3c69b11c Mon Sep 17 00:00:00 2001
From: Sami Imseih <[email protected]>
Date: Mon, 10 Nov 2025 00:03:41 -0600
Subject: [PATCH v4 2/2] Allow cumulative statistics to serialize auxiliary
 data to disk.

Cumulative Statistics kinds can now write additional per-entry data to
the statistics file that doesn't fit in shared memory. This is useful
for statistics with variable-length auxiliary data.

Three new optional callbacks are added to PgStat_KindInfo:

* to_serialized_extra_stats: writes auxiliary data for an entry
* from_serialized_extra_stats: reads auxiliary data for an entry
* end_extra_stats: performs cleanup after read/write/discard operations

All three callbacks must be provided together to ensure the reader
consumes exactly what the writer produces. The end_extra_stats callback
is invoked after processing all entries of a kind, allowing extensions
to close file handles and clean up resources.

Tests are also added to test_custom_stats.

Discussion: https://www.postgresql.org/message-id/flat/CAA5RZ0s9SDOu+Z6veoJCHWk+kDeTktAtC-KY9fQ9Z6BJdDUirQ@mail.gmail.com
---
 src/backend/utils/activity/pgstat.c           |  81 ++++-
 src/include/utils/pgstat_internal.h           |  37 ++
 .../test_custom_stats/t/001_custom_stats.pl   |  20 +-
 .../test_custom_var_stats--1.0.sql            |   6 +-
 .../test_custom_stats/test_custom_var_stats.c | 327 +++++++++++++++++-
 5 files changed, 447 insertions(+), 24 deletions(-)

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index 8713c7a0483..8804c1688c5 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -194,6 +194,7 @@ static void pgstat_build_snapshot(void);
 static void pgstat_build_snapshot_fixed(PgStat_Kind kind);
 
 static inline bool pgstat_is_kind_valid(PgStat_Kind kind);
+static inline void pgstat_check_extra_callbacks(const PgStat_KindInfo *kind_info, int elevel);
 
 
 /* ----------
@@ -523,6 +524,7 @@ pgstat_discard_stats(void)
 
 	/* NB: this needs to be done even in single user mode */
 
+	/* First, cleanup the main stats file, PGSTAT_STAT_PERMANENT_FILENAME */
 	ret = unlink(PGSTAT_STAT_PERMANENT_FILENAME);
 	if (ret != 0)
 	{
@@ -544,6 +546,15 @@ pgstat_discard_stats(void)
 								 PGSTAT_STAT_PERMANENT_FILENAME)));
 	}
 
+	/* Let each stats kind run its cleanup callback, if it provides one */
+	for (PgStat_Kind kind = PGSTAT_KIND_MIN; kind <= PGSTAT_KIND_MAX; kind++)
+	{
+		const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+		if (kind_info && kind_info->end_extra_stats)
+			kind_info->end_extra_stats(STATS_DISCARD);
+	}
+
 	/*
 	 * Reset stats contents. This will set reset timestamps of fixed-numbered
 	 * stats to the current time (no variable stats exist).
@@ -645,6 +656,10 @@ pgstat_initialize(void)
 
 	pgstat_attach_shmem();
 
+	/* Check a kind's extra-data callback setup */
+	for (PgStat_Kind kind = PGSTAT_KIND_BUILTIN_MIN; kind <= PGSTAT_KIND_BUILTIN_MAX; kind++)
+		pgstat_check_extra_callbacks(&pgstat_kind_builtin_infos[kind], PANIC);
+
 	pgstat_init_snapshot_fixed();
 
 	/* Backend initialization callbacks */
@@ -1432,6 +1447,32 @@ pgstat_is_kind_valid(PgStat_Kind kind)
 	return pgstat_is_kind_builtin(kind) || pgstat_is_kind_custom(kind);
 }
 
+/*
+ * Validate that extra stats callbacks are all provided together or not at all.
+ * Reports error at specified level if validation fails.
+ */
+static inline void
+pgstat_check_extra_callbacks(const PgStat_KindInfo *kind_info, int elevel)
+{
+	bool		has_extra;
+
+	has_extra =
+		kind_info->to_serialized_extra_stats ||
+		kind_info->from_serialized_extra_stats ||
+		kind_info->end_extra_stats;
+
+	if (has_extra &&
+		(!kind_info->to_serialized_extra_stats ||
+		 !kind_info->from_serialized_extra_stats ||
+		 !kind_info->end_extra_stats))
+	{
+		ereport(elevel,
+				(errmsg("incomplete serialization callbacks for statistics kind \"%s\"",
+						kind_info->name),
+				 errdetail("callbacks to_serialized_extra_stats, from_serialized_extra_stats, and end_extra_stats must be provided together.")));
+	}
+}
+
 const PgStat_KindInfo *
 pgstat_get_kind_info(PgStat_Kind kind)
 {
@@ -1525,6 +1566,9 @@ pgstat_register_kind(PgStat_Kind kind, const PgStat_KindInfo *kind_info)
 					 errdetail("Existing cumulative statistics with ID %u has the same name.", existing_kind)));
 	}
 
+	/* Check a kind's extra-data callback setup */
+	pgstat_check_extra_callbacks(kind_info, ERROR);
+
 	/* Register it */
 	pgstat_kind_custom_infos[idx] = kind_info;
 	ereport(LOG,
@@ -1702,6 +1746,9 @@ pgstat_write_statsfile(void)
 		pgstat_write_chunk(fpout,
 						   pgstat_get_entry_data(ps->key.kind, shstats),
 						   pgstat_get_entry_len(ps->key.kind));
+
+		if (kind_info->to_serialized_extra_stats)
+			kind_info->to_serialized_extra_stats(&ps->key, shstats, fpout);
 	}
 	dshash_seq_term(&hstat);
 
@@ -1734,6 +1781,15 @@ pgstat_write_statsfile(void)
 		/* durable_rename already emitted log message */
 		unlink(tmpfile);
 	}
+
+	/* Now, allow kinds to finalize the writes for the extra files */
+	for (PgStat_Kind kind = PGSTAT_KIND_MIN; kind <= PGSTAT_KIND_MAX; kind++)
+	{
+		const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+		if (kind_info && kind_info->end_extra_stats)
+			kind_info->end_extra_stats(STATS_WRITE);
+	}
 }
 
 /* helper for pgstat_read_statsfile() */
@@ -1871,6 +1927,7 @@ pgstat_read_statsfile(void)
 					PgStat_HashKey key;
 					PgStatShared_HashEntry *p;
 					PgStatShared_Common *header;
+					const PgStat_KindInfo *kind_info = NULL;
 
 					CHECK_FOR_INTERRUPTS();
 
@@ -1891,7 +1948,8 @@ pgstat_read_statsfile(void)
 							goto error;
 						}
 
-						if (!pgstat_get_kind_info(key.kind))
+						kind_info = pgstat_get_kind_info(key.kind);
+						if (!kind_info)
 						{
 							elog(WARNING, "could not find information of kind for entry %u/%u/%" PRIu64 " of type %c",
 								 key.kind, key.dboid,
@@ -1902,7 +1960,6 @@ pgstat_read_statsfile(void)
 					else
 					{
 						/* stats entry identified by name on disk (e.g. slots) */
-						const PgStat_KindInfo *kind_info = NULL;
 						PgStat_Kind kind;
 						NameData	name;
 
@@ -1996,6 +2053,16 @@ pgstat_read_statsfile(void)
 						goto error;
 					}
 
+					if (kind_info->from_serialized_extra_stats)
+					{
+						if (!kind_info->from_serialized_extra_stats(&key, header, fpin))
+						{
+							elog(WARNING, "could not read extra stats for entry %u/%u/%" PRIu64,
+								 key.kind, key.dboid, key.objid);
+							goto error;
+						}
+					}
+
 					break;
 				}
 			case PGSTAT_FILE_ENTRY_END:
@@ -2019,11 +2086,21 @@ pgstat_read_statsfile(void)
 	}
 
 done:
+	/* First, cleanup the main stats file, PGSTAT_STAT_PERMANENT_FILENAME */
 	FreeFile(fpin);
 
 	elog(DEBUG2, "removing permanent stats file \"%s\"", statfile);
 	unlink(statfile);
 
+	/* Let each stats kind run its cleanup callback, if it provides one */
+	for (PgStat_Kind kind = PGSTAT_KIND_MIN; kind <= PGSTAT_KIND_MAX; kind++)
+	{
+		const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind);
+
+		if (kind_info && kind_info->end_extra_stats)
+			kind_info->end_extra_stats(STATS_READ);
+	}
+
 	return;
 
 error:
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index ca1ba6420ca..48b40816570 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -63,6 +63,20 @@ typedef struct PgStat_HashKey
 								 * identifier. */
 } PgStat_HashKey;
 
+/*
+ * Tracks if the stats file is being read, written or discarded.
+ *
+ * These states allow plugins that create extra statistics files
+ * to determine the current operation and perform any necessary
+ * file cleanup.
+ */
+typedef enum PgStat_StatsFileOp
+{
+	STATS_WRITE,
+	STATS_READ,
+	STATS_DISCARD,
+}			PgStat_StatsFileOp;
+
 /*
  * PgStat_HashKey should not have any padding.  Checking that the structure
  * size matches with the sum of each field is a check simple enough to
@@ -303,6 +317,29 @@ typedef struct PgStat_KindInfo
 									   const PgStatShared_Common *header, NameData *name);
 	bool		(*from_serialized_name) (const NameData *name, PgStat_HashKey *key);
 
+	/*
+	 * Optional callbacks for kinds that write additional per-entry data to
+	 * the stats file.  If any of these callbacks are provided, all three must
+	 * be provided to ensure that the reader consumes exactly the data written
+	 * by the writer.
+	 *
+	 * to_serialized_extra_stats: write extra data for an entry.
+	 *
+	 * from_serialized_extra_stats: read the extra data for an entry. Returns
+	 * true on success, false on read error.
+	 *
+	 * end_extra_stats: invoked once per operation (read, write, discard)
+	 * after all entries of this kind have been processed.
+	 *
+	 * Note: statfile is a pointer to the main stats file,
+	 * PGSTAT_STAT_PERMANENT_FILENAME.
+	 */
+	void		(*to_serialized_extra_stats) (const PgStat_HashKey *key,
+											  const PgStatShared_Common *header, FILE *statfile);
+	bool		(*from_serialized_extra_stats) (const PgStat_HashKey *key,
+												const PgStatShared_Common *header, FILE *statfile);
+	void		(*end_extra_stats) (PgStat_StatsFileOp status);
+
 	/*
 	 * For fixed-numbered statistics: Initialize shared memory state.
 	 *
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
index c4fceb7d267..55a8956a0d9 100644
--- a/src/test/modules/test_custom_stats/t/001_custom_stats.pl
+++ b/src/test/modules/test_custom_stats/t/001_custom_stats.pl
@@ -26,10 +26,10 @@ $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')));
+$node->safe_psql('postgres', q(select pgstat_create_custom_var_stats('entry1', 'Test entry 1')));
+$node->safe_psql('postgres', q(select pgstat_create_custom_var_stats('entry2', 'Test entry 2')));
+$node->safe_psql('postgres', q(select pgstat_create_custom_var_stats('entry3', 'Test entry 3')));
+$node->safe_psql('postgres', q(select pgstat_create_custom_var_stats('entry4', 'Test entry 4')));
 
 # Update counters: entry1=2, entry2=3, entry3=2, entry4=3, fixed=3
 $node->safe_psql('postgres', q(select pgstat_update_custom_var_stats('entry1')));
@@ -48,16 +48,16 @@ $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");
+is($result, "entry1|2|Test entry 1", "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");
+is($result, "entry2|3|Test entry 2", "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");
+is($result, "entry3|2|Test entry 3", "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");
+is($result, "entry4|3|Test entry 4", "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");
@@ -76,10 +76,10 @@ $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");
+is($result, "entry1|2|Test entry 1", "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");
+is($result, "entry2|3|Test entry 2", "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");
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
index 84ae2bf5666..c509567de7d 100644
--- 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
@@ -3,7 +3,7 @@
 -- 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)
+CREATE FUNCTION pgstat_create_custom_var_stats(IN name TEXT, in description TEXT)
 RETURNS void
 AS 'MODULE_PATHNAME', 'pgstat_create_custom_var_stats'
 LANGUAGE C STRICT PARALLEL UNSAFE;
@@ -19,7 +19,9 @@ 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)
+CREATE FUNCTION pgstat_report_custom_var_stats(INOUT name TEXT,
+                                               OUT calls BIGINT,
+                                               OUT description TEXT)
 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
index 6320eaf2cae..6d755904df5 100644
--- a/src/test/modules/test_custom_stats/test_custom_var_stats.c
+++ b/src/test/modules/test_custom_stats/test_custom_var_stats.c
@@ -14,6 +14,7 @@
 
 #include "common/hashfn.h"
 #include "funcapi.h"
+#include "storage/dsm_registry.h"
 #include "utils/builtins.h"
 #include "utils/pgstat_internal.h"
 
@@ -33,6 +34,9 @@ PG_MODULE_MAGIC_EXT(
  */
 #define PGSTAT_KIND_TEST_CUSTOM_VAR_STATS 25
 
+/* File paths for extra statistics data serialization */
+#define PGSTAT_CUSTOM_EXTRA_DATA_DESC "pg_stat/test_custom_var_stats_desc.stats"
+
 /*
  * Hash statistic name to generate entry index for pgstat lookup.
  */
@@ -54,15 +58,41 @@ typedef struct PgStatShared_CustomEntry
 {
 	PgStatShared_Common header; /* standard pgstat entry header */
 	PgStat_StatCustomEntry stats;	/* custom statistics data */
+	dsa_pointer description;	/* extra statistics data */
 }			PgStatShared_CustomEntry;
 
+/*--------------------------------------------------------------------------
+ * Global Variables
+ *--------------------------------------------------------------------------
+ */
+
+/* File handle for extra statistics data serialization */
+static FILE *fd_description = NULL;
+
+/* Current write offset in fd_description file */
+static long fd_description_offset = 0;
+
+/* DSA area for storing variable-length description strings */
+dsa_area   *custom_stats_description_dsa = NULL;
+
 /*--------------------------------------------------------------------------
  * Function prototypes
  *--------------------------------------------------------------------------
  */
 
 /* Flush callback: merge pending stats into shared memory */
-static bool pgstat_custom_entry_flush_cb(PgStat_EntryRef *entry_ref, bool nowait);
+static bool pgstat_custom_var_entry_flush_cb(PgStat_EntryRef *entry_ref, bool nowait);
+
+/* Serialization callback: serialize extra statistics data */
+static void pgstat_custom_var_stats_serialize(const PgStat_HashKey *key,
+											  const PgStatShared_Common *header, FILE *statfile);
+
+/* Deserialization callback: deserialize extra statistics data */
+static bool pgstat_custom_var_stats_deserialize(const PgStat_HashKey *key,
+												const PgStatShared_Common *header, FILE *statfile);
+
+/* Cleanup callback: end of statistics file operations */
+static void pgstat_custom_var_stats_file_cleanup(PgStat_StatsFileOp status);
 
 /*--------------------------------------------------------------------------
  * Custom kind configuration
@@ -79,7 +109,10 @@ static const PgStat_KindInfo custom_stats = {
 	.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,
+	.flush_pending_cb = pgstat_custom_var_entry_flush_cb,
+	.to_serialized_extra_stats = pgstat_custom_var_stats_serialize,
+	.from_serialized_extra_stats = pgstat_custom_var_stats_deserialize,
+	.end_extra_stats = pgstat_custom_var_stats_file_cleanup,
 };
 
 /*--------------------------------------------------------------------------
@@ -113,7 +146,7 @@ _PG_init(void)
  * Returns false only if nowait=true and lock acquisition fails.
  */
 static bool
-pgstat_custom_entry_flush_cb(PgStat_EntryRef *entry_ref, bool nowait)
+pgstat_custom_var_entry_flush_cb(PgStat_EntryRef *entry_ref, bool nowait)
 {
 	PgStat_StatCustomEntry *pending_entry;
 	PgStatShared_CustomEntry *shared_entry;
@@ -132,6 +165,234 @@ pgstat_custom_entry_flush_cb(PgStat_EntryRef *entry_ref, bool nowait)
 	return true;
 }
 
+/*
+ * pgstat_custom_var_stats_serialize() -
+ *
+ * Serialize extra data (descriptions) for custom statistics entries to
+ * the statistics file. Called during statistics file writing to preserve
+ * description strings across restarts.
+ */
+static void
+pgstat_custom_var_stats_serialize(const PgStat_HashKey *key,
+								  const PgStatShared_Common *header, FILE *statfile)
+{
+	char	   *description;
+	size_t		len;
+	long		offset;
+	PgStatShared_CustomEntry *entry = (PgStatShared_CustomEntry *) header;
+	bool		found;
+
+	if (!custom_stats_description_dsa)
+		custom_stats_description_dsa = GetNamedDSA("pgstat_custom_stat_dsa", &found);
+
+	/* Open statistics file for writing if not already open */
+	if (!fd_description)
+	{
+		fd_description = AllocateFile(PGSTAT_CUSTOM_EXTRA_DATA_DESC, PG_BINARY_W);
+		if (fd_description == NULL)
+		{
+			ereport(LOG,
+					(errcode_for_file_access(),
+					 errmsg("could not open statistics file \"%s\" for writing: %m",
+							PGSTAT_CUSTOM_EXTRA_DATA_DESC)));
+			len = 0;
+			offset = 0;
+			fwrite(&len, sizeof(len), 1, statfile);
+			fwrite(&offset, sizeof(offset), 1, statfile);
+			return;
+		}
+		fd_description_offset = 0;
+	}
+
+	/* Handle entries without descriptions */
+	if (!DsaPointerIsValid(entry->description) || !custom_stats_description_dsa)
+	{
+		len = 0;
+		offset = 0;
+		fwrite(&len, sizeof(len), 1, statfile);
+		fwrite(&offset, sizeof(offset), 1, statfile);
+		return;
+	}
+
+	/* Get current offset in fd_description */
+	offset = fd_description_offset;
+
+	/* Retrieve description from DSA and write to fd_description */
+	description = dsa_get_address(custom_stats_description_dsa, entry->description);
+	len = strlen(description) + 1;
+	fwrite(description, 1, len, fd_description);
+	fd_description_offset += len;
+
+	/* Write length and offset to statfile */
+	fwrite(&len, sizeof(len), 1, statfile);
+	fwrite(&offset, sizeof(offset), 1, statfile);
+}
+
+/*
+ * pgstat_custom_var_stats_deserialize() -
+ *
+ * Deserialize extra data (descriptions) for custom statistics entries from
+ * the statistics file. Called during statistics file reading to restore
+ * description strings after a restart.
+ */
+static bool
+pgstat_custom_var_stats_deserialize(const PgStat_HashKey *key,
+									const PgStatShared_Common *header, FILE *statfile)
+{
+	PgStatShared_CustomEntry *entry;
+	dsa_pointer dp;
+	size_t		len;
+	long		offset;
+	char	   *buffer;
+	bool		found;
+
+	/* Read length and offset from statfile */
+	if (fread(&len, sizeof(len), 1, statfile) != 1 ||
+		fread(&offset, sizeof(offset), 1, statfile) != 1)
+	{
+		elog(WARNING, "failed to read description metadata from statistics file");
+		return false;
+	}
+
+	entry = (PgStatShared_CustomEntry *) header;
+
+	/* Handle empty descriptions */
+	if (len == 0)
+	{
+		entry->description = InvalidDsaPointer;
+		return true;
+	}
+
+	/* Initialize DSA if needed */
+	if (!custom_stats_description_dsa)
+		custom_stats_description_dsa = GetNamedDSA("pgstat_custom_stat_dsa", &found);
+
+	if (!custom_stats_description_dsa)
+	{
+		elog(WARNING, "could not access DSA for custom statistics descriptions");
+		return false;
+	}
+
+	/* Open statistics file for reading if not already open */
+	if (!fd_description)
+	{
+		fd_description = AllocateFile(PGSTAT_CUSTOM_EXTRA_DATA_DESC, PG_BINARY_R);
+		if (fd_description == NULL)
+		{
+			if (errno != ENOENT)
+				ereport(LOG,
+						(errcode_for_file_access(),
+						 errmsg("could not open statistics file \"%s\" for reading: %m",
+								PGSTAT_CUSTOM_EXTRA_DATA_DESC)));
+			pgstat_reset_of_kind(PGSTAT_KIND_TEST_CUSTOM_VAR_STATS);
+			return false;
+		}
+	}
+
+	/* Seek to the offset and read description */
+	if (fseek(fd_description, offset, SEEK_SET) != 0)
+	{
+		elog(WARNING, "failed to seek to offset %ld in description file", offset);
+		return false;
+	}
+
+	buffer = palloc(len);
+	if (fread(buffer, 1, len, fd_description) != len)
+	{
+		pfree(buffer);
+		elog(WARNING, "failed to read description from file");
+		return false;
+	}
+
+	/* Allocate space in DSA and copy the description */
+	dp = dsa_allocate(custom_stats_description_dsa, len);
+	memcpy(dsa_get_address(custom_stats_description_dsa, dp), buffer, len);
+	entry->description = dp;
+
+	pfree(buffer);
+
+	return true;
+}
+
+/*
+ * pgstat_custom_var_stats_file_cleanup() -
+ *
+ * Cleanup function called at the end of statistics file operations.
+ * Handles closing files and cleanup based on the operation type.
+ */
+static void
+pgstat_custom_var_stats_file_cleanup(PgStat_StatsFileOp status)
+{
+	switch (status)
+	{
+		case STATS_WRITE:
+			if (!fd_description)
+				return;
+
+			fd_description_offset = 0;
+
+			/* Check for write errors and cleanup if necessary */
+			if (ferror(fd_description))
+			{
+				ereport(LOG,
+						(errcode_for_file_access(),
+						 errmsg("could not write statistics file \"%s\": %m",
+								PGSTAT_CUSTOM_EXTRA_DATA_DESC)));
+				FreeFile(fd_description);
+				unlink(PGSTAT_CUSTOM_EXTRA_DATA_DESC);
+			}
+			else if (FreeFile(fd_description) < 0)
+			{
+				ereport(LOG,
+						(errcode_for_file_access(),
+						 errmsg("could not close statistics file \"%s\": %m",
+								PGSTAT_CUSTOM_EXTRA_DATA_DESC)));
+				unlink(PGSTAT_CUSTOM_EXTRA_DATA_DESC);
+			}
+			break;
+
+		case STATS_READ:
+			if (!fd_description)
+				return;
+
+			FreeFile(fd_description);
+
+			/* Remove the temporary statistics file after reading */
+			elog(DEBUG2, "removing statistics file \"%s\"", PGSTAT_CUSTOM_EXTRA_DATA_DESC);
+			unlink(PGSTAT_CUSTOM_EXTRA_DATA_DESC);
+			break;
+
+		case STATS_DISCARD:
+			{
+				int			ret;
+
+				/* Attempt to remove the statistics file */
+				ret = unlink(PGSTAT_CUSTOM_EXTRA_DATA_DESC);
+				if (ret != 0)
+				{
+					if (errno == ENOENT)
+						elog(LOG,
+							 "didn't need to unlink permanent stats file \"%s\" - didn't exist",
+							 PGSTAT_CUSTOM_EXTRA_DATA_DESC);
+					else
+						ereport(LOG,
+								(errcode_for_file_access(),
+								 errmsg("could not unlink permanent statistics file \"%s\": %m",
+										PGSTAT_CUSTOM_EXTRA_DATA_DESC)));
+				}
+				else
+				{
+					ereport(LOG,
+							(errmsg_internal("unlinked permanent statistics file \"%s\"",
+											 PGSTAT_CUSTOM_EXTRA_DATA_DESC)));
+				}
+			}
+			break;
+	}
+
+	fd_description = NULL;
+}
+
 /*--------------------------------------------------------------------------
  * Helper functions
  *--------------------------------------------------------------------------
@@ -161,8 +422,7 @@ pgstat_fetch_custom_entry(const char *stat_name)
  * 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.
+ * Initializes a statistics entry with the given name and description.
  */
 PG_FUNCTION_INFO_V1(pgstat_create_custom_var_stats);
 Datum
@@ -171,6 +431,9 @@ 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));
+	char	   *description = text_to_cstring(PG_GETARG_TEXT_PP(1));
+	dsa_pointer dp = InvalidDsaPointer;
+	bool		found;
 
 	/* Validate name length first */
 	if (strlen(stat_name) >= NAMEDATALEN)
@@ -179,6 +442,20 @@ pgstat_create_custom_var_stats(PG_FUNCTION_ARGS)
 				 errmsg("custom statistic name \"%s\" is too long", stat_name),
 				 errdetail("Name must be less than %d characters.", NAMEDATALEN)));
 
+	/* Initialize DSA and description provided */
+	if (!custom_stats_description_dsa)
+		custom_stats_description_dsa = GetNamedDSA("pgstat_custom_stat_dsa", &found);
+
+	if (!custom_stats_description_dsa)
+		ereport(ERROR,
+				(errmsg("could not access DSA for custom statistics descriptions")));
+
+	/* Allocate space in DSA and copy description */
+	dp = dsa_allocate(custom_stats_description_dsa, strlen(description) + 1);
+	memcpy(dsa_get_address(custom_stats_description_dsa, dp),
+		   description,
+		   strlen(description) + 1);
+
 	/* 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);
@@ -191,6 +468,9 @@ pgstat_create_custom_var_stats(PG_FUNCTION_ARGS)
 	/* Zero-initialize statistics */
 	memset(&shared_entry->stats, 0, sizeof(shared_entry->stats));
 
+	/* Store description pointer */
+	shared_entry->description = dp;
+
 	pgstat_unlock_entry(entry_ref);
 
 	PG_RETURN_VOID();
@@ -225,8 +505,7 @@ pgstat_update_custom_var_stats(PG_FUNCTION_ARGS)
  * pgstat_drop_custom_var_stats
  *		Remove custom statistic entry
  *
- * Drops the named statistic from shared memory and requests
- * garbage collection if needed.
+ * Drops the named statistic from shared memory.
  */
 PG_FUNCTION_INFO_V1(pgstat_drop_custom_var_stats);
 Datum
@@ -246,7 +525,7 @@ pgstat_drop_custom_var_stats(PG_FUNCTION_ARGS)
  * pgstat_report_custom_var_stats
  *		Retrieve custom statistic values
  *
- * Returns single row with statistic name and call count if the
+ * Returns single row with statistic name, call count, and description if the
  * statistic exists, otherwise returns no rows.
  */
 PG_FUNCTION_INFO_V1(pgstat_report_custom_var_stats);
@@ -280,9 +559,13 @@ pgstat_report_custom_var_stats(PG_FUNCTION_ARGS)
 
 	if (funcctx->call_cntr < funcctx->max_calls)
 	{
-		Datum		values[2];
-		bool		nulls[2] = {false, false};
+		Datum		values[3];
+		bool		nulls[3] = {false, false, false};
 		HeapTuple	tuple;
+		PgStat_EntryRef *entry_ref;
+		PgStatShared_CustomEntry *shared_entry;
+		char	   *description = NULL;
+		bool		found;
 
 		stat_name = text_to_cstring(PG_GETARG_TEXT_PP(0));
 		stat_entry = pgstat_fetch_custom_entry(stat_name);
@@ -290,9 +573,33 @@ pgstat_report_custom_var_stats(PG_FUNCTION_ARGS)
 		/* Return row only if entry exists */
 		if (stat_entry)
 		{
+			/* Get entry ref to access shared entry */
+			entry_ref = pgstat_get_entry_ref(PGSTAT_KIND_TEST_CUSTOM_VAR_STATS, InvalidOid,
+											 PGSTAT_CUSTOM_VAR_STATS_IDX(stat_name), false, NULL);
+
+			if (entry_ref)
+			{
+				shared_entry = (PgStatShared_CustomEntry *) entry_ref->shared_stats;
+
+				/* Get description from DSA if available */
+				if (DsaPointerIsValid(shared_entry->description))
+				{
+					if (!custom_stats_description_dsa)
+						custom_stats_description_dsa = GetNamedDSA("pgstat_custom_stat_dsa", &found);
+
+					if (custom_stats_description_dsa)
+						description = dsa_get_address(custom_stats_description_dsa, shared_entry->description);
+				}
+			}
+
 			values[0] = PointerGetDatum(cstring_to_text(stat_name));
 			values[1] = Int64GetDatum(stat_entry->numcalls);
 
+			if (description)
+				values[2] = PointerGetDatum(cstring_to_text(description));
+			else
+				nulls[2] = true;
+
 			tuple = heap_form_tuple(funcctx->tuple_desc, values, nulls);
 			SRF_RETURN_NEXT(funcctx, HeapTupleGetDatum(tuple));
 		}
-- 
2.43.0



  [application/octet-stream] v4-0001-Move-custom-stats-tests-from-injection_points-to-.patch (58.0K, ../CAA5RZ0s90-gSt9EpgA3FJHQBwhLeYk=LyaynffaaV-yNJMnu2w@mail.gmail.com/3-v4-0001-Move-custom-stats-tests-from-injection_points-to-.patch)
  download | inline diff:
From 2bcde3ae93c6f45a71d399ebdd831398361a821d Mon Sep 17 00:00:00 2001
From: Ubuntu <[email protected]>
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



view thread (47+ messages)  latest in thread

reply

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Reply to all the recipients using the --to and --cc options:
  reply via email

  To: [email protected]
  Cc: [email protected], [email protected], [email protected], [email protected]
  Subject: Re: [Proposal] Adding callback support for custom statistics kinds
  In-Reply-To: <CAA5RZ0s90-gSt9EpgA3FJHQBwhLeYk=LyaynffaaV-yNJMnu2w@mail.gmail.com>

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox