Skip to content

Commit e6e1012

Browse files
authored
[SEA-NodeJS] SEA async execute, TLS & connection/statement options (#413)
* [SEA-NodeJS] SEA connection & statement options Wire the SEA connection-level and per-statement option surfaces onto the merged-kernel napi binding (thin forwarding — the kernel owns the behaviour): Connection options (SeaAuth.buildSeaConnectionOptions): - `maxConnections` → kernel pool size, validated as a positive integer within the napi u32 range. - TLS: `checkServerCertificate` (secure-by-default — omit to keep the kernel's verify-on default; `false` opts into insecure) and `customCaCert` (PEM string or Buffer; strings are PEM-sanity-checked and normalised to a Buffer before the FFI boundary), via the new `buildSeaTlsOptions`. - `intervalsAsString: true` is always set so SEA interval/duration columns render as strings — a byte-compatible drop-in for the Thrift backend. `complexTypesAsJson` is intentionally left at the kernel default (native Arrow), which already decodes identically to Thrift via the shared converter. Statement options (SeaSessionBackend.executeStatement, via buildExecuteOptions): - `queryTimeout` → `queryTimeoutSecs`; `rowLimit` → `rowLimit` (SEA-only cap). - `queryTags` serialised JS-side (reusing Thrift's `serializeQueryTags`) into the reserved `query_tags` conf key, merged with any explicit `statementConf` — the napi `queryTags` field can't carry null-valued tags, and the kernel rejects setting both. `queryTags` / `queryTimeout` are no longer rejected. - Still rejected (genuinely unsupported on SEA): `useCloudFetch`, `useLZ4Compression`, `stagingAllowedLocalPath`. `rowLimit` / `statementConf` added to the public `ExecuteStatementOptions`; SEA-only knobs (`maxConnections` / `checkServerCertificate` / `customCaCert`) added to the internal `InternalConnectionOptions`. Validated against a live warehouse: secure-by-default connect, maxConnections, checkServerCertificate, rowLimit (caps rows), queryTimeout, queryTags, statementConf, and non-PEM customCaCert rejection. Co-authored-by: Isaac Signed-off-by: Madhavendra Rathore <madhavendra.rathore@databricks.com> * [SEA-NodeJS] SEA async execute (submit / poll / awaitResult) Switch the SEA query path from the blocking `executeStatement` to the kernel's async `submitStatement`, matching the Thrift backend's always-async (`runAsync: true`) model. `submitStatement` returns immediately with a pending `AsyncStatement` (kernel `wait_timeout=0s`) while the query runs server-side. SeaOperationBackend becomes dual-mode (exactly one of): - `asyncStatement` (query path): `waitUntilReady()` polls `status()` to a terminal state on a 100ms cadence (matching Thrift), firing the progress callback each tick. Polling `status()` rather than blocking on `awaitResult()` keeps `cancel()` responsive — a blocking awaitResult would hold the kernel statement mutex for the whole query and queue cancel behind it. On Succeeded it materialises the result handle (first fetch is free); on Failed it drives `awaitResult()` to surface the kernel's typed SQL-error envelope; on a server-side Cancelled/Closed/Unknown it throws a clear error. `status()` reports the real Pending/Running/Succeeded state. - `statement` (metadata path): the kernel `list*`/`get*` statement is already terminal, so `waitUntilReady()` stays the one-shot completion tick. The fetch pipeline is shared: `awaitResult()`'s `AsyncResultHandle` and the metadata `Statement` expose the same `fetchNextBatch()` / `schema()` surface, so `SeaResultsProvider` → `ArrowResultConverter` → `ResultSlicer` consume either interchangeably via a single memoised fetch handle. cancel()/close() route through a `lifecycleHandle` abstraction over whichever handle backs the op. Re-exports the kernel `AsyncStatement` / `AsyncResultHandle` types from `SeaNativeLoader`. Validated against a live warehouse: async fetchAll correctness, multi-row drain (5000 rows), long-running aggregate (count over 20M), kernel SQL-error surfacing, and cancellation mid-execution. PR1's params/metadata/getInfo all still pass through the new async path. Co-authored-by: Isaac Signed-off-by: Madhavendra Rathore <madhavendra.rathore@databricks.com> * [SEA-NodeJS] prettier-format SEA connection/options + async files (CI code-style) The CI "Check code style" step runs `prettier . --check` (whole repo); these files were committed without prettier formatting. Formatting-only. Co-authored-by: Isaac Signed-off-by: Madhavendra Rathore <madhavendra.rathore@databricks.com> * [SEA-NodeJS] Address code-review findings on async/TLS/options (#413) Code-review #413 (81/100). Validated each against the code + a live warehouse: - F1 (HIGH): the async poll loop threw plain HiveDriverError for server-driven Cancelled/Closed/Unknown. The DBSQLOperation facade only mirrors its cancelled/closed flags when `err instanceof OperationStateError` (and OperationStateError extends HiveDriverError, not the reverse), so a server-side cancel/close/admin-kill left the facade desynced. Now throws OperationStateError(Canceled/Closed/Unknown) — matching the Thrift backend. The Failed branch still surfaces the kernel SQL-error envelope via awaitResult. - F2 (MED): the server-Cancelled test asserted only instanceOf(HiveDriverError), which passes for both the correct and incorrect type — it couldn't catch F1. Now asserts instanceOf(OperationStateError) + errorCode, plus a new Closed test. - F3 (MED): queryTimeout was forwarded to submitStatement but the kernel ignores queryTimeoutSecs on submit (always wait_timeout=0s), so the documented public option was a silent no-op, and the poll loop had no client-side deadline (a stalled Running statement polled forever). Now enforced client-side: the poll loop tracks a deadline, best-effort cancels the statement on expiry, and throws OperationStateError(Timeout) — matching Thrift's server TIMEDOUT outcome. Stopped forwarding the ignored queryTimeoutSecs to the napi options. Validated live: a 2s timeout interrupts a slow cross-join with TIMEOUT. - F4 (LOW): customCaCert PEM string check now requires the END marker too (a truncated/headerless cert no longer passes), consistent with the Buffer path. - F5 (LOW): SeaAuth reads the SEA-only fields (checkServerCertificate / customCaCert / maxConnections) through `InternalConnectionOptions` instead of ad-hoc inline casts, so a typo'd key fails to compile. - F6 (LOW): corrected the poll-loop comment — the prior text justified polling by an incorrect "blocking awaitResult holds the mutex and queues cancel" claim; cancel() is documented lock-free. The real rationale (real-time status to the progress callback + cancel observed between ticks) is now stated. Co-authored-by: Isaac Signed-off-by: Madhavendra Rathore <madhavendra.rathore@databricks.com> * [SEA-NodeJS] add TIMESTAMP_NTZ / TIMESTAMP_LTZ bound-param types Consolidates the last net-new bit of the superseded #408: two SEA-path DBSQLParameterType variants for binding timezone-explicit timestamps. The type name flows through the existing param codec (toSparkParameter → sqlType), which the kernel accepts — validated live (SELECT ? with TIMESTAMP_NTZ/LTZ returns the bound values). On the Thrift backend they degrade to a plain TIMESTAMP bind. Co-authored-by: Isaac Signed-off-by: Madhavendra Rathore <madhavendra.rathore@databricks.com> * [SEA-NodeJS] Address #413 review: TIMESTAMP_LTZ wire type + Int64 queryTimeout coercion Code-review #413 (gopalldb). Two P1s: - TIMESTAMP_LTZ was sent verbatim on the wire, but Spark has no distinct TIMESTAMP_LTZ type (TIMESTAMP already carries LTZ semantics) — so a Thrift caller got an opaque server bind error, and the enum comment falsely claimed NTZ/LTZ "degrade to a plain TIMESTAMP bind" (there was no such logic). `toSparkParameter` now maps TIMESTAMP_LTZ → `TIMESTAMP` (valid on both Thrift and kernel); TIMESTAMP_NTZ stays native (a real Spark type). Comment corrected. Added DBSQLParameter tests for both wire types (the Thrift behaviour the review flagged as untested) and updated the kernel positional-params test. - queryTimeout (`number | bigint | Int64`) was coerced with `Number(...)`, which yields NaN for an Int64 (node-int64 has no valueOf) → the client-side deadline was silently disabled for Int64 inputs. Now uses `numberToInt64(...).toNumber()`, matching the Thrift backend. Added a regression test that an `Int64(1)` queryTimeout actually fires the deadline (OperationStateError(Timeout)) rather than polling forever. (P1 "queryTimeout silently dropped on submit" and the unbounded-poll note were already resolved earlier by the client-side deadline fix; doc comment updated to match. P2 polarity/Date-NTZ items noted for the public-surface follow-up.) Validated live: NTZ binds natively and LTZ binds as TIMESTAMP on the kernel path. Co-authored-by: Isaac Signed-off-by: Madhavendra Rathore <madhavendra.rathore@databricks.com> --------- Signed-off-by: Madhavendra Rathore <madhavendra.rathore@databricks.com>
1 parent fb0b30a commit e6e1012

14 files changed

Lines changed: 925 additions & 110 deletions

lib/DBSQLParameter.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ export enum DBSQLParameterType {
88
STRING = 'STRING',
99
DATE = 'DATE',
1010
TIMESTAMP = 'TIMESTAMP',
11+
// `TIMESTAMP_NTZ` binds a timezone-free (wall-clock) timestamp. It is a real
12+
// Spark type, bound natively on both the Thrift and kernel backends (requires
13+
// a server that supports TIMESTAMP_NTZ; Spark 3.4+ / recent DBR).
14+
TIMESTAMP_NTZ = 'TIMESTAMP_NTZ',
15+
// `TIMESTAMP_LTZ` is an alias for `TIMESTAMP`: Spark has no distinct
16+
// TIMESTAMP_LTZ type — `TIMESTAMP` already carries local/instant (LTZ)
17+
// semantics. `toSparkParameter` therefore binds it as `TIMESTAMP` on the wire
18+
// (valid on both backends); it exists only as a self-documenting alias.
19+
TIMESTAMP_LTZ = 'TIMESTAMP_LTZ',
1120
FLOAT = 'FLOAT',
1221
DECIMAL = 'DECIMAL',
1322
DOUBLE = 'DOUBLE',
@@ -50,10 +59,16 @@ export class DBSQLParameter {
5059
return new TSparkParameter({ name }); // for NULL neither `type` nor `value` should be set
5160
}
5261

62+
// Map timezone-explicit timestamp aliases to their Spark wire type. Spark
63+
// has no distinct TIMESTAMP_LTZ type (TIMESTAMP carries LTZ semantics), so
64+
// bind it as TIMESTAMP — valid on both the Thrift and kernel backends.
65+
// TIMESTAMP_NTZ is a real Spark type and is bound natively.
66+
const wireType = this.type === DBSQLParameterType.TIMESTAMP_LTZ ? DBSQLParameterType.TIMESTAMP : this.type;
67+
5368
if (typeof this.value === 'boolean') {
5469
return new TSparkParameter({
5570
name,
56-
type: this.type ?? DBSQLParameterType.BOOLEAN,
71+
type: wireType ?? DBSQLParameterType.BOOLEAN,
5772
value: new TSparkParameterValue({
5873
stringValue: this.value ? 'TRUE' : 'FALSE',
5974
}),
@@ -63,7 +78,7 @@ export class DBSQLParameter {
6378
if (typeof this.value === 'number') {
6479
return new TSparkParameter({
6580
name,
66-
type: this.type ?? (Number.isInteger(this.value) ? DBSQLParameterType.INTEGER : DBSQLParameterType.DOUBLE),
81+
type: wireType ?? (Number.isInteger(this.value) ? DBSQLParameterType.INTEGER : DBSQLParameterType.DOUBLE),
6782
value: new TSparkParameterValue({
6883
stringValue: Number(this.value).toString(),
6984
}),
@@ -73,7 +88,7 @@ export class DBSQLParameter {
7388
if (this.value instanceof Int64 || typeof this.value === 'bigint') {
7489
return new TSparkParameter({
7590
name,
76-
type: this.type ?? DBSQLParameterType.BIGINT,
91+
type: wireType ?? DBSQLParameterType.BIGINT,
7792
value: new TSparkParameterValue({
7893
stringValue: this.value.toString(),
7994
}),
@@ -83,7 +98,7 @@ export class DBSQLParameter {
8398
if (this.value instanceof Date) {
8499
return new TSparkParameter({
85100
name,
86-
type: this.type ?? DBSQLParameterType.TIMESTAMP,
101+
type: wireType ?? DBSQLParameterType.TIMESTAMP,
87102
value: new TSparkParameterValue({
88103
stringValue: this.value.toISOString(),
89104
}),
@@ -92,7 +107,7 @@ export class DBSQLParameter {
92107

93108
return new TSparkParameter({
94109
name,
95-
type: this.type ?? DBSQLParameterType.STRING,
110+
type: wireType ?? DBSQLParameterType.STRING,
96111
value: new TSparkParameterValue({
97112
stringValue: this.value,
98113
}),

lib/contracts/IDBSQLSession.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,18 @@ export type ExecuteStatementOptions = {
2727
* These tags apply only to this statement and do not persist across queries.
2828
*/
2929
queryTags?: Record<string, string | null | undefined>;
30+
/**
31+
* SEA-only: server-side row cap for this statement (kernel `row_limit`). The
32+
* Thrift backend has no execute-time server cap, so this is a no-op there;
33+
* use `maxRows` for the cross-backend client-side fetch limit.
34+
*/
35+
rowLimit?: number;
36+
/**
37+
* SEA-only: per-statement Spark conf overlay (kernel `statement_conf`).
38+
* Merged with the serialized `queryTags` (which land under the reserved
39+
* `query_tags` key). Ignored by the Thrift backend.
40+
*/
41+
statementConf?: Record<string, string>;
3042
};
3143

3244
export type TypeInfoRequest = {

lib/contracts/InternalConnectionOptions.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,27 @@ export interface InternalConnectionOptions {
1818
* @internal Not stable; M0 stub only.
1919
*/
2020
useSEA?: boolean;
21+
22+
/**
23+
* SEA-only: kernel connection-pool size (`ConnectionOptions.max_connections`).
24+
* Validated as a positive integer within the napi `u32` range.
25+
* @internal SEA path only.
26+
*/
27+
maxConnections?: number;
28+
29+
/**
30+
* SEA-only: verify the server's TLS certificate. Secure-by-default — omit
31+
* to keep full chain + hostname verification; set `false` only to opt into
32+
* the insecure accept-anything mode.
33+
* @internal SEA path only.
34+
*/
35+
checkServerCertificate?: boolean;
36+
37+
/**
38+
* SEA-only: PEM-encoded CA certificate (string or `Buffer`) added to the
39+
* trust store on top of the system roots — for TLS-inspecting proxies or
40+
* on-prem internal CAs. Honoured regardless of `checkServerCertificate`.
41+
* @internal SEA path only.
42+
*/
43+
customCaCert?: Buffer | string;
2144
}

lib/sea/SeaAuth.ts

Lines changed: 141 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// limitations under the License.
1414

1515
import { ConnectionOptions } from '../contracts/IDBSQLClient';
16+
import { InternalConnectionOptions } from '../contracts/InternalConnectionOptions';
1617
import AuthenticationError from '../errors/AuthenticationError';
1718
import HiveDriverError from '../errors/HiveDriverError';
1819

@@ -66,9 +67,58 @@ export interface SeaSessionDefaults {
6667
catalog?: string;
6768
schema?: string;
6869
sessionConf?: Record<string, string>;
70+
/**
71+
* Render `INTERVAL` / `DURATION` result columns as strings
72+
* (kernel `ResultConfig.intervals_as_string`). The kernel default is
73+
* native Arrow `month_interval` / `duration[us]`, but the NodeJS
74+
* Thrift driver surfaces intervals as strings — so the SEA path sets
75+
* this `true` so its result shape is a byte-compatible drop-in for the
76+
* Thrift backend. Omitting it falls back to the kernel's native types.
77+
*/
78+
intervalsAsString?: boolean;
79+
/**
80+
* Render complex (`ARRAY` / `MAP` / `STRUCT` / `VARIANT`) result
81+
* columns as JSON strings (kernel `ResultConfig.complex_types_as_json`).
82+
* Left unset on the SEA path: native Arrow nested types already decode
83+
* identically to the Thrift backend through the shared Arrow converter,
84+
* so forcing JSON here would *introduce* a divergence rather than
85+
* remove one.
86+
*/
87+
complexTypesAsJson?: boolean;
88+
/**
89+
* Per-session kernel connection-pool size
90+
* (kernel `ConnectionOptions.max_connections`). Validated as a positive
91+
* integer within the napi `u32` range by `buildSeaConnectionOptions`.
92+
*/
93+
maxConnections?: number;
94+
}
95+
96+
/**
97+
* TLS options shared across all auth-mode variants. Mirror the napi
98+
* binding's `ConnectionOptions.checkServerCertificate` / `.customCaCert`
99+
* (kernel `Session::builder().tls(TlsConfig)`).
100+
*
101+
* The napi shape takes `customCaCert` as a `Buffer` only; the public
102+
* `ConnectionOptions` additionally accepts a PEM string, which
103+
* `buildSeaConnectionOptions` normalises to a `Buffer` before crossing
104+
* the FFI boundary.
105+
*/
106+
export interface SeaTlsOptions {
107+
/**
108+
* Verify the server's TLS certificate. The SEA backend is
109+
* **secure-by-default**: omitting this leaves the kernel default of
110+
* `true` (full chain + hostname verification). Set `false` only to opt
111+
* into the insecure, accept-anything mode (analogous to Thrift's
112+
* `rejectUnauthorized: false`); prefer pairing strict checking with
113+
* `customCaCert` over disabling verification entirely.
114+
*/
115+
checkServerCertificate?: boolean;
116+
/** PEM-encoded CA bytes to add to the trust store. */
117+
customCaCert?: Buffer;
69118
}
70119

71120
export type SeaNativeConnectionOptions = SeaSessionDefaults &
121+
SeaTlsOptions &
72122
(
73123
| {
74124
hostName: string;
@@ -114,6 +164,64 @@ export function isBlankOrReserved(s: string): boolean {
114164
return normalized.length === 0 || normalized === 'undefined' || normalized === 'null';
115165
}
116166

167+
/** napi-rs marshals `maxConnections` as a `u32`; reject values it can't hold. */
168+
const MAX_U32 = 0xffffffff;
169+
170+
/**
171+
* Normalise the public TLS options (`checkServerCertificate` /
172+
* `customCaCert`) into the napi shape.
173+
*
174+
* - `checkServerCertificate` passes through verbatim (only when set; an
175+
* absent value leaves the kernel default, which is secure — verify on).
176+
* - `customCaCert` accepts a PEM string or `Buffer` on the public
177+
* surface; we convert a string to a `Buffer` here and do a light PEM
178+
* sanity check. The bytes are NOT parsed in JS — the kernel returns a
179+
* meaningful error if the PEM is malformed.
180+
*
181+
* Throws `HiveDriverError` when `customCaCert` is supplied but empty or
182+
* (for strings) lacks a PEM certificate header.
183+
*/
184+
export function buildSeaTlsOptions(options: ConnectionOptions): SeaTlsOptions {
185+
// Read the SEA-only fields through the purpose-built internal options type
186+
// rather than an ad-hoc inline cast, so the shape can't silently drift from
187+
// its declaration and a typo'd key fails to compile.
188+
const { checkServerCertificate, customCaCert } = options as ConnectionOptions & InternalConnectionOptions;
189+
190+
const tls: SeaTlsOptions = {};
191+
192+
if (checkServerCertificate !== undefined) {
193+
tls.checkServerCertificate = checkServerCertificate;
194+
}
195+
196+
if (customCaCert !== undefined) {
197+
if (typeof customCaCert === 'string') {
198+
// Light PEM sanity check — require both the BEGIN and END markers so a
199+
// truncated/headerless cert is rejected here rather than surfacing as an
200+
// opaque kernel TLS error. Full parsing is deferred to the kernel.
201+
if (
202+
!customCaCert.includes('-----BEGIN CERTIFICATE-----') ||
203+
!customCaCert.includes('-----END CERTIFICATE-----')
204+
) {
205+
throw new HiveDriverError(
206+
'SEA backend: `customCaCert` string does not look like a PEM certificate ' +
207+
"(missing the '-----BEGIN CERTIFICATE-----' / '-----END CERTIFICATE-----' markers). " +
208+
'Pass PEM text or a Buffer of PEM bytes.',
209+
);
210+
}
211+
tls.customCaCert = Buffer.from(customCaCert, 'utf8');
212+
} else if (Buffer.isBuffer(customCaCert)) {
213+
if (customCaCert.length === 0) {
214+
throw new HiveDriverError('SEA backend: `customCaCert` Buffer is empty.');
215+
}
216+
tls.customCaCert = customCaCert;
217+
} else {
218+
throw new HiveDriverError('SEA backend: `customCaCert` must be a PEM string or a Buffer.');
219+
}
220+
}
221+
222+
return tls;
223+
}
224+
117225
/**
118226
* Validate the user-supplied `ConnectionOptions` and build the
119227
* napi-binding's connection-options shape.
@@ -170,11 +278,43 @@ export function isBlankOrReserved(s: string): boolean {
170278
export function buildSeaConnectionOptions(options: ConnectionOptions): SeaNativeConnectionOptions {
171279
const { authType } = options as { authType?: string };
172280

173-
const base = {
281+
const base: {
282+
hostName: string;
283+
httpPath: string;
284+
intervalsAsString: boolean;
285+
maxConnections?: number;
286+
} & SeaTlsOptions = {
174287
hostName: options.host,
175288
httpPath: prependSlash(options.path),
289+
// Match the NodeJS Thrift driver, which surfaces INTERVAL columns as
290+
// strings. The kernel defaults to native Arrow interval/duration types;
291+
// forcing the string rendering here keeps the SEA path a byte-compatible
292+
// drop-in. Complex types are intentionally left at the kernel default
293+
// (native Arrow) — they already decode identically to Thrift via the
294+
// shared Arrow converter, so `complexTypesAsJson` is not forced on.
295+
intervalsAsString: true,
296+
// TLS knobs (server-cert verification toggle + custom CA). Validated and
297+
// normalised (string PEM → Buffer) here so the napi shape only sees a Buffer.
298+
...buildSeaTlsOptions(options),
176299
};
177300

301+
// SEA-only pool sizing; read via cast to match how this function reads the
302+
// other SEA-specific options (TLS) — they live on the internal options
303+
// surface, not the published public `ConnectionOptions` `.d.ts`.
304+
const { maxConnections } = options as ConnectionOptions & InternalConnectionOptions;
305+
if (maxConnections !== undefined) {
306+
if (!Number.isInteger(maxConnections) || maxConnections < 1) {
307+
throw new HiveDriverError(`SEA backend: \`maxConnections\` must be a positive integer; got ${maxConnections}.`);
308+
}
309+
if (maxConnections > MAX_U32) {
310+
throw new HiveDriverError(
311+
`SEA backend: \`maxConnections\` exceeds the napi u32 limit (${MAX_U32}); got ${maxConnections}. ` +
312+
'Typical pool sizes are 10-500.',
313+
);
314+
}
315+
base.maxConnections = maxConnections;
316+
}
317+
178318
const oauth = options as {
179319
oauthClientId?: string;
180320
oauthClientSecret?: string;

lib/sea/SeaNativeLoader.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ import type {
3636
ExecuteOptions as NativeExecuteOptions,
3737
TypedValueInput as NativeTypedValueInput,
3838
NamedTypedValueInput as NativeNamedTypedValueInput,
39+
AsyncStatement as NativeAsyncStatement,
40+
AsyncResultHandle as NativeAsyncResultHandle,
3941
} from '../../native/sea';
4042

4143
// SEA-prefixed re-exports. The kernel-generated `.d.ts` keeps the
@@ -59,6 +61,14 @@ export type SeaNativeExecuteOptions = NativeExecuteOptions;
5961
export type SeaNativeTypedValueInput = NativeTypedValueInput;
6062
export type SeaNativeNamedTypedValueInput = NativeNamedTypedValueInput;
6163

64+
// Async-submit surface: `Connection.submitStatement` returns an
65+
// `AsyncStatement` (status / awaitResult / cancel / close); `awaitResult`
66+
// yields an `AsyncResultHandle` whose `fetchNextBatch` / `schema` match the
67+
// blocking `Statement`'s fetch surface, so the results pipeline consumes
68+
// either interchangeably.
69+
export type SeaNativeAsyncStatement = NativeAsyncStatement;
70+
export type SeaNativeAsyncResultHandle = NativeAsyncResultHandle;
71+
6272
/**
6373
* The full native binding surface, derived from the generated module
6474
* so it can never drift from the `.d.ts` contract: when the kernel

0 commit comments

Comments
 (0)