pg_stat_statements: add missing tests for nesting_level

Started by Sami Imseih4 days ago4 messages
#1Sami Imseih
samimseih@gmail.com
1 attachment(s)

Hi,

While looking at pg_stat_statements nesting_level, I realized that there
are missing nesting_level tests for pgss_planner and pgss_ExecutorFinish.
That is, if you remove nesting_level++ and nesting_level-- in those 2 hooks,
the tests will still succeed.

For pgss_planner the nesting_level updates missing tests are the ones
when track_planning is enabled.

Attached is a quick patch to add coverage.

--
Sami Imseih
Amazon Web Services (AWS)

Attachments:

v1-0001-pg_stat_statements-Add-missing-tests-for-nested-s.patchapplication/octet-stream; name=v1-0001-pg_stat_statements-Add-missing-tests-for-nested-s.patchDownload
From 7520cc211a6f4a78514c6f32ddc7eb185365e50a Mon Sep 17 00:00:00 2001
From: Ubuntu <ubuntu@ip-172-31-46-230.ec2.internal>
Date: Tue, 20 Jan 2026 23:25:52 +0000
Subject: [PATCH v1 1/1] pg_stat_statements: Add missing tests for nested
 statements

This adds tests to verify nesting_level is set correctly when tracking
nested queries in pgss_planner and pgss_ExecutorFinish.
---
 .../expected/level_tracking.out               | 62 +++++++++++++++++++
 .../pg_stat_statements/sql/level_tracking.sql | 35 +++++++++++
 2 files changed, 97 insertions(+)

diff --git a/contrib/pg_stat_statements/expected/level_tracking.out b/contrib/pg_stat_statements/expected/level_tracking.out
index 8e8388dd5cb..71a55421f03 100644
--- a/contrib/pg_stat_statements/expected/level_tracking.out
+++ b/contrib/pg_stat_statements/expected/level_tracking.out
@@ -1535,6 +1535,68 @@ SELECT toplevel, calls, rows, query FROM pg_stat_statements ORDER BY query COLLA
  t        |     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
 (8 rows)
 
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+SET pg_stat_statements.track_planning = TRUE;
+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_statements ORDER BY query COLLATE "C";
+ toplevel | calls | rows | plans |                                             query                                             
+----------+-------+------+-------+-----------------------------------------------------------------------------------------------
+ t        |     2 |    2 |     2 | SELECT PLUS_THREE($1)
+ f        |     2 |    2 |     2 | SELECT i + 3 LIMIT 1
+ t        |     1 |    1 |     0 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+ t        |     0 |    0 |     1 | SELECT toplevel, calls, rows, plans, query FROM pg_stat_statements ORDER BY query COLLATE "C"
+(4 rows)
+
+RESET pg_stat_statements.track_planning;
+-- AFTER trigger SQL - top-level tracking.
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(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_statements ORDER BY query COLLATE "C";
+ toplevel | calls | rows | plans |                        query                        
+----------+-------+------+-------+-----------------------------------------------------
+ f        |     2 |    2 |     0 | INSERT INTO audit_table VALUES ($15, TG_OP, NEW.id)
+ t        |     2 |    2 |     0 | INSERT INTO test_trigger VALUES ($1, $2)
+ t        |     1 |    1 |     0 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+DROP TRIGGER audit_after_trigger ON test_trigger;
+DROP FUNCTION audit_trigger_func();
+DROP TABLE audit_table, test_trigger;
 --
 -- pg_stat_statements.track = none
 --
diff --git a/contrib/pg_stat_statements/sql/level_tracking.sql b/contrib/pg_stat_statements/sql/level_tracking.sql
index 86f007e8552..b3bd0ddabf2 100644
--- a/contrib/pg_stat_statements/sql/level_tracking.sql
+++ b/contrib/pg_stat_statements/sql/level_tracking.sql
@@ -431,6 +431,41 @@ SELECT PLUS_THREE(8);
 SELECT PLUS_THREE(10);
 
 SELECT toplevel, calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+SET pg_stat_statements.track_planning = TRUE;
+
+SELECT PLUS_THREE(8);
+SELECT PLUS_THREE(10);
+
+SELECT toplevel, calls, rows, plans, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+RESET pg_stat_statements.track_planning;
+
+-- AFTER trigger SQL - all-level tracking.
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+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_statements 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_statements.track = none
-- 
2.43.0

#2Michael Paquier
michael@paquier.xyz
In reply to: Sami Imseih (#1)
Re: pg_stat_statements: add missing tests for nesting_level

On Tue, Jan 20, 2026 at 06:08:14PM -0600, Sami Imseih wrote:

While looking at pg_stat_statements nesting_level, I realized that there
are missing nesting_level tests for pgss_planner and pgss_ExecutorFinish.
That is, if you remove nesting_level++ and nesting_level-- in those 2 hooks,
the tests will still succeed.

For pgss_planner the nesting_level updates missing tests are the ones
when track_planning is enabled.

Attached is a quick patch to add coverage.

Confirmed these two deficiencies, nice catch. If one does the same
removal of the nesting level calculation in other code paths like
pgss_ExecutorRun(), one get complaints. Will see to get this addition
done.
--
Michael

#3Alexander Lakhin
exclusion@gmail.com
In reply to: Michael Paquier (#2)
Re: pg_stat_statements: add missing tests for nesting_level

Hello Michael and Sami,

21.01.2026 02:41, Michael Paquier wrote:

On Tue, Jan 20, 2026 at 06:08:14PM -0600, Sami Imseih wrote:

While looking at pg_stat_statements nesting_level, I realized that there
are missing nesting_level tests for pgss_planner and pgss_ExecutorFinish.
That is, if you remove nesting_level++ and nesting_level-- in those 2 hooks,
the tests will still succeed.

For pgss_planner the nesting_level updates missing tests are the ones
when track_planning is enabled.

Attached is a quick patch to add coverage.

Confirmed these two deficiencies, nice catch. If one does the same
removal of the nesting level calculation in other code paths like
pgss_ExecutorRun(), one get complaints. Will see to get this addition
done.

Two buildfarm animals [1]https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=avocet&amp;dt=2026-01-22%2000%3A58%3A36, [2]https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=trilobite&amp;dt=2026-01-24%2023%3A10%3A13 say that that addition is incompatible with
the CLOBBER_CACHE_ALWAYS mode:
not ok 5     - level_tracking                          52571 ms

diff -U3 /home/buildfarm/avocet/buildroot/HEAD/pgsql.build/contrib/pg_stat_statements/expected/level_tracking.out 
/home/buildfarm/avocet/buildroot/HEAD/pgsql.build/contrib/pg_stat_statements/results/level_tracking.out
--- /home/buildfarm/avocet/buildroot/HEAD/pgsql.build/contrib/pg_stat_statements/expected/level_tracking.out 2026-01-22 
01:59:12.213054121 +0100
+++ /home/buildfarm/avocet/buildroot/HEAD/pgsql.build/contrib/pg_stat_statements/results/level_tracking.out 2026-01-22 
05:24:17.363666155 +0100
@@ -1560,7 +1560,7 @@
   toplevel | calls | rows | plans | query
  ----------+-------+------+-------+--------------------------------------------------------------------
   t        |     2 |    2 |     2 | SELECT PLUS_THREE($1)
- f        |     2 |    2 |     2 | SELECT i + 3 LIMIT 1
+ f        |     2 |    2 |     2 | SELECT i + $2 LIMIT $3
   t        |     1 |    1 |     0 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
   t        |     0 |    0 |     1 | SELECT toplevel, calls, rows, plans, query FROM pg_stat_statements+
            |       |      |       |   ORDER BY query COLLATE "C"

I can reproduce this locally with no extra tricks. Could you please adjust
the test for this mode?

[1]: https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=avocet&amp;dt=2026-01-22%2000%3A58%3A36
[2]: https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=trilobite&amp;dt=2026-01-24%2023%3A10%3A13

Best regards,
Alexander

#4Michael Paquier
michael@paquier.xyz
In reply to: Alexander Lakhin (#3)
1 attachment(s)
Re: pg_stat_statements: add missing tests for nesting_level

On Sun, Jan 25, 2026 at 08:00:00AM +0200, Alexander Lakhin wrote:

I can reproduce this locally with no extra tricks. Could you please adjust
the test for this mode?

Reproduced here. That was trickier than it looks. A trick with
debug_discard_caches cannot help, because with a CLOBBER_CACHE_ALWAYS
build we would still do a GetCachedPlan() -> RevalidateCachedQuery()
that goes through the post-parse analyze hook where the query would
still be normalized, showing up in the output anyway.

So I have come up with a plan B. If we do a DISCARD PLANS before the
*first* function call, we can force the test to revalidate the cached
query without caring about CLOBBER_CACHE_ALWAYS, meaning that we would
always store a normalized version of the inner query. The point of
the test is to check after the nesting level calculation in the
planner hook, and the test is still able to check that correctly. If
I remove the nesting_level bits from the code while the DISCARD is
around, the entry is stored as a top level entry incorrectly, but it
should be stored as toplevel=false. I'll go apply the attached
shortly, after some more checks..
--
Michael

Attachments:

0001-Fix-PGSS-test.patchtext/x-diff; charset=us-asciiDownload
From 2a4dfcc3e47316e3784e959df30a9be251a36b8e Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Sun, 25 Jan 2026 16:39:41 +0900
Subject: [PATCH] Fix PGSS test

---
 contrib/pg_stat_statements/expected/level_tracking.out | 9 ++++++++-
 contrib/pg_stat_statements/sql/level_tracking.sql      | 8 +++++++-
 2 files changed, 15 insertions(+), 2 deletions(-)

diff --git a/contrib/pg_stat_statements/expected/level_tracking.out b/contrib/pg_stat_statements/expected/level_tracking.out
index 35e889fabd27..a15d897e59b0 100644
--- a/contrib/pg_stat_statements/expected/level_tracking.out
+++ b/contrib/pg_stat_statements/expected/level_tracking.out
@@ -1543,6 +1543,13 @@ SELECT pg_stat_statements_reset() IS NOT NULL AS t;
 
 -- planner - all-level tracking.
 SET pg_stat_statements.track_planning = TRUE;
+-- 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 
 ------------
@@ -1560,7 +1567,7 @@ SELECT toplevel, calls, rows, plans, query FROM pg_stat_statements
  toplevel | calls | rows | plans |                               query                                
 ----------+-------+------+-------+--------------------------------------------------------------------
  t        |     2 |    2 |     2 | SELECT PLUS_THREE($1)
- f        |     2 |    2 |     2 | SELECT i + 3 LIMIT 1
+ f        |     2 |    2 |     2 | SELECT i + $2 LIMIT $3
  t        |     1 |    1 |     0 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
  t        |     0 |    0 |     1 | SELECT toplevel, calls, rows, plans, query FROM pg_stat_statements+
           |       |      |       |   ORDER BY query COLLATE "C"
diff --git a/contrib/pg_stat_statements/sql/level_tracking.sql b/contrib/pg_stat_statements/sql/level_tracking.sql
index 8dd3fc2df089..003efb8184b1 100644
--- a/contrib/pg_stat_statements/sql/level_tracking.sql
+++ b/contrib/pg_stat_statements/sql/level_tracking.sql
@@ -435,7 +435,13 @@ SELECT pg_stat_statements_reset() IS NOT NULL AS t;
 
 -- planner - all-level tracking.
 SET pg_stat_statements.track_planning = TRUE;
-
+-- 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);
 
-- 
2.51.0