From 753609db3f411c2ae65127fbfa3dc2f8758a4cba Mon Sep 17 00:00:00 2001 From: Vu Anh Phung Date: Mon, 17 Aug 2026 18:34:11 +0000 Subject: [PATCH 1/8] feat(kernel): forward identity federation client ID --- KERNEL_REV | 2 +- lib/contracts/IDBSQLClient.ts | 4 ++++ lib/kernel/KernelAuth.ts | 20 +++++++++++++++++++- native/kernel/index.d.ts | 11 ++++++++--- tests/unit/kernel/auth-m2m.test.ts | 15 +++++++++++++++ tests/unit/kernel/auth-pat.test.ts | 22 ++++++++++++++++++++++ tests/unit/kernel/auth-u2m.test.ts | 11 +++++++++++ 7 files changed, 80 insertions(+), 5 deletions(-) diff --git a/KERNEL_REV b/KERNEL_REV index 7dd91996..95cfce81 100644 --- a/KERNEL_REV +++ b/KERNEL_REV @@ -1 +1 @@ -0d46716c466897148dfc1d2976ff03bdf097998c +eff8950428f4e6cc9975c663ec919f334962f7d0 diff --git a/lib/contracts/IDBSQLClient.ts b/lib/contracts/IDBSQLClient.ts index bbaa4c69..88bcb980 100644 --- a/lib/contracts/IDBSQLClient.ts +++ b/lib/contracts/IDBSQLClient.ts @@ -14,6 +14,8 @@ type AuthOptions = | { authType?: 'access-token'; token: string; + /** Kernel backend: selects mandatory SP-wide Workload Identity Federation. */ + identityFederationClientId?: string; } | { authType: 'databricks-oauth'; @@ -26,6 +28,8 @@ type AuthOptions = // U2M flow to `['sql', 'offline_access']` (parity with the Thrift driver's // `defaultOAuthScopes`), overriding the kernel's bare `all-apis offline_access`. oauthScopes?: Array; + /** Kernel backend: selects mandatory SP-wide Workload Identity Federation. */ + identityFederationClientId?: string; } | { authType: 'custom'; diff --git a/lib/kernel/KernelAuth.ts b/lib/kernel/KernelAuth.ts index 7cf99afa..ee6aedc7 100644 --- a/lib/kernel/KernelAuth.ts +++ b/lib/kernel/KernelAuth.ts @@ -61,6 +61,9 @@ const DEFAULT_OAUTH_CLIENT_ID = 'databricks-sql-connector'; * everything else (client_id, scopes, callback timeout, * token_url_override) uses kernel defaults. * + * A non-empty `identityFederationClientId` selects mandatory SP-wide + * workload-identity token exchange for every auth mode. + * * The `authMode` string literals MUST match the napi-emitted `AuthMode` * variant names verbatim (`'Pat'`, `'OAuthM2m'`, `'OAuthU2m'` — napi-rs's * `#[napi(string_enum)]` without an explicit case option emits the @@ -212,10 +215,19 @@ export interface KernelProxyOptions { }; } +export interface KernelFederationOptions { + /** + * SP-wide Workload Identity Federation client id. Omitted selects BYOT / + * account-wide WIF. + */ + identityFederationClientId?: string; +} + export type KernelNativeConnectionOptions = KernelSessionDefaults & KernelTlsOptions & KernelHttpOptions & KernelProxyOptions & + KernelFederationOptions & ( | { hostName: string; @@ -556,7 +568,8 @@ export function buildKernelConnectionOptions(options: ConnectionOptions): Kernel maxConnections?: number; } & KernelTlsOptions & KernelHttpOptions & - KernelProxyOptions = { + KernelProxyOptions & + KernelFederationOptions = { hostName: options.host, httpPath: prependSlash(options.path), // Match the NodeJS Thrift driver, which surfaces INTERVAL columns as @@ -576,6 +589,11 @@ export function buildKernelConnectionOptions(options: ConnectionOptions): Kernel ...buildKernelProxyOptions(options), }; + const { identityFederationClientId } = options as { identityFederationClientId?: string }; + if (identityFederationClientId) { + base.identityFederationClientId = identityFederationClientId; + } + // kernel-only pool sizing; read via cast to match how this function reads the // other kernel-specific options (TLS) — they live on the internal options // surface, not the published public `ConnectionOptions` `.d.ts`. diff --git a/native/kernel/index.d.ts b/native/kernel/index.d.ts index 0b042121..f401c31e 100644 --- a/native/kernel/index.d.ts +++ b/native/kernel/index.d.ts @@ -159,7 +159,7 @@ export interface ProxyInput { * - `Pat` — `token` required. * - `OAuthM2m` — `oauthClientId` + `oauthClientSecret` required. * - `OAuthU2m` — `oauthClientId` / `oauthRedirectPort` optional - * (defaults to the `databricks-sql-connector` client on port 8020). + * (defaults to the `databricks-sql-connector` client on port 8030). * * Catalog / schema / sessionConf are applied once at session creation * and remain in effect for every statement run on the resulting @@ -197,14 +197,19 @@ export interface ConnectionOptions { oauthClientSecret?: string /** * Localhost callback port for the [`AuthMode::OAuthU2m`] browser - * flow. Omitted ⇒ kernel default (8020). + * flow. Omitted ⇒ kernel default (8030). */ oauthRedirectPort?: number /** * OAuth scopes override (M2M / U2M). Omitted ⇒ kernel defaults - * (`["all-apis"]` for M2M; `["all-apis", "offline_access"]` for U2M). + * (`["all-apis"]` for M2M; `["sql", "offline_access"]` for U2M). */ oauthScopes?: Array + /** + * SP-wide Workload Identity Federation client id used during mandatory + * token exchange. Omitted selects BYOT / account-wide WIF. + */ + identityFederationClientId?: string /** * Default catalog for statements executed on this session. * Routed through the kernel's `DefaultOpts` and onto the SEA diff --git a/tests/unit/kernel/auth-m2m.test.ts b/tests/unit/kernel/auth-m2m.test.ts index 7b55bcb2..7df6b6d3 100644 --- a/tests/unit/kernel/auth-m2m.test.ts +++ b/tests/unit/kernel/auth-m2m.test.ts @@ -43,6 +43,19 @@ describe('KernelAuth + KernelBackend — OAuth M2M auth flow', () => { }); }); + it('forwards a federation client id on M2M auth', () => { + const native = buildKernelConnectionOptions({ + host: 'example.cloud.databricks.com', + path: '/sql/1.0/warehouses/abc', + authType: 'databricks-oauth', + oauthClientId: 'client-uuid', + oauthClientSecret: 'dose-fake-secret', + identityFederationClientId: 'federation-client', + }); + + expect(native.identityFederationClientId).to.equal('federation-client'); + }); + it('defaults M2M oauthScopes to all-apis (Thrift + kernel parity)', () => { const native = buildKernelConnectionOptions({ host: 'example.cloud.databricks.com', @@ -190,6 +203,7 @@ describe('KernelAuth + KernelBackend — OAuth M2M auth flow', () => { authType: 'databricks-oauth', oauthClientId: 'client-uuid', oauthClientSecret: 'dose-fake-secret', + identityFederationClientId: 'federation-client', }); const session = await backend.openSession({}); @@ -207,6 +221,7 @@ describe('KernelAuth + KernelBackend — OAuth M2M auth flow', () => { oauthClientId: 'client-uuid', oauthClientSecret: 'dose-fake-secret', oauthScopes: ['all-apis'], + identityFederationClientId: 'federation-client', }); await session.close(); diff --git a/tests/unit/kernel/auth-pat.test.ts b/tests/unit/kernel/auth-pat.test.ts index 5304298c..9fac0088 100644 --- a/tests/unit/kernel/auth-pat.test.ts +++ b/tests/unit/kernel/auth-pat.test.ts @@ -53,6 +53,28 @@ describe('KernelAuth — PAT auth options builder', () => { } }); + it('forwards a federation client id on PAT auth', () => { + const native = buildKernelConnectionOptions({ + host: 'example.cloud.databricks.com', + path: '/sql/1.0/warehouses/abc', + token: 'dapi-fake-pat', + identityFederationClientId: 'federation-client', + }); + + expect(native.identityFederationClientId).to.equal('federation-client'); + }); + + it('omits an empty federation client id', () => { + const native = buildKernelConnectionOptions({ + host: 'example.cloud.databricks.com', + path: '/sql/1.0/warehouses/abc', + token: 'dapi-fake-pat', + identityFederationClientId: '', + }); + + expect(native).not.to.have.property('identityFederationClientId'); + }); + it('prepends `/` to a path missing the leading slash', () => { const opts: ConnectionOptions = { host: 'example.cloud.databricks.com', diff --git a/tests/unit/kernel/auth-u2m.test.ts b/tests/unit/kernel/auth-u2m.test.ts index c21493d5..3ca8b4cc 100644 --- a/tests/unit/kernel/auth-u2m.test.ts +++ b/tests/unit/kernel/auth-u2m.test.ts @@ -40,6 +40,17 @@ describe('KernelAuth + KernelBackend — OAuth U2M auth flow', () => { }); }); + it('forwards a federation client id on U2M auth', () => { + const native = buildKernelConnectionOptions({ + host: 'example.cloud.databricks.com', + path: '/sql/1.0/warehouses/abc', + authType: 'databricks-oauth', + identityFederationClientId: 'federation-client', + }); + + expect(native.identityFederationClientId).to.equal('federation-client'); + }); + it('defaults U2M oauthScopes to Thrift parity (sql offline_access)', () => { const native = buildKernelConnectionOptions({ host: 'example.cloud.databricks.com', From f26244c7cf934e4780363293e380a9f745952a4b Mon Sep 17 00:00:00 2001 From: Vu Anh Phung Date: Wed, 19 Aug 2026 20:17:52 +0000 Subject: [PATCH 2/8] feat(kernel): support static token federation --- CONNECTION_PARAMETERS.md | 23 +++--- lib/contracts/IDBSQLClient.ts | 4 - lib/kernel/KernelAuth.ts | 35 ++++++-- lib/kernel/KernelBackend.ts | 11 ++- tests/unit/kernel/auth-m2m.test.ts | 15 ---- tests/unit/kernel/auth-pat.test.ts | 26 +----- tests/unit/kernel/auth-static-token.test.ts | 91 +++++++++++++++++++++ tests/unit/kernel/auth-u2m.test.ts | 11 --- 8 files changed, 136 insertions(+), 80 deletions(-) create mode 100644 tests/unit/kernel/auth-static-token.test.ts diff --git a/CONNECTION_PARAMETERS.md b/CONNECTION_PARAMETERS.md index 30853562..dd2f6011 100644 --- a/CONNECTION_PARAMETERS.md +++ b/CONNECTION_PARAMETERS.md @@ -55,16 +55,16 @@ column. ## Authentication -| Option | Type | Thrift | Kernel | Default Value | Note | -| ---------------------------------------------- | -------------------------------------------------------------------------- | :------: | :------: | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `authType` — supported on both backends | `'access-token'` \| `'databricks-oauth'` | ✅ | ✅ | `'access-token'` | The two auth modes both backends accept. `access-token` uses `token` (PAT) and is the default when `authType` is omitted. `databricks-oauth` covers M2M (`oauthClientId` + `oauthClientSecret`; kernel runs OIDC discovery + client-credentials internally) and U2M (browser; no secret — kernel U2M differs slightly, see the OAuth sub-option rows below). | -| `authType` — Thrift-only | `'custom'` \| `'token-provider'` \| `'external-token'` \| `'static-token'` | ✅ | ❌ | — | **Thrift-only.** `custom` (`provider: IAuthentication`), `token-provider` (`tokenProvider: ITokenProvider`), `external-token` (`getToken: TokenCallback`), `static-token` (`staticToken`). The kernel throws `unsupported auth mode` for all four — it supports only the two modes above. | -| `oauthScopes` | `Array` | ❌ | ✅ | U2M `['sql','offline_access']`, M2M `['all-apis']` | **Thrift ignores `oauthScopes`** — `createAuthProvider` never threads it into `DatabricksOAuth`, so `authenticate()` always falls back to `defaultOAuthScopes` (`['sql','offline_access']`). Only the kernel honors a custom `oauthScopes`; its defaults happen to match Thrift's fallback. | -| `oauthClientId` (U2M) | `string` | ✅ | ✅ | napi default `client_id` when absent | The kernel adapter (`buildKernelConnectionOptions`) forwards a custom `oauthClientId` verbatim on the U2M arm; when it is absent the napi binding applies its own default `client_id`. Whether the native binding then honors or rejects a custom id is not observable from this repo — the TypeScript layer neither hardcodes an id nor rejects one. | -| `oauthClientId` + no secret | `string` | ✅ (U2M) | ✅ (U2M) | — | **Parity.** The kernel keys flow selection off `oauthClientSecret` presence exactly like Thrift, so `oauthClientId` + no secret routes to **U2M** (with the id forwarded) — it does **not** throw an M2M "secret required" error. | -| `azureTenantId` / `useDatabricksOAuthInAzure` | `string` / `boolean` | ✅ | ❌ | — | **Thrift-only.** Kernel rejects Azure-direct (Entra) OAuth; workspace-OIDC discovery covers Azure workspaces without it. | -| `persistence` (custom OAuth token store) | `OAuthPersistence` | ✅ | ❌ | — | **Thrift-only.** Kernel throws; it auto-persists U2M tokens to `~/.config/databricks-sql-kernel/oauth/` and does not cache M2M. | -| `enableTokenFederation` / `federationClientId` | `boolean` / `string` | ✅ | ❌ | `false` / — | **Thrift-only** (available on the token-provider / external-token / static-token arms, none of which the kernel supports). | +| Option | Type | Thrift | Kernel | Default Value | Note | +| ---------------------------------------------- | ------------------------------------------------------------ | :------: | :------: | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `authType` — supported on both backends | `'access-token'` \| `'databricks-oauth'` \| `'static-token'` | ✅ | ✅ | `'access-token'` | `access-token` uses `token` (PAT) and is the default when `authType` is omitted. `static-token` uses `staticToken`; the kernel maps it to its native bearer-token mode. `databricks-oauth` covers M2M (`oauthClientId` + `oauthClientSecret`) and U2M (browser; no secret). | +| `authType` — Thrift-only | `'custom'` \| `'token-provider'` \| `'external-token'` | ✅ | ❌ | — | **Thrift-only.** `custom` uses `provider: IAuthentication`, `token-provider` uses `tokenProvider: ITokenProvider`, and `external-token` uses `getToken: TokenCallback`. The kernel throws `unsupported auth mode` for these modes. | +| `oauthScopes` | `Array` | ❌ | ✅ | U2M `['sql','offline_access']`, M2M `['all-apis']` | **Thrift ignores `oauthScopes`** — `createAuthProvider` never threads it into `DatabricksOAuth`, so `authenticate()` always falls back to `defaultOAuthScopes` (`['sql','offline_access']`). Only the kernel honors a custom `oauthScopes`; its defaults happen to match Thrift's fallback. | +| `oauthClientId` (U2M) | `string` | ✅ | ✅ | napi default `client_id` when absent | The kernel adapter (`buildKernelConnectionOptions`) forwards a custom `oauthClientId` verbatim on the U2M arm; when it is absent the napi binding applies its own default `client_id`. Whether the native binding then honors or rejects a custom id is not observable from this repo — the TypeScript layer neither hardcodes an id nor rejects one. | +| `oauthClientId` + no secret | `string` | ✅ (U2M) | ✅ (U2M) | — | **Parity.** The kernel keys flow selection off `oauthClientSecret` presence exactly like Thrift, so `oauthClientId` + no secret routes to **U2M** (with the id forwarded) — it does **not** throw an M2M "secret required" error. | +| `azureTenantId` / `useDatabricksOAuthInAzure` | `string` / `boolean` | ✅ | ❌ | — | **Thrift-only.** Kernel rejects Azure-direct (Entra) OAuth; workspace-OIDC discovery covers Azure workspaces without it. | +| `persistence` (custom OAuth token store) | `OAuthPersistence` | ✅ | ❌ | — | **Thrift-only.** Kernel throws; it auto-persists U2M tokens to `~/.config/databricks-sql-kernel/oauth/` and does not cache M2M. | +| `enableTokenFederation` / `federationClientId` | `boolean` / `string` | ✅ | ⚠️ | `false` / — | On the kernel backend these options are honored only for `static-token`; `federationClientId` is forwarded only when `enableTokenFederation` is `true`. The Thrift backend also supports them for `token-provider` and `external-token`. | ## HTTP client, proxy, retries @@ -161,8 +161,7 @@ backend, so they are read regardless of `useKernel`. Defaults are sourced from 1. `enableMetricViewMetadata` — auto-injected for both backends in `DBSQLClient.openSession`, but the conf key is likely dropped by the kernel's session-conf allowlist, so it has no effect on the kernel path. -2. Auth types `custom`, `token-provider`, `external-token`, `static-token`, - plus `enableTokenFederation` / `federationClientId`. +2. Auth types `custom`, `token-provider`, and `external-token`. 3. `azureTenantId` / `useDatabricksOAuthInAzure` (Azure-direct OAuth). 4. `persistence` (custom OAuth token store). 5. SOCKS proxies. diff --git a/lib/contracts/IDBSQLClient.ts b/lib/contracts/IDBSQLClient.ts index 88bcb980..bbaa4c69 100644 --- a/lib/contracts/IDBSQLClient.ts +++ b/lib/contracts/IDBSQLClient.ts @@ -14,8 +14,6 @@ type AuthOptions = | { authType?: 'access-token'; token: string; - /** Kernel backend: selects mandatory SP-wide Workload Identity Federation. */ - identityFederationClientId?: string; } | { authType: 'databricks-oauth'; @@ -28,8 +26,6 @@ type AuthOptions = // U2M flow to `['sql', 'offline_access']` (parity with the Thrift driver's // `defaultOAuthScopes`), overriding the kernel's bare `all-apis offline_access`. oauthScopes?: Array; - /** Kernel backend: selects mandatory SP-wide Workload Identity Federation. */ - identityFederationClientId?: string; } | { authType: 'custom'; diff --git a/lib/kernel/KernelAuth.ts b/lib/kernel/KernelAuth.ts index ee6aedc7..8e4b4123 100644 --- a/lib/kernel/KernelAuth.ts +++ b/lib/kernel/KernelAuth.ts @@ -61,8 +61,9 @@ const DEFAULT_OAUTH_CLIENT_ID = 'databricks-sql-connector'; * everything else (client_id, scopes, callback timeout, * token_url_override) uses kernel defaults. * - * A non-empty `identityFederationClientId` selects mandatory SP-wide - * workload-identity token exchange for every auth mode. + * `static-token` reuses the native PAT bearer-token mode. When its + * `enableTokenFederation` option is true, a non-empty `federationClientId` + * is forwarded under the native name `identityFederationClientId`. * * The `authMode` string literals MUST match the napi-emitted `AuthMode` * variant names verbatim (`'Pat'`, `'OAuthM2m'`, `'OAuthU2m'` — napi-rs's @@ -455,6 +456,10 @@ export function buildKernelHttpOptions(options: ConnectionOptions): KernelHttpOp * - PAT: `authType: 'access-token'` (or undefined, which already means * PAT throughout the existing driver — see * `DBSQLClient.createAuthProvider`). + * - Static token: `authType: 'static-token'` + `staticToken`. The token is + * forwarded through the native PAT bearer-token mode. Optional SP-wide + * token federation is enabled by `enableTokenFederation` and selected by + * `federationClientId`. * - OAuth M2M: `authType: 'databricks-oauth'` + `oauthClientId` + * `oauthClientSecret`. Kernel handles OIDC discovery, client_credentials * exchange, and re-auth on expiry internally. @@ -589,11 +594,6 @@ export function buildKernelConnectionOptions(options: ConnectionOptions): Kernel ...buildKernelProxyOptions(options), }; - const { identityFederationClientId } = options as { identityFederationClientId?: string }; - if (identityFederationClientId) { - base.identityFederationClientId = identityFederationClientId; - } - // kernel-only pool sizing; read via cast to match how this function reads the // other kernel-specific options (TLS) — they live on the internal options // surface, not the published public `ConnectionOptions` `.d.ts`. @@ -639,6 +639,25 @@ export function buildKernelConnectionOptions(options: ConnectionOptions): Kernel return { ...base, authMode: 'Pat', token }; } + if (authType === 'static-token') { + const { staticToken, enableTokenFederation, federationClientId } = options as { + staticToken?: string; + enableTokenFederation?: boolean; + federationClientId?: string; + }; + if (typeof staticToken !== 'string' || isBlankOrReserved(staticToken)) { + throw new AuthenticationError( + "kernel backend: a non-empty token must be supplied via `staticToken` when using `authType: 'static-token'`.", + ); + } + return { + ...base, + authMode: 'Pat', + token: staticToken, + ...(enableTokenFederation && federationClientId ? { identityFederationClientId: federationClientId } : {}), + }; + } + if (authType === 'databricks-oauth') { if ((options as { token?: string }).token !== undefined) { throw new HiveDriverError( @@ -712,7 +731,7 @@ export function buildKernelConnectionOptions(options: ConnectionOptions): Kernel throw new HiveDriverError( `kernel backend: unsupported auth mode '${authType}'. ` + - "Supported modes on the kernel backend today: 'access-token' (PAT) and 'databricks-oauth' " + + "Supported modes on the kernel backend today: 'access-token' (PAT), 'static-token', and 'databricks-oauth' " + '(M2M with oauthClientId+oauthClientSecret, or U2M with neither).', ); } diff --git a/lib/kernel/KernelBackend.ts b/lib/kernel/KernelBackend.ts index 221e7beb..2b98a64b 100644 --- a/lib/kernel/KernelBackend.ts +++ b/lib/kernel/KernelBackend.ts @@ -47,10 +47,10 @@ export interface KernelBackendOptions { * kernel-backed implementation of `IBackend`. * * **M0 dispatch model:** the napi binding's `openSession()` already - * builds a kernel `Session` from PAT + hostname + httpPath, so there is + * builds a kernel `Session` from auth options + hostname + httpPath, so there is * no "connect" round-trip before `openSession` — `connect()` only - * captures the `ConnectionOptions` and validates that PAT auth is in - * use. The actual session open happens inside `openSession()`. + * captures and validates the `ConnectionOptions`. The actual session open + * happens inside `openSession()`. * * **Auth validation:** delegates to `buildKernelConnectionOptions` from * `KernelAuth`, which mirrors the existing DBSQLClient validation pattern @@ -84,9 +84,8 @@ export default class KernelBackend implements IBackend { } public async connect(options: ConnectionOptions): Promise { - // Validate PAT auth + capture the napi-binding option shape. - // Any non-PAT mode (or a missing/empty token) throws here, before - // we ever touch the native binding. + // Validate auth + capture the napi-binding option shape before touching + // the native binding. // Forward the driver's retry config to the kernel, which owns the retry // loop on the kernel path. This keeps kernel and Thrift governed by one retry // config (the same `ClientConfig` knobs the Thrift `HttpRetryPolicy` reads), diff --git a/tests/unit/kernel/auth-m2m.test.ts b/tests/unit/kernel/auth-m2m.test.ts index 7df6b6d3..7b55bcb2 100644 --- a/tests/unit/kernel/auth-m2m.test.ts +++ b/tests/unit/kernel/auth-m2m.test.ts @@ -43,19 +43,6 @@ describe('KernelAuth + KernelBackend — OAuth M2M auth flow', () => { }); }); - it('forwards a federation client id on M2M auth', () => { - const native = buildKernelConnectionOptions({ - host: 'example.cloud.databricks.com', - path: '/sql/1.0/warehouses/abc', - authType: 'databricks-oauth', - oauthClientId: 'client-uuid', - oauthClientSecret: 'dose-fake-secret', - identityFederationClientId: 'federation-client', - }); - - expect(native.identityFederationClientId).to.equal('federation-client'); - }); - it('defaults M2M oauthScopes to all-apis (Thrift + kernel parity)', () => { const native = buildKernelConnectionOptions({ host: 'example.cloud.databricks.com', @@ -203,7 +190,6 @@ describe('KernelAuth + KernelBackend — OAuth M2M auth flow', () => { authType: 'databricks-oauth', oauthClientId: 'client-uuid', oauthClientSecret: 'dose-fake-secret', - identityFederationClientId: 'federation-client', }); const session = await backend.openSession({}); @@ -221,7 +207,6 @@ describe('KernelAuth + KernelBackend — OAuth M2M auth flow', () => { oauthClientId: 'client-uuid', oauthClientSecret: 'dose-fake-secret', oauthScopes: ['all-apis'], - identityFederationClientId: 'federation-client', }); await session.close(); diff --git a/tests/unit/kernel/auth-pat.test.ts b/tests/unit/kernel/auth-pat.test.ts index 9fac0088..7d8b7b25 100644 --- a/tests/unit/kernel/auth-pat.test.ts +++ b/tests/unit/kernel/auth-pat.test.ts @@ -53,28 +53,6 @@ describe('KernelAuth — PAT auth options builder', () => { } }); - it('forwards a federation client id on PAT auth', () => { - const native = buildKernelConnectionOptions({ - host: 'example.cloud.databricks.com', - path: '/sql/1.0/warehouses/abc', - token: 'dapi-fake-pat', - identityFederationClientId: 'federation-client', - }); - - expect(native.identityFederationClientId).to.equal('federation-client'); - }); - - it('omits an empty federation client id', () => { - const native = buildKernelConnectionOptions({ - host: 'example.cloud.databricks.com', - path: '/sql/1.0/warehouses/abc', - token: 'dapi-fake-pat', - identityFederationClientId: '', - }); - - expect(native).not.to.have.property('identityFederationClientId'); - }); - it('prepends `/` to a path missing the leading slash', () => { const opts: ConnectionOptions = { host: 'example.cloud.databricks.com', @@ -133,8 +111,8 @@ describe('KernelAuth — PAT auth options builder', () => { ); }); - it('rejects external-token, static-token, and custom auth modes', () => { - const authTypes = ['external-token', 'static-token', 'custom'] as const; + it('rejects external-token and custom auth modes', () => { + const authTypes = ['external-token', 'custom'] as const; for (const authType of authTypes) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const opts = { diff --git a/tests/unit/kernel/auth-static-token.test.ts b/tests/unit/kernel/auth-static-token.test.ts new file mode 100644 index 00000000..708fb69b --- /dev/null +++ b/tests/unit/kernel/auth-static-token.test.ts @@ -0,0 +1,91 @@ +// Copyright (c) 2026 Databricks, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { expect } from 'chai'; +import expectNativeConnectionOptions from './_helpers/nativeOptions'; +import { buildKernelConnectionOptions } from '../../../lib/kernel/KernelAuth'; +import AuthenticationError from '../../../lib/errors/AuthenticationError'; + +describe('KernelAuth — static-token auth options builder', () => { + it('maps a static token to the native bearer-token mode', () => { + const native = buildKernelConnectionOptions({ + host: 'example.cloud.databricks.com', + path: '/sql/1.0/warehouses/abc', + authType: 'static-token', + staticToken: 'header.payload.signature', + }); + + expectNativeConnectionOptions(native, { + hostName: 'example.cloud.databricks.com', + httpPath: '/sql/1.0/warehouses/abc', + intervalsAsString: true, + authMode: 'Pat', + token: 'header.payload.signature', + }); + }); + + it('forwards federationClientId when token federation is enabled', () => { + const native = buildKernelConnectionOptions({ + host: 'example.cloud.databricks.com', + path: '/sql/1.0/warehouses/abc', + authType: 'static-token', + staticToken: 'header.payload.signature', + enableTokenFederation: true, + federationClientId: 'federation-client', + }); + + expect(native.identityFederationClientId).to.equal('federation-client'); + }); + + it('does not forward federationClientId when token federation is disabled', () => { + for (const enableTokenFederation of [undefined, false]) { + const native = buildKernelConnectionOptions({ + host: 'example.cloud.databricks.com', + path: '/sql/1.0/warehouses/abc', + authType: 'static-token', + staticToken: 'header.payload.signature', + enableTokenFederation, + federationClientId: 'federation-client', + }); + + expect(native).not.to.have.property('identityFederationClientId'); + } + }); + + it('omits an empty federationClientId', () => { + const native = buildKernelConnectionOptions({ + host: 'example.cloud.databricks.com', + path: '/sql/1.0/warehouses/abc', + authType: 'static-token', + staticToken: 'header.payload.signature', + enableTokenFederation: true, + federationClientId: '', + }); + + expect(native).not.to.have.property('identityFederationClientId'); + }); + + it('rejects a missing or blank static token', () => { + for (const staticToken of [undefined, '', ' ', 'undefined', 'null']) { + expect(() => + buildKernelConnectionOptions({ + host: 'example.cloud.databricks.com', + path: '/sql/1.0/warehouses/abc', + authType: 'static-token', + staticToken, + } as any), + ).to.throw(AuthenticationError, /non-empty token.*`staticToken`/); + } + }); +}); diff --git a/tests/unit/kernel/auth-u2m.test.ts b/tests/unit/kernel/auth-u2m.test.ts index 3ca8b4cc..c21493d5 100644 --- a/tests/unit/kernel/auth-u2m.test.ts +++ b/tests/unit/kernel/auth-u2m.test.ts @@ -40,17 +40,6 @@ describe('KernelAuth + KernelBackend — OAuth U2M auth flow', () => { }); }); - it('forwards a federation client id on U2M auth', () => { - const native = buildKernelConnectionOptions({ - host: 'example.cloud.databricks.com', - path: '/sql/1.0/warehouses/abc', - authType: 'databricks-oauth', - identityFederationClientId: 'federation-client', - }); - - expect(native.identityFederationClientId).to.equal('federation-client'); - }); - it('defaults U2M oauthScopes to Thrift parity (sql offline_access)', () => { const native = buildKernelConnectionOptions({ host: 'example.cloud.databricks.com', From b2016b7d41b8d470506b6327eee10289ee85811a Mon Sep 17 00:00:00 2001 From: Vu Anh Phung Date: Wed, 19 Aug 2026 21:36:15 +0000 Subject: [PATCH 3/8] fix(kernel): reject conflicting static token auth --- lib/kernel/KernelAuth.ts | 9 ++++++++- tests/unit/kernel/auth-static-token.test.ts | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/lib/kernel/KernelAuth.ts b/lib/kernel/KernelAuth.ts index 8e4b4123..7024e2b1 100644 --- a/lib/kernel/KernelAuth.ts +++ b/lib/kernel/KernelAuth.ts @@ -640,8 +640,9 @@ export function buildKernelConnectionOptions(options: ConnectionOptions): Kernel } if (authType === 'static-token') { - const { staticToken, enableTokenFederation, federationClientId } = options as { + const { staticToken, token, enableTokenFederation, federationClientId } = options as { staticToken?: string; + token?: string; enableTokenFederation?: boolean; federationClientId?: string; }; @@ -650,6 +651,12 @@ export function buildKernelConnectionOptions(options: ConnectionOptions): Kernel "kernel backend: a non-empty token must be supplied via `staticToken` when using `authType: 'static-token'`.", ); } + if (token !== undefined || oauth.oauthClientId !== undefined || oauth.oauthClientSecret !== undefined) { + throw new HiveDriverError( + 'kernel backend: cannot supply `staticToken` alongside `token` or ' + + '`oauthClientId`/`oauthClientSecret` on the same connection. Pick one auth mode.', + ); + } return { ...base, authMode: 'Pat', diff --git a/tests/unit/kernel/auth-static-token.test.ts b/tests/unit/kernel/auth-static-token.test.ts index 708fb69b..21e65f87 100644 --- a/tests/unit/kernel/auth-static-token.test.ts +++ b/tests/unit/kernel/auth-static-token.test.ts @@ -16,6 +16,7 @@ import { expect } from 'chai'; import expectNativeConnectionOptions from './_helpers/nativeOptions'; import { buildKernelConnectionOptions } from '../../../lib/kernel/KernelAuth'; import AuthenticationError from '../../../lib/errors/AuthenticationError'; +import HiveDriverError from '../../../lib/errors/HiveDriverError'; describe('KernelAuth — static-token auth options builder', () => { it('maps a static token to the native bearer-token mode', () => { @@ -88,4 +89,22 @@ describe('KernelAuth — static-token auth options builder', () => { ).to.throw(AuthenticationError, /non-empty token.*`staticToken`/); } }); + + it('rejects conflicting token and OAuth credentials', () => { + for (const conflicting of [ + { token: 'dapi-pat' }, + { oauthClientId: 'oauth-client' }, + { oauthClientSecret: 'oauth-secret' }, + ]) { + expect(() => + buildKernelConnectionOptions({ + host: 'example.cloud.databricks.com', + path: '/sql/1.0/warehouses/abc', + authType: 'static-token', + staticToken: 'header.payload.signature', + ...conflicting, + } as any), + ).to.throw(HiveDriverError, /cannot supply `staticToken` alongside/); + } + }); }); From 376cc0e5eab47edd44be6380b677c79c77b9ff79 Mon Sep 17 00:00:00 2001 From: Vu Anh Phung Date: Wed, 19 Aug 2026 21:42:14 +0000 Subject: [PATCH 4/8] test(kernel): narrow static token conflict coverage --- tests/unit/kernel/auth-static-token.test.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/unit/kernel/auth-static-token.test.ts b/tests/unit/kernel/auth-static-token.test.ts index 21e65f87..733c9369 100644 --- a/tests/unit/kernel/auth-static-token.test.ts +++ b/tests/unit/kernel/auth-static-token.test.ts @@ -90,12 +90,8 @@ describe('KernelAuth — static-token auth options builder', () => { } }); - it('rejects conflicting token and OAuth credentials', () => { - for (const conflicting of [ - { token: 'dapi-pat' }, - { oauthClientId: 'oauth-client' }, - { oauthClientSecret: 'oauth-secret' }, - ]) { + it('rejects conflicting OAuth credentials', () => { + for (const conflicting of [{ oauthClientId: 'oauth-client' }, { oauthClientSecret: 'oauth-secret' }]) { expect(() => buildKernelConnectionOptions({ host: 'example.cloud.databricks.com', From b23baa673ea04ed1b6a255e7cac2cb3eff0d3079 Mon Sep 17 00:00:00 2001 From: Vu Anh Phung Date: Wed, 19 Aug 2026 21:43:16 +0000 Subject: [PATCH 5/8] fix(kernel): narrow static token ambiguity guard --- lib/kernel/KernelAuth.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/kernel/KernelAuth.ts b/lib/kernel/KernelAuth.ts index 7024e2b1..c45c658d 100644 --- a/lib/kernel/KernelAuth.ts +++ b/lib/kernel/KernelAuth.ts @@ -640,9 +640,8 @@ export function buildKernelConnectionOptions(options: ConnectionOptions): Kernel } if (authType === 'static-token') { - const { staticToken, token, enableTokenFederation, federationClientId } = options as { + const { staticToken, enableTokenFederation, federationClientId } = options as { staticToken?: string; - token?: string; enableTokenFederation?: boolean; federationClientId?: string; }; @@ -651,10 +650,10 @@ export function buildKernelConnectionOptions(options: ConnectionOptions): Kernel "kernel backend: a non-empty token must be supplied via `staticToken` when using `authType: 'static-token'`.", ); } - if (token !== undefined || oauth.oauthClientId !== undefined || oauth.oauthClientSecret !== undefined) { + if (oauth.oauthClientId !== undefined || oauth.oauthClientSecret !== undefined) { throw new HiveDriverError( - 'kernel backend: cannot supply `staticToken` alongside `token` or ' + - '`oauthClientId`/`oauthClientSecret` on the same connection. Pick one auth mode.', + 'kernel backend: cannot supply `staticToken` alongside `oauthClientId`/`oauthClientSecret` ' + + 'on the same connection. Pick one auth mode.', ); } return { From 1f66bf1d987a5de8ef47789eda9d096662fea5a8 Mon Sep 17 00:00:00 2001 From: Vu Anh Phung Date: Wed, 19 Aug 2026 22:14:19 +0000 Subject: [PATCH 6/8] fix(kernel): preserve account-wide federation intent --- CONNECTION_PARAMETERS.md | 2 +- lib/kernel/KernelAuth.ts | 10 ++++------ tests/unit/kernel/auth-static-token.test.ts | 22 +++++++++++---------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/CONNECTION_PARAMETERS.md b/CONNECTION_PARAMETERS.md index dd2f6011..757101ba 100644 --- a/CONNECTION_PARAMETERS.md +++ b/CONNECTION_PARAMETERS.md @@ -64,7 +64,7 @@ column. | `oauthClientId` + no secret | `string` | ✅ (U2M) | ✅ (U2M) | — | **Parity.** The kernel keys flow selection off `oauthClientSecret` presence exactly like Thrift, so `oauthClientId` + no secret routes to **U2M** (with the id forwarded) — it does **not** throw an M2M "secret required" error. | | `azureTenantId` / `useDatabricksOAuthInAzure` | `string` / `boolean` | ✅ | ❌ | — | **Thrift-only.** Kernel rejects Azure-direct (Entra) OAuth; workspace-OIDC discovery covers Azure workspaces without it. | | `persistence` (custom OAuth token store) | `OAuthPersistence` | ✅ | ❌ | — | **Thrift-only.** Kernel throws; it auto-persists U2M tokens to `~/.config/databricks-sql-kernel/oauth/` and does not cache M2M. | -| `enableTokenFederation` / `federationClientId` | `boolean` / `string` | ✅ | ⚠️ | `false` / — | On the kernel backend these options are honored only for `static-token`; `federationClientId` is forwarded only when `enableTokenFederation` is `true`. The Thrift backend also supports them for `token-provider` and `external-token`. | +| `enableTokenFederation` / `federationClientId` | `boolean` / `string` | ✅ | ⚠️ | `false` / — | On the kernel backend these options are honored only for `static-token`. When enabled, an omitted or empty client ID selects account-wide WIF; a non-empty ID selects SP-wide WIF. The Thrift backend also supports them for `token-provider` and `external-token`. | ## HTTP client, proxy, retries diff --git a/lib/kernel/KernelAuth.ts b/lib/kernel/KernelAuth.ts index c45c658d..70a0c2b1 100644 --- a/lib/kernel/KernelAuth.ts +++ b/lib/kernel/KernelAuth.ts @@ -656,12 +656,10 @@ export function buildKernelConnectionOptions(options: ConnectionOptions): Kernel 'on the same connection. Pick one auth mode.', ); } - return { - ...base, - authMode: 'Pat', - token: staticToken, - ...(enableTokenFederation && federationClientId ? { identityFederationClientId: federationClientId } : {}), - }; + if (enableTokenFederation) { + base.identityFederationClientId = federationClientId || undefined; + } + return { ...base, authMode: 'Pat', token: staticToken }; } if (authType === 'databricks-oauth') { diff --git a/tests/unit/kernel/auth-static-token.test.ts b/tests/unit/kernel/auth-static-token.test.ts index 733c9369..1c889ca8 100644 --- a/tests/unit/kernel/auth-static-token.test.ts +++ b/tests/unit/kernel/auth-static-token.test.ts @@ -64,17 +64,19 @@ describe('KernelAuth — static-token auth options builder', () => { } }); - it('omits an empty federationClientId', () => { - const native = buildKernelConnectionOptions({ - host: 'example.cloud.databricks.com', - path: '/sql/1.0/warehouses/abc', - authType: 'static-token', - staticToken: 'header.payload.signature', - enableTokenFederation: true, - federationClientId: '', - }); + it('selects account-wide federation when enabled without a client id', () => { + for (const federationClientId of [undefined, '']) { + const native = buildKernelConnectionOptions({ + host: 'example.cloud.databricks.com', + path: '/sql/1.0/warehouses/abc', + authType: 'static-token', + staticToken: 'header.payload.signature', + enableTokenFederation: true, + federationClientId, + }); - expect(native).not.to.have.property('identityFederationClientId'); + expect(native).to.have.property('identityFederationClientId', undefined); + } }); it('rejects a missing or blank static token', () => { From 7e5c2684708517d68bf800138c4085def93952fe Mon Sep 17 00:00:00 2001 From: Vu Anh Phung Date: Thu, 20 Aug 2026 00:25:08 +0000 Subject: [PATCH 7/8] fix(kernel): document mandatory static token federation --- CONNECTION_PARAMETERS.md | 2 +- lib/contracts/IDBSQLClient.ts | 2 + lib/kernel/KernelAuth.ts | 19 ++++----- tests/unit/kernel/auth-static-token.test.ts | 44 ++++++++------------- 4 files changed, 28 insertions(+), 39 deletions(-) diff --git a/CONNECTION_PARAMETERS.md b/CONNECTION_PARAMETERS.md index 757101ba..6c135dd4 100644 --- a/CONNECTION_PARAMETERS.md +++ b/CONNECTION_PARAMETERS.md @@ -64,7 +64,7 @@ column. | `oauthClientId` + no secret | `string` | ✅ (U2M) | ✅ (U2M) | — | **Parity.** The kernel keys flow selection off `oauthClientSecret` presence exactly like Thrift, so `oauthClientId` + no secret routes to **U2M** (with the id forwarded) — it does **not** throw an M2M "secret required" error. | | `azureTenantId` / `useDatabricksOAuthInAzure` | `string` / `boolean` | ✅ | ❌ | — | **Thrift-only.** Kernel rejects Azure-direct (Entra) OAuth; workspace-OIDC discovery covers Azure workspaces without it. | | `persistence` (custom OAuth token store) | `OAuthPersistence` | ✅ | ❌ | — | **Thrift-only.** Kernel throws; it auto-persists U2M tokens to `~/.config/databricks-sql-kernel/oauth/` and does not cache M2M. | -| `enableTokenFederation` / `federationClientId` | `boolean` / `string` | ✅ | ⚠️ | `false` / — | On the kernel backend these options are honored only for `static-token`. When enabled, an omitted or empty client ID selects account-wide WIF; a non-empty ID selects SP-wide WIF. The Thrift backend also supports them for `token-provider` and `external-token`. | +| `enableTokenFederation` / `federationClientId` | `boolean` / `string` | ✅ | ⚠️ | `false` / — | On the kernel backend these options apply only to `static-token`. Federation is always enabled, so `enableTokenFederation` is ignored; an omitted or empty client ID selects account-wide WIF and a non-empty ID selects SP-wide WIF. Thrift honors the boolean and also supports these options for `token-provider` and `external-token`. | ## HTTP client, proxy, retries diff --git a/lib/contracts/IDBSQLClient.ts b/lib/contracts/IDBSQLClient.ts index bbaa4c69..761ebafd 100644 --- a/lib/contracts/IDBSQLClient.ts +++ b/lib/contracts/IDBSQLClient.ts @@ -46,7 +46,9 @@ type AuthOptions = | { authType: 'static-token'; staticToken: string; + /** Ignored by the kernel backend, where token federation is always enabled. */ enableTokenFederation?: boolean; + /** Selects SP-wide federation; omitted selects account-wide federation. */ federationClientId?: string; }; diff --git a/lib/kernel/KernelAuth.ts b/lib/kernel/KernelAuth.ts index 70a0c2b1..45cde7de 100644 --- a/lib/kernel/KernelAuth.ts +++ b/lib/kernel/KernelAuth.ts @@ -61,9 +61,9 @@ const DEFAULT_OAUTH_CLIENT_ID = 'databricks-sql-connector'; * everything else (client_id, scopes, callback timeout, * token_url_override) uses kernel defaults. * - * `static-token` reuses the native PAT bearer-token mode. When its - * `enableTokenFederation` option is true, a non-empty `federationClientId` - * is forwarded under the native name `identityFederationClientId`. + * `static-token` reuses the native PAT bearer-token mode, where federation is + * always enabled. `enableTokenFederation` is ignored; a non-empty + * `federationClientId` selects SP-wide WIF and omission selects account-wide. * * The `authMode` string literals MUST match the napi-emitted `AuthMode` * variant names verbatim (`'Pat'`, `'OAuthM2m'`, `'OAuthU2m'` — napi-rs's @@ -457,9 +457,9 @@ export function buildKernelHttpOptions(options: ConnectionOptions): KernelHttpOp * PAT throughout the existing driver — see * `DBSQLClient.createAuthProvider`). * - Static token: `authType: 'static-token'` + `staticToken`. The token is - * forwarded through the native PAT bearer-token mode. Optional SP-wide - * token federation is enabled by `enableTokenFederation` and selected by - * `federationClientId`. + * forwarded through the native PAT bearer-token mode, where federation is + * always enabled. `federationClientId` selects SP-wide WIF; omission + * selects account-wide WIF. `enableTokenFederation` is ignored. * - OAuth M2M: `authType: 'databricks-oauth'` + `oauthClientId` + * `oauthClientSecret`. Kernel handles OIDC discovery, client_credentials * exchange, and re-auth on expiry internally. @@ -640,9 +640,8 @@ export function buildKernelConnectionOptions(options: ConnectionOptions): Kernel } if (authType === 'static-token') { - const { staticToken, enableTokenFederation, federationClientId } = options as { + const { staticToken, federationClientId } = options as { staticToken?: string; - enableTokenFederation?: boolean; federationClientId?: string; }; if (typeof staticToken !== 'string' || isBlankOrReserved(staticToken)) { @@ -656,9 +655,7 @@ export function buildKernelConnectionOptions(options: ConnectionOptions): Kernel 'on the same connection. Pick one auth mode.', ); } - if (enableTokenFederation) { - base.identityFederationClientId = federationClientId || undefined; - } + base.identityFederationClientId = federationClientId || undefined; return { ...base, authMode: 'Pat', token: staticToken }; } diff --git a/tests/unit/kernel/auth-static-token.test.ts b/tests/unit/kernel/auth-static-token.test.ts index 1c889ca8..256dc24a 100644 --- a/tests/unit/kernel/auth-static-token.test.ts +++ b/tests/unit/kernel/auth-static-token.test.ts @@ -33,24 +33,12 @@ describe('KernelAuth — static-token auth options builder', () => { intervalsAsString: true, authMode: 'Pat', token: 'header.payload.signature', + identityFederationClientId: undefined, }); }); - it('forwards federationClientId when token federation is enabled', () => { - const native = buildKernelConnectionOptions({ - host: 'example.cloud.databricks.com', - path: '/sql/1.0/warehouses/abc', - authType: 'static-token', - staticToken: 'header.payload.signature', - enableTokenFederation: true, - federationClientId: 'federation-client', - }); - - expect(native.identityFederationClientId).to.equal('federation-client'); - }); - - it('does not forward federationClientId when token federation is disabled', () => { - for (const enableTokenFederation of [undefined, false]) { + it('forwards federationClientId regardless of enableTokenFederation', () => { + for (const enableTokenFederation of [undefined, false, true]) { const native = buildKernelConnectionOptions({ host: 'example.cloud.databricks.com', path: '/sql/1.0/warehouses/abc', @@ -60,22 +48,24 @@ describe('KernelAuth — static-token auth options builder', () => { federationClientId: 'federation-client', }); - expect(native).not.to.have.property('identityFederationClientId'); + expect(native.identityFederationClientId).to.equal('federation-client'); } }); - it('selects account-wide federation when enabled without a client id', () => { - for (const federationClientId of [undefined, '']) { - const native = buildKernelConnectionOptions({ - host: 'example.cloud.databricks.com', - path: '/sql/1.0/warehouses/abc', - authType: 'static-token', - staticToken: 'header.payload.signature', - enableTokenFederation: true, - federationClientId, - }); + it('selects account-wide federation without a client id regardless of enableTokenFederation', () => { + for (const enableTokenFederation of [undefined, false, true]) { + for (const federationClientId of [undefined, '']) { + const native = buildKernelConnectionOptions({ + host: 'example.cloud.databricks.com', + path: '/sql/1.0/warehouses/abc', + authType: 'static-token', + staticToken: 'header.payload.signature', + enableTokenFederation, + federationClientId, + }); - expect(native).to.have.property('identityFederationClientId', undefined); + expect(native).to.have.property('identityFederationClientId', undefined); + } } }); From dd91bd9d535ba4b020053d15d6fe102a118e4770 Mon Sep 17 00:00:00 2001 From: Vu Anh Phung Date: Thu, 20 Aug 2026 23:34:10 +0000 Subject: [PATCH 8/8] chore(kernel): regenerate native bindings --- native/kernel/index.d.ts | 1785 ++++++++++---------- native/kernel/index.js | 918 +++++++--- tests/unit/kernel/native-packaging.test.ts | 4 +- 3 files changed, 1562 insertions(+), 1145 deletions(-) diff --git a/native/kernel/index.d.ts b/native/kernel/index.d.ts index f401c31e..161c59d9 100644 --- a/native/kernel/index.d.ts +++ b/native/kernel/index.d.ts @@ -1,510 +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 8030). + * 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 (8030). - */ - oauthRedirectPort?: number - /** - * OAuth scopes override (M2M / U2M). Omitted ⇒ kernel defaults - * (`["all-apis"]` for M2M; `["sql", "offline_access"]` for U2M). + * 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. */ - oauthScopes?: Array + awaitResult(): Promise /** - * SP-wide Workload Identity Federation client id used during mandatory - * token exchange. Omitted selects BYOT / account-wide WIF. + * 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`. */ - identityFederationClientId?: string + 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`]. + * 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`. */ - customCaCert?: Buffer + 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. + * 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`. */ - value?: string + 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). @@ -516,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()`. @@ -528,547 +576,526 @@ 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 8030). * - * 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 (8030). */ - 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; `["sql", "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. + * SP-wide Workload Identity Federation client id used during mandatory + * token exchange. Omitted selects BYOT / account-wide WIF. */ - fetchNextBatch(): Promise + identityFederationClientId?: 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 catalog for statements executed on this session. + * Routed through the kernel's `DefaultOpts` and onto the SEA + * `CreateSession.catalog` 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 { + catalog?: 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. + * Default schema for statements executed on this session. + * Routed through the kernel's `DefaultOpts` and onto the SEA + * `CreateSession.schema` wire field. */ - get statementId(): string | null + schema?: string /** - * 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. + * Server-bound session conf (Spark conf, `ANSI_MODE`, `TIMEZONE`, + * query-tag presets, …). Forwarded verbatim to SEA + * `session_confs`. Unknown keys are rejected server-side. */ - result(): Promise + sessionConf?: Record /** - * Server-side cancel of the in-flight statement. + * 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. * - * 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. + * 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). */ - 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 { + maxConnections?: number /** - * 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 `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). */ - get sessionId(): string + intervalsAsString?: boolean /** - * 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. + * 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). */ - executeStatement(sql: string, options?: ExecuteOptions | undefined | null): Promise + complexTypesAsJson?: 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: + * Whether to verify the server's TLS certificate. * - * - 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()`). + * 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. * - * 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. + * `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. * - * **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. + * 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`. */ - executeStatementDirect(sql: string, options?: ExecuteOptions | undefined | null): Promise + checkServerCertificate?: 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*. + * Whether to verify that the server certificate matches the host + * (hostname-vs-SNI check), **independently** of full chain validation. * - * `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. + * 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. * - * Option semantics are identical to `executeStatement`. - * Mirrors the pyo3 `Statement.canceller()` / `Statement.execute()` - * split (PR #121): obtain the canceller before the blocking drive. + * Mirrors the Python connector's `_tls_verify_hostname` knob and the + * kernel's [`TlsConfig::skip_hostname_verification`] (= `!check`). */ - executeStatementCancellable(sql: string, options?: ExecuteOptions | undefined | null): Promise + checkServerCertificateHostname?: boolean /** - * 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. + * 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`]. */ - close(): Promise + customCaCert?: Buffer /** - * 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..38b0cd03 100644 --- a/tests/unit/kernel/native-packaging.test.ts +++ b/tests/unit/kernel/native-packaging.test.ts @@ -29,7 +29,9 @@ describe('kernel native binding — packaging (native/kernel/index.js)', () => { 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]); + 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);