Skip to content

Expose query progress and connection id as SQL functions#389

Open
sfc-gh-mslot wants to merge 1 commit into
mainfrom
marcoslot/query-progress
Open

Expose query progress and connection id as SQL functions#389
sfc-gh-mslot wants to merge 1 commit into
mainfrom
marcoslot/query-progress

Conversation

@sfc-gh-mslot

@sfc-gh-mslot sfc-gh-mslot commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

Problem

DuckDB's executor already tracks per-query progress (ClientContext::query_progress, populated when enable_progress_bar is on), but the only way to read it from outside the running query was to call duckdb_query_progress against the same duckdb_connection. There is no SQL surface for asking another session's progress, which makes it impossible for a sidecar / monitor connection to report on a running query.

Solution

Two new SQL functions registered by the duckdb_pglake extension:

-- the connection_id pgduck_server assigned to this session
-- (the same value libpq's PQbackendPID returns to the client)
SELECT pg_lake_connection_id();

-- progress for one specific session, identified by connection_id
SELECT percentage, rows_processed, total_rows_to_process
FROM pg_lake_query_progress(42);

pg_lake_query_progress(connection_id) iterates the ConnectionManager's connection list (the same pattern pg_lake_stat_activity uses), finds the session whose PgLakeQueryListener.connectionId matches the argument, and returns one row with the values from ClientContext::GetQueryProgress(). The result is empty when no session has the requested id or the matching session has no active query. Returning by id rather than as a global list keeps this function orthogonal to whatever shape pg_lake_stat_activity evolves into; if we later want progress alongside the activity columns we can extend pg_lake_stat_activity itself.

The aggregator only measures source-side scans — postgres_scan, parquet, csv, range, and so on. Sinks (CSV serialization, S3 multipart upload, parquet writer flush, COPY TO) and post-scan operators (sort, join, aggregate beyond their input) are not measured. The percentage therefore tracks "how much of the source has been read", not wall-clock progress; for COPY (SELECT FROM postgres_scan(...)) TO ... the bar can sit pinned near 100% while the destination side is still flushing.

pg_lake_connection_id() reads the current ClientContext's PgLakeQueryListener.connectionId. Behind the scenes, pgduck_server now seeds that identifier from the cancellation proc id (the random int32 it already sends in BackendKeyData) instead of the raw client socket fd. That unifies the value with libpq's PQbackendPID, makes pg_lake_stat_activity look more like real postgres pg_stat_activity, and avoids the rapid fd recycling that would otherwise let a stat snapshot taken across a couple of disconnect/reconnect cycles show the same "id" for two different sessions.

duckdb_pglake_init_connection also enables enable_progress_bar by default for every new pgduck session and disables print_progress_bar (no terminal to render to). Without this default the new function would silently return -1 / 0 / 0 even though everything else is wired up; turning the bar on lets the executor populate ClientContext::query_progress after the standard wait_time delay (default 2000ms). Clients can still issue SET enable_progress_bar = false per session to opt back out.

The progress struct's fields are atomic and DuckDB documents GetQueryProgress as safe to call from another thread, which is the same access pattern used by the C-API duckdb_query_progress.

Test plan

  • Manual smoke test: pg_lake_connection_id() returns the same value as PQbackendPID(conn) from a libpq client connected to the same session.
  • Manual smoke test: pg_lake_query_progress(-1) returns an empty result; pg_lake_query_progress(pg_lake_connection_id()) returns one row.
  • Manual smoke test: with two psql sessions, A running COPY (SELECT FROM postgres_scan(...)) TO ... (no manual SET) and B querying pg_lake_query_progress(<id-from-stat-activity>), the percentage advances after the 2-second wait_time.
  • CI: new pytest under pgduck_server/tests/pytests/test_query_progress.py covering both functions and the empty-result-for-unknown-id case.

@sfc-gh-mslot sfc-gh-mslot force-pushed the marcoslot/query-progress branch from 75584d0 to e2145d5 Compare June 8, 2026 17:56
@sfc-gh-mslot sfc-gh-mslot marked this pull request as draft June 8, 2026 18:08
@sfc-gh-mslot sfc-gh-mslot force-pushed the marcoslot/query-progress branch from e2145d5 to e06ca1a Compare June 8, 2026 19:29
Comment thread duckdb_pglake/src/utility_functions.cpp
@sfc-gh-mslot sfc-gh-mslot force-pushed the marcoslot/query-progress branch from e06ca1a to cebdcd1 Compare June 9, 2026 09:22
@sfc-gh-mslot sfc-gh-mslot marked this pull request as ready for review June 9, 2026 12:23
Add two utility functions to the duckdb_pglake extension:

  - pg_lake_connection_id() returns the connection identifier
    pgduck_server has assigned to the current session.  This is the
    same value libpq's PQbackendPID returns to a client connected
    to pgduck (and the same value reported in the connection_id
    column of pg_lake_stat_activity), so a client can capture its
    own id from libpq directly and only call this function from
    SQL contexts that have no libpq access.

  - pg_lake_query_progress(connection_id) returns at most one row
    with the percentage / rows_processed / total_rows_to_process
    values that DuckDB's executor publishes on
    ClientContext::query_progress for the matching session.  The
    aggregator only measures source-side scans (postgres_scan,
    parquet, csv, range, ...); sinks and post-scan operators are
    not measured, so the percentage tracks "how much of the source
    has been read", not wall-clock progress.  The result is empty
    when no session has the requested id or the matching session
    has no active query.

Behind the scenes pgduck_server now seeds the DuckDB-side connection
identifier from the cancellation proc id (the random int32 it
already sends in BackendKeyData) instead of the raw client socket
fd. That unifies the value with libpq's PQbackendPID, makes
pg_lake_stat_activity look more like real postgres pg_stat_activity,
and avoids the rapid fd recycling that would otherwise let a stat
snapshot taken across a couple of disconnect/reconnect cycles show
the same "id" for two different sessions.

duckdb_pglake_init_connection also enables enable_progress_bar by
default for every new pgduck session and disables print_progress_bar
since pgduck has no terminal to render to.  Without this default the
new pg_lake_query_progress function would silently return -1 / 0 / 0
even though everything else is wired up; turning the bar on lets the
executor populate ClientContext::query_progress after the standard
wait_time delay (default 2000ms).  Clients can still issue
SET enable_progress_bar = false per session to opt back out.

The progress fields are atomic and DuckDB documents
GetQueryProgress as safe to call from another thread, which is the
same access pattern used internally by duckdb_query_progress.

Signed-off-by: Marco Slot <marco.slot@snowflake.com>
@sfc-gh-mslot sfc-gh-mslot force-pushed the marcoslot/query-progress branch from cebdcd1 to 3963577 Compare June 9, 2026 13:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant