From 85ab2d84b61e211dbadbca632484abe87202ae14 Mon Sep 17 00:00:00 2001 From: Vu Anh Phung Date: Wed, 12 Aug 2026 17:05:45 +0000 Subject: [PATCH 1/7] Upgrade napi-rs CLI to v3 Signed-off-by: Vu Anh Phung --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2ec01635..55606da5 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "test": "nyc --report-dir=${NYC_REPORT_DIR:-coverage_unit} mocha --config tests/unit/.mocharc.js", "update-version": "node bin/update-version.js && prettier --write ./lib/version.ts", "build": "npm run update-version && tsc --project tsconfig.build.json", - "build:native": "bash -c 'cd ${DATABRICKS_SQL_KERNEL_REPO:-../../databricks-sql-kernel}/napi && npx --yes @napi-rs/cli@2.18.4 build --platform ${BUILD_PROFILE:---release} && cp index.* $OLDPWD/native/kernel/'", + "build:native": "bash -c 'cd ${DATABRICKS_SQL_KERNEL_REPO:-../../databricks-sql-kernel}/napi && npx --yes --package @napi-rs/cli@3 napi build --platform ${BUILD_PROFILE:---release} && cp index.* $OLDPWD/native/kernel/'", "prepack": "test -f native/kernel/index.js || { echo 'ERROR: native/kernel/index.js (napi-rs router) is missing — the published tarball would fail to load kernel. It is committed to git; run `npm run build:native` if you removed it.' >&2; exit 1; }", "watch": "tsc --project tsconfig.build.json --watch", "type-check": "tsc --noEmit", From 02240719535ac54c377c80c2e74e7a79bc4b27f3 Mon Sep 17 00:00:00 2001 From: Vu Anh Phung Date: Wed, 12 Aug 2026 17:22:51 +0000 Subject: [PATCH 2/7] Pin napi-rs CLI to 3.8.2 Signed-off-by: Vu Anh Phung --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 55606da5..756d997e 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "test": "nyc --report-dir=${NYC_REPORT_DIR:-coverage_unit} mocha --config tests/unit/.mocharc.js", "update-version": "node bin/update-version.js && prettier --write ./lib/version.ts", "build": "npm run update-version && tsc --project tsconfig.build.json", - "build:native": "bash -c 'cd ${DATABRICKS_SQL_KERNEL_REPO:-../../databricks-sql-kernel}/napi && npx --yes --package @napi-rs/cli@3 napi build --platform ${BUILD_PROFILE:---release} && cp index.* $OLDPWD/native/kernel/'", + "build:native": "bash -c 'cd ${DATABRICKS_SQL_KERNEL_REPO:-../../databricks-sql-kernel}/napi && npx --yes --package @napi-rs/cli@3.8.2 napi build --platform ${BUILD_PROFILE:---release} && cp index.* $OLDPWD/native/kernel/'", "prepack": "test -f native/kernel/index.js || { echo 'ERROR: native/kernel/index.js (napi-rs router) is missing — the published tarball would fail to load kernel. It is committed to git; run `npm run build:native` if you removed it.' >&2; exit 1; }", "watch": "tsc --project tsconfig.build.json --watch", "type-check": "tsc --noEmit", From f0d1862021757319c7aaee00b3eb85f9bdbae53d Mon Sep 17 00:00:00 2001 From: Vu Anh Phung Date: Wed, 12 Aug 2026 18:18:30 +0000 Subject: [PATCH 3/7] Bump SQL kernel revision for napi-rs v3 Signed-off-by: Vu Anh Phung --- KERNEL_REV | 2 +- native/kernel/index.d.ts | 1783 ++++++++++---------- native/kernel/index.js | 918 +++++++--- tests/unit/kernel/native-packaging.test.ts | 6 +- 4 files changed, 1563 insertions(+), 1146 deletions(-) diff --git a/KERNEL_REV b/KERNEL_REV index 7dd91996..1561112e 100644 --- a/KERNEL_REV +++ b/KERNEL_REV @@ -1 +1 @@ -0d46716c466897148dfc1d2976ff03bdf097998c +0ecc09c9d0075c630df2adce1a6414051a16cc9e diff --git a/native/kernel/index.d.ts b/native/kernel/index.d.ts index 0b042121..5f38f927 100644 --- a/native/kernel/index.d.ts +++ b/native/kernel/index.d.ts @@ -1,505 +1,557 @@ -/* tslint:disable */ -/* eslint-disable */ - /* auto-generated by NAPI-RS */ - +/* eslint-disable */ /** - * Per-statement options for `Connection.executeStatement`. - * - * Mirrors the kernel `StatementSpec` knobs that are safe to thread - * through napi without a kernel-side change. Today this covers: - * - `statementConf` — per-statement Spark conf overlay - * (`StatementSpec.statement_conf` → SEA `parameters` / - * Thrift `confOverlay`) - * - `queryTags` — convenience wrapper over `statementConf` with - * key `query_tags`; serialised to the same comma-separated - * `key:value` wire shape NodeJS Thrift's `serializeQueryTags` - * produces (`lib/utils/queryTags.ts`). Backslashes in keys are - * doubled; backslash/colon/comma in values are backslash-escaped. + * Opaque result-fetch handle returned by + * `AsyncStatement.awaitResult()`. Wraps a kernel `ResultStream` + * directly; structurally analogous to the sync `Statement`'s + * fetch-side surface (`fetchNextBatch` / `schema` / + * `statementId`). * - * `rowLimit` (SEA `row_limit`) is exposed here and threaded onto the kernel - * `StatementSpec`. `positionalParams` (`?`) and `namedParams` (`:name`) - * carry bound query parameters, decoded via `params::parse_typed_value`. - * (There is no `queryTimeoutSecs`: it abused the SEA `wait_timeout` inline-hold - * window and was removed — a real per-statement timeout is `STATEMENT_TIMEOUT`.) + * `cancel()` / `close()` are not exposed: the parent + * `AsyncStatement` owns server-side lifecycle. A `close()` here + * would create dual-ownership of the same statement_id with + * inconsistent close semantics. Callers `close()` the parent + * `AsyncStatement` after they're done fetching. * - * **Tag-order caveat (M4 parity note).** The napi `queryTags` field - * is a Rust `HashMap` whose iteration order is - * non-deterministic, so the serialised `query_tags` value may have - * a different key order than Thrift's `serializeQueryTags` (which - * iterates `Object.keys(...)` in insertion order) for the same - * input. The SEA server is order-insensitive on conf values, so - * the two are functionally equivalent. If a caller needs - * byte-identical Thrift parity, the JS adapter pre-serialises via - * `serializeQueryTags` and writes the result into - * `statementConf["query_tags"]` directly — see - * `KernelSessionBackend.executeStatement` in the NodeJS driver. This - * path is the one the production code uses. + * Schema is cached at construction so it survives the underlying + * stream being drained; mirrors the sync `Statement.schema()` + * post-close contract. */ -export interface ExecuteOptions { - /** - * Per-statement Spark conf overlay. Merged on top of the - * session-level `sessionConf` at execute time; this map wins - * on key collisions. Unknown keys are rejected by the server. - */ - statementConf?: Record - /** - * Query tags as key→value pairs. Serialised to a comma- - * separated `key:value` string (backslash-escaping `\`, `:`, - * `,`) and placed into `statementConf["query_tags"]`, matching - * NodeJS Thrift's `serializeQueryTags` wire shape. Passing - * both `queryTags` AND a `query_tags` key in `statementConf` - * raises `InvalidArgument` — the caller's intent is ambiguous - * so we refuse to silently pick one over the other. - * - * A **`null`** value emits a **bare key** (no colon) — e.g. - * `{ production: null }` → `"production"` — matching the - * connectors' `key`-only tag form. - * - * See the struct-level "Tag-order caveat" for the - * HashMap-iteration-order vs `Object.keys`-iteration-order - * divergence and the byte-identical-Thrift-parity workaround. - */ - queryTags?: Record - /** - * Server-side cap on the number of rows this statement returns - * (SEA `row_limit`), independent of any SQL `LIMIT`. Maps to - * `StatementSpec.row_limit`. Omitted ⇒ no driver-imposed cap. - */ - rowLimit?: number +export declare class AsyncResultHandle { /** - * Positional parameters, in 1-based wire order. Index `i` in this - * Vec corresponds to the `i+1`-th `?` placeholder in the SQL. - * Each entry is a `{ sqlType, value }` pair — `value` is the - * string-encoded literal or `null` for SQL NULL. Mirrors - * `StatementSpec::positional_params`; decoded via [`parse_typed_value`]. + * Server-issued statement id. Cached at construction; readable + * for log correlation. Matches the parent `AsyncStatement`'s + * `statementId`. */ - positionalParams?: Array + get statementId(): string /** - * Named parameters (`:name` placeholders). Each carries its `name` - * alongside the `{ sqlType, value? }` pair. Mapped to a kernel - * `TypedValue` via the same [`parse_typed_value`] codec and bound with - * `StatementSpec::param_named`. Named is the SEA-spec-required public - * param form (`StatementParameter.name` is `openapi_required`); - * positional is the documented-undocumented variant. The two are - * mutually exclusive at the SQL level (`?` vs `:name`). + * Pull the next batch of results. Returns `null` when the + * stream is exhausted. The returned `ArrowBatch.ipcBytes` is a + * complete Arrow IPC stream (schema header + 1 record-batch + * message), suitable for handing to `apache-arrow`'s + * `RecordBatchReader`. Byte-identical to the sync + * `Statement.fetchNextBatch()` payload for the same query. */ - namedParams?: Array -} -/** - * A named bound parameter — a [`TypedValueInput`] plus its `:name`. Kept a - * distinct napi object (rather than an optional `name` on `TypedValueInput`) - * so the positional surface stays a clean ordered list with no name field. - */ -export interface NamedTypedValueInput { - name: string - sqlType: string - value?: string -} -/** - * Authentication mode selector crossing the napi boundary. The string - * literals are what napi-rs emits from this `#[napi(string_enum)]` — the - * NodeJS SEA adapter (`KernelAuth`) matches them verbatim (`'Pat'`, - * `'OAuthM2m'`, `'OAuthU2m'`). - * - * Mirrors the kernel [`AuthConfig`] variants this binding supports. - * `OAuthFederation` / `External` are intentionally not exposed yet — the - * kernel marks federation as not-yet-implemented and `External` is a - * Rust-trait escape hatch with no JS-callback bridge. - */ -export const enum AuthMode { - /** Personal access token (`token`). */ - Pat = 'Pat', - /** OAuth 2.0 machine-to-machine — `oauthClientId` + `oauthClientSecret`. */ - OAuthM2m = 'OAuthM2m', + fetchNextBatch(): Promise /** - * OAuth 2.0 user-to-machine (browser flow) — optional `oauthClientId` - * + `oauthRedirectPort`. + * Result schema as an Arrow IPC payload (schema header only, + * no record-batch message). Available before any batches have + * been fetched. Sync because the body has no `.await` — + * `encode_ipc_stream` is pure CPU work over the cached + * `Arc`. */ - OAuthU2m = 'OAuthU2m' -} -/** - * A single extra HTTP header as an explicit `{ name, value }` pair. - * - * An ordered list of these (`ConnectionOptions.custom_headers`) mirrors - * the kernel core's `Vec<(String, String)>` and the pyo3 binding's - * `http_headers`: order is preserved and duplicate `name`s are allowed. - * A struct (rather than a raw `[name, value]` tuple) because napi-rs - * does not marshal Rust tuples through `#[napi(object)]` fields; the - * struct is the idiomatic, self-documenting equivalent and maps to a JS - * `{ name: string, value: string }`. - */ -export interface HeaderEntry { - name: string - value: string -} -/** - * Programmatic HTTP/HTTPS proxy configuration, mirroring the kernel's - * internal [`ProxyConfig`]. Supplied as a structured object rather than a - * flattened URL so credentials never have to be percent-encoded into the URL - * and the bypass-host list can be expressed. - * - * - `url` — proxy endpoint, e.g. `"http://proxy.corp.example.com:8080"`. Must - * use the `http://` or `https://` scheme. - * - `username` / `password` — optional proxy basic-auth, applied via - * `reqwest`'s `Proxy::basic_auth` (not embedded in the URL). - * - `bypassHosts` — optional comma-separated host/domain list that should - * bypass the proxy (e.g. `"localhost,*.internal.corp"`). - */ -export interface ProxyInput { - url: string - username?: string - password?: string - bypassHosts?: string + schema(): ArrowSchema } + /** - * JS-visible options for opening a Databricks SQL session. + * Opaque async-statement handle. * - * Authentication is selected by `authMode` (default [`AuthMode::Pat`]): - * - `Pat` — `token` required. - * - `OAuthM2m` — `oauthClientId` + `oauthClientSecret` required. - * - `OAuthU2m` — `oauthClientId` / `oauthRedirectPort` optional - * (defaults to the `databricks-sql-connector` client on port 8020). + * Returned by `Connection.submitStatement(...)` after the kernel + * `Statement::submit()` returns (server sent `wait_timeout=0s`, so + * the response carries a `statement_id` but the statement is still + * `Pending`/`Running`). JS drives polling via `status()` / + * `awaitResult()`. * - * Catalog / schema / sessionConf are applied once at session creation - * and remain in effect for every statement run on the resulting - * `Connection`. The SEA wire protocol carries them on - * `CreateSession`, not on `ExecuteStatement` — so there is no - * per-statement override path on this binding. + * Concurrency shape: `status()`, `awaitResult()`, and `close()` take + * `inner.lock()` and hold the guard across the kernel `.await` (tokio + * `Mutex` is FIFO), so `status()` / `close()` queue behind any + * in-flight `awaitResult()` until it returns naturally. `cancel()` is + * the deliberate exception: it does **not** touch `inner` — it fires + * through the detached `AsyncStatementCanceller` (session + + * statement_id, captured at construction), so an explicit + * `stmt.cancel()` interrupts an in-flight `awaitResult()` instead of + * queueing behind it. The server-side cancel flips the statement + * terminal, which the parked `awaitResult()` poll loop observes + * (`Cancelled`) and returns on. The kernel's `AwaitResultCancelGuard` + * still covers the drop-cancel case (Promise.race / timeout) + * independently — see module docs. */ -export interface ConnectionOptions { - /** - * Workspace host, e.g. `adb-…azuredatabricks.net`. The kernel - * normalises this — bare hostnames get `https://` prepended. - */ - hostName: string - /** - * JDBC-style HTTP path, e.g. `/sql/1.0/warehouses/abc123`. The - * kernel parses out the warehouse id. - */ - httpPath: string +export declare class AsyncStatement { /** - * Authentication mode. Omitted ⇒ [`AuthMode::Pat`] (back-compat: - * existing PAT callers pass only `token`). + * Server-issued statement id. Cached at construction; readable + * even after `close()` so JS-side log lines can correlate + * against kernel / server logs which key on the same id. */ - authMode?: AuthMode + get statementId(): string /** - * Personal access token. Required (and non-empty) for - * [`AuthMode::Pat`]; ignored otherwise. + * One-shot status check. Returns a string enum matching the + * kernel `StatementStatus` shape: + * `'Pending' | 'Running' | 'Succeeded' | 'Failed' | + * 'Cancelled' | 'Closed' | 'Unknown'`. (`'Unknown'` is the + * `#[non_exhaustive]` forward-compat catch-all that + * `StatementStatus::as_str` can return — consumers switching on + * the state must handle it.) Returns + * `KernelError(InvalidStatementHandle)` if the statement has + * been explicitly `close()`d. + * + * The `Failed` variant collapses to the string `'Failed'` on + * the JS side; the underlying error envelope (sql_state / + * error_code / query_id) is surfaced by `awaitResult()`'s + * rejection, which is where callers actually need the typed + * error. `status()` is intended for polling progress UIs + * that only need the state name. */ - token?: string + status(): Promise + /** Rows modified by the statement (UPDATE / INSERT / DELETE / MERGE). */ + numModifiedRows(): Promise /** - * OAuth client id. Required for [`AuthMode::OAuthM2m`]; optional for - * [`AuthMode::OAuthU2m`] (defaults to `databricks-sql-connector`). + * Server-supplied user-facing message (may contain SQL fragments — + * redact before centralised logging). */ - oauthClientId?: string - /** OAuth client secret. Required for [`AuthMode::OAuthM2m`]. */ - oauthClientSecret?: string + displayMessage(): Promise + /** Server-supplied diagnostic detail. */ + diagnosticInfo(): Promise + /** Server-supplied structured error detail (JSON), when enabled. */ + errorDetailsJson(): Promise /** - * Localhost callback port for the [`AuthMode::OAuthU2m`] browser - * flow. Omitted ⇒ kernel default (8020). + * Block until the server reaches a terminal state, then return + * an `AsyncResultHandle` that wraps the materialised result + * stream. The handle exposes `fetchNextBatch()` / `schema()` + * for consuming the result, plus `statementId` for log + * correlation. + * + * Drop-cancel safety: kernel `await_result` installs + * `AwaitResultCancelGuard` which fires a fire-and-forget + * `cancel_statement` if the future is dropped mid-poll + * (timeout, tokio::select! loser, JS-side `Promise.race` + * loser). The `util::guarded` `catch_unwind` here covers the + * V8-panic-across-boundary case on top. Returns + * `KernelError(InvalidStatementHandle)` if the statement has + * been explicitly `close()`d. */ - oauthRedirectPort?: number + awaitResult(): Promise /** - * OAuth scopes override (M2M / U2M). Omitted ⇒ kernel defaults - * (`["all-apis"]` for M2M; `["all-apis", "offline_access"]` for U2M). + * Server-side cancel. Returns + * `KernelError(InvalidStatementHandle)` if the statement has + * been explicitly `close()`d. Idempotent against a server + * that already reached a terminal state — the kernel's + * `cancel_statement` is a no-op there. + * + * **Lock-free by design.** Unlike `status()` / `awaitResult()` / + * `close()`, this does not take `inner.lock()` — it fires through + * the detached `AsyncStatementCanceller` captured at construction. + * That lets `stmt.cancel()` interrupt an in-flight `awaitResult()` + * (which holds the mutex for the whole poll) instead of queueing + * behind it: the server-side cancel flips the statement terminal, + * the parked `awaitResult()` poll loop observes `Cancelled` and + * returns. The closed-state check reads a lock-free flag so a + * cancel after an explicit `close()` still surfaces + * `InvalidStatementHandle`. */ - oauthScopes?: Array + cancel(): Promise /** - * Default catalog for statements executed on this session. - * Routed through the kernel's `DefaultOpts` and onto the SEA - * `CreateSession.catalog` wire field. + * Explicit close. Idempotent — a second call on an + * already-closed handle returns `Ok(())`. On `Err`, the napi + * inner is already `None`, so a JS-side retry sees the + * closed-handle short-circuit and returns `Ok(())` without + * re-attempting the wire call. The kernel's own `Drop` + * fire-and-forget retry runs once in the background. */ - catalog?: string + close(): Promise +} + +/** + * Handle returned by `Connection.executeStatementCancellable`. Owns the + * built-but-not-yet-executed kernel `Statement` plus a detached + * [`StatementCanceller`] captured before dispatch, so JS can fire a + * server-side cancel while the blocking `result()` is in flight. + * + * `pending` is `Arc>>` so `result()` can + * `.take()` the statement (the kernel `execute()` borrows it `&mut`, + * then it moves into the produced `Statement` wrapper to keep its + * `ValidityFlag` set — see `statement.rs`). A second `result()` call + * after the first resolved surfaces `InvalidStatementHandle`. + */ +export declare class CancellableExecution { /** - * Default schema for statements executed on this session. - * Routed through the kernel's `DefaultOpts` and onto the SEA - * `CreateSession.schema` wire field. + * The server-issued statement id this execution targets, if the + * server has issued one yet (`null` before the initial submit + * round-trip publishes it mid-`result()`). Useful for log + * correlation while the blocking drive is in flight. */ - schema?: string + get statementId(): string | null /** - * Server-bound session conf (Spark conf, `ANSI_MODE`, `TIMEZONE`, - * query-tag presets, …). Forwarded verbatim to SEA - * `session_confs`. Unknown keys are rejected server-side. + * Drive the blocking `execute()` and resolve to a `Statement` + * (identical to what `executeStatement` returns) once the kernel + * reaches a terminal state and the result stream is ready. + * + * Consumes the pending statement: a second `result()` call returns + * `KernelError(InvalidStatementHandle)`. The future is + * drop-cancel-safe — the kernel's per-execute `MidExecuteCancelState` + * guard fires a fire-and-forget `cancel_statement` if this future is + * dropped mid-flight (`Promise.race` / timeout loser), independently + * of an explicit `cancel()`. + * + * On a server-side cancel the kernel's blocking `execute()` currently + * surfaces `InvalidArgument` (a known kernel quirk — the async path + * returns `Cancelled`). When this handle's `cancel()` actually dispatched a + * server-side cancel, we normalise that into `Cancelled` here so JS callers + * can rely on a single cancelled-status code regardless of execution path. + * + * Three outcomes can race the blocking drive: (1) a natural terminal state + * → `Ok` or the genuine error; (2) an explicit `cancel()` that dispatched a + * server cancel → this `result()` rejects with a `Cancelled`-coded error + * (the normalisation above); (3) the future being **dropped** mid-flight + * (`Promise.race`/timeout loser) → the kernel's `MidExecuteCancelState` + * drop-guard fires a fire-and-forget `cancel_statement`, but there is no + * `result()` left to observe a code. Only (2) yields a `Cancelled` error. */ - sessionConf?: Record + result(): Promise /** - * Maximum number of pooled HTTP connections per host. Routes - * through the kernel's [`HttpConfig::pool_max_idle_per_host`]. - * Tunes the underlying `reqwest` connection pool — higher values - * reduce reconnect overhead when many statements run - * concurrently against the same warehouse. - * - * When the JS caller does NOT provide `maxConnections`, the napi - * binding applies a NodeJS-driver-appropriate default of - * [`NAPI_DEFAULT_POOL_MAX_IDLE_PER_HOST`] (100) — chosen to match - * the JDBC driver's `HttpConnectionPoolSize` default and to close - * the throughput gap vs the NodeJS Thrift driver's - * `maxSockets: Infinity` pool for bursty workloads. The kernel - * core's [`HttpConfig::pool_max_idle_per_host`] default is also 100 - * (matching the same JDBC default), so napi pins its own copy rather - * than inheriting it. Mirrors the Python connector's - * `max_connections` kwarg on the SEA backend, which exposes the - * knob but keeps its own urllib3-aligned default of 10. + * Server-side cancel of the in-flight statement. * - * Napi-rs serialises `u32` as JS `number`; values up to - * `2^32 - 1` round-trip safely (any reasonable pool size fits). + * Lock-free: fires the detached `StatementCanceller` captured at + * construction rather than taking the mutex `result()` holds, so it + * interrupts a still-running blocking `result()` instead of queueing + * behind it. No-op (returns `Ok`) if `result()` already finished + * successfully, or if no statement id has been observed yet (query still + * in its initial submit round-trip), and idempotent against a server + * already in a terminal state. */ - maxConnections?: number + cancel(): Promise +} + +/** + * Opaque connection handle wrapping a kernel `Session`. + * + * `inner` is `Arc>>` so: + * - the Drop impl can clone the `Arc` and `.take()` the session on a + * background tokio task without holding `&mut self` (which Drop is + * forbidden from doing across an `await`), + * - `close()` can `.take()` the session to consume it for the kernel's + * move-by-value `Session::close(self)` signature. + * + * **Concurrency shape** — both `executeStatement` and + * `submitStatement` build the kernel `Statement` under `inner.lock()` + * and then RELEASE the guard before the wire call + * (`stmt.execute().await` / `stmt.submit().await`). `Session::statement()` + * is `&self`-callable and only clones the session's internal `Arc`, so + * the built statement is independent of the guard. Concurrent + * `Promise.all([executeStatement(q1), submitStatement(q2)])` therefore + * serialise only for the microsecond statement-build, not the network + * round-trip, and `close()` never blocks behind an in-flight execute or + * submit. See + * `sea-workflow/jira-candidates/2026-05-24-napi-cancel-during-fetch.md`. + */ +export declare class Connection { /** - * Render `INTERVAL` / `DURATION` result columns as strings - * (`ResultConfig.intervals_as_string`). The kernel default is - * native Arrow `month_interval` / `duration[us]` types; the NodeJS - * Thrift driver surfaces intervals as strings, so the SEA driver - * sets this `true` for byte-compatible parity. Omitted ⇒ kernel - * default (native Arrow interval types). + * Server-issued session id. Cached at construction; readable + * even after `close()` so JS-side log lines can correlate + * against kernel / server logs which key on the same id. */ - intervalsAsString?: boolean + get sessionId(): string /** - * Render complex (`ARRAY` / `MAP` / `STRUCT` / `VARIANT`) result - * columns as JSON strings (`ResultConfig.complex_types_as_json`) - * instead of native Arrow nested types. Omitted ⇒ kernel default - * (native Arrow nested types, which the NodeJS Arrow decoder - * already renders identically to the Thrift path). + * Execute a SQL statement and return a Statement handle that + * streams batches via `fetchNextBatch()`. + * + * Catalog / schema / sessionConf are session-level + * (`openSession`). Per-statement options on `ExecuteOptions`: + * - `statementConf` — per-statement Spark conf overlay + * - `queryTags` — serialised to a comma-separated `key:value` + * string and placed in `statement_conf["query_tags"]`, + * matching NodeJS Thrift's `serializeQueryTags` wire shape + * + * `options` is omitted/`None` for the no-options path; passing + * `{ statementConf: {} }` (an empty map) is treated the same as + * omission to keep the wire shape stable for the common case. */ - complexTypesAsJson?: boolean + executeStatement(sql: string, options?: ExecuteOptions | undefined | null): Promise /** - * Whether to verify the server's TLS certificate. + * directResults execute — the Thrift/JDBC model. Sends ExecuteStatement + * with no `wait_timeout` field (server applies its ~10s default inline wait + * and auto-closes on success) and returns WITHOUT polling past it: * - * Omitted / `true` ⇒ strict validation against the system / Mozilla - * trust store (full chain + expiry + hostname), matching JDBC / ODBC - * and every modern HTTPS client. This is the **default** for the SEA - * backend — secure by default. + * - a **`Statement`** (left arm) when the query finished within the inline + * wait — terminal, result ready inline, `close()` is a clean release; + * - an **`AsyncStatement`** (right arm) when it did not — a poll/cancel + * handle the caller drives (`status()` / `awaitResult()` / `cancel()`). * - * `false` ⇒ permissive: accept self-signed / untrusted / expired - * certs AND skip the hostname-vs-SNI check. This is **insecure** (no - * protection against active MITM); it exists only as an opt-out for - * parity with the legacy NodeJS Thrift driver, which hard-codes - * `rejectUnauthorized: false`. Prefer pairing strict checking with - * `custom_ca_cert` over disabling verification entirely. + * JS distinguishes the arms by feature-detecting `awaitResult` (present + * only on `AsyncStatement`). This is the path that gives mid-run cancel for + * long queries WITHOUT the eager-handle / close-drives workaround: the + * returned handle always corresponds to a server-owned statement. * - * This is the master verify toggle: `false` disables chain validation - * (`TlsConfig::accept_self_signed`) **and** subsumes the hostname - * check (`skip_hostname_verification`), regardless of - * `check_server_certificate_hostname`. + * **Load-bearing contract:** the kernel's `DirectStatement::{Completed, + * Running}` discriminant cannot ride on these opaque `#[napi]` classes, so + * consumers MUST feature-detect via `awaitResult` (the only member unique to + * `AsyncStatement`). `Statement` (the Completed arm) MUST NOT gain an + * `awaitResult` member, or every consumer silently misroutes. The pyo3 + * binding makes the same `await_result`-probe assumption. */ - checkServerCertificate?: boolean + executeStatementDirect(sql: string, options?: ExecuteOptions | undefined | null): Promise /** - * Whether to verify that the server certificate matches the host - * (hostname-vs-SNI check), **independently** of full chain validation. + * Execute a SQL statement on the blocking (sync) path, but return a + * `CancellableExecution` handle so a concurrent JS task can cancel + * the query *while it is still running server-side*. * - * Omitted / `true` ⇒ the hostname check runs (the secure default). - * `false` ⇒ skip only the hostname check while still validating the - * chain + expiry against the trust store — for connecting via an IP - * literal or a host the cert wasn't issued for, without dropping all - * validation. Ignored (already implied) when - * `check_server_certificate` is `false`, which disables everything. + * `executeStatement` builds the kernel `Statement`, awaits the + * blocking `execute()`, and only then hands JS a `Statement` — so a + * query that runs for several seconds is uncancellable from JS on + * that path (there is no handle until the blocking call resolves). + * This method instead builds the statement, captures a detached + * `StatementCanceller` **before** dispatching `execute()`, and hands + * JS a `CancellableExecution` immediately. The caller drives the + * blocking execution via `result()` (resolves to the same + * `Statement` `executeStatement` returns) and can fire `cancel()` + * concurrently to interrupt a still-running query mid-COMPUTE. * - * Mirrors the Python connector's `_tls_verify_hostname` knob and the - * kernel's [`TlsConfig::skip_hostname_verification`] (= `!check`). + * Option semantics are identical to `executeStatement`. + * Mirrors the pyo3 `Statement.canceller()` / `Statement.execute()` + * split (PR #121): obtain the canceller before the blocking drive. */ - checkServerCertificateHostname?: boolean + executeStatementCancellable(sql: string, options?: ExecuteOptions | undefined | null): Promise /** - * PEM-encoded CA certificate bytes to add to the trust store on - * top of the system roots. Use for corporate TLS-inspecting - * proxies that re-sign TLS, or on-prem deployments with an - * internal CA. Honoured regardless of `check_server_certificate`. - * Maps onto the kernel [`TlsConfig::custom_ca_cert`]. - */ - customCaCert?: Buffer + * Submit a SQL statement and return immediately with an + * `AsyncStatement` handle, without blocking until the query + * finishes. The kernel's `Statement::submit()` sends + * `wait_timeout=0s`, so the server responds as soon as it has a + * `statement_id` (state `Pending`/`Running`); JS drives polling + * via `AsyncStatement.status()` and materialises results with + * `AsyncStatement.awaitResult()`. + * + * This is the async-execution path the Thrift backend always + * uses (`runAsync: true`): the SEA backend submits, returns a + * pending operation handle, and polls to terminal during + * fetch. Option semantics (statementConf / queryTags / + * rowLimit / positional + named params) match `executeStatement`. + * Submit always sends `wait_timeout=0s` so the call returns + * immediately; the caller drives completion via `status()` / + * `awaitResult()`. Only the blocking-vs-pending return contract + * differs from `executeStatement`. + */ + submitStatement(sql: string, options?: ExecuteOptions | undefined | null): Promise /** - * PEM-encoded client certificate for mutual TLS (mTLS). Set this - * together with `client_key_pem` when the server requires the - * client to present a certificate. A PEM carrying a leaf cert - * optionally followed by its intermediate chain is accepted. - * Maps onto the kernel [`TlsConfig::client_cert_pem`]. + * Explicit close. Awaits the server-side `DeleteSession` so the + * JS caller can observe failures (auth revoked mid-session, + * warehouse stopped, network error). Idempotent — a second call + * on an already-closed connection returns `Ok`. * - * `client_cert_pem` and `client_key_pem` must be supplied together; - * the kernel rejects setting only one at `open_session` with - * `InvalidArgument`. + * **Errors are terminal from the JS side.** The kernel session + * handle is consumed (`take()`) BEFORE the wire `DeleteSession` + * runs, because `Session::close` takes `self` by value. On `Err`, + * the napi `inner` is already `None`, so a JS-side retry sees a + * closed connection and returns `Ok(())` without re-attempting + * the wire call. The kernel's own `Drop` fire-and-forget retry + * runs once in the background — the JS caller can log the error + * but cannot drive a retry. If you need retry-on-failure + * semantics for `DeleteSession`, layer them above this method. */ - clientCertPem?: Buffer + close(): Promise /** - * PEM-encoded private key for the mTLS client certificate. Set this - * together with `client_cert_pem`. For portability across the - * kernel's TLS backends supply a PKCS#8 key (`BEGIN PRIVATE KEY`). - * Maps onto the kernel [`TlsConfig::client_key_pem`]. + * All catalogs visible to the session. + * + * JDBC `getCatalogs` shape: `TABLE_CAT: Utf8`. */ - clientKeyPem?: Buffer + listCatalogs(): Promise /** - * Extra HTTP headers to send on every request — the route for - * caller-supplied headers (the NodeJS driver's `customHeaders` and - * the composed `User-Agent`). Maps onto the kernel - * [`HttpConfig::custom_headers`]. + * Schemas filtered by catalog (exact) and schema name pattern. * - * An **ordered list** of `(name, value)` pairs, mirroring the kernel - * core's `Vec<(String, String)>` and the pyo3 binding's - * `http_headers` — order is preserved and duplicate names are - * allowed (the kernel emits each entry, and for `User-Agent` folds - * the **last** one into its base UA). + * JDBC `getSchemas` shape: `TABLE_SCHEM, TABLE_CATALOG`. + */ + listSchemas(catalog?: string | undefined | null, schemaPattern?: string | undefined | null): Promise + /** + * Tables filtered by catalog (**pattern**), schema (pattern), table + * (pattern). * - * Three names are handled specially by the kernel: - * - `Authorization` / `x-databricks-org-id` are **reserved** — a - * caller entry for either is silently dropped (skip-and-warn) so - * auth and multi-tenant routing can't be hijacked by a custom - * header. (The NodeJS driver also drops these before they cross - * the FFI, matching the Python connector's double-wall.) - * - `User-Agent` is **appended** to the kernel base UA (rather than - * replacing it), preserving the `DatabricksJDBCDriverOSS/...` - * token the SEA server keys on while still surfacing the caller's - * identity. The NodeJS driver folds its `userAgentEntry` into a - * `User-Agent` entry here. + * The catalog is an ODBC/JDBC LIKE pattern (`%` / `_`), matching + * Thrift `getTables`: a wildcard catalog matches multiple catalogs; + * a literal name (or an escaped `\_` / `\%`) takes the fast exact + * path. `undefined`/omitted catalog means "all catalogs", while an + * **empty string** means "match nothing" (zero rows) — pass + * `undefined`, not `""`, for all catalogs. The catalog pattern is + * validated (whitespace-only / NUL / >255 bytes are rejected with an + * error). + * + * JDBC `getTables` shape: 10 columns. `tableTypes`, when provided, + * filters rows by `TABLE_TYPE` kernel-side. + * + * `tableTypes` is an advisory filter. Databricks `SHOW TABLES` does + * NOT honour the table-type filter server-side; the kernel applies + * it client-side after the result returns. Callers expecting + * server-side rejection of off-type tables should not rely on this. */ - customHeaders?: Array + listTables(catalog?: string | undefined | null, schemaPattern?: string | undefined | null, tablePattern?: string | undefined | null, tableTypes?: Array | undefined | null): Promise /** - * Retry/backoff tuning — all optional. An unset field keeps the kernel's - * built-in policy (1s/60s exponential backoff, 6 total attempts, 900s - * budget). Mirrors the pyo3 binding's `retry_*` kwargs so the Node.js - * driver can forward the same retry knobs the Python connector does. + * Columns of tables matching the filter. * - * Lower bound of the exponential backoff (also clamps a server - * `Retry-After`). Maps onto [`HttpConfig::retry_min_wait`]. + * JDBC `getColumns` shape: 23 columns. */ - retryMinWaitSecs?: number + listColumns(catalog?: string | undefined | null, schemaPattern?: string | undefined | null, tablePattern?: string | undefined | null, columnPattern?: string | undefined | null): Promise /** - * Upper bound of the exponential backoff. Maps onto - * [`HttpConfig::retry_max_wait`]. + * Functions visible to the session. `catalog` is exact; + * `schemaPattern` and `functionPattern` are SQL LIKE. */ - retryMaxWaitSecs?: number + listFunctions(catalog?: string | undefined | null, schemaPattern?: string | undefined | null, functionPattern?: string | undefined | null): Promise /** - * **Total** number of attempts (matching the connector's - * `_retry_stop_after_attempts_count` and JDBC count semantics). The - * kernel's [`HttpConfig::retry_max_retries`] counts retries *after* the - * first attempt, so this is converted with `max(0, attempts - 1)` in - * [`build_http_config`] — `0` / `1` both mean a single attempt, no retry. + * Procedures visible to the session. `catalog` is exact; + * `schemaPattern` and `procedurePattern` are SQL LIKE. */ - retryMaxAttempts?: number + listProcedures(catalog?: string | undefined | null, schemaPattern?: string | undefined | null, procedurePattern?: string | undefined | null): Promise /** - * Overall retry budget in whole seconds. Maps onto - * [`HttpConfig::overall_timeout`]. + * All table types (`TABLE`, `VIEW`, `SYSTEM TABLE`, …). + * No wire call — static in-memory result. */ - retryOverallTimeoutSecs?: number + listTableTypes(): Promise /** - * Programmatic HTTP/HTTPS proxy ([`ProxyInput`]) to route all kernel - * traffic through. Carries the proxy `url`, optional basic-auth - * `username` / `password`, and an optional `bypassHosts` list — mapped - * field-for-field onto the kernel [`ProxyConfig`]. - * - * Omitted ⇒ the kernel does NOT configure a proxy explicitly and - * `reqwest`'s standard behaviour applies — the `HTTPS_PROXY` / - * `HTTP_PROXY` / `NO_PROXY` environment variables are still honoured. - * Setting this **overrides** those env vars. This complements the env-var - * path: callers who cannot set process env vars (e.g. a long-lived Node - * server) can now route a single connection through a proxy - * programmatically. + * SQL data types supported by the workspace. + * No wire call — static in-memory result. */ - proxy?: ProxyInput + listTypeInfo(): Promise /** - * Per-connection socket read timeout, in milliseconds. Caps how - * long a single HTTP round-trip may block waiting on the server - * before the request errors out. Maps onto the kernel - * [`HttpConfig::request_timeout`] (the internal reqwest - * `Client::timeout`). - * - * Omitted ⇒ kernel default (120 000 ms / 120 s). Napi-rs - * serialises `u32` as JS `number`; the largest representable value - * (~49.7 days) far exceeds any sensible socket timeout. + * Primary keys for the given table. All three identifiers are + * exact — ODBC `SQLPrimaryKeys` does not support patterns. */ - socketTimeoutMs?: number -} -/** - * Open a Databricks SQL session and return an opaque `Connection` - * wrapping the kernel `Session`. Authentication is selected by - * `options.auth_mode` (PAT / OAuth M2M / OAuth U2M) — see - * [`build_auth_config`]. - * - * The JS-visible name is `openSession` (napi-rs converts snake_case - * to camelCase for free functions). - */ -export declare function openSession(options: ConnectionOptions): Promise -/** - * One kernel log event, as handed to JS. `level` is a lower-case string - * (`error`/`warn`/`info`/`debug`/`trace`) the Node side maps onto its - * `LogLevel`; `target` is the originating `tracing` target (e.g. - * `databricks::sql::kernel`); `message` is the rendered event plus any - * structured `key=value` fields. - */ -export interface LogRecord { - level: string - target: string - message: string -} -/** - * Install (idempotently) the kernel→JS log bridge and set its level. - * - * `callback` is invoked with **an array of [`LogRecord`]s** (`(err, records)`) - * for each forwarded batch. `level` is one of - * `off`/`error`/`warn`/`info`/`debug`/`trace` (case-insensitive); unknown - * values fall back to `warn`. - * - * Safe to call more than once: the process-global subscriber is installed on - * the first call only, while every call refreshes the sink + level (last - * writer wins — see module docs). - */ -export declare function initKernelLogging(callback: (err: Error | null, arg: Array) => any, level: string): void -/** - * Snapshot of the bridge's runtime state for observability. - * - * `installed` is `true` only when the process-global subscriber was - * successfully installed by *this* bridge (and the drain thread started); - * `false` means another global subscriber was already set or the drain - * thread could not be spawned, so kernel logs are NOT reaching the JS sink. - * `dropped` is the cumulative count of records discarded because the - * bounded channel was full during a burst (drop-newest) — a nonzero, - * growing value signals the sink can't keep up. - */ -export interface KernelLoggingStats { - installed: boolean - dropped: number + getPrimaryKeys(catalog: string, schema: string, table: string): Promise + /** + * Foreign-key relationships. The foreign side must be fully + * specified (catalog + schema + table); the parent side is + * optional. All identifiers are exact — no LIKE patterns. + */ + getCrossReference(parentCatalog: string | undefined | null, parentSchema: string | undefined | null, parentTable: string | undefined | null, foreignCatalog: string, foreignSchema: string, foreignTable: string): Promise } + /** - * Return the bridge's [`KernelLoggingStats`]. Safe to call before - * `initKernelLogging` (reports `installed: false`, `dropped: 0`). - */ -export declare function kernelLoggingStats(): KernelLoggingStats -/** - * Live-retarget the bridge's level (one of - * `off`/`error`/`warn`/`info`/`debug`/`trace`, case-insensitive). - */ -export declare function setKernelLogLevel(level: string): void -/** - * JS-visible binding for a single positional parameter. + * Opaque executed-statement handle. * - * Shape mirrors the `TSparkParameter` wire object the Thrift backend - * already emits via `DBSQLParameter.toSparkParameter()` — `type` is the - * canonical Databricks SQL type name (`"INT"`, `"STRING"`, - * `"DECIMAL(10,2)"`, ...), `value` is the string-encoded literal or - * `None` for SQL NULL. + * **Current concurrency shape** — every method takes `inner.lock()` + * and holds the guard across the kernel `.await`. tokio `Mutex` is + * FIFO, so cancel/close queue behind any in-flight `fetchNextBatch` + * until it returns naturally. This is a known limitation that exists + * because the napi shape has not yet been split into an + * `Arc` (for cancel/close, which the + * kernel exposes as `&self`-callable) plus a `Mutex>` only + * for the borrowed-mut fetch path. The lock-shape refactor needs a + * small kernel-side accessor and lands in a follow-up PR — see + * `sea-workflow/jira-candidates/2026-05-24-napi-cancel-during-fetch.md`. * - * Why a string for `value` instead of a tagged JS union: round-tripping - * arbitrary JS values across the FFI requires either (a) a custom - * napi `FromNapiValue` per arm, or (b) a `serde_json::Value`-style - * dynamic dispatch on the Rust side. The Node-driver adapter already - * stringifies before calling the binding (see `DBSQLParameter` and the - * existing pyo3 wrapper), so the string-in / string-parsed contract - * adds no JS-side complexity and keeps the kernel-side validation in - * one place. + * `schema` and `statement_id` are cached at construction so they + * survive `close()` — JS callers building error reports against a + * disposed statement can still read them. */ -export interface TypedValueInput { +export declare class Statement { /** - * Canonical Databricks SQL type name. Case-insensitive for the - * simple variants; for DECIMAL the parenthesised form - * (`"DECIMAL(10,2)"`) is required so the kernel can extract - * precision/scale. + * Server-issued statement id. Cached at construction; readable + * even after `close()` so JS-side log lines can correlate against + * kernel / server logs which key on the same id. */ - sqlType: string + get statementId(): string /** - * String-encoded value. `None` always produces `TypedValue::Null` - * regardless of `sql_type` — matches the connector's - * `VoidParameter` shape and the pyo3 binding's contract. - */ - value?: string + * Number of rows modified by the statement (UPDATE / INSERT / + * DELETE / MERGE). `null` for SELECT and on warehouses that don't + * surface the counter. Mirrors Thrift's + * `TGetOperationStatusResp.numModifiedRows`. + */ + numModifiedRows(): Promise + /** + * Server-supplied user-facing message. Mirrors Thrift's + * `TGetOperationStatusResp.displayMessage`. **PII / sensitive- + * data note:** may contain SQL fragments or parameter values — + * redact before centralised logging. + * + * Populated on `Succeeded` / `Closed` paths (incl. an empty `Closed`). + * On terminal-error states (`Failed` / `Cancelled`) the kernel returns + * an Error instead of a `Statement`, and the same field rides on the JS + * Error envelope under the same `displayMessage` key. + */ + displayMessage(): Promise + /** + * Server-supplied diagnostic detail — multi-line operator / + * stack context. Mirrors Thrift's + * `TGetOperationStatusResp.diagnosticInfo`. For support surfaces, + * not user-facing. Same reachability + PII caveats as + * `displayMessage`. + */ + diagnosticInfo(): Promise + /** + * Server-supplied JSON blob with extended error details. Mirrors + * Thrift's `TGetOperationStatusResp.errorDetailsJson`. + * Pass-through string — JS callers parse with `JSON.parse` if + * they need structured access. + * + * **Server-side gating:** populated only when the workspace has + * `spark.databricks.sql.errorDetailsJson.enabled = true` on the + * underlying SQL cluster. The flag is internal-only / default- + * false in the Databricks runtime, so for most JS callers this + * will return `null`. Admin-enabled workspaces return content + * shaped like `{"errorClass": "...", "messageTemplate": "..."}`. + * + * **Unbounded:** when populated, server can return a multi-MB + * blob; size before logging. + */ + errorDetailsJson(): Promise + /** + * Pull the next batch of results. Returns `null` when the stream + * is exhausted. The returned `ArrowBatch.ipcBytes` is a complete + * Arrow IPC stream (schema header + 1 record-batch message) + * suitable for handing to `apache-arrow`'s `RecordBatchReader`. + * + * On `Err`, the stream is in an unspecified state — call + * `close()` and discard the `Statement`. Subsequent + * `fetchNextBatch()` calls after an error are not guaranteed to + * succeed or fail consistently. + */ + fetchNextBatch(): Promise + /** + * Result schema as an Arrow IPC payload (schema header only, no + * record-batch message). Available before any batches have been + * fetched, and remains available after `close()` — the kernel + * materialises the schema eagerly so JS callers can build error + * reports against a disposed statement. + * + * Sync because the body has no `.await` — `encode_ipc_stream` is + * pure CPU work over an `Arc` already cached on the + * wrapper. Mirrors `pyo3/src/statement.rs::arrow_schema` (sync). + * napi-rs converts a panic in a sync `#[napi]` entry point into a + * thrown JS error via its own macro-expanded boundary, so the + * `util::guarded` `catch_unwind` wrapper that the `async fn` + * entry points use is not required for this method. + */ + schema(): ArrowSchema + /** + * Server-side cancel. + * + * For executed statements: short-circuits to `Ok(())` if + * `fetchNextBatch` has already returned `null` (stream + * naturally exhausted) — matches the JDBC `Statement.cancel()` + * no-op-after-completion contract, so JS callers can fire cancel + * defensively without distinguishing "real cancel" from "raced + * with natural completion." + * + * For metadata streams: no-op (the kernel has no in-flight + * cancellation surface for metadata calls today). + * + * Returns `KernelError(InvalidStatementHandle)` if the statement + * has been explicitly `close()`d. + */ + cancel(): Promise + /** + * Explicit close. + * + * For executed statements: awaits the server-side `CloseStatement` + * so the JS caller can observe failures (auth revoked mid-session, + * network error, server-side error). Idempotent — a second call + * on an already-closed statement returns `Ok`. + * + * **Errors are terminal from the JS side.** The kernel executed + * handle is taken out of `inner` BEFORE the wire `CloseStatement` + * runs (so `Drop` knows there's nothing left to clean up). On + * `Err`, the napi `inner` is already `None`, so a JS-side retry + * sees a closed statement and returns `Ok(())` without re- + * attempting the wire call. The kernel-level `ExecutedStatement` + * has been consumed at that point and the value is dropped on + * the way out of the closure — the kernel's `ExecutedStatement:: + * Drop` then fires-and-forgets a single retry on the captured + * runtime. The JS caller can log the error but cannot drive a + * further retry. If you need retry-on-failure semantics for + * `CloseStatement`, layer them above this method. + * + * For metadata streams: drops the stream (no server round-trip + * needed — metadata results have no in-flight server-side + * resource to release). + */ + close(): Promise } + /** * A single Arrow IPC stream payload encoding one record batch (plus * the schema header so the JS-side reader is stateless). @@ -511,6 +563,7 @@ export interface ArrowBatch { */ ipcBytes: Buffer } + /** * An Arrow IPC stream payload encoding just the result schema (no * record-batch messages). Returned by `Statement.schema()`. @@ -523,547 +576,521 @@ export interface ArrowSchema { */ ipcBytes: Buffer } + /** - * Returns the native binding's crate version (`CARGO_PKG_VERSION`). + * Authentication mode selector crossing the napi boundary. The string + * literals are what napi-rs emits from this `#[napi(string_enum)]` — the + * NodeJS SEA adapter (`KernelAuth`) matches them verbatim (`'Pat'`, + * `'OAuthM2m'`, `'OAuthU2m'`). * - * Originally the round-1b smoke test; kept as a cheap "is the binding - * loaded?" probe for the JS-side loader's structured diagnostics. + * Mirrors the kernel [`AuthConfig`] variants this binding supports. + * `OAuthFederation` / `External` are intentionally not exposed yet — the + * kernel marks federation as not-yet-implemented and `External` is a + * Rust-trait escape hatch with no JS-callback bridge. */ -export declare function version(): string +export declare const enum AuthMode { + /** Personal access token (`token`). */ + Pat = 'Pat', + /** OAuth 2.0 machine-to-machine — `oauthClientId` + `oauthClientSecret`. */ + OAuthM2m = 'OAuthM2m', + /** + * OAuth 2.0 user-to-machine (browser flow) — optional `oauthClientId` + * + `oauthRedirectPort`. + */ + OAuthU2m = 'OAuthU2m' +} + /** - * Opaque async-statement handle. + * JS-visible options for opening a Databricks SQL session. * - * Returned by `Connection.submitStatement(...)` after the kernel - * `Statement::submit()` returns (server sent `wait_timeout=0s`, so - * the response carries a `statement_id` but the statement is still - * `Pending`/`Running`). JS drives polling via `status()` / - * `awaitResult()`. + * Authentication is selected by `authMode` (default [`AuthMode::Pat`]): + * - `Pat` — `token` required. + * - `OAuthM2m` — `oauthClientId` + `oauthClientSecret` required. + * - `OAuthU2m` — `oauthClientId` / `oauthRedirectPort` optional + * (defaults to the `databricks-sql-connector` client on port 8020). * - * Concurrency shape: `status()`, `awaitResult()`, and `close()` take - * `inner.lock()` and hold the guard across the kernel `.await` (tokio - * `Mutex` is FIFO), so `status()` / `close()` queue behind any - * in-flight `awaitResult()` until it returns naturally. `cancel()` is - * the deliberate exception: it does **not** touch `inner` — it fires - * through the detached `AsyncStatementCanceller` (session + - * statement_id, captured at construction), so an explicit - * `stmt.cancel()` interrupts an in-flight `awaitResult()` instead of - * queueing behind it. The server-side cancel flips the statement - * terminal, which the parked `awaitResult()` poll loop observes - * (`Cancelled`) and returns on. The kernel's `AwaitResultCancelGuard` - * still covers the drop-cancel case (Promise.race / timeout) - * independently — see module docs. + * Catalog / schema / sessionConf are applied once at session creation + * and remain in effect for every statement run on the resulting + * `Connection`. The SEA wire protocol carries them on + * `CreateSession`, not on `ExecuteStatement` — so there is no + * per-statement override path on this binding. */ -export declare class AsyncStatement { +export interface ConnectionOptions { /** - * Server-issued statement id. Cached at construction; readable - * even after `close()` so JS-side log lines can correlate - * against kernel / server logs which key on the same id. + * Workspace host, e.g. `adb-…azuredatabricks.net`. The kernel + * normalises this — bare hostnames get `https://` prepended. */ - get statementId(): string + hostName: string /** - * One-shot status check. Returns a string enum matching the - * kernel `StatementStatus` shape: - * `'Pending' | 'Running' | 'Succeeded' | 'Failed' | - * 'Cancelled' | 'Closed' | 'Unknown'`. (`'Unknown'` is the - * `#[non_exhaustive]` forward-compat catch-all that - * `StatementStatus::as_str` can return — consumers switching on - * the state must handle it.) Returns - * `KernelError(InvalidStatementHandle)` if the statement has - * been explicitly `close()`d. - * - * The `Failed` variant collapses to the string `'Failed'` on - * the JS side; the underlying error envelope (sql_state / - * error_code / query_id) is surfaced by `awaitResult()`'s - * rejection, which is where callers actually need the typed - * error. `status()` is intended for polling progress UIs - * that only need the state name. + * JDBC-style HTTP path, e.g. `/sql/1.0/warehouses/abc123`. The + * kernel parses out the warehouse id. */ - status(): Promise - /** Rows modified by the statement (UPDATE / INSERT / DELETE / MERGE). */ - numModifiedRows(): Promise + httpPath: string /** - * Server-supplied user-facing message (may contain SQL fragments — - * redact before centralised logging). + * Authentication mode. Omitted ⇒ [`AuthMode::Pat`] (back-compat: + * existing PAT callers pass only `token`). */ - displayMessage(): Promise - /** Server-supplied diagnostic detail. */ - diagnosticInfo(): Promise - /** Server-supplied structured error detail (JSON), when enabled. */ - errorDetailsJson(): Promise + authMode?: AuthMode /** - * Block until the server reaches a terminal state, then return - * an `AsyncResultHandle` that wraps the materialised result - * stream. The handle exposes `fetchNextBatch()` / `schema()` - * for consuming the result, plus `statementId` for log - * correlation. - * - * Drop-cancel safety: kernel `await_result` installs - * `AwaitResultCancelGuard` which fires a fire-and-forget - * `cancel_statement` if the future is dropped mid-poll - * (timeout, tokio::select! loser, JS-side `Promise.race` - * loser). The `util::guarded` `catch_unwind` here covers the - * V8-panic-across-boundary case on top. Returns - * `KernelError(InvalidStatementHandle)` if the statement has - * been explicitly `close()`d. + * Personal access token. Required (and non-empty) for + * [`AuthMode::Pat`]; ignored otherwise. */ - awaitResult(): Promise + token?: string /** - * Server-side cancel. Returns - * `KernelError(InvalidStatementHandle)` if the statement has - * been explicitly `close()`d. Idempotent against a server - * that already reached a terminal state — the kernel's - * `cancel_statement` is a no-op there. - * - * **Lock-free by design.** Unlike `status()` / `awaitResult()` / - * `close()`, this does not take `inner.lock()` — it fires through - * the detached `AsyncStatementCanceller` captured at construction. - * That lets `stmt.cancel()` interrupt an in-flight `awaitResult()` - * (which holds the mutex for the whole poll) instead of queueing - * behind it: the server-side cancel flips the statement terminal, - * the parked `awaitResult()` poll loop observes `Cancelled` and - * returns. The closed-state check reads a lock-free flag so a - * cancel after an explicit `close()` still surfaces - * `InvalidStatementHandle`. + * OAuth client id. Required for [`AuthMode::OAuthM2m`]; optional for + * [`AuthMode::OAuthU2m`] (defaults to `databricks-sql-connector`). */ - cancel(): Promise + oauthClientId?: string + /** OAuth client secret. Required for [`AuthMode::OAuthM2m`]. */ + oauthClientSecret?: string /** - * Explicit close. Idempotent — a second call on an - * already-closed handle returns `Ok(())`. On `Err`, the napi - * inner is already `None`, so a JS-side retry sees the - * closed-handle short-circuit and returns `Ok(())` without - * re-attempting the wire call. The kernel's own `Drop` - * fire-and-forget retry runs once in the background. + * Localhost callback port for the [`AuthMode::OAuthU2m`] browser + * flow. Omitted ⇒ kernel default (8020). */ - close(): Promise -} -/** - * Opaque result-fetch handle returned by - * `AsyncStatement.awaitResult()`. Wraps a kernel `ResultStream` - * directly; structurally analogous to the sync `Statement`'s - * fetch-side surface (`fetchNextBatch` / `schema` / - * `statementId`). - * - * `cancel()` / `close()` are not exposed: the parent - * `AsyncStatement` owns server-side lifecycle. A `close()` here - * would create dual-ownership of the same statement_id with - * inconsistent close semantics. Callers `close()` the parent - * `AsyncStatement` after they're done fetching. - * - * Schema is cached at construction so it survives the underlying - * stream being drained; mirrors the sync `Statement.schema()` - * post-close contract. - */ -export declare class AsyncResultHandle { + oauthRedirectPort?: number /** - * Server-issued statement id. Cached at construction; readable - * for log correlation. Matches the parent `AsyncStatement`'s - * `statementId`. + * OAuth scopes override (M2M / U2M). Omitted ⇒ kernel defaults + * (`["all-apis"]` for M2M; `["all-apis", "offline_access"]` for U2M). */ - get statementId(): string + oauthScopes?: Array /** - * Pull the next batch of results. Returns `null` when the - * stream is exhausted. The returned `ArrowBatch.ipcBytes` is a - * complete Arrow IPC stream (schema header + 1 record-batch - * message), suitable for handing to `apache-arrow`'s - * `RecordBatchReader`. Byte-identical to the sync - * `Statement.fetchNextBatch()` payload for the same query. + * Default catalog for statements executed on this session. + * Routed through the kernel's `DefaultOpts` and onto the SEA + * `CreateSession.catalog` wire field. */ - fetchNextBatch(): Promise + catalog?: string /** - * Result schema as an Arrow IPC payload (schema header only, - * no record-batch message). Available before any batches have - * been fetched. Sync because the body has no `.await` — - * `encode_ipc_stream` is pure CPU work over the cached - * `Arc`. + * Default schema for statements executed on this session. + * Routed through the kernel's `DefaultOpts` and onto the SEA + * `CreateSession.schema` wire field. */ - schema(): ArrowSchema -} -/** - * Handle returned by `Connection.executeStatementCancellable`. Owns the - * built-but-not-yet-executed kernel `Statement` plus a detached - * [`StatementCanceller`] captured before dispatch, so JS can fire a - * server-side cancel while the blocking `result()` is in flight. - * - * `pending` is `Arc>>` so `result()` can - * `.take()` the statement (the kernel `execute()` borrows it `&mut`, - * then it moves into the produced `Statement` wrapper to keep its - * `ValidityFlag` set — see `statement.rs`). A second `result()` call - * after the first resolved surfaces `InvalidStatementHandle`. - */ -export declare class CancellableExecution { + schema?: string /** - * The server-issued statement id this execution targets, if the - * server has issued one yet (`null` before the initial submit - * round-trip publishes it mid-`result()`). Useful for log - * correlation while the blocking drive is in flight. + * Server-bound session conf (Spark conf, `ANSI_MODE`, `TIMEZONE`, + * query-tag presets, …). Forwarded verbatim to SEA + * `session_confs`. Unknown keys are rejected server-side. */ - get statementId(): string | null + sessionConf?: Record /** - * Drive the blocking `execute()` and resolve to a `Statement` - * (identical to what `executeStatement` returns) once the kernel - * reaches a terminal state and the result stream is ready. - * - * Consumes the pending statement: a second `result()` call returns - * `KernelError(InvalidStatementHandle)`. The future is - * drop-cancel-safe — the kernel's per-execute `MidExecuteCancelState` - * guard fires a fire-and-forget `cancel_statement` if this future is - * dropped mid-flight (`Promise.race` / timeout loser), independently - * of an explicit `cancel()`. + * Maximum number of pooled HTTP connections per host. Routes + * through the kernel's [`HttpConfig::pool_max_idle_per_host`]. + * Tunes the underlying `reqwest` connection pool — higher values + * reduce reconnect overhead when many statements run + * concurrently against the same warehouse. * - * On a server-side cancel the kernel's blocking `execute()` currently - * surfaces `InvalidArgument` (a known kernel quirk — the async path - * returns `Cancelled`). When this handle's `cancel()` actually dispatched a - * server-side cancel, we normalise that into `Cancelled` here so JS callers - * can rely on a single cancelled-status code regardless of execution path. + * When the JS caller does NOT provide `maxConnections`, the napi + * binding applies a NodeJS-driver-appropriate default of + * [`NAPI_DEFAULT_POOL_MAX_IDLE_PER_HOST`] (100) — chosen to match + * the JDBC driver's `HttpConnectionPoolSize` default and to close + * the throughput gap vs the NodeJS Thrift driver's + * `maxSockets: Infinity` pool for bursty workloads. The kernel + * core's [`HttpConfig::pool_max_idle_per_host`] default is also 100 + * (matching the same JDBC default), so napi pins its own copy rather + * than inheriting it. Mirrors the Python connector's + * `max_connections` kwarg on the SEA backend, which exposes the + * knob but keeps its own urllib3-aligned default of 10. * - * Three outcomes can race the blocking drive: (1) a natural terminal state - * → `Ok` or the genuine error; (2) an explicit `cancel()` that dispatched a - * server cancel → this `result()` rejects with a `Cancelled`-coded error - * (the normalisation above); (3) the future being **dropped** mid-flight - * (`Promise.race`/timeout loser) → the kernel's `MidExecuteCancelState` - * drop-guard fires a fire-and-forget `cancel_statement`, but there is no - * `result()` left to observe a code. Only (2) yields a `Cancelled` error. + * Napi-rs serialises `u32` as JS `number`; values up to + * `2^32 - 1` round-trip safely (any reasonable pool size fits). */ - result(): Promise + maxConnections?: number /** - * Server-side cancel of the in-flight statement. - * - * Lock-free: fires the detached `StatementCanceller` captured at - * construction rather than taking the mutex `result()` holds, so it - * interrupts a still-running blocking `result()` instead of queueing - * behind it. No-op (returns `Ok`) if `result()` already finished - * successfully, or if no statement id has been observed yet (query still - * in its initial submit round-trip), and idempotent against a server - * already in a terminal state. + * Render `INTERVAL` / `DURATION` result columns as strings + * (`ResultConfig.intervals_as_string`). The kernel default is + * native Arrow `month_interval` / `duration[us]` types; the NodeJS + * Thrift driver surfaces intervals as strings, so the SEA driver + * sets this `true` for byte-compatible parity. Omitted ⇒ kernel + * default (native Arrow interval types). */ - cancel(): Promise -} -/** - * Opaque connection handle wrapping a kernel `Session`. - * - * `inner` is `Arc>>` so: - * - the Drop impl can clone the `Arc` and `.take()` the session on a - * background tokio task without holding `&mut self` (which Drop is - * forbidden from doing across an `await`), - * - `close()` can `.take()` the session to consume it for the kernel's - * move-by-value `Session::close(self)` signature. - * - * **Concurrency shape** — both `executeStatement` and - * `submitStatement` build the kernel `Statement` under `inner.lock()` - * and then RELEASE the guard before the wire call - * (`stmt.execute().await` / `stmt.submit().await`). `Session::statement()` - * is `&self`-callable and only clones the session's internal `Arc`, so - * the built statement is independent of the guard. Concurrent - * `Promise.all([executeStatement(q1), submitStatement(q2)])` therefore - * serialise only for the microsecond statement-build, not the network - * round-trip, and `close()` never blocks behind an in-flight execute or - * submit. See - * `sea-workflow/jira-candidates/2026-05-24-napi-cancel-during-fetch.md`. - */ -export declare class Connection { + intervalsAsString?: boolean /** - * Server-issued session id. Cached at construction; readable - * even after `close()` so JS-side log lines can correlate - * against kernel / server logs which key on the same id. + * Render complex (`ARRAY` / `MAP` / `STRUCT` / `VARIANT`) result + * columns as JSON strings (`ResultConfig.complex_types_as_json`) + * instead of native Arrow nested types. Omitted ⇒ kernel default + * (native Arrow nested types, which the NodeJS Arrow decoder + * already renders identically to the Thrift path). */ - get sessionId(): string + complexTypesAsJson?: boolean /** - * Execute a SQL statement and return a Statement handle that - * streams batches via `fetchNextBatch()`. + * Whether to verify the server's TLS certificate. * - * Catalog / schema / sessionConf are session-level - * (`openSession`). Per-statement options on `ExecuteOptions`: - * - `statementConf` — per-statement Spark conf overlay - * - `queryTags` — serialised to a comma-separated `key:value` - * string and placed in `statement_conf["query_tags"]`, - * matching NodeJS Thrift's `serializeQueryTags` wire shape + * Omitted / `true` ⇒ strict validation against the system / Mozilla + * trust store (full chain + expiry + hostname), matching JDBC / ODBC + * and every modern HTTPS client. This is the **default** for the SEA + * backend — secure by default. * - * `options` is omitted/`None` for the no-options path; passing - * `{ statementConf: {} }` (an empty map) is treated the same as - * omission to keep the wire shape stable for the common case. + * `false` ⇒ permissive: accept self-signed / untrusted / expired + * certs AND skip the hostname-vs-SNI check. This is **insecure** (no + * protection against active MITM); it exists only as an opt-out for + * parity with the legacy NodeJS Thrift driver, which hard-codes + * `rejectUnauthorized: false`. Prefer pairing strict checking with + * `custom_ca_cert` over disabling verification entirely. + * + * This is the master verify toggle: `false` disables chain validation + * (`TlsConfig::accept_self_signed`) **and** subsumes the hostname + * check (`skip_hostname_verification`), regardless of + * `check_server_certificate_hostname`. */ - executeStatement(sql: string, options?: ExecuteOptions | undefined | null): Promise + checkServerCertificate?: boolean /** - * directResults execute — the Thrift/JDBC model. Sends ExecuteStatement - * with no `wait_timeout` field (server applies its ~10s default inline wait - * and auto-closes on success) and returns WITHOUT polling past it: - * - * - a **`Statement`** (left arm) when the query finished within the inline - * wait — terminal, result ready inline, `close()` is a clean release; - * - an **`AsyncStatement`** (right arm) when it did not — a poll/cancel - * handle the caller drives (`status()` / `awaitResult()` / `cancel()`). + * Whether to verify that the server certificate matches the host + * (hostname-vs-SNI check), **independently** of full chain validation. * - * JS distinguishes the arms by feature-detecting `awaitResult` (present - * only on `AsyncStatement`). This is the path that gives mid-run cancel for - * long queries WITHOUT the eager-handle / close-drives workaround: the - * returned handle always corresponds to a server-owned statement. + * Omitted / `true` ⇒ the hostname check runs (the secure default). + * `false` ⇒ skip only the hostname check while still validating the + * chain + expiry against the trust store — for connecting via an IP + * literal or a host the cert wasn't issued for, without dropping all + * validation. Ignored (already implied) when + * `check_server_certificate` is `false`, which disables everything. * - * **Load-bearing contract:** the kernel's `DirectStatement::{Completed, - * Running}` discriminant cannot ride on these opaque `#[napi]` classes, so - * consumers MUST feature-detect via `awaitResult` (the only member unique to - * `AsyncStatement`). `Statement` (the Completed arm) MUST NOT gain an - * `awaitResult` member, or every consumer silently misroutes. The pyo3 - * binding makes the same `await_result`-probe assumption. + * Mirrors the Python connector's `_tls_verify_hostname` knob and the + * kernel's [`TlsConfig::skip_hostname_verification`] (= `!check`). */ - executeStatementDirect(sql: string, options?: ExecuteOptions | undefined | null): Promise + checkServerCertificateHostname?: boolean /** - * Execute a SQL statement on the blocking (sync) path, but return a - * `CancellableExecution` handle so a concurrent JS task can cancel - * the query *while it is still running server-side*. - * - * `executeStatement` builds the kernel `Statement`, awaits the - * blocking `execute()`, and only then hands JS a `Statement` — so a - * query that runs for several seconds is uncancellable from JS on - * that path (there is no handle until the blocking call resolves). - * This method instead builds the statement, captures a detached - * `StatementCanceller` **before** dispatching `execute()`, and hands - * JS a `CancellableExecution` immediately. The caller drives the - * blocking execution via `result()` (resolves to the same - * `Statement` `executeStatement` returns) and can fire `cancel()` - * concurrently to interrupt a still-running query mid-COMPUTE. - * - * Option semantics are identical to `executeStatement`. - * Mirrors the pyo3 `Statement.canceller()` / `Statement.execute()` - * split (PR #121): obtain the canceller before the blocking drive. + * PEM-encoded CA certificate bytes to add to the trust store on + * top of the system roots. Use for corporate TLS-inspecting + * proxies that re-sign TLS, or on-prem deployments with an + * internal CA. Honoured regardless of `check_server_certificate`. + * Maps onto the kernel [`TlsConfig::custom_ca_cert`]. */ - executeStatementCancellable(sql: string, options?: ExecuteOptions | undefined | null): Promise + customCaCert?: Buffer /** - * Submit a SQL statement and return immediately with an - * `AsyncStatement` handle, without blocking until the query - * finishes. The kernel's `Statement::submit()` sends - * `wait_timeout=0s`, so the server responds as soon as it has a - * `statement_id` (state `Pending`/`Running`); JS drives polling - * via `AsyncStatement.status()` and materialises results with - * `AsyncStatement.awaitResult()`. - * - * This is the async-execution path the Thrift backend always - * uses (`runAsync: true`): the SEA backend submits, returns a - * pending operation handle, and polls to terminal during - * fetch. Option semantics (statementConf / queryTags / - * rowLimit / positional + named params) match `executeStatement`. - * Submit always sends `wait_timeout=0s` so the call returns - * immediately; the caller drives completion via `status()` / - * `awaitResult()`. Only the blocking-vs-pending return contract - * differs from `executeStatement`. - */ - submitStatement(sql: string, options?: ExecuteOptions | undefined | null): Promise - /** - * Explicit close. Awaits the server-side `DeleteSession` so the - * JS caller can observe failures (auth revoked mid-session, - * warehouse stopped, network error). Idempotent — a second call - * on an already-closed connection returns `Ok`. - * - * **Errors are terminal from the JS side.** The kernel session - * handle is consumed (`take()`) BEFORE the wire `DeleteSession` - * runs, because `Session::close` takes `self` by value. On `Err`, - * the napi `inner` is already `None`, so a JS-side retry sees a - * closed connection and returns `Ok(())` without re-attempting - * the wire call. The kernel's own `Drop` fire-and-forget retry - * runs once in the background — the JS caller can log the error - * but cannot drive a retry. If you need retry-on-failure - * semantics for `DeleteSession`, layer them above this method. - */ - close(): Promise - /** - * All catalogs visible to the session. + * PEM-encoded client certificate for mutual TLS (mTLS). Set this + * together with `client_key_pem` when the server requires the + * client to present a certificate. A PEM carrying a leaf cert + * optionally followed by its intermediate chain is accepted. + * Maps onto the kernel [`TlsConfig::client_cert_pem`]. * - * JDBC `getCatalogs` shape: `TABLE_CAT: Utf8`. + * `client_cert_pem` and `client_key_pem` must be supplied together; + * the kernel rejects setting only one at `open_session` with + * `InvalidArgument`. */ - listCatalogs(): Promise + clientCertPem?: Buffer /** - * Schemas filtered by catalog (exact) and schema name pattern. - * - * JDBC `getSchemas` shape: `TABLE_SCHEM, TABLE_CATALOG`. + * PEM-encoded private key for the mTLS client certificate. Set this + * together with `client_cert_pem`. For portability across the + * kernel's TLS backends supply a PKCS#8 key (`BEGIN PRIVATE KEY`). + * Maps onto the kernel [`TlsConfig::client_key_pem`]. */ - listSchemas(catalog?: string | undefined | null, schemaPattern?: string | undefined | null): Promise + clientKeyPem?: Buffer /** - * Tables filtered by catalog (exact), schema (pattern), table (pattern). + * Extra HTTP headers to send on every request — the route for + * caller-supplied headers (the NodeJS driver's `customHeaders` and + * the composed `User-Agent`). Maps onto the kernel + * [`HttpConfig::custom_headers`]. * - * JDBC `getTables` shape: 10 columns. `tableTypes`, when provided, - * filters rows by `TABLE_TYPE` kernel-side. + * An **ordered list** of `(name, value)` pairs, mirroring the kernel + * core's `Vec<(String, String)>` and the pyo3 binding's + * `http_headers` — order is preserved and duplicate names are + * allowed (the kernel emits each entry, and for `User-Agent` folds + * the **last** one into its base UA). * - * `tableTypes` is an advisory filter. Databricks `SHOW TABLES` does - * NOT honour the table-type filter server-side; the kernel applies - * it client-side after the result returns. Callers expecting - * server-side rejection of off-type tables should not rely on this. + * Three names are handled specially by the kernel: + * - `Authorization` / `x-databricks-org-id` are **reserved** — a + * caller entry for either is silently dropped (skip-and-warn) so + * auth and multi-tenant routing can't be hijacked by a custom + * header. (The NodeJS driver also drops these before they cross + * the FFI, matching the Python connector's double-wall.) + * - `User-Agent` is **appended** to the kernel base UA (rather than + * replacing it), preserving the `DatabricksJDBCDriverOSS/...` + * token the SEA server keys on while still surfacing the caller's + * identity. The NodeJS driver folds its `userAgentEntry` into a + * `User-Agent` entry here. */ - listTables(catalog?: string | undefined | null, schemaPattern?: string | undefined | null, tablePattern?: string | undefined | null, tableTypes?: Array | undefined | null): Promise + customHeaders?: Array /** - * Columns of tables matching the filter. + * Retry/backoff tuning — all optional. An unset field keeps the kernel's + * built-in policy (1s/60s exponential backoff, 6 total attempts, 900s + * budget). Mirrors the pyo3 binding's `retry_*` kwargs so the Node.js + * driver can forward the same retry knobs the Python connector does. * - * JDBC `getColumns` shape: 23 columns. - */ - listColumns(catalog?: string | undefined | null, schemaPattern?: string | undefined | null, tablePattern?: string | undefined | null, columnPattern?: string | undefined | null): Promise - /** - * Functions visible to the session. `catalog` is exact; - * `schemaPattern` and `functionPattern` are SQL LIKE. + * Lower bound of the exponential backoff (also clamps a server + * `Retry-After`). Maps onto [`HttpConfig::retry_min_wait`]. */ - listFunctions(catalog?: string | undefined | null, schemaPattern?: string | undefined | null, functionPattern?: string | undefined | null): Promise + retryMinWaitSecs?: number /** - * Procedures visible to the session. `catalog` is exact; - * `schemaPattern` and `procedurePattern` are SQL LIKE. + * Upper bound of the exponential backoff. Maps onto + * [`HttpConfig::retry_max_wait`]. */ - listProcedures(catalog?: string | undefined | null, schemaPattern?: string | undefined | null, procedurePattern?: string | undefined | null): Promise + retryMaxWaitSecs?: number /** - * All table types (`TABLE`, `VIEW`, `SYSTEM TABLE`, …). - * No wire call — static in-memory result. + * **Total** number of attempts (matching the connector's + * `_retry_stop_after_attempts_count` and JDBC count semantics). The + * kernel's [`HttpConfig::retry_max_retries`] counts retries *after* the + * first attempt, so this is converted with `max(0, attempts - 1)` in + * [`build_http_config`] — `0` / `1` both mean a single attempt, no retry. */ - listTableTypes(): Promise + retryMaxAttempts?: number /** - * SQL data types supported by the workspace. - * No wire call — static in-memory result. + * Overall retry budget in whole seconds. Maps onto + * [`HttpConfig::overall_timeout`]. */ - listTypeInfo(): Promise + retryOverallTimeoutSecs?: number /** - * Primary keys for the given table. All three identifiers are - * exact — ODBC `SQLPrimaryKeys` does not support patterns. + * Programmatic HTTP/HTTPS proxy ([`ProxyInput`]) to route all kernel + * traffic through. Carries the proxy `url`, optional basic-auth + * `username` / `password`, and an optional `bypassHosts` list — mapped + * field-for-field onto the kernel [`ProxyConfig`]. + * + * Omitted ⇒ the kernel does NOT configure a proxy explicitly and + * `reqwest`'s standard behaviour applies — the `HTTPS_PROXY` / + * `HTTP_PROXY` / `NO_PROXY` environment variables are still honoured. + * Setting this **overrides** those env vars. This complements the env-var + * path: callers who cannot set process env vars (e.g. a long-lived Node + * server) can now route a single connection through a proxy + * programmatically. */ - getPrimaryKeys(catalog: string, schema: string, table: string): Promise + proxy?: ProxyInput /** - * Foreign-key relationships. The foreign side must be fully - * specified (catalog + schema + table); the parent side is - * optional. All identifiers are exact — no LIKE patterns. + * Per-connection socket read timeout, in milliseconds. Caps how + * long a single HTTP round-trip may block waiting on the server + * before the request errors out. Maps onto the kernel + * [`HttpConfig::request_timeout`] (the internal reqwest + * `Client::timeout`). + * + * Omitted ⇒ kernel default (120 000 ms / 120 s). Napi-rs + * serialises `u32` as JS `number`; the largest representable value + * (~49.7 days) far exceeds any sensible socket timeout. */ - getCrossReference(parentCatalog: string | undefined | null, parentSchema: string | undefined | null, parentTable: string | undefined | null, foreignCatalog: string, foreignSchema: string, foreignTable: string): Promise + socketTimeoutMs?: number } + /** - * Opaque executed-statement handle. + * Per-statement options for `Connection.executeStatement`. * - * **Current concurrency shape** — every method takes `inner.lock()` - * and holds the guard across the kernel `.await`. tokio `Mutex` is - * FIFO, so cancel/close queue behind any in-flight `fetchNextBatch` - * until it returns naturally. This is a known limitation that exists - * because the napi shape has not yet been split into an - * `Arc` (for cancel/close, which the - * kernel exposes as `&self`-callable) plus a `Mutex>` only - * for the borrowed-mut fetch path. The lock-shape refactor needs a - * small kernel-side accessor and lands in a follow-up PR — see - * `sea-workflow/jira-candidates/2026-05-24-napi-cancel-during-fetch.md`. + * Mirrors the kernel `StatementSpec` knobs that are safe to thread + * through napi without a kernel-side change. Today this covers: + * - `statementConf` — per-statement Spark conf overlay + * (`StatementSpec.statement_conf` → SEA `parameters` / + * Thrift `confOverlay`) + * - `queryTags` — convenience wrapper over `statementConf` with + * key `query_tags`; serialised to the same comma-separated + * `key:value` wire shape NodeJS Thrift's `serializeQueryTags` + * produces (`lib/utils/queryTags.ts`). Backslashes in keys are + * doubled; backslash/colon/comma in values are backslash-escaped. * - * `schema` and `statement_id` are cached at construction so they - * survive `close()` — JS callers building error reports against a - * disposed statement can still read them. + * `rowLimit` (SEA `row_limit`) is exposed here and threaded onto the kernel + * `StatementSpec`. `positionalParams` (`?`) and `namedParams` (`:name`) + * carry bound query parameters, decoded via `params::parse_typed_value`. + * (There is no `queryTimeoutSecs`: it abused the SEA `wait_timeout` inline-hold + * window and was removed — a real per-statement timeout is `STATEMENT_TIMEOUT`.) + * + * **Tag-order caveat (M4 parity note).** The napi `queryTags` field + * is a Rust `HashMap` whose iteration order is + * non-deterministic, so the serialised `query_tags` value may have + * a different key order than Thrift's `serializeQueryTags` (which + * iterates `Object.keys(...)` in insertion order) for the same + * input. The SEA server is order-insensitive on conf values, so + * the two are functionally equivalent. If a caller needs + * byte-identical Thrift parity, the JS adapter pre-serialises via + * `serializeQueryTags` and writes the result into + * `statementConf["query_tags"]` directly — see + * `KernelSessionBackend.executeStatement` in the NodeJS driver. This + * path is the one the production code uses. */ -export declare class Statement { - /** - * Server-issued statement id. Cached at construction; readable - * even after `close()` so JS-side log lines can correlate against - * kernel / server logs which key on the same id. - */ - get statementId(): string +export interface ExecuteOptions { /** - * Number of rows modified by the statement (UPDATE / INSERT / - * DELETE / MERGE). `null` for SELECT and on warehouses that don't - * surface the counter. Mirrors Thrift's - * `TGetOperationStatusResp.numModifiedRows`. + * Per-statement Spark conf overlay. Merged on top of the + * session-level `sessionConf` at execute time; this map wins + * on key collisions. Unknown keys are rejected by the server. */ - numModifiedRows(): Promise + statementConf?: Record /** - * Server-supplied user-facing message. Mirrors Thrift's - * `TGetOperationStatusResp.displayMessage`. **PII / sensitive- - * data note:** may contain SQL fragments or parameter values — - * redact before centralised logging. + * Query tags as key→value pairs. Serialised to a comma- + * separated `key:value` string (backslash-escaping `\`, `:`, + * `,`) and placed into `statementConf["query_tags"]`, matching + * NodeJS Thrift's `serializeQueryTags` wire shape. Passing + * both `queryTags` AND a `query_tags` key in `statementConf` + * raises `InvalidArgument` — the caller's intent is ambiguous + * so we refuse to silently pick one over the other. * - * Populated on `Succeeded` / `Closed` paths (incl. an empty `Closed`). - * On terminal-error states (`Failed` / `Cancelled`) the kernel returns - * an Error instead of a `Statement`, and the same field rides on the JS - * Error envelope under the same `displayMessage` key. - */ - displayMessage(): Promise - /** - * Server-supplied diagnostic detail — multi-line operator / - * stack context. Mirrors Thrift's - * `TGetOperationStatusResp.diagnosticInfo`. For support surfaces, - * not user-facing. Same reachability + PII caveats as - * `displayMessage`. + * A **`null`** value emits a **bare key** (no colon) — e.g. + * `{ production: null }` → `"production"` — matching the + * connectors' `key`-only tag form. + * + * See the struct-level "Tag-order caveat" for the + * HashMap-iteration-order vs `Object.keys`-iteration-order + * divergence and the byte-identical-Thrift-parity workaround. */ - diagnosticInfo(): Promise + queryTags?: Record /** - * Server-supplied JSON blob with extended error details. Mirrors - * Thrift's `TGetOperationStatusResp.errorDetailsJson`. - * Pass-through string — JS callers parse with `JSON.parse` if - * they need structured access. - * - * **Server-side gating:** populated only when the workspace has - * `spark.databricks.sql.errorDetailsJson.enabled = true` on the - * underlying SQL cluster. The flag is internal-only / default- - * false in the Databricks runtime, so for most JS callers this - * will return `null`. Admin-enabled workspaces return content - * shaped like `{"errorClass": "...", "messageTemplate": "..."}`. - * - * **Unbounded:** when populated, server can return a multi-MB - * blob; size before logging. + * Server-side cap on the number of rows this statement returns + * (SEA `row_limit`), independent of any SQL `LIMIT`. Maps to + * `StatementSpec.row_limit`. Omitted ⇒ no driver-imposed cap. */ - errorDetailsJson(): Promise + rowLimit?: number /** - * Pull the next batch of results. Returns `null` when the stream - * is exhausted. The returned `ArrowBatch.ipcBytes` is a complete - * Arrow IPC stream (schema header + 1 record-batch message) - * suitable for handing to `apache-arrow`'s `RecordBatchReader`. - * - * On `Err`, the stream is in an unspecified state — call - * `close()` and discard the `Statement`. Subsequent - * `fetchNextBatch()` calls after an error are not guaranteed to - * succeed or fail consistently. + * Positional parameters, in 1-based wire order. Index `i` in this + * Vec corresponds to the `i+1`-th `?` placeholder in the SQL. + * Each entry is a `{ sqlType, value }` pair — `value` is the + * string-encoded literal or `null` for SQL NULL. Mirrors + * `StatementSpec::positional_params`; decoded via [`parse_typed_value`]. */ - fetchNextBatch(): Promise + positionalParams?: Array /** - * Result schema as an Arrow IPC payload (schema header only, no - * record-batch message). Available before any batches have been - * fetched, and remains available after `close()` — the kernel - * materialises the schema eagerly so JS callers can build error - * reports against a disposed statement. - * - * Sync because the body has no `.await` — `encode_ipc_stream` is - * pure CPU work over an `Arc` already cached on the - * wrapper. Mirrors `pyo3/src/statement.rs::arrow_schema` (sync). - * napi-rs converts a panic in a sync `#[napi]` entry point into a - * thrown JS error via its own macro-expanded boundary, so the - * `util::guarded` `catch_unwind` wrapper that the `async fn` - * entry points use is not required for this method. + * Named parameters (`:name` placeholders). Each carries its `name` + * alongside the `{ sqlType, value? }` pair. Mapped to a kernel + * `TypedValue` via the same [`parse_typed_value`] codec and bound with + * `StatementSpec::param_named`. Named is the SEA-spec-required public + * param form (`StatementParameter.name` is `openapi_required`); + * positional is the documented-undocumented variant. The two are + * mutually exclusive at the SQL level (`?` vs `:name`). */ - schema(): ArrowSchema + namedParams?: Array +} + +/** + * A single extra HTTP header as an explicit `{ name, value }` pair. + * + * An ordered list of these (`ConnectionOptions.custom_headers`) mirrors + * the kernel core's `Vec<(String, String)>` and the pyo3 binding's + * `http_headers`: order is preserved and duplicate `name`s are allowed. + * A struct (rather than a raw `[name, value]` tuple) because napi-rs + * does not marshal Rust tuples through `#[napi(object)]` fields; the + * struct is the idiomatic, self-documenting equivalent and maps to a JS + * `{ name: string, value: string }`. + */ +export interface HeaderEntry { + name: string + value: string +} + +/** + * Install (idempotently) the kernel→JS log bridge and set its level. + * + * `callback` is invoked with **an array of [`LogRecord`]s** (`(err, records)`) + * for each forwarded batch. `level` is one of + * `off`/`error`/`warn`/`info`/`debug`/`trace` (case-insensitive); unknown + * values fall back to `warn`. + * + * Safe to call more than once: the process-global subscriber is installed on + * the first call only, while every call refreshes the sink + level (last + * writer wins — see module docs). + */ +export declare function initKernelLogging(callback: ((err: Error | null, arg: Array) => any), level: string): void + +/** + * Return the bridge's [`KernelLoggingStats`]. Safe to call before + * `initKernelLogging` (reports `installed: false`, `dropped: 0`). + */ +export declare function kernelLoggingStats(): KernelLoggingStats + +/** + * Snapshot of the bridge's runtime state for observability. + * + * `installed` is `true` only when the process-global subscriber was + * successfully installed by *this* bridge (and the drain thread started); + * `false` means another global subscriber was already set or the drain + * thread could not be spawned, so kernel logs are NOT reaching the JS sink. + * `dropped` is the cumulative count of records discarded because the + * bounded channel was full during a burst (drop-newest) — a nonzero, + * growing value signals the sink can't keep up. + */ +export interface KernelLoggingStats { + installed: boolean + dropped: number +} + +/** + * One kernel log event, as handed to JS. `level` is a lower-case string + * (`error`/`warn`/`info`/`debug`/`trace`) the Node side maps onto its + * `LogLevel`; `target` is the originating `tracing` target (e.g. + * `databricks::sql::kernel`); `message` is the rendered event plus any + * structured `key=value` fields. + */ +export interface LogRecord { + level: string + target: string + message: string +} + +/** + * A named bound parameter — a [`TypedValueInput`] plus its `:name`. Kept a + * distinct napi object (rather than an optional `name` on `TypedValueInput`) + * so the positional surface stays a clean ordered list with no name field. + */ +export interface NamedTypedValueInput { + name: string + sqlType: string + value?: string +} + +/** + * Open a Databricks SQL session and return an opaque `Connection` + * wrapping the kernel `Session`. Authentication is selected by + * `options.auth_mode` (PAT / OAuth M2M / OAuth U2M) — see + * [`build_auth_config`]. + * + * The JS-visible name is `openSession` (napi-rs converts snake_case + * to camelCase for free functions). + */ +export declare function openSession(options: ConnectionOptions): Promise + +/** + * Programmatic HTTP/HTTPS proxy configuration, mirroring the kernel's + * internal [`ProxyConfig`]. Supplied as a structured object rather than a + * flattened URL so credentials never have to be percent-encoded into the URL + * and the bypass-host list can be expressed. + * + * - `url` — proxy endpoint, e.g. `"http://proxy.corp.example.com:8080"`. Must + * use the `http://` or `https://` scheme. + * - `username` / `password` — optional proxy basic-auth, applied via + * `reqwest`'s `Proxy::basic_auth` (not embedded in the URL). + * - `bypassHosts` — optional comma-separated host/domain list that should + * bypass the proxy (e.g. `"localhost,*.internal.corp"`). + */ +export interface ProxyInput { + url: string + username?: string + password?: string + bypassHosts?: string +} + +/** + * Live-retarget the bridge's level (one of + * `off`/`error`/`warn`/`info`/`debug`/`trace`, case-insensitive). + */ +export declare function setKernelLogLevel(level: string): void + +/** + * JS-visible binding for a single positional parameter. + * + * Shape mirrors the `TSparkParameter` wire object the Thrift backend + * already emits via `DBSQLParameter.toSparkParameter()` — `type` is the + * canonical Databricks SQL type name (`"INT"`, `"STRING"`, + * `"DECIMAL(10,2)"`, ...), `value` is the string-encoded literal or + * `None` for SQL NULL. + * + * Why a string for `value` instead of a tagged JS union: round-tripping + * arbitrary JS values across the FFI requires either (a) a custom + * napi `FromNapiValue` per arm, or (b) a `serde_json::Value`-style + * dynamic dispatch on the Rust side. The Node-driver adapter already + * stringifies before calling the binding (see `DBSQLParameter` and the + * existing pyo3 wrapper), so the string-in / string-parsed contract + * adds no JS-side complexity and keeps the kernel-side validation in + * one place. + */ +export interface TypedValueInput { /** - * Server-side cancel. - * - * For executed statements: short-circuits to `Ok(())` if - * `fetchNextBatch` has already returned `null` (stream - * naturally exhausted) — matches the JDBC `Statement.cancel()` - * no-op-after-completion contract, so JS callers can fire cancel - * defensively without distinguishing "real cancel" from "raced - * with natural completion." - * - * For metadata streams: no-op (the kernel has no in-flight - * cancellation surface for metadata calls today). - * - * Returns `KernelError(InvalidStatementHandle)` if the statement - * has been explicitly `close()`d. + * Canonical Databricks SQL type name. Case-insensitive for the + * simple variants; for DECIMAL the parenthesised form + * (`"DECIMAL(10,2)"`) is required so the kernel can extract + * precision/scale. */ - cancel(): Promise + sqlType: string /** - * Explicit close. - * - * For executed statements: awaits the server-side `CloseStatement` - * so the JS caller can observe failures (auth revoked mid-session, - * network error, server-side error). Idempotent — a second call - * on an already-closed statement returns `Ok`. - * - * **Errors are terminal from the JS side.** The kernel executed - * handle is taken out of `inner` BEFORE the wire `CloseStatement` - * runs (so `Drop` knows there's nothing left to clean up). On - * `Err`, the napi `inner` is already `None`, so a JS-side retry - * sees a closed statement and returns `Ok(())` without re- - * attempting the wire call. The kernel-level `ExecutedStatement` - * has been consumed at that point and the value is dropped on - * the way out of the closure — the kernel's `ExecutedStatement:: - * Drop` then fires-and-forgets a single retry on the captured - * runtime. The JS caller can log the error but cannot drive a - * further retry. If you need retry-on-failure semantics for - * `CloseStatement`, layer them above this method. - * - * For metadata streams: drops the stream (no server round-trip - * needed — metadata results have no in-flight server-side - * resource to release). + * String-encoded value. `None` always produces `TypedValue::Null` + * regardless of `sql_type` — matches the connector's + * `VoidParameter` shape and the pyo3 binding's contract. */ - close(): Promise + value?: string } + +/** + * Returns the native binding's crate version (`CARGO_PKG_VERSION`). + * + * Originally the round-1b smoke test; kept as a cheap "is the binding + * loaded?" probe for the JS-side loader's structured diagnostics. + */ +export declare function version(): string diff --git a/native/kernel/index.js b/native/kernel/index.js index ad50ecc9..45ea1ec8 100644 --- a/native/kernel/index.js +++ b/native/kernel/index.js @@ -1,325 +1,713 @@ -/* tslint:disable */ +// prettier-ignore /* eslint-disable */ -/* prettier-ignore */ - +// @ts-nocheck /* auto-generated by NAPI-RS */ -const { existsSync, readFileSync } = require('fs') -const { join } = require('path') +const { readFileSync } = require('fs') +let nativeBinding = null +const loadErrors = [] -const { platform, arch } = process +const isMusl = () => { + let musl = false + if (process.platform === 'linux') { + musl = isMuslFromFilesystem() + if (musl === null) { + musl = isMuslFromReport() + } + if (musl === null) { + musl = isMuslFromChildProcess() + } + } + return musl +} -let nativeBinding = null -let localFileExisted = false -let loadError = null +const isFileMusl = (f) => f.includes('libc.musl-') || f.includes('ld-musl-') -function isMusl() { - // For Node 10 - if (!process.report || typeof process.report.getReport !== 'function') { - try { - const lddPath = require('child_process').execSync('which ldd').toString().trim() - return readFileSync(lddPath, 'utf8').includes('musl') - } catch (e) { +const isMuslFromFilesystem = () => { + try { + return readFileSync('/usr/bin/ldd', 'utf-8').includes('musl') + } catch { + return null + } +} + +const isMuslFromReport = () => { + let report = null + if (process.report && typeof process.report.getReport === 'function') { + process.report.excludeNetwork = true + report = process.report.getReport() + } + if (!report) { + return null + } + if (report.header && report.header.glibcVersionRuntime) { + return false + } + if (Array.isArray(report.sharedObjects)) { + if (report.sharedObjects.some(isFileMusl)) { return true } - } else { - const { glibcVersionRuntime } = process.report.getReport().header - return !glibcVersionRuntime } + return false } -switch (platform) { - case 'android': - switch (arch) { - case 'arm64': - localFileExisted = existsSync(join(__dirname, 'index.android-arm64.node')) +const isMuslFromChildProcess = () => { + try { + return require('child_process').execSync('ldd --version', { encoding: 'utf8' }).includes('musl') + } catch (e) { + // If we reach this case, we don't know if the system is musl or not, so is better to just fallback to false + return false + } +} + +function requireNative() { + if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) { + try { + return require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH); + } catch (err) { + loadErrors.push(err) + } + } else if (process.platform === 'android') { + if (process.arch === 'arm64') { + try { + return require('./index.android-arm64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@databricks/databricks-sql-kernel-android-arm64') + const bindingPackageVersion = require('@databricks/databricks-sql-kernel-android-arm64/package.json').version + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 'arm') { + try { + return require('./index.android-arm-eabi.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@databricks/databricks-sql-kernel-android-arm-eabi') + const bindingPackageVersion = require('@databricks/databricks-sql-kernel-android-arm-eabi/package.json').version + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + loadErrors.push(new Error(`Unsupported architecture on Android ${process.arch}`)) + } + } else if (process.platform === 'win32') { + if (process.arch === 'x64') { + if ((process.config && process.config.variables && process.config.variables.shlib_suffix === 'dll.a') || (process.config && process.config.variables && process.config.variables.node_target_type === 'shared_library')) { try { - if (localFileExisted) { - nativeBinding = require('./index.android-arm64.node') - } else { - nativeBinding = require('@databricks/databricks-sql-kernel-android-arm64') - } + return require('./index.win32-x64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@databricks/databricks-sql-kernel-win32-x64-gnu') + const bindingPackageVersion = require('@databricks/databricks-sql-kernel-win32-x64-gnu/package.json').version + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + try { + return require('./index.win32-x64-msvc.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@databricks/databricks-sql-kernel-win32-x64-msvc') + const bindingPackageVersion = require('@databricks/databricks-sql-kernel-win32-x64-msvc/package.json').version + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } + } else if (process.arch === 'ia32') { + try { + return require('./index.win32-ia32-msvc.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@databricks/databricks-sql-kernel-win32-ia32-msvc') + const bindingPackageVersion = require('@databricks/databricks-sql-kernel-win32-ia32-msvc/package.json').version + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 'arm64') { + try { + return require('./index.win32-arm64-msvc.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@databricks/databricks-sql-kernel-win32-arm64-msvc') + const bindingPackageVersion = require('@databricks/databricks-sql-kernel-win32-arm64-msvc/package.json').version + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + loadErrors.push(new Error(`Unsupported architecture on Windows: ${process.arch}`)) + } + } else if (process.platform === 'darwin') { + try { + return require('./index.darwin-universal.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@databricks/databricks-sql-kernel-darwin-universal') + const bindingPackageVersion = require('@databricks/databricks-sql-kernel-darwin-universal/package.json').version + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + if (process.arch === 'x64') { + try { + return require('./index.darwin-x64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@databricks/databricks-sql-kernel-darwin-x64') + const bindingPackageVersion = require('@databricks/databricks-sql-kernel-darwin-x64/package.json').version + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 'arm64') { + try { + return require('./index.darwin-arm64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@databricks/databricks-sql-kernel-darwin-arm64') + const bindingPackageVersion = require('@databricks/databricks-sql-kernel-darwin-arm64/package.json').version + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + loadErrors.push(new Error(`Unsupported architecture on macOS: ${process.arch}`)) + } + } else if (process.platform === 'freebsd') { + if (process.arch === 'x64') { + try { + return require('./index.freebsd-x64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@databricks/databricks-sql-kernel-freebsd-x64') + const bindingPackageVersion = require('@databricks/databricks-sql-kernel-freebsd-x64/package.json').version + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 'arm64') { + try { + return require('./index.freebsd-arm64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@databricks/databricks-sql-kernel-freebsd-arm64') + const bindingPackageVersion = require('@databricks/databricks-sql-kernel-freebsd-arm64/package.json').version + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + loadErrors.push(new Error(`Unsupported architecture on FreeBSD: ${process.arch}`)) + } + } else if (process.platform === 'linux') { + if (process.arch === 'x64') { + if (isMusl()) { + try { + return require('./index.linux-x64-musl.node') } catch (e) { - loadError = e + loadErrors.push(e) } - break - case 'arm': - localFileExisted = existsSync(join(__dirname, 'index.android-arm-eabi.node')) try { - if (localFileExisted) { - nativeBinding = require('./index.android-arm-eabi.node') - } else { - nativeBinding = require('@databricks/databricks-sql-kernel-android-arm-eabi') + const binding = require('@databricks/databricks-sql-kernel-linux-x64-musl') + const bindingPackageVersion = require('@databricks/databricks-sql-kernel-linux-x64-musl/package.json').version + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } + return binding } catch (e) { - loadError = e + loadErrors.push(e) } - break - default: - throw new Error(`Unsupported architecture on Android ${arch}`) - } - break - case 'win32': - switch (arch) { - case 'x64': - localFileExisted = existsSync( - join(__dirname, 'index.win32-x64-msvc.node') - ) + } else { try { - if (localFileExisted) { - nativeBinding = require('./index.win32-x64-msvc.node') - } else { - nativeBinding = require('@databricks/databricks-sql-kernel-win32-x64-msvc') - } + return require('./index.linux-x64-gnu.node') } catch (e) { - loadError = e + loadErrors.push(e) } - break - case 'ia32': - localFileExisted = existsSync( - join(__dirname, 'index.win32-ia32-msvc.node') - ) try { - if (localFileExisted) { - nativeBinding = require('./index.win32-ia32-msvc.node') - } else { - nativeBinding = require('@databricks/databricks-sql-kernel-win32-ia32-msvc') + const binding = require('@databricks/databricks-sql-kernel-linux-x64-gnu') + const bindingPackageVersion = require('@databricks/databricks-sql-kernel-linux-x64-gnu/package.json').version + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } + return binding } catch (e) { - loadError = e + loadErrors.push(e) } - break - case 'arm64': - localFileExisted = existsSync( - join(__dirname, 'index.win32-arm64-msvc.node') - ) + } + } else if (process.arch === 'arm64') { + if (isMusl()) { try { - if (localFileExisted) { - nativeBinding = require('./index.win32-arm64-msvc.node') - } else { - nativeBinding = require('@databricks/databricks-sql-kernel-win32-arm64-msvc') + return require('./index.linux-arm64-musl.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@databricks/databricks-sql-kernel-linux-arm64-musl') + const bindingPackageVersion = require('@databricks/databricks-sql-kernel-linux-arm64-musl/package.json').version + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } + return binding } catch (e) { - loadError = e + loadErrors.push(e) } - break - default: - throw new Error(`Unsupported architecture on Windows: ${arch}`) - } - break - case 'darwin': - localFileExisted = existsSync(join(__dirname, 'index.darwin-universal.node')) - try { - if (localFileExisted) { - nativeBinding = require('./index.darwin-universal.node') } else { - nativeBinding = require('@databricks/databricks-sql-kernel-darwin-universal') - } - break - } catch {} - switch (arch) { - case 'x64': - localFileExisted = existsSync(join(__dirname, 'index.darwin-x64.node')) try { - if (localFileExisted) { - nativeBinding = require('./index.darwin-x64.node') - } else { - nativeBinding = require('@databricks/databricks-sql-kernel-darwin-x64') - } + return require('./index.linux-arm64-gnu.node') } catch (e) { - loadError = e + loadErrors.push(e) } - break - case 'arm64': - localFileExisted = existsSync( - join(__dirname, 'index.darwin-arm64.node') - ) try { - if (localFileExisted) { - nativeBinding = require('./index.darwin-arm64.node') - } else { - nativeBinding = require('@databricks/databricks-sql-kernel-darwin-arm64') + const binding = require('@databricks/databricks-sql-kernel-linux-arm64-gnu') + const bindingPackageVersion = require('@databricks/databricks-sql-kernel-linux-arm64-gnu/package.json').version + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } + return binding } catch (e) { - loadError = e + loadErrors.push(e) } - break - default: - throw new Error(`Unsupported architecture on macOS: ${arch}`) - } - break - case 'freebsd': - if (arch !== 'x64') { - throw new Error(`Unsupported architecture on FreeBSD: ${arch}`) - } - localFileExisted = existsSync(join(__dirname, 'index.freebsd-x64.node')) - try { - if (localFileExisted) { - nativeBinding = require('./index.freebsd-x64.node') - } else { - nativeBinding = require('@databricks/databricks-sql-kernel-freebsd-x64') } - } catch (e) { - loadError = e - } - break - case 'linux': - switch (arch) { - case 'x64': - if (isMusl()) { - localFileExisted = existsSync( - join(__dirname, 'index.linux-x64-musl.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./index.linux-x64-musl.node') - } else { - nativeBinding = require('@databricks/databricks-sql-kernel-linux-x64-musl') - } - } catch (e) { - loadError = e - } - } else { - localFileExisted = existsSync( - join(__dirname, 'index.linux-x64-gnu.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./index.linux-x64-gnu.node') - } else { - nativeBinding = require('@databricks/databricks-sql-kernel-linux-x64-gnu') - } - } catch (e) { - loadError = e + } else if (process.arch === 'arm') { + if (isMusl()) { + try { + return require('./index.linux-arm-musleabihf.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@databricks/databricks-sql-kernel-linux-arm-musleabihf') + const bindingPackageVersion = require('@databricks/databricks-sql-kernel-linux-arm-musleabihf/package.json').version + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + try { + return require('./index.linux-arm-gnueabihf.node') + } catch (e) { + loadErrors.push(e) } - break - case 'arm64': - if (isMusl()) { - localFileExisted = existsSync( - join(__dirname, 'index.linux-arm64-musl.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./index.linux-arm64-musl.node') - } else { - nativeBinding = require('@databricks/databricks-sql-kernel-linux-arm64-musl') - } - } catch (e) { - loadError = e + try { + const binding = require('@databricks/databricks-sql-kernel-linux-arm-gnueabihf') + const bindingPackageVersion = require('@databricks/databricks-sql-kernel-linux-arm-gnueabihf/package.json').version + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } - } else { - localFileExisted = existsSync( - join(__dirname, 'index.linux-arm64-gnu.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./index.linux-arm64-gnu.node') - } else { - nativeBinding = require('@databricks/databricks-sql-kernel-linux-arm64-gnu') - } - } catch (e) { - loadError = e + return binding + } catch (e) { + loadErrors.push(e) + } + } + } else if (process.arch === 'loong64') { + if (isMusl()) { + try { + return require('./index.linux-loong64-musl.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@databricks/databricks-sql-kernel-linux-loong64-musl') + const bindingPackageVersion = require('@databricks/databricks-sql-kernel-linux-loong64-musl/package.json').version + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } + return binding + } catch (e) { + loadErrors.push(e) } - break - case 'arm': - if (isMusl()) { - localFileExisted = existsSync( - join(__dirname, 'index.linux-arm-musleabihf.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./index.linux-arm-musleabihf.node') - } else { - nativeBinding = require('@databricks/databricks-sql-kernel-linux-arm-musleabihf') - } - } catch (e) { - loadError = e + } else { + try { + return require('./index.linux-loong64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@databricks/databricks-sql-kernel-linux-loong64-gnu') + const bindingPackageVersion = require('@databricks/databricks-sql-kernel-linux-loong64-gnu/package.json').version + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } - } else { - localFileExisted = existsSync( - join(__dirname, 'index.linux-arm-gnueabihf.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./index.linux-arm-gnueabihf.node') - } else { - nativeBinding = require('@databricks/databricks-sql-kernel-linux-arm-gnueabihf') - } - } catch (e) { - loadError = e + return binding + } catch (e) { + loadErrors.push(e) + } + } + } else if (process.arch === 'riscv64') { + if (isMusl()) { + try { + return require('./index.linux-riscv64-musl.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@databricks/databricks-sql-kernel-linux-riscv64-musl') + const bindingPackageVersion = require('@databricks/databricks-sql-kernel-linux-riscv64-musl/package.json').version + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } + return binding + } catch (e) { + loadErrors.push(e) } - break - case 'riscv64': - if (isMusl()) { - localFileExisted = existsSync( - join(__dirname, 'index.linux-riscv64-musl.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./index.linux-riscv64-musl.node') - } else { - nativeBinding = require('@databricks/databricks-sql-kernel-linux-riscv64-musl') - } - } catch (e) { - loadError = e + } else { + try { + return require('./index.linux-riscv64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@databricks/databricks-sql-kernel-linux-riscv64-gnu') + const bindingPackageVersion = require('@databricks/databricks-sql-kernel-linux-riscv64-gnu/package.json').version + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } - } else { - localFileExisted = existsSync( - join(__dirname, 'index.linux-riscv64-gnu.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./index.linux-riscv64-gnu.node') - } else { - nativeBinding = require('@databricks/databricks-sql-kernel-linux-riscv64-gnu') - } - } catch (e) { - loadError = e + return binding + } catch (e) { + loadErrors.push(e) + } + } + } else if (process.arch === 'ppc64') { + try { + return require('./index.linux-ppc64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@databricks/databricks-sql-kernel-linux-ppc64-gnu') + const bindingPackageVersion = require('@databricks/databricks-sql-kernel-linux-ppc64-gnu/package.json').version + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 's390x') { + try { + return require('./index.linux-s390x-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@databricks/databricks-sql-kernel-linux-s390x-gnu') + const bindingPackageVersion = require('@databricks/databricks-sql-kernel-linux-s390x-gnu/package.json').version + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + loadErrors.push(new Error(`Unsupported architecture on Linux: ${process.arch}`)) + } + } else if (process.platform === 'openharmony') { + if (process.arch === 'arm64') { + try { + return require('./index.openharmony-arm64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@databricks/databricks-sql-kernel-openharmony-arm64') + const bindingPackageVersion = require('@databricks/databricks-sql-kernel-openharmony-arm64/package.json').version + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 'x64') { + try { + return require('./index.openharmony-x64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@databricks/databricks-sql-kernel-openharmony-x64') + const bindingPackageVersion = require('@databricks/databricks-sql-kernel-openharmony-x64/package.json').version + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 'arm') { + try { + return require('./index.openharmony-arm.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@databricks/databricks-sql-kernel-openharmony-arm') + const bindingPackageVersion = require('@databricks/databricks-sql-kernel-openharmony-arm/package.json').version + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + loadErrors.push(new Error(`Unsupported architecture on OpenHarmony: ${process.arch}`)) + } + } else { + loadErrors.push(new Error(`Unsupported OS: ${process.platform}, architecture: ${process.arch}`)) + } +} + +function createLoadErrorChain(errors) { + return errors.reduce((previous, current) => { + let message + try { + message = + current && typeof current.message === 'string' + ? current.message + : String(current) + } catch { + message = 'Unknown error' + } + const error = new Error(message) + error.cause = previous + return error + }, null) +} + +// NAPI_RS_FORCE_WASI is a tri-state flag: +// unset / any other value → native binding preferred, WASI is only a fallback +// 'true' → prefer WASI, but retain native as a lazy fallback +// 'error' → require WASI without initializing a native fallback +// Treating any non-empty string as truthy (the historical behavior) meant +// NAPI_RS_FORCE_WASI=false, NAPI_RS_FORCE_WASI=0, etc. inadvertently triggered +// the WASI path, causing ENOENT for packages shipped without a .wasi.cjs file. +// +// NAPI_RS_WASI_FLAVOR selects one exact generated flavor and implies strict +// WASI loading. It never crosses into another flavor or falls back to native. +const __napiWasiFlavors = ["wasm32-wasi"] +const __napiWasiFlavor = process.env.NAPI_RS_WASI_FLAVOR +const __napiWasiFlavorRequested = + typeof __napiWasiFlavor === 'string' && __napiWasiFlavor.length > 0 +if ( + __napiWasiFlavorRequested && + __napiWasiFlavors.indexOf(__napiWasiFlavor) === -1 +) { + throw new Error( + 'Unsupported WASI flavor "' + + __napiWasiFlavor + + '". Available flavors: ' + + __napiWasiFlavors.join(', '), + ) +} +const forceWasiError = process.env.NAPI_RS_FORCE_WASI === 'error' +const forceWasi = + process.env.NAPI_RS_FORCE_WASI === 'true' || + forceWasiError || + __napiWasiFlavorRequested + +if (!forceWasi) { + nativeBinding = requireNative() +} + +if (!nativeBinding || forceWasi) { + let wasiBinding = null + let wasiBindingLoaded = false + const wasiBindingErrors = [] + const __napiWasiResolveCandidate = (specifier, isPackage, localArtifacts) => { + try { + require.resolve(specifier) + } catch (resolveError) { + if (!resolveError || resolveError.code !== 'MODULE_NOT_FOUND') { + throw resolveError + } + if (isPackage) { + try { + require.resolve(specifier + '/package.json') + } catch (packageError) { + if (packageError && packageError.code === 'MODULE_NOT_FOUND') { + return resolveError } + // An exports restriction proves the package exists even when its + // package.json is not public. Preserve the root resolution failure. + throw resolveError } - break - case 's390x': - localFileExisted = existsSync( - join(__dirname, 'index.linux-s390x-gnu.node') - ) + // The package exists but its main/export target is broken. + throw resolveError + } + return resolveError + } + if (localArtifacts) { + let artifactError = null + for (let i = 0; i < localArtifacts.length; i++) { try { - if (localFileExisted) { - nativeBinding = require('./index.linux-s390x-gnu.node') - } else { - nativeBinding = require('@databricks/databricks-sql-kernel-linux-s390x-gnu') + require.resolve(localArtifacts[i]) + return null + } catch (resolveError) { + if (!resolveError || resolveError.code !== 'MODULE_NOT_FOUND') { + throw resolveError + } + artifactError = resolveError + } + } + return artifactError + } + return null + } + if (!wasiBindingLoaded && (!__napiWasiFlavorRequested || __napiWasiFlavor === "wasm32-wasi")) { + let candidateError = null + let candidateFailed = false + try { + candidateError = __napiWasiResolveCandidate('./index.wasi.cjs', false, ["./index.wasm32-wasi.debug.wasm","./index.wasm32-wasi.wasm"]) + candidateFailed = candidateError !== null + if (!candidateFailed) { + wasiBinding = require('./index.wasi.cjs') + nativeBinding = wasiBinding + wasiBindingLoaded = true + } + } catch (err) { + candidateError = err + candidateFailed = true + } + if (candidateFailed) { + wasiBindingErrors.push(candidateError) + loadErrors.push(candidateError) + } + } + if (!wasiBindingLoaded && (!__napiWasiFlavorRequested || __napiWasiFlavor === "wasm32-wasi")) { + let candidateError = null + let candidateFailed = false + try { + candidateError = __napiWasiResolveCandidate('@databricks/databricks-sql-kernel-wasm32-wasi', true, undefined) + candidateFailed = candidateError !== null + if (!candidateFailed) { + if (process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + const bindingPackageVersion = require('@databricks/databricks-sql-kernel-wasm32-wasi/package.json').version + if (bindingPackageVersion !== '0.2.0') { + throw new Error(`WASI binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } - } catch (e) { - loadError = e } - break - default: - throw new Error(`Unsupported architecture on Linux: ${arch}`) + wasiBinding = require('@databricks/databricks-sql-kernel-wasm32-wasi') + nativeBinding = wasiBinding + wasiBindingLoaded = true + } + } catch (err) { + candidateError = err + candidateFailed = true } - break - default: - throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`) + if (candidateFailed) { + wasiBindingErrors.push(candidateError) + loadErrors.push(candidateError) + } + } + if ( + !wasiBindingLoaded && + forceWasi && + !forceWasiError && + !__napiWasiFlavorRequested + ) { + nativeBinding = requireNative() + } + if ((forceWasiError || __napiWasiFlavorRequested) && !wasiBindingLoaded) { + const error = new Error( + __napiWasiFlavorRequested + ? 'WASI binding for flavor "' + __napiWasiFlavor + '" not found' + : 'WASI binding not found and NAPI_RS_FORCE_WASI is set to error', + ) + error.cause = createLoadErrorChain(wasiBindingErrors) + throw error + } } if (!nativeBinding) { - if (loadError) { - throw loadError + if (loadErrors.length > 0) { + const error = new Error( + `Cannot find native binding. ` + + `npm has a bug related to optional dependencies (https://github.com/npm/cli/issues/4828). ` + + 'Please try `npm i` again after removing both package-lock.json and node_modules directory.', + ) + // assign instead of the `new Error(message, { cause })` options form, + // which Node < 16.9 silently ignores + error.cause = createLoadErrorChain(loadErrors) + throw error } throw new Error(`Failed to load native binding`) } -const { AsyncStatement, AsyncResultHandle, CancellableExecution, Connection, AuthMode, openSession, initKernelLogging, kernelLoggingStats, setKernelLogLevel, Statement, version } = nativeBinding - -module.exports.AsyncStatement = AsyncStatement -module.exports.AsyncResultHandle = AsyncResultHandle -module.exports.CancellableExecution = CancellableExecution -module.exports.Connection = Connection -module.exports.AuthMode = AuthMode -module.exports.openSession = openSession -module.exports.initKernelLogging = initKernelLogging -module.exports.kernelLoggingStats = kernelLoggingStats -module.exports.setKernelLogLevel = setKernelLogLevel -module.exports.Statement = Statement -module.exports.version = version +module.exports = nativeBinding +module.exports.AsyncResultHandle = nativeBinding.AsyncResultHandle +module.exports.AsyncStatement = nativeBinding.AsyncStatement +module.exports.CancellableExecution = nativeBinding.CancellableExecution +module.exports.Connection = nativeBinding.Connection +module.exports.Statement = nativeBinding.Statement +module.exports.AuthMode = nativeBinding.AuthMode +module.exports.initKernelLogging = nativeBinding.initKernelLogging +module.exports.kernelLoggingStats = nativeBinding.kernelLoggingStats +module.exports.openSession = nativeBinding.openSession +module.exports.setKernelLogLevel = nativeBinding.setKernelLogLevel +module.exports.version = nativeBinding.version diff --git a/tests/unit/kernel/native-packaging.test.ts b/tests/unit/kernel/native-packaging.test.ts index f3f8508f..77a1c934 100644 --- a/tests/unit/kernel/native-packaging.test.ts +++ b/tests/unit/kernel/native-packaging.test.ts @@ -28,8 +28,10 @@ describe('kernel native binding — packaging (native/kernel/index.js)', () => { // depend on the module system's `__dirname`. const indexJs = readFileSync(join(process.cwd(), 'native/kernel/index.js'), 'utf8'); - // Every `require('@databricks/...')` fallback in the generated router. - const required = Array.from(indexJs.matchAll(/require\('(@databricks\/[^']+)'\)/g)).map((m) => m[1]); + // Every fallback package, including CLI v3's package.json version checks. + const required = Array.from(indexJs.matchAll(/require\('(@databricks\/[^']+)'\)/g)).map((m) => + m[1].replace(/\/package\.json$/, ''), + ); it('declares at least one @databricks/* npm fallback', () => { expect(required.length, 'no @databricks/* require() found in the router').to.be.greaterThan(0); From b39e5281493a4ba161d640a8fe99af0fb775e71f Mon Sep 17 00:00:00 2001 From: Vu Anh Phung Date: Wed, 12 Aug 2026 19:04:08 +0000 Subject: [PATCH 4/7] Select napi-rs CLI by kernel major Signed-off-by: Vu Anh Phung --- KERNEL_REV | 2 +- bin/build-native.sh | 26 + native/kernel/index.d.ts | 1787 ++++++++++---------- native/kernel/index.js | 918 +++------- package.json | 2 +- tests/unit/kernel/native-packaging.test.ts | 6 +- 6 files changed, 1175 insertions(+), 1566 deletions(-) create mode 100644 bin/build-native.sh diff --git a/KERNEL_REV b/KERNEL_REV index 1561112e..7dd91996 100644 --- a/KERNEL_REV +++ b/KERNEL_REV @@ -1 +1 @@ -0ecc09c9d0075c630df2adce1a6414051a16cc9e +0d46716c466897148dfc1d2976ff03bdf097998c diff --git a/bin/build-native.sh b/bin/build-native.sh new file mode 100644 index 00000000..93109348 --- /dev/null +++ b/bin/build-native.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +set -euo pipefail + +driver_repo=$(pwd) +kernel_repo=${DATABRICKS_SQL_KERNEL_REPO:-../../databricks-sql-kernel} +napi_dir="${kernel_repo}/napi" +napi_major=$(sed -nE 's/^napi = \{ version = "([0-9]+).*/\1/p' "${napi_dir}/Cargo.toml") + +# napi-rs v2 and v3 derive macros expect different CLI environment variables. +case "${napi_major}" in + 2) + cli=(npx --yes @napi-rs/cli@2.18.4) + ;; + 3) + cli=(npx --yes --package @napi-rs/cli@3.8.2 napi) + ;; + *) + echo "Unsupported napi-rs major version: ${napi_major:-unknown}" >&2 + exit 1 + ;; +esac + +cd "${napi_dir}" +"${cli[@]}" build --platform "${BUILD_PROFILE:---release}" +cp index.* "${driver_repo}/native/kernel/" diff --git a/native/kernel/index.d.ts b/native/kernel/index.d.ts index 5f38f927..0b042121 100644 --- a/native/kernel/index.d.ts +++ b/native/kernel/index.d.ts @@ -1,557 +1,505 @@ -/* auto-generated by NAPI-RS */ +/* tslint:disable */ /* eslint-disable */ -/** - * Opaque result-fetch handle returned by - * `AsyncStatement.awaitResult()`. Wraps a kernel `ResultStream` - * directly; structurally analogous to the sync `Statement`'s - * fetch-side surface (`fetchNextBatch` / `schema` / - * `statementId`). - * - * `cancel()` / `close()` are not exposed: the parent - * `AsyncStatement` owns server-side lifecycle. A `close()` here - * would create dual-ownership of the same statement_id with - * inconsistent close semantics. Callers `close()` the parent - * `AsyncStatement` after they're done fetching. - * - * Schema is cached at construction so it survives the underlying - * stream being drained; mirrors the sync `Statement.schema()` - * post-close contract. - */ -export declare class AsyncResultHandle { - /** - * Server-issued statement id. Cached at construction; readable - * for log correlation. Matches the parent `AsyncStatement`'s - * `statementId`. - */ - get statementId(): string - /** - * Pull the next batch of results. Returns `null` when the - * stream is exhausted. The returned `ArrowBatch.ipcBytes` is a - * complete Arrow IPC stream (schema header + 1 record-batch - * message), suitable for handing to `apache-arrow`'s - * `RecordBatchReader`. Byte-identical to the sync - * `Statement.fetchNextBatch()` payload for the same query. - */ - fetchNextBatch(): Promise - /** - * Result schema as an Arrow IPC payload (schema header only, - * no record-batch message). Available before any batches have - * been fetched. Sync because the body has no `.await` — - * `encode_ipc_stream` is pure CPU work over the cached - * `Arc`. - */ - schema(): ArrowSchema -} + +/* auto-generated by NAPI-RS */ /** - * Opaque async-statement handle. + * Per-statement options for `Connection.executeStatement`. * - * Returned by `Connection.submitStatement(...)` after the kernel - * `Statement::submit()` returns (server sent `wait_timeout=0s`, so - * the response carries a `statement_id` but the statement is still - * `Pending`/`Running`). JS drives polling via `status()` / - * `awaitResult()`. + * Mirrors the kernel `StatementSpec` knobs that are safe to thread + * through napi without a kernel-side change. Today this covers: + * - `statementConf` — per-statement Spark conf overlay + * (`StatementSpec.statement_conf` → SEA `parameters` / + * Thrift `confOverlay`) + * - `queryTags` — convenience wrapper over `statementConf` with + * key `query_tags`; serialised to the same comma-separated + * `key:value` wire shape NodeJS Thrift's `serializeQueryTags` + * produces (`lib/utils/queryTags.ts`). Backslashes in keys are + * doubled; backslash/colon/comma in values are backslash-escaped. * - * Concurrency shape: `status()`, `awaitResult()`, and `close()` take - * `inner.lock()` and hold the guard across the kernel `.await` (tokio - * `Mutex` is FIFO), so `status()` / `close()` queue behind any - * in-flight `awaitResult()` until it returns naturally. `cancel()` is - * the deliberate exception: it does **not** touch `inner` — it fires - * through the detached `AsyncStatementCanceller` (session + - * statement_id, captured at construction), so an explicit - * `stmt.cancel()` interrupts an in-flight `awaitResult()` instead of - * queueing behind it. The server-side cancel flips the statement - * terminal, which the parked `awaitResult()` poll loop observes - * (`Cancelled`) and returns on. The kernel's `AwaitResultCancelGuard` - * still covers the drop-cancel case (Promise.race / timeout) - * independently — see module docs. + * `rowLimit` (SEA `row_limit`) is exposed here and threaded onto the kernel + * `StatementSpec`. `positionalParams` (`?`) and `namedParams` (`:name`) + * carry bound query parameters, decoded via `params::parse_typed_value`. + * (There is no `queryTimeoutSecs`: it abused the SEA `wait_timeout` inline-hold + * window and was removed — a real per-statement timeout is `STATEMENT_TIMEOUT`.) + * + * **Tag-order caveat (M4 parity note).** The napi `queryTags` field + * is a Rust `HashMap` whose iteration order is + * non-deterministic, so the serialised `query_tags` value may have + * a different key order than Thrift's `serializeQueryTags` (which + * iterates `Object.keys(...)` in insertion order) for the same + * input. The SEA server is order-insensitive on conf values, so + * the two are functionally equivalent. If a caller needs + * byte-identical Thrift parity, the JS adapter pre-serialises via + * `serializeQueryTags` and writes the result into + * `statementConf["query_tags"]` directly — see + * `KernelSessionBackend.executeStatement` in the NodeJS driver. This + * path is the one the production code uses. */ -export declare class AsyncStatement { +export interface ExecuteOptions { /** - * Server-issued statement id. Cached at construction; readable - * even after `close()` so JS-side log lines can correlate - * against kernel / server logs which key on the same id. + * Per-statement Spark conf overlay. Merged on top of the + * session-level `sessionConf` at execute time; this map wins + * on key collisions. Unknown keys are rejected by the server. */ - get statementId(): string + statementConf?: Record /** - * One-shot status check. Returns a string enum matching the - * kernel `StatementStatus` shape: - * `'Pending' | 'Running' | 'Succeeded' | 'Failed' | - * 'Cancelled' | 'Closed' | 'Unknown'`. (`'Unknown'` is the - * `#[non_exhaustive]` forward-compat catch-all that - * `StatementStatus::as_str` can return — consumers switching on - * the state must handle it.) Returns - * `KernelError(InvalidStatementHandle)` if the statement has - * been explicitly `close()`d. + * Query tags as key→value pairs. Serialised to a comma- + * separated `key:value` string (backslash-escaping `\`, `:`, + * `,`) and placed into `statementConf["query_tags"]`, matching + * NodeJS Thrift's `serializeQueryTags` wire shape. Passing + * both `queryTags` AND a `query_tags` key in `statementConf` + * raises `InvalidArgument` — the caller's intent is ambiguous + * so we refuse to silently pick one over the other. * - * The `Failed` variant collapses to the string `'Failed'` on - * the JS side; the underlying error envelope (sql_state / - * error_code / query_id) is surfaced by `awaitResult()`'s - * rejection, which is where callers actually need the typed - * error. `status()` is intended for polling progress UIs - * that only need the state name. - */ - status(): Promise - /** Rows modified by the statement (UPDATE / INSERT / DELETE / MERGE). */ - numModifiedRows(): Promise - /** - * Server-supplied user-facing message (may contain SQL fragments — - * redact before centralised logging). + * A **`null`** value emits a **bare key** (no colon) — e.g. + * `{ production: null }` → `"production"` — matching the + * connectors' `key`-only tag form. + * + * See the struct-level "Tag-order caveat" for the + * HashMap-iteration-order vs `Object.keys`-iteration-order + * divergence and the byte-identical-Thrift-parity workaround. */ - displayMessage(): Promise - /** Server-supplied diagnostic detail. */ - diagnosticInfo(): Promise - /** Server-supplied structured error detail (JSON), when enabled. */ - errorDetailsJson(): Promise + queryTags?: Record /** - * Block until the server reaches a terminal state, then return - * an `AsyncResultHandle` that wraps the materialised result - * stream. The handle exposes `fetchNextBatch()` / `schema()` - * for consuming the result, plus `statementId` for log - * correlation. - * - * Drop-cancel safety: kernel `await_result` installs - * `AwaitResultCancelGuard` which fires a fire-and-forget - * `cancel_statement` if the future is dropped mid-poll - * (timeout, tokio::select! loser, JS-side `Promise.race` - * loser). The `util::guarded` `catch_unwind` here covers the - * V8-panic-across-boundary case on top. Returns - * `KernelError(InvalidStatementHandle)` if the statement has - * been explicitly `close()`d. + * Server-side cap on the number of rows this statement returns + * (SEA `row_limit`), independent of any SQL `LIMIT`. Maps to + * `StatementSpec.row_limit`. Omitted ⇒ no driver-imposed cap. */ - awaitResult(): Promise + rowLimit?: number /** - * Server-side cancel. Returns - * `KernelError(InvalidStatementHandle)` if the statement has - * been explicitly `close()`d. Idempotent against a server - * that already reached a terminal state — the kernel's - * `cancel_statement` is a no-op there. - * - * **Lock-free by design.** Unlike `status()` / `awaitResult()` / - * `close()`, this does not take `inner.lock()` — it fires through - * the detached `AsyncStatementCanceller` captured at construction. - * That lets `stmt.cancel()` interrupt an in-flight `awaitResult()` - * (which holds the mutex for the whole poll) instead of queueing - * behind it: the server-side cancel flips the statement terminal, - * the parked `awaitResult()` poll loop observes `Cancelled` and - * returns. The closed-state check reads a lock-free flag so a - * cancel after an explicit `close()` still surfaces - * `InvalidStatementHandle`. + * Positional parameters, in 1-based wire order. Index `i` in this + * Vec corresponds to the `i+1`-th `?` placeholder in the SQL. + * Each entry is a `{ sqlType, value }` pair — `value` is the + * string-encoded literal or `null` for SQL NULL. Mirrors + * `StatementSpec::positional_params`; decoded via [`parse_typed_value`]. */ - cancel(): Promise + positionalParams?: Array /** - * Explicit close. Idempotent — a second call on an - * already-closed handle returns `Ok(())`. On `Err`, the napi - * inner is already `None`, so a JS-side retry sees the - * closed-handle short-circuit and returns `Ok(())` without - * re-attempting the wire call. The kernel's own `Drop` - * fire-and-forget retry runs once in the background. + * Named parameters (`:name` placeholders). Each carries its `name` + * alongside the `{ sqlType, value? }` pair. Mapped to a kernel + * `TypedValue` via the same [`parse_typed_value`] codec and bound with + * `StatementSpec::param_named`. Named is the SEA-spec-required public + * param form (`StatementParameter.name` is `openapi_required`); + * positional is the documented-undocumented variant. The two are + * mutually exclusive at the SQL level (`?` vs `:name`). */ - close(): Promise + namedParams?: Array } - /** - * Handle returned by `Connection.executeStatementCancellable`. Owns the - * built-but-not-yet-executed kernel `Statement` plus a detached - * [`StatementCanceller`] captured before dispatch, so JS can fire a - * server-side cancel while the blocking `result()` is in flight. + * A named bound parameter — a [`TypedValueInput`] plus its `:name`. Kept a + * distinct napi object (rather than an optional `name` on `TypedValueInput`) + * so the positional surface stays a clean ordered list with no name field. + */ +export interface NamedTypedValueInput { + name: string + sqlType: string + value?: string +} +/** + * Authentication mode selector crossing the napi boundary. The string + * literals are what napi-rs emits from this `#[napi(string_enum)]` — the + * NodeJS SEA adapter (`KernelAuth`) matches them verbatim (`'Pat'`, + * `'OAuthM2m'`, `'OAuthU2m'`). * - * `pending` is `Arc>>` so `result()` can - * `.take()` the statement (the kernel `execute()` borrows it `&mut`, - * then it moves into the produced `Statement` wrapper to keep its - * `ValidityFlag` set — see `statement.rs`). A second `result()` call - * after the first resolved surfaces `InvalidStatementHandle`. + * Mirrors the kernel [`AuthConfig`] variants this binding supports. + * `OAuthFederation` / `External` are intentionally not exposed yet — the + * kernel marks federation as not-yet-implemented and `External` is a + * Rust-trait escape hatch with no JS-callback bridge. */ -export declare class CancellableExecution { +export const enum AuthMode { + /** Personal access token (`token`). */ + Pat = 'Pat', + /** OAuth 2.0 machine-to-machine — `oauthClientId` + `oauthClientSecret`. */ + OAuthM2m = 'OAuthM2m', /** - * The server-issued statement id this execution targets, if the - * server has issued one yet (`null` before the initial submit - * round-trip publishes it mid-`result()`). Useful for log - * correlation while the blocking drive is in flight. + * OAuth 2.0 user-to-machine (browser flow) — optional `oauthClientId` + * + `oauthRedirectPort`. */ - get statementId(): string | null - /** - * Drive the blocking `execute()` and resolve to a `Statement` - * (identical to what `executeStatement` returns) once the kernel - * reaches a terminal state and the result stream is ready. - * - * Consumes the pending statement: a second `result()` call returns - * `KernelError(InvalidStatementHandle)`. The future is - * drop-cancel-safe — the kernel's per-execute `MidExecuteCancelState` - * guard fires a fire-and-forget `cancel_statement` if this future is - * dropped mid-flight (`Promise.race` / timeout loser), independently - * of an explicit `cancel()`. - * - * On a server-side cancel the kernel's blocking `execute()` currently - * surfaces `InvalidArgument` (a known kernel quirk — the async path - * returns `Cancelled`). When this handle's `cancel()` actually dispatched a - * server-side cancel, we normalise that into `Cancelled` here so JS callers - * can rely on a single cancelled-status code regardless of execution path. - * - * Three outcomes can race the blocking drive: (1) a natural terminal state - * → `Ok` or the genuine error; (2) an explicit `cancel()` that dispatched a - * server cancel → this `result()` rejects with a `Cancelled`-coded error - * (the normalisation above); (3) the future being **dropped** mid-flight - * (`Promise.race`/timeout loser) → the kernel's `MidExecuteCancelState` - * drop-guard fires a fire-and-forget `cancel_statement`, but there is no - * `result()` left to observe a code. Only (2) yields a `Cancelled` error. - */ - result(): Promise - /** - * Server-side cancel of the in-flight statement. - * - * Lock-free: fires the detached `StatementCanceller` captured at - * construction rather than taking the mutex `result()` holds, so it - * interrupts a still-running blocking `result()` instead of queueing - * behind it. No-op (returns `Ok`) if `result()` already finished - * successfully, or if no statement id has been observed yet (query still - * in its initial submit round-trip), and idempotent against a server - * already in a terminal state. - */ - cancel(): Promise + OAuthU2m = 'OAuthU2m' } - /** - * Opaque connection handle wrapping a kernel `Session`. + * A single extra HTTP header as an explicit `{ name, value }` pair. * - * `inner` is `Arc>>` so: - * - the Drop impl can clone the `Arc` and `.take()` the session on a - * background tokio task without holding `&mut self` (which Drop is - * forbidden from doing across an `await`), - * - `close()` can `.take()` the session to consume it for the kernel's - * move-by-value `Session::close(self)` signature. + * An ordered list of these (`ConnectionOptions.custom_headers`) mirrors + * the kernel core's `Vec<(String, String)>` and the pyo3 binding's + * `http_headers`: order is preserved and duplicate `name`s are allowed. + * A struct (rather than a raw `[name, value]` tuple) because napi-rs + * does not marshal Rust tuples through `#[napi(object)]` fields; the + * struct is the idiomatic, self-documenting equivalent and maps to a JS + * `{ name: string, value: string }`. + */ +export interface HeaderEntry { + name: string + value: string +} +/** + * Programmatic HTTP/HTTPS proxy configuration, mirroring the kernel's + * internal [`ProxyConfig`]. Supplied as a structured object rather than a + * flattened URL so credentials never have to be percent-encoded into the URL + * and the bypass-host list can be expressed. * - * **Concurrency shape** — both `executeStatement` and - * `submitStatement` build the kernel `Statement` under `inner.lock()` - * and then RELEASE the guard before the wire call - * (`stmt.execute().await` / `stmt.submit().await`). `Session::statement()` - * is `&self`-callable and only clones the session's internal `Arc`, so - * the built statement is independent of the guard. Concurrent - * `Promise.all([executeStatement(q1), submitStatement(q2)])` therefore - * serialise only for the microsecond statement-build, not the network - * round-trip, and `close()` never blocks behind an in-flight execute or - * submit. See - * `sea-workflow/jira-candidates/2026-05-24-napi-cancel-during-fetch.md`. + * - `url` — proxy endpoint, e.g. `"http://proxy.corp.example.com:8080"`. Must + * use the `http://` or `https://` scheme. + * - `username` / `password` — optional proxy basic-auth, applied via + * `reqwest`'s `Proxy::basic_auth` (not embedded in the URL). + * - `bypassHosts` — optional comma-separated host/domain list that should + * bypass the proxy (e.g. `"localhost,*.internal.corp"`). */ -export declare class Connection { - /** - * Server-issued session id. Cached at construction; readable - * even after `close()` so JS-side log lines can correlate - * against kernel / server logs which key on the same id. - */ - get sessionId(): string - /** - * Execute a SQL statement and return a Statement handle that - * streams batches via `fetchNextBatch()`. - * - * Catalog / schema / sessionConf are session-level - * (`openSession`). Per-statement options on `ExecuteOptions`: - * - `statementConf` — per-statement Spark conf overlay - * - `queryTags` — serialised to a comma-separated `key:value` - * string and placed in `statement_conf["query_tags"]`, - * matching NodeJS Thrift's `serializeQueryTags` wire shape - * - * `options` is omitted/`None` for the no-options path; passing - * `{ statementConf: {} }` (an empty map) is treated the same as - * omission to keep the wire shape stable for the common case. - */ - executeStatement(sql: string, options?: ExecuteOptions | undefined | null): Promise - /** - * directResults execute — the Thrift/JDBC model. Sends ExecuteStatement - * with no `wait_timeout` field (server applies its ~10s default inline wait - * and auto-closes on success) and returns WITHOUT polling past it: - * - * - a **`Statement`** (left arm) when the query finished within the inline - * wait — terminal, result ready inline, `close()` is a clean release; - * - an **`AsyncStatement`** (right arm) when it did not — a poll/cancel - * handle the caller drives (`status()` / `awaitResult()` / `cancel()`). - * - * JS distinguishes the arms by feature-detecting `awaitResult` (present - * only on `AsyncStatement`). This is the path that gives mid-run cancel for - * long queries WITHOUT the eager-handle / close-drives workaround: the - * returned handle always corresponds to a server-owned statement. - * - * **Load-bearing contract:** the kernel's `DirectStatement::{Completed, - * Running}` discriminant cannot ride on these opaque `#[napi]` classes, so - * consumers MUST feature-detect via `awaitResult` (the only member unique to - * `AsyncStatement`). `Statement` (the Completed arm) MUST NOT gain an - * `awaitResult` member, or every consumer silently misroutes. The pyo3 - * binding makes the same `await_result`-probe assumption. - */ - executeStatementDirect(sql: string, options?: ExecuteOptions | undefined | null): Promise +export interface ProxyInput { + url: string + username?: string + password?: string + bypassHosts?: string +} +/** + * JS-visible options for opening a Databricks SQL session. + * + * Authentication is selected by `authMode` (default [`AuthMode::Pat`]): + * - `Pat` — `token` required. + * - `OAuthM2m` — `oauthClientId` + `oauthClientSecret` required. + * - `OAuthU2m` — `oauthClientId` / `oauthRedirectPort` optional + * (defaults to the `databricks-sql-connector` client on port 8020). + * + * Catalog / schema / sessionConf are applied once at session creation + * and remain in effect for every statement run on the resulting + * `Connection`. The SEA wire protocol carries them on + * `CreateSession`, not on `ExecuteStatement` — so there is no + * per-statement override path on this binding. + */ +export interface ConnectionOptions { /** - * Execute a SQL statement on the blocking (sync) path, but return a - * `CancellableExecution` handle so a concurrent JS task can cancel - * the query *while it is still running server-side*. - * - * `executeStatement` builds the kernel `Statement`, awaits the - * blocking `execute()`, and only then hands JS a `Statement` — so a - * query that runs for several seconds is uncancellable from JS on - * that path (there is no handle until the blocking call resolves). - * This method instead builds the statement, captures a detached - * `StatementCanceller` **before** dispatching `execute()`, and hands - * JS a `CancellableExecution` immediately. The caller drives the - * blocking execution via `result()` (resolves to the same - * `Statement` `executeStatement` returns) and can fire `cancel()` - * concurrently to interrupt a still-running query mid-COMPUTE. - * - * Option semantics are identical to `executeStatement`. - * Mirrors the pyo3 `Statement.canceller()` / `Statement.execute()` - * split (PR #121): obtain the canceller before the blocking drive. + * Workspace host, e.g. `adb-…azuredatabricks.net`. The kernel + * normalises this — bare hostnames get `https://` prepended. */ - executeStatementCancellable(sql: string, options?: ExecuteOptions | undefined | null): Promise + hostName: string /** - * Submit a SQL statement and return immediately with an - * `AsyncStatement` handle, without blocking until the query - * finishes. The kernel's `Statement::submit()` sends - * `wait_timeout=0s`, so the server responds as soon as it has a - * `statement_id` (state `Pending`/`Running`); JS drives polling - * via `AsyncStatement.status()` and materialises results with - * `AsyncStatement.awaitResult()`. - * - * This is the async-execution path the Thrift backend always - * uses (`runAsync: true`): the SEA backend submits, returns a - * pending operation handle, and polls to terminal during - * fetch. Option semantics (statementConf / queryTags / - * rowLimit / positional + named params) match `executeStatement`. - * Submit always sends `wait_timeout=0s` so the call returns - * immediately; the caller drives completion via `status()` / - * `awaitResult()`. Only the blocking-vs-pending return contract - * differs from `executeStatement`. + * JDBC-style HTTP path, e.g. `/sql/1.0/warehouses/abc123`. The + * kernel parses out the warehouse id. */ - submitStatement(sql: string, options?: ExecuteOptions | undefined | null): Promise + httpPath: string /** - * Explicit close. Awaits the server-side `DeleteSession` so the - * JS caller can observe failures (auth revoked mid-session, - * warehouse stopped, network error). Idempotent — a second call - * on an already-closed connection returns `Ok`. - * - * **Errors are terminal from the JS side.** The kernel session - * handle is consumed (`take()`) BEFORE the wire `DeleteSession` - * runs, because `Session::close` takes `self` by value. On `Err`, - * the napi `inner` is already `None`, so a JS-side retry sees a - * closed connection and returns `Ok(())` without re-attempting - * the wire call. The kernel's own `Drop` fire-and-forget retry - * runs once in the background — the JS caller can log the error - * but cannot drive a retry. If you need retry-on-failure - * semantics for `DeleteSession`, layer them above this method. + * Authentication mode. Omitted ⇒ [`AuthMode::Pat`] (back-compat: + * existing PAT callers pass only `token`). */ - close(): Promise + authMode?: AuthMode /** - * All catalogs visible to the session. - * - * JDBC `getCatalogs` shape: `TABLE_CAT: Utf8`. + * Personal access token. Required (and non-empty) for + * [`AuthMode::Pat`]; ignored otherwise. */ - listCatalogs(): Promise + token?: string /** - * Schemas filtered by catalog (exact) and schema name pattern. - * - * JDBC `getSchemas` shape: `TABLE_SCHEM, TABLE_CATALOG`. + * OAuth client id. Required for [`AuthMode::OAuthM2m`]; optional for + * [`AuthMode::OAuthU2m`] (defaults to `databricks-sql-connector`). */ - listSchemas(catalog?: string | undefined | null, schemaPattern?: string | undefined | null): Promise + oauthClientId?: string + /** OAuth client secret. Required for [`AuthMode::OAuthM2m`]. */ + oauthClientSecret?: string /** - * Tables filtered by catalog (**pattern**), schema (pattern), table - * (pattern). - * - * The catalog is an ODBC/JDBC LIKE pattern (`%` / `_`), matching - * Thrift `getTables`: a wildcard catalog matches multiple catalogs; - * a literal name (or an escaped `\_` / `\%`) takes the fast exact - * path. `undefined`/omitted catalog means "all catalogs", while an - * **empty string** means "match nothing" (zero rows) — pass - * `undefined`, not `""`, for all catalogs. The catalog pattern is - * validated (whitespace-only / NUL / >255 bytes are rejected with an - * error). - * - * JDBC `getTables` shape: 10 columns. `tableTypes`, when provided, - * filters rows by `TABLE_TYPE` kernel-side. - * - * `tableTypes` is an advisory filter. Databricks `SHOW TABLES` does - * NOT honour the table-type filter server-side; the kernel applies - * it client-side after the result returns. Callers expecting - * server-side rejection of off-type tables should not rely on this. + * Localhost callback port for the [`AuthMode::OAuthU2m`] browser + * flow. Omitted ⇒ kernel default (8020). */ - listTables(catalog?: string | undefined | null, schemaPattern?: string | undefined | null, tablePattern?: string | undefined | null, tableTypes?: Array | undefined | null): Promise + oauthRedirectPort?: number /** - * Columns of tables matching the filter. - * - * JDBC `getColumns` shape: 23 columns. + * OAuth scopes override (M2M / U2M). Omitted ⇒ kernel defaults + * (`["all-apis"]` for M2M; `["all-apis", "offline_access"]` for U2M). */ - listColumns(catalog?: string | undefined | null, schemaPattern?: string | undefined | null, tablePattern?: string | undefined | null, columnPattern?: string | undefined | null): Promise + oauthScopes?: Array /** - * Functions visible to the session. `catalog` is exact; - * `schemaPattern` and `functionPattern` are SQL LIKE. + * Default catalog for statements executed on this session. + * Routed through the kernel's `DefaultOpts` and onto the SEA + * `CreateSession.catalog` wire field. */ - listFunctions(catalog?: string | undefined | null, schemaPattern?: string | undefined | null, functionPattern?: string | undefined | null): Promise + catalog?: string /** - * Procedures visible to the session. `catalog` is exact; - * `schemaPattern` and `procedurePattern` are SQL LIKE. + * Default schema for statements executed on this session. + * Routed through the kernel's `DefaultOpts` and onto the SEA + * `CreateSession.schema` wire field. */ - listProcedures(catalog?: string | undefined | null, schemaPattern?: string | undefined | null, procedurePattern?: string | undefined | null): Promise + schema?: string /** - * All table types (`TABLE`, `VIEW`, `SYSTEM TABLE`, …). - * No wire call — static in-memory result. + * Server-bound session conf (Spark conf, `ANSI_MODE`, `TIMEZONE`, + * query-tag presets, …). Forwarded verbatim to SEA + * `session_confs`. Unknown keys are rejected server-side. */ - listTableTypes(): Promise + sessionConf?: Record /** - * SQL data types supported by the workspace. - * No wire call — static in-memory result. + * Maximum number of pooled HTTP connections per host. Routes + * through the kernel's [`HttpConfig::pool_max_idle_per_host`]. + * Tunes the underlying `reqwest` connection pool — higher values + * reduce reconnect overhead when many statements run + * concurrently against the same warehouse. + * + * When the JS caller does NOT provide `maxConnections`, the napi + * binding applies a NodeJS-driver-appropriate default of + * [`NAPI_DEFAULT_POOL_MAX_IDLE_PER_HOST`] (100) — chosen to match + * the JDBC driver's `HttpConnectionPoolSize` default and to close + * the throughput gap vs the NodeJS Thrift driver's + * `maxSockets: Infinity` pool for bursty workloads. The kernel + * core's [`HttpConfig::pool_max_idle_per_host`] default is also 100 + * (matching the same JDBC default), so napi pins its own copy rather + * than inheriting it. Mirrors the Python connector's + * `max_connections` kwarg on the SEA backend, which exposes the + * knob but keeps its own urllib3-aligned default of 10. + * + * Napi-rs serialises `u32` as JS `number`; values up to + * `2^32 - 1` round-trip safely (any reasonable pool size fits). */ - listTypeInfo(): Promise + maxConnections?: number /** - * Primary keys for the given table. All three identifiers are - * exact — ODBC `SQLPrimaryKeys` does not support patterns. + * Render `INTERVAL` / `DURATION` result columns as strings + * (`ResultConfig.intervals_as_string`). The kernel default is + * native Arrow `month_interval` / `duration[us]` types; the NodeJS + * Thrift driver surfaces intervals as strings, so the SEA driver + * sets this `true` for byte-compatible parity. Omitted ⇒ kernel + * default (native Arrow interval types). */ - getPrimaryKeys(catalog: string, schema: string, table: string): Promise + intervalsAsString?: boolean /** - * Foreign-key relationships. The foreign side must be fully - * specified (catalog + schema + table); the parent side is - * optional. All identifiers are exact — no LIKE patterns. + * Render complex (`ARRAY` / `MAP` / `STRUCT` / `VARIANT`) result + * columns as JSON strings (`ResultConfig.complex_types_as_json`) + * instead of native Arrow nested types. Omitted ⇒ kernel default + * (native Arrow nested types, which the NodeJS Arrow decoder + * already renders identically to the Thrift path). */ - getCrossReference(parentCatalog: string | undefined | null, parentSchema: string | undefined | null, parentTable: string | undefined | null, foreignCatalog: string, foreignSchema: string, foreignTable: string): Promise -} - -/** - * Opaque executed-statement handle. - * - * **Current concurrency shape** — every method takes `inner.lock()` - * and holds the guard across the kernel `.await`. tokio `Mutex` is - * FIFO, so cancel/close queue behind any in-flight `fetchNextBatch` - * until it returns naturally. This is a known limitation that exists - * because the napi shape has not yet been split into an - * `Arc` (for cancel/close, which the - * kernel exposes as `&self`-callable) plus a `Mutex>` only - * for the borrowed-mut fetch path. The lock-shape refactor needs a - * small kernel-side accessor and lands in a follow-up PR — see - * `sea-workflow/jira-candidates/2026-05-24-napi-cancel-during-fetch.md`. - * - * `schema` and `statement_id` are cached at construction so they - * survive `close()` — JS callers building error reports against a - * disposed statement can still read them. - */ -export declare class Statement { + complexTypesAsJson?: boolean /** - * Server-issued statement id. Cached at construction; readable - * even after `close()` so JS-side log lines can correlate against - * kernel / server logs which key on the same id. + * Whether to verify the server's TLS certificate. + * + * Omitted / `true` ⇒ strict validation against the system / Mozilla + * trust store (full chain + expiry + hostname), matching JDBC / ODBC + * and every modern HTTPS client. This is the **default** for the SEA + * backend — secure by default. + * + * `false` ⇒ permissive: accept self-signed / untrusted / expired + * certs AND skip the hostname-vs-SNI check. This is **insecure** (no + * protection against active MITM); it exists only as an opt-out for + * parity with the legacy NodeJS Thrift driver, which hard-codes + * `rejectUnauthorized: false`. Prefer pairing strict checking with + * `custom_ca_cert` over disabling verification entirely. + * + * This is the master verify toggle: `false` disables chain validation + * (`TlsConfig::accept_self_signed`) **and** subsumes the hostname + * check (`skip_hostname_verification`), regardless of + * `check_server_certificate_hostname`. */ - get statementId(): string + checkServerCertificate?: boolean /** - * Number of rows modified by the statement (UPDATE / INSERT / - * DELETE / MERGE). `null` for SELECT and on warehouses that don't - * surface the counter. Mirrors Thrift's - * `TGetOperationStatusResp.numModifiedRows`. + * Whether to verify that the server certificate matches the host + * (hostname-vs-SNI check), **independently** of full chain validation. + * + * Omitted / `true` ⇒ the hostname check runs (the secure default). + * `false` ⇒ skip only the hostname check while still validating the + * chain + expiry against the trust store — for connecting via an IP + * literal or a host the cert wasn't issued for, without dropping all + * validation. Ignored (already implied) when + * `check_server_certificate` is `false`, which disables everything. + * + * Mirrors the Python connector's `_tls_verify_hostname` knob and the + * kernel's [`TlsConfig::skip_hostname_verification`] (= `!check`). */ - numModifiedRows(): Promise + checkServerCertificateHostname?: boolean /** - * Server-supplied user-facing message. Mirrors Thrift's - * `TGetOperationStatusResp.displayMessage`. **PII / sensitive- - * data note:** may contain SQL fragments or parameter values — - * redact before centralised logging. + * PEM-encoded CA certificate bytes to add to the trust store on + * top of the system roots. Use for corporate TLS-inspecting + * proxies that re-sign TLS, or on-prem deployments with an + * internal CA. Honoured regardless of `check_server_certificate`. + * Maps onto the kernel [`TlsConfig::custom_ca_cert`]. + */ + customCaCert?: Buffer + /** + * PEM-encoded client certificate for mutual TLS (mTLS). Set this + * together with `client_key_pem` when the server requires the + * client to present a certificate. A PEM carrying a leaf cert + * optionally followed by its intermediate chain is accepted. + * Maps onto the kernel [`TlsConfig::client_cert_pem`]. * - * Populated on `Succeeded` / `Closed` paths (incl. an empty `Closed`). - * On terminal-error states (`Failed` / `Cancelled`) the kernel returns - * an Error instead of a `Statement`, and the same field rides on the JS - * Error envelope under the same `displayMessage` key. + * `client_cert_pem` and `client_key_pem` must be supplied together; + * the kernel rejects setting only one at `open_session` with + * `InvalidArgument`. */ - displayMessage(): Promise + clientCertPem?: Buffer /** - * Server-supplied diagnostic detail — multi-line operator / - * stack context. Mirrors Thrift's - * `TGetOperationStatusResp.diagnosticInfo`. For support surfaces, - * not user-facing. Same reachability + PII caveats as - * `displayMessage`. + * PEM-encoded private key for the mTLS client certificate. Set this + * together with `client_cert_pem`. For portability across the + * kernel's TLS backends supply a PKCS#8 key (`BEGIN PRIVATE KEY`). + * Maps onto the kernel [`TlsConfig::client_key_pem`]. */ - diagnosticInfo(): Promise + clientKeyPem?: Buffer /** - * Server-supplied JSON blob with extended error details. Mirrors - * Thrift's `TGetOperationStatusResp.errorDetailsJson`. - * Pass-through string — JS callers parse with `JSON.parse` if - * they need structured access. + * Extra HTTP headers to send on every request — the route for + * caller-supplied headers (the NodeJS driver's `customHeaders` and + * the composed `User-Agent`). Maps onto the kernel + * [`HttpConfig::custom_headers`]. * - * **Server-side gating:** populated only when the workspace has - * `spark.databricks.sql.errorDetailsJson.enabled = true` on the - * underlying SQL cluster. The flag is internal-only / default- - * false in the Databricks runtime, so for most JS callers this - * will return `null`. Admin-enabled workspaces return content - * shaped like `{"errorClass": "...", "messageTemplate": "..."}`. + * An **ordered list** of `(name, value)` pairs, mirroring the kernel + * core's `Vec<(String, String)>` and the pyo3 binding's + * `http_headers` — order is preserved and duplicate names are + * allowed (the kernel emits each entry, and for `User-Agent` folds + * the **last** one into its base UA). * - * **Unbounded:** when populated, server can return a multi-MB - * blob; size before logging. + * Three names are handled specially by the kernel: + * - `Authorization` / `x-databricks-org-id` are **reserved** — a + * caller entry for either is silently dropped (skip-and-warn) so + * auth and multi-tenant routing can't be hijacked by a custom + * header. (The NodeJS driver also drops these before they cross + * the FFI, matching the Python connector's double-wall.) + * - `User-Agent` is **appended** to the kernel base UA (rather than + * replacing it), preserving the `DatabricksJDBCDriverOSS/...` + * token the SEA server keys on while still surfacing the caller's + * identity. The NodeJS driver folds its `userAgentEntry` into a + * `User-Agent` entry here. */ - errorDetailsJson(): Promise + customHeaders?: Array /** - * Pull the next batch of results. Returns `null` when the stream - * is exhausted. The returned `ArrowBatch.ipcBytes` is a complete - * Arrow IPC stream (schema header + 1 record-batch message) - * suitable for handing to `apache-arrow`'s `RecordBatchReader`. + * Retry/backoff tuning — all optional. An unset field keeps the kernel's + * built-in policy (1s/60s exponential backoff, 6 total attempts, 900s + * budget). Mirrors the pyo3 binding's `retry_*` kwargs so the Node.js + * driver can forward the same retry knobs the Python connector does. * - * On `Err`, the stream is in an unspecified state — call - * `close()` and discard the `Statement`. Subsequent - * `fetchNextBatch()` calls after an error are not guaranteed to - * succeed or fail consistently. + * Lower bound of the exponential backoff (also clamps a server + * `Retry-After`). Maps onto [`HttpConfig::retry_min_wait`]. */ - fetchNextBatch(): Promise + retryMinWaitSecs?: number /** - * Result schema as an Arrow IPC payload (schema header only, no - * record-batch message). Available before any batches have been - * fetched, and remains available after `close()` — the kernel - * materialises the schema eagerly so JS callers can build error - * reports against a disposed statement. - * - * Sync because the body has no `.await` — `encode_ipc_stream` is - * pure CPU work over an `Arc` already cached on the - * wrapper. Mirrors `pyo3/src/statement.rs::arrow_schema` (sync). - * napi-rs converts a panic in a sync `#[napi]` entry point into a - * thrown JS error via its own macro-expanded boundary, so the - * `util::guarded` `catch_unwind` wrapper that the `async fn` - * entry points use is not required for this method. + * Upper bound of the exponential backoff. Maps onto + * [`HttpConfig::retry_max_wait`]. */ - schema(): ArrowSchema + retryMaxWaitSecs?: number /** - * Server-side cancel. - * - * For executed statements: short-circuits to `Ok(())` if - * `fetchNextBatch` has already returned `null` (stream - * naturally exhausted) — matches the JDBC `Statement.cancel()` - * no-op-after-completion contract, so JS callers can fire cancel - * defensively without distinguishing "real cancel" from "raced - * with natural completion." - * - * For metadata streams: no-op (the kernel has no in-flight - * cancellation surface for metadata calls today). + * **Total** number of attempts (matching the connector's + * `_retry_stop_after_attempts_count` and JDBC count semantics). The + * kernel's [`HttpConfig::retry_max_retries`] counts retries *after* the + * first attempt, so this is converted with `max(0, attempts - 1)` in + * [`build_http_config`] — `0` / `1` both mean a single attempt, no retry. + */ + retryMaxAttempts?: number + /** + * Overall retry budget in whole seconds. Maps onto + * [`HttpConfig::overall_timeout`]. + */ + retryOverallTimeoutSecs?: number + /** + * Programmatic HTTP/HTTPS proxy ([`ProxyInput`]) to route all kernel + * traffic through. Carries the proxy `url`, optional basic-auth + * `username` / `password`, and an optional `bypassHosts` list — mapped + * field-for-field onto the kernel [`ProxyConfig`]. * - * Returns `KernelError(InvalidStatementHandle)` if the statement - * has been explicitly `close()`d. + * Omitted ⇒ the kernel does NOT configure a proxy explicitly and + * `reqwest`'s standard behaviour applies — the `HTTPS_PROXY` / + * `HTTP_PROXY` / `NO_PROXY` environment variables are still honoured. + * Setting this **overrides** those env vars. This complements the env-var + * path: callers who cannot set process env vars (e.g. a long-lived Node + * server) can now route a single connection through a proxy + * programmatically. */ - cancel(): Promise + proxy?: ProxyInput /** - * Explicit close. + * Per-connection socket read timeout, in milliseconds. Caps how + * long a single HTTP round-trip may block waiting on the server + * before the request errors out. Maps onto the kernel + * [`HttpConfig::request_timeout`] (the internal reqwest + * `Client::timeout`). * - * For executed statements: awaits the server-side `CloseStatement` - * so the JS caller can observe failures (auth revoked mid-session, - * network error, server-side error). Idempotent — a second call - * on an already-closed statement returns `Ok`. - * - * **Errors are terminal from the JS side.** The kernel executed - * handle is taken out of `inner` BEFORE the wire `CloseStatement` - * runs (so `Drop` knows there's nothing left to clean up). On - * `Err`, the napi `inner` is already `None`, so a JS-side retry - * sees a closed statement and returns `Ok(())` without re- - * attempting the wire call. The kernel-level `ExecutedStatement` - * has been consumed at that point and the value is dropped on - * the way out of the closure — the kernel's `ExecutedStatement:: - * Drop` then fires-and-forgets a single retry on the captured - * runtime. The JS caller can log the error but cannot drive a - * further retry. If you need retry-on-failure semantics for - * `CloseStatement`, layer them above this method. - * - * For metadata streams: drops the stream (no server round-trip - * needed — metadata results have no in-flight server-side - * resource to release). + * Omitted ⇒ kernel default (120 000 ms / 120 s). Napi-rs + * serialises `u32` as JS `number`; the largest representable value + * (~49.7 days) far exceeds any sensible socket timeout. */ - close(): Promise + socketTimeoutMs?: number +} +/** + * Open a Databricks SQL session and return an opaque `Connection` + * wrapping the kernel `Session`. Authentication is selected by + * `options.auth_mode` (PAT / OAuth M2M / OAuth U2M) — see + * [`build_auth_config`]. + * + * The JS-visible name is `openSession` (napi-rs converts snake_case + * to camelCase for free functions). + */ +export declare function openSession(options: ConnectionOptions): Promise +/** + * One kernel log event, as handed to JS. `level` is a lower-case string + * (`error`/`warn`/`info`/`debug`/`trace`) the Node side maps onto its + * `LogLevel`; `target` is the originating `tracing` target (e.g. + * `databricks::sql::kernel`); `message` is the rendered event plus any + * structured `key=value` fields. + */ +export interface LogRecord { + level: string + target: string + message: string +} +/** + * Install (idempotently) the kernel→JS log bridge and set its level. + * + * `callback` is invoked with **an array of [`LogRecord`]s** (`(err, records)`) + * for each forwarded batch. `level` is one of + * `off`/`error`/`warn`/`info`/`debug`/`trace` (case-insensitive); unknown + * values fall back to `warn`. + * + * Safe to call more than once: the process-global subscriber is installed on + * the first call only, while every call refreshes the sink + level (last + * writer wins — see module docs). + */ +export declare function initKernelLogging(callback: (err: Error | null, arg: Array) => any, level: string): void +/** + * Snapshot of the bridge's runtime state for observability. + * + * `installed` is `true` only when the process-global subscriber was + * successfully installed by *this* bridge (and the drain thread started); + * `false` means another global subscriber was already set or the drain + * thread could not be spawned, so kernel logs are NOT reaching the JS sink. + * `dropped` is the cumulative count of records discarded because the + * bounded channel was full during a burst (drop-newest) — a nonzero, + * growing value signals the sink can't keep up. + */ +export interface KernelLoggingStats { + installed: boolean + dropped: number +} +/** + * Return the bridge's [`KernelLoggingStats`]. Safe to call before + * `initKernelLogging` (reports `installed: false`, `dropped: 0`). + */ +export declare function kernelLoggingStats(): KernelLoggingStats +/** + * Live-retarget the bridge's level (one of + * `off`/`error`/`warn`/`info`/`debug`/`trace`, case-insensitive). + */ +export declare function setKernelLogLevel(level: string): void +/** + * JS-visible binding for a single positional parameter. + * + * Shape mirrors the `TSparkParameter` wire object the Thrift backend + * already emits via `DBSQLParameter.toSparkParameter()` — `type` is the + * canonical Databricks SQL type name (`"INT"`, `"STRING"`, + * `"DECIMAL(10,2)"`, ...), `value` is the string-encoded literal or + * `None` for SQL NULL. + * + * Why a string for `value` instead of a tagged JS union: round-tripping + * arbitrary JS values across the FFI requires either (a) a custom + * napi `FromNapiValue` per arm, or (b) a `serde_json::Value`-style + * dynamic dispatch on the Rust side. The Node-driver adapter already + * stringifies before calling the binding (see `DBSQLParameter` and the + * existing pyo3 wrapper), so the string-in / string-parsed contract + * adds no JS-side complexity and keeps the kernel-side validation in + * one place. + */ +export interface TypedValueInput { + /** + * Canonical Databricks SQL type name. Case-insensitive for the + * simple variants; for DECIMAL the parenthesised form + * (`"DECIMAL(10,2)"`) is required so the kernel can extract + * precision/scale. + */ + sqlType: string + /** + * String-encoded value. `None` always produces `TypedValue::Null` + * regardless of `sql_type` — matches the connector's + * `VoidParameter` shape and the pyo3 binding's contract. + */ + value?: string } - /** * A single Arrow IPC stream payload encoding one record batch (plus * the schema header so the JS-side reader is stateless). @@ -563,7 +511,6 @@ export interface ArrowBatch { */ ipcBytes: Buffer } - /** * An Arrow IPC stream payload encoding just the result schema (no * record-batch messages). Returned by `Statement.schema()`. @@ -576,521 +523,547 @@ export interface ArrowSchema { */ ipcBytes: Buffer } - /** - * Authentication mode selector crossing the napi boundary. The string - * literals are what napi-rs emits from this `#[napi(string_enum)]` — the - * NodeJS SEA adapter (`KernelAuth`) matches them verbatim (`'Pat'`, - * `'OAuthM2m'`, `'OAuthU2m'`). + * Returns the native binding's crate version (`CARGO_PKG_VERSION`). * - * Mirrors the kernel [`AuthConfig`] variants this binding supports. - * `OAuthFederation` / `External` are intentionally not exposed yet — the - * kernel marks federation as not-yet-implemented and `External` is a - * Rust-trait escape hatch with no JS-callback bridge. + * Originally the round-1b smoke test; kept as a cheap "is the binding + * loaded?" probe for the JS-side loader's structured diagnostics. */ -export declare const enum AuthMode { - /** Personal access token (`token`). */ - Pat = 'Pat', - /** OAuth 2.0 machine-to-machine — `oauthClientId` + `oauthClientSecret`. */ - OAuthM2m = 'OAuthM2m', - /** - * OAuth 2.0 user-to-machine (browser flow) — optional `oauthClientId` - * + `oauthRedirectPort`. - */ - OAuthU2m = 'OAuthU2m' -} - +export declare function version(): string /** - * JS-visible options for opening a Databricks SQL session. + * Opaque async-statement handle. * - * Authentication is selected by `authMode` (default [`AuthMode::Pat`]): - * - `Pat` — `token` required. - * - `OAuthM2m` — `oauthClientId` + `oauthClientSecret` required. - * - `OAuthU2m` — `oauthClientId` / `oauthRedirectPort` optional - * (defaults to the `databricks-sql-connector` client on port 8020). + * Returned by `Connection.submitStatement(...)` after the kernel + * `Statement::submit()` returns (server sent `wait_timeout=0s`, so + * the response carries a `statement_id` but the statement is still + * `Pending`/`Running`). JS drives polling via `status()` / + * `awaitResult()`. * - * Catalog / schema / sessionConf are applied once at session creation - * and remain in effect for every statement run on the resulting - * `Connection`. The SEA wire protocol carries them on - * `CreateSession`, not on `ExecuteStatement` — so there is no - * per-statement override path on this binding. + * Concurrency shape: `status()`, `awaitResult()`, and `close()` take + * `inner.lock()` and hold the guard across the kernel `.await` (tokio + * `Mutex` is FIFO), so `status()` / `close()` queue behind any + * in-flight `awaitResult()` until it returns naturally. `cancel()` is + * the deliberate exception: it does **not** touch `inner` — it fires + * through the detached `AsyncStatementCanceller` (session + + * statement_id, captured at construction), so an explicit + * `stmt.cancel()` interrupts an in-flight `awaitResult()` instead of + * queueing behind it. The server-side cancel flips the statement + * terminal, which the parked `awaitResult()` poll loop observes + * (`Cancelled`) and returns on. The kernel's `AwaitResultCancelGuard` + * still covers the drop-cancel case (Promise.race / timeout) + * independently — see module docs. */ -export interface ConnectionOptions { +export declare class AsyncStatement { /** - * Workspace host, e.g. `adb-…azuredatabricks.net`. The kernel - * normalises this — bare hostnames get `https://` prepended. + * Server-issued statement id. Cached at construction; readable + * even after `close()` so JS-side log lines can correlate + * against kernel / server logs which key on the same id. */ - hostName: string + get statementId(): string /** - * JDBC-style HTTP path, e.g. `/sql/1.0/warehouses/abc123`. The - * kernel parses out the warehouse id. + * One-shot status check. Returns a string enum matching the + * kernel `StatementStatus` shape: + * `'Pending' | 'Running' | 'Succeeded' | 'Failed' | + * 'Cancelled' | 'Closed' | 'Unknown'`. (`'Unknown'` is the + * `#[non_exhaustive]` forward-compat catch-all that + * `StatementStatus::as_str` can return — consumers switching on + * the state must handle it.) Returns + * `KernelError(InvalidStatementHandle)` if the statement has + * been explicitly `close()`d. + * + * The `Failed` variant collapses to the string `'Failed'` on + * the JS side; the underlying error envelope (sql_state / + * error_code / query_id) is surfaced by `awaitResult()`'s + * rejection, which is where callers actually need the typed + * error. `status()` is intended for polling progress UIs + * that only need the state name. */ - httpPath: string + status(): Promise + /** Rows modified by the statement (UPDATE / INSERT / DELETE / MERGE). */ + numModifiedRows(): Promise /** - * Authentication mode. Omitted ⇒ [`AuthMode::Pat`] (back-compat: - * existing PAT callers pass only `token`). + * Server-supplied user-facing message (may contain SQL fragments — + * redact before centralised logging). */ - authMode?: AuthMode + displayMessage(): Promise + /** Server-supplied diagnostic detail. */ + diagnosticInfo(): Promise + /** Server-supplied structured error detail (JSON), when enabled. */ + errorDetailsJson(): Promise /** - * Personal access token. Required (and non-empty) for - * [`AuthMode::Pat`]; ignored otherwise. + * Block until the server reaches a terminal state, then return + * an `AsyncResultHandle` that wraps the materialised result + * stream. The handle exposes `fetchNextBatch()` / `schema()` + * for consuming the result, plus `statementId` for log + * correlation. + * + * Drop-cancel safety: kernel `await_result` installs + * `AwaitResultCancelGuard` which fires a fire-and-forget + * `cancel_statement` if the future is dropped mid-poll + * (timeout, tokio::select! loser, JS-side `Promise.race` + * loser). The `util::guarded` `catch_unwind` here covers the + * V8-panic-across-boundary case on top. Returns + * `KernelError(InvalidStatementHandle)` if the statement has + * been explicitly `close()`d. */ - token?: string + awaitResult(): Promise /** - * OAuth client id. Required for [`AuthMode::OAuthM2m`]; optional for - * [`AuthMode::OAuthU2m`] (defaults to `databricks-sql-connector`). + * Server-side cancel. Returns + * `KernelError(InvalidStatementHandle)` if the statement has + * been explicitly `close()`d. Idempotent against a server + * that already reached a terminal state — the kernel's + * `cancel_statement` is a no-op there. + * + * **Lock-free by design.** Unlike `status()` / `awaitResult()` / + * `close()`, this does not take `inner.lock()` — it fires through + * the detached `AsyncStatementCanceller` captured at construction. + * That lets `stmt.cancel()` interrupt an in-flight `awaitResult()` + * (which holds the mutex for the whole poll) instead of queueing + * behind it: the server-side cancel flips the statement terminal, + * the parked `awaitResult()` poll loop observes `Cancelled` and + * returns. The closed-state check reads a lock-free flag so a + * cancel after an explicit `close()` still surfaces + * `InvalidStatementHandle`. */ - oauthClientId?: string - /** OAuth client secret. Required for [`AuthMode::OAuthM2m`]. */ - oauthClientSecret?: string + cancel(): Promise /** - * Localhost callback port for the [`AuthMode::OAuthU2m`] browser - * flow. Omitted ⇒ kernel default (8020). - */ - oauthRedirectPort?: number + * Explicit close. Idempotent — a second call on an + * already-closed handle returns `Ok(())`. On `Err`, the napi + * inner is already `None`, so a JS-side retry sees the + * closed-handle short-circuit and returns `Ok(())` without + * re-attempting the wire call. The kernel's own `Drop` + * fire-and-forget retry runs once in the background. + */ + close(): Promise +} +/** + * Opaque result-fetch handle returned by + * `AsyncStatement.awaitResult()`. Wraps a kernel `ResultStream` + * directly; structurally analogous to the sync `Statement`'s + * fetch-side surface (`fetchNextBatch` / `schema` / + * `statementId`). + * + * `cancel()` / `close()` are not exposed: the parent + * `AsyncStatement` owns server-side lifecycle. A `close()` here + * would create dual-ownership of the same statement_id with + * inconsistent close semantics. Callers `close()` the parent + * `AsyncStatement` after they're done fetching. + * + * Schema is cached at construction so it survives the underlying + * stream being drained; mirrors the sync `Statement.schema()` + * post-close contract. + */ +export declare class AsyncResultHandle { /** - * OAuth scopes override (M2M / U2M). Omitted ⇒ kernel defaults - * (`["all-apis"]` for M2M; `["all-apis", "offline_access"]` for U2M). + * Server-issued statement id. Cached at construction; readable + * for log correlation. Matches the parent `AsyncStatement`'s + * `statementId`. */ - oauthScopes?: Array + get statementId(): string /** - * Default catalog for statements executed on this session. - * Routed through the kernel's `DefaultOpts` and onto the SEA - * `CreateSession.catalog` wire field. + * Pull the next batch of results. Returns `null` when the + * stream is exhausted. The returned `ArrowBatch.ipcBytes` is a + * complete Arrow IPC stream (schema header + 1 record-batch + * message), suitable for handing to `apache-arrow`'s + * `RecordBatchReader`. Byte-identical to the sync + * `Statement.fetchNextBatch()` payload for the same query. */ - catalog?: string + fetchNextBatch(): Promise /** - * Default schema for statements executed on this session. - * Routed through the kernel's `DefaultOpts` and onto the SEA - * `CreateSession.schema` wire field. + * Result schema as an Arrow IPC payload (schema header only, + * no record-batch message). Available before any batches have + * been fetched. Sync because the body has no `.await` — + * `encode_ipc_stream` is pure CPU work over the cached + * `Arc`. */ - schema?: string + schema(): ArrowSchema +} +/** + * Handle returned by `Connection.executeStatementCancellable`. Owns the + * built-but-not-yet-executed kernel `Statement` plus a detached + * [`StatementCanceller`] captured before dispatch, so JS can fire a + * server-side cancel while the blocking `result()` is in flight. + * + * `pending` is `Arc>>` so `result()` can + * `.take()` the statement (the kernel `execute()` borrows it `&mut`, + * then it moves into the produced `Statement` wrapper to keep its + * `ValidityFlag` set — see `statement.rs`). A second `result()` call + * after the first resolved surfaces `InvalidStatementHandle`. + */ +export declare class CancellableExecution { /** - * Server-bound session conf (Spark conf, `ANSI_MODE`, `TIMEZONE`, - * query-tag presets, …). Forwarded verbatim to SEA - * `session_confs`. Unknown keys are rejected server-side. + * The server-issued statement id this execution targets, if the + * server has issued one yet (`null` before the initial submit + * round-trip publishes it mid-`result()`). Useful for log + * correlation while the blocking drive is in flight. */ - sessionConf?: Record + get statementId(): string | null /** - * Maximum number of pooled HTTP connections per host. Routes - * through the kernel's [`HttpConfig::pool_max_idle_per_host`]. - * Tunes the underlying `reqwest` connection pool — higher values - * reduce reconnect overhead when many statements run - * concurrently against the same warehouse. + * Drive the blocking `execute()` and resolve to a `Statement` + * (identical to what `executeStatement` returns) once the kernel + * reaches a terminal state and the result stream is ready. * - * When the JS caller does NOT provide `maxConnections`, the napi - * binding applies a NodeJS-driver-appropriate default of - * [`NAPI_DEFAULT_POOL_MAX_IDLE_PER_HOST`] (100) — chosen to match - * the JDBC driver's `HttpConnectionPoolSize` default and to close - * the throughput gap vs the NodeJS Thrift driver's - * `maxSockets: Infinity` pool for bursty workloads. The kernel - * core's [`HttpConfig::pool_max_idle_per_host`] default is also 100 - * (matching the same JDBC default), so napi pins its own copy rather - * than inheriting it. Mirrors the Python connector's - * `max_connections` kwarg on the SEA backend, which exposes the - * knob but keeps its own urllib3-aligned default of 10. + * Consumes the pending statement: a second `result()` call returns + * `KernelError(InvalidStatementHandle)`. The future is + * drop-cancel-safe — the kernel's per-execute `MidExecuteCancelState` + * guard fires a fire-and-forget `cancel_statement` if this future is + * dropped mid-flight (`Promise.race` / timeout loser), independently + * of an explicit `cancel()`. * - * Napi-rs serialises `u32` as JS `number`; values up to - * `2^32 - 1` round-trip safely (any reasonable pool size fits). + * On a server-side cancel the kernel's blocking `execute()` currently + * surfaces `InvalidArgument` (a known kernel quirk — the async path + * returns `Cancelled`). When this handle's `cancel()` actually dispatched a + * server-side cancel, we normalise that into `Cancelled` here so JS callers + * can rely on a single cancelled-status code regardless of execution path. + * + * Three outcomes can race the blocking drive: (1) a natural terminal state + * → `Ok` or the genuine error; (2) an explicit `cancel()` that dispatched a + * server cancel → this `result()` rejects with a `Cancelled`-coded error + * (the normalisation above); (3) the future being **dropped** mid-flight + * (`Promise.race`/timeout loser) → the kernel's `MidExecuteCancelState` + * drop-guard fires a fire-and-forget `cancel_statement`, but there is no + * `result()` left to observe a code. Only (2) yields a `Cancelled` error. */ - maxConnections?: number + result(): Promise /** - * Render `INTERVAL` / `DURATION` result columns as strings - * (`ResultConfig.intervals_as_string`). The kernel default is - * native Arrow `month_interval` / `duration[us]` types; the NodeJS - * Thrift driver surfaces intervals as strings, so the SEA driver - * sets this `true` for byte-compatible parity. Omitted ⇒ kernel - * default (native Arrow interval types). + * Server-side cancel of the in-flight statement. + * + * Lock-free: fires the detached `StatementCanceller` captured at + * construction rather than taking the mutex `result()` holds, so it + * interrupts a still-running blocking `result()` instead of queueing + * behind it. No-op (returns `Ok`) if `result()` already finished + * successfully, or if no statement id has been observed yet (query still + * in its initial submit round-trip), and idempotent against a server + * already in a terminal state. */ - intervalsAsString?: boolean + cancel(): Promise +} +/** + * Opaque connection handle wrapping a kernel `Session`. + * + * `inner` is `Arc>>` so: + * - the Drop impl can clone the `Arc` and `.take()` the session on a + * background tokio task without holding `&mut self` (which Drop is + * forbidden from doing across an `await`), + * - `close()` can `.take()` the session to consume it for the kernel's + * move-by-value `Session::close(self)` signature. + * + * **Concurrency shape** — both `executeStatement` and + * `submitStatement` build the kernel `Statement` under `inner.lock()` + * and then RELEASE the guard before the wire call + * (`stmt.execute().await` / `stmt.submit().await`). `Session::statement()` + * is `&self`-callable and only clones the session's internal `Arc`, so + * the built statement is independent of the guard. Concurrent + * `Promise.all([executeStatement(q1), submitStatement(q2)])` therefore + * serialise only for the microsecond statement-build, not the network + * round-trip, and `close()` never blocks behind an in-flight execute or + * submit. See + * `sea-workflow/jira-candidates/2026-05-24-napi-cancel-during-fetch.md`. + */ +export declare class Connection { /** - * Render complex (`ARRAY` / `MAP` / `STRUCT` / `VARIANT`) result - * columns as JSON strings (`ResultConfig.complex_types_as_json`) - * instead of native Arrow nested types. Omitted ⇒ kernel default - * (native Arrow nested types, which the NodeJS Arrow decoder - * already renders identically to the Thrift path). + * Server-issued session id. Cached at construction; readable + * even after `close()` so JS-side log lines can correlate + * against kernel / server logs which key on the same id. */ - complexTypesAsJson?: boolean + get sessionId(): string /** - * Whether to verify the server's TLS certificate. - * - * Omitted / `true` ⇒ strict validation against the system / Mozilla - * trust store (full chain + expiry + hostname), matching JDBC / ODBC - * and every modern HTTPS client. This is the **default** for the SEA - * backend — secure by default. + * Execute a SQL statement and return a Statement handle that + * streams batches via `fetchNextBatch()`. * - * `false` ⇒ permissive: accept self-signed / untrusted / expired - * certs AND skip the hostname-vs-SNI check. This is **insecure** (no - * protection against active MITM); it exists only as an opt-out for - * parity with the legacy NodeJS Thrift driver, which hard-codes - * `rejectUnauthorized: false`. Prefer pairing strict checking with - * `custom_ca_cert` over disabling verification entirely. + * Catalog / schema / sessionConf are session-level + * (`openSession`). Per-statement options on `ExecuteOptions`: + * - `statementConf` — per-statement Spark conf overlay + * - `queryTags` — serialised to a comma-separated `key:value` + * string and placed in `statement_conf["query_tags"]`, + * matching NodeJS Thrift's `serializeQueryTags` wire shape * - * This is the master verify toggle: `false` disables chain validation - * (`TlsConfig::accept_self_signed`) **and** subsumes the hostname - * check (`skip_hostname_verification`), regardless of - * `check_server_certificate_hostname`. + * `options` is omitted/`None` for the no-options path; passing + * `{ statementConf: {} }` (an empty map) is treated the same as + * omission to keep the wire shape stable for the common case. */ - checkServerCertificate?: boolean + executeStatement(sql: string, options?: ExecuteOptions | undefined | null): Promise /** - * Whether to verify that the server certificate matches the host - * (hostname-vs-SNI check), **independently** of full chain validation. + * directResults execute — the Thrift/JDBC model. Sends ExecuteStatement + * with no `wait_timeout` field (server applies its ~10s default inline wait + * and auto-closes on success) and returns WITHOUT polling past it: * - * Omitted / `true` ⇒ the hostname check runs (the secure default). - * `false` ⇒ skip only the hostname check while still validating the - * chain + expiry against the trust store — for connecting via an IP - * literal or a host the cert wasn't issued for, without dropping all - * validation. Ignored (already implied) when - * `check_server_certificate` is `false`, which disables everything. + * - a **`Statement`** (left arm) when the query finished within the inline + * wait — terminal, result ready inline, `close()` is a clean release; + * - an **`AsyncStatement`** (right arm) when it did not — a poll/cancel + * handle the caller drives (`status()` / `awaitResult()` / `cancel()`). * - * Mirrors the Python connector's `_tls_verify_hostname` knob and the - * kernel's [`TlsConfig::skip_hostname_verification`] (= `!check`). + * JS distinguishes the arms by feature-detecting `awaitResult` (present + * only on `AsyncStatement`). This is the path that gives mid-run cancel for + * long queries WITHOUT the eager-handle / close-drives workaround: the + * returned handle always corresponds to a server-owned statement. + * + * **Load-bearing contract:** the kernel's `DirectStatement::{Completed, + * Running}` discriminant cannot ride on these opaque `#[napi]` classes, so + * consumers MUST feature-detect via `awaitResult` (the only member unique to + * `AsyncStatement`). `Statement` (the Completed arm) MUST NOT gain an + * `awaitResult` member, or every consumer silently misroutes. The pyo3 + * binding makes the same `await_result`-probe assumption. */ - checkServerCertificateHostname?: boolean + executeStatementDirect(sql: string, options?: ExecuteOptions | undefined | null): Promise /** - * PEM-encoded CA certificate bytes to add to the trust store on - * top of the system roots. Use for corporate TLS-inspecting - * proxies that re-sign TLS, or on-prem deployments with an - * internal CA. Honoured regardless of `check_server_certificate`. - * Maps onto the kernel [`TlsConfig::custom_ca_cert`]. + * Execute a SQL statement on the blocking (sync) path, but return a + * `CancellableExecution` handle so a concurrent JS task can cancel + * the query *while it is still running server-side*. + * + * `executeStatement` builds the kernel `Statement`, awaits the + * blocking `execute()`, and only then hands JS a `Statement` — so a + * query that runs for several seconds is uncancellable from JS on + * that path (there is no handle until the blocking call resolves). + * This method instead builds the statement, captures a detached + * `StatementCanceller` **before** dispatching `execute()`, and hands + * JS a `CancellableExecution` immediately. The caller drives the + * blocking execution via `result()` (resolves to the same + * `Statement` `executeStatement` returns) and can fire `cancel()` + * concurrently to interrupt a still-running query mid-COMPUTE. + * + * Option semantics are identical to `executeStatement`. + * Mirrors the pyo3 `Statement.canceller()` / `Statement.execute()` + * split (PR #121): obtain the canceller before the blocking drive. */ - customCaCert?: Buffer + executeStatementCancellable(sql: string, options?: ExecuteOptions | undefined | null): Promise /** - * PEM-encoded client certificate for mutual TLS (mTLS). Set this - * together with `client_key_pem` when the server requires the - * client to present a certificate. A PEM carrying a leaf cert - * optionally followed by its intermediate chain is accepted. - * Maps onto the kernel [`TlsConfig::client_cert_pem`]. + * Submit a SQL statement and return immediately with an + * `AsyncStatement` handle, without blocking until the query + * finishes. The kernel's `Statement::submit()` sends + * `wait_timeout=0s`, so the server responds as soon as it has a + * `statement_id` (state `Pending`/`Running`); JS drives polling + * via `AsyncStatement.status()` and materialises results with + * `AsyncStatement.awaitResult()`. * - * `client_cert_pem` and `client_key_pem` must be supplied together; - * the kernel rejects setting only one at `open_session` with - * `InvalidArgument`. + * This is the async-execution path the Thrift backend always + * uses (`runAsync: true`): the SEA backend submits, returns a + * pending operation handle, and polls to terminal during + * fetch. Option semantics (statementConf / queryTags / + * rowLimit / positional + named params) match `executeStatement`. + * Submit always sends `wait_timeout=0s` so the call returns + * immediately; the caller drives completion via `status()` / + * `awaitResult()`. Only the blocking-vs-pending return contract + * differs from `executeStatement`. */ - clientCertPem?: Buffer + submitStatement(sql: string, options?: ExecuteOptions | undefined | null): Promise /** - * PEM-encoded private key for the mTLS client certificate. Set this - * together with `client_cert_pem`. For portability across the - * kernel's TLS backends supply a PKCS#8 key (`BEGIN PRIVATE KEY`). - * Maps onto the kernel [`TlsConfig::client_key_pem`]. + * Explicit close. Awaits the server-side `DeleteSession` so the + * JS caller can observe failures (auth revoked mid-session, + * warehouse stopped, network error). Idempotent — a second call + * on an already-closed connection returns `Ok`. + * + * **Errors are terminal from the JS side.** The kernel session + * handle is consumed (`take()`) BEFORE the wire `DeleteSession` + * runs, because `Session::close` takes `self` by value. On `Err`, + * the napi `inner` is already `None`, so a JS-side retry sees a + * closed connection and returns `Ok(())` without re-attempting + * the wire call. The kernel's own `Drop` fire-and-forget retry + * runs once in the background — the JS caller can log the error + * but cannot drive a retry. If you need retry-on-failure + * semantics for `DeleteSession`, layer them above this method. */ - clientKeyPem?: Buffer + close(): Promise /** - * Extra HTTP headers to send on every request — the route for - * caller-supplied headers (the NodeJS driver's `customHeaders` and - * the composed `User-Agent`). Maps onto the kernel - * [`HttpConfig::custom_headers`]. + * All catalogs visible to the session. * - * An **ordered list** of `(name, value)` pairs, mirroring the kernel - * core's `Vec<(String, String)>` and the pyo3 binding's - * `http_headers` — order is preserved and duplicate names are - * allowed (the kernel emits each entry, and for `User-Agent` folds - * the **last** one into its base UA). + * JDBC `getCatalogs` shape: `TABLE_CAT: Utf8`. + */ + listCatalogs(): Promise + /** + * Schemas filtered by catalog (exact) and schema name pattern. * - * Three names are handled specially by the kernel: - * - `Authorization` / `x-databricks-org-id` are **reserved** — a - * caller entry for either is silently dropped (skip-and-warn) so - * auth and multi-tenant routing can't be hijacked by a custom - * header. (The NodeJS driver also drops these before they cross - * the FFI, matching the Python connector's double-wall.) - * - `User-Agent` is **appended** to the kernel base UA (rather than - * replacing it), preserving the `DatabricksJDBCDriverOSS/...` - * token the SEA server keys on while still surfacing the caller's - * identity. The NodeJS driver folds its `userAgentEntry` into a - * `User-Agent` entry here. + * JDBC `getSchemas` shape: `TABLE_SCHEM, TABLE_CATALOG`. */ - customHeaders?: Array + listSchemas(catalog?: string | undefined | null, schemaPattern?: string | undefined | null): Promise /** - * Retry/backoff tuning — all optional. An unset field keeps the kernel's - * built-in policy (1s/60s exponential backoff, 6 total attempts, 900s - * budget). Mirrors the pyo3 binding's `retry_*` kwargs so the Node.js - * driver can forward the same retry knobs the Python connector does. + * Tables filtered by catalog (exact), schema (pattern), table (pattern). * - * Lower bound of the exponential backoff (also clamps a server - * `Retry-After`). Maps onto [`HttpConfig::retry_min_wait`]. + * JDBC `getTables` shape: 10 columns. `tableTypes`, when provided, + * filters rows by `TABLE_TYPE` kernel-side. + * + * `tableTypes` is an advisory filter. Databricks `SHOW TABLES` does + * NOT honour the table-type filter server-side; the kernel applies + * it client-side after the result returns. Callers expecting + * server-side rejection of off-type tables should not rely on this. */ - retryMinWaitSecs?: number + listTables(catalog?: string | undefined | null, schemaPattern?: string | undefined | null, tablePattern?: string | undefined | null, tableTypes?: Array | undefined | null): Promise /** - * Upper bound of the exponential backoff. Maps onto - * [`HttpConfig::retry_max_wait`]. + * Columns of tables matching the filter. + * + * JDBC `getColumns` shape: 23 columns. */ - retryMaxWaitSecs?: number + listColumns(catalog?: string | undefined | null, schemaPattern?: string | undefined | null, tablePattern?: string | undefined | null, columnPattern?: string | undefined | null): Promise /** - * **Total** number of attempts (matching the connector's - * `_retry_stop_after_attempts_count` and JDBC count semantics). The - * kernel's [`HttpConfig::retry_max_retries`] counts retries *after* the - * first attempt, so this is converted with `max(0, attempts - 1)` in - * [`build_http_config`] — `0` / `1` both mean a single attempt, no retry. + * Functions visible to the session. `catalog` is exact; + * `schemaPattern` and `functionPattern` are SQL LIKE. */ - retryMaxAttempts?: number + listFunctions(catalog?: string | undefined | null, schemaPattern?: string | undefined | null, functionPattern?: string | undefined | null): Promise /** - * Overall retry budget in whole seconds. Maps onto - * [`HttpConfig::overall_timeout`]. + * Procedures visible to the session. `catalog` is exact; + * `schemaPattern` and `procedurePattern` are SQL LIKE. */ - retryOverallTimeoutSecs?: number + listProcedures(catalog?: string | undefined | null, schemaPattern?: string | undefined | null, procedurePattern?: string | undefined | null): Promise /** - * Programmatic HTTP/HTTPS proxy ([`ProxyInput`]) to route all kernel - * traffic through. Carries the proxy `url`, optional basic-auth - * `username` / `password`, and an optional `bypassHosts` list — mapped - * field-for-field onto the kernel [`ProxyConfig`]. - * - * Omitted ⇒ the kernel does NOT configure a proxy explicitly and - * `reqwest`'s standard behaviour applies — the `HTTPS_PROXY` / - * `HTTP_PROXY` / `NO_PROXY` environment variables are still honoured. - * Setting this **overrides** those env vars. This complements the env-var - * path: callers who cannot set process env vars (e.g. a long-lived Node - * server) can now route a single connection through a proxy - * programmatically. + * All table types (`TABLE`, `VIEW`, `SYSTEM TABLE`, …). + * No wire call — static in-memory result. */ - proxy?: ProxyInput + listTableTypes(): Promise /** - * Per-connection socket read timeout, in milliseconds. Caps how - * long a single HTTP round-trip may block waiting on the server - * before the request errors out. Maps onto the kernel - * [`HttpConfig::request_timeout`] (the internal reqwest - * `Client::timeout`). - * - * Omitted ⇒ kernel default (120 000 ms / 120 s). Napi-rs - * serialises `u32` as JS `number`; the largest representable value - * (~49.7 days) far exceeds any sensible socket timeout. + * SQL data types supported by the workspace. + * No wire call — static in-memory result. */ - socketTimeoutMs?: number + listTypeInfo(): Promise + /** + * Primary keys for the given table. All three identifiers are + * exact — ODBC `SQLPrimaryKeys` does not support patterns. + */ + getPrimaryKeys(catalog: string, schema: string, table: string): Promise + /** + * Foreign-key relationships. The foreign side must be fully + * specified (catalog + schema + table); the parent side is + * optional. All identifiers are exact — no LIKE patterns. + */ + getCrossReference(parentCatalog: string | undefined | null, parentSchema: string | undefined | null, parentTable: string | undefined | null, foreignCatalog: string, foreignSchema: string, foreignTable: string): Promise } - /** - * Per-statement options for `Connection.executeStatement`. - * - * Mirrors the kernel `StatementSpec` knobs that are safe to thread - * through napi without a kernel-side change. Today this covers: - * - `statementConf` — per-statement Spark conf overlay - * (`StatementSpec.statement_conf` → SEA `parameters` / - * Thrift `confOverlay`) - * - `queryTags` — convenience wrapper over `statementConf` with - * key `query_tags`; serialised to the same comma-separated - * `key:value` wire shape NodeJS Thrift's `serializeQueryTags` - * produces (`lib/utils/queryTags.ts`). Backslashes in keys are - * doubled; backslash/colon/comma in values are backslash-escaped. + * Opaque executed-statement handle. * - * `rowLimit` (SEA `row_limit`) is exposed here and threaded onto the kernel - * `StatementSpec`. `positionalParams` (`?`) and `namedParams` (`:name`) - * carry bound query parameters, decoded via `params::parse_typed_value`. - * (There is no `queryTimeoutSecs`: it abused the SEA `wait_timeout` inline-hold - * window and was removed — a real per-statement timeout is `STATEMENT_TIMEOUT`.) + * **Current concurrency shape** — every method takes `inner.lock()` + * and holds the guard across the kernel `.await`. tokio `Mutex` is + * FIFO, so cancel/close queue behind any in-flight `fetchNextBatch` + * until it returns naturally. This is a known limitation that exists + * because the napi shape has not yet been split into an + * `Arc` (for cancel/close, which the + * kernel exposes as `&self`-callable) plus a `Mutex>` only + * for the borrowed-mut fetch path. The lock-shape refactor needs a + * small kernel-side accessor and lands in a follow-up PR — see + * `sea-workflow/jira-candidates/2026-05-24-napi-cancel-during-fetch.md`. * - * **Tag-order caveat (M4 parity note).** The napi `queryTags` field - * is a Rust `HashMap` whose iteration order is - * non-deterministic, so the serialised `query_tags` value may have - * a different key order than Thrift's `serializeQueryTags` (which - * iterates `Object.keys(...)` in insertion order) for the same - * input. The SEA server is order-insensitive on conf values, so - * the two are functionally equivalent. If a caller needs - * byte-identical Thrift parity, the JS adapter pre-serialises via - * `serializeQueryTags` and writes the result into - * `statementConf["query_tags"]` directly — see - * `KernelSessionBackend.executeStatement` in the NodeJS driver. This - * path is the one the production code uses. + * `schema` and `statement_id` are cached at construction so they + * survive `close()` — JS callers building error reports against a + * disposed statement can still read them. */ -export interface ExecuteOptions { +export declare class Statement { /** - * Per-statement Spark conf overlay. Merged on top of the - * session-level `sessionConf` at execute time; this map wins - * on key collisions. Unknown keys are rejected by the server. + * Server-issued statement id. Cached at construction; readable + * even after `close()` so JS-side log lines can correlate against + * kernel / server logs which key on the same id. */ - statementConf?: Record + get statementId(): string /** - * Query tags as key→value pairs. Serialised to a comma- - * separated `key:value` string (backslash-escaping `\`, `:`, - * `,`) and placed into `statementConf["query_tags"]`, matching - * NodeJS Thrift's `serializeQueryTags` wire shape. Passing - * both `queryTags` AND a `query_tags` key in `statementConf` - * raises `InvalidArgument` — the caller's intent is ambiguous - * so we refuse to silently pick one over the other. - * - * A **`null`** value emits a **bare key** (no colon) — e.g. - * `{ production: null }` → `"production"` — matching the - * connectors' `key`-only tag form. + * Number of rows modified by the statement (UPDATE / INSERT / + * DELETE / MERGE). `null` for SELECT and on warehouses that don't + * surface the counter. Mirrors Thrift's + * `TGetOperationStatusResp.numModifiedRows`. + */ + numModifiedRows(): Promise + /** + * Server-supplied user-facing message. Mirrors Thrift's + * `TGetOperationStatusResp.displayMessage`. **PII / sensitive- + * data note:** may contain SQL fragments or parameter values — + * redact before centralised logging. * - * See the struct-level "Tag-order caveat" for the - * HashMap-iteration-order vs `Object.keys`-iteration-order - * divergence and the byte-identical-Thrift-parity workaround. + * Populated on `Succeeded` / `Closed` paths (incl. an empty `Closed`). + * On terminal-error states (`Failed` / `Cancelled`) the kernel returns + * an Error instead of a `Statement`, and the same field rides on the JS + * Error envelope under the same `displayMessage` key. */ - queryTags?: Record + displayMessage(): Promise /** - * Server-side cap on the number of rows this statement returns - * (SEA `row_limit`), independent of any SQL `LIMIT`. Maps to - * `StatementSpec.row_limit`. Omitted ⇒ no driver-imposed cap. + * Server-supplied diagnostic detail — multi-line operator / + * stack context. Mirrors Thrift's + * `TGetOperationStatusResp.diagnosticInfo`. For support surfaces, + * not user-facing. Same reachability + PII caveats as + * `displayMessage`. */ - rowLimit?: number + diagnosticInfo(): Promise /** - * Positional parameters, in 1-based wire order. Index `i` in this - * Vec corresponds to the `i+1`-th `?` placeholder in the SQL. - * Each entry is a `{ sqlType, value }` pair — `value` is the - * string-encoded literal or `null` for SQL NULL. Mirrors - * `StatementSpec::positional_params`; decoded via [`parse_typed_value`]. + * Server-supplied JSON blob with extended error details. Mirrors + * Thrift's `TGetOperationStatusResp.errorDetailsJson`. + * Pass-through string — JS callers parse with `JSON.parse` if + * they need structured access. + * + * **Server-side gating:** populated only when the workspace has + * `spark.databricks.sql.errorDetailsJson.enabled = true` on the + * underlying SQL cluster. The flag is internal-only / default- + * false in the Databricks runtime, so for most JS callers this + * will return `null`. Admin-enabled workspaces return content + * shaped like `{"errorClass": "...", "messageTemplate": "..."}`. + * + * **Unbounded:** when populated, server can return a multi-MB + * blob; size before logging. */ - positionalParams?: Array + errorDetailsJson(): Promise /** - * Named parameters (`:name` placeholders). Each carries its `name` - * alongside the `{ sqlType, value? }` pair. Mapped to a kernel - * `TypedValue` via the same [`parse_typed_value`] codec and bound with - * `StatementSpec::param_named`. Named is the SEA-spec-required public - * param form (`StatementParameter.name` is `openapi_required`); - * positional is the documented-undocumented variant. The two are - * mutually exclusive at the SQL level (`?` vs `:name`). + * Pull the next batch of results. Returns `null` when the stream + * is exhausted. The returned `ArrowBatch.ipcBytes` is a complete + * Arrow IPC stream (schema header + 1 record-batch message) + * suitable for handing to `apache-arrow`'s `RecordBatchReader`. + * + * On `Err`, the stream is in an unspecified state — call + * `close()` and discard the `Statement`. Subsequent + * `fetchNextBatch()` calls after an error are not guaranteed to + * succeed or fail consistently. */ - namedParams?: Array -} - -/** - * A single extra HTTP header as an explicit `{ name, value }` pair. - * - * An ordered list of these (`ConnectionOptions.custom_headers`) mirrors - * the kernel core's `Vec<(String, String)>` and the pyo3 binding's - * `http_headers`: order is preserved and duplicate `name`s are allowed. - * A struct (rather than a raw `[name, value]` tuple) because napi-rs - * does not marshal Rust tuples through `#[napi(object)]` fields; the - * struct is the idiomatic, self-documenting equivalent and maps to a JS - * `{ name: string, value: string }`. - */ -export interface HeaderEntry { - name: string - value: string -} - -/** - * Install (idempotently) the kernel→JS log bridge and set its level. - * - * `callback` is invoked with **an array of [`LogRecord`]s** (`(err, records)`) - * for each forwarded batch. `level` is one of - * `off`/`error`/`warn`/`info`/`debug`/`trace` (case-insensitive); unknown - * values fall back to `warn`. - * - * Safe to call more than once: the process-global subscriber is installed on - * the first call only, while every call refreshes the sink + level (last - * writer wins — see module docs). - */ -export declare function initKernelLogging(callback: ((err: Error | null, arg: Array) => any), level: string): void - -/** - * Return the bridge's [`KernelLoggingStats`]. Safe to call before - * `initKernelLogging` (reports `installed: false`, `dropped: 0`). - */ -export declare function kernelLoggingStats(): KernelLoggingStats - -/** - * Snapshot of the bridge's runtime state for observability. - * - * `installed` is `true` only when the process-global subscriber was - * successfully installed by *this* bridge (and the drain thread started); - * `false` means another global subscriber was already set or the drain - * thread could not be spawned, so kernel logs are NOT reaching the JS sink. - * `dropped` is the cumulative count of records discarded because the - * bounded channel was full during a burst (drop-newest) — a nonzero, - * growing value signals the sink can't keep up. - */ -export interface KernelLoggingStats { - installed: boolean - dropped: number -} - -/** - * One kernel log event, as handed to JS. `level` is a lower-case string - * (`error`/`warn`/`info`/`debug`/`trace`) the Node side maps onto its - * `LogLevel`; `target` is the originating `tracing` target (e.g. - * `databricks::sql::kernel`); `message` is the rendered event plus any - * structured `key=value` fields. - */ -export interface LogRecord { - level: string - target: string - message: string -} - -/** - * A named bound parameter — a [`TypedValueInput`] plus its `:name`. Kept a - * distinct napi object (rather than an optional `name` on `TypedValueInput`) - * so the positional surface stays a clean ordered list with no name field. - */ -export interface NamedTypedValueInput { - name: string - sqlType: string - value?: string -} - -/** - * Open a Databricks SQL session and return an opaque `Connection` - * wrapping the kernel `Session`. Authentication is selected by - * `options.auth_mode` (PAT / OAuth M2M / OAuth U2M) — see - * [`build_auth_config`]. - * - * The JS-visible name is `openSession` (napi-rs converts snake_case - * to camelCase for free functions). - */ -export declare function openSession(options: ConnectionOptions): Promise - -/** - * Programmatic HTTP/HTTPS proxy configuration, mirroring the kernel's - * internal [`ProxyConfig`]. Supplied as a structured object rather than a - * flattened URL so credentials never have to be percent-encoded into the URL - * and the bypass-host list can be expressed. - * - * - `url` — proxy endpoint, e.g. `"http://proxy.corp.example.com:8080"`. Must - * use the `http://` or `https://` scheme. - * - `username` / `password` — optional proxy basic-auth, applied via - * `reqwest`'s `Proxy::basic_auth` (not embedded in the URL). - * - `bypassHosts` — optional comma-separated host/domain list that should - * bypass the proxy (e.g. `"localhost,*.internal.corp"`). - */ -export interface ProxyInput { - url: string - username?: string - password?: string - bypassHosts?: string -} - -/** - * Live-retarget the bridge's level (one of - * `off`/`error`/`warn`/`info`/`debug`/`trace`, case-insensitive). - */ -export declare function setKernelLogLevel(level: string): void - -/** - * JS-visible binding for a single positional parameter. - * - * Shape mirrors the `TSparkParameter` wire object the Thrift backend - * already emits via `DBSQLParameter.toSparkParameter()` — `type` is the - * canonical Databricks SQL type name (`"INT"`, `"STRING"`, - * `"DECIMAL(10,2)"`, ...), `value` is the string-encoded literal or - * `None` for SQL NULL. - * - * Why a string for `value` instead of a tagged JS union: round-tripping - * arbitrary JS values across the FFI requires either (a) a custom - * napi `FromNapiValue` per arm, or (b) a `serde_json::Value`-style - * dynamic dispatch on the Rust side. The Node-driver adapter already - * stringifies before calling the binding (see `DBSQLParameter` and the - * existing pyo3 wrapper), so the string-in / string-parsed contract - * adds no JS-side complexity and keeps the kernel-side validation in - * one place. - */ -export interface TypedValueInput { + fetchNextBatch(): Promise /** - * Canonical Databricks SQL type name. Case-insensitive for the - * simple variants; for DECIMAL the parenthesised form - * (`"DECIMAL(10,2)"`) is required so the kernel can extract - * precision/scale. + * Result schema as an Arrow IPC payload (schema header only, no + * record-batch message). Available before any batches have been + * fetched, and remains available after `close()` — the kernel + * materialises the schema eagerly so JS callers can build error + * reports against a disposed statement. + * + * Sync because the body has no `.await` — `encode_ipc_stream` is + * pure CPU work over an `Arc` already cached on the + * wrapper. Mirrors `pyo3/src/statement.rs::arrow_schema` (sync). + * napi-rs converts a panic in a sync `#[napi]` entry point into a + * thrown JS error via its own macro-expanded boundary, so the + * `util::guarded` `catch_unwind` wrapper that the `async fn` + * entry points use is not required for this method. */ - sqlType: string + schema(): ArrowSchema /** - * String-encoded value. `None` always produces `TypedValue::Null` - * regardless of `sql_type` — matches the connector's - * `VoidParameter` shape and the pyo3 binding's contract. + * Server-side cancel. + * + * For executed statements: short-circuits to `Ok(())` if + * `fetchNextBatch` has already returned `null` (stream + * naturally exhausted) — matches the JDBC `Statement.cancel()` + * no-op-after-completion contract, so JS callers can fire cancel + * defensively without distinguishing "real cancel" from "raced + * with natural completion." + * + * For metadata streams: no-op (the kernel has no in-flight + * cancellation surface for metadata calls today). + * + * Returns `KernelError(InvalidStatementHandle)` if the statement + * has been explicitly `close()`d. */ - value?: string + cancel(): Promise + /** + * Explicit close. + * + * For executed statements: awaits the server-side `CloseStatement` + * so the JS caller can observe failures (auth revoked mid-session, + * network error, server-side error). Idempotent — a second call + * on an already-closed statement returns `Ok`. + * + * **Errors are terminal from the JS side.** The kernel executed + * handle is taken out of `inner` BEFORE the wire `CloseStatement` + * runs (so `Drop` knows there's nothing left to clean up). On + * `Err`, the napi `inner` is already `None`, so a JS-side retry + * sees a closed statement and returns `Ok(())` without re- + * attempting the wire call. The kernel-level `ExecutedStatement` + * has been consumed at that point and the value is dropped on + * the way out of the closure — the kernel's `ExecutedStatement:: + * Drop` then fires-and-forgets a single retry on the captured + * runtime. The JS caller can log the error but cannot drive a + * further retry. If you need retry-on-failure semantics for + * `CloseStatement`, layer them above this method. + * + * For metadata streams: drops the stream (no server round-trip + * needed — metadata results have no in-flight server-side + * resource to release). + */ + close(): Promise } - -/** - * Returns the native binding's crate version (`CARGO_PKG_VERSION`). - * - * Originally the round-1b smoke test; kept as a cheap "is the binding - * loaded?" probe for the JS-side loader's structured diagnostics. - */ -export declare function version(): string diff --git a/native/kernel/index.js b/native/kernel/index.js index 45ea1ec8..ad50ecc9 100644 --- a/native/kernel/index.js +++ b/native/kernel/index.js @@ -1,713 +1,325 @@ -// prettier-ignore +/* tslint:disable */ /* eslint-disable */ -// @ts-nocheck +/* prettier-ignore */ + /* auto-generated by NAPI-RS */ -const { readFileSync } = require('fs') -let nativeBinding = null -const loadErrors = [] +const { existsSync, readFileSync } = require('fs') +const { join } = require('path') -const isMusl = () => { - let musl = false - if (process.platform === 'linux') { - musl = isMuslFromFilesystem() - if (musl === null) { - musl = isMuslFromReport() - } - if (musl === null) { - musl = isMuslFromChildProcess() - } - } - return musl -} +const { platform, arch } = process -const isFileMusl = (f) => f.includes('libc.musl-') || f.includes('ld-musl-') - -const isMuslFromFilesystem = () => { - try { - return readFileSync('/usr/bin/ldd', 'utf-8').includes('musl') - } catch { - return null - } -} +let nativeBinding = null +let localFileExisted = false +let loadError = null -const isMuslFromReport = () => { - let report = null - if (process.report && typeof process.report.getReport === 'function') { - process.report.excludeNetwork = true - report = process.report.getReport() - } - if (!report) { - return null - } - if (report.header && report.header.glibcVersionRuntime) { - return false - } - if (Array.isArray(report.sharedObjects)) { - if (report.sharedObjects.some(isFileMusl)) { +function isMusl() { + // For Node 10 + if (!process.report || typeof process.report.getReport !== 'function') { + try { + const lddPath = require('child_process').execSync('which ldd').toString().trim() + return readFileSync(lddPath, 'utf8').includes('musl') + } catch (e) { return true } - } - return false -} - -const isMuslFromChildProcess = () => { - try { - return require('child_process').execSync('ldd --version', { encoding: 'utf8' }).includes('musl') - } catch (e) { - // If we reach this case, we don't know if the system is musl or not, so is better to just fallback to false - return false + } else { + const { glibcVersionRuntime } = process.report.getReport().header + return !glibcVersionRuntime } } -function requireNative() { - if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) { - try { - return require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH); - } catch (err) { - loadErrors.push(err) - } - } else if (process.platform === 'android') { - if (process.arch === 'arm64') { - try { - return require('./index.android-arm64.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-android-arm64') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-android-arm64/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else if (process.arch === 'arm') { - try { - return require('./index.android-arm-eabi.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-android-arm-eabi') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-android-arm-eabi/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else { - loadErrors.push(new Error(`Unsupported architecture on Android ${process.arch}`)) - } - } else if (process.platform === 'win32') { - if (process.arch === 'x64') { - if ((process.config && process.config.variables && process.config.variables.shlib_suffix === 'dll.a') || (process.config && process.config.variables && process.config.variables.node_target_type === 'shared_library')) { - try { - return require('./index.win32-x64-gnu.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-win32-x64-gnu') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-win32-x64-gnu/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else { - try { - return require('./index.win32-x64-msvc.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-win32-x64-msvc') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-win32-x64-msvc/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } - } else if (process.arch === 'ia32') { - try { - return require('./index.win32-ia32-msvc.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-win32-ia32-msvc') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-win32-ia32-msvc/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else if (process.arch === 'arm64') { - try { - return require('./index.win32-arm64-msvc.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-win32-arm64-msvc') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-win32-arm64-msvc/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else { - loadErrors.push(new Error(`Unsupported architecture on Windows: ${process.arch}`)) - } - } else if (process.platform === 'darwin') { - try { - return require('./index.darwin-universal.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-darwin-universal') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-darwin-universal/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - if (process.arch === 'x64') { - try { - return require('./index.darwin-x64.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-darwin-x64') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-darwin-x64/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else if (process.arch === 'arm64') { - try { - return require('./index.darwin-arm64.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-darwin-arm64') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-darwin-arm64/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else { - loadErrors.push(new Error(`Unsupported architecture on macOS: ${process.arch}`)) - } - } else if (process.platform === 'freebsd') { - if (process.arch === 'x64') { - try { - return require('./index.freebsd-x64.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-freebsd-x64') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-freebsd-x64/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else if (process.arch === 'arm64') { - try { - return require('./index.freebsd-arm64.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-freebsd-arm64') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-freebsd-arm64/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else { - loadErrors.push(new Error(`Unsupported architecture on FreeBSD: ${process.arch}`)) - } - } else if (process.platform === 'linux') { - if (process.arch === 'x64') { - if (isMusl()) { +switch (platform) { + case 'android': + switch (arch) { + case 'arm64': + localFileExisted = existsSync(join(__dirname, 'index.android-arm64.node')) try { - return require('./index.linux-x64-musl.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-linux-x64-musl') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-linux-x64-musl/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (localFileExisted) { + nativeBinding = require('./index.android-arm64.node') + } else { + nativeBinding = require('@databricks/databricks-sql-kernel-android-arm64') } - return binding } catch (e) { - loadErrors.push(e) + loadError = e } - } else { + break + case 'arm': + localFileExisted = existsSync(join(__dirname, 'index.android-arm-eabi.node')) try { - return require('./index.linux-x64-gnu.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-linux-x64-gnu') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-linux-x64-gnu/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (localFileExisted) { + nativeBinding = require('./index.android-arm-eabi.node') + } else { + nativeBinding = require('@databricks/databricks-sql-kernel-android-arm-eabi') } - return binding - } catch (e) { - loadErrors.push(e) - } - } - } else if (process.arch === 'arm64') { - if (isMusl()) { - try { - return require('./index.linux-arm64-musl.node') } catch (e) { - loadErrors.push(e) + loadError = e } + break + default: + throw new Error(`Unsupported architecture on Android ${arch}`) + } + break + case 'win32': + switch (arch) { + case 'x64': + localFileExisted = existsSync( + join(__dirname, 'index.win32-x64-msvc.node') + ) try { - const binding = require('@databricks/databricks-sql-kernel-linux-arm64-musl') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-linux-arm64-musl/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (localFileExisted) { + nativeBinding = require('./index.win32-x64-msvc.node') + } else { + nativeBinding = require('@databricks/databricks-sql-kernel-win32-x64-msvc') } - return binding - } catch (e) { - loadErrors.push(e) - } - } else { - try { - return require('./index.linux-arm64-gnu.node') } catch (e) { - loadErrors.push(e) + loadError = e } + break + case 'ia32': + localFileExisted = existsSync( + join(__dirname, 'index.win32-ia32-msvc.node') + ) try { - const binding = require('@databricks/databricks-sql-kernel-linux-arm64-gnu') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-linux-arm64-gnu/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (localFileExisted) { + nativeBinding = require('./index.win32-ia32-msvc.node') + } else { + nativeBinding = require('@databricks/databricks-sql-kernel-win32-ia32-msvc') } - return binding } catch (e) { - loadErrors.push(e) + loadError = e } - } - } else if (process.arch === 'arm') { - if (isMusl()) { + break + case 'arm64': + localFileExisted = existsSync( + join(__dirname, 'index.win32-arm64-msvc.node') + ) try { - return require('./index.linux-arm-musleabihf.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-linux-arm-musleabihf') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-linux-arm-musleabihf/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (localFileExisted) { + nativeBinding = require('./index.win32-arm64-msvc.node') + } else { + nativeBinding = require('@databricks/databricks-sql-kernel-win32-arm64-msvc') } - return binding } catch (e) { - loadErrors.push(e) + loadError = e } + break + default: + throw new Error(`Unsupported architecture on Windows: ${arch}`) + } + break + case 'darwin': + localFileExisted = existsSync(join(__dirname, 'index.darwin-universal.node')) + try { + if (localFileExisted) { + nativeBinding = require('./index.darwin-universal.node') } else { - try { - return require('./index.linux-arm-gnueabihf.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-linux-arm-gnueabihf') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-linux-arm-gnueabihf/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } + nativeBinding = require('@databricks/databricks-sql-kernel-darwin-universal') } - } else if (process.arch === 'loong64') { - if (isMusl()) { - try { - return require('./index.linux-loong64-musl.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-linux-loong64-musl') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-linux-loong64-musl/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else { + break + } catch {} + switch (arch) { + case 'x64': + localFileExisted = existsSync(join(__dirname, 'index.darwin-x64.node')) try { - return require('./index.linux-loong64-gnu.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-linux-loong64-gnu') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-linux-loong64-gnu/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (localFileExisted) { + nativeBinding = require('./index.darwin-x64.node') + } else { + nativeBinding = require('@databricks/databricks-sql-kernel-darwin-x64') } - return binding - } catch (e) { - loadErrors.push(e) - } - } - } else if (process.arch === 'riscv64') { - if (isMusl()) { - try { - return require('./index.linux-riscv64-musl.node') } catch (e) { - loadErrors.push(e) + loadError = e } + break + case 'arm64': + localFileExisted = existsSync( + join(__dirname, 'index.darwin-arm64.node') + ) try { - const binding = require('@databricks/databricks-sql-kernel-linux-riscv64-musl') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-linux-riscv64-musl/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (localFileExisted) { + nativeBinding = require('./index.darwin-arm64.node') + } else { + nativeBinding = require('@databricks/databricks-sql-kernel-darwin-arm64') } - return binding } catch (e) { - loadErrors.push(e) + loadError = e } + break + default: + throw new Error(`Unsupported architecture on macOS: ${arch}`) + } + break + case 'freebsd': + if (arch !== 'x64') { + throw new Error(`Unsupported architecture on FreeBSD: ${arch}`) + } + localFileExisted = existsSync(join(__dirname, 'index.freebsd-x64.node')) + try { + if (localFileExisted) { + nativeBinding = require('./index.freebsd-x64.node') } else { - try { - return require('./index.linux-riscv64-gnu.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-linux-riscv64-gnu') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-linux-riscv64-gnu/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } - } else if (process.arch === 'ppc64') { - try { - return require('./index.linux-ppc64-gnu.node') - } catch (e) { - loadErrors.push(e) + nativeBinding = require('@databricks/databricks-sql-kernel-freebsd-x64') } - try { - const binding = require('@databricks/databricks-sql-kernel-linux-ppc64-gnu') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-linux-ppc64-gnu/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else if (process.arch === 's390x') { - try { - return require('./index.linux-s390x-gnu.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-linux-s390x-gnu') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-linux-s390x-gnu/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else { - loadErrors.push(new Error(`Unsupported architecture on Linux: ${process.arch}`)) + } catch (e) { + loadError = e } - } else if (process.platform === 'openharmony') { - if (process.arch === 'arm64') { - try { - return require('./index.openharmony-arm64.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-openharmony-arm64') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-openharmony-arm64/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else if (process.arch === 'x64') { - try { - return require('./index.openharmony-x64.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-openharmony-x64') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-openharmony-x64/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + break + case 'linux': + switch (arch) { + case 'x64': + if (isMusl()) { + localFileExisted = existsSync( + join(__dirname, 'index.linux-x64-musl.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./index.linux-x64-musl.node') + } else { + nativeBinding = require('@databricks/databricks-sql-kernel-linux-x64-musl') + } + } catch (e) { + loadError = e + } + } else { + localFileExisted = existsSync( + join(__dirname, 'index.linux-x64-gnu.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./index.linux-x64-gnu.node') + } else { + nativeBinding = require('@databricks/databricks-sql-kernel-linux-x64-gnu') + } + } catch (e) { + loadError = e + } } - return binding - } catch (e) { - loadErrors.push(e) - } - } else if (process.arch === 'arm') { - try { - return require('./index.openharmony-arm.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-openharmony-arm') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-openharmony-arm/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + break + case 'arm64': + if (isMusl()) { + localFileExisted = existsSync( + join(__dirname, 'index.linux-arm64-musl.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./index.linux-arm64-musl.node') + } else { + nativeBinding = require('@databricks/databricks-sql-kernel-linux-arm64-musl') + } + } catch (e) { + loadError = e + } + } else { + localFileExisted = existsSync( + join(__dirname, 'index.linux-arm64-gnu.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./index.linux-arm64-gnu.node') + } else { + nativeBinding = require('@databricks/databricks-sql-kernel-linux-arm64-gnu') + } + } catch (e) { + loadError = e + } } - return binding - } catch (e) { - loadErrors.push(e) - } - } else { - loadErrors.push(new Error(`Unsupported architecture on OpenHarmony: ${process.arch}`)) - } - } else { - loadErrors.push(new Error(`Unsupported OS: ${process.platform}, architecture: ${process.arch}`)) - } -} - -function createLoadErrorChain(errors) { - return errors.reduce((previous, current) => { - let message - try { - message = - current && typeof current.message === 'string' - ? current.message - : String(current) - } catch { - message = 'Unknown error' - } - const error = new Error(message) - error.cause = previous - return error - }, null) -} - -// NAPI_RS_FORCE_WASI is a tri-state flag: -// unset / any other value → native binding preferred, WASI is only a fallback -// 'true' → prefer WASI, but retain native as a lazy fallback -// 'error' → require WASI without initializing a native fallback -// Treating any non-empty string as truthy (the historical behavior) meant -// NAPI_RS_FORCE_WASI=false, NAPI_RS_FORCE_WASI=0, etc. inadvertently triggered -// the WASI path, causing ENOENT for packages shipped without a .wasi.cjs file. -// -// NAPI_RS_WASI_FLAVOR selects one exact generated flavor and implies strict -// WASI loading. It never crosses into another flavor or falls back to native. -const __napiWasiFlavors = ["wasm32-wasi"] -const __napiWasiFlavor = process.env.NAPI_RS_WASI_FLAVOR -const __napiWasiFlavorRequested = - typeof __napiWasiFlavor === 'string' && __napiWasiFlavor.length > 0 -if ( - __napiWasiFlavorRequested && - __napiWasiFlavors.indexOf(__napiWasiFlavor) === -1 -) { - throw new Error( - 'Unsupported WASI flavor "' + - __napiWasiFlavor + - '". Available flavors: ' + - __napiWasiFlavors.join(', '), - ) -} -const forceWasiError = process.env.NAPI_RS_FORCE_WASI === 'error' -const forceWasi = - process.env.NAPI_RS_FORCE_WASI === 'true' || - forceWasiError || - __napiWasiFlavorRequested - -if (!forceWasi) { - nativeBinding = requireNative() -} - -if (!nativeBinding || forceWasi) { - let wasiBinding = null - let wasiBindingLoaded = false - const wasiBindingErrors = [] - const __napiWasiResolveCandidate = (specifier, isPackage, localArtifacts) => { - try { - require.resolve(specifier) - } catch (resolveError) { - if (!resolveError || resolveError.code !== 'MODULE_NOT_FOUND') { - throw resolveError - } - if (isPackage) { - try { - require.resolve(specifier + '/package.json') - } catch (packageError) { - if (packageError && packageError.code === 'MODULE_NOT_FOUND') { - return resolveError + break + case 'arm': + if (isMusl()) { + localFileExisted = existsSync( + join(__dirname, 'index.linux-arm-musleabihf.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./index.linux-arm-musleabihf.node') + } else { + nativeBinding = require('@databricks/databricks-sql-kernel-linux-arm-musleabihf') + } + } catch (e) { + loadError = e + } + } else { + localFileExisted = existsSync( + join(__dirname, 'index.linux-arm-gnueabihf.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./index.linux-arm-gnueabihf.node') + } else { + nativeBinding = require('@databricks/databricks-sql-kernel-linux-arm-gnueabihf') + } + } catch (e) { + loadError = e } - // An exports restriction proves the package exists even when its - // package.json is not public. Preserve the root resolution failure. - throw resolveError } - // The package exists but its main/export target is broken. - throw resolveError - } - return resolveError - } - if (localArtifacts) { - let artifactError = null - for (let i = 0; i < localArtifacts.length; i++) { - try { - require.resolve(localArtifacts[i]) - return null - } catch (resolveError) { - if (!resolveError || resolveError.code !== 'MODULE_NOT_FOUND') { - throw resolveError + break + case 'riscv64': + if (isMusl()) { + localFileExisted = existsSync( + join(__dirname, 'index.linux-riscv64-musl.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./index.linux-riscv64-musl.node') + } else { + nativeBinding = require('@databricks/databricks-sql-kernel-linux-riscv64-musl') + } + } catch (e) { + loadError = e + } + } else { + localFileExisted = existsSync( + join(__dirname, 'index.linux-riscv64-gnu.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./index.linux-riscv64-gnu.node') + } else { + nativeBinding = require('@databricks/databricks-sql-kernel-linux-riscv64-gnu') + } + } catch (e) { + loadError = e } - artifactError = resolveError } - } - return artifactError - } - return null - } - if (!wasiBindingLoaded && (!__napiWasiFlavorRequested || __napiWasiFlavor === "wasm32-wasi")) { - let candidateError = null - let candidateFailed = false - try { - candidateError = __napiWasiResolveCandidate('./index.wasi.cjs', false, ["./index.wasm32-wasi.debug.wasm","./index.wasm32-wasi.wasm"]) - candidateFailed = candidateError !== null - if (!candidateFailed) { - wasiBinding = require('./index.wasi.cjs') - nativeBinding = wasiBinding - wasiBindingLoaded = true - } - } catch (err) { - candidateError = err - candidateFailed = true - } - if (candidateFailed) { - wasiBindingErrors.push(candidateError) - loadErrors.push(candidateError) - } - } - if (!wasiBindingLoaded && (!__napiWasiFlavorRequested || __napiWasiFlavor === "wasm32-wasi")) { - let candidateError = null - let candidateFailed = false - try { - candidateError = __napiWasiResolveCandidate('@databricks/databricks-sql-kernel-wasm32-wasi', true, undefined) - candidateFailed = candidateError !== null - if (!candidateFailed) { - if (process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-wasm32-wasi/package.json').version - if (bindingPackageVersion !== '0.2.0') { - throw new Error(`WASI binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + break + case 's390x': + localFileExisted = existsSync( + join(__dirname, 'index.linux-s390x-gnu.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./index.linux-s390x-gnu.node') + } else { + nativeBinding = require('@databricks/databricks-sql-kernel-linux-s390x-gnu') } + } catch (e) { + loadError = e } - wasiBinding = require('@databricks/databricks-sql-kernel-wasm32-wasi') - nativeBinding = wasiBinding - wasiBindingLoaded = true - } - } catch (err) { - candidateError = err - candidateFailed = true + break + default: + throw new Error(`Unsupported architecture on Linux: ${arch}`) } - if (candidateFailed) { - wasiBindingErrors.push(candidateError) - loadErrors.push(candidateError) - } - } - if ( - !wasiBindingLoaded && - forceWasi && - !forceWasiError && - !__napiWasiFlavorRequested - ) { - nativeBinding = requireNative() - } - if ((forceWasiError || __napiWasiFlavorRequested) && !wasiBindingLoaded) { - const error = new Error( - __napiWasiFlavorRequested - ? 'WASI binding for flavor "' + __napiWasiFlavor + '" not found' - : 'WASI binding not found and NAPI_RS_FORCE_WASI is set to error', - ) - error.cause = createLoadErrorChain(wasiBindingErrors) - throw error - } + break + default: + throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`) } if (!nativeBinding) { - if (loadErrors.length > 0) { - const error = new Error( - `Cannot find native binding. ` + - `npm has a bug related to optional dependencies (https://github.com/npm/cli/issues/4828). ` + - 'Please try `npm i` again after removing both package-lock.json and node_modules directory.', - ) - // assign instead of the `new Error(message, { cause })` options form, - // which Node < 16.9 silently ignores - error.cause = createLoadErrorChain(loadErrors) - throw error + if (loadError) { + throw loadError } throw new Error(`Failed to load native binding`) } -module.exports = nativeBinding -module.exports.AsyncResultHandle = nativeBinding.AsyncResultHandle -module.exports.AsyncStatement = nativeBinding.AsyncStatement -module.exports.CancellableExecution = nativeBinding.CancellableExecution -module.exports.Connection = nativeBinding.Connection -module.exports.Statement = nativeBinding.Statement -module.exports.AuthMode = nativeBinding.AuthMode -module.exports.initKernelLogging = nativeBinding.initKernelLogging -module.exports.kernelLoggingStats = nativeBinding.kernelLoggingStats -module.exports.openSession = nativeBinding.openSession -module.exports.setKernelLogLevel = nativeBinding.setKernelLogLevel -module.exports.version = nativeBinding.version +const { AsyncStatement, AsyncResultHandle, CancellableExecution, Connection, AuthMode, openSession, initKernelLogging, kernelLoggingStats, setKernelLogLevel, Statement, version } = nativeBinding + +module.exports.AsyncStatement = AsyncStatement +module.exports.AsyncResultHandle = AsyncResultHandle +module.exports.CancellableExecution = CancellableExecution +module.exports.Connection = Connection +module.exports.AuthMode = AuthMode +module.exports.openSession = openSession +module.exports.initKernelLogging = initKernelLogging +module.exports.kernelLoggingStats = kernelLoggingStats +module.exports.setKernelLogLevel = setKernelLogLevel +module.exports.Statement = Statement +module.exports.version = version diff --git a/package.json b/package.json index 756d997e..d321e802 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "test": "nyc --report-dir=${NYC_REPORT_DIR:-coverage_unit} mocha --config tests/unit/.mocharc.js", "update-version": "node bin/update-version.js && prettier --write ./lib/version.ts", "build": "npm run update-version && tsc --project tsconfig.build.json", - "build:native": "bash -c 'cd ${DATABRICKS_SQL_KERNEL_REPO:-../../databricks-sql-kernel}/napi && npx --yes --package @napi-rs/cli@3.8.2 napi build --platform ${BUILD_PROFILE:---release} && cp index.* $OLDPWD/native/kernel/'", + "build:native": "bash bin/build-native.sh", "prepack": "test -f native/kernel/index.js || { echo 'ERROR: native/kernel/index.js (napi-rs router) is missing — the published tarball would fail to load kernel. It is committed to git; run `npm run build:native` if you removed it.' >&2; exit 1; }", "watch": "tsc --project tsconfig.build.json --watch", "type-check": "tsc --noEmit", diff --git a/tests/unit/kernel/native-packaging.test.ts b/tests/unit/kernel/native-packaging.test.ts index 77a1c934..f3f8508f 100644 --- a/tests/unit/kernel/native-packaging.test.ts +++ b/tests/unit/kernel/native-packaging.test.ts @@ -28,10 +28,8 @@ describe('kernel native binding — packaging (native/kernel/index.js)', () => { // depend on the module system's `__dirname`. const indexJs = readFileSync(join(process.cwd(), 'native/kernel/index.js'), 'utf8'); - // Every fallback package, including CLI v3's package.json version checks. - const required = Array.from(indexJs.matchAll(/require\('(@databricks\/[^']+)'\)/g)).map((m) => - m[1].replace(/\/package\.json$/, ''), - ); + // Every `require('@databricks/...')` fallback in the generated router. + const required = Array.from(indexJs.matchAll(/require\('(@databricks\/[^']+)'\)/g)).map((m) => m[1]); it('declares at least one @databricks/* npm fallback', () => { expect(required.length, 'no @databricks/* require() found in the router').to.be.greaterThan(0); From caaee3cc6606734eeed7b74656db1d5e32907891 Mon Sep 17 00:00:00 2001 From: Vu Anh Phung Date: Wed, 12 Aug 2026 20:19:57 +0000 Subject: [PATCH 5/7] Resolve napi version through Cargo metadata Signed-off-by: Vu Anh Phung --- bin/build-native.sh | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/bin/build-native.sh b/bin/build-native.sh index 93109348..f5d31a31 100644 --- a/bin/build-native.sh +++ b/bin/build-native.sh @@ -5,7 +5,17 @@ set -euo pipefail driver_repo=$(pwd) kernel_repo=${DATABRICKS_SQL_KERNEL_REPO:-../../databricks-sql-kernel} napi_dir="${kernel_repo}/napi" -napi_major=$(sed -nE 's/^napi = \{ version = "([0-9]+).*/\1/p' "${napi_dir}/Cargo.toml") +napi_major=$( + cargo metadata --format-version 1 --locked --manifest-path "${napi_dir}/Cargo.toml" | + node -e ' + const metadata = JSON.parse(require("fs").readFileSync(0, "utf8")); + const rootId = metadata.resolve.root ?? metadata.workspace_members[0]; + const root = metadata.resolve.nodes.find(({ id }) => id === rootId); + const napiId = root?.deps.find(({ name }) => name === "napi")?.pkg; + const version = metadata.packages.find(({ id }) => id === napiId)?.version; + process.stdout.write(version?.split(".")[0] ?? ""); + ' +) # napi-rs v2 and v3 derive macros expect different CLI environment variables. case "${napi_major}" in From c2eecf789615425c14bbf24add4775db995b4bb7 Mon Sep 17 00:00:00 2001 From: Vu Anh Phung Date: Wed, 12 Aug 2026 20:26:14 +0000 Subject: [PATCH 6/7] Preserve multi-argument native build profiles Signed-off-by: Vu Anh Phung --- bin/build-native.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bin/build-native.sh b/bin/build-native.sh index f5d31a31..7feb84a4 100644 --- a/bin/build-native.sh +++ b/bin/build-native.sh @@ -31,6 +31,8 @@ case "${napi_major}" in ;; esac +read -r -a build_profile <<< "${BUILD_PROFILE:---release}" + cd "${napi_dir}" -"${cli[@]}" build --platform "${BUILD_PROFILE:---release}" +"${cli[@]}" build --platform "${build_profile[@]}" cp index.* "${driver_repo}/native/kernel/" From 18e0516eb3645c4ed720ae9834c3ac54f5696def Mon Sep 17 00:00:00 2001 From: Vu Anh Phung Date: Wed, 12 Aug 2026 20:41:25 +0000 Subject: [PATCH 7/7] Support empty native build profiles Signed-off-by: Vu Anh Phung --- bin/build-native.sh | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/bin/build-native.sh b/bin/build-native.sh index 7feb84a4..f4743b99 100644 --- a/bin/build-native.sh +++ b/bin/build-native.sh @@ -31,8 +31,13 @@ case "${napi_major}" in ;; esac -read -r -a build_profile <<< "${BUILD_PROFILE:---release}" +build_profile=${BUILD_PROFILE---release} cd "${napi_dir}" -"${cli[@]}" build --platform "${build_profile[@]}" +if [[ -n "${build_profile//[[:space:]]/}" ]]; then + read -r -a build_profile_args <<< "${build_profile}" + "${cli[@]}" build --platform "${build_profile_args[@]}" +else + "${cli[@]}" build --platform +fi cp index.* "${driver_repo}/native/kernel/"