Skip to content

fix(querylog): correct the ExceptionBeforeStart boundary, classify CALL - #1014

Merged
fuziontech merged 1 commit into
mainfrom
querylog-exception-semantics
Jul 30, 2026
Merged

fix(querylog): correct the ExceptionBeforeStart boundary, classify CALL#1014
fuziontech merged 1 commit into
mainfrom
querylog-exception-semantics

Conversation

@fuziontech

Copy link
Copy Markdown
Member

Two fixes, both found by reading query_log on prod tenants after #1002 rolled out. No behaviour change to the event model itself; one doc correction and one extraction gap.

1. ExceptionBeforeStart was documented wrong

#1002 described the class as "failed before any engine saw it". That's inaccurate, and not marginally — it's wrong for the largest population of the class.

Sampling one prod tenant over 24h: all 92 ExceptionBeforeStart events are protocol=extended carrying flight execute: rpc error … failed to prepare query: Binder Error. That's the extended-protocol Describe path, which hands the statement to a worker to learn its result schema. The engine does see the SQL — it just never runs it.

The boundary the code actually implements is "execution began" (execStarted, set at queryContextInner), which is correct and is the same line ClickHouse draws: analysis-time failures are ExceptionBeforeStart. So the classification was right; only the words around it were wrong. Left as-is, "never reached a worker" would send triage looking in the wrong place for the most common case.

Corrected in query_event.go, query_metrics.go, querylog.go, README.md, and the design doc, each stating the boundary and naming the Describe case explicitly.

Prod also confirms the paired invariant holds: 0 of 92 ExceptionBeforeStart rows have a QueryStart.

2. CALL was classified as an incomplete extraction

querymeta had no case for CallStmt, so it fell to the default branch: access_kinds=unknown with metadata_complete=false. Four occurrences over 7 days on the sampled tenant.

unknown was right. incomplete was not. A procedure body is invisible to any parser, so:

  • "parsed, but its access is opaque" → complete extraction, unknown access
  • "we could not parse it" → incomplete extraction

Both deny under a future gate, but only the second is fixable by a better parser — and conflating them turns an allowlistable procedure call into what looks like a parser bug. CALL is now handled explicitly, and the procedure name is recorded so a policy can allowlist specific procedures rather than refusing CALL wholesale.

Verification

Verified the chain before writing a hard e2e assertion around it: pg_query parses CALL pragma_version() as CallStmt, querymeta returns kinds=[unknown] complete=true funcs=[pragma_version], the transpiler passes it through unchanged, and DuckDB executes it. So the harness gets a real assertion, not a tolerant skip.

  • Unit: TestExtractCallStatement, TestUnknownAccessDistinguishesOpaqueFromUnparseable (pins opaque-vs-unparseable as distinct facts).
  • e2e (query_log_access_metadata): a CALL must log access_kinds=unknown with metadata_complete=true.
  • Full ./server/... suite green; golangci-lint v2.11.4 clean on ./server/....

Prod observation, not addressed here

Those binder errors are pre-existing, not a regression: the 24h window before the #1002 rollout has 94 identical XX000 errors (labelled ExceptionWhileProcessing under the old blanket rule); after, 92 labelled ExceptionBeforeStart. Same volume — the new classification just made them legible. Some extended-protocol client with no application_name is sending SQL DuckDB can't bind, ~92×/day on one tenant. Worth tracing separately.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FQWUvX5Hkvx2mRhCg8JECv

Both findings come from reading the query log on prod tenants after the
rollout.

ExceptionBeforeStart was documented as "failed before any engine saw it".
That is wrong, and not marginally: every one of the ~92/day
ExceptionBeforeStart events on the tenant I sampled is an
extended-protocol Describe whose prepare the worker rejected with a
binder error. Describe hands the statement to a worker to learn its
result schema, so the engine does see it — it just never runs. The
boundary the code actually implements is "execution began" (execStarted,
set at queryContextInner), which is both correct and the same line
ClickHouse draws: analysis-time failures are ExceptionBeforeStart. Since
this is the LARGEST source of the class, "never reached a worker" would
have sent triage the wrong way. No behaviour change — the classification
was right, the words around it were not. Verified against prod: 0 of 92
have a paired QueryStart, so the "no start event" invariant holds.

querymeta had no case for CALL, so it fell to the default branch and
came back access_kinds=unknown WITH metadata_complete=false. Unknown was
right; incomplete was not. A procedure body is invisible to any parser,
so "parsed, but its access is opaque" is a different fact from "we could
not parse it" — both deny, but only the second is fixable by a better
parser, and conflating them turns an allowlistable procedure call into a
parser bug report. CALL is now handled explicitly: unknown access,
complete extraction, and the procedure name recorded so a policy can
allowlist specific procedures instead of refusing CALL wholesale.

Verified the whole chain before asserting on it: pg_query parses CALL as
CallStmt, the transpiler passes it through unchanged, and DuckDB executes
it — so the e2e assertion is a hard one, not a tolerant skip.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FQWUvX5Hkvx2mRhCg8JECv
@fuziontech
fuziontech requested a review from a team July 30, 2026 09:20
@github-actions

Copy link
Copy Markdown

Test Impact Plan

Deterministic summary of how this PR changes tests, CI runners, and coverage-risk signals.

Summary

Area Added Changed Deleted
Test files 0 4 0
E2E/journey files 0 0 0
Workflow files 0 0 0

Signals

  • Test cases: +2 / -0
  • Assertions: +9 / -0
  • Skips or known failures added: 0
  • Workflow continue-on-error added: 0
  • Workflow path filters added: 0
  • Test commands removed from justfile: 0
  • E2E/journey retry lines added: 0

Coverage risk: neutral or increased

No coverage-reduction warnings detected.

@fuziontech
fuziontech merged commit 8fe6d44 into main Jul 30, 2026
22 checks passed
@fuziontech
fuziontech deleted the querylog-exception-semantics branch July 30, 2026 09:23
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.

2 participants