Thread

  1. pg_stat_statements: Fix nested tracking for implicitly closed cursors

    Sami Imseih <samimseih@gmail.com> — 2025-12-23T23:07:29Z

    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