Skip to content

feat(pg_top_queries): add io_read_time_ms / io_write_time_ms for pg_stat_statements >= 1.10#16

Open
HighviewOne wants to merge 4 commits into
mainfrom
feat/pg-top-queries-io-timing
Open

feat(pg_top_queries): add io_read_time_ms / io_write_time_ms for pg_stat_statements >= 1.10#16
HighviewOne wants to merge 4 commits into
mainfrom
feat/pg-top-queries-io-timing

Conversation

@HighviewOne

Copy link
Copy Markdown
Collaborator

Closes #11.

What

pg_top_queries previously had no way to tell whether a slow query was CPU-bound or IO-bound. pg_stat_statements 1.10 (Postgres 16) added shared_blk_read_time and shared_blk_write_time which answer exactly that question.

How

  • Gate on compareVersions(extVersion, "1.10") >= 0 — same pattern already used for the 1.8 column-rename detection
  • NULLIF(shared_blk_read_time, 0) converts zero (meaning track_io_timing = off) to null so callers can distinguish "timing disabled" from "query did no IO"
  • When pg_stat_statements < 1.10, the columns are absent from the result entirely (not null, not zero — just not there)
  • Tool description updated to mention the columns and how to enable track_io_timing

Test

Added an integration test that:

  • Checks the installed extversion and skips the column assertion if < 1.10 (environment-independent)
  • When >= 1.10: asserts io_read_time_ms and io_write_time_ms are present as keys on every row
  • Asserts values are either null or a JS number (not a string)

Tested locally against pg_stat_statements 1.12 on Postgres 18. All 87 integration tests pass.

🤖 Generated with Claude Code

…TESTS=1

The _warnings-path test for pg_replication_status forces a permission error
by revoking PUBLIC EXECUTE on pg_catalog.pg_current_wal_lsn(). A hard
process kill between REVOKE and GRANT leaves the cluster broken for every
non-superuser session until manually repaired.

Add a destructiveTestsEnabled() helper in fixtures.ts and skip the risky
describe block unless POSTGRES_MCP_DESTRUCTIVE_TESTS=1 is set. The test
still runs on disposable CI clusters that opt in; local and shared clusters
skip it safely. Closes #9.
@jeffyaw

jeffyaw commented Jun 11, 2026

Copy link
Copy Markdown
Member

Review punch list for this PR. Item 1 is a blocker -- as written the feature breaks pg_top_queries on every PG 15/16 cluster, and the PG17/PG18-only test matrix can't see it. Verified against the PG 16 docs: pg_stat_statements 1.10 has only blk_read_time / blk_write_time; the shared_blk_* names arrive with 1.11 (PG 17).

  1. src/tools/stats.ts (new ioTimingCols block) -- bug, breaks the tool on PG 15/16: the gate fires at compareVersions(extVersion, "1.10") >= 0 but selects shared_blk_read_time / shared_blk_write_time, which only exist in pg_stat_statements 1.11. On a 1.10 cluster every pg_top_queries call hard-errors with column "shared_blk_read_time" does not exist. Fix: pick the column name by version (blk_read_time for < 1.11, shared_blk_* for >= 1.11 -- the old names have existed with identical ms semantics since 1.1), or at minimum bump the gate to "1.11". The comment "added in pg_stat_statements 1.10 (Postgres 16)" is also off -- 1.10 shipped with PG 15.

  2. src/tools/stats.ts NULLIF comment + tool description -- the NULLIF(x, 0) rationale is backwards: it claims callers can tell "IO timing disabled" from "no IO occurred", but NULLIF maps both cases to the same null. The description's "null when track_io_timing = off" is also wrong for a fully-cached query under timing-on. Fix: either query current_setting('track_io_timing') and null the columns only when it's actually off, or reword comment + description to "null = no IO time recorded (track_io_timing off, or the query did no measurable IO)".

  3. src/integration/query.integration.test.ts (new io-timing test) -- mirrors the wrong "1.10" constant and the same null-semantics claim. On a PG 15/16 cluster it would fail confusingly at assert.match(res.error, /pg_stat_statements/) (the column-not-exist error never mentions the extension) instead of pointing at the real bug. Keep the test's gate in lockstep with whichever handler fix lands.

  4. Branch hygiene -- this PR is stacked on test: gate pg_current_wal_lsn REVOKE behind POSTGRES_MCP_DESTRUCTIVE_TESTS=1 #15 (it carries 0e97683, the destructive-test-gate commit), so the diff-vs-main shows test: gate pg_current_wal_lsn REVOKE behind POSTGRES_MCP_DESTRUCTIVE_TESTS=1 #15's changes too. Mark it "depends on test: gate pg_current_wal_lsn REVOKE behind POSTGRES_MCP_DESTRUCTIVE_TESTS=1 #15" or rebase onto main after test: gate pg_current_wal_lsn REVOKE behind POSTGRES_MCP_DESTRUCTIVE_TESTS=1 #15 merges so it reviews in isolation.

  5. Minor -- the new description text uses and an em-dash where the repo's tool descriptions and comments are deliberately ASCII (mojibake-avoidance stance documented in release.sh); use >= and --.

🤖 Generated with Claude Code

HighviewOne and others added 3 commits June 11, 2026 18:55
- scripts/wsl-test-matrix.sh: add POSTGRES_MCP_DESTRUCTIVE_TESTS=1 so
  the _warnings-under-restricted-role block doesn't silently drop out
  of the PG matrix once the gate merges
- README.md: extend integration-test example with the new var and a
  note that it's safe only on a disposable cluster
- admin.integration.test.ts: replace bare { skip: bool } with a string
  reason so operators see how to opt in ("set POSTGRES_MCP_DESTRUCTIVE_TESTS=1
  (disposable cluster only)")

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tat_statements >= 1.10

pg_stat_statements 1.10 (Postgres 16) added shared_blk_read_time and
shared_blk_write_time, which separate IO-bound wait from CPU time. This
lets you distinguish "slow because disk" from "slow because CPU" -- the
classic gap in pg_stat_statements output before 1.10.

The columns are gated on a version check using the same compareVersions()
already used for the 1.8 column-rename detection. NULLIF(x, 0) converts
zero (meaning track_io_timing=off) to null so callers can tell "IO timing
disabled" from "query did no IO". Enabling track_io_timing in postgresql.conf
is documented in the tool description.

Adds an integration test that verifies the columns are present (null or
number) on installations with pg_stat_statements >= 1.10, and skips the
assertion on older versions without failing. Closes #11.
…I, NULLIF comment

Blocker fix:
- shared_blk_read_time / shared_blk_write_time were selected for
  pg_stat_statements >= 1.10, but those column names only exist in
  1.11 (PG 17). On 1.10 (PG 15) the correct names are blk_read_time /
  blk_write_time (unchanged semantics). Added hasSharedBlkCols gate at
  1.11; 1.10 clusters now get blk_read_time / blk_write_time instead of
  a hard "column does not exist" error.

Additional fixes:
- NULLIF comment corrected: NULLIF(x, 0) maps both "track_io_timing off"
  and "no IO recorded" to null -- the two cases are indistinguishable
  without querying current_setting(), not told apart by null vs 0
- description: replaced unicode >= and em-dash with ASCII; corrected
  "1.10 (Postgres 16)" to "1.10 (Postgres 15+)"
- integration test comments updated to describe the 1.10/1.11 branch and
  correct null semantics; gate and assertions unchanged (>= 1.10 is still
  the right cutoff for the feature being present)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@HighviewOne HighviewOne force-pushed the feat/pg-top-queries-io-timing branch from d482fb1 to 1080dd2 Compare June 12, 2026 02:03
@HighviewOne

Copy link
Copy Markdown
Collaborator Author

Addressed all review points. Blocker and branch hygiene resolved:

  1. Blocker fixed — Added hasSharedBlkCols gate at "1.11". Clusters on 1.10 (PG 15) now get blk_read_time / blk_write_time; clusters on >= 1.11 (PG 17) get shared_blk_read_time / shared_blk_write_time. Both exposed under the same io_*_time_ms output names.
  2. NULLIF comment — corrected; no longer claims the two null cases are distinguishable. Description now says "null = no IO time recorded (track_io_timing off, or the query did no measurable IO)".
  3. Integration test — comments updated to describe the 1.10/1.11 branch; gate stays at >= 1.10 (correct -- that's when the feature first appears).
  4. Branch hygiene — rebased onto the latest fix/destructive-test-gate (PR test: gate pg_current_wal_lsn REVOKE behind POSTGRES_MCP_DESTRUCTIVE_TESTS=1 #15); the stacked commit is gone, diff is now isolated to this feature.
  5. ASCII — replaced and em-dash with >= and -- throughout.

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.

pg_top_queries: expose shared_blk_read_time / shared_blk_write_time (IO vs CPU diagnostics)

2 participants