From 4d8d47faf44db0e282f91e044d044a454a747c0b Mon Sep 17 00:00:00 2001 From: Artem Gavrilov Date: Thu, 30 Jul 2026 18:23:21 +0200 Subject: [PATCH 1/9] PG-2424 Support PostgreSQL 19 Make pg_stat_monitor compilable against PostgreSQL 19 PostgreSQL 19 optimized SELECT ... INTO statements execution(ce8d5fe), so some of them are no longer tracked by pg_stat_statements/pg_stat_monitor. So we update level tracking test to address this change. Build and test CI workflow required timeout increase as PG 19 requires slightly more time. --- .github/workflows/build-and-test.yml | 2 +- regression/expected/decode_error_level.out | 3 +- regression/expected/decode_error_level_1.out | 27 ++ regression/expected/level_tracking_2.out | 329 ++++++++++++++++++ regression/sql/decode_error_level.sql | 2 +- src/hash_query.c | 10 + src/pg_stat_monitor.c | 81 ++++- ...7_settings_pgsm_query_shared_buffer.out.19 | 150 ++++++++ 8 files changed, 593 insertions(+), 11 deletions(-) create mode 100644 regression/expected/decode_error_level_1.out create mode 100644 regression/expected/level_tracking_2.out create mode 100644 t/expected/007_settings_pgsm_query_shared_buffer.out.19 diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 858622ee..dce87444 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -29,7 +29,7 @@ jobs: build: name: Build runs-on: ${{ inputs.os }} - timeout-minutes: 10 + timeout-minutes: 15 steps: - name: Remove existing PostgreSQL if: startsWith(inputs.os, 'ubuntu-') diff --git a/regression/expected/decode_error_level.out b/regression/expected/decode_error_level.out index ce5066a8..1e8d1cb4 100644 --- a/regression/expected/decode_error_level.out +++ b/regression/expected/decode_error_level.out @@ -3,7 +3,7 @@ DO $$ DECLARE i int; BEGIN - FOR i IN 10..24 LOOP + FOR i IN 10..25 LOOP RAISE NOTICE 'error_code: %, error_level: %', i, decode_error_level(i); END LOOP; END @@ -23,4 +23,5 @@ NOTICE: error_code: 21, error_level: ERROR NOTICE: error_code: 22, error_level: FATAL NOTICE: error_code: 23, error_level: PANIC NOTICE: error_code: 24, error_level: +NOTICE: error_code: 25, error_level: DROP EXTENSION pg_stat_monitor; diff --git a/regression/expected/decode_error_level_1.out b/regression/expected/decode_error_level_1.out new file mode 100644 index 00000000..11d089ce --- /dev/null +++ b/regression/expected/decode_error_level_1.out @@ -0,0 +1,27 @@ +CREATE EXTENSION pg_stat_monitor; +DO $$ +DECLARE + i int; +BEGIN + FOR i IN 10..25 LOOP + RAISE NOTICE 'error_code: %, error_level: %', i, decode_error_level(i); + END LOOP; +END +$$; +NOTICE: error_code: 10, error_level: DEBUG5 +NOTICE: error_code: 11, error_level: DEBUG4 +NOTICE: error_code: 12, error_level: DEBUG3 +NOTICE: error_code: 13, error_level: DEBUG2 +NOTICE: error_code: 14, error_level: DEBUG1 +NOTICE: error_code: 15, error_level: LOG +NOTICE: error_code: 16, error_level: LOG_SERVER_ONLY +NOTICE: error_code: 17, error_level: INFO +NOTICE: error_code: 18, error_level: NOTICE +NOTICE: error_code: 19, error_level: WARNING +NOTICE: error_code: 20, error_level: WARNING_CLIENT_ONLY +NOTICE: error_code: 21, error_level: ERROR +NOTICE: error_code: 22, error_level: FATAL +NOTICE: error_code: 23, error_level: FATAL_CLIENT_ONLY +NOTICE: error_code: 24, error_level: PANIC +NOTICE: error_code: 25, error_level: +DROP EXTENSION pg_stat_monitor; diff --git a/regression/expected/level_tracking_2.out b/regression/expected/level_tracking_2.out new file mode 100644 index 00000000..077766eb --- /dev/null +++ b/regression/expected/level_tracking_2.out @@ -0,0 +1,329 @@ +-- +-- Statement level tracking +-- +CREATE EXTENSION pg_stat_monitor; +SET pg_stat_monitor.pgsm_track_utility = on; +SET pg_stat_monitor.pgsm_normalized_query = on; +SELECT pg_stat_monitor_reset(); + pg_stat_monitor_reset +----------------------- + +(1 row) + +-- DO block - top-level tracking. +CREATE TABLE stats_track_tab (x int); +SET pg_stat_monitor.pgsm_track = 'top'; +DELETE FROM stats_track_tab; +DO $$ +BEGIN + DELETE FROM stats_track_tab; +END +$$; +SELECT toplevel, calls, query FROM pg_stat_monitor + WHERE query LIKE '%DELETE%' ORDER BY query COLLATE "C", toplevel; + toplevel | calls | query +----------+-------+---------------------------------- + t | 1 | DELETE FROM stats_track_tab + t | 1 | DO $$ + + | | BEGIN + + | | DELETE FROM stats_track_tab;+ + | | END + + | | $$ +(2 rows) + +SELECT pg_stat_monitor_reset(); + pg_stat_monitor_reset +----------------------- + +(1 row) + +-- DO block - all-level tracking. +SET pg_stat_monitor.pgsm_track = 'all'; +DELETE FROM stats_track_tab; +DO $$ +BEGIN + DELETE FROM stats_track_tab; +END +$$; +DO $$ +BEGIN + -- this is a SELECT + PERFORM 'hello world'::text; +END +$$; +SELECT toplevel, calls, query FROM pg_stat_monitor + ORDER BY query COLLATE "C", toplevel; + toplevel | calls | query +----------+-------+---------------------------------------- + f | 1 | DELETE FROM stats_track_tab + t | 1 | DELETE FROM stats_track_tab + t | 1 | DO $$ + + | | BEGIN + + | | -- this is a SELECT + + | | PERFORM 'hello world'::text; + + | | END + + | | $$ + t | 1 | DO $$ + + | | BEGIN + + | | DELETE FROM stats_track_tab; + + | | END + + | | $$ + f | 1 | SELECT $1::text + t | 1 | SELECT pg_stat_monitor_reset() + t | 1 | SET pg_stat_monitor.pgsm_track = 'all' +(7 rows) + +-- DO block - top-level tracking without utility. +SET pg_stat_monitor.pgsm_track = 'top'; +SET pg_stat_monitor.pgsm_track_utility = off; +SELECT pg_stat_monitor_reset(); + pg_stat_monitor_reset +----------------------- + +(1 row) + +DELETE FROM stats_track_tab; +DO $$ +BEGIN + DELETE FROM stats_track_tab; +END +$$; +DO $$ +BEGIN + -- this is a SELECT + PERFORM 'hello world'::text; +END +$$; +SELECT toplevel, calls, query FROM pg_stat_monitor + ORDER BY query COLLATE "C", toplevel; + toplevel | calls | query +----------+-------+-------------------------------- + t | 1 | DELETE FROM stats_track_tab + t | 1 | SELECT pg_stat_monitor_reset() +(2 rows) + +-- DO block - all-level tracking without utility. +SET pg_stat_monitor.pgsm_track = 'all'; +SELECT pg_stat_monitor_reset(); + pg_stat_monitor_reset +----------------------- + +(1 row) + +DELETE FROM stats_track_tab; +DO $$ +BEGIN + DELETE FROM stats_track_tab; +END +$$; +DO $$ +BEGIN + -- this is a SELECT + PERFORM 'hello world'::text; +END +$$; +SELECT toplevel, calls, query FROM pg_stat_monitor + ORDER BY query COLLATE "C", toplevel; + toplevel | calls | query +----------+-------+-------------------------------- + f | 1 | DELETE FROM stats_track_tab + t | 1 | DELETE FROM stats_track_tab + f | 1 | SELECT $1::text + t | 1 | SELECT pg_stat_monitor_reset() +(4 rows) + +-- PL/pgSQL function - top-level tracking. +SET pg_stat_monitor.pgsm_track = 'top'; +SET pg_stat_monitor.pgsm_track_utility = off; +SELECT pg_stat_monitor_reset(); + pg_stat_monitor_reset +----------------------- + +(1 row) + +CREATE FUNCTION plus_two(i int) RETURNS int AS $$ +DECLARE + r int; +BEGIN + SELECT (i + 1 + 1.0)::int INTO r; + RETURN r; +END +$$ LANGUAGE plpgsql; +SELECT plus_two(3); + plus_two +---------- + 5 +(1 row) + +SELECT plus_two(7); + plus_two +---------- + 9 +(1 row) + +-- SQL function --- use LIMIT to keep it from being inlined +CREATE FUNCTION plus_one(i int) RETURNS int AS +$$ SELECT (i + 1.0)::int LIMIT 1 $$ LANGUAGE sql; +SELECT plus_one(8); + plus_one +---------- + 9 +(1 row) + +SELECT plus_one(10); + plus_one +---------- + 11 +(1 row) + +SELECT calls, rows, query FROM pg_stat_monitor ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+-------------------------------- + 1 | 1 | SELECT pg_stat_monitor_reset() + 2 | 2 | SELECT plus_one($1) + 2 | 2 | SELECT plus_two($1) +(3 rows) + +-- immutable SQL function --- can be executed at plan time +CREATE FUNCTION plus_three(i int) RETURNS int AS +$$ SELECT i + 3 LIMIT 1 $$ IMMUTABLE LANGUAGE sql; +SELECT plus_three(8); + plus_three +------------ + 11 +(1 row) + +SELECT plus_three(10); + plus_three +------------ + 13 +(1 row) + +SELECT toplevel, calls, rows, query FROM pg_stat_monitor ORDER BY query COLLATE "C"; + toplevel | calls | rows | query +----------+-------+------+--------------------------------------------------------------------------- + t | 1 | 3 | SELECT calls, rows, query FROM pg_stat_monitor ORDER BY query COLLATE "C" + t | 1 | 1 | SELECT pg_stat_monitor_reset() + t | 2 | 2 | SELECT plus_one($1) + t | 2 | 2 | SELECT plus_three($1) + t | 2 | 2 | SELECT plus_two($1) +(5 rows) + +-- PL/pgSQL function - all-level tracking. +SET pg_stat_monitor.pgsm_track = 'all'; +SELECT pg_stat_monitor_reset(); + pg_stat_monitor_reset +----------------------- + +(1 row) + +-- we drop and recreate the functions to avoid any caching funnies +DROP FUNCTION plus_one(int); +DROP FUNCTION plus_two(int); +DROP FUNCTION plus_three(int); +-- PL/pgSQL function +CREATE FUNCTION plus_two(i int) RETURNS int AS $$ +DECLARE + r int; +BEGIN + SELECT (i + 1 + 1.0)::int INTO r; + RETURN r; +END +$$ LANGUAGE plpgsql; +SELECT plus_two(-1); + plus_two +---------- + 1 +(1 row) + +SELECT plus_two(2); + plus_two +---------- + 4 +(1 row) + +-- SQL function --- use LIMIT to keep it from being inlined +CREATE FUNCTION plus_one(i int) RETURNS int AS +$$ SELECT (i + 1.0)::int LIMIT 1 $$ LANGUAGE sql; +SELECT plus_one(3); + plus_one +---------- + 4 +(1 row) + +SELECT plus_one(1); + plus_one +---------- + 2 +(1 row) + +SELECT calls, rows, query FROM pg_stat_monitor ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+-------------------------------- + 2 | 2 | SELECT (i + $2)::int LIMIT $3 + 1 | 1 | SELECT pg_stat_monitor_reset() + 2 | 2 | SELECT plus_one($1) + 2 | 2 | SELECT plus_two($1) +(4 rows) + +-- immutable SQL function --- can be executed at plan time +CREATE FUNCTION plus_three(i int) RETURNS int AS +$$ SELECT i + 3 LIMIT 1 $$ IMMUTABLE LANGUAGE sql; +SELECT plus_three(8); + plus_three +------------ + 11 +(1 row) + +SELECT plus_three(10); + plus_three +------------ + 13 +(1 row) + +SELECT toplevel, calls, rows, query FROM pg_stat_monitor ORDER BY query COLLATE "C"; + toplevel | calls | rows | query +----------+-------+------+--------------------------------------------------------------------------- + f | 2 | 2 | SELECT (i + $2)::int LIMIT $3 + t | 1 | 4 | SELECT calls, rows, query FROM pg_stat_monitor ORDER BY query COLLATE "C" + f | 2 | 2 | SELECT i + $2 LIMIT $3 + t | 1 | 1 | SELECT pg_stat_monitor_reset() + t | 2 | 2 | SELECT plus_one($1) + t | 2 | 2 | SELECT plus_three($1) + t | 2 | 2 | SELECT plus_two($1) +(7 rows) + +-- +-- pg_stat_monitor.pgsm_track = none +-- +SET pg_stat_monitor.pgsm_track = 'none'; +SELECT pg_stat_monitor_reset(); + pg_stat_monitor_reset +----------------------- + +(1 row) + +SELECT 1 AS one; + one +----- + 1 +(1 row) + +SELECT 1 + 1 AS two; + two +----- + 2 +(1 row) + +SELECT calls, rows, query FROM pg_stat_monitor ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+------- +(0 rows) + +SELECT pg_stat_monitor_reset(); + pg_stat_monitor_reset +----------------------- + +(1 row) + +DROP EXTENSION pg_stat_monitor; diff --git a/regression/sql/decode_error_level.sql b/regression/sql/decode_error_level.sql index 681a93db..1f709f77 100644 --- a/regression/sql/decode_error_level.sql +++ b/regression/sql/decode_error_level.sql @@ -4,7 +4,7 @@ DO $$ DECLARE i int; BEGIN - FOR i IN 10..24 LOOP + FOR i IN 10..25 LOOP RAISE NOTICE 'error_code: %, error_level: %', i, decode_error_level(i); END LOOP; END diff --git a/src/hash_query.c b/src/hash_query.c index 501a1b0e..ce36af27 100644 --- a/src/hash_query.c +++ b/src/hash_query.c @@ -117,7 +117,11 @@ pgsm_startup(void) pgsm->raw_dsa_area = p; dsa = dsa_create_in_place(pgsm->raw_dsa_area, pgsm_query_area_size(), +#if PG_VERSION_NUM >= 190000 + LWLockNewTrancheId("pg_stat_monitor_dsa"), 0); +#else LWLockNewTrancheId(), 0); +#endif dsa_pin(dsa); dsa_set_size_limit(dsa, pgsm_query_area_size()); @@ -156,9 +160,15 @@ pgsm_create_bucket_hash(void) .entrysize = sizeof(pgsmEntry), }; +#if PG_VERSION_NUM >= 190000 + return ShmemInitHash("pg_stat_monitor: bucket hashtable", + pgsm_bucket_hash_max_entries(), + &info, HASH_ELEM | HASH_BLOBS); +#else return ShmemInitHash("pg_stat_monitor: bucket hashtable", pgsm_bucket_hash_max_entries(), pgsm_bucket_hash_max_entries(), &info, HASH_ELEM | HASH_BLOBS); +#endif } /* diff --git a/src/pg_stat_monitor.c b/src/pg_stat_monitor.c index 9e979b9b..9e0feab7 100644 --- a/src/pg_stat_monitor.c +++ b/src/pg_stat_monitor.c @@ -46,6 +46,7 @@ #include #include #include +#include #if PG_VERSION_NUM >= 180000 #include @@ -102,6 +103,12 @@ static int max_nesting_level; static int plan_nested_level = 0; #endif +#if PG_VERSION_NUM >= 190000 +#define pgsm_query_instr(qd) ((qd)->query_instr) +#else +#define pgsm_query_instr(qd) ((qd)->totaltime) +#endif + /* Histogram bucket variables */ static double hist_bucket_min; static double hist_bucket_max; @@ -160,7 +167,11 @@ static ExecutorCheckPerms_hook_type prev_ExecutorCheckPerms_hook = NULL; static void pgsm_shmem_request(void); #endif static void pgsm_emit_log_hook(ErrorData *edata); +#if PG_VERSION_NUM >= 190000 +static void pgsm_post_parse_analyze(ParseState *pstate, Query *query, const JumbleState *jstate); +#else static void pgsm_post_parse_analyze(ParseState *pstate, Query *query, JumbleState *jstate); +#endif static void pgsm_ExecutorStart(QueryDesc *queryDesc, int eflags); #if PG_VERSION_NUM >= 180000 static void pgsm_ExecutorRun(QueryDesc *queryDesc, ScanDirection direction, uint64 count); @@ -174,8 +185,14 @@ static bool pgsm_ExecutorCheckPerms(List *rangeTable, List *rtePermInfos, bool e #else static bool pgsm_ExecutorCheckPerms(List *rangeTable, bool ereport_on_violation); #endif +#if PG_VERSION_NUM >= 190000 +static PlannedStmt *pgsm_planner_hook(Query *parse, const char *query_string, + int cursorOptions, ParamListInfo boundParams, + ExplainState *es); +#else static PlannedStmt *pgsm_planner_hook(Query *parse, const char *query_string, int cursorOptions, ParamListInfo boundParams); +#endif static void pgsm_ProcessUtility(PlannedStmt *pstmt, const char *queryString, bool readOnlyTree, ProcessUtilityContext context, @@ -267,9 +284,9 @@ static void pg_stat_monitor_internal(FunctionCallInfo fcinfo, pgsmVersion api_version, bool showtext); -static char *generate_normalized_query(JumbleState *jstate, const char *query, +static char *generate_normalized_query(const JumbleState *jstate, const char *query, int query_loc, int *query_len_p); -static void fill_in_constant_lengths(JumbleState *jstate, const char *query, int query_loc); +static void fill_in_constant_lengths(const JumbleState *jstate, const char *query, int query_loc); static int comp_location(const void *a, const void *b); static uint64 get_next_wbucket(pgsmSharedState *pgsm); @@ -423,7 +440,11 @@ pgsm_shmem_request(void) * Post-parse-analysis hook: mark query with a queryId */ static void +#if PG_VERSION_NUM >= 190000 +pgsm_post_parse_analyze(ParseState *pstate, Query *query, const JumbleState *jstate) +#else pgsm_post_parse_analyze(ParseState *pstate, Query *query, JumbleState *jstate) +#endif { const char *query_text; int query_len; @@ -526,6 +547,17 @@ pgsm_ExecutorStart(QueryDesc *queryDesc, int eflags) if (pgsm_enabled(nesting_level)) getrusage(RUSAGE_SELF, &rusage_start); +#if PG_VERSION_NUM >= 190000 + + /* + * Query-level instrumentation is allocated by ExecutorStart based on + * query_instr_options since PostgreSQL 19. Request all summary + * instrumentation (timing, buffers and WAL) before starting the executor. + */ + if (pgsm_enabled(nesting_level) && queryDesc->plannedstmt->queryId != INT64CONST(0)) + queryDesc->query_instr_options |= INSTRUMENT_ALL; +#endif + if (prev_ExecutorStart) prev_ExecutorStart(queryDesc, eflags); else @@ -547,6 +579,8 @@ pgsm_ExecutorStart(QueryDesc *queryDesc, int eflags) (void) pgsm_get_query_stats(queryDesc->plannedstmt->queryId, 0, queryDesc->sourceText, queryDesc->operation); +#if PG_VERSION_NUM < 190000 + /* * Set up to track total elapsed time in ExecutorRun. Make sure the * space is allocated in the per-query context so it will go away at @@ -560,6 +594,7 @@ pgsm_ExecutorStart(QueryDesc *queryDesc, int eflags) queryDesc->totaltime = InstrAlloc(1, INSTRUMENT_ALL, false); MemoryContextSwitchTo(oldcxt); } +#endif } } @@ -687,7 +722,7 @@ pgsm_ExecutorEnd(QueryDesc *queryDesc) plan_ptr = &plan_info; } - if (queryId != INT64CONST(0) && queryDesc->totaltime && pgsm_enabled(nesting_level)) + if (queryId != INT64CONST(0) && pgsm_query_instr(queryDesc) && pgsm_enabled(nesting_level)) { pgsmQueryStats *stats; struct rusage rusage_end; @@ -699,11 +734,15 @@ pgsm_ExecutorEnd(QueryDesc *queryDesc) if (stats->key.planid == 0 && planid != 0) stats->key.planid = planid; +#if PG_VERSION_NUM < 190000 + /* * Make sure stats accumulation is done. (Note: it's okay if several - * levels of hook all do this.) + * levels of hook all do this.) In PG 19+ the query-level + * instrumentation is finalized by the executor itself. */ InstrEndLoop(queryDesc->totaltime); +#endif getrusage(RUSAGE_SELF, &rusage_end); sys_info.utime = time_diff(rusage_end.ru_utime, rusage_start.ru_utime); @@ -715,10 +754,14 @@ pgsm_ExecutorEnd(QueryDesc *queryDesc) plan_ptr, /* PlanInfo */ &sys_info, /* SysInfo */ 0, /* plan_total_time */ +#if PG_VERSION_NUM >= 190000 + INSTR_TIME_GET_MILLISEC(queryDesc->query_instr->total), /* exec_total_time */ +#else queryDesc->totaltime->total * 1000.0, /* exec_total_time */ +#endif queryDesc->estate->es_processed, /* rows */ - &queryDesc->totaltime->bufusage, /* bufusage */ - &queryDesc->totaltime->walusage, /* walusage */ + &pgsm_query_instr(queryDesc)->bufusage, /* bufusage */ + &pgsm_query_instr(queryDesc)->walusage, /* walusage */ #if PG_VERSION_NUM >= 150000 queryDesc->estate->es_jit ? &queryDesc->estate->es_jit->instr : NULL, /* jitusage */ #else @@ -814,7 +857,11 @@ pgsm_ExecutorCheckPerms(List *rangeTable, bool ereport_on_violation) } static PlannedStmt * +#if PG_VERSION_NUM >= 190000 +pgsm_planner_hook(Query *parse, const char *query_string, int cursorOptions, ParamListInfo boundParams, ExplainState *es) +#else pgsm_planner_hook(Query *parse, const char *query_string, int cursorOptions, ParamListInfo boundParams) +#endif { PlannedStmt *result; int64 queryId = parse->queryId; @@ -876,9 +923,15 @@ pgsm_planner_hook(Query *parse, const char *query_string, int cursorOptions, Par * the second call would trigger an assertion failure. */ if (planner_hook_next) +#if PG_VERSION_NUM >= 190000 + result = planner_hook_next(parse, query_string, cursorOptions, boundParams, es); + else + result = standard_planner(parse, query_string, cursorOptions, boundParams, es); +#else result = planner_hook_next(parse, query_string, cursorOptions, boundParams); else result = standard_planner(parse, query_string, cursorOptions, boundParams); +#endif } PG_FINALLY(); { @@ -937,9 +990,15 @@ pgsm_planner_hook(Query *parse, const char *query_string, int cursorOptions, Par PG_TRY(); { if (planner_hook_next) +#if PG_VERSION_NUM >= 190000 + result = planner_hook_next(parse, query_string, cursorOptions, boundParams, es); + else + result = standard_planner(parse, query_string, cursorOptions, boundParams, es); +#else result = planner_hook_next(parse, query_string, cursorOptions, boundParams); else result = standard_planner(parse, query_string, cursorOptions, boundParams); +#endif } PG_FINALLY(); { @@ -2418,6 +2477,10 @@ decode_error_level(int elevel) return "ERROR"; case FATAL: return "FATAL"; +#if PG_VERSION_NUM >= 190000 + case FATAL_CLIENT_ONLY: + return "FATAL_CLIENT_ONLY"; +#endif case PANIC: return "PANIC"; default: @@ -2637,7 +2700,7 @@ get_pgsm_query_id_hash(const char *norm_query, int norm_len) * Returns a palloc'd string. */ static char * -generate_normalized_query(JumbleState *jstate, const char *query, +generate_normalized_query(const JumbleState *jstate, const char *query, int query_loc, int *query_len_p) { char *norm_query; @@ -2773,7 +2836,7 @@ generate_normalized_query(JumbleState *jstate, const char *query, * reason for a constant to start with a '-'. */ static void -fill_in_constant_lengths(JumbleState *jstate, const char *query, +fill_in_constant_lengths(const JumbleState *jstate, const char *query, int query_loc) { LocationLen *locs; @@ -2799,8 +2862,10 @@ fill_in_constant_lengths(JumbleState *jstate, const char *query, &ScanKeywords, ScanKeywordTokens); +#if PG_VERSION_NUM < 190000 /* we don't want to re-emit any escape string warnings */ yyextra.escape_string_warning = false; +#endif /* Search for each constant, in sequence */ for (i = 0; i < jstate->clocations_count; i++) diff --git a/t/expected/007_settings_pgsm_query_shared_buffer.out.19 b/t/expected/007_settings_pgsm_query_shared_buffer.out.19 new file mode 100644 index 00000000..246ac758 --- /dev/null +++ b/t/expected/007_settings_pgsm_query_shared_buffer.out.19 @@ -0,0 +1,150 @@ +CREATE EXTENSION pg_stat_monitor; +SELECT pg_stat_monitor_reset(); + pg_stat_monitor_reset +----------------------- + +(1 row) + +SELECT name, setting, unit, context, vartype, source, min_val, max_val, enumvals, boot_val, reset_val, pending_restart FROM pg_settings WHERE name = 'pg_stat_monitor.pgsm_query_shared_buffer'; + name | setting | unit | context | vartype | source | min_val | max_val | enumvals | boot_val | reset_val | pending_restart +------------------------------------------+---------+------+------------+---------+--------------------+---------+---------+----------+----------+-----------+----------------- + pg_stat_monitor.pgsm_query_shared_buffer | 1 | MB | postmaster | integer | configuration file | 1 | 10000 | | 20 | 1 | f +(1 row) + +CREATE DATABASE example; +SELECT datname, substr(query, 0, 150) AS query, sum(calls) AS calls FROM pg_stat_monitor GROUP BY datname, query ORDER BY datname, query, calls DESC LIMIT 20; + datname | query | calls +---------+---------------------------------------------------------------------------------------------------------------+------- + example | INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP) | 10000 + example | SELECT abalance FROM pgbench_accounts WHERE aid = $1 | 10000 + example | SELECT relkind FROM pg_catalog.pg_class WHERE oid=$1::pg_catalog.regclass | 3 + example | UPDATE pgbench_accounts SET abalance = abalance + $1 WHERE aid = $2 | 10000 + example | UPDATE pgbench_branches SET bbalance = bbalance + $1 WHERE bid = $2 | 10000 + example | UPDATE pgbench_tellers SET tbalance = tbalance + $1 WHERE tid = $2 | 10000 + example | alter table pgbench_accounts add primary key (aid) | 1 + example | alter table pgbench_branches add primary key (bid) | 1 + example | alter table pgbench_tellers add primary key (tid) | 1 + example | begin | 10001 + example | commit | 10001 + example | copy pgbench_accounts from stdin with (freeze on) | 1 + example | copy pgbench_branches from stdin with (freeze on) | 1 + example | copy pgbench_tellers from stdin with (freeze on) | 1 + example | create table pgbench_accounts(aid int not null,bid int,abalance int,filler char(84)) with (fillfactor=100) | 1 + example | create table pgbench_branches(bid int not null,bbalance int,filler char(88)) with (fillfactor=100) | 1 + example | create table pgbench_history(tid int,bid int,aid int,delta int,mtime timestamp,filler char(22)) | 1 + example | create table pgbench_tellers(tid int not null,bid int,tbalance int,filler char(84)) with (fillfactor=100) | 1 + example | drop table if exists pgbench_accounts, pgbench_branches, pgbench_history, pgbench_tellers | 1 + example | select count(*) from pgbench_branches | 1 +(20 rows) + +SELECT pg_stat_monitor_reset(); + pg_stat_monitor_reset +----------------------- + +(1 row) + +SELECT name, setting, unit, context, vartype, source, min_val, max_val, enumvals, boot_val, reset_val, pending_restart FROM pg_settings WHERE name = 'pg_stat_monitor.pgsm_query_shared_buffer'; + name | setting | unit | context | vartype | source | min_val | max_val | enumvals | boot_val | reset_val | pending_restart +------------------------------------------+---------+------+------------+---------+--------------------+---------+---------+----------+----------+-----------+----------------- + pg_stat_monitor.pgsm_query_shared_buffer | 2 | MB | postmaster | integer | configuration file | 1 | 10000 | | 20 | 2 | f +(1 row) + +SELECT datname, substr(query, 0, 150) AS query, sum(calls) AS calls FROM pg_stat_monitor GROUP BY datname, query ORDER BY datname, query, calls DESC LIMIT 20; + datname | query | calls +---------+---------------------------------------------------------------------------------------------------------------+------- + example | INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP) | 10000 + example | SELECT abalance FROM pgbench_accounts WHERE aid = $1 | 10000 + example | SELECT relkind FROM pg_catalog.pg_class WHERE oid=$1::pg_catalog.regclass | 3 + example | UPDATE pgbench_accounts SET abalance = abalance + $1 WHERE aid = $2 | 10000 + example | UPDATE pgbench_branches SET bbalance = bbalance + $1 WHERE bid = $2 | 10000 + example | UPDATE pgbench_tellers SET tbalance = tbalance + $1 WHERE tid = $2 | 10000 + example | alter table pgbench_accounts add primary key (aid) | 1 + example | alter table pgbench_branches add primary key (bid) | 1 + example | alter table pgbench_tellers add primary key (tid) | 1 + example | begin | 10001 + example | commit | 10001 + example | copy pgbench_accounts from stdin with (freeze on) | 1 + example | copy pgbench_branches from stdin with (freeze on) | 1 + example | copy pgbench_tellers from stdin with (freeze on) | 1 + example | create table pgbench_accounts(aid int not null,bid int,abalance int,filler char(84)) with (fillfactor=100) | 1 + example | create table pgbench_branches(bid int not null,bbalance int,filler char(88)) with (fillfactor=100) | 1 + example | create table pgbench_history(tid int,bid int,aid int,delta int,mtime timestamp,filler char(22)) | 1 + example | create table pgbench_tellers(tid int not null,bid int,tbalance int,filler char(84)) with (fillfactor=100) | 1 + example | drop table if exists pgbench_accounts, pgbench_branches, pgbench_history, pgbench_tellers | 1 + example | select count(*) from pgbench_branches | 1 +(20 rows) + +SELECT pg_stat_monitor_reset(); + pg_stat_monitor_reset +----------------------- + +(1 row) + +SELECT name, setting, unit, context, vartype, source, min_val, max_val, enumvals, boot_val, reset_val, pending_restart FROM pg_settings WHERE name = 'pg_stat_monitor.pgsm_query_shared_buffer'; + name | setting | unit | context | vartype | source | min_val | max_val | enumvals | boot_val | reset_val | pending_restart +------------------------------------------+---------+------+------------+---------+--------------------+---------+---------+----------+----------+-----------+----------------- + pg_stat_monitor.pgsm_query_shared_buffer | 20 | MB | postmaster | integer | configuration file | 1 | 10000 | | 20 | 20 | f +(1 row) + +SELECT datname, substr(query, 0, 150) AS query, sum(calls) AS calls FROM pg_stat_monitor GROUP BY datname, query ORDER BY datname, query, calls DESC LIMIT 20; + datname | query | calls +---------+---------------------------------------------------------------------------------------------------------------+------- + example | INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP) | 10000 + example | SELECT abalance FROM pgbench_accounts WHERE aid = $1 | 10000 + example | SELECT relkind FROM pg_catalog.pg_class WHERE oid=$1::pg_catalog.regclass | 3 + example | UPDATE pgbench_accounts SET abalance = abalance + $1 WHERE aid = $2 | 10000 + example | UPDATE pgbench_branches SET bbalance = bbalance + $1 WHERE bid = $2 | 10000 + example | UPDATE pgbench_tellers SET tbalance = tbalance + $1 WHERE tid = $2 | 10000 + example | alter table pgbench_accounts add primary key (aid) | 1 + example | alter table pgbench_branches add primary key (bid) | 1 + example | alter table pgbench_tellers add primary key (tid) | 1 + example | begin | 10001 + example | commit | 10001 + example | copy pgbench_accounts from stdin with (freeze on) | 1 + example | copy pgbench_branches from stdin with (freeze on) | 1 + example | copy pgbench_tellers from stdin with (freeze on) | 1 + example | create table pgbench_accounts(aid int not null,bid int,abalance int,filler char(84)) with (fillfactor=100) | 1 + example | create table pgbench_branches(bid int not null,bbalance int,filler char(88)) with (fillfactor=100) | 1 + example | create table pgbench_history(tid int,bid int,aid int,delta int,mtime timestamp,filler char(22)) | 1 + example | create table pgbench_tellers(tid int not null,bid int,tbalance int,filler char(84)) with (fillfactor=100) | 1 + example | drop table if exists pgbench_accounts, pgbench_branches, pgbench_history, pgbench_tellers | 1 + example | select count(*) from pgbench_branches | 1 +(20 rows) + +SELECT pg_stat_monitor_reset(); + pg_stat_monitor_reset +----------------------- + +(1 row) + +SELECT name, setting, unit, context, vartype, source, min_val, max_val, enumvals, boot_val, reset_val, pending_restart FROM pg_settings WHERE name = 'pg_stat_monitor.pgsm_query_shared_buffer'; + name | setting | unit | context | vartype | source | min_val | max_val | enumvals | boot_val | reset_val | pending_restart +------------------------------------------+---------+------+------------+---------+--------------------+---------+---------+----------+----------+-----------+----------------- + pg_stat_monitor.pgsm_query_shared_buffer | 2048 | MB | postmaster | integer | configuration file | 1 | 10000 | | 20 | 2048 | f +(1 row) + +SELECT datname, substr(query, 0, 150) AS query, sum(calls) AS calls FROM pg_stat_monitor GROUP BY datname, query ORDER BY datname, query, calls DESC LIMIT 20; + datname | query | calls +---------+---------------------------------------------------------------------------------------------------------------+------- + example | INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP) | 10000 + example | SELECT abalance FROM pgbench_accounts WHERE aid = $1 | 10000 + example | SELECT relkind FROM pg_catalog.pg_class WHERE oid=$1::pg_catalog.regclass | 3 + example | UPDATE pgbench_accounts SET abalance = abalance + $1 WHERE aid = $2 | 10000 + example | UPDATE pgbench_branches SET bbalance = bbalance + $1 WHERE bid = $2 | 10000 + example | UPDATE pgbench_tellers SET tbalance = tbalance + $1 WHERE tid = $2 | 10000 + example | alter table pgbench_accounts add primary key (aid) | 1 + example | alter table pgbench_branches add primary key (bid) | 1 + example | alter table pgbench_tellers add primary key (tid) | 1 + example | begin | 10001 + example | commit | 10001 + example | copy pgbench_accounts from stdin with (freeze on) | 1 + example | copy pgbench_branches from stdin with (freeze on) | 1 + example | copy pgbench_tellers from stdin with (freeze on) | 1 + example | create table pgbench_accounts(aid int not null,bid int,abalance int,filler char(84)) with (fillfactor=100) | 1 + example | create table pgbench_branches(bid int not null,bbalance int,filler char(88)) with (fillfactor=100) | 1 + example | create table pgbench_history(tid int,bid int,aid int,delta int,mtime timestamp,filler char(22)) | 1 + example | create table pgbench_tellers(tid int not null,bid int,tbalance int,filler char(84)) with (fillfactor=100) | 1 + example | drop table if exists pgbench_accounts, pgbench_branches, pgbench_history, pgbench_tellers | 1 + example | select count(*) from pgbench_branches | 1 +(20 rows) + From 14aed53da544226a57c33efb56f33b1d1f3a6173 Mon Sep 17 00:00:00 2001 From: Artem Gavrilov Date: Thu, 30 Jul 2026 18:31:18 +0200 Subject: [PATCH 2/9] PG-2424 Add PostgreSQL 19 to ci (source only) There are no PostgreSQL 19 packages yet, so add this version only to source based workflows. --- .github/workflows/matrix.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/matrix.yml b/.github/workflows/matrix.yml index 03dd6600..e2b3b9d9 100644 --- a/.github/workflows/matrix.yml +++ b/.github/workflows/matrix.yml @@ -55,7 +55,7 @@ jobs: fail-fast: false matrix: pg_package: [source] - pg_version: [14, 15, 16, 17, 18] + pg_version: [14, 15, 16, 17, 18, 19] os: [ubuntu-24.04] compiler: [gcc, clang] build_type: [debugoptimized] @@ -74,7 +74,7 @@ jobs: fail-fast: false matrix: pg_package: [source] - pg_version: [14, 15, 16, 17, 18] + pg_version: [14, 15, 16, 17, 18, 19] os: [ubuntu-24.04] compiler: [gcc] build_type: [exec-backend] From 2d741587dc92c226dd58624f97f36a988e997be8 Mon Sep 17 00:00:00 2001 From: Artem Gavrilov Date: Thu, 30 Jul 2026 16:51:19 +0200 Subject: [PATCH 3/9] PG-2424 Add generic and custom plan counts pg_stat_statements introduces two new coutners in PostgreSQL 19: - generic_plan_calls - custom_plan_calls These counters track how many times a prepared statement was executed using a generic or custom plan. Backport them to pg_stat_monitor. --- pg_stat_monitor--2.3--next.sql | 201 +++++++++++++++++++++++++++++- regression/expected/functions.out | 3 +- src/hash_query.h | 2 + src/pg_stat_monitor.c | 62 +++++++-- t/018_column_names.pl | 18 +++ t/025_compare_pgss.pl | 15 +++ t/035_plancache.pl | 122 ++++++++++++++++++ 7 files changed, 410 insertions(+), 13 deletions(-) create mode 100644 t/035_plancache.pl diff --git a/pg_stat_monitor--2.3--next.sql b/pg_stat_monitor--2.3--next.sql index 65ed9c0a..638c0fca 100644 --- a/pg_stat_monitor--2.3--next.sql +++ b/pg_stat_monitor--2.3--next.sql @@ -53,11 +53,210 @@ DROP FUNCTION pgsm_create_view(); DROP FUNCTION pgsm_create_13_view(); DROP VIEW pg_stat_monitor; +DROP FUNCTION pg_stat_monitor_internal; + +CREATE FUNCTION pg_stat_monitor_internal( + IN showtext boolean, + OUT bucket int8, -- 0 + OUT userid oid, + OUT username text, + OUT dbid oid, + OUT datname text, + OUT client_ip int8, + + OUT queryid int8, -- 6 + OUT planid int8, + OUT query text, + OUT query_plan text, + OUT pgsm_query_id int8, + OUT top_queryid int8, + OUT top_query text, + OUT application_name text, + + OUT relations text, -- 14 + OUT cmd_type int, + OUT elevel int, + OUT sqlcode TEXT, + OUT message text, + OUT bucket_start_time timestamptz, + + OUT calls int8, -- 20 + + OUT total_exec_time float8, -- 21 + OUT min_exec_time float8, + OUT max_exec_time float8, + OUT mean_exec_time float8, + OUT stddev_exec_time float8, + + OUT rows int8, -- 26 + + OUT plans int8, -- 27 + + OUT total_plan_time float8, -- 28 + OUT min_plan_time float8, + OUT max_plan_time float8, + OUT mean_plan_time float8, + OUT stddev_plan_time float8, + + OUT shared_blks_hit int8, -- 33 + OUT shared_blks_read int8, + OUT shared_blks_dirtied int8, + OUT shared_blks_written int8, + OUT local_blks_hit int8, + OUT local_blks_read int8, + OUT local_blks_dirtied int8, + OUT local_blks_written int8, + OUT temp_blks_read int8, + OUT temp_blks_written int8, + OUT shared_blk_read_time float8, + OUT shared_blk_write_time float8, + OUT local_blk_read_time float8, + OUT local_blk_write_time float8, + OUT temp_blk_read_time float8, + OUT temp_blk_write_time float8, + + OUT resp_calls text, -- 49 + OUT cpu_user_time float8, + OUT cpu_sys_time float8, + OUT wal_records int8, + OUT wal_fpi int8, + OUT wal_bytes numeric, + OUT wal_buffers_full int8, + OUT comments TEXT, + + OUT jit_functions int8, -- 57 + OUT jit_generation_time float8, + OUT jit_inlining_count int8, + OUT jit_inlining_time float8, + OUT jit_optimization_count int8, + OUT jit_optimization_time float8, + OUT jit_emission_count int8, + OUT jit_emission_time float8, + OUT jit_deform_count int8, + OUT jit_deform_time float8, + + OUT parallel_workers_to_launch int, -- 67 + OUT parallel_workers_launched int, + + OUT generic_plan_calls int8, -- 69 + OUT custom_plan_calls int8, + + OUT stats_since timestamp with time zone, -- 71 + OUT minmax_stats_since timestamp with time zone, + + OUT toplevel BOOLEAN, -- 73 + OUT bucket_done BOOLEAN +) +RETURNS SETOF record +STRICT +PARALLEL SAFE +LANGUAGE c +AS 'MODULE_PATHNAME', 'pg_stat_monitor_NEXT'; + +CREATE FUNCTION pgsm_create_19_view() +RETURNS int +LANGUAGE plpgsql +AS $$ +BEGIN +CREATE VIEW pg_stat_monitor AS SELECT + bucket, + bucket_start_time, + userid, + username, + dbid, + datname, + '0.0.0.0'::inet + client_ip AS client_ip, + pgsm_query_id, + queryid, + toplevel, + top_queryid, + query, + comments, + planid, + query_plan, + top_query, + application_name, + string_to_array(relations, ',') AS relations, + cmd_type, + get_cmd_type(cmd_type) AS cmd_type_text, + elevel, + sqlcode, + message, + calls, + total_exec_time, + min_exec_time, + max_exec_time, + mean_exec_time, + stddev_exec_time, + rows, + shared_blks_hit, + shared_blks_read, + shared_blks_dirtied, + shared_blks_written, + local_blks_hit, + local_blks_read, + local_blks_dirtied, + local_blks_written, + temp_blks_read, + temp_blks_written, + shared_blk_read_time, + shared_blk_write_time, + local_blk_read_time, + local_blk_write_time, + temp_blk_read_time, + temp_blk_write_time, + + (string_to_array(resp_calls, ',')) resp_calls, + cpu_user_time, + cpu_sys_time, + wal_records, + wal_fpi, + wal_bytes, + wal_buffers_full, + bucket_done, + + plans, + total_plan_time, + min_plan_time, + max_plan_time, + mean_plan_time, + stddev_plan_time, + + jit_functions, + jit_generation_time, + jit_inlining_count, + jit_inlining_time, + jit_optimization_count, + jit_optimization_time, + jit_emission_count, + jit_emission_time, + jit_deform_count, + jit_deform_time, + + parallel_workers_to_launch, + parallel_workers_launched, + + generic_plan_calls, + custom_plan_calls, + + stats_since, + minmax_stats_since + +FROM pg_stat_monitor_internal(TRUE) +ORDER BY bucket_start_time; +RETURN 0; +END; +$$; + +REVOKE ALL ON FUNCTION pgsm_create_19_view FROM PUBLIC; + DO $$ DECLARE version int := current_setting('server_version_num'); BEGIN - IF version >= 180000 THEN + IF version >= 190000 THEN + PERFORM pgsm_create_19_view(); + ELSEIF version >= 180000 THEN PERFORM pgsm_create_18_view(); ELSEIF version >= 170000 THEN PERFORM pgsm_create_17_view(); diff --git a/regression/expected/functions.out b/regression/expected/functions.out index 332bbfb9..2e0aa625 100644 --- a/regression/expected/functions.out +++ b/regression/expected/functions.out @@ -24,8 +24,9 @@ SELECT routine_schema, routine_name, routine_type, data_type FROM information_sc public | pgsm_create_15_view | FUNCTION | integer public | pgsm_create_17_view | FUNCTION | integer public | pgsm_create_18_view | FUNCTION | integer + public | pgsm_create_19_view | FUNCTION | integer public | range | FUNCTION | ARRAY -(12 rows) +(13 rows) SET ROLE u1; SELECT routine_schema, routine_name, routine_type, data_type FROM information_schema.routines WHERE routine_schema = 'public' ORDER BY routine_name COLLATE "C"; diff --git a/src/hash_query.h b/src/hash_query.h index e6022456..87ec9e8a 100644 --- a/src/hash_query.h +++ b/src/hash_query.h @@ -171,6 +171,8 @@ typedef struct Counters * to be launched */ int64 parallel_workers_launched; /* # of parallel workers actually * launched */ + int64 generic_plan_calls; /* # of calls using a generic plan */ + int64 custom_plan_calls; /* # of calls using a custom plan */ } Counters; /* diff --git a/src/pg_stat_monitor.c b/src/pg_stat_monitor.c index 9e0feab7..2334ebbb 100644 --- a/src/pg_stat_monitor.c +++ b/src/pg_stat_monitor.c @@ -69,7 +69,8 @@ PG_MODULE_MAGIC; #define PG_STAT_MONITOR_COLS_V2_0 64 #define PG_STAT_MONITOR_COLS_V2_1 70 #define PG_STAT_MONITOR_COLS_V2_3 73 -#define PG_STAT_MONITOR_COLS PG_STAT_MONITOR_COLS_V2_3 /* maximum of above */ +#define PG_STAT_MONITOR_COLS_NEXT 75 +#define PG_STAT_MONITOR_COLS PG_STAT_MONITOR_COLS_NEXT /* maximum of above */ #define pgsm_enabled(level) \ (!IsParallelWorker() && \ @@ -87,6 +88,7 @@ typedef enum pgsmVersion PGSM_V2_0, PGSM_V2_1, PGSM_V2_3, + PGSM_NEXT, } pgsmVersion; /*---- Initialization Function Declarations ----*/ @@ -206,6 +208,7 @@ PG_FUNCTION_INFO_V1(pg_stat_monitor_1_0); PG_FUNCTION_INFO_V1(pg_stat_monitor_2_0); PG_FUNCTION_INFO_V1(pg_stat_monitor_2_1); PG_FUNCTION_INFO_V1(pg_stat_monitor_2_3); +PG_FUNCTION_INFO_V1(pg_stat_monitor_NEXT); PG_FUNCTION_INFO_V1(pg_stat_monitor); PG_FUNCTION_INFO_V1(get_histogram_timings); PG_FUNCTION_INFO_V1(pg_stat_monitor_hook_stats); @@ -276,7 +279,8 @@ static void pgsm_update_counters(Counters *counters, const WalUsage *walusage, const struct JitInstrumentation *jitusage, int parallel_workers_to_launch, - int parallel_workers_launched); + int parallel_workers_launched, + int plan_origin); static void pgsm_merge_counters(Counters *dst, const Counters *src); static void pgsm_store(const pgsmQueryStats *stats); @@ -769,12 +773,16 @@ pgsm_ExecutorEnd(QueryDesc *queryDesc) #endif #if PG_VERSION_NUM >= 180000 queryDesc->estate->es_parallel_workers_to_launch, /* parallel_workers_to_launch */ - queryDesc->estate->es_parallel_workers_launched); /* parallel_workers_launched */ + queryDesc->estate->es_parallel_workers_launched, /* parallel_workers_launched */ #else 0, /* parallel_workers_to_launch */ - 0); /* parallel_workers_launched */ + 0, /* parallel_workers_launched */ +#endif +#if PG_VERSION_NUM >= 190000 + queryDesc->plannedstmt->planOrigin); /* plan_origin */ +#else + 0); /* plan_origin */ #endif - pgsm_store(stats); @@ -966,7 +974,8 @@ pgsm_planner_hook(Query *parse, const char *query_string, int cursorOptions, Par &walusage, /* walusage */ NULL, /* jitusage */ 0, /* parallel_workers_to_launch */ - 0); /* parallel_workers_launched */ + 0, /* parallel_workers_launched */ + 0); /* plan_origin */ } else { @@ -1164,7 +1173,8 @@ pgsm_ProcessUtility(PlannedStmt *pstmt, const char *queryString, &walusage, /* walusage */ NULL, /* jitusage */ 0, /* parallel_workers_to_launch */ - 0); /* parallel_workers_launched */ + 0, /* parallel_workers_launched */ + 0); /* plan_origin */ pgsm_store(&stats); @@ -1324,7 +1334,8 @@ pgsm_update_counters(Counters *counters, const WalUsage *walusage, const struct JitInstrumentation *jitusage, int parallel_workers_to_launch, - int parallel_workers_launched) + int parallel_workers_launched, + int plan_origin) { /* * Only update the totals here, min/max/mean will be computed in @@ -1412,6 +1423,14 @@ pgsm_update_counters(Counters *counters, /* parallel worker counters */ counters->parallel_workers_to_launch += parallel_workers_to_launch; counters->parallel_workers_launched += parallel_workers_launched; + + /* cached plan origin counters (generic vs custom) */ +#if PG_VERSION_NUM >= 190000 + if (plan_origin == PLAN_STMT_CACHE_GENERIC) + counters->generic_plan_calls++; + else if (plan_origin == PLAN_STMT_CACHE_CUSTOM) + counters->custom_plan_calls++; +#endif } /* @@ -1525,6 +1544,10 @@ pgsm_merge_counters(Counters *dst, const Counters *src) /* parallel worker counters */ dst->parallel_workers_to_launch += src->parallel_workers_to_launch; dst->parallel_workers_launched += src->parallel_workers_launched; + + /* cached plan origin counters (generic vs custom) */ + dst->generic_plan_calls += src->generic_plan_calls; + dst->custom_plan_calls += src->custom_plan_calls; } static void @@ -1970,6 +1993,13 @@ pg_stat_monitor_2_3(PG_FUNCTION_ARGS) return (Datum) 0; } +Datum +pg_stat_monitor_NEXT(PG_FUNCTION_ARGS) +{ + pg_stat_monitor_internal(fcinfo, PGSM_NEXT, true); + return (Datum) 0; +} + /* * Legacy entry point for pg_stat_monitor() API versions 1.0 */ @@ -2027,6 +2057,9 @@ pg_stat_monitor_internal(FunctionCallInfo fcinfo, case PGSM_V2_3: expected_columns = PG_STAT_MONITOR_COLS_V2_3; break; + case PGSM_NEXT: + expected_columns = PG_STAT_MONITOR_COLS_NEXT; + break; default: ereport(ERROR, errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -2424,17 +2457,24 @@ pg_stat_monitor_internal(FunctionCallInfo fcinfo, values[i++] = Int64GetDatumFast(tmp.parallel_workers_launched); } - if (api_version >= PGSM_V2_1) + if (api_version >= PGSM_NEXT) { /* at column number 69 */ + values[i++] = Int64GetDatumFast(tmp.generic_plan_calls); + values[i++] = Int64GetDatumFast(tmp.custom_plan_calls); + } + + if (api_version >= PGSM_V2_1) + { + /* at column number 71 */ values[i++] = TimestampTzGetDatum(entry->stats_since); values[i++] = TimestampTzGetDatum(entry->minmax_stats_since); } - /* toplevel at column number 71 */ + /* toplevel at column number 73 */ values[i++] = BoolGetDatum(toplevel); - /* bucket_done at column number 72 */ + /* bucket_done at column number 74 */ values[i++] = BoolGetDatum(bucketid != current_bucket); /* clean up and return the tuplestore */ diff --git a/t/018_column_names.pl b/t/018_column_names.pl index 7946c2e9..7d42fea1 100644 --- a/t/018_column_names.pl +++ b/t/018_column_names.pl @@ -18,6 +18,24 @@ # Dictionary for expected PGSM columns names on different PG server versions my %pg_versions_pgsm_columns = ( + 19 => "application_name," + . "bucket,bucket_done,bucket_start_time,calls," + . "client_ip,cmd_type,cmd_type_text,comments,cpu_sys_time,cpu_user_time," + . "custom_plan_calls,datname,dbid,elevel,generic_plan_calls," + . "jit_deform_count,jit_deform_time," + . "jit_emission_count,jit_emission_time,jit_functions,jit_generation_time," + . "jit_inlining_count,jit_inlining_time,jit_optimization_count,jit_optimization_time," + . "local_blk_read_time,local_blk_write_time,local_blks_dirtied,local_blks_hit," + . "local_blks_read,local_blks_written,max_exec_time,max_plan_time,mean_exec_time," + . "mean_plan_time,message,min_exec_time,min_plan_time,minmax_stats_since," + . "parallel_workers_launched,parallel_workers_to_launch," + . "pgsm_query_id,planid,plans,query,query_plan,queryid,relations,resp_calls,rows," + . "shared_blk_read_time,shared_blk_write_time,shared_blks_dirtied," + . "shared_blks_hit,shared_blks_read,shared_blks_written,sqlcode,stats_since," + . "stddev_exec_time,stddev_plan_time,temp_blk_read_time,temp_blk_write_time," + . "temp_blks_read,temp_blks_written,top_query,top_queryid,toplevel," + . "total_exec_time,total_plan_time,userid,username,wal_buffers_full,wal_bytes," + . "wal_fpi,wal_records", 18 => "application_name," . "bucket,bucket_done,bucket_start_time,calls," . "client_ip,cmd_type,cmd_type_text,comments,cpu_sys_time,cpu_user_time," diff --git a/t/025_compare_pgss.pl b/t/025_compare_pgss.pl index 49b01d63..ce5eb21f 100644 --- a/t/025_compare_pgss.pl +++ b/t/025_compare_pgss.pl @@ -218,6 +218,21 @@ is($stdout, 't', "Compare: parallel_workers_launched are equal."); } +if ($PGSM::PG_MAJOR_VERSION >= 19) +{ + ($cmdret, $stdout, $stderr) = $node->psql('postgres', + 'SELECT sum(pgsm.generic_plan_calls) = sum(pgss.generic_plan_calls) FROM pg_stat_monitor AS pgsm INNER JOIN pg_stat_statements AS pgss ON pgss.query = pgsm.query WHERE pgsm.query LIKE \'%DELETE FROM pgbench_accounts%\' GROUP BY pgsm.query;' + ); + trim($stdout); + is($stdout, 't', "Compare: generic_plan_calls are equal."); + + ($cmdret, $stdout, $stderr) = $node->psql('postgres', + 'SELECT sum(pgsm.custom_plan_calls) = sum(pgss.custom_plan_calls) FROM pg_stat_monitor AS pgsm INNER JOIN pg_stat_statements AS pgss ON pgss.query = pgsm.query WHERE pgsm.query LIKE \'%DELETE FROM pgbench_accounts%\' GROUP BY pgsm.query;' + ); + trim($stdout); + is($stdout, 't', "Compare: custom_plan_calls are equal."); +} + # Compare values for query 'INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP)' ($cmdret, $stdout, $stderr) = $node->psql('postgres', 'SELECT sum(pgsm.calls) = sum(pgss.calls) FROM pg_stat_monitor AS pgsm INNER JOIN pg_stat_statements AS pgss ON pgss.query = pgsm.query WHERE pgsm.query LIKE \'%INSERT INTO pgbench_history%\' GROUP BY pgsm.query;' diff --git a/t/035_plancache.pl b/t/035_plancache.pl new file mode 100644 index 00000000..3118554d --- /dev/null +++ b/t/035_plancache.pl @@ -0,0 +1,122 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use File::Basename; +use Text::Trim qw(trim); +use Test::More; +use lib 't'; +use pgsm; + +# Get filename and create out file name and dirs where requried +PGSM::setup_files_dir(basename($0)); + +if ($PGSM::PG_MAJOR_VERSION < 19) +{ + plan skip_all => + "generic_plan_calls/custom_plan_calls require PostgreSQL 19 or later"; +} + +# Create new PostgreSQL node and do initdb +my $node = PGSM->pgsm_init_pg(); + +$node->append_conf( + 'postgresql.conf', qq( +shared_preload_libraries = 'pg_stat_statements, pg_stat_monitor' +# Set bucket duration to 36000 seconds so bucket doesn't change. +pg_stat_monitor.pgsm_bucket_time = 36000 +pg_stat_monitor.pgsm_normalized_query = on +)); + +# Start server +$node->start; + +# Create extensions +my ($cmdret, $stdout, $stderr) = $node->psql( + 'postgres', + 'CREATE EXTENSION pg_stat_statements; CREATE EXTENSION pg_stat_monitor;', + extra_params => ['-a']); +is($cmdret, 0, "CREATE EXTENSIONS"); +PGSM::append_to_file($stdout); + +# ---------------------------------------------------------------------------- +# Prepared statements, simple query protocol. +# +# One execution under a forced generic plan and one under a forced custom plan +# must be recorded as exactly one generic_plan_calls and one custom_plan_calls +# for the prepared statement. +# ---------------------------------------------------------------------------- +($cmdret, $stdout, $stderr) = $node->psql( + 'postgres', qq( +SELECT pg_stat_monitor_reset(); +SELECT pg_stat_statements_reset(); +PREPARE p1 AS SELECT \$1 AS a; +SET plan_cache_mode TO force_generic_plan; +EXECUTE p1(1); +SET plan_cache_mode TO force_custom_plan; +EXECUTE p1(1); +DEALLOCATE p1; +), extra_params => ['-a']); +is($cmdret, 0, "Simple protocol: run prepared statement workload"); +PGSM::append_to_file($stdout); + +# Sanity: pg_stat_statements recorded the expected counts. +($cmdret, $stdout, $stderr) = $node->psql('postgres', + 'SELECT coalesce(sum(generic_plan_calls), 0) = 1 AND coalesce(sum(custom_plan_calls), 0) = 1 FROM pg_stat_statements WHERE query LIKE \'%$1 AS a%\';' +); +trim($stdout); +is($stdout, 't', + "Simple protocol: pg_stat_statements recorded 1 generic and 1 custom plan call" +); + +# pg_stat_monitor must record the same counts. +($cmdret, $stdout, $stderr) = $node->psql('postgres', + 'SELECT coalesce(sum(generic_plan_calls), 0) = 1 AND coalesce(sum(custom_plan_calls), 0) = 1 FROM pg_stat_monitor WHERE query LIKE \'%$1 AS a%\';' +); +trim($stdout); +is($stdout, 't', + "Simple protocol: pg_stat_monitor recorded 1 generic and 1 custom plan call" +); + +# ---------------------------------------------------------------------------- +# Prepared statements, extended query protocol (\parse / \bind_named). +# ---------------------------------------------------------------------------- +($cmdret, $stdout, $stderr) = $node->psql( + 'postgres', qq( +SELECT pg_stat_monitor_reset(); +SELECT pg_stat_statements_reset(); +SELECT \$1 AS a \\parse p1 +SET plan_cache_mode TO force_generic_plan; +\\bind_named p1 1 +; +SET plan_cache_mode TO force_custom_plan; +\\bind_named p1 1 +; +\\close_prepared p1 +), extra_params => ['-a']); +is($cmdret, 0, "Extended protocol: run prepared statement workload"); +PGSM::append_to_file($stdout); + +# Sanity: pg_stat_statements recorded the expected counts. +($cmdret, $stdout, $stderr) = $node->psql('postgres', + 'SELECT coalesce(sum(generic_plan_calls), 0) = 1 AND coalesce(sum(custom_plan_calls), 0) = 1 FROM pg_stat_statements WHERE query LIKE \'%$1 AS a%\';' +); +trim($stdout); +is($stdout, 't', + "Extended protocol: pg_stat_statements recorded 1 generic and 1 custom plan call" +); + +# pg_stat_monitor must record the same counts. +($cmdret, $stdout, $stderr) = $node->psql('postgres', + 'SELECT coalesce(sum(generic_plan_calls), 0) = 1 AND coalesce(sum(custom_plan_calls), 0) = 1 FROM pg_stat_monitor WHERE query LIKE \'%$1 AS a%\';' +); +trim($stdout); +is($stdout, 't', + "Extended protocol: pg_stat_monitor recorded 1 generic and 1 custom plan call" +); + +# Stop the server +$node->stop; + +# Done testing for this testcase file. +done_testing(); From 364f3e99649c815ed0e417bc89f0f247ac3e0688 Mon Sep 17 00:00:00 2001 From: Artem Gavrilov Date: Fri, 31 Jul 2026 17:33:30 +0200 Subject: [PATCH 4/9] PG-2424 Use ComputeConstantLengths function for PostgreSQL 19 PostgreSQL 19 exposes ComputeConstantLengths function, so there is no more reasons to compute constant lenght with our own implemenation. --- src/pg_stat_monitor.c | 59 ++++++++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 17 deletions(-) diff --git a/src/pg_stat_monitor.c b/src/pg_stat_monitor.c index 2334ebbb..11d382c2 100644 --- a/src/pg_stat_monitor.c +++ b/src/pg_stat_monitor.c @@ -290,8 +290,11 @@ static void pg_stat_monitor_internal(FunctionCallInfo fcinfo, static char *generate_normalized_query(const JumbleState *jstate, const char *query, int query_loc, int *query_len_p); -static void fill_in_constant_lengths(const JumbleState *jstate, const char *query, int query_loc); +#if PG_VERSION_NUM < 190000 +static LocationLen *ComputeConstantLengths(const JumbleState *jstate, + const char *query, int query_loc); static int comp_location(const void *a, const void *b); +#endif static uint64 get_next_wbucket(pgsmSharedState *pgsm); @@ -2751,15 +2754,17 @@ generate_normalized_query(const JumbleState *jstate, const char *query, n_quer_loc = 0, /* Normalized query byte location */ last_off = 0, /* Offset from start for previous tok */ last_tok_len = 0; /* Length (in bytes) of that tok */ + LocationLen *locs; #if PG_VERSION_NUM >= 180000 int num_constants_replaced = 0; #endif /* - * Get constants' lengths (core system only gives us locations). Note - * this also ensures the items are sorted by location. + * Determine constants' lengths (core system only gives us locations), + * and return a sorted copy of jstate's LocationLen data with lengths + * filled in. */ - fill_in_constant_lengths(jstate, query, query_loc); + locs = ComputeConstantLengths(jstate, query, query_loc); /* * Allow for $n symbols to be longer than the constants they replace. @@ -2787,15 +2792,15 @@ generate_normalized_query(const JumbleState *jstate, const char *query, * the parameter in the next iteration (or after the loop is done), * which is a bit odd but seems to work okay in most cases. */ - if (jstate->clocations[i].extern_param && !jstate->has_squashed_lists) + if (locs[i].extern_param && !jstate->has_squashed_lists) continue; #endif - off = jstate->clocations[i].location; + off = locs[i].location; /* Adjust recorded location if we're dealing with partial string */ off -= query_loc; - tok_len = jstate->clocations[i].length; + tok_len = locs[i].length; if (tok_len < 0) continue; /* ignore any duplicates */ @@ -2817,7 +2822,7 @@ generate_normalized_query(const JumbleState *jstate, const char *query, */ n_quer_loc += sprintf(norm_query + n_quer_loc, "$%d%s", num_constants_replaced + 1 + jstate->highest_extern_param_id, - jstate->clocations[i].squashed ? " /*, ... */" : ""); + locs[i].squashed ? " /*, ... */" : ""); num_constants_replaced++; #else /* And insert a param symbol in place of the constant token */ @@ -2831,6 +2836,10 @@ generate_normalized_query(const JumbleState *jstate, const char *query, last_tok_len = tok_len; } + /* Clean up, if needed */ + if (locs) + pfree(locs); + /* * We've copied up until the last ignorable constant. Copy over the * remaining bytes of the original query string. @@ -2845,10 +2854,15 @@ generate_normalized_query(const JumbleState *jstate, const char *query, norm_query[n_quer_loc] = '\0'; *query_len_p = n_quer_loc; + return norm_query; } +#if PG_VERSION_NUM < 190000 + /* + * Compatibility version of ComputeConstantLengths for Postgres < 19. + * * Given a valid SQL string and an array of constant-location records, * fill in the textual lengths of those constants. * @@ -2874,10 +2888,13 @@ generate_normalized_query(const JumbleState *jstate, const char *query, * N.B. There is an assumption that a '-' character at a Const location begins * a negative numeric constant. This precludes there ever being another * reason for a constant to start with a '-'. + * + * Returns a sorted copy of jstate's LocationLen data with lengths filled in. + * The caller is responsible for pfree'ing the result. */ -static void -fill_in_constant_lengths(const JumbleState *jstate, const char *query, - int query_loc) +static LocationLen * +ComputeConstantLengths(const JumbleState *jstate, const char *query, + int query_loc) { LocationLen *locs; core_yyscan_t yyscanner; @@ -2885,16 +2902,22 @@ fill_in_constant_lengths(const JumbleState *jstate, const char *query, core_YYSTYPE yylval; YYLTYPE yylloc; int last_loc = -1; - int i; + + if (jstate->clocations_count == 0) + return NULL; + + /* Copy constant locations to avoid modifying jstate */ + locs = palloc(sizeof(LocationLen) * jstate->clocations_count); + memcpy(locs, jstate->clocations, + jstate->clocations_count * sizeof(LocationLen)); /* * Sort the records by location so that we can process them in order while * scanning the query text. */ if (jstate->clocations_count > 1) - qsort(jstate->clocations, jstate->clocations_count, + qsort(locs, jstate->clocations_count, sizeof(LocationLen), comp_location); - locs = jstate->clocations; /* initialize the flex scanner --- should match raw_parser() */ yyscanner = scanner_init(query, @@ -2902,13 +2925,11 @@ fill_in_constant_lengths(const JumbleState *jstate, const char *query, &ScanKeywords, ScanKeywordTokens); -#if PG_VERSION_NUM < 190000 /* we don't want to re-emit any escape string warnings */ yyextra.escape_string_warning = false; -#endif /* Search for each constant, in sequence */ - for (i = 0; i < jstate->clocations_count; i++) + for (int i = 0; i < jstate->clocations_count; i++) { int loc = locs[i].location; int tok; @@ -2977,6 +2998,8 @@ fill_in_constant_lengths(const JumbleState *jstate, const char *query, } scanner_finish(yyscanner); + + return locs; } /* @@ -2996,6 +3019,8 @@ comp_location(const void *a, const void *b) return 0; } +#endif /* PG_VERSION_NUM < 190000 */ + /* Convert array of integers into Text datum */ static Datum intarray_get_datum(const int32 *arr, int len) From 1d5bf646a3bae63684f6ef24d2d641188f286a86 Mon Sep 17 00:00:00 2001 From: Artem Gavrilov Date: Fri, 31 Jul 2026 18:16:35 +0200 Subject: [PATCH 5/9] PG-2424 Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cc113fb..da029723 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Our name and version are now in `pg_get_loaded_modules()` for PostgreSQL 18+ - Support for `EXEC_BACKEND` builds ([PG-2547](https://perconadev.atlassian.net/browse/PG-2547)) +- Add PostgreSQL 19 support ([PG-2424](https://perconadev.atlassian.net/browse/PG-2424)): add generic and custom plan counts, support property graphs, use ComputeConstantLengths API for constants squashing ### Changed From 0b5c757d441d648ac326e7c303cfef2f4244a13a Mon Sep 17 00:00:00 2001 From: Artem Gavrilov Date: Tue, 4 Aug 2026 15:24:54 +0200 Subject: [PATCH 6/9] PG-2424 Backport test updates from PG19 In PostgreSQL 19 some pg_stat_statements tests got updates. Backport them to pg_stat_monitor. --- regression/expected/level_tracking.out | 71 ++++++++++++++++++ regression/expected/level_tracking_1.out | 71 ++++++++++++++++++ regression/expected/level_tracking_2.out | 71 ++++++++++++++++++ regression/expected/squashing_1.out | 95 ++++++++++++++++++++++++ regression/sql/level_tracking.sql | 44 +++++++++++ regression/sql/squashing.sql | 30 ++++++++ 6 files changed, 382 insertions(+) diff --git a/regression/expected/level_tracking.out b/regression/expected/level_tracking.out index bbb8015e..ad761007 100644 --- a/regression/expected/level_tracking.out +++ b/regression/expected/level_tracking.out @@ -296,6 +296,77 @@ SELECT toplevel, calls, rows, query FROM pg_stat_monitor ORDER BY query COLLATE t | 2 | 2 | SELECT plus_two($1) (8 rows) +SELECT pg_stat_monitor_reset(); + pg_stat_monitor_reset +----------------------- + +(1 row) + +-- planner - all-level tracking. +SET pg_stat_monitor.pgsm_track_planning = on; +-- Release all cached plans before the first function call. This matters +-- when debug_discard_caches is enabled, which would store a normalized +-- version of the inner query of the function. Forcing a plan rebuild +-- ensures that a normalized version is always stored with the stats entry, +-- while checking that the nesting level is computed correctly in the +-- planner hook. +DISCARD PLANS; +SELECT plus_three(8); + plus_three +------------ + 11 +(1 row) + +SELECT plus_three(10); + plus_three +------------ + 13 +(1 row) + +SELECT toplevel, calls, rows, plans, query FROM pg_stat_monitor + ORDER BY query COLLATE "C"; + toplevel | calls | rows | plans | query +----------+-------+------+-------+-------------------------------- + f | 2 | 2 | 2 | SELECT i + $2 LIMIT $3 + t | 1 | 1 | 1 | SELECT pg_stat_monitor_reset() + t | 2 | 2 | 2 | SELECT plus_three($1) +(3 rows) + +RESET pg_stat_monitor.pgsm_track_planning; +-- AFTER trigger SQL (ExecutorFinish) - all-level tracking. +SET pg_stat_monitor.pgsm_track = 'all'; +SELECT pg_stat_monitor_reset(); + pg_stat_monitor_reset +----------------------- + +(1 row) + +CREATE TABLE test_trigger (id int, name text); +CREATE TABLE audit_table (table_name text, action text, row_id int); +CREATE OR REPLACE FUNCTION audit_trigger_func() +RETURNS TRIGGER AS $$ +BEGIN + INSERT INTO audit_table VALUES ('test_trigger', TG_OP, NEW.id); + RETURN NULL; +END; +$$ LANGUAGE plpgsql; +CREATE TRIGGER audit_after_trigger + AFTER INSERT ON test_trigger + FOR EACH ROW EXECUTE FUNCTION audit_trigger_func(); +INSERT INTO test_trigger VALUES (1, 'test1'); +INSERT INTO test_trigger VALUES (2, 'test2'); +SELECT toplevel, calls, rows, plans, query FROM pg_stat_monitor + ORDER BY query COLLATE "C"; + toplevel | calls | rows | plans | query +----------+-------+------+-------+----------------------------------------------------- + f | 2 | 2 | 2 | INSERT INTO audit_table VALUES ($15, TG_OP, NEW.id) + t | 2 | 2 | 2 | INSERT INTO test_trigger VALUES ($1, $2) + t | 1 | 1 | 1 | SELECT pg_stat_monitor_reset() +(3 rows) + +DROP TRIGGER audit_after_trigger ON test_trigger; +DROP FUNCTION audit_trigger_func(); +DROP TABLE audit_table, test_trigger; -- -- pg_stat_monitor.pgsm_track = none -- diff --git a/regression/expected/level_tracking_1.out b/regression/expected/level_tracking_1.out index 3de5eb9a..5abfde21 100644 --- a/regression/expected/level_tracking_1.out +++ b/regression/expected/level_tracking_1.out @@ -295,6 +295,77 @@ SELECT toplevel, calls, rows, query FROM pg_stat_monitor ORDER BY query COLLATE t | 2 | 2 | SELECT plus_two($1) (8 rows) +SELECT pg_stat_monitor_reset(); + pg_stat_monitor_reset +----------------------- + +(1 row) + +-- planner - all-level tracking. +SET pg_stat_monitor.pgsm_track_planning = on; +-- Release all cached plans before the first function call. This matters +-- when debug_discard_caches is enabled, which would store a normalized +-- version of the inner query of the function. Forcing a plan rebuild +-- ensures that a normalized version is always stored with the stats entry, +-- while checking that the nesting level is computed correctly in the +-- planner hook. +DISCARD PLANS; +SELECT plus_three(8); + plus_three +------------ + 11 +(1 row) + +SELECT plus_three(10); + plus_three +------------ + 13 +(1 row) + +SELECT toplevel, calls, rows, plans, query FROM pg_stat_monitor + ORDER BY query COLLATE "C"; + toplevel | calls | rows | plans | query +----------+-------+------+-------+-------------------------------- + f | 2 | 2 | 2 | SELECT i + $2 LIMIT $3 + t | 1 | 1 | 1 | SELECT pg_stat_monitor_reset() + t | 2 | 2 | 2 | SELECT plus_three($1) +(3 rows) + +RESET pg_stat_monitor.pgsm_track_planning; +-- AFTER trigger SQL (ExecutorFinish) - all-level tracking. +SET pg_stat_monitor.pgsm_track = 'all'; +SELECT pg_stat_monitor_reset(); + pg_stat_monitor_reset +----------------------- + +(1 row) + +CREATE TABLE test_trigger (id int, name text); +CREATE TABLE audit_table (table_name text, action text, row_id int); +CREATE OR REPLACE FUNCTION audit_trigger_func() +RETURNS TRIGGER AS $$ +BEGIN + INSERT INTO audit_table VALUES ('test_trigger', TG_OP, NEW.id); + RETURN NULL; +END; +$$ LANGUAGE plpgsql; +CREATE TRIGGER audit_after_trigger + AFTER INSERT ON test_trigger + FOR EACH ROW EXECUTE FUNCTION audit_trigger_func(); +INSERT INTO test_trigger VALUES (1, 'test1'); +INSERT INTO test_trigger VALUES (2, 'test2'); +SELECT toplevel, calls, rows, plans, query FROM pg_stat_monitor + ORDER BY query COLLATE "C"; + toplevel | calls | rows | plans | query +----------+-------+------+-------+----------------------------------------------------- + f | 2 | 2 | 2 | INSERT INTO audit_table VALUES ($15, TG_OP, NEW.id) + t | 2 | 2 | 2 | INSERT INTO test_trigger VALUES ($1, $2) + t | 1 | 1 | 1 | SELECT pg_stat_monitor_reset() +(3 rows) + +DROP TRIGGER audit_after_trigger ON test_trigger; +DROP FUNCTION audit_trigger_func(); +DROP TABLE audit_table, test_trigger; -- -- pg_stat_monitor.pgsm_track = none -- diff --git a/regression/expected/level_tracking_2.out b/regression/expected/level_tracking_2.out index 077766eb..a28091f0 100644 --- a/regression/expected/level_tracking_2.out +++ b/regression/expected/level_tracking_2.out @@ -293,6 +293,77 @@ SELECT toplevel, calls, rows, query FROM pg_stat_monitor ORDER BY query COLLATE t | 2 | 2 | SELECT plus_two($1) (7 rows) +SELECT pg_stat_monitor_reset(); + pg_stat_monitor_reset +----------------------- + +(1 row) + +-- planner - all-level tracking. +SET pg_stat_monitor.pgsm_track_planning = on; +-- Release all cached plans before the first function call. This matters +-- when debug_discard_caches is enabled, which would store a normalized +-- version of the inner query of the function. Forcing a plan rebuild +-- ensures that a normalized version is always stored with the stats entry, +-- while checking that the nesting level is computed correctly in the +-- planner hook. +DISCARD PLANS; +SELECT plus_three(8); + plus_three +------------ + 11 +(1 row) + +SELECT plus_three(10); + plus_three +------------ + 13 +(1 row) + +SELECT toplevel, calls, rows, plans, query FROM pg_stat_monitor + ORDER BY query COLLATE "C"; + toplevel | calls | rows | plans | query +----------+-------+------+-------+-------------------------------- + f | 2 | 2 | 2 | SELECT i + $2 LIMIT $3 + t | 1 | 1 | 1 | SELECT pg_stat_monitor_reset() + t | 2 | 2 | 2 | SELECT plus_three($1) +(3 rows) + +RESET pg_stat_monitor.pgsm_track_planning; +-- AFTER trigger SQL (ExecutorFinish) - all-level tracking. +SET pg_stat_monitor.pgsm_track = 'all'; +SELECT pg_stat_monitor_reset(); + pg_stat_monitor_reset +----------------------- + +(1 row) + +CREATE TABLE test_trigger (id int, name text); +CREATE TABLE audit_table (table_name text, action text, row_id int); +CREATE OR REPLACE FUNCTION audit_trigger_func() +RETURNS TRIGGER AS $$ +BEGIN + INSERT INTO audit_table VALUES ('test_trigger', TG_OP, NEW.id); + RETURN NULL; +END; +$$ LANGUAGE plpgsql; +CREATE TRIGGER audit_after_trigger + AFTER INSERT ON test_trigger + FOR EACH ROW EXECUTE FUNCTION audit_trigger_func(); +INSERT INTO test_trigger VALUES (1, 'test1'); +INSERT INTO test_trigger VALUES (2, 'test2'); +SELECT toplevel, calls, rows, plans, query FROM pg_stat_monitor + ORDER BY query COLLATE "C"; + toplevel | calls | rows | plans | query +----------+-------+------+-------+----------------------------------------------------- + f | 2 | 2 | 2 | INSERT INTO audit_table VALUES ($15, TG_OP, NEW.id) + t | 2 | 2 | 2 | INSERT INTO test_trigger VALUES ($1, $2) + t | 1 | 1 | 1 | SELECT pg_stat_monitor_reset() +(3 rows) + +DROP TRIGGER audit_after_trigger ON test_trigger; +DROP FUNCTION audit_trigger_func(); +DROP TABLE audit_table, test_trigger; -- -- pg_stat_monitor.pgsm_track = none -- diff --git a/regression/expected/squashing_1.out b/regression/expected/squashing_1.out index cf5777a1..56cbe224 100644 --- a/regression/expected/squashing_1.out +++ b/regression/expected/squashing_1.out @@ -775,6 +775,99 @@ SELECT query, calls FROM pg_stat_monitor ORDER BY query COLLATE "C"; SELECT pg_stat_monitor_reset() IS NOT NULL AS t | 1 (2 rows) +-- composite function with row expansion +create table test_composite(x integer); +CREATE FUNCTION composite_f(a integer[], out x integer, out y integer) returns +record as $$ begin + x = a[1]; + y = a[2]; + end; +$$ language plpgsql; +SELECT pg_stat_monitor_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +SELECT ((composite_f(array[1, 2]))).* FROM test_composite; + x | y +---+--- +(0 rows) + +SELECT ((composite_f(array[1, 2, 3]))).* FROM test_composite; + x | y +---+--- +(0 rows) + +SELECT ((composite_f(array[1, 2, 3]))).*, 1, 2, 3, ((composite_f(array[1, 2, 3]))).*, 1, 2 +FROM test_composite +WHERE x IN (1, 2, 3); + x | y | ?column? | ?column? | ?column? | x | y | ?column? | ?column? +---+---+----------+----------+----------+---+---+----------+---------- +(0 rows) + +-- The \bind case from upstream is omitted here, see: https://perconadev.atlassian.net/browse/PG-1936 +-- ROW() expression with row expansion +SELECT (ROW(ARRAY[1,2])).*; + f1 +------- + {1,2} +(1 row) + +SELECT (ROW(ARRAY[1, 2], ARRAY[1, 2, 3])).*; + f1 | f2 +-------+--------- + {1,2} | {1,2,3} +(1 row) + +SELECT 1, 2, (ROW(ARRAY[1, 2], ARRAY[1, 2, 3])).*, 3, 4; + ?column? | ?column? | f1 | f2 | ?column? | ?column? +----------+----------+-------+---------+----------+---------- + 1 | 2 | {1,2} | {1,2,3} | 3 | 4 +(1 row) + +-- The \bind case from upstream is omitted here, see: https://perconadev.atlassian.net/browse/PG-1936 +SELECT query, calls FROM pg_stat_monitor ORDER BY query COLLATE "C"; + query | calls +-------------------------------------------------------------------------------------------------------------+------- + SELECT $1, $2, (ROW(ARRAY[$3 /*, ... */], ARRAY[$4 /*, ... */])).*, $5, $6 | 1 + SELECT ((composite_f(array[$1 /*, ... */]))).* FROM test_composite | 2 + SELECT ((composite_f(array[$1 /*, ... */]))).*, $2, $3, $4, ((composite_f(array[$5 /*, ... */]))).*, $6, $7+| 1 + FROM test_composite +| + WHERE x IN ($8 /*, ... */) | + SELECT (ROW(ARRAY[$1 /*, ... */])).* | 1 + SELECT (ROW(ARRAY[$1 /*, ... */], ARRAY[$2 /*, ... */])).* | 1 + SELECT pg_stat_monitor_reset() IS NOT NULL AS t | 1 +(6 rows) + +-- IN and ANY clauses with Vars are not squashed. +SELECT * FROM test_squash a, test_squash b WHERE a.id IN (1, 2, 3, b.id, b.id + 1); + id | data | id | data +----+------+----+------ +(0 rows) + +SELECT * FROM test_squash a, test_squash b WHERE a.id = ANY (array[1, ((b.id + b.id * 2)), 5]); + id | data | id | data +----+------+----+------ +(0 rows) + +-- The \bind case from upstream is omitted here, see: https://perconadev.atlassian.net/browse/PG-1936 +SELECT query, calls FROM pg_stat_monitor ORDER BY query COLLATE "C"; + query | calls +-------------------------------------------------------------------------------------------------------------+------- + SELECT $1, $2, (ROW(ARRAY[$3 /*, ... */], ARRAY[$4 /*, ... */])).*, $5, $6 | 1 + SELECT ((composite_f(array[$1 /*, ... */]))).* FROM test_composite | 2 + SELECT ((composite_f(array[$1 /*, ... */]))).*, $2, $3, $4, ((composite_f(array[$5 /*, ... */]))).*, $6, $7+| 1 + FROM test_composite +| + WHERE x IN ($8 /*, ... */) | + SELECT (ROW(ARRAY[$1 /*, ... */])).* | 1 + SELECT (ROW(ARRAY[$1 /*, ... */], ARRAY[$2 /*, ... */])).* | 1 + SELECT * FROM test_squash a, test_squash b WHERE a.id = ANY (array[$1, ((b.id + b.id * $2)), $3]) | 1 + SELECT * FROM test_squash a, test_squash b WHERE a.id IN ($1, $2, $3, b.id, b.id + $4) | 1 + SELECT pg_stat_monitor_reset() IS NOT NULL AS t | 1 + SELECT query, calls FROM pg_stat_monitor ORDER BY query COLLATE "C" | 1 +(9 rows) + -- -- cleanup -- @@ -784,6 +877,8 @@ DROP TABLE test_squash_numeric; DROP TABLE test_squash_bigint; DROP TABLE test_squash_cast CASCADE; DROP TABLE test_squash_jsonb; +DROP TABLE test_composite; +DROP FUNCTION composite_f; SELECT pg_stat_monitor_reset(); pg_stat_monitor_reset ----------------------- diff --git a/regression/sql/level_tracking.sql b/regression/sql/level_tracking.sql index 3c56e539..0d4d51eb 100644 --- a/regression/sql/level_tracking.sql +++ b/regression/sql/level_tracking.sql @@ -147,6 +147,50 @@ SELECT plus_three(8); SELECT plus_three(10); SELECT toplevel, calls, rows, query FROM pg_stat_monitor ORDER BY query COLLATE "C"; +SELECT pg_stat_monitor_reset(); + +-- planner - all-level tracking. +SET pg_stat_monitor.pgsm_track_planning = on; +-- Release all cached plans before the first function call. This matters +-- when debug_discard_caches is enabled, which would store a normalized +-- version of the inner query of the function. Forcing a plan rebuild +-- ensures that a normalized version is always stored with the stats entry, +-- while checking that the nesting level is computed correctly in the +-- planner hook. +DISCARD PLANS; +SELECT plus_three(8); +SELECT plus_three(10); + +SELECT toplevel, calls, rows, plans, query FROM pg_stat_monitor + ORDER BY query COLLATE "C"; +RESET pg_stat_monitor.pgsm_track_planning; + +-- AFTER trigger SQL (ExecutorFinish) - all-level tracking. +SET pg_stat_monitor.pgsm_track = 'all'; +SELECT pg_stat_monitor_reset(); + +CREATE TABLE test_trigger (id int, name text); +CREATE TABLE audit_table (table_name text, action text, row_id int); +CREATE OR REPLACE FUNCTION audit_trigger_func() +RETURNS TRIGGER AS $$ +BEGIN + INSERT INTO audit_table VALUES ('test_trigger', TG_OP, NEW.id); + RETURN NULL; +END; +$$ LANGUAGE plpgsql; +CREATE TRIGGER audit_after_trigger + AFTER INSERT ON test_trigger + FOR EACH ROW EXECUTE FUNCTION audit_trigger_func(); + +INSERT INTO test_trigger VALUES (1, 'test1'); +INSERT INTO test_trigger VALUES (2, 'test2'); + +SELECT toplevel, calls, rows, plans, query FROM pg_stat_monitor + ORDER BY query COLLATE "C"; + +DROP TRIGGER audit_after_trigger ON test_trigger; +DROP FUNCTION audit_trigger_func(); +DROP TABLE audit_table, test_trigger; -- -- pg_stat_monitor.pgsm_track = none diff --git a/regression/sql/squashing.sql b/regression/sql/squashing.sql index 8230942f..69598e27 100644 --- a/regression/sql/squashing.sql +++ b/regression/sql/squashing.sql @@ -300,6 +300,34 @@ SELECT WHERE '1' IN ('1'::int::text, '2'::int::text); SELECT WHERE '1' = ANY(array['1'::int::text, '2'::int::text]); SELECT query, calls FROM pg_stat_monitor ORDER BY query COLLATE "C"; +-- composite function with row expansion +create table test_composite(x integer); +CREATE FUNCTION composite_f(a integer[], out x integer, out y integer) returns +record as $$ begin + x = a[1]; + y = a[2]; + end; +$$ language plpgsql; +SELECT pg_stat_monitor_reset() IS NOT NULL AS t; +SELECT ((composite_f(array[1, 2]))).* FROM test_composite; +SELECT ((composite_f(array[1, 2, 3]))).* FROM test_composite; +SELECT ((composite_f(array[1, 2, 3]))).*, 1, 2, 3, ((composite_f(array[1, 2, 3]))).*, 1, 2 +FROM test_composite +WHERE x IN (1, 2, 3); +-- The \bind case from upstream is omitted here, see: https://perconadev.atlassian.net/browse/PG-1936 +-- ROW() expression with row expansion +SELECT (ROW(ARRAY[1,2])).*; +SELECT (ROW(ARRAY[1, 2], ARRAY[1, 2, 3])).*; +SELECT 1, 2, (ROW(ARRAY[1, 2], ARRAY[1, 2, 3])).*, 3, 4; +-- The \bind case from upstream is omitted here, see: https://perconadev.atlassian.net/browse/PG-1936 +SELECT query, calls FROM pg_stat_monitor ORDER BY query COLLATE "C"; + +-- IN and ANY clauses with Vars are not squashed. +SELECT * FROM test_squash a, test_squash b WHERE a.id IN (1, 2, 3, b.id, b.id + 1); +SELECT * FROM test_squash a, test_squash b WHERE a.id = ANY (array[1, ((b.id + b.id * 2)), 5]); +-- The \bind case from upstream is omitted here, see: https://perconadev.atlassian.net/browse/PG-1936 +SELECT query, calls FROM pg_stat_monitor ORDER BY query COLLATE "C"; + -- -- cleanup -- @@ -309,5 +337,7 @@ DROP TABLE test_squash_numeric; DROP TABLE test_squash_bigint; DROP TABLE test_squash_cast CASCADE; DROP TABLE test_squash_jsonb; +DROP TABLE test_composite; +DROP FUNCTION composite_f; SELECT pg_stat_monitor_reset(); DROP EXTENSION pg_stat_monitor; From 9a075ec6169efb07514a38a012326eaecf324c02 Mon Sep 17 00:00:00 2001 From: Artem Gavrilov Date: Tue, 4 Aug 2026 17:45:59 +0200 Subject: [PATCH 7/9] PG-2424 Fix squashing constant duplicates Backport the upstream pg_stat_statements fix (commit b1635c16669) from PostgreSQL 18. Mark same-location duplicates with length = -1 before the squashed check. --- src/pg_stat_monitor.c | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/pg_stat_monitor.c b/src/pg_stat_monitor.c index 11d382c2..891962ae 100644 --- a/src/pg_stat_monitor.c +++ b/src/pg_stat_monitor.c @@ -2760,9 +2760,9 @@ generate_normalized_query(const JumbleState *jstate, const char *query, #endif /* - * Determine constants' lengths (core system only gives us locations), - * and return a sorted copy of jstate's LocationLen data with lengths - * filled in. + * Determine constants' lengths (core system only gives us locations), and + * return a sorted copy of jstate's LocationLen data with lengths filled + * in. */ locs = ComputeConstantLengths(jstate, query, query_loc); @@ -2901,7 +2901,9 @@ ComputeConstantLengths(const JumbleState *jstate, const char *query, core_yy_extra_type yyextra; core_YYSTYPE yylval; YYLTYPE yylloc; +#if PG_VERSION_NUM < 180000 int last_loc = -1; +#endif if (jstate->clocations_count == 0) return NULL; @@ -2940,12 +2942,20 @@ ComputeConstantLengths(const JumbleState *jstate, const char *query, Assert(loc >= 0); #if PG_VERSION_NUM >= 180000 + + /* Ignore constants after the first one in the same location */ + if (i > 0 && locs[i].location == locs[i - 1].location) + { + locs[i].length = -1; + continue; + } + if (locs[i].squashed) continue; /* squashable list, ignore */ -#endif - +#else if (loc <= last_loc) continue; /* Duplicate constant, ignore */ +#endif /* Lex tokens until we find the desired constant */ for (;;) @@ -2994,7 +3004,9 @@ ComputeConstantLengths(const JumbleState *jstate, const char *query, if (tok == 0) break; +#if PG_VERSION_NUM < 180000 last_loc = loc; +#endif } scanner_finish(yyscanner); From 696e3c4bd624db9efb583a677a31a5daebdeb39a Mon Sep 17 00:00:00 2001 From: Artem Gavrilov Date: Tue, 4 Aug 2026 18:46:50 +0200 Subject: [PATCH 8/9] PG-2424 Mark property graphs with * Mark property graphs with * the same way as we do for views. --- Makefile | 1 + regression/expected/relations_propgraph.out | 6 +++ regression/expected/relations_propgraph_1.out | 45 +++++++++++++++++++ regression/sql/relations_propgraph.sql | 31 +++++++++++++ src/pg_stat_monitor.c | 6 ++- 5 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 regression/expected/relations_propgraph.out create mode 100644 regression/expected/relations_propgraph_1.out create mode 100644 regression/sql/relations_propgraph.sql diff --git a/Makefile b/Makefile index add70a4d..9141ebc4 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,7 @@ REGRESS = basic \ functions \ counters \ relations \ + relations_propgraph \ database \ error_insert \ application_name \ diff --git a/regression/expected/relations_propgraph.out b/regression/expected/relations_propgraph.out new file mode 100644 index 00000000..d9c9f0af --- /dev/null +++ b/regression/expected/relations_propgraph.out @@ -0,0 +1,6 @@ +-- +-- Property graph relations (PostgreSQL 19+) +-- +SELECT setting::int < 190000 AS skip_test FROM pg_settings where name = 'server_version_num' \gset +\if :skip_test +\quit diff --git a/regression/expected/relations_propgraph_1.out b/regression/expected/relations_propgraph_1.out new file mode 100644 index 00000000..5a60d0a9 --- /dev/null +++ b/regression/expected/relations_propgraph_1.out @@ -0,0 +1,45 @@ +-- +-- Property graph relations (PostgreSQL 19+) +-- +SELECT setting::int < 190000 AS skip_test FROM pg_settings where name = 'server_version_num' \gset +\if :skip_test +\quit +\endif +CREATE EXTENSION pg_stat_monitor; +CREATE TABLE people (id int PRIMARY KEY, name text); +CREATE TABLE knows (a int REFERENCES people (id), b int REFERENCES people (id)); +CREATE PROPERTY GRAPH social + VERTEX TABLES ( people KEY (id) ) + EDGE TABLES ( knows KEY (a, b) + SOURCE KEY (a) REFERENCES people (id) + DESTINATION KEY (b) REFERENCES people (id) ); +-- A property graph must be marked with a trailing '*' in the relations column, +-- and its underlying element tables must be listed alongside it. +SELECT pg_stat_monitor_reset(); + pg_stat_monitor_reset +----------------------- + +(1 row) + +SELECT * FROM GRAPH_TABLE (social MATCH (p IS people) COLUMNS (p.name)) AS g; + name +------ +(0 rows) + +SELECT query, relations FROM pg_stat_monitor ORDER BY query COLLATE "C"; + query | relations +------------------------------------------------------------------------------+-------------------------------- + SELECT * FROM GRAPH_TABLE (social MATCH (p IS people) COLUMNS (p.name)) AS g | {public.social*,public.people} + SELECT pg_stat_monitor_reset() | +(2 rows) + +SELECT pg_stat_monitor_reset(); + pg_stat_monitor_reset +----------------------- + +(1 row) + +DROP PROPERTY GRAPH social; +DROP TABLE knows; +DROP TABLE people; +DROP EXTENSION pg_stat_monitor; diff --git a/regression/sql/relations_propgraph.sql b/regression/sql/relations_propgraph.sql new file mode 100644 index 00000000..fca7dc5c --- /dev/null +++ b/regression/sql/relations_propgraph.sql @@ -0,0 +1,31 @@ +-- +-- Property graph relations (PostgreSQL 19+) +-- +SELECT setting::int < 190000 AS skip_test FROM pg_settings where name = 'server_version_num' \gset +\if :skip_test +\quit +\endif + +CREATE EXTENSION pg_stat_monitor; + +CREATE TABLE people (id int PRIMARY KEY, name text); +CREATE TABLE knows (a int REFERENCES people (id), b int REFERENCES people (id)); + +CREATE PROPERTY GRAPH social + VERTEX TABLES ( people KEY (id) ) + EDGE TABLES ( knows KEY (a, b) + SOURCE KEY (a) REFERENCES people (id) + DESTINATION KEY (b) REFERENCES people (id) ); + +-- A property graph must be marked with a trailing '*' in the relations column, +-- and its underlying element tables must be listed alongside it. +SELECT pg_stat_monitor_reset(); +SELECT * FROM GRAPH_TABLE (social MATCH (p IS people) COLUMNS (p.name)) AS g; +SELECT query, relations FROM pg_stat_monitor ORDER BY query COLLATE "C"; +SELECT pg_stat_monitor_reset(); + +DROP PROPERTY GRAPH social; +DROP TABLE knows; +DROP TABLE people; + +DROP EXTENSION pg_stat_monitor; diff --git a/src/pg_stat_monitor.c b/src/pg_stat_monitor.c index 891962ae..1c366f5e 100644 --- a/src/pg_stat_monitor.c +++ b/src/pg_stat_monitor.c @@ -845,7 +845,11 @@ pgsm_ExecutorCheckPerms(List *rangeTable, bool ereport_on_violation) namespace_name = get_namespace_name(get_rel_namespace(rte->relid)); relation_name = get_rel_name(rte->relid); - if (rte->relkind == RELKIND_VIEW) + if (rte->relkind == RELKIND_VIEW +#if PG_VERSION_NUM >= 190000 + || rte->relkind == RELKIND_PROPGRAPH +#endif + ) snprintf(relations[num_relations], REL_LEN, "%s.%s*", namespace_name, relation_name); else snprintf(relations[num_relations], REL_LEN, "%s.%s", namespace_name, relation_name); From 96a3336b75daaba360ff66a90a352011f8a54cd0 Mon Sep 17 00:00:00 2001 From: Artem Gavrilov Date: Wed, 5 Aug 2026 15:45:35 +0200 Subject: [PATCH 9/9] PG-2424 Fix RTE skip condition Condition had and issue where it was keeping all subqueries, but it should keep only subqueries that are views or property graphs. Rewrite it with better condition: perminfoindex is non-zero for all types of RTEs that we need. --- src/pg_stat_monitor.c | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/pg_stat_monitor.c b/src/pg_stat_monitor.c index 1c366f5e..9198e21e 100644 --- a/src/pg_stat_monitor.c +++ b/src/pg_stat_monitor.c @@ -821,11 +821,18 @@ pgsm_ExecutorCheckPerms(List *rangeTable, bool ereport_on_violation) char *namespace_name; char *relation_name; - if (rte->rtekind != RTE_RELATION + /* + * Report only RTEs that name a real, permission-checked object: plain + * relations, and the subquery RTEs that were once relations (views + * and property graphs). In PG16+ that set is exactly the RTEs + * carrying a perminfoindex (see the assertion in + * ExecCheckPermissions). + */ #if PG_VERSION_NUM >= 160000 - && rte->rtekind != RTE_SUBQUERY && rte->relkind != RELKIND_VIEW + if (rte->perminfoindex == 0) +#else + if (rte->rtekind != RTE_RELATION) #endif - ) continue; /* Skip duplicates */