Skip to content

Guard against non-explainable statements in the Query Plan panel - #385

Open
dpage wants to merge 3 commits into
mainfrom
fix/issue-368-explain-guard
Open

Guard against non-explainable statements in the Query Plan panel#385
dpage wants to merge 3 commits into
mainfrom
fix/issue-368-explain-guard

Conversation

@dpage

@dpage dpage commented Jul 29, 2026

Copy link
Copy Markdown
Member

Summary

  • useQueryPlan built its request by concatenating EXPLAIN with the
    captured statement text, so a Top Queries row holding a utility
    statement (a bare VACUUM from a scheduled maintenance job, a
    standalone ANALYZE, REINDEX, and so on) produced invalid SQL and
    the panel surfaced PostgreSQL's raw syntax error at or near "VACUUM"
    verbatim.
  • The hook now decides up front whether EXPLAIN can accept the
    statement and, when it cannot, sets an explanatory message without
    issuing the request at all. The check is an allowlist of the leading
    keywords EXPLAIN supports (SELECT, INSERT, UPDATE, DELETE,
    MERGE, VALUES, EXECUTE, DECLARE, WITH, CREATE TABLE ... AS
    and CREATE MATERIALIZED VIEW) rather than a denylist of utility
    commands, so it fails safe: anything unrecognised yields the friendly
    message instead of a raw server error, and there is no catalogue of
    utility commands to maintain as PostgreSQL grows new ones.
  • Leading whitespace, -- and /* */ comments, and opening parentheses
    are stripped before the first keyword is inspected, since captured
    pg_stat_statements text is often prefixed with a framework tag
    comment. CREATE TABLE ... AS SELECT stays explainable whilst a plain
    CREATE TABLE with a GENERATED ALWAYS AS column does not.

No component changes were needed: QueryPlanPanel already renders an
error with no accompanying plan as an info alert, so the message reads
as "not available" rather than as a failure, sitting alongside the
existing friendly handling of parameterised queries.

Test plan

  • npx vitest run src/hooks/__tests__/useQueryPlan.test.ts — 79
    passed (11 pre-existing plus 68 new). The new cases cover the helper
    directly across case, whitespace, comment, parenthesis, CTAS and
    materialised-view forms plus the utility statements from the issue,
    and cover the hook asserting that a non-explainable statement produces
    the message and that the API mock is never called.
  • npx vitest run --coverage on the modified hook — 97.91% lines
    (94/96), 97.82% branches; the two uncovered lines are pre-existing and
    every new line is covered, comfortably over the project's 90% floor.
  • Full client suite: 171 files, 3556 tests passed.
  • npm run lint — 0 errors, 40 warnings, all pre-existing
    non-null-assertion warnings across the test tree (confirmed by blame).
  • npm run build succeeds.

Closes #368

