Hi Sami,

Please add a `PG_TRY` and `PG_FINALLY` to make sure we always reset the
nesting_level.

Also this will break the following scenario
```
BEGIN;
COMMIT;
SELECT 1; -- This will be stored as inner level because COMMIT sets
is_txn_end flag
```

Can we reset is_txn_end at executorStart to solve the problem?

--
Martin Huang
Amazon Web Services

On Fri, Jan 9, 2026 at 1:02 PM Sami Imseih <[email protected]> wrote:

> Hi,
>
> It was brought to my attention that there is pg_stat_statements
> behavior where an implicitly closed cursor, via COMMIT or END,
> is stored as toplevel for both the utility DECLARE CURSOR
> statement and the underlying query.
>
> ```
> BEGIN;
> DECLARE FOOCUR CURSOR FOR SELECT * from stats_track_tab;
> FETCH FORWARD 1 FROM foocur;
>  x
> ---
> (0 rows)
>
> COMMIT;
> SELECT toplevel, calls, query FROM pg_stat_statements
>   ORDER BY query COLLATE "C";
>  toplevel | calls |                          query
>
> ----------+-------+----------------------------------------------------------
>  t        |     1 | BEGIN
>  t        |     1 | COMMIT
>  t        |     1 | DECLARE FOOCUR CURSOR FOR SELECT * from stats_track_tab
>  t        |     1 | DECLARE FOOCUR CURSOR FOR SELECT * from
> stats_track_tab;
>  t        |     1 | FETCH FORWARD $1 FROM foocur
>  t        |     1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
> (6 rows)
> ```
>
> Also, with track_planning enabled, the underlying query is
> stored with toplevel = false for the plans counter and with
> toplevel = true for the calls counter, resulting in an
> additional entry.
>
> ```
> SELECT toplevel, calls, plans, query FROM pg_stat_statements
>   ORDER BY query COLLATE "C";
>  toplevel | calls | plans |                            query
>
> ----------+-------+-------+--------------------------------------------------------------
>  t        |     1 |     0 | BEGIN
>  t        |     1 |     0 | COMMIT
>  t        |     1 |     0 | DECLARE FOOCUR CURSOR FOR SELECT * from
> stats_track_tab
>  t        |     1 |     0 | DECLARE FOOCUR CURSOR FOR SELECT * from
> stats_track_tab;
>  f        |     0 |     1 | DECLARE FOOCUR CURSOR FOR SELECT * from
> stats_track_tab;
>  t        |     1 |     0 | FETCH FORWARD $1 FROM foocur
>  t        |     1 |     0 | SELECT pg_stat_statements_reset() IS NOT NULL
> AS t
>  t        |     0 |     1 | SELECT toplevel, calls, plans, query FROM
> pg_stat_statements+
>           |       |       |   ORDER BY query COLLATE "C"
>  ```
>
> The reason this occurs is because PortalCleanup, which triggers
> ExecutorEnd, runs after the ProcessUtility hook. At that point,
> nesting_level has already been reset back to 0.
>
> I am not sure how common this pattern is, but it is probably
> worth fixing. At a minimum, we need tests to show this behavior,
> but we can do better by checking whether we just processed a
> COMMIT statement and setting a flag to let ExecutorEnd increment
> nesting_level. There should be no other way to reach ExecutorEnd
> after a COMMIT besides PortalCleanup, AFAICT. I could be proven
> wrong.
>
> The attached patch fixes this as described above.
>
> Note that, due to f85f6ab051b7, there is a separate issue that
> should be improved. Tracking the underlying SQL of a utility
> statement using the utility statement itself is confusing
> and should be fixed. That is a separate point, but I am
> mentioning it here for clarity.
>
>
> --
> Sami Imseih
> Amazon Web Services
>

Reply via email to