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/.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] 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 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/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/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/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/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 new file mode 100644 index 00000000..a28091f0 --- /dev/null +++ b/regression/expected/level_tracking_2.out @@ -0,0 +1,400 @@ +-- +-- 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) + +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 +-- +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/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/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/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/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/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/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; 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/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 9e979b9b..9198e21e 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 @@ -68,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() && \ @@ -86,6 +88,7 @@ typedef enum pgsmVersion PGSM_V2_0, PGSM_V2_1, PGSM_V2_3, + PGSM_NEXT, } pgsmVersion; /*---- Initialization Function Declarations ----*/ @@ -102,6 +105,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 +169,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 +187,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, @@ -189,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); @@ -259,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); @@ -267,10 +288,13 @@ 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); +#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); @@ -423,7 +447,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 +554,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 +586,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 +601,7 @@ pgsm_ExecutorStart(QueryDesc *queryDesc, int eflags) queryDesc->totaltime = InstrAlloc(1, INSTRUMENT_ALL, false); MemoryContextSwitchTo(oldcxt); } +#endif } } @@ -687,7 +729,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 +741,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 +761,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 @@ -726,12 +776,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); @@ -767,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 */ @@ -791,7 +852,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); @@ -814,7 +879,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 +945,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(); { @@ -913,7 +988,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 { @@ -937,9 +1013,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(); { @@ -1105,7 +1187,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); @@ -1265,7 +1348,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 @@ -1353,6 +1437,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 } /* @@ -1466,6 +1558,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 @@ -1911,6 +2007,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 */ @@ -1968,6 +2071,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), @@ -2365,17 +2471,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 */ @@ -2418,6 +2531,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 +2754,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; @@ -2648,15 +2765,17 @@ generate_normalized_query(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. @@ -2684,15 +2803,15 @@ generate_normalized_query(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 */ @@ -2714,7 +2833,7 @@ generate_normalized_query(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 */ @@ -2728,6 +2847,10 @@ generate_normalized_query(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. @@ -2742,10 +2865,15 @@ generate_normalized_query(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. * @@ -2771,27 +2899,38 @@ generate_normalized_query(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(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; core_yy_extra_type yyextra; core_YYSTYPE yylval; YYLTYPE yylloc; +#if PG_VERSION_NUM < 180000 int last_loc = -1; - int i; +#endif + + 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, @@ -2803,7 +2942,7 @@ fill_in_constant_lengths(JumbleState *jstate, const char *query, yyextra.escape_string_warning = false; /* 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; @@ -2814,12 +2953,20 @@ fill_in_constant_lengths(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 (;;) @@ -2868,10 +3015,14 @@ fill_in_constant_lengths(JumbleState *jstate, const char *query, if (tok == 0) break; +#if PG_VERSION_NUM < 180000 last_loc = loc; +#endif } scanner_finish(yyscanner); + + return locs; } /* @@ -2891,6 +3042,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) 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(); 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) +