Skip to content

fix: wrap explain_query in the Raw { engine, format, payload, ... } shape (#89) - #111

Merged
aesslinger merged 1 commit into
mainfrom
fix/89-explain-query-raw-wrapper
Sep 17, 2026
Merged

aesslinger merged 1 commit into
mainfrom
fix/89-explain-query-raw-wrapper

Conversation

@aesslinger

Copy link
Copy Markdown
Collaborator

Summary

  • explain_query returned the raw EXPLAIN JSON value directly, with a comment acknowledging the host would fall through to ExplainQueryOutput::Plan { plan: res } instead of the Raw variant the builtin driver emits.
  • The host's plugin adapter (tabularis plugins/driver.rs::explain_query) only classifies a response as Raw when it finds engine/format/payload as strings (via .as_str()) plus an optional original_query — any other shape, including a bare JSON array, falls through to Plan. Runtime-registered EXPLAIN parsers select on engine+format, so this plugin's output was never selectable by them, and the parsed-plan renderer ran instead of the raw-payload path the builtin uses.
  • Wrapped the plan JSON in the exact shape the adapter checks: { engine: "postgres", format: "postgres-json", payload: <stringified JSON>, original_query: <the query> }. payload must be the JSON string form, not the live JSON value — the adapter reads it with object.get("payload")?.as_str(), which silently returns None (not an error) for a nested object/array, dropping the response into the Plan fallback exactly like the pre-fix bare-array response did.

Fixes #89.

Test plan

  • TDD: added raw_explain_output unit tests to query_tests.rs, including one that specifically checks payload is Value::String (not a nested value) to catch that exact silent-fallback failure mode. Confirmed the new tests fail to even compile against the pre-fix code (the function didn't exist yet) before the fix.
  • cargo test --lib — 327 passed
  • cargo clippy --all-targets -- -D warnings — clean
  • cargo fmt --all -- --check — clean
  • Verified live against a real PostgreSQL instance by simulating the host adapter's exact matching logic (object.get(field)?.as_str() for each of engine/format/payload) against real explain_query responses:
    • Confirmed the pre-fix binary's bare-array response is not classified as Raw (falls through to Plan, reproducing the bug)
    • Confirmed the post-fix binary's response is classified as Raw, with engine/format/original_query correct and payload containing a real EXPLAIN plan
    • Confirmed this holds both without and with analyze=true, and that real execution stats ("Actual Rows") come through correctly in the ANALYZE case

Note for tabularis (not part of this PR)

While reviewing the cross-repo parity suite (src-tauri/tests/postgres_integration/parity_explain.rs), noticed its top-of-file comment says: "ExplainQueryOutput differs structurally between built-in (Raw variant) and plugin (Plan variant)" — documenting the exact divergence this PR fixes. The tests themselves only assert Ok (not the response shape), so they'll keep passing either way, but that comment is now stale once this merges. Flagging for awareness; not something to fix in this repo.

…hape (#89)

explain_query returned the raw EXPLAIN JSON value directly, with a
comment acknowledging the host would fall through to
ExplainQueryOutput::Plan { plan: res } instead of the Raw variant the
builtin driver emits. The host's plugin adapter only classifies a
response as Raw when it finds engine/format/payload as strings (via
.as_str()) plus an optional original_query -- any other shape,
including a bare JSON array, falls through to Plan. Runtime-registered
EXPLAIN parsers select on engine+format, so this plugin's output was
never selectable by them, and the parsed-plan renderer ran instead of
the raw-payload path the builtin uses.

Wrapped the plan JSON in the exact shape the adapter checks:
{ engine: "postgres", format: "postgres-json", payload: <stringified
JSON>, original_query: <the query> }. payload must be the JSON
*string* form, not the live JSON value -- the adapter reads it with
object.get("payload")?.as_str(), which silently returns None (not an
error) for a nested object/array, dropping the response into the Plan
fallback exactly like the pre-fix bare-array response did.

TDD: added raw_explain_output unit tests to query_tests.rs, including
one that specifically checks payload is Value::String (not a nested
value) to catch that exact silent-fallback failure mode. Confirmed the
new tests fail to even compile against the pre-fix code (the function
didn't exist yet) before the fix.

Verified live against a real PostgreSQL instance by simulating the
host adapter's exact matching logic (object.get(field)?.as_str() for
each of engine/format/payload) against real explain_query responses:
confirmed the pre-fix binary's bare-array response is NOT classified
as Raw (falls through to Plan, reproducing the bug), and the post-fix
binary's response IS classified as Raw, with engine/format/
original_query correct and payload containing a real EXPLAIN plan
(both without and with analyze=true, confirming actual execution
stats like "Actual Rows" come through correctly in the ANALYZE case).
@aesslinger aesslinger added the prerelease:rc Version suggestion targets a release candidate label Sep 17, 2026
@github-actions

Copy link
Copy Markdown

Version suggestion

Based on this PR's title (fix) and the prerelease:rc label:

Current 1.0.0-rc.3
Suggested next tag v1.0.0-rc.4

This is informational only — no tag or release is created automatically yet.

@aesslinger aesslinger self-assigned this Sep 17, 2026
@aesslinger
aesslinger merged commit 9821fa9 into main Sep 17, 2026
14 checks passed
@aesslinger
aesslinger deleted the fix/89-explain-query-raw-wrapper branch September 17, 2026 11:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

prerelease:rc Version suggestion targets a release candidate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

explain_query returns raw JSON instead of the Raw { engine, format, payload, original_query } wrapper the builtin emits

1 participant