Summary by CodeRabbit

  • Bug Fixes
    • Improved Query Plan behavior for PostgreSQL statements that can’t be explained, showing a clear “query plans unavailable” message instead of surfacing a database error.
    • Avoids making plan requests for planless/non-explainable inputs and clears any stale plan output.
  • Tests
    • Expanded hook tests to cover explainability detection, short-circuiting, and correct handling of various EXPLAIN response shapes.
  • Documentation
    • Updated the Unreleased changelog with the Query Detail view Query Plan panel fix (issue #368).

pg_stat_statements records utility statements alongside SELECT and
DML, so a Top Queries row can legitimately hold text such as a bare
VACUUM from a scheduled maintenance job. useQueryPlan built its
request by concatenating EXPLAIN with that captured text, and since
PostgreSQL's EXPLAIN accepts only SELECT, INSERT, UPDATE, DELETE,
MERGE, VALUES, EXECUTE, DECLARE, CREATE TABLE AS and CREATE
MATERIALIZED VIEW, the server rejected the result as invalid SQL and
the panel surfaced the raw syntax error verbatim.

Decide up front whether EXPLAIN can accept the statement, and if it
cannot, set an explanatory message without issuing the request at
all. The check is an allowlist of the leading keywords EXPLAIN
supports rather than a denylist of utility commands, so it fails
safe: anything unrecognised yields the friendly message instead of a
raw server error, and there is no list of every utility command to
keep up to date as PostgreSQL grows new ones. Leading whitespace,
comments and parentheses are stripped before the first keyword is
inspected, because captured text is often prefixed with a framework
tag comment.

The Query Plan panel already renders an error with no accompanying
plan as an informational alert, so the message reads as "not
available" rather than as a failure, alongside the existing friendly
handling of parameterised queries.

Closes #368
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b0880180-fcfe-4813-8532-d19a3aa427e7

📥 Commits

Reviewing files that changed from the base of the PR and between 56d846e and ad0d1be.

📒 Files selected for processing (3)
  • client/src/hooks/__tests__/useQueryPlan.test.ts
  • client/src/hooks/useQueryPlan.ts
  • docs/changelog.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/changelog.md

Walkthrough

useQueryPlan now classifies SQL before fetching plans, distinguishes unsupported and planless statements, validates JSON plan responses, and adds comprehensive tests plus changelog documentation.

Changes

Query plan explainability

Layer / File(s) Summary
Statement classification contract
client/src/hooks/useQueryPlan.ts, client/src/hooks/__tests__/useQueryPlan.test.ts
Adds exported classification types and messages, strips leading SQL noise, and covers plan, planless, unsupported, empty, and null-ish inputs.
Hook preflight and plan extraction
client/src/hooks/useQueryPlan.ts
Skips cache and EXPLAIN requests for non-plan statements, clears plan state, sets specific errors, and validates JSON plan structures.
Behavior coverage and documentation
client/src/hooks/__tests__/useQueryPlan.test.ts, docs/changelog.md
Tests supported statements, short-circuit behavior, JSON response shapes, and documents the Query Plan panel fix.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant useQueryPlan
  participant classifyExplainSupport
  participant PostgreSQL
  useQueryPlan->>classifyExplainSupport: classify SQL query
  alt planless or unsupported
    classifyExplainSupport-->>useQueryPlan: return classification
    useQueryPlan->>useQueryPlan: clear plans and set error
  else plan
    classifyExplainSupport-->>useQueryPlan: return plan
    useQueryPlan->>PostgreSQL: execute text and JSON EXPLAIN
    PostgreSQL-->>useQueryPlan: return plan responses
  end
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: guarding the Query Plan panel against non-explainable statements.
Linked Issues check ✅ Passed The PR prevents EXPLAIN from running on utility statements and shows a friendly unavailable message, matching issue #368.
Out of Scope Changes check ✅ Passed The added planless handling, JSON plan validation, tests, and changelog entry all support the stated Query Plan fix.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-368-explain-guard

Comment @coderabbitai help to get the list of available commands.

Record the #368 fix under Unreleased/Fixed, describing what users
now see in place of the raw database syntax error.
@codacy-production

codacy-production Bot commented Jul 29, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 30 complexity

Metric Results
Complexity 30

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

PostgreSQL's ExplainableStmt covers two forms the first version of
the guard turned away. A bare TABLE foo is a SELECT in disguise and
plans exactly like one, so suppressing it lost the user a plan they
could have had. REFRESH MATERIALIZED VIEW is accepted by EXPLAIN too,
although it runs as a utility command, so the server answers with its
internal "Utility statements have no plan structure" notice rather
than a plan, and in FORMAT JSON it answers with the bare string array
[ "Utility Statement" ]. Both behaviours were confirmed against
PostgreSQL 18.

Classify statements three ways rather than two: EXPLAIN returns a
plan, EXPLAIN is accepted but returns no plan, or EXPLAIN rejects the
statement outright. TABLE joins the leading keywords that yield a
plan, whilst the planless case gets a message of its own explaining
that PostgreSQL accepts the statement but produces no plan for it.
Deciding this from the statement text keeps the property the fix
rests on, that no request is issued which cannot usefully succeed,
and avoids inferring statically known facts by pattern-matching
server prose that is not a stable contract across versions.

Shape-check the FORMAT JSON response as well, so that a planless
reply reaching the parser reports no plan structure instead of
assigning an array of bare strings into the plan state, where the
truthy value would have sent the panel to its plan tabs and asked the
tree renderer to draw a string as a plan node.

Closes #368
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.

Query Plan panel shows raw syntax error for non-explainable statements (VACUUM, ANALYZE, etc.)

1 participant