Skip to content

fix(server): extend --statement-timeout to COPY, Describe probes, and rewrite paths - #1197

Merged
fuziontech merged 1 commit into
mainfrom
fix/statement-timeout-copy-and-probes
Sep 17, 2026
Merged

fuziontech merged 1 commit into
mainfrom
fix/statement-timeout-copy-and-probes

Conversation

@fuziontech

Copy link
Copy Markdown
Member

Follow-up to #1194. That PR wired --statement-timeout through extended-protocol Execute; this one closes the paths it left context-less, and fixes a SQLSTATE bug in the merged classification.

The bug in the merged classification

classifyErrorCode is cancel-only after #1194 (correctly), so a deadline error maps to XX000. But most cancellation sites computed errCode from classifyErrorCode and overrode only the message — so a timed-out statement reported PostgreSQL's timeout wording with SQLSTATE XX000 (internal error) instead of 57014 (query_canceled). Drivers and ORMs match on the SQLSTATE to decide retryability; the wording is cosmetic. Every cancellation branch now forces 57014 (Exec/Query initial-error paths on simple, batched, and extended, plus the secret-DDL site; the rows.Err paths already forced it).

Caught by driving a wedging fake executor through a real handleExecute and reading the wire bytes — the deadline-presence test added in #1194 can't see this.

The remaining context-less paths

Each of these ran the engine on context.Background() — no timeout, and no CancelRequest registration:

  • Describe schema probes (extended protocol, statement + portal). They really execute the statement (LIMIT 0 bounds rows, not planning or side effects — see the DML-RETURNING carve-out), so a statement that wedges in the engine wedges Describe too. Now under a statement context, same no-monitor variant as Execute.
  • Writable-CTE rewrite paths on both protocols (executeMultiStatement, executeMultiStatementExtended) — the shape an incremental INSERT with CTEs compiles to. The extended one now shares the Execute's statement context (created above the branch); the simple one takes its own. Cleanup statements deliberately stay context-less, like ROLLBACK: they must still run after the deadline fires.
  • COPY, all variants: TO STDOUT's driving query, FROM STDIN's schema probe + spooled load + remote-streaming + batch-insert fallbacks, and passthrough file COPY. One context per COPY statement; FROM STDIN uses the no-monitor variant because it reads CopyData inline (a monitor's Peek would race the read loop). The timeout bounds the COPY end to end including the load; a CancelRequest now reaches the probe and the load, though it cannot interrupt a blocked wire read (unchanged).
  • validateWithDuckDB's EXPLAIN probe is bounded by the timeout when configured (deliberately not queryContext — it's validation, not statement execution, so no CancelRequest registration and no execStarted marking).

Small plumbing: Resolved.StatementTimeout was parsed from all three config sources but never populated into Resolved; RegisterQuery lazily initializes its map for bare fixtures; startDisconnectMonitor no-ops when a conn has no wire reader (test fixtures).

Tests

server/statement_timeout_test.go gains handler-level coverage with a fake executor that fails fast on any context-less call, so a bypassing path can never pass green by hanging: wedged extended Execute (both branches) → wire-level 57014 + exact wording; Describe probes carry a deadline; both rewrite paths bounded; COPY out + in bounded; a suspended portal's context survives suspension and is torn down at completion; a cursor's lifetime timeout classifies correctly after intervening statements.

Full go test ./... green; tests/integration green. (TestCatalogPsqlCommands/psql_dn is order-sensitive against a reused local compose stack — fails on a dirty stack with or without this change, passes fresh; not touched here.)

No mw-dev harness assertion, unchanged from #1194's documented reasoning (server-global knob; see the tests/mw-dev/README.md bullet, whose stale isQueryCancelled claim this also fixes).

… rewrite paths

Follow-up to #1194, which wired the timeout through extended-protocol
Execute. Wire-level tests driving the real handlers with a wedging fake
executor (one that fails fast on any context-less call, so a bypassing
path can never pass green by hanging) found the remaining gaps:

- A timed-out statement on the main Exec/Query error paths got the
  timeout MESSAGE but SQLSTATE XX000, not 57014: those sites computed
  errCode from classifyErrorCode (cancel-only after #1194, so a deadline
  maps to XX000) and overrode only the message. Drivers match on the
  SQLSTATE, so the wedge containment worked but reported the wrong class.
  Every cancellation branch now forces 57014. Same fix at the secret-DDL
  site.
- The extended-protocol Describe probes execute the statement at LIMIT 0;
  they now run under a statement context (deadline + CancelRequest
  registration, no disconnect monitor, matching Execute).
- The writable-CTE rewrite paths on BOTH protocols (executeMultiStatement,
  executeMultiStatementExtended — the shape an incremental INSERT with
  CTEs compiles to) ran context-less; they now share/own a statement
  context. Cleanup statements deliberately stay context-less, like
  ROLLBACK: they must run after the deadline fires.
- COPY in all variants (TO STDOUT driving query, FROM STDIN probe + spool
  load + remote streaming + batch-insert fallbacks, passthrough file COPY)
  ran context-less; each now runs under one statement context. COPY FROM
  STDIN uses the no-monitor variant because it reads the wire inline. Side
  effect: COPY now registers for CancelRequest.
- Resolved.StatementTimeout was parsed but never populated.
- RegisterQuery lazily initializes its map for bare test fixtures.
- validateWithDuckDB's EXPLAIN probe is bounded by the timeout when set.
- startDisconnectMonitor no-ops without a wire conn/reader (fixtures).
- tests/mw-dev README bullet still claimed isQueryCancelled matches
  "context deadline exceeded"; the merged code is cancel-only. Fixed, and
  the coverage list updated. CLAUDE.md bullet updated likewise.

Tests: server/statement_timeout_test.go now drives handleExecute,
handleDescribe, executeMultiStatement(Extended), handleFetchCursor, and
handleCopyIn/Out end to end, asserting wire-level 57014 + wording, that
a suspended portal's context survives suspension and is torn down at
completion, and that a cursor lifetime timeout classifies correctly after
intervening statements. Full go test ./... green; integration suite green
(TestCatalogPsqlCommands/psql_dn is order-sensitive against a reused local
compose stack; passes fresh, unrelated to this change).

Co-authored-by: Shelley <shelley@exe.dev>
@fuziontech
fuziontech requested a review from a team September 17, 2026 18: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 5 0
E2E/journey files 0 0 0
Workflow files 0 0 0

Signals

  • Test cases: +9 / -0
  • Assertions: +49 / -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.

@bill-ph bill-ph left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed commit e82c9ec. This follow-up routes Describe, COPY, writable-CTE rewrites, validation probes, and extended execution through statement-aware contexts, fixes timeout SQLSTATE classification to 57014, and adds broad handler-level regression coverage. The changes preserve cleanup behavior and correctly keep suspended portal/cursor context lifetimes. No P0 blockers found.

Non-blocking notes: COPY FROM STDIN still cannot interrupt a blocked wire read, as documented, and the cursor/portal lifetime timeout semantics are intentionally stricter than PostgreSQL.

— Robo Bill

@benben benben left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review generated on behalf of @benben.

Approve. Routes the Describe probes, both writable-CTE rewrite paths, and every COPY variant through the statement context, and forces SQLSTATE 57014 at each cancellation site. The wiring is correct on every path I traced; the remaining issues are edge-case classification and lifecycle details, not correctness of the stated fix.

  • minor server/conn.go:901 Timed-out EXPLAIN probe surfaces as 42601/XX000, not 57014. Wedged engine during validateWithDuckDB on the native-DuckDB fallback path (extended Parse wraps it as 'syntax error: context deadline exceeded').
  • minor server/conn_copy.go:706 COPY FROM STDIN deadline expires unobserved during wire receive. Spooled/binary/CSV-blob paths read all CopyData first; a slow upload longer than the timeout is fully received, then the load fails 57014 immediately.
  • minor server/conn_extended_query.go:410 Describe probe clobbers a suspended portal's cancel registration. Suspended portal open, then Describe of another statement: RegisterQuery overwrites and probeCleanup deletes the key, leaving the portal uncancellable by CancelRequest.
  • nit server/conn_extended_query.go:840 Nested-BEGIN warning path now registers a query and marks exec started. BEGIN inside an open transaction via extended protocol returns at line 867 after queryContextInner already ran.

Model: fable, PR authored with cursor.

@fuziontech
fuziontech merged commit b1a2665 into main Sep 17, 2026
35 checks passed
@fuziontech
fuziontech deleted the fix/statement-timeout-copy-and-probes branch September 17, 2026 19:17
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.

3 participants