From 50fb48cfce161438ad9d3f8d39ffe0d4c7d542a4 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Tue, 2 Dec 2025 17:00:04 +0000 Subject: [PATCH v2 2/2] Tests for custom stat kinds Creates a new test module to test custom stat kinds. This also updates documentation to use this module as an example for extension developers that use custom stat kinds. --- doc/src/sgml/xfunc.sgml | 6 +- src/test/modules/Makefile | 1 + src/test/modules/meson.build | 1 + .../modules/test_custom_statkind/.gitignore | 4 + .../modules/test_custom_statkind/Makefile | 23 + .../modules/test_custom_statkind/meson.build | 34 ++ .../test_custom_statkind/t/001_custom_stat.pl | 92 +++ .../test_custom_statkind--1.0.sql | 19 + .../test_custom_statkind.c | 570 ++++++++++++++++++ .../test_custom_statkind.control | 4 + 10 files changed, 753 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/test_custom_statkind/.gitignore create mode 100644 src/test/modules/test_custom_statkind/Makefile create mode 100644 src/test/modules/test_custom_statkind/meson.build create mode 100644 src/test/modules/test_custom_statkind/t/001_custom_stat.pl create mode 100644 src/test/modules/test_custom_statkind/test_custom_statkind--1.0.sql create mode 100644 src/test/modules/test_custom_statkind/test_custom_statkind.c create mode 100644 src/test/modules/test_custom_statkind/test_custom_statkind.control diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml index 537ee6fa254..d50626523f2 100644 --- a/doc/src/sgml/xfunc.sgml +++ b/doc/src/sgml/xfunc.sgml @@ -3967,6 +3967,9 @@ static const PgStat_KindInfo custom_stats = { .shared_data_off = offsetof(PgStatShared_Custom, stats), .shared_data_len = sizeof(((PgStatShared_Custom *) 0)->stats), .pending_size = sizeof(PgStat_StatCustomEntry), + .to_serialized_extra_stats = custom_stats_serialize, + .from_serialized_extra_stats = custom_stats_deserialize, + .end_extra_stats = custom_stats_file_cleanup, } @@ -4005,7 +4008,8 @@ extern PgStat_Kind pgstat_register_kind(PgStat_Kind kind, An example describing how to register and use custom statistics can be - found in src/test/modules/injection_points. + found in src/test/modules/test_custom_statkind and + src/test/modules/injection_points diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile index d079b91b1a2..d35e83296e6 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_statkind \ test_ddl_deparse \ test_dsa \ test_dsm_registry \ diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build index f5114469b92..4f19298b6ec 100644 --- a/src/test/modules/meson.build +++ b/src/test/modules/meson.build @@ -20,6 +20,7 @@ subdir('test_bitmapset') subdir('test_bloomfilter') subdir('test_copy_callbacks') subdir('test_custom_rmgrs') +subdir('test_custom_statkind') subdir('test_ddl_deparse') subdir('test_dsa') subdir('test_dsm_registry') diff --git a/src/test/modules/test_custom_statkind/.gitignore b/src/test/modules/test_custom_statkind/.gitignore new file mode 100644 index 00000000000..5dcb3ff9723 --- /dev/null +++ b/src/test/modules/test_custom_statkind/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/test_custom_statkind/Makefile b/src/test/modules/test_custom_statkind/Makefile new file mode 100644 index 00000000000..42f48e9c8f4 --- /dev/null +++ b/src/test/modules/test_custom_statkind/Makefile @@ -0,0 +1,23 @@ +# src/test/modules/test_custom_statkind/Makefile + +MODULE_big = test_custom_statkind +OBJS = \ + $(WIN32RES) \ + test_custom_statkind.o +PGFILEDESC = "test_custom_statkind - test code for custom stat kinds" + +EXTENSION = test_custom_statkind +DATA = test_custom_statkind--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_statkind +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_custom_statkind/meson.build b/src/test/modules/test_custom_statkind/meson.build new file mode 100644 index 00000000000..50dc4dfd387 --- /dev/null +++ b/src/test/modules/test_custom_statkind/meson.build @@ -0,0 +1,34 @@ +# Copyright (c) 2025, PostgreSQL Global Development Group + +test_custom_statkind_sources = files( + 'test_custom_statkind.c', +) + +if host_system == 'windows' + test_custom_statkind_sources += rc_lib_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'test_custom_statkind', + '--FILEDESC', 'test_custom_statkind - test code for custom stat kinds',]) +endif + +test_custom_statkind = shared_module('test_custom_statkind', + test_custom_statkind_sources, + kwargs: pg_test_mod_args, +) +test_install_libs += test_custom_statkind + +test_install_data += files( + 'test_custom_statkind.control', + 'test_custom_statkind--1.0.sql', +) + +tests += { + 'name': 'test_custom_statkind', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'tap': { + 'tests': [ + 't/001_custom_stat.pl', + ], + 'runningcheck': false, + }, +} diff --git a/src/test/modules/test_custom_statkind/t/001_custom_stat.pl b/src/test/modules/test_custom_statkind/t/001_custom_stat.pl new file mode 100644 index 00000000000..00327407292 --- /dev/null +++ b/src/test/modules/test_custom_statkind/t/001_custom_stat.pl @@ -0,0 +1,92 @@ +# Copyright (c) 2021-2025, PostgreSQL Global Development Group + +# +# Test custom statistics functionality +# +# This test verifies that custom statistics entries can be created, +# incremented, reported, and persisted across server restarts. +# + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; +use File::Copy; + +# Create a new PostgreSQL test cluster +my $node = PostgreSQL::Test::Cluster->new('main'); + +# Initialize the cluster and configure it to load our test module +$node->init; +$node->append_conf('postgresql.conf', + "shared_preload_libraries = test_custom_statkind"); +$node->start; + +# Create the extension to enable custom statistics functions +$node->safe_psql('postgres', q(CREATE EXTENSION test_custom_statkind)); + +# Create two custom statistics entries with descriptions +$node->safe_psql('postgres', q(select pgstat_create_custom_stat('entry1', 'this is the description for entry1'))); +$node->safe_psql('postgres', q(select pgstat_create_custom_stat('entry2', 'this is the description for entry2'))); + +# Increment the statistics counters: +# entry1: 2 calls, entry2: 3 calls +$node->safe_psql('postgres', q(select pgstat_call_custom_stat('entry1'))); +$node->safe_psql('postgres', q(select pgstat_call_custom_stat('entry1'))); +$node->safe_psql('postgres', q(select pgstat_call_custom_stat('entry2'))); +$node->safe_psql('postgres', q(select pgstat_call_custom_stat('entry2'))); +$node->safe_psql('postgres', q(select pgstat_call_custom_stat('entry2'))); + +# Verify that the custom statistics are correctly stored and reported +my $result = $node->safe_psql('postgres', q(select * from pgstat_report_custom_stat('entry1'))); +is($result, "entry1|this is the description for entry1|2", "entry1 should have 2 calls with description"); + +$result = $node->safe_psql('postgres', q(select * from pgstat_report_custom_stat('entry2'))); +is($result, "entry2|this is the description for entry2|3", "entry2 should have 3 calls with description"); + +# Test persistence across server restart +# Perform a clean shutdown to ensure statistics are written to disk +$node->stop(); + +# Create a backup of the statistics file for later testing +my $statsfile = $PostgreSQL::Test::Utils::tmp_check . '/' . "discard_custom_stats"; +ok(!-f "$statsfile", "backup statsfile should not already exist"); + +# Locate and backup the original statistics file +my $datadir = $node->data_dir(); +my $og_stats = "$datadir/pg_stat/test_custom_statkind.stat"; +ok(-f "$og_stats", "original stats file should exist after shutdown"); +copy($og_stats, $statsfile) or die "Copy failed: $!"; + +# Restart the server to test statistics persistence +$node->start(); + +# Verify that statistics persisted across the restart +$result = $node->safe_psql('postgres', q(select * from pgstat_report_custom_stat('entry1'))); +is($result, "entry1|this is the description for entry1|2", "entry1 stats should persist after restart"); + +$result = $node->safe_psql('postgres', q(select * from pgstat_report_custom_stat('entry2'))); +is($result, "entry2|this is the description for entry2|3", "entry2 stats should persist after restart"); + +# Test statistics reset behavior +# Perform an immediate shutdown (simulates crash) to prevent stats writing +$node->stop('immediate'); + +# Restore the backed up statistics file and restart +copy($statsfile, $og_stats) or die "Copy failed: $!"; +$node->start; + +# After immediate shutdown, the stats file should be cleaned up +ok(!-f "$og_stats", "stats file should be removed after immediate shutdown recovery"); + +# Verify that statistics are reset (no descriptions, zero counts) +$result = $node->safe_psql('postgres', q(select * from pgstat_report_custom_stat('entry1'))); +is($result, "entry1||0", "entry1 should be reset after stats file cleanup"); + +$result = $node->safe_psql('postgres', q(select * from pgstat_report_custom_stat('entry2'))); +is($result, "entry2||0", "entry2 should be reset after stats file cleanup"); + +# Test completed successfully +done_testing(); \ No newline at end of file diff --git a/src/test/modules/test_custom_statkind/test_custom_statkind--1.0.sql b/src/test/modules/test_custom_statkind/test_custom_statkind--1.0.sql new file mode 100644 index 00000000000..8b085206ac8 --- /dev/null +++ b/src/test/modules/test_custom_statkind/test_custom_statkind--1.0.sql @@ -0,0 +1,19 @@ +/* src/test/modules/test_custom_statkind/test_custom_statkind--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_custom_statkind" to load this file. \quit + +CREATE FUNCTION pgstat_create_custom_stat(IN name TEXT, IN description text) +RETURNS void +AS 'MODULE_PATHNAME', 'pgstat_create_custom_stat' +LANGUAGE C STRICT PARALLEL UNSAFE; + +CREATE FUNCTION pgstat_call_custom_stat(IN name TEXT) +RETURNS void +AS 'MODULE_PATHNAME', 'pgstat_call_custom_stat' +LANGUAGE C STRICT PARALLEL UNSAFE; + +CREATE FUNCTION pgstat_report_custom_stat(INOUT name TEXT , OUT description TEXT, OUT calls BIGINT) +RETURNS SETOF record +AS 'MODULE_PATHNAME', 'pgstat_report_custom_stat' +LANGUAGE C STRICT PARALLEL UNSAFE; diff --git a/src/test/modules/test_custom_statkind/test_custom_statkind.c b/src/test/modules/test_custom_statkind/test_custom_statkind.c new file mode 100644 index 00000000000..bc9077f80a6 --- /dev/null +++ b/src/test/modules/test_custom_statkind/test_custom_statkind.c @@ -0,0 +1,570 @@ +/*-------------------------------------------------------------------------- + * + * test_custom_statkind.c + * Test module for pgstats + * + * Copyright (c) 2024-2025, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_custom_statkind/test_custom_statkind.c + * + * ------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "common/hashfn.h" +#include "funcapi.h" +#include "pgstat.h" +#include "storage/dsm_registry.h" +#include "utils/builtins.h" +#include "utils/guc.h" +#include "utils/pgstat_internal.h" + +PG_MODULE_MAGIC_EXT( + .name = "test_custom_statkind", + .version = PG_VERSION +); + +/*-------------------------------------------------------------------------- + * Macros and constants + *-------------------------------------------------------------------------- + */ + +/* + * Kind ID reserved for test_custom_statkind. This re-uses the same ID as + * Injection points to avoid reserving another kind id. + */ +#define PGSTAT_KIND_TEST_CUSTOM_STATKIND 25 + +/* + * Compute statistics entry index from point name using an 8-byte hash. + */ +#define PGSTAT_CUSTOM_STAT_IDX(name) hash_bytes_extended((const unsigned char *) name, strlen(name), 0) +#define PGSTAT_CUSTOM_EXTRA_DATA_DESC "pg_stat/test_custom_statkind.stat" + +/*-------------------------------------------------------------------------- + * Type definitions + *-------------------------------------------------------------------------- + */ + +/* Local statistics entry for pending data */ +typedef struct PgStat_StatCustomEntry +{ + PgStat_Counter numcalls; /* number of times the entry has been looked + * up */ +} PgStat_StatCustomEntry; + +/* Shared memory statistics entry */ +typedef struct PgStatShared_CustomEntry +{ + PgStatShared_Common header; + PgStat_StatCustomEntry stats; + char name[NAMEDATALEN]; + dsa_pointer description; +} PgStatShared_CustomEntry; + +/*-------------------------------------------------------------------------- + * Global variables + *-------------------------------------------------------------------------- + */ + +/* Flag indicating if custom statistics kind is loaded */ +static bool pgstat_custom_kind_loaded = false; + +/* File handle for statistics serialization */ +static FILE *fd = NULL; + +/* DSA area to store custom statistics descriptions */ +dsa_area *custom_stats_description_dsa = NULL; + +/*-------------------------------------------------------------------------- + * Function prototypes + *-------------------------------------------------------------------------- + */ + +/* Statistics callback functions */ +static bool pgstat_custom_entry_flush_cb(PgStat_EntryRef *entry_ref, bool nowait); +static void pgstat_custom_entry_extra_serialize(const PgStat_HashKey *key, + const PgStatShared_Common *header, FILE *statfile); +static void pgstat_custom_entry_extra_deserialize(const PgStat_HashKey *key, + const PgStatShared_Common *header, FILE *statfile); +static void pgstat_custom_entry_end_extra(PgStat_StatsFileOp status); + +/*-------------------------------------------------------------------------- + * Custom kind configuration + *-------------------------------------------------------------------------- + */ + +static const PgStat_KindInfo custom_stats = { + .name = "test_custom_pgstat", + .fixed_amount = false, /* Bounded by the number of points */ + .write_to_file = true, + .track_entry_count = true, + .accessed_across_databases = true, /* System-wide 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, + .to_serialized_extra_stats = pgstat_custom_entry_extra_serialize, + .from_serialized_extra_stats = pgstat_custom_entry_extra_deserialize, + .end_extra_stats = pgstat_custom_entry_end_extra, +}; + +/*-------------------------------------------------------------------------- + * Module initialization + *-------------------------------------------------------------------------- + */ + +void +_PG_init(void) +{ + if (!process_shared_preload_libraries_in_progress) + return; + + pgstat_register_kind(PGSTAT_KIND_TEST_CUSTOM_STATKIND, &custom_stats); + + pgstat_custom_kind_loaded = true; +} + +/*-------------------------------------------------------------------------- + * Statistics callback functions + *-------------------------------------------------------------------------- + */ + +/* + * pgstat_custom_entry_flush_cb() - + * + * Flush callback for custom statistics entries. Merges pending statistics + * data from local memory into shared memory. + * + * Returns true if the flush was successful, false if we couldn't acquire + * the necessary locks (when nowait is true). + */ +static bool +pgstat_custom_entry_flush_cb(PgStat_EntryRef *entry_ref, bool nowait) +{ + PgStat_StatCustomEntry *localent; + PgStatShared_CustomEntry *shfuncent; + + localent = (PgStat_StatCustomEntry *) entry_ref->pending; + shfuncent = (PgStatShared_CustomEntry *) 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; +} + +/* + * pgstat_custom_entry_extra_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_entry_extra_serialize(const PgStat_HashKey *key, + const PgStatShared_Common *header, FILE *statfile) +{ + char *description; + size_t qlen; + PgStatShared_CustomEntry *entry = (PgStatShared_CustomEntry *) header; + bool found; + + if (!pgstat_custom_kind_loaded) + return; + + if (!custom_stats_description_dsa) + custom_stats_description_dsa = GetNamedDSA("pgstat_custom_stat_dsa", &found); + + /* Exit early if DSA is not available */ + if (!custom_stats_description_dsa) + return; + + /* Open statistics file for writing if not already open */ + if (!fd) + { + fd = AllocateFile(PGSTAT_CUSTOM_EXTRA_DATA_DESC, PG_BINARY_W); + if (fd == NULL) + { + ereport(LOG, + (errcode_for_file_access(), + errmsg("could not open statistics file \"%s\" for writing: %m", + PGSTAT_CUSTOM_EXTRA_DATA_DESC))); + return; + } + } + + /* Write the hash key to identify this entry */ + pgstat_write_chunk(fd, (void *) key, sizeof(PgStat_HashKey)); + + /* Handle entries without descriptions */ + if (!DsaPointerIsValid(entry->description)) + { + fputc('\0', fd); /* Write null terminator for empty description */ + return; + } + + if (!custom_stats_description_dsa) + { + fputc('\0', fd); /* Write null terminator if DSA unavailable */ + return; + } + + /* Retrieve description from DSA and write to file */ + description = dsa_get_address(custom_stats_description_dsa, entry->description); + qlen = strlen(description) + 1; /* include null terminator */ + + pgstat_write_chunk(fd, description, qlen); +} + +/* + * pgstat_custom_entry_extra_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 void +pgstat_custom_entry_extra_deserialize(const PgStat_HashKey *key, + const PgStatShared_Common *header, FILE *statfile) +{ + PgStatShared_CustomEntry *entry; + dsa_pointer dp; + size_t bufsize; + size_t len; + char *buffer; + int c; + bool found; + + if (!pgstat_custom_kind_loaded) + return; + + /* Initialize DSA if needed */ + if (!custom_stats_description_dsa) + custom_stats_description_dsa = GetNamedDSA("pgstat_custom_stat_dsa", &found); + + if (!custom_stats_description_dsa) + return; + + /* Open statistics file for reading if not already open */ + if (!fd) + { + fd = AllocateFile(PGSTAT_CUSTOM_EXTRA_DATA_DESC, PG_BINARY_R); + if (fd == 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))); + /* Reset statistics if file is missing or unreadable */ + pgstat_reset_of_kind(PGSTAT_KIND_TEST_CUSTOM_STATKIND); + return; + } + } + + /* Read and verify the hash key */ + if (!pgstat_read_chunk(fd, (void *) key, sizeof(PgStat_HashKey))) + return; + + entry = (PgStatShared_CustomEntry *) header; + + /* Read null-terminated description string from file */ + bufsize = 128; + len = 0; + buffer = palloc(bufsize); + + /* Read description character by character until null terminator */ + while ((c = fgetc(fd)) != EOF) + { + /* Expand buffer if needed */ + if (len + 1 >= bufsize) + { + bufsize *= 2; + buffer = repalloc(buffer, bufsize); + } + + buffer[len++] = (char) c; + + if (c == '\0') + break; + } + + /* Handle unexpected EOF */ + if (c == EOF) + { + pfree(buffer); + return; + } + + /* 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); +} + +/* + * pgstat_custom_entry_end_extra() - + * + * Cleanup function called at the end of statistics file operations. + * Handles closing files and cleanup based on the operation type. + */ +static void +pgstat_custom_entry_end_extra(PgStat_StatsFileOp status) +{ + if (!pgstat_custom_kind_loaded) + return; + + /* Handle cleanup after writing statistics */ + if (status == STATS_WRITE) + { + if (!fd) + return; + + /* Check for write errors and cleanup if necessary */ + if (ferror(fd)) + { + ereport(LOG, + (errcode_for_file_access(), + errmsg("could not write statistics file \"%s\": %m", + PGSTAT_CUSTOM_EXTRA_DATA_DESC))); + FreeFile(fd); + unlink(PGSTAT_CUSTOM_EXTRA_DATA_DESC); + } + else if (FreeFile(fd) < 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); + } + } + /* Handle cleanup after reading statistics */ + else if (status == STATS_READ) + { + if (!fd) + return; + + FreeFile(fd); + + /* Remove the temporary statistics file after reading */ + elog(DEBUG2, "removing statistics file \"%s\"", PGSTAT_CUSTOM_EXTRA_DATA_DESC); + unlink(PGSTAT_CUSTOM_EXTRA_DATA_DESC); + } + /* Handle other cleanup operations (reset, etc.) */ + else + { + 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))); + } + } + + fd = NULL; +} + +/*-------------------------------------------------------------------------- + * SQL-callable functions + *-------------------------------------------------------------------------- + */ + +/* + * pgstat_create_custom_stat() - + * + * SQL-callable function to create a new custom statistics entry. + * Initializes shared memory structure for tracking statistics with + * a name and optional description. + */ +PG_FUNCTION_INFO_V1(pgstat_create_custom_stat); +Datum +pgstat_create_custom_stat(PG_FUNCTION_ARGS) +{ + PgStat_EntryRef *entry_ref; + bool found; + char *desc_copy; + size_t len; + PgStatShared_CustomEntry *shstatent; + char *name = text_to_cstring(PG_GETARG_TEXT_PP(0)); + char *description = text_to_cstring(PG_GETARG_TEXT_PP(1)); + + if (!pgstat_custom_kind_loaded) + PG_RETURN_VOID(); + + entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_TEST_CUSTOM_STATKIND, InvalidOid, + PGSTAT_CUSTOM_STAT_IDX(name), true); + + if (!entry_ref) + PG_RETURN_VOID(); + + shstatent = (PgStatShared_CustomEntry *) entry_ref->shared_stats; + + /* Initialize shared memory statistics data to zero */ + memset(&shstatent->stats, 0, sizeof(shstatent->stats)); + + /* Validate and store the statistic name */ + if (strlen(name) >= NAMEDATALEN) + ereport(ERROR, + (errcode(ERRCODE_NAME_TOO_LONG), + errmsg("custom statistic name \"%s\" is too long", name), + errdetail("Name must be less than %d characters.", NAMEDATALEN))); + + strcpy(shstatent->name, name); + + /* Store description in DSA if provided */ + if (description) + { + len = strlen(description) + 1; + + /* Initialize DSA for descriptions if not already done */ + if (!custom_stats_description_dsa) + custom_stats_description_dsa = GetNamedDSA("pgstat_custom_stat_dsa", &found); + + if (custom_stats_description_dsa) + { + /* Allocate space in DSA and copy description */ + shstatent->description = dsa_allocate(custom_stats_description_dsa, len); + if (!DsaPointerIsValid(shstatent->description)) + { + /* DSA allocation failed, continue without description */ + shstatent->description = InvalidDsaPointer; + } + else + { + desc_copy = dsa_get_address(custom_stats_description_dsa, + shstatent->description); + memcpy(desc_copy, description, len); + } + } + } + + pgstat_unlock_entry(entry_ref); + + PG_RETURN_VOID(); +} + +/* + * pgstat_call_custom_stat() - + * + * SQL-callable function to increment the call count for a custom statistic. + * This is typically called from user code to track usage or events. + */ +PG_FUNCTION_INFO_V1(pgstat_call_custom_stat); +Datum +pgstat_call_custom_stat(PG_FUNCTION_ARGS) +{ + char *name = text_to_cstring(PG_GETARG_TEXT_PP(0)); + PgStat_EntryRef *entry_ref; + PgStat_StatCustomEntry *pending; + + if (!pgstat_custom_kind_loaded) + PG_RETURN_VOID(); + + /* Get the pending statistics entry for this custom stat */ + entry_ref = pgstat_prep_pending_entry(PGSTAT_KIND_TEST_CUSTOM_STATKIND, InvalidOid, + PGSTAT_CUSTOM_STAT_IDX(name), NULL); + + pending = (PgStat_StatCustomEntry *) entry_ref->pending; + + /* Increment the call counter in local pending stats */ + pending->numcalls++; + + PG_RETURN_VOID(); +} + +/* + * pgstat_report_custom_stat() - + * + * SQL-callable function to retrieve statistics for a custom statistic. + * Returns a composite type containing the name, description, and call count. + */ +PG_FUNCTION_INFO_V1(pgstat_report_custom_stat); +Datum +pgstat_report_custom_stat(PG_FUNCTION_ARGS) +{ + char *name = text_to_cstring(PG_GETARG_TEXT_PP(0)); + PgStat_EntryRef *entry_ref; + PgStatShared_CustomEntry *shfuncent; + Datum values[3]; + bool nulls[3] = {false, false, false}; + TupleDesc tupdesc; + TupleDesc expected; + bool found; + + if (!pgstat_custom_kind_loaded) + PG_RETURN_NULL(); + + /* Initialize DSA for descriptions if needed */ + if (!custom_stats_description_dsa) + custom_stats_description_dsa = + GetNamedDSA("pgstat_custom_stat_dsa", &found); + + /* Return NULL if DSA is not available */ + if (!custom_stats_description_dsa) + PG_RETURN_NULL(); + + /* Get the return tuple descriptor from pg_proc */ + if (get_call_result_type(fcinfo, NULL, &expected) != TYPEFUNC_COMPOSITE) + elog(ERROR, "pgstat_report_custom_stat: return type is not composite"); + + tupdesc = BlessTupleDesc(expected); + + /* Look up the statistics entry in shared memory */ + entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_TEST_CUSTOM_STATKIND, + InvalidOid, + PGSTAT_CUSTOM_STAT_IDX(name), + true); + + if (entry_ref) + { + char *desc_copy = NULL; + + shfuncent = (PgStatShared_CustomEntry *) entry_ref->shared_stats; + + /* Build the return tuple with name, description, and call count */ + values[0] = CStringGetTextDatum(name); /* name */ + if (DsaPointerIsValid(shfuncent->description)) + { + /* Get description from DSA */ + desc_copy = dsa_get_address(custom_stats_description_dsa, + shfuncent->description); + + values[1] = CStringGetTextDatum(desc_copy); /* description */ + } + else + nulls[1] = true; /* no description available */ + + values[2] = Int64GetDatum(shfuncent->stats.numcalls); /* calls */ + } + + pgstat_unlock_entry(entry_ref); + + PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls))); +} diff --git a/src/test/modules/test_custom_statkind/test_custom_statkind.control b/src/test/modules/test_custom_statkind/test_custom_statkind.control new file mode 100644 index 00000000000..7ce22cc195f --- /dev/null +++ b/src/test/modules/test_custom_statkind/test_custom_statkind.control @@ -0,0 +1,4 @@ +comment = 'Test code for custom stat kinds' +default_version = '1.0' +module_pathname = '$libdir/test_custom_statkind' +relocatable = true -- 2.43.0