fix(server): extend --statement-timeout to COPY, Describe probes, and rewrite paths - #1197
Conversation
… 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>
Test Impact PlanDeterministic summary of how this PR changes tests, CI runners, and coverage-risk signals. Summary
Signals
Coverage risk: neutral or increased No coverage-reduction warnings detected. |
bill-ph
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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:901Timed-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:706COPY 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:410Describe 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:840Nested-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.
Follow-up to #1194. That PR wired
--statement-timeoutthrough 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
classifyErrorCodeis cancel-only after #1194 (correctly), so a deadline error maps toXX000. But most cancellation sites computederrCodefromclassifyErrorCodeand overrode only the message — so a timed-out statement reported PostgreSQL's timeout wording with SQLSTATEXX000(internal error) instead of57014(query_canceled). Drivers and ORMs match on the SQLSTATE to decide retryability; the wording is cosmetic. Every cancellation branch now forces57014(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
handleExecuteand 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:LIMIT 0bounds 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.executeMultiStatement,executeMultiStatementExtended) — the shape an incrementalINSERTwith 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, likeROLLBACK: they must still run after the deadline fires.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 STDINuses the no-monitor variant because it reads CopyData inline (a monitor'sPeekwould 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'sEXPLAINprobe is bounded by the timeout when configured (deliberately notqueryContext— it's validation, not statement execution, so no CancelRequest registration and noexecStartedmarking).Small plumbing:
Resolved.StatementTimeoutwas parsed from all three config sources but never populated intoResolved;RegisterQuerylazily initializes its map for bare fixtures;startDisconnectMonitorno-ops when a conn has no wire reader (test fixtures).Tests
server/statement_timeout_test.gogains 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-level57014+ 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/integrationgreen. (TestCatalogPsqlCommands/psql_dnis 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.mdbullet, whose staleisQueryCancelledclaim this also fixes).