Skip to content

Latest commit

 

History

40 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

About the Extension

The pg_query_stack extension allows you to retrieve the full stack of SQL queries for the current session (unlike the current_query() function, which returns only the top-level query). This is useful for debugging, logging, and analyzing complex call chains in the database.

It works based on two minimal executor hooks (ExecutorRun/ExecutorFinish) that record raw QueryDesc* pointers into a per-backend ring buffer for exactly as long as the query executes. The SQL function reads the ring at call time using a snapshot-once walker with three defensive guards. The source code is extensively commented (currently only in Russian).

Unlike pg_self_query, its predecessor, the extension does not depend on the pg_query_state module or PostgreSQL core patches, so it can be installed on a vanilla PostgreSQL and likely on any fork.

Architecture

The hot path is two executor hooks that wrap the downstream call, 2 branches each (active flag + ring-full check), no string copy, no heap allocation, no PG_TRY:

  • ExecutorRun: push queryDesc into a 100-slot BSS ring (single 8-byte store), call downstream, restore the ring head to its saved value. Called once per plain statement and once per FETCH batch of a cursor or suspended portal.
  • ExecutorFinish: the same wrapper. AFTER triggers fire inside standard_ExecutorFinish, so an audit trigger reading pg_query_stack() sees its own UPDATE or DELETE as a frame.
  • SubXactCallback: per-subxid LIFO snapshot/restore of the ring head, the cleanup for frames abandoned by a longjmp when a PL/pgSQL EXCEPTION block swallows an error.
  • XactCallback: BSS reset (ring head, snapshot stack, unreliable flag) on COMMIT, ABORT and PREPARE TRANSACTION.

A frame lives exactly as long as its query executes: until the downstream ExecutorRun or ExecutorFinish returns. Push and pop therefore follow the C call stack and do not depend on portal lifetimes:

  • An open but idle cursor (DECLARE or OPEN without FETCH) is not a frame. During FETCH the cursor's query is a frame: frame 0 when the FETCH itself is the top-level statement, the next frame when the FETCH runs inside a function called from another query. CLOSE runs the cursor's ExecutorFinish, which pushes and pops a frame like any other and leaves nothing behind.
  • CLOSE in any order, refcursor values returned from functions, cursors dropped at COMMIT, holdable cursors persisted at COMMIT and cursors whose FETCH failed inside a subtransaction never leave a stale pointer in the ring. Every pointer in the ring belongs to a query that is in the middle of its own Run or Finish.
  • pg_query_stack() called from another extension's ExecutorStart or ExecutorEnd hook does not see the current query.

Known boundaries of this model:

  • More than 256 nested subtransactions (SAVEPOINT or EXCEPTION levels, MAX_SUBXACT_STACK_DEPTH): the snapshot stack is full and a later ABORT_SUB cannot clean up, so the stack is marked unreliable until the end of the transaction and pg_query_stack() returns no rows. The next transaction starts clean (test 032).
  • SQL executed during ExecutorStart of a query, for example a domain CHECK evaluated while an aggregate with a domain state type is initialized, does not see that query as a frame. The frame appears at ExecutorRun.
  • C code that catches an ERROR without a subtransaction and continues (PL/Perl return_next turning the error into a Perl exception) skips the pop of the failed query. Its frame stays until the enclosing Run or Finish returns or the transaction ends, and until then pg_query_stack() can read a freed QueryDesc (cassert builds fail assertion A3 when the enclosing hook returns). The hooks deliberately have no PG_TRY, because a sigsetjmp per query costs more than this boundary.

The pg_query_stack() function walks the ring at call time: snapshot-once at FIRSTCALL, lazy pstrdup per slot, three defensive guards (null queryDesc / null estate / null sourceText) emit placeholder strings instead of dereferencing.

Memory footprint: pgs_ring[100] (800 B) + pgs_ring_subxact_snap[256] (2048 B) + 2 ints + 1 bool = 2857 B BSS per backend, zero-initialized.

Depth beyond 100: deeper frames are not recorded. The hook passes control downstream without a push and has nothing to restore afterwards, so the ring keeps the 100 outermost frames.

Parallel workers: skipped via IsParallelWorker(). A worker has its own per-process ring and would report its plan fragment as a top-level frame without the leader's context.

Features

  • Retrieve the full stack of nested SQL queries for the current session.
  • Independence from other modules (no dependency on pg_query_state).
  • Compatible with PostgreSQL 16 / 17 / 18 (verified on release and cassert builds).
  • Hot-path overhead of about 40 extra instructions per query, wall-clock difference within measurement noise (see Compatibility for the numbers).
  • Extensively commented source code (currently only in Russian).

Compatibility

Verified on PostgreSQL 16.15 / 17.11 / 18.6 (release + cassert builds, Linux x86_64) and 16.14 / 17.10 / 18.4 (cassert builds, macOS arm64). The public API used by the extension (ExecutorRun_hook, ExecutorFinish_hook, QueryDesc with its estate and sourceText) is stable across these versions. The only difference is the ExecutorRun_hook signature, which lost its execute_once parameter in PG 18 and is handled by a compile-time version check. Compatibility with earlier versions may require minor source modifications.

Measured overhead. Setup: PG 16.15 PGDG on an Ubuntu 26.04 KVM guest (16 vCPU AMD EPYC), gcc 15.2 -O2. Method: perf stat attached to the backend for 5 million EXECUTE 'SELECT 1' iterations of a PL/pgSQL loop, median of 3 runs. One iteration is about 25,100 instructions.

build hooks conditional branches (objdump, x86_64) instructions in hook bodies extra instructions per query, enabled vs no preload extra branches per query
2.0.0 ExecutorStart + ExecutorEnd 2 + 3 17 + 19 +23.5 +4.9
this build ExecutorRun + ExecutorFinish 2 + 2 30 + 29 +40.1 +5.8

The wrapper keeps a real stack frame around the downstream call, where the old hooks tail-called it, and that is where the extra instructions come from. Wall-clock deltas per query stay inside the measurement noise of this VM (about ±150 ns on a 3.9 µs iteration) for both builds, and pgbench -c 1 -M prepared TPS on SELECT 1, a nested plpgsql call and an audited INSERT are likewise indistinguishable from the no-preload baseline. With pg_query_stack.enabled = off the hooks cost about +7 instructions per query in both builds.

Use Cases

This module can be useful in the following situations:

  • Logging (via triggers) of queries that modified data in a table.
  • Capturing the actual result of a DSQL query that was executed and recording it somewhere.
  • Debugging very complex functions or triggers with intricate call chains.

Installation

  1. After cloning this repository and navigating to the extension's folder, run:

    make install USE_PGXS=1

    Make sure that pg_config points to the desired version of PostgreSQL if you have multiple versions installed!

  2. Then, change the session_preload_libraries parameter in postgresql.conf:

    session_preload_libraries = 'pg_query_stack'
    

    The extension should be loaded into the session! Do not add it to shared_preload_libraries.

  3. Restart PostgreSQL:

    sudo systemctl restart postgresql  # or another method to restart
  4. In the target database, run to create the extension:

    CREATE EXTENSION pg_query_stack;
  5. Done!

Description of the pg_query_stack Function

pg_query_stack(_skip_count int DEFAULT 1)
    RETURNS TABLE (
        frame_number integer,
        query_text text
    )

As a result of executing the function, a table containing the query stack will be returned, starting from the top-level query (frame number 0) down to the lowest level (frame number N) minus the skipped frames.

What the _skip_count parameter means:

  • 0 - returns the entire query stack "as is," including the query where pg_query_stack itself is called.
  • 1 - (default) returns the stack without the query where pg_query_stack itself is called.
  • N - the specified number of queries in the stack starting from the lowest level will be skipped.

Example of the Extension's Operation

Let's create two functions in the database:

CREATE OR REPLACE FUNCTION test() RETURNS void
LANGUAGE plpgsql
AS
$$
BEGIN
   DROP TABLE IF EXISTS test1;
   CREATE TEMP TABLE test1 AS
   SELECT
      *
   FROM pg_query_stack();

   DROP TABLE IF EXISTS test2;
   CREATE TEMP TABLE test2 AS
   SELECT
      *
   FROM pg_query_stack(0);
END;
$$;

CREATE OR REPLACE FUNCTION test_up() RETURNS void
LANGUAGE plpgsql
AS
$$
BEGIN
    PERFORM test();
END;
$$;

Now let's call the test_up function:

SELECT test_up();

Now let's see what was recorded in the test1 table:

SELECT * FROM test1;

Result:

frame_number query_text
1 SELECT test()
0 SELECT test_up()

The test1 table contains the query stack without the current call to pg_query_stack, since the _skip_count parameter has the default value of 1.

Now let's see what was recorded in the test2 table:

SELECT * FROM test2;

Result:

frame_number query_text
2 CREATE TEMP TABLE test2 AS SELECT * FROM pg_query_stack(0)
1 SELECT test()
0 SELECT test_up()

The test2 table contains the full query stack, including the current call to pg_query_stack, because we passed the parameter _skip_count = 0.

Explanation:

In the first case, we get the stack without the current pg_query_stack() query, which can be useful for obtaining information about external queries. In the second case, we see the full stack, including the innermost call, which allows us to fully trace the call chain.

Updating the Extension Version

After compiling from the source files, execute:

DROP EXTENSION pg_query_stack;
CREATE EXTENSION pg_query_stack;

Then restart PostgreSQL.

Migration from the pg_self_query Extension

If you have previously used the pg_self_query extension with the function of the same name, to ensure backward compatibility and avoid the need to change code, you can create a "wrapper":

CREATE OR REPLACE FUNCTION public.pg_self_query()
   RETURNS TABLE (
      frame_number integer,
      query_text   text
   )
AS
$$
   SELECT
      frame_number,
      query_text
   FROM public.pg_query_stack(2)
$$
LANGUAGE SQL;

At the moment, it is automatically created when the extension is installed.

License

This project is licensed under the MIT License. This means you are free to use, modify, and distribute this code for commercial and non-commercial purposes, provided you retain the copyright notice.

The full text of the license is available in the LICENSE file.

About

Extension allows you to retrieve the full stack of SQL queries for the current session

Resources

Stars

7 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages