@@ -51,7 +51,12 @@ import ResultSlicer from '../result/ResultSlicer';
5151import SeaResultsProvider from './SeaResultsProvider' ;
5252import { arrowSchemaToThriftSchema , decodeIpcSchema , patchIpcBytes } from './SeaArrowIpc' ;
5353import { decodeNapiKernelError } from './SeaErrorMapping' ;
54- import { SeaStatement , SeaNativeAsyncStatement , SeaNativeAsyncResultHandle } from './SeaNativeLoader' ;
54+ import {
55+ SeaStatement ,
56+ SeaNativeAsyncStatement ,
57+ SeaNativeAsyncResultHandle ,
58+ SeaNativeCancellableExecution ,
59+ } from './SeaNativeLoader' ;
5560import {
5661 SeaStatementHandle ,
5762 SeaOperationLifecycleState ,
@@ -116,6 +121,15 @@ export interface SeaOperationBackendOptions {
116121 asyncStatement ?: SeaNativeAsyncStatement ;
117122 /** The terminal napi `Statement` from a metadata call. */
118123 statement ?: SeaOperationStatement ;
124+ /**
125+ * The pending napi `CancellableExecution` from
126+ * `Connection.executeStatementCancellable(...)` — the sync (`runAsync: false`)
127+ * query path. `result()` drives the blocking `execute()` to a terminal
128+ * `Statement` (the fetch handle); `cancel()` fires a detached canceller that
129+ * interrupts a still-running `result()` mid-COMPUTE. Exactly one of
130+ * `asyncStatement`, `statement`, or `cancellableExecution` must be set.
131+ */
132+ cancellableExecution ?: SeaNativeCancellableExecution ;
119133 context : IClientContext ;
120134 /**
121135 * Optional override for `id`. Defaults to the napi statement-id when the
@@ -134,15 +148,23 @@ export interface SeaOperationBackendOptions {
134148}
135149
136150export default class SeaOperationBackend implements IOperationBackend {
137- // Query path: pending async statement we poll to terminal. Undefined on the
138- // metadata path .
151+ // Async query path: pending async statement we poll to terminal. Undefined on
152+ // the metadata / sync-execute paths .
139153 private readonly asyncStatement ?: SeaNativeAsyncStatement ;
140154
141- // Metadata path: terminal statement. Undefined on the query path.
142- private readonly blockingStatement ?: SeaOperationStatement ;
155+ // Sync query path (`runAsync: false`): pending cancellable execution whose
156+ // `result()` drives the blocking `execute()` to a terminal `Statement`.
157+ // Undefined on the async / metadata paths.
158+ private readonly cancellableExecution ?: SeaNativeCancellableExecution ;
159+
160+ // Metadata path: terminal statement. Also the resolved fetch handle on the
161+ // sync-execute path once `cancellableExecution.result()` settles.
162+ private blockingStatement ?: SeaOperationStatement ;
143163
144164 // The cancel/close surface — whichever handle backs this operation. Both
145- // `AsyncStatement` and `Statement` expose `cancel()` / `close()`.
165+ // `AsyncStatement` and `Statement` expose `cancel()` / `close()`; the
166+ // sync-execute path uses a composite that routes `cancel()` to the
167+ // cancellable execution (mid-compute) and `close()` to the resolved statement.
146168 private readonly lifecycleHandle : SeaStatementHandle ;
147169
148170 private readonly context : IClientContext ;
@@ -168,15 +190,43 @@ export default class SeaOperationBackend implements IOperationBackend {
168190 // undefined when unset. Enforced in the async poll loop.
169191 private readonly queryTimeoutMs ?: number ;
170192
171- constructor ( { asyncStatement, statement, context, id, queryTimeoutSecs } : SeaOperationBackendOptions ) {
172- if ( ( asyncStatement === undefined ) === ( statement === undefined ) ) {
173- throw new HiveDriverError ( 'SeaOperationBackend: exactly one of `asyncStatement` or `statement` must be provided' ) ;
193+ constructor ( {
194+ asyncStatement,
195+ statement,
196+ cancellableExecution,
197+ context,
198+ id,
199+ queryTimeoutSecs,
200+ } : SeaOperationBackendOptions ) {
201+ // Exactly one of the three handle kinds must be supplied.
202+ const providedCount =
203+ ( asyncStatement !== undefined ? 1 : 0 ) +
204+ ( statement !== undefined ? 1 : 0 ) +
205+ ( cancellableExecution !== undefined ? 1 : 0 ) ;
206+ if ( providedCount !== 1 ) {
207+ throw new HiveDriverError (
208+ 'SeaOperationBackend: exactly one of `asyncStatement`, `statement`, or `cancellableExecution` must be provided' ,
209+ ) ;
174210 }
175211 this . asyncStatement = asyncStatement ;
212+ this . cancellableExecution = cancellableExecution ;
176213 this . blockingStatement = statement ;
177- this . lifecycleHandle = ( asyncStatement ?? statement ) as SeaStatementHandle ;
214+ // Lifecycle surface. The async/metadata handles expose both cancel/close.
215+ // The sync-execute path uses a composite: `cancel()` always routes to the
216+ // cancellable execution (lock-free, interrupts a running `result()`
217+ // mid-compute and is a no-op once terminal); `close()` routes to the
218+ // resolved terminal statement once `result()` has produced it (before that
219+ // there is nothing server-side to close, and the kernel's per-execute drop
220+ // guard handles an abandoned in-flight execution).
221+ this . lifecycleHandle = cancellableExecution
222+ ? {
223+ cancel : ( ) => cancellableExecution . cancel ( ) ,
224+ close : ( ) => ( this . blockingStatement ? this . blockingStatement . close ( ) : Promise . resolve ( ) ) ,
225+ }
226+ : ( ( asyncStatement ?? statement ) as SeaStatementHandle ) ;
178227 this . context = context ;
179- this . _id = id ?? asyncStatement ?. statementId ?? statement ?. statementId ?? uuidv4 ( ) ;
228+ this . _id =
229+ id ?? asyncStatement ?. statementId ?? statement ?. statementId ?? cancellableExecution ?. statementId ?? uuidv4 ( ) ;
180230 this . queryTimeoutMs = queryTimeoutSecs !== undefined && queryTimeoutSecs > 0 ? queryTimeoutSecs * 1000 : undefined ;
181231 }
182232
@@ -312,11 +362,20 @@ export default class SeaOperationBackend implements IOperationBackend {
312362 return { state : OperationState . Closed , hasResultSet : true } ;
313363 }
314364 if ( this . asyncStatement ) {
315- // Query path: report the real kernel state (single GetStatementStatus
316- // RPC — no polling here; `waitUntilReady` owns the poll loop).
365+ // Async query path: report the real kernel state (single
366+ // GetStatementStatus RPC — no polling here; `waitUntilReady` owns the
367+ // poll loop).
317368 const state = statusStringToOperationState ( await this . asyncStatement . status ( ) ) ;
318369 return { state, hasResultSet : true } ;
319370 }
371+ if ( this . cancellableExecution ) {
372+ // Sync (`runAsync: false`) path: the kernel `execute()` blocks and polls
373+ // server-side; there is no per-status RPC to query while it runs. Report
374+ // Running until `result()` has materialised the terminal statement, then
375+ // Succeeded — mirroring the kernel's blocking-then-terminal lifecycle.
376+ const state = this . fetchHandlePromise ? OperationState . Succeeded : OperationState . Running ;
377+ return { state, hasResultSet : true } ;
378+ }
320379 // Metadata path: the kernel statement is already terminal.
321380 return { state : OperationState . Succeeded , hasResultSet : true } ;
322381 }
@@ -325,6 +384,9 @@ export default class SeaOperationBackend implements IOperationBackend {
325384 if ( this . asyncStatement ) {
326385 return this . waitUntilReadyAsync ( options ) ;
327386 }
387+ if ( this . cancellableExecution ) {
388+ return this . waitUntilReadyCancellable ( options ) ;
389+ }
328390 // Metadata path: the kernel statement has already resolved, so there is
329391 // nothing to poll. seaFinished fires the progress callback once with a
330392 // synthesised completion tick, matching the Thrift path's final tick.
@@ -420,6 +482,36 @@ export default class SeaOperationBackend implements IOperationBackend {
420482 }
421483 }
422484
485+ /**
486+ * Sync (`runAsync: false`) execute path. Drives the blocking
487+ * `CancellableExecution.result()` to a terminal `Statement` (the kernel polls
488+ * to completion server-side, honouring `queryTimeoutSecs` on this path). The
489+ * await is interruptible: a JS-initiated `cancel()` fires the detached
490+ * canceller, the server flips the statement terminal, and the parked
491+ * `result()` rejects with `Cancelled` — which we map to the typed
492+ * `OperationStateError(Canceled)`.
493+ *
494+ * Unlike the async path there is no status poll loop (the kernel owns
495+ * polling), so the progress callback fires once on completion, matching the
496+ * metadata path's single completion tick.
497+ */
498+ private async waitUntilReadyCancellable ( options ?: IOperationBackendWaitOptions ) : Promise < void > {
499+ // Already materialised → terminal-and-ready, nothing to wait for.
500+ if ( this . fetchHandlePromise ) {
501+ return ;
502+ }
503+ // A JS-initiated cancel/close before we start short-circuits to the typed
504+ // state error rather than dispatching the blocking execute.
505+ failIfNotActive ( this . lifecycle ) ;
506+ // `getFetchHandle()` drives `result()` and memoises the resolved Statement
507+ // (also stored on `blockingStatement` so `close()` can reach it).
508+ await this . getFetchHandle ( ) ;
509+ // Single completion tick, matching the metadata path.
510+ if ( options ?. callback ) {
511+ await Promise . resolve ( options . callback ( { state : OperationState . Succeeded , hasResultSet : true } ) ) ;
512+ }
513+ }
514+
423515 /**
424516 * Drive `awaitResult()` on a Failed statement to surface the kernel's typed
425517 * SQL-error envelope. Falls back to a generic error if `awaitResult()`
@@ -444,6 +536,29 @@ export default class SeaOperationBackend implements IOperationBackend {
444536 this . fetchHandlePromise = this . asyncStatement . awaitResult ( ) . catch ( ( err ) => {
445537 throw decodeNapiKernelError ( err ) ;
446538 } ) as Promise < SeaNativeAsyncResultHandle > ;
539+ } else if ( this . cancellableExecution ) {
540+ // Sync (`runAsync: false`) path: drive the blocking `result()` to the
541+ // terminal `Statement`. Store it on `blockingStatement` so `close()` can
542+ // reach it post-execute, and so a subsequent fetch uses it directly.
543+ this . fetchHandlePromise = this . cancellableExecution
544+ . result ( )
545+ . then ( ( stmt ) => {
546+ this . blockingStatement = stmt as unknown as SeaOperationStatement ;
547+ return stmt as unknown as SeaFetchHandle ;
548+ } )
549+ . catch ( ( err ) => {
550+ const mapped = decodeNapiKernelError ( err ) ;
551+ // A cancel-induced rejection surfaces as the kernel's Cancelled
552+ // error; map it to the typed `OperationStateError(Canceled)` so the
553+ // `DBSQLOperation` facade mirrors its cancelled flag (it only does so
554+ // for `OperationStateError`), matching the Thrift path. If the
555+ // operation was cancelled client-side, prefer the typed code
556+ // regardless of the kernel error text.
557+ if ( this . lifecycle . isCancelled ) {
558+ throw new OperationStateError ( OperationStateErrorCode . Canceled ) ;
559+ }
560+ throw mapped ;
561+ } ) ;
447562 } else {
448563 const stmt = this . blockingStatement ! ;
449564 if ( ! stmt . fetchNextBatch ) {
0 commit comments