import IOperationBackend, { IOperationBackendWaitOptions } from '../contracts/IOperationBackend'; import { OperationStatus } from '../contracts/OperationStatus'; import { ResultMetadata } from '../contracts/ResultMetadata'; import IClientContext from '../contracts/IClientContext'; import Status from '../dto/Status'; import { KernelStatement, KernelNativeAsyncStatement, KernelNativeCancellableExecution } from './KernelNativeLoader'; import { KernelStatementHandle } from './KernelOperationLifecycle'; /** * Structural union of the lifecycle surface (cancel/close) and the * fetch surface (fetchNextBatch/schema). The real napi `Statement` * implements both; lifecycle-only test stubs implement only the * cancel/close half — fetch methods are accessed lazily and the * lifecycle tests never reach that path. */ export type KernelOperationStatement = KernelStatementHandle & Partial; /** * Constructor options for `KernelOperationBackend`. Exactly one of * `asyncStatement` (query path — `Connection.submitStatement`) or `statement` * (metadata path — `Connection.list*` / `get*`, already terminal) must be set. */ export interface KernelOperationBackendOptions { /** The pending napi `AsyncStatement` from `Connection.submitStatement(...)`. */ asyncStatement?: KernelNativeAsyncStatement; /** The terminal napi `Statement` from a metadata call. */ statement?: KernelOperationStatement; /** * The pending napi `CancellableExecution` from * `Connection.executeStatementCancellable(...)` — the sync (`runAsync: false`) * query path. `result()` drives the blocking `execute()` to a terminal * `Statement` (the fetch handle); `cancel()` fires a detached canceller that * interrupts a still-running `result()` mid-COMPUTE. Exactly one of * `asyncStatement`, `statement`, or `cancellableExecution` must be set. */ cancellableExecution?: KernelNativeCancellableExecution; context: IClientContext; /** * Optional override for `id`. Defaults to the napi statement-id when the * handle exposes one, else a fresh UUIDv4. */ id?: string; } export default class KernelOperationBackend implements IOperationBackend { private readonly asyncStatement?; private readonly cancellableExecution?; private blockingStatement?; private richStatusFieldsPromise?; private readonly lifecycleHandle; private readonly context; private readonly _id; private readonly lifecycle; private resultSlicer?; private resultsProvider?; private metadata?; private metadataPromise?; private fetchHandlePromise?; constructor({ asyncStatement, statement, cancellableExecution, context, id }: KernelOperationBackendOptions); get id(): string; hasResultSet(): boolean; fetchChunk({ limit, disableBuffering, isClosed, }: { limit: number; disableBuffering?: boolean; isClosed?: () => boolean; }): Promise>; hasMore(): Promise; getResultMetadata(): Promise; status(_progress: boolean): Promise; waitUntilReady(options?: IOperationBackendWaitOptions): Promise; cancel(): Promise; close(): Promise; /** * Read the kernel's rich operation-status fields (`numModifiedRows` / * `displayMessage` / `diagnosticInfo` / `errorDetailsJson`) off the terminal * sync `Statement`. These accessors live only on the blocking `Statement` * (metadata path, or the sync `runAsync:false` path once `result()` has * resolved) — not on the async `AsyncStatement` / `AsyncResultHandle` — so: * * - on the async path we have no `Statement`, so we return all-null; * - on the sync path we await `getFetchHandle()` first, which both drives * `result()` to completion and stores the resolved `Statement` on * `blockingStatement` (the handle that backs the accessors); * - if the (older) binding predates these accessors we degrade to all-null * rather than throwing — `getOperationStatus()` must never fail just * because the rich fields are unavailable. * * Errors from the individual accessors are swallowed to null: a failed * status-field read must not turn a successful operation's status query into * a throw. The fields are best-effort metadata, not the operation outcome. */ private readRichStatusFields; private computeRichStatusFields; /** * Read the four rich-status accessors (`numModifiedRows` / `displayMessage` / * `diagnosticInfo` / `errorDetailsJson`) off a kernel handle — the terminal * sync `Statement` or the async `AsyncStatement`, which expose the same * accessor shape. Per-field read errors are swallowed to `null`: a failed * status-field read must never turn a successful operation's status query * into a throw. Degrades to all-null for a missing handle or a binding that * predates the accessors. */ private readStatusFieldsFrom; /** * Poll the kernel `AsyncStatement` to a terminal state on a fixed 100ms * cadence, mirroring the Thrift backend's `waitUntilReady` loop. We poll * `status()` (a cheap GetStatementStatus RPC) rather than awaiting * `awaitResult()` directly so that `status()` reports the real * Pending/Running/Succeeded state to a progress callback each tick, and so a * JS-initiated `cancel()`/`close()` is observed between ticks via * `failIfNotActive`. On success it materialises the result handle (so the * first fetch is free); on a server-driven terminal state it throws the typed * error the `IOperationBackend` contract requires. * * Terminal errors are thrown as `OperationStateError` (NOT plain * `HiveDriverError`) for Cancelled/Closed/Unknown, because the DBSQLOperation * facade only mirrors its `cancelled`/`closed` flags when * `err instanceof OperationStateError` — exactly as the Thrift backend does. * The Failed branch surfaces the kernel's typed SQL-error envelope via * `awaitResult()`. */ private waitUntilReadyAsync; /** * Sync (`runAsync: false`) execute path. Drives the blocking * `CancellableExecution.result()` to a terminal `Statement` (the kernel polls * to completion server-side). The * await is interruptible: a JS-initiated `cancel()` fires the detached * canceller, the server flips the statement terminal, and the parked * `result()` rejects with `Cancelled` — which we map to the typed * `OperationStateError(Canceled)`. * * Unlike the async path there is no status poll loop (the kernel owns * polling), so the progress callback fires once on completion, matching the * metadata path's single completion tick. */ private waitUntilReadyCancellable; /** * Drive `awaitResult()` on a Failed statement to surface the kernel's typed * SQL-error envelope. Falls back to a generic error if `awaitResult()` * unexpectedly resolves instead of rejecting. */ private throwAsyncError; /** * Best-effort close of the kernel statement when the poll loop ends on a * server-driven terminal error (Failed/Cancelled/Closed/Unknown/Timeout). * Without it the kernel-side statement handle leaks until session close (the * poll loop, unlike `fetchChunk`, otherwise just throws). Never masks the * original error; warn-logs a close failure so the leak is diagnosable. */ private bestEffortClose; /** * Resolve (and memoise) the fetch handle: `awaitResult()`'s `AsyncResultHandle` * on the query path, or the already-terminal `Statement` on the metadata path. */ private getFetchHandle; private getResultSlicer; }