From 38b4fcee3adab6cc218f41cbfaa33a217c29d54a Mon Sep 17 00:00:00 2001 From: eric-wang-1990 Date: Fri, 21 Aug 2026 01:03:43 -0700 Subject: [PATCH 01/14] feat(kernel): thread Azure Entra auth (U2M + SP M2M) through the kernel path On useKernel=true + authType='databricks-oauth', route Azure Entra auth to the kernel instead of rejecting it. Mirrors the Thrift OAuthManager.getManager: useDatabricksOAuthInAzure selects the flavour on an Azure host. - useDatabricksOAuthInAzure: true -> in-house workspace-federated flow, which the kernel runs natively: no secret -> OAuthU2m (browser), secret -> OAuthM2m (workspace-OIDC client-credentials). Works against Azure workspaces. - absent/false on an Azure host -> Entra-direct: with a secret -> the kernel's Azure SP M2M (AzureSpM2m; Entra SP creds ride oauthClientId/oauthClientSecret, azureTenantId optional/auto-discovered); without a secret -> Entra-direct browser U2M, which the kernel does not implement -> rejected with a pointer to useDatabricksOAuthInAzure: true or the Thrift backend. - Non-Azure host: the flags are inert (in-house is the only flow). Adds an AzureSpM2m variant to the native-options union + an isAzureHost helper. Replaces the previous blanket Azure-OAuth rejection. Verified end-to-end against a live Azure workspace (U2M browser flow + SP M2M data token) with a native module built from databricks-sql-kernel#280. Co-authored-by: Isaac Signed-off-by: eric-wang-1990 --- CHANGELOG.md | 4 ++ lib/kernel/KernelAuth.ts | 98 ++++++++++++++++++++++++++---- tests/unit/kernel/auth-m2m.test.ts | 63 +++++++++++++++---- tests/unit/kernel/auth-u2m.test.ts | 25 +++++--- 4 files changed, 160 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 036c71a4..db2b6a8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Release History +## Unreleased + +- Kernel backend (`useKernel: true`): **Azure Entra (Azure AD) auth is now threaded through the kernel path.** On `authType: 'databricks-oauth'`, `useDatabricksOAuthInAzure` selects the flavour on an Azure host (mirroring the Thrift `OAuthManager.getManager`): `true` → the in-house workspace-federated flow, which the kernel runs natively (browser U2M → `OAuthU2m`, client-credentials M2M → `OAuthM2m`, both via workspace-OIDC discovery, which works against Azure workspaces); absent/`false` on an Azure host → the Entra-direct flow — with a secret it maps to the kernel's Azure service-principal M2M (`AzureSpM2m`, the Entra SP creds ride `oauthClientId`/`oauthClientSecret`, `azureTenantId` optional and auto-discovered when omitted), and without a secret (Entra-direct browser U2M, which the kernel does not implement) it is rejected with a pointer to `useDatabricksOAuthInAzure: true` or the Thrift backend. On a non-Azure host these flags are inert. Requires a `databricks-sql-kernel` native module that exposes the Azure SP surface ([databricks-sql-kernel#280](https://github.com/databricks/databricks-sql-kernel/pull/280)). (PECOBLR-4141 / PECOBLR-4120) + ## 2.0.0 **Breaking changes — completes the security cleanup that 1.17.0 could not do without breaking changes.** diff --git a/lib/kernel/KernelAuth.ts b/lib/kernel/KernelAuth.ts index 45cde7de..dea5fad3 100644 --- a/lib/kernel/KernelAuth.ts +++ b/lib/kernel/KernelAuth.ts @@ -252,6 +252,14 @@ export type KernelNativeConnectionOptions = KernelSessionDefaults & oauthScopes?: Array; oauthClientId?: string; } + | { + hostName: string; + httpPath: string; + authMode: 'AzureSpM2m'; + azureClientId: string; + azureClientSecret: string; + azureTenantId?: string; + } ); function prependSlash(str: string): string { @@ -261,6 +269,28 @@ function prependSlash(str: string): string { return str; } +/** + * Azure Databricks host suffixes — the superset the Thrift driver's + * `OAuthManager.getManager` recognises (`.azuredatabricks.net`, + * `.databricks.azure.us`, `.databricks.azure.cn`). Used to decide whether an + * OAuth connection is on Azure and therefore subject to the in-house-vs- + * Entra-direct split. + */ +const AZURE_HOST_SUFFIXES = ['.azuredatabricks.net', '.databricks.azure.us', '.databricks.azure.cn']; + +/** + * True when `host` is an Azure Databricks workspace host. Normalises the input + * the same way `getManager` does (lowercase, strip scheme + any path) so a + * caller passing a bare host or a full URL is treated identically. + */ +function isAzureHost(host: string): boolean { + const normalized = host + .toLowerCase() + .replace(/^https?:\/\//, '') + .split('/')[0]; + return AZURE_HOST_SUFFIXES.some((suffix) => normalized.endsWith(suffix)); +} + /** * Reject inputs that pass `typeof === 'string' && length > 0` but are * structurally useless as credentials: whitespace-only strings, and the @@ -481,11 +511,24 @@ export function buildKernelHttpOptions(options: ConnectionOptions): KernelHttpOp * binding makes them, happen below the TypeScript layer and are not * observable from this repo. * + * Azure (Entra) on the OAuth path — mirrors Thrift `OAuthManager.getManager`, + * with `useDatabricksOAuthInAzure` selecting the flavour on an Azure host: + * - `useDatabricksOAuthInAzure: true` → **in-house** (workspace-federated). + * The kernel runs it natively via workspace-OIDC discovery — U2M browser + * flow (`OAuthU2m`) and M2M client-credentials (`OAuthM2m`) — so it is NOT + * rejected. `azureTenantId` is ignored here (the in-house flow does not use + * it), matching Thrift. + * - absent/`false` on an Azure host → **Entra-direct**: + * - with a secret → Azure service-principal M2M (`AzureSpM2m`); the Entra + * SP creds ride `oauthClientId`/`oauthClientSecret`, `azureTenantId` + * optional (kernel auto-discovers). + * - without a secret → Entra-direct browser U2M, which the kernel does + * not implement → **rejected** with a pointer to + * `useDatabricksOAuthInAzure: true` or the Thrift backend. + * - On a non-Azure host these flags are inert (the in-house flow is the only + * one), matching Thrift. + * * Out of scope on the OAuth paths (rejected with a clear error): - * - `azureTenantId` / `useDatabricksOAuthInAzure` → Microsoft Entra - * direct flow. The kernel uses workspace-OIDC discovery (which works - * against Azure workspaces too — they serve `/oidc/.well-known/...`) - * and does not implement the Entra-direct scope-rewrite path. * - `persistence` on M2M → M2M tokens are not cached (re-issuing is * cheap; no refresh token). * - `persistence` on U2M → custom token store is a parity gap; @@ -499,7 +542,7 @@ export function buildKernelHttpOptions(options: ConnectionOptions): KernelHttpOp * * Throws: * - `AuthenticationError` for missing/blank required credentials. - * - `HiveDriverError` for unsupported auth modes / Azure-direct / + * - `HiveDriverError` for unsupported auth modes / Entra-direct U2M / * custom persistence / ambiguous combinations. */ /** @@ -667,12 +710,45 @@ export function buildKernelConnectionOptions(options: ConnectionOptions): Kernel ); } - if (oauth.azureTenantId !== undefined || oauth.useDatabricksOAuthInAzure === true) { - throw new HiveDriverError( - 'kernel backend: Azure-direct OAuth (azureTenantId / useDatabricksOAuthInAzure) ' + - 'is not supported. The workspace-OIDC discovery path handles Azure workspaces ' + - 'today without these options.', - ); + // Azure routing. `useDatabricksOAuthInAzure` selects the in-house + // (workspace-federated) flow vs the Entra-direct flow, mirroring the Thrift + // driver's `OAuthManager.getManager`: on an Azure host, `true` → in-house, + // absent/false → Entra-direct. The kernel runs the in-house flow natively + // via workspace-OIDC discovery (U2M browser flow AND M2M client-credentials + // — Azure workspaces serve `/oidc/.well-known/...`), but has NO Entra-direct + // browser U2M; Entra-direct SP M2M maps to the kernel's dedicated + // azure-sp-m2m. On a non-Azure host these flags are inert (the in-house flow + // is the only one), matching Thrift. + const entraDirect = isAzureHost(options.host) && oauth.useDatabricksOAuthInAzure !== true; + if (entraDirect) { + if (oauth.oauthClientSecret === undefined) { + // Entra-direct browser U2M — the kernel has no direct-Entra U2M flow. + throw new HiveDriverError( + 'kernel backend: Azure AD (Entra-direct) OAuth U2M is not supported. Set ' + + '`useDatabricksOAuthInAzure: true` to use the in-house workspace-federated browser ' + + 'flow (which the kernel runs against Azure Databricks workspaces), or use the Thrift ' + + 'backend (default) for the Entra-direct flow.', + ); + } + // Entra-direct service-principal M2M → the kernel's azure-sp-m2m. The Entra + // SP credentials ride the generic `oauthClientId` / `oauthClientSecret` + // (Thrift convention); forward them as `azureClientId` / `azureClientSecret`. + // `azureTenantId` is optional — the kernel auto-discovers it from the + // workspace `/aad/auth` redirect when omitted. + const azureClientId = oauth.oauthClientId; + if (azureClientId === undefined) { + throw new HiveDriverError( + 'kernel backend: Azure service-principal M2M requires `oauthClientId` (the Entra ' + + 'app-registration client id) alongside `oauthClientSecret`.', + ); + } + const azure = { + ...base, + authMode: 'AzureSpM2m' as const, + azureClientId, + azureClientSecret: oauth.oauthClientSecret, + }; + return oauth.azureTenantId !== undefined ? { ...azure, azureTenantId: oauth.azureTenantId } : azure; } // Flow selector + client-id resolution mirror the Thrift driver EXACTLY diff --git a/tests/unit/kernel/auth-m2m.test.ts b/tests/unit/kernel/auth-m2m.test.ts index 7b55bcb2..7b8e1503 100644 --- a/tests/unit/kernel/auth-m2m.test.ts +++ b/tests/unit/kernel/auth-m2m.test.ts @@ -127,23 +127,56 @@ describe('KernelAuth + KernelBackend — OAuth M2M auth flow', () => { expect((native as { oauthClientId?: string }).oauthClientId).to.equal('client-uuid'); }); - it('rejects azureTenantId with a clear Entra-direct-out-of-scope error', () => { + it('routes Azure host + secret (Entra-direct default) to azure-sp-m2m, forwarding the tenant', () => { + // On an Azure host with no `useDatabricksOAuthInAzure`, the default is the + // Entra-direct flow — for M2M that is the kernel's azure-sp-m2m. The Entra + // SP creds ride oauthClientId/oauthClientSecret; azureTenantId is forwarded + // (optional — the kernel auto-discovers it when omitted). const opts: ConnectionOptions = { host: 'adb-12345.0.azuredatabricks.net', path: '/sql/1.0/warehouses/abc', authType: 'databricks-oauth', - oauthClientId: 'client-uuid', - oauthClientSecret: 'dose-fake-secret', + oauthClientId: 'entra-app-id', + oauthClientSecret: 'entra-secret', azureTenantId: 'tenant-uuid', }; - expect(() => buildKernelConnectionOptions(opts)).to.throw( - HiveDriverError, - /Azure-direct OAuth.*is not supported/, - ); + const native = buildKernelConnectionOptions(opts); + expectNativeConnectionOptions(native, { + hostName: 'adb-12345.0.azuredatabricks.net', + httpPath: '/sql/1.0/warehouses/abc', + intervalsAsString: true, + authMode: 'AzureSpM2m', + azureClientId: 'entra-app-id', + azureClientSecret: 'entra-secret', + azureTenantId: 'tenant-uuid', + }); + }); + + it('routes Azure host + secret without a tenant to azure-sp-m2m (kernel auto-discovers)', () => { + const opts: ConnectionOptions = { + host: 'adb-12345.0.azuredatabricks.net', + path: '/sql/1.0/warehouses/abc', + authType: 'databricks-oauth', + oauthClientId: 'entra-app-id', + oauthClientSecret: 'entra-secret', + }; + + const native = buildKernelConnectionOptions(opts); + expectNativeConnectionOptions(native, { + hostName: 'adb-12345.0.azuredatabricks.net', + httpPath: '/sql/1.0/warehouses/abc', + intervalsAsString: true, + authMode: 'AzureSpM2m', + azureClientId: 'entra-app-id', + azureClientSecret: 'entra-secret', + }); }); - it('rejects useDatabricksOAuthInAzure with the same Entra-direct error', () => { + it('routes Azure host + useDatabricksOAuthInAzure:true + secret to in-house OAuthM2m', () => { + // `useDatabricksOAuthInAzure: true` opts into the in-house + // (workspace-federated) flow — for M2M that is the kernel's generic + // workspace-OIDC client-credentials (OAuthM2m), which works on Azure hosts. const opts: ConnectionOptions = { host: 'adb-12345.0.azuredatabricks.net', path: '/sql/1.0/warehouses/abc', @@ -153,10 +186,16 @@ describe('KernelAuth + KernelBackend — OAuth M2M auth flow', () => { useDatabricksOAuthInAzure: true, }; - expect(() => buildKernelConnectionOptions(opts)).to.throw( - HiveDriverError, - /Azure-direct OAuth.*is not supported/, - ); + const native = buildKernelConnectionOptions(opts); + expectNativeConnectionOptions(native, { + hostName: 'adb-12345.0.azuredatabricks.net', + httpPath: '/sql/1.0/warehouses/abc', + intervalsAsString: true, + authMode: 'OAuthM2m', + oauthClientId: 'client-uuid', + oauthClientSecret: 'dose-fake-secret', + oauthScopes: ['all-apis'], + }); }); it('rejects a `persistence` hook on M2M (no cache needed)', () => { diff --git a/tests/unit/kernel/auth-u2m.test.ts b/tests/unit/kernel/auth-u2m.test.ts index c21493d5..3545b81f 100644 --- a/tests/unit/kernel/auth-u2m.test.ts +++ b/tests/unit/kernel/auth-u2m.test.ts @@ -100,7 +100,10 @@ describe('KernelAuth + KernelBackend — OAuth U2M auth flow', () => { expect(native.httpPath).to.equal('/sql/1.0/warehouses/abc'); }); - it('rejects azureTenantId on the U2M path with the Entra-direct error', () => { + it('rejects Entra-direct U2M (Azure host, no secret, no useDatabricksOAuthInAzure)', () => { + // On an Azure host the default is the Entra-direct flow; the kernel has no + // direct-Entra browser U2M, so this is rejected with a pointer to the + // in-house flag (or Thrift). azureTenantId does not change that. const opts: ConnectionOptions = { host: 'adb-12345.0.azuredatabricks.net', path: '/sql/1.0/warehouses/abc', @@ -110,11 +113,14 @@ describe('KernelAuth + KernelBackend — OAuth U2M auth flow', () => { expect(() => buildKernelConnectionOptions(opts)).to.throw( HiveDriverError, - /Azure-direct OAuth.*is not supported/, + /Entra-direct\) OAuth U2M is not supported/, ); }); - it('rejects useDatabricksOAuthInAzure on the U2M path', () => { + it('routes Azure host + useDatabricksOAuthInAzure:true (no secret) to in-house OAuthU2m', () => { + // `useDatabricksOAuthInAzure: true` opts into the in-house + // (workspace-federated) browser flow, which the kernel runs against Azure + // Databricks workspaces — so this is the U2M happy path, not a rejection. const opts: ConnectionOptions = { host: 'adb-12345.0.azuredatabricks.net', path: '/sql/1.0/warehouses/abc', @@ -122,10 +128,15 @@ describe('KernelAuth + KernelBackend — OAuth U2M auth flow', () => { useDatabricksOAuthInAzure: true, }; - expect(() => buildKernelConnectionOptions(opts)).to.throw( - HiveDriverError, - /Azure-direct OAuth.*is not supported/, - ); + const native = buildKernelConnectionOptions(opts); + expectNativeConnectionOptions(native, { + hostName: 'adb-12345.0.azuredatabricks.net', + httpPath: '/sql/1.0/warehouses/abc', + intervalsAsString: true, + authMode: 'OAuthU2m', + oauthRedirectPort: 8030, + oauthScopes: ['sql', 'offline_access'], + }); }); it('rejects a `persistence` hook on U2M citing the AuthConfig::External kernel-plumbing gap', () => { From acbccbf392ae6a3ed39810872a065a1ad51fba9d Mon Sep 17 00:00:00 2001 From: eric-wang-1990 Date: Fri, 21 Aug 2026 01:08:22 -0700 Subject: [PATCH 02/14] ci(kernel): bump KERNEL_REV to the build carrying the Azure SP surface Point the kernel-e2e KERNEL_REV at the kernel commit that adds the napi AzureSpM2m surface (databricks-sql-kernel#280, stacked on #263), so the connector's kernel-e2e builds a kernel that can accept azure-sp-m2m. This is a temporary pin to the unmerged #280 branch tip; re-point to a kernel main SHA once #263 + #280 merge and a kernel release is cut. Co-authored-by: Isaac Signed-off-by: eric-wang-1990 --- KERNEL_REV | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/KERNEL_REV b/KERNEL_REV index 95cfce81..47ce9f1f 100644 --- a/KERNEL_REV +++ b/KERNEL_REV @@ -1 +1 @@ -eff8950428f4e6cc9975c663ec919f334962f7d0 +1495174dac3aae39f23c34040fe639c54a3c3f35 From a748e809d98fec24c6fcc9a4b18f2ec15edd5293 Mon Sep 17 00:00:00 2001 From: eric-wang-1990 Date: Fri, 21 Aug 2026 10:26:23 -0700 Subject: [PATCH 03/14] fix(kernel): route Azure U2M to the in-house flow instead of rejecting it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prior routing rejected Azure U2M when useDatabricksOAuthInAzure was absent/false (the 'Entra-direct' default), on the assumption the kernel needed an Entra-direct browser U2M flow it lacks. That was wrong: the kernel runs a single, cloud-blind in-house U2M flow — it uses the workspace's OIDC-discovered authorize endpoint ({host}/oidc/v1/authorize) verbatim — and that workspace-federated flow works against Azure workspaces (the workspace federates the browser login to Entra server-side; verified E2E). So all U2M now falls through to OAuthU2m with the in-house app (databricks-sql-connector) + sql/offline_access, regardless of cloud or useDatabricksOAuthInAzure — consistent with the Go connector's uniform U2M mapping. Only Entra-direct M2M (Azure host + secret + not useDatabricksOAuthInAzure) still maps to the kernel's azure-sp-m2m. Updated the test that asserted the rejection to assert OAuthU2m, and the docstring/CHANGELOG. Co-authored-by: Isaac Signed-off-by: eric-wang-1990 --- CHANGELOG.md | 2 +- lib/kernel/KernelAuth.ts | 84 +++++++++++++++--------------- tests/unit/kernel/auth-u2m.test.ts | 24 ++++++--- 3 files changed, 60 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db2b6a8b..dbd2754d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## Unreleased -- Kernel backend (`useKernel: true`): **Azure Entra (Azure AD) auth is now threaded through the kernel path.** On `authType: 'databricks-oauth'`, `useDatabricksOAuthInAzure` selects the flavour on an Azure host (mirroring the Thrift `OAuthManager.getManager`): `true` → the in-house workspace-federated flow, which the kernel runs natively (browser U2M → `OAuthU2m`, client-credentials M2M → `OAuthM2m`, both via workspace-OIDC discovery, which works against Azure workspaces); absent/`false` on an Azure host → the Entra-direct flow — with a secret it maps to the kernel's Azure service-principal M2M (`AzureSpM2m`, the Entra SP creds ride `oauthClientId`/`oauthClientSecret`, `azureTenantId` optional and auto-discovered when omitted), and without a secret (Entra-direct browser U2M, which the kernel does not implement) it is rejected with a pointer to `useDatabricksOAuthInAzure: true` or the Thrift backend. On a non-Azure host these flags are inert. Requires a `databricks-sql-kernel` native module that exposes the Azure SP surface ([databricks-sql-kernel#280](https://github.com/databricks/databricks-sql-kernel/pull/280)). (PECOBLR-4141 / PECOBLR-4120) +- Kernel backend (`useKernel: true`): **Azure Entra (Azure AD) auth is now threaded through the kernel path.** On `authType: 'databricks-oauth'`: **U2M** (no secret) always routes to `OAuthU2m` — the kernel runs one cloud-blind in-house workspace-federated browser flow (it uses the workspace's OIDC-discovered authorize endpoint verbatim), which works against Azure workspaces, so Azure U2M forwards the in-house app (`databricks-sql-connector`) + `sql offline_access` scopes exactly like AWS/GCP, regardless of `useDatabricksOAuthInAzure` (verified E2E against a live Azure workspace). **M2M** (secret): `useDatabricksOAuthInAzure: true` (or non-Azure) → `OAuthM2m` (workspace-OIDC client-credentials); an Azure host with `useDatabricksOAuthInAzure` absent/`false` → the Entra-direct Azure service-principal M2M (`AzureSpM2m`, the Entra SP creds ride `oauthClientId`/`oauthClientSecret`, `azureTenantId` optional and auto-discovered when omitted). On a non-Azure host `useDatabricksOAuthInAzure` is inert. The `AzureSpM2m` path requires a `databricks-sql-kernel` native module that exposes the Azure SP surface ([databricks-sql-kernel#280](https://github.com/databricks/databricks-sql-kernel/pull/280)); U2M works on any kernel build. (PECOBLR-4141 / PECOBLR-4120) ## 2.0.0 diff --git a/lib/kernel/KernelAuth.ts b/lib/kernel/KernelAuth.ts index dea5fad3..56211266 100644 --- a/lib/kernel/KernelAuth.ts +++ b/lib/kernel/KernelAuth.ts @@ -511,22 +511,23 @@ export function buildKernelHttpOptions(options: ConnectionOptions): KernelHttpOp * binding makes them, happen below the TypeScript layer and are not * observable from this repo. * - * Azure (Entra) on the OAuth path — mirrors Thrift `OAuthManager.getManager`, - * with `useDatabricksOAuthInAzure` selecting the flavour on an Azure host: - * - `useDatabricksOAuthInAzure: true` → **in-house** (workspace-federated). - * The kernel runs it natively via workspace-OIDC discovery — U2M browser - * flow (`OAuthU2m`) and M2M client-credentials (`OAuthM2m`) — so it is NOT - * rejected. `azureTenantId` is ignored here (the in-house flow does not use - * it), matching Thrift. - * - absent/`false` on an Azure host → **Entra-direct**: - * - with a secret → Azure service-principal M2M (`AzureSpM2m`); the Entra - * SP creds ride `oauthClientId`/`oauthClientSecret`, `azureTenantId` - * optional (kernel auto-discovers). - * - without a secret → Entra-direct browser U2M, which the kernel does - * not implement → **rejected** with a pointer to - * `useDatabricksOAuthInAzure: true` or the Thrift backend. - * - On a non-Azure host these flags are inert (the in-house flow is the only - * one), matching Thrift. + * Azure (Entra) on the OAuth path. The kernel runs a single, cloud-blind + * in-house U2M flow and workspace-OIDC M2M; only Entra-direct **M2M** gets a + * dedicated kernel mode: + * - **U2M (no secret), any cloud, any `useDatabricksOAuthInAzure`** → + * `OAuthU2m`. The kernel uses the workspace's OIDC-discovered authorize + * endpoint (`{host}/oidc/v1/authorize`) verbatim; that in-house + * workspace-federated flow works against Azure workspaces too (they federate + * the browser login to Entra server-side — verified E2E). So Azure U2M is + * NOT special-cased and NOT rejected — it forwards the in-house app + * (`databricks-sql-connector`) + `sql offline_access`, exactly like AWS/GCP. + * - **M2M (secret) with `useDatabricksOAuthInAzure: true`** (or non-Azure) → + * `OAuthM2m` (workspace-OIDC client-credentials). + * - **M2M (secret) on an Azure host with `useDatabricksOAuthInAzure` absent/ + * `false`** (Entra-direct) → Azure service-principal M2M (`AzureSpM2m`); the + * Entra SP creds ride `oauthClientId`/`oauthClientSecret`, `azureTenantId` + * optional (kernel auto-discovers). + * - On a non-Azure host `useDatabricksOAuthInAzure` is inert. * * Out of scope on the OAuth paths (rejected with a clear error): * - `persistence` on M2M → M2M tokens are not cached (re-issuing is @@ -710,31 +711,32 @@ export function buildKernelConnectionOptions(options: ConnectionOptions): Kernel ); } - // Azure routing. `useDatabricksOAuthInAzure` selects the in-house - // (workspace-federated) flow vs the Entra-direct flow, mirroring the Thrift - // driver's `OAuthManager.getManager`: on an Azure host, `true` → in-house, - // absent/false → Entra-direct. The kernel runs the in-house flow natively - // via workspace-OIDC discovery (U2M browser flow AND M2M client-credentials - // — Azure workspaces serve `/oidc/.well-known/...`), but has NO Entra-direct - // browser U2M; Entra-direct SP M2M maps to the kernel's dedicated - // azure-sp-m2m. On a non-Azure host these flags are inert (the in-house flow - // is the only one), matching Thrift. - const entraDirect = isAzureHost(options.host) && oauth.useDatabricksOAuthInAzure !== true; - if (entraDirect) { - if (oauth.oauthClientSecret === undefined) { - // Entra-direct browser U2M — the kernel has no direct-Entra U2M flow. - throw new HiveDriverError( - 'kernel backend: Azure AD (Entra-direct) OAuth U2M is not supported. Set ' + - '`useDatabricksOAuthInAzure: true` to use the in-house workspace-federated browser ' + - 'flow (which the kernel runs against Azure Databricks workspaces), or use the Thrift ' + - 'backend (default) for the Entra-direct flow.', - ); - } - // Entra-direct service-principal M2M → the kernel's azure-sp-m2m. The Entra - // SP credentials ride the generic `oauthClientId` / `oauthClientSecret` - // (Thrift convention); forward them as `azureClientId` / `azureClientSecret`. - // `azureTenantId` is optional — the kernel auto-discovers it from the - // workspace `/aad/auth` redirect when omitted. + // Azure Entra-direct **M2M** → the kernel's dedicated azure-sp-m2m. Mirroring + // the Thrift driver's `OAuthManager.getManager`, an Azure host with + // `useDatabricksOAuthInAzure` NOT set to true (the Entra-direct default) plus a + // secret is an Entra service-principal client-credentials flow: the Entra SP + // credentials ride the generic `oauthClientId` / `oauthClientSecret` (Thrift + // convention); forward them as `azureClientId` / `azureClientSecret`. + // `azureTenantId` is optional — the kernel auto-discovers it from the workspace + // `/aad/auth` redirect when omitted. + // + // Azure **U2M** is deliberately NOT special-cased and NOT rejected. The kernel + // runs a single, cloud-blind in-house U2M flow: it uses the workspace's + // OIDC-discovered authorize endpoint (`{host}/oidc/v1/authorize`) verbatim, and + // that in-house workspace-federated flow works against Azure workspaces (the + // workspace federates the browser login to Entra server-side; verified E2E). So + // ALL U2M — including Azure, with or without `useDatabricksOAuthInAzure` — falls + // through to the standard `OAuthU2m` path below, which forwards the in-house app + // (`databricks-sql-connector`) + `sql offline_access` scopes, exactly like + // AWS/GCP. Handing the kernel the Thrift Azure Entra-direct app / scope instead + // would derail its in-house flow to a broken AAD authorize URL. + // The `oauthClientSecret !== undefined` check is inline (not extracted to a + // const) so TypeScript narrows it to `string` for the AzureSpM2m literal below. + if ( + isAzureHost(options.host) && + oauth.useDatabricksOAuthInAzure !== true && + oauth.oauthClientSecret !== undefined + ) { const azureClientId = oauth.oauthClientId; if (azureClientId === undefined) { throw new HiveDriverError( diff --git a/tests/unit/kernel/auth-u2m.test.ts b/tests/unit/kernel/auth-u2m.test.ts index 3545b81f..1943e099 100644 --- a/tests/unit/kernel/auth-u2m.test.ts +++ b/tests/unit/kernel/auth-u2m.test.ts @@ -100,10 +100,13 @@ describe('KernelAuth + KernelBackend — OAuth U2M auth flow', () => { expect(native.httpPath).to.equal('/sql/1.0/warehouses/abc'); }); - it('rejects Entra-direct U2M (Azure host, no secret, no useDatabricksOAuthInAzure)', () => { - // On an Azure host the default is the Entra-direct flow; the kernel has no - // direct-Entra browser U2M, so this is rejected with a pointer to the - // in-house flag (or Thrift). azureTenantId does not change that. + it('routes Azure U2M (no secret, no useDatabricksOAuthInAzure) to in-house OAuthU2m', () => { + // Azure U2M is NOT rejected and NOT special-cased: the kernel runs a single + // cloud-blind in-house workspace-federated U2M flow (it uses the workspace's + // OIDC-discovered authorize endpoint verbatim), which works against Azure + // workspaces. So it routes to OAuthU2m with the in-house app + + // sql/offline_access regardless of useDatabricksOAuthInAzure. azureTenantId + // is inert on the kernel U2M path. const opts: ConnectionOptions = { host: 'adb-12345.0.azuredatabricks.net', path: '/sql/1.0/warehouses/abc', @@ -111,10 +114,15 @@ describe('KernelAuth + KernelBackend — OAuth U2M auth flow', () => { azureTenantId: 'tenant-uuid', }; - expect(() => buildKernelConnectionOptions(opts)).to.throw( - HiveDriverError, - /Entra-direct\) OAuth U2M is not supported/, - ); + const native = buildKernelConnectionOptions(opts); + expectNativeConnectionOptions(native, { + hostName: 'adb-12345.0.azuredatabricks.net', + httpPath: '/sql/1.0/warehouses/abc', + intervalsAsString: true, + authMode: 'OAuthU2m', + oauthRedirectPort: 8030, + oauthScopes: ['sql', 'offline_access'], + }); }); it('routes Azure host + useDatabricksOAuthInAzure:true (no secret) to in-house OAuthU2m', () => { From 2003130879a284afdf601d8024a41b854e65453f Mon Sep 17 00:00:00 2001 From: "peco-engineer-bot[bot]" Date: Fri, 21 Aug 2026 22:18:38 +0000 Subject: [PATCH 04/14] ai: apply changes for #505 (4 review threads) Addresses: - #3828516211 at lib/kernel/KernelAuth.ts:747 - #3828516217 at lib/kernel/KernelAuth.ts:741 - #3828530379 at lib/kernel/KernelAuth.ts:292 - #3832331772 at lib/kernel/KernelAuth.ts:546 Signed-off-by: peco-engineer-bot[bot] --- lib/kernel/KernelAuth.ts | 21 +++++++++++++++++---- tests/unit/kernel/auth-m2m.test.ts | 20 ++++++++++++++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/lib/kernel/KernelAuth.ts b/lib/kernel/KernelAuth.ts index 56211266..d80f8056 100644 --- a/lib/kernel/KernelAuth.ts +++ b/lib/kernel/KernelAuth.ts @@ -280,14 +280,17 @@ const AZURE_HOST_SUFFIXES = ['.azuredatabricks.net', '.databricks.azure.us', '.d /** * True when `host` is an Azure Databricks workspace host. Normalises the input - * the same way `getManager` does (lowercase, strip scheme + any path) so a - * caller passing a bare host or a full URL is treated identically. + * the same way `getManager` does (trim surrounding whitespace, lowercase, strip + * scheme, then drop any path and explicit `:port`) so a caller passing a bare + * host, a padded string, or a full URL with a port is treated identically. */ function isAzureHost(host: string): boolean { const normalized = host + .trim() .toLowerCase() .replace(/^https?:\/\//, '') - .split('/')[0]; + .split('/')[0] + .split(':')[0]; return AZURE_HOST_SUFFIXES.some((suffix) => normalized.endsWith(suffix)); } @@ -543,7 +546,7 @@ export function buildKernelHttpOptions(options: ConnectionOptions): KernelHttpOp * * Throws: * - `AuthenticationError` for missing/blank required credentials. - * - `HiveDriverError` for unsupported auth modes / Entra-direct U2M / + * - `HiveDriverError` for unsupported auth modes / * custom persistence / ambiguous combinations. */ /** @@ -737,6 +740,16 @@ export function buildKernelConnectionOptions(options: ConnectionOptions): Kernel oauth.useDatabricksOAuthInAzure !== true && oauth.oauthClientSecret !== undefined ) { + // Entra-direct SP M2M is a client-credentials flow (no refresh token), so + // `persistence` is rejected here for parity with the workspace-OIDC M2M and + // U2M arms below (and matching the contract docblock's "persistence on M2M + // → rejected" note). Otherwise a caller's hook would be silently dropped. + if (oauth.persistence !== undefined) { + throw new HiveDriverError( + 'kernel backend: `persistence` is not supported on Azure service-principal M2M ' + + '(M2M tokens have no refresh token; the kernel re-issues on expiry).', + ); + } const azureClientId = oauth.oauthClientId; if (azureClientId === undefined) { throw new HiveDriverError( diff --git a/tests/unit/kernel/auth-m2m.test.ts b/tests/unit/kernel/auth-m2m.test.ts index 7b8e1503..82ce1c28 100644 --- a/tests/unit/kernel/auth-m2m.test.ts +++ b/tests/unit/kernel/auth-m2m.test.ts @@ -173,6 +173,26 @@ describe('KernelAuth + KernelBackend — OAuth M2M auth flow', () => { }); }); + it('rejects Azure host + secret without an oauthClientId (Entra app has no default)', () => { + // Divergence from the regular M2M path (which defaults a missing + // oauthClientId to the built-in client): the Entra-direct azure-sp-m2m + // branch requires an explicit `oauthClientId` (the Entra app-registration + // client id has no sensible default) and rejects rather than silently + // defaulting on the Azure path. + const opts: ConnectionOptions = { + host: 'adb-12345.0.azuredatabricks.net', + path: '/sql/1.0/warehouses/abc', + authType: 'databricks-oauth', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + oauthClientSecret: 'entra-secret', + } as any; + + expect(() => buildKernelConnectionOptions(opts)).to.throw( + HiveDriverError, + /Azure service-principal M2M requires `oauthClientId`/, + ); + }); + it('routes Azure host + useDatabricksOAuthInAzure:true + secret to in-house OAuthM2m', () => { // `useDatabricksOAuthInAzure: true` opts into the in-house // (workspace-federated) flow — for M2M that is the kernel's generic From cf1605c3d7904b31bd64dd8d4689e53b55ea40d1 Mon Sep 17 00:00:00 2001 From: eric-wang-1990 Date: Fri, 21 Aug 2026 15:24:44 -0700 Subject: [PATCH 05/14] ci(kernel): repoint KERNEL_REV to merged main (Azure SP surface now on main) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit kernel #282 (napi AuthMode::AzureSpM2m + C-ABI set_auth_azure_sp) merged to databricks-sql-kernel main at 5e5dea9. Repoint KERNEL_REV from the old #280 feature-branch tip (1495174 — now orphaned; #280 was mis-merged into the already-merged #263 branch) to the merged-main SHA, so kernel-e2e builds the native module from a main that actually carries the Azure SP surface. Co-authored-by: Isaac Signed-off-by: eric-wang-1990 --- KERNEL_REV | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/KERNEL_REV b/KERNEL_REV index 47ce9f1f..beffc14b 100644 --- a/KERNEL_REV +++ b/KERNEL_REV @@ -1 +1 @@ -1495174dac3aae39f23c34040fe639c54a3c3f35 +5e5dea91ebc17df49d63665e1f933bacb3072c65 From aa0a0b422ad63f7633d9aaa8b5b451ea26f8667a Mon Sep 17 00:00:00 2001 From: "peco-engineer-bot[bot]" Date: Fri, 21 Aug 2026 22:30:25 +0000 Subject: [PATCH 06/14] ai: apply changes for #505 (1 review thread) Addresses: - #3834110807 at lib/kernel/KernelAuth.ts:760 Signed-off-by: peco-engineer-bot[bot] --- lib/kernel/KernelAuth.ts | 21 +++++++++++++---- tests/unit/kernel/auth-m2m.test.ts | 38 ++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/lib/kernel/KernelAuth.ts b/lib/kernel/KernelAuth.ts index d80f8056..fd505fa5 100644 --- a/lib/kernel/KernelAuth.ts +++ b/lib/kernel/KernelAuth.ts @@ -733,8 +733,8 @@ export function buildKernelConnectionOptions(options: ConnectionOptions): Kernel // (`databricks-sql-connector`) + `sql offline_access` scopes, exactly like // AWS/GCP. Handing the kernel the Thrift Azure Entra-direct app / scope instead // would derail its in-house flow to a broken AAD authorize URL. - // The `oauthClientSecret !== undefined` check is inline (not extracted to a - // const) so TypeScript narrows it to `string` for the AzureSpM2m literal below. + // The `oauthClientSecret !== undefined` check is inline so TypeScript narrows + // the field to `string` inside the branch (for the AzureSpM2m literal below). if ( isAzureHost(options.host) && oauth.useDatabricksOAuthInAzure !== true && @@ -750,8 +750,21 @@ export function buildKernelConnectionOptions(options: ConnectionOptions): Kernel '(M2M tokens have no refresh token; the kernel re-issues on expiry).', ); } + // Reject a present-but-degenerate secret (`''`, whitespace, or the reserved + // `'undefined'`/`'null'` shell-export strings) up front. Unlike the generic + // `OAuthM2m` arm below — which forwards such values verbatim for byte-for-byte + // Thrift parity — this Azure arm has no parity contract (it already rejects a + // missing id outright), so a blank credential is as unusable as a missing one + // and would only surface an opaque Entra `invalid_client` downstream. + const azureClientSecret = oauth.oauthClientSecret; + if (isBlankOrReserved(azureClientSecret)) { + throw new HiveDriverError( + 'kernel backend: Azure service-principal M2M requires a non-blank `oauthClientSecret` ' + + '(the Entra app-registration client secret).', + ); + } const azureClientId = oauth.oauthClientId; - if (azureClientId === undefined) { + if (typeof azureClientId !== 'string' || isBlankOrReserved(azureClientId)) { throw new HiveDriverError( 'kernel backend: Azure service-principal M2M requires `oauthClientId` (the Entra ' + 'app-registration client id) alongside `oauthClientSecret`.', @@ -761,7 +774,7 @@ export function buildKernelConnectionOptions(options: ConnectionOptions): Kernel ...base, authMode: 'AzureSpM2m' as const, azureClientId, - azureClientSecret: oauth.oauthClientSecret, + azureClientSecret, }; return oauth.azureTenantId !== undefined ? { ...azure, azureTenantId: oauth.azureTenantId } : azure; } diff --git a/tests/unit/kernel/auth-m2m.test.ts b/tests/unit/kernel/auth-m2m.test.ts index 82ce1c28..726cf510 100644 --- a/tests/unit/kernel/auth-m2m.test.ts +++ b/tests/unit/kernel/auth-m2m.test.ts @@ -193,6 +193,44 @@ describe('KernelAuth + KernelBackend — OAuth M2M auth flow', () => { ); }); + it('rejects Azure host + secret with a blank/reserved oauthClientId (as unusable as missing)', () => { + // Unlike the generic M2M arm (which forwards `''` verbatim for Thrift + // parity), the Entra-direct azure-sp-m2m branch has no parity contract and + // rejects a missing id outright — so a blank/reserved id is treated the same + // rather than reaching the kernel as an opaque `invalid_client`. + for (const badId of ['', ' ', 'undefined', 'NULL']) { + const opts: ConnectionOptions = { + host: 'adb-12345.0.azuredatabricks.net', + path: '/sql/1.0/warehouses/abc', + authType: 'databricks-oauth', + oauthClientId: badId, + oauthClientSecret: 'entra-secret', + }; + + expect(() => buildKernelConnectionOptions(opts), `for oauthClientId=${JSON.stringify(badId)}`).to.throw( + HiveDriverError, + /Azure service-principal M2M requires `oauthClientId`/, + ); + } + }); + + it('rejects Azure host + a blank/reserved oauthClientSecret (as unusable as missing)', () => { + for (const badSecret of [' ', 'undefined', 'NULL']) { + const opts: ConnectionOptions = { + host: 'adb-12345.0.azuredatabricks.net', + path: '/sql/1.0/warehouses/abc', + authType: 'databricks-oauth', + oauthClientId: 'entra-app-id', + oauthClientSecret: badSecret, + }; + + expect(() => buildKernelConnectionOptions(opts), `for oauthClientSecret=${JSON.stringify(badSecret)}`).to.throw( + HiveDriverError, + /Azure service-principal M2M requires a non-blank `oauthClientSecret`/, + ); + } + }); + it('routes Azure host + useDatabricksOAuthInAzure:true + secret to in-house OAuthM2m', () => { // `useDatabricksOAuthInAzure: true` opts into the in-house // (workspace-federated) flow — for M2M that is the kernel's generic From 16634e420753e8c76f45db230456cf6e19bf46a8 Mon Sep 17 00:00:00 2001 From: "peco-engineer-bot[bot]" Date: Fri, 21 Aug 2026 22:37:36 +0000 Subject: [PATCH 07/14] ai: apply changes for #505 (1 review thread) Addresses: - #3834140852 at lib/kernel/KernelAuth.ts:778 Signed-off-by: peco-engineer-bot[bot] --- lib/kernel/KernelAuth.ts | 11 ++++++++++- tests/unit/kernel/auth-m2m.test.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/lib/kernel/KernelAuth.ts b/lib/kernel/KernelAuth.ts index fd505fa5..a5338173 100644 --- a/lib/kernel/KernelAuth.ts +++ b/lib/kernel/KernelAuth.ts @@ -776,7 +776,16 @@ export function buildKernelConnectionOptions(options: ConnectionOptions): Kernel azureClientId, azureClientSecret, }; - return oauth.azureTenantId !== undefined ? { ...azure, azureTenantId: oauth.azureTenantId } : azure; + // Forward `azureTenantId` only when it's a real value. A blank/reserved + // string (`''`, whitespace, `'undefined'`/`'null'` shell-export artifacts) + // is treated as omitted so the kernel auto-discovers the tenant from the + // workspace `/aad/auth` redirect, rather than being handed a degenerate + // tenant that suppresses discovery and yields a malformed AAD URL. Matches + // this arm's `oauthClientId`/`oauthClientSecret` hygiene above and the + // Thrift `AzureOAuthManager` empty-tenant fallback. + return oauth.azureTenantId !== undefined && !isBlankOrReserved(oauth.azureTenantId) + ? { ...azure, azureTenantId: oauth.azureTenantId } + : azure; } // Flow selector + client-id resolution mirror the Thrift driver EXACTLY diff --git a/tests/unit/kernel/auth-m2m.test.ts b/tests/unit/kernel/auth-m2m.test.ts index 726cf510..99b67b99 100644 --- a/tests/unit/kernel/auth-m2m.test.ts +++ b/tests/unit/kernel/auth-m2m.test.ts @@ -173,6 +173,34 @@ describe('KernelAuth + KernelBackend — OAuth M2M auth flow', () => { }); }); + it('treats a blank/reserved azureTenantId as omitted (kernel auto-discovers)', () => { + // A tenant that resolves to `''`, whitespace, or the `'undefined'`/`'null'` + // shell-export artifacts is degenerate: forwarding it verbatim would + // suppress the kernel's `/aad/auth` auto-discovery and yield a malformed + // AAD URL. It must fall back to auto-discovery, matching this arm's + // oauthClientId/oauthClientSecret hygiene and the Thrift empty-tenant guard. + for (const badTenant of ['', ' ', 'undefined', 'NULL']) { + const opts: ConnectionOptions = { + host: 'adb-12345.0.azuredatabricks.net', + path: '/sql/1.0/warehouses/abc', + authType: 'databricks-oauth', + oauthClientId: 'entra-app-id', + oauthClientSecret: 'entra-secret', + azureTenantId: badTenant, + }; + + const native = buildKernelConnectionOptions(opts); + expectNativeConnectionOptions(native, { + hostName: 'adb-12345.0.azuredatabricks.net', + httpPath: '/sql/1.0/warehouses/abc', + intervalsAsString: true, + authMode: 'AzureSpM2m', + azureClientId: 'entra-app-id', + azureClientSecret: 'entra-secret', + }); + } + }); + it('rejects Azure host + secret without an oauthClientId (Entra app has no default)', () => { // Divergence from the regular M2M path (which defaults a missing // oauthClientId to the built-in client): the Entra-direct azure-sp-m2m From c01742098446b46b048c5f551d7a8afda6faf567 Mon Sep 17 00:00:00 2001 From: "peco-engineer-bot[bot]" Date: Fri, 21 Aug 2026 22:41:57 +0000 Subject: [PATCH 08/14] ai: apply changes for #505 (1 review thread) Addresses: - #3834161802 at CHANGELOG.md:5 Signed-off-by: peco-engineer-bot[bot] --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbd2754d..ad9da98d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## Unreleased -- Kernel backend (`useKernel: true`): **Azure Entra (Azure AD) auth is now threaded through the kernel path.** On `authType: 'databricks-oauth'`: **U2M** (no secret) always routes to `OAuthU2m` — the kernel runs one cloud-blind in-house workspace-federated browser flow (it uses the workspace's OIDC-discovered authorize endpoint verbatim), which works against Azure workspaces, so Azure U2M forwards the in-house app (`databricks-sql-connector`) + `sql offline_access` scopes exactly like AWS/GCP, regardless of `useDatabricksOAuthInAzure` (verified E2E against a live Azure workspace). **M2M** (secret): `useDatabricksOAuthInAzure: true` (or non-Azure) → `OAuthM2m` (workspace-OIDC client-credentials); an Azure host with `useDatabricksOAuthInAzure` absent/`false` → the Entra-direct Azure service-principal M2M (`AzureSpM2m`, the Entra SP creds ride `oauthClientId`/`oauthClientSecret`, `azureTenantId` optional and auto-discovered when omitted). On a non-Azure host `useDatabricksOAuthInAzure` is inert. The `AzureSpM2m` path requires a `databricks-sql-kernel` native module that exposes the Azure SP surface ([databricks-sql-kernel#280](https://github.com/databricks/databricks-sql-kernel/pull/280)); U2M works on any kernel build. (PECOBLR-4141 / PECOBLR-4120) +- Kernel backend (`useKernel: true`): **Azure Entra (Azure AD) auth is now threaded through the kernel path.** On `authType: 'databricks-oauth'`: **U2M** (no secret) always routes to `OAuthU2m` — the kernel runs one cloud-blind in-house workspace-federated browser flow (it uses the workspace's OIDC-discovered authorize endpoint verbatim), which works against Azure workspaces, so Azure U2M forwards the in-house app (`databricks-sql-connector`) + `sql offline_access` scopes exactly like AWS/GCP, regardless of `useDatabricksOAuthInAzure` (verified E2E against a live Azure workspace). **M2M** (secret): `useDatabricksOAuthInAzure: true` (or non-Azure) → `OAuthM2m` (workspace-OIDC client-credentials); an Azure host with `useDatabricksOAuthInAzure` absent/`false` → the Entra-direct Azure service-principal M2M (`AzureSpM2m`, the Entra SP creds ride `oauthClientId`/`oauthClientSecret`, `azureTenantId` optional and auto-discovered when omitted). On a non-Azure host `useDatabricksOAuthInAzure` is inert. The `AzureSpM2m` path requires a `databricks-sql-kernel` native module that exposes the Azure SP surface — landed on `main` via [databricks-sql-kernel#282](https://github.com/databricks/databricks-sql-kernel/pull/282) (which the pinned `KERNEL_REV` `5e5dea9` carries; the surface was originally proposed in [#280](https://github.com/databricks/databricks-sql-kernel/pull/280), which never reached `main`); U2M works on any kernel build. (PECOBLR-4141 / PECOBLR-4120) ## 2.0.0 From 0a8efcf072456bd83f08a33d756c06b0ce2cf65c Mon Sep 17 00:00:00 2001 From: "peco-engineer-bot[bot]" Date: Fri, 21 Aug 2026 22:52:48 +0000 Subject: [PATCH 09/14] ai: apply changes for #505 (1 review thread) Addresses: - #3834195415 at lib/kernel/KernelAuth.ts:740 Signed-off-by: peco-engineer-bot[bot] --- lib/kernel/KernelAuth.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/kernel/KernelAuth.ts b/lib/kernel/KernelAuth.ts index a5338173..ed9dd4b3 100644 --- a/lib/kernel/KernelAuth.ts +++ b/lib/kernel/KernelAuth.ts @@ -714,8 +714,8 @@ export function buildKernelConnectionOptions(options: ConnectionOptions): Kernel ); } - // Azure Entra-direct **M2M** → the kernel's dedicated azure-sp-m2m. Mirroring - // the Thrift driver's `OAuthManager.getManager`, an Azure host with + // Azure Entra-direct **M2M** → the kernel's dedicated azure-sp-m2m. Closely + // mirroring the Thrift driver's `OAuthManager.getManager`, an Azure host with // `useDatabricksOAuthInAzure` NOT set to true (the Entra-direct default) plus a // secret is an Entra service-principal client-credentials flow: the Entra SP // credentials ride the generic `oauthClientId` / `oauthClientSecret` (Thrift @@ -733,6 +733,13 @@ export function buildKernelConnectionOptions(options: ConnectionOptions): Kernel // (`databricks-sql-connector`) + `sql offline_access` scopes, exactly like // AWS/GCP. Handing the kernel the Thrift Azure Entra-direct app / scope instead // would derail its in-house flow to a broken AAD authorize URL. + // + // One deliberate divergence from Thrift: `isAzureHost` uses the full suffix + // superset (incl. `.databricks.azure.us`) for every branch, whereas Thrift's + // `useDatabricksOAuthInAzure`-true arm omits `.databricks.azure.us` and so + // throws `OAuth is not supported` for a US-gov host in that mode. Here such a + // host falls through to the in-house flow (accepted) instead — intentional, + // since the kernel's in-house flow is cloud-blind and reachable everywhere. // The `oauthClientSecret !== undefined` check is inline so TypeScript narrows // the field to `string` inside the branch (for the AzureSpM2m literal below). if ( From d74ffaa08a7a5ad44ad67cc7b2429f3ec3093afc Mon Sep 17 00:00:00 2001 From: eric-wang-1990 Date: Fri, 21 Aug 2026 15:56:31 -0700 Subject: [PATCH 10/14] chore(kernel): refresh generated native contract for KERNEL_REV 5e5dea9 Regenerate the committed napi artifacts (native/kernel/index.d.ts + index.js) to match the bumped KERNEL_REV (kernel main 5e5dea9, which includes the Azure SP surface via #282). index.d.ts now exposes the AuthMode.AzureSpM2m arm and the azureClientId/azureClientSecret/azureTenantId/azureWorkspaceResourceId ConnectionOptions fields. Equivalent to `npm run build:native`'s `cp index.*` step (the kernel's committed napi/index.* at 5e5dea9 are the generator output); run here because @napi-rs/cli isn't reachable from the CI npm registry. Addresses the review thread flagging the stale generated contract. Co-authored-by: Isaac Signed-off-by: eric-wang-1990 --- native/kernel/index.d.ts | 1619 ++++++++++++++++++++------------------ native/kernel/index.js | 918 +++++++-------------- 2 files changed, 1139 insertions(+), 1398 deletions(-) diff --git a/native/kernel/index.d.ts b/native/kernel/index.d.ts index 161c59d9..bc16c0d2 100644 --- a/native/kernel/index.d.ts +++ b/native/kernel/index.d.ts @@ -1,681 +1,362 @@ -/* auto-generated by NAPI-RS */ +/* tslint:disable */ /* eslint-disable */ + +/* auto-generated by NAPI-RS */ + /** - * 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`). + * Per-statement options for `Connection.executeStatement`. * - * `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. + * 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 is cached at construction so it survives the underlying - * stream being drained; mirrors the sync `Statement.schema()` - * post-close contract. + * `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 AsyncResultHandle { - /** - * Server-issued statement id. Cached at construction; readable - * for log correlation. Matches the parent `AsyncStatement`'s - * `statementId`. - */ - get statementId(): string +export interface ExecuteOptions { /** - * 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. + * 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. */ - fetchNextBatch(): Promise + statementConf?: Record /** - * 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`. + * 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. */ - schema(): ArrowSchema -} - -/** - * Opaque async-statement handle. - * - * 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()`. - * - * 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 declare class AsyncStatement { + queryTags?: Record /** - * 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. + * 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. */ - get statementId(): string + rowLimit?: number /** - * 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. + * 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`]. */ - status(): Promise - /** Rows modified by the statement (UPDATE / INSERT / DELETE / MERGE). */ - numModifiedRows(): Promise + positionalParams?: Array /** - * Server-supplied user-facing message (may contain SQL fragments — - * redact before centralised logging). + * 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`). */ - displayMessage(): Promise - /** Server-supplied diagnostic detail. */ - diagnosticInfo(): Promise - /** Server-supplied structured error detail (JSON), when enabled. */ - errorDetailsJson(): Promise + 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'`, `'AzureSpM2m'`). + * + * 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', /** - * 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. + * OAuth 2.0 machine-to-machine with a JWT private-key client + * assertion — `oauthClientId` + `jwtKeyFile` + `jwtKid`. */ - awaitResult(): Promise + OAuthM2mJwt = 'OAuthM2mJwt', /** - * 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 2.0 user-to-machine (browser flow) — optional `oauthClientId` + * + `oauthRedirectPort`. */ - cancel(): Promise + OAuthU2m = 'OAuthU2m', /** - * 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. + * Azure Entra service-principal M2M — `azureClientId` + + * `azureClientSecret` (+ optional `azureTenantId` / + * `azureWorkspaceResourceId`). The kernel builds the Entra token + * endpoint + `.default` scope, auto-discovers the tenant from the + * workspace when `azureTenantId` is omitted, and — when + * `azureWorkspaceResourceId` is set — additionally sends the SP + * management token for an RBAC-only service principal. */ - close(): Promise + AzureSpM2m = 'AzureSpM2m' } - /** - * Handle returned by `Connection.executeStatementCancellable`. Owns the - * built-but-not-yet-executed kernel `Statement` plus a detached - * [`StatementCanceller`] captured before dispatch, so JS can fire a - * server-side cancel while the blocking `result()` is in flight. + * A single extra HTTP header as an explicit `{ name, value }` pair. * - * `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`. + * 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 declare class CancellableExecution { - /** - * 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. - */ - get statementId(): string | null - /** - * Drive the blocking `execute()` and resolve to a `Statement` - * (identical to what `executeStatement` returns) once the kernel - * reaches a terminal state and the result stream is ready. - * - * Consumes the pending statement: a second `result()` call returns - * `KernelError(InvalidStatementHandle)`. The future is - * drop-cancel-safe — the kernel's per-execute `MidExecuteCancelState` - * guard fires a fire-and-forget `cancel_statement` if this future is - * dropped mid-flight (`Promise.race` / timeout loser), independently - * of an explicit `cancel()`. - * - * On a server-side cancel the kernel's blocking `execute()` currently - * surfaces `InvalidArgument` (a known kernel quirk — the async path - * returns `Cancelled`). When this handle's `cancel()` actually dispatched a - * server-side cancel, we normalise that into `Cancelled` here so JS callers - * can rely on a single cancelled-status code regardless of execution path. - * - * Three outcomes can race the blocking drive: (1) a natural terminal state - * → `Ok` or the genuine error; (2) an explicit `cancel()` that dispatched a - * server cancel → this `result()` rejects with a `Cancelled`-coded error - * (the normalisation above); (3) the future being **dropped** mid-flight - * (`Promise.race`/timeout loser) → the kernel's `MidExecuteCancelState` - * drop-guard fires a fire-and-forget `cancel_statement`, but there is no - * `result()` left to observe a code. Only (2) yields a `Cancelled` error. - */ - result(): Promise - /** - * Server-side cancel of the in-flight statement. - * - * Lock-free: fires the detached `StatementCanceller` captured at - * construction rather than taking the mutex `result()` holds, so it - * interrupts a still-running blocking `result()` instead of queueing - * behind it. No-op (returns `Ok`) if `result()` already finished - * successfully, or if no statement id has been observed yet (query still - * in its initial submit round-trip), and idempotent against a server - * already in a terminal state. - */ - cancel(): Promise +export interface HeaderEntry { + name: string + value: string } - /** - * Opaque connection handle wrapping a kernel `Session`. + * 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. * - * `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. + * - `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 +} +/** + * JS-visible options for opening a Databricks SQL session. * - * **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`. + * 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). + * - `AzureSpM2m` — `azureClientId` + `azureClientSecret` required + * (+ optional `azureTenantId` / `azureWorkspaceResourceId`). + * + * 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 Connection { +export interface ConnectionOptions { /** - * 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. + * Workspace host, e.g. `adb-…azuredatabricks.net`. The kernel + * normalises this — bare hostnames get `https://` prepended. */ - get sessionId(): string + hostName: string /** - * Execute a SQL statement and return a Statement handle that - * streams batches via `fetchNextBatch()`. - * - * Catalog / schema / sessionConf are session-level - * (`openSession`). Per-statement options on `ExecuteOptions`: - * - `statementConf` — per-statement Spark conf overlay - * - `queryTags` — serialised to a comma-separated `key:value` - * string and placed in `statement_conf["query_tags"]`, - * matching NodeJS Thrift's `serializeQueryTags` wire shape - * - * `options` is omitted/`None` for the no-options path; passing - * `{ statementConf: {} }` (an empty map) is treated the same as - * omission to keep the wire shape stable for the common case. + * JDBC-style HTTP path, e.g. `/sql/1.0/warehouses/abc123`. The + * kernel parses out the warehouse id. */ - executeStatement(sql: string, options?: ExecuteOptions | undefined | null): Promise + httpPath: string /** - * directResults execute — the Thrift/JDBC model. Sends ExecuteStatement - * with no `wait_timeout` field (server applies its ~10s default inline wait - * and auto-closes on success) and returns WITHOUT polling past it: - * - * - a **`Statement`** (left arm) when the query finished within the inline - * wait — terminal, result ready inline, `close()` is a clean release; - * - an **`AsyncStatement`** (right arm) when it did not — a poll/cancel - * handle the caller drives (`status()` / `awaitResult()` / `cancel()`). - * - * JS distinguishes the arms by feature-detecting `awaitResult` (present - * only on `AsyncStatement`). This is the path that gives mid-run cancel for - * long queries WITHOUT the eager-handle / close-drives workaround: the - * returned handle always corresponds to a server-owned statement. - * - * **Load-bearing contract:** the kernel's `DirectStatement::{Completed, - * Running}` discriminant cannot ride on these opaque `#[napi]` classes, so - * consumers MUST feature-detect via `awaitResult` (the only member unique to - * `AsyncStatement`). `Statement` (the Completed arm) MUST NOT gain an - * `awaitResult` member, or every consumer silently misroutes. The pyo3 - * binding makes the same `await_result`-probe assumption. + * Authentication mode. Omitted ⇒ [`AuthMode::Pat`] (back-compat: + * existing PAT callers pass only `token`). */ - executeStatementDirect(sql: string, options?: ExecuteOptions | undefined | null): Promise + authMode?: AuthMode /** - * Execute a SQL statement on the blocking (sync) path, but return a - * `CancellableExecution` handle so a concurrent JS task can cancel - * the query *while it is still running server-side*. - * - * `executeStatement` builds the kernel `Statement`, awaits the - * blocking `execute()`, and only then hands JS a `Statement` — so a - * query that runs for several seconds is uncancellable from JS on - * that path (there is no handle until the blocking call resolves). - * This method instead builds the statement, captures a detached - * `StatementCanceller` **before** dispatching `execute()`, and hands - * JS a `CancellableExecution` immediately. The caller drives the - * blocking execution via `result()` (resolves to the same - * `Statement` `executeStatement` returns) and can fire `cancel()` - * concurrently to interrupt a still-running query mid-COMPUTE. - * - * Option semantics are identical to `executeStatement`. - * Mirrors the pyo3 `Statement.canceller()` / `Statement.execute()` - * split (PR #121): obtain the canceller before the blocking drive. + * Personal access token. Required (and non-empty) for + * [`AuthMode::Pat`]; ignored otherwise. */ - executeStatementCancellable(sql: string, options?: ExecuteOptions | undefined | null): Promise + token?: string /** - * Submit a SQL statement and return immediately with an - * `AsyncStatement` handle, without blocking until the query - * finishes. The kernel's `Statement::submit()` sends - * `wait_timeout=0s`, so the server responds as soon as it has a - * `statement_id` (state `Pending`/`Running`); JS drives polling - * via `AsyncStatement.status()` and materialises results with - * `AsyncStatement.awaitResult()`. - * - * This is the async-execution path the Thrift backend always - * uses (`runAsync: true`): the SEA backend submits, returns a - * pending operation handle, and polls to terminal during - * fetch. Option semantics (statementConf / queryTags / - * rowLimit / positional + named params) match `executeStatement`. - * Submit always sends `wait_timeout=0s` so the call returns - * immediately; the caller drives completion via `status()` / - * `awaitResult()`. Only the blocking-vs-pending return contract - * differs from `executeStatement`. + * OAuth client id. Required for [`AuthMode::OAuthM2m`]; optional for + * [`AuthMode::OAuthU2m`] (defaults to `databricks-sql-connector`). */ - submitStatement(sql: string, options?: ExecuteOptions | undefined | null): Promise + oauthClientId?: string + /** OAuth client secret. Required for [`AuthMode::OAuthM2m`]. */ + oauthClientSecret?: string /** - * Explicit close. Awaits the server-side `DeleteSession` so the - * JS caller can observe failures (auth revoked mid-session, - * warehouse stopped, network error). Idempotent — a second call - * on an already-closed connection returns `Ok`. - * - * **Errors are terminal from the JS side.** The kernel session - * handle is consumed (`take()`) BEFORE the wire `DeleteSession` - * runs, because `Session::close` takes `self` by value. On `Err`, - * the napi `inner` is already `None`, so a JS-side retry sees a - * closed connection and returns `Ok(())` without re-attempting - * the wire call. The kernel's own `Drop` fire-and-forget retry - * runs once in the background — the JS caller can log the error - * but cannot drive a retry. If you need retry-on-failure - * semantics for `DeleteSession`, layer them above this method. + * Localhost callback port for the [`AuthMode::OAuthU2m`] browser + * flow. Omitted ⇒ kernel default (8030). */ - close(): Promise + oauthRedirectPort?: number /** - * All catalogs visible to the session. - * - * JDBC `getCatalogs` shape: `TABLE_CAT: Utf8`. + * OAuth scopes override (M2M / U2M). Omitted ⇒ kernel defaults + * (`["all-apis"]` for M2M; `["sql", "offline_access"]` for U2M). */ - listCatalogs(): Promise + oauthScopes?: Array /** - * Schemas filtered by catalog (exact) and schema name pattern. - * - * JDBC `getSchemas` shape: `TABLE_SCHEM, TABLE_CATALOG`. + * OAuth token endpoint override. When omitted the kernel discovers the + * endpoint via the workspace's OIDC configuration. Set it to point the + * client-credentials / JWT client-assertion grant at the workspace's + * configured IdP token endpoint (e.g. Entra ID for Azure Databricks), + * which is required when that IdP — not Databricks-native OIDC — is the + * authority for [`AuthMode::OAuthM2m`] / [`AuthMode::OAuthM2mJwt`]. */ - listSchemas(catalog?: string | undefined | null, schemaPattern?: string | undefined | null): Promise + tokenUrl?: string /** - * Tables filtered by catalog (**pattern**), schema (pattern), table - * (pattern). - * - * The catalog is an ODBC/JDBC LIKE pattern (`%` / `_`), matching - * Thrift `getTables`: a wildcard catalog matches multiple catalogs; - * a literal name (or an escaped `\_` / `\%`) takes the fast exact - * path. `undefined`/omitted catalog means "all catalogs", while an - * **empty string** means "match nothing" (zero rows) — pass - * `undefined`, not `""`, for all catalogs. The catalog pattern is - * validated (whitespace-only / NUL / >255 bytes are rejected with an - * error). - * - * JDBC `getTables` shape: 10 columns. `tableTypes`, when provided, - * filters rows by `TABLE_TYPE` kernel-side. - * - * `tableTypes` is an advisory filter. Databricks `SHOW TABLES` does - * NOT honour the table-type filter server-side; the kernel applies - * it client-side after the result returns. Callers expecting - * server-side rejection of off-type tables should not rely on this. + * Path to the PEM private-key file. Required for + * [`AuthMode::OAuthM2mJwt`]. */ - listTables(catalog?: string | undefined | null, schemaPattern?: string | undefined | null, tablePattern?: string | undefined | null, tableTypes?: Array | undefined | null): Promise + jwtKeyFile?: string + /** JWT key id (`kid`). Required for [`AuthMode::OAuthM2mJwt`]. */ + jwtKid?: string /** - * Columns of tables matching the filter. - * - * JDBC `getColumns` shape: 23 columns. + * Passphrase for an encrypted PKCS#8 key. Optional for + * [`AuthMode::OAuthM2mJwt`] (omit for an unencrypted key). */ - listColumns(catalog?: string | undefined | null, schemaPattern?: string | undefined | null, tablePattern?: string | undefined | null, columnPattern?: string | undefined | null): Promise + jwtPassphrase?: string /** - * Functions visible to the session. `catalog` is exact; - * `schemaPattern` and `functionPattern` are SQL LIKE. + * JWT signing algorithm (`RS256`/`384`/`512`, `PS256`/`384`/`512`, + * `ES256`, `ES384`). Optional for [`AuthMode::OAuthM2mJwt`] + * (omitted ⇒ kernel default `RS256`). */ - listFunctions(catalog?: string | undefined | null, schemaPattern?: string | undefined | null, functionPattern?: string | undefined | null): Promise + jwtAlgorithm?: string /** - * Procedures visible to the session. `catalog` is exact; - * `schemaPattern` and `procedurePattern` are SQL LIKE. + * SP-wide Workload Identity Federation client id used during mandatory + * token exchange. Omitted selects BYOT / account-wide WIF. */ - listProcedures(catalog?: string | undefined | null, schemaPattern?: string | undefined | null, procedurePattern?: string | undefined | null): Promise + identityFederationClientId?: string /** - * All table types (`TABLE`, `VIEW`, `SYSTEM TABLE`, …). - * No wire call — static in-memory result. + * Azure Entra app-registration client id. Required for + * [`AuthMode::AzureSpM2m`]; ignored otherwise. */ - listTableTypes(): Promise + azureClientId?: string /** - * SQL data types supported by the workspace. - * No wire call — static in-memory result. + * Azure Entra app-registration client secret. Required for + * [`AuthMode::AzureSpM2m`]. */ - listTypeInfo(): Promise + azureClientSecret?: string /** - * Primary keys for the given table. All three identifiers are - * exact — ODBC `SQLPrimaryKeys` does not support patterns. + * Azure Entra tenant id. Optional for [`AuthMode::AzureSpM2m`]; + * omitted ⇒ the kernel auto-discovers it from the workspace's + * `/aad/auth` redirect (matching the Thrift connector). */ - getPrimaryKeys(catalog: string, schema: string, table: string): Promise + azureTenantId?: string /** - * 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. + * Azure workspace ARM resource id. Optional for [`AuthMode::AzureSpM2m`]; + * when set the kernel also sends the SP management token + * (`X-Databricks-Azure-SP-Management-Token`) + the + * `X-Databricks-Azure-Workspace-Resource-Id` header, so a service + * principal with an Azure RBAC role but no workspace membership can + * authenticate. Omitted ⇒ the data token authenticates alone. */ - getCrossReference(parentCatalog: string | undefined | null, parentSchema: string | undefined | null, parentTable: string | undefined | null, foreignCatalog: string, foreignSchema: string, foreignTable: string): Promise -} - -/** - * Opaque executed-statement handle. - * - * **Current concurrency shape** — every method takes `inner.lock()` - * and holds the guard across the kernel `.await`. tokio `Mutex` is - * FIFO, so cancel/close queue behind any in-flight `fetchNextBatch` - * until it returns naturally. This is a known limitation that exists - * because the napi shape has not yet been split into an - * `Arc` (for cancel/close, which the - * kernel exposes as `&self`-callable) plus a `Mutex>` only - * for the borrowed-mut fetch path. The lock-shape refactor needs a - * small kernel-side accessor and lands in a follow-up PR — see - * `sea-workflow/jira-candidates/2026-05-24-napi-cancel-during-fetch.md`. - * - * `schema` and `statement_id` are cached at construction so they - * survive `close()` — JS callers building error reports against a - * disposed statement can still read them. - */ -export declare class Statement { - /** - * 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 - /** - * Number of rows modified by the statement (UPDATE / INSERT / - * DELETE / MERGE). `null` for SELECT and on warehouses that don't - * surface the counter. Mirrors Thrift's - * `TGetOperationStatusResp.numModifiedRows`. - */ - numModifiedRows(): Promise - /** - * Server-supplied user-facing message. Mirrors Thrift's - * `TGetOperationStatusResp.displayMessage`. **PII / sensitive- - * data note:** may contain SQL fragments or parameter values — - * redact before centralised logging. - * - * Populated on `Succeeded` / `Closed` paths (incl. an empty `Closed`). - * On terminal-error states (`Failed` / `Cancelled`) the kernel returns - * an Error instead of a `Statement`, and the same field rides on the JS - * Error envelope under the same `displayMessage` key. - */ - displayMessage(): Promise - /** - * Server-supplied diagnostic detail — multi-line operator / - * stack context. Mirrors Thrift's - * `TGetOperationStatusResp.diagnosticInfo`. For support surfaces, - * not user-facing. Same reachability + PII caveats as - * `displayMessage`. - */ - diagnosticInfo(): Promise - /** - * Server-supplied JSON blob with extended error details. Mirrors - * Thrift's `TGetOperationStatusResp.errorDetailsJson`. - * Pass-through string — JS callers parse with `JSON.parse` if - * they need structured access. - * - * **Server-side gating:** populated only when the workspace has - * `spark.databricks.sql.errorDetailsJson.enabled = true` on the - * underlying SQL cluster. The flag is internal-only / default- - * false in the Databricks runtime, so for most JS callers this - * will return `null`. Admin-enabled workspaces return content - * shaped like `{"errorClass": "...", "messageTemplate": "..."}`. - * - * **Unbounded:** when populated, server can return a multi-MB - * blob; size before logging. - */ - errorDetailsJson(): Promise - /** - * Pull the next batch of results. Returns `null` when the stream - * is exhausted. The returned `ArrowBatch.ipcBytes` is a complete - * Arrow IPC stream (schema header + 1 record-batch message) - * suitable for handing to `apache-arrow`'s `RecordBatchReader`. - * - * On `Err`, the stream is in an unspecified state — call - * `close()` and discard the `Statement`. Subsequent - * `fetchNextBatch()` calls after an error are not guaranteed to - * succeed or fail consistently. - */ - fetchNextBatch(): Promise - /** - * Result schema as an Arrow IPC payload (schema header only, no - * record-batch message). Available before any batches have been - * fetched, and remains available after `close()` — the kernel - * materialises the schema eagerly so JS callers can build error - * reports against a disposed statement. - * - * Sync because the body has no `.await` — `encode_ipc_stream` is - * pure CPU work over an `Arc` already cached on the - * wrapper. Mirrors `pyo3/src/statement.rs::arrow_schema` (sync). - * napi-rs converts a panic in a sync `#[napi]` entry point into a - * thrown JS error via its own macro-expanded boundary, so the - * `util::guarded` `catch_unwind` wrapper that the `async fn` - * entry points use is not required for this method. - */ - schema(): ArrowSchema - /** - * Server-side cancel. - * - * For executed statements: short-circuits to `Ok(())` if - * `fetchNextBatch` has already returned `null` (stream - * naturally exhausted) — matches the JDBC `Statement.cancel()` - * no-op-after-completion contract, so JS callers can fire cancel - * defensively without distinguishing "real cancel" from "raced - * with natural completion." - * - * For metadata streams: no-op (the kernel has no in-flight - * cancellation surface for metadata calls today). - * - * Returns `KernelError(InvalidStatementHandle)` if the statement - * has been explicitly `close()`d. - */ - cancel(): Promise + azureWorkspaceResourceId?: 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). + * Default catalog for statements executed on this session. + * Routed through the kernel's `DefaultOpts` and onto the SEA + * `CreateSession.catalog` wire field. */ - close(): Promise -} - -/** - * A single Arrow IPC stream payload encoding one record batch (plus - * the schema header so the JS-side reader is stateless). - */ -export interface ArrowBatch { + catalog?: string /** - * Arrow IPC stream payload (schema header + 1 record-batch - * message). Decode with `apache-arrow`'s `RecordBatchReader`. + * Default schema for statements executed on this session. + * Routed through the kernel's `DefaultOpts` and onto the SEA + * `CreateSession.schema` wire field. */ - ipcBytes: Buffer -} - -/** - * An Arrow IPC stream payload encoding just the result schema (no - * record-batch messages). Returned by `Statement.schema()`. - */ -export interface ArrowSchema { + schema?: string /** - * Arrow IPC stream payload (schema header only, no record-batch - * messages). Decode with `apache-arrow`'s `RecordBatchReader` — - * the reader will expose the schema and immediately end. + * Server-bound session conf (Spark conf, `ANSI_MODE`, `TIMEZONE`, + * query-tag presets, …). Forwarded verbatim to SEA + * `session_confs`. Unknown keys are rejected server-side. */ - ipcBytes: Buffer -} - -/** - * Authentication mode selector crossing the napi boundary. The string - * literals are what napi-rs emits from this `#[napi(string_enum)]` — the - * NodeJS SEA adapter (`KernelAuth`) matches them verbatim (`'Pat'`, - * `'OAuthM2m'`, `'OAuthU2m'`). - * - * 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 const enum AuthMode { - /** Personal access token (`token`). */ - Pat = 'Pat', - /** OAuth 2.0 machine-to-machine — `oauthClientId` + `oauthClientSecret`. */ - OAuthM2m = 'OAuthM2m', + sessionConf?: Record /** - * OAuth 2.0 user-to-machine (browser flow) — optional `oauthClientId` - * + `oauthRedirectPort`. + * Driver name reported in telemetry system configuration. Omitted ⇒ + * kernel default. */ - OAuthU2m = 'OAuthU2m' -} - -/** - * JS-visible options for opening a Databricks SQL session. - * - * Authentication is selected by `authMode` (default [`AuthMode::Pat`]): - * - `Pat` — `token` required. - * - `OAuthM2m` — `oauthClientId` + `oauthClientSecret` required. - * - `OAuthU2m` — `oauthClientId` / `oauthRedirectPort` optional - * (defaults to the `databricks-sql-connector` client on port 8030). - * - * Catalog / schema / sessionConf are applied once at session creation - * and remain in effect for every statement run on the resulting - * `Connection`. The SEA wire protocol carries them on - * `CreateSession`, not on `ExecuteStatement` — so there is no - * per-statement override path on this binding. - */ -export interface ConnectionOptions { + driverName?: string /** - * Workspace host, e.g. `adb-…azuredatabricks.net`. The kernel - * normalises this — bare hostnames get `https://` prepended. + * Driver version reported in telemetry system configuration. Omitted ⇒ + * kernel default. */ - hostName: string + driverVersion?: string /** - * JDBC-style HTTP path, e.g. `/sql/1.0/warehouses/abc123`. The - * kernel parses out the warehouse id. + * Runtime name reported in telemetry system configuration. Omitted ⇒ + * kernel default. */ - httpPath: string + runtimeName?: string /** - * Authentication mode. Omitted ⇒ [`AuthMode::Pat`] (back-compat: - * existing PAT callers pass only `token`). + * Runtime version reported in telemetry system configuration. Omitted ⇒ + * kernel default. */ - authMode?: AuthMode + runtimeVersion?: string /** - * Personal access token. Required (and non-empty) for - * [`AuthMode::Pat`]; ignored otherwise. + * Runtime vendor reported in telemetry system configuration. Omitted ⇒ + * kernel default. */ - token?: string + runtimeVendor?: string /** - * OAuth client id. Required for [`AuthMode::OAuthM2m`]; optional for - * [`AuthMode::OAuthU2m`] (defaults to `databricks-sql-connector`). + * Operating system name reported in telemetry system configuration. + * Omitted ⇒ kernel default. */ - oauthClientId?: string - /** OAuth client secret. Required for [`AuthMode::OAuthM2m`]. */ - oauthClientSecret?: string + osName?: string /** - * Localhost callback port for the [`AuthMode::OAuthU2m`] browser - * flow. Omitted ⇒ kernel default (8030). + * Operating system version reported in telemetry system configuration. + * Omitted ⇒ kernel default. */ - oauthRedirectPort?: number + osVersion?: string /** - * OAuth scopes override (M2M / U2M). Omitted ⇒ kernel defaults - * (`["all-apis"]` for M2M; `["sql", "offline_access"]` for U2M). + * Operating system architecture reported in telemetry system + * configuration. Omitted ⇒ kernel default. */ - oauthScopes?: Array + osArch?: string /** - * SP-wide Workload Identity Federation client id used during mandatory - * token exchange. Omitted selects BYOT / account-wide WIF. + * Client application name reported in telemetry system configuration. + * Omitted ⇒ absent unless the kernel can infer a default. */ - identityFederationClientId?: string + clientAppName?: string /** - * Default catalog for statements executed on this session. - * Routed through the kernel's `DefaultOpts` and onto the SEA - * `CreateSession.catalog` wire field. + * Locale name reported in telemetry system configuration. Omitted ⇒ + * kernel default. */ - catalog?: string + localeName?: string /** - * Default schema for statements executed on this session. - * Routed through the kernel's `DefaultOpts` and onto the SEA - * `CreateSession.schema` wire field. + * Character-set encoding reported in telemetry system configuration. + * Omitted ⇒ kernel default. */ - schema?: string + charSetEncoding?: string /** - * Server-bound session conf (Spark conf, `ANSI_MODE`, `TIMEZONE`, - * query-tag presets, …). Forwarded verbatim to SEA - * `session_confs`. Unknown keys are rejected server-side. + * Process name reported in telemetry system configuration. Omitted ⇒ + * kernel default. */ - sessionConf?: Record + processName?: string /** * Maximum number of pooled HTTP connections per host. Routes * through the kernel's [`HttpConfig::pool_max_idle_per_host`]. @@ -832,6 +513,33 @@ export interface ConnectionOptions { * [`HttpConfig::overall_timeout`]. */ retryOverallTimeoutSecs?: number + /** + * Enable kernel telemetry export. Omitted / `false` keeps telemetry + * disabled. When enabled, the kernel records operation telemetry below the + * binding layer and exports through the Databricks SQL driver telemetry + * endpoint. + */ + telemetryEnabled?: boolean + /** + * Number of telemetry events buffered before a flush. Omitted ⇒ kernel + * default. Must be greater than zero when supplied. + */ + telemetryBatchSize?: number + /** + * Periodic telemetry flush interval in milliseconds. Omitted ⇒ kernel + * default. Must be greater than zero when supplied. + */ + telemetryFlushIntervalMs?: number + /** Maximum telemetry export retries. `0` disables telemetry export retries. */ + telemetryMaxRetries?: number + /** Fixed telemetry retry delay in milliseconds. `0` means immediate retry. */ + telemetryRetryDelayMs?: number + /** + * Maximum time explicit close waits for telemetry final flush, in + * milliseconds. Omitted ⇒ kernel default. Must be greater than zero when + * supplied. + */ + telemetryCloseFlushTimeoutMs?: number /** * Programmatic HTTP/HTTPS proxy ([`ProxyInput`]) to route all kernel * traffic through. Carries the proxy `url`, optional basic-auth @@ -860,107 +568,28 @@ export interface ConnectionOptions { */ socketTimeoutMs?: number } - /** - * Per-statement options for `Connection.executeStatement`. + * 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`]. * - * 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. - * - * `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. + * The JS-visible name is `openSession` (napi-rs converts snake_case + * to camelCase for free functions). */ -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 - /** - * 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`]. - */ - positionalParams?: Array - /** - * 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`). - */ - namedParams?: Array -} - +export declare function openSession(options: ConnectionOptions): Promise /** - * 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 }`. + * 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 HeaderEntry { - name: string - value: string +export interface LogRecord { + level: string + target: string + message: string } - /** * Install (idempotently) the kernel→JS log bridge and set its level. * @@ -973,14 +602,7 @@ export interface HeaderEntry { * 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 - +export declare function initKernelLogging(callback: (err: Error | null, arg: Array) => any, level: string): void /** * Snapshot of the bridge's runtime state for observability. * @@ -996,68 +618,16 @@ 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"`). + * Return the bridge's [`KernelLoggingStats`]. Safe to call before + * `initKernelLogging` (reports `installed: false`, `dropped: 0`). */ -export interface ProxyInput { - url: string - username?: string - password?: string - bypassHosts?: string -} - +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. * @@ -1091,7 +661,29 @@ export interface TypedValueInput { */ value?: string } - +/** + * A single Arrow IPC stream payload encoding one record batch (plus + * the schema header so the JS-side reader is stateless). + */ +export interface ArrowBatch { + /** + * Arrow IPC stream payload (schema header + 1 record-batch + * message). Decode with `apache-arrow`'s `RecordBatchReader`. + */ + ipcBytes: Buffer +} +/** + * An Arrow IPC stream payload encoding just the result schema (no + * record-batch messages). Returned by `Statement.schema()`. + */ +export interface ArrowSchema { + /** + * Arrow IPC stream payload (schema header only, no record-batch + * messages). Decode with `apache-arrow`'s `RecordBatchReader` — + * the reader will expose the schema and immediately end. + */ + ipcBytes: Buffer +} /** * Returns the native binding's crate version (`CARGO_PKG_VERSION`). * @@ -1099,3 +691,540 @@ export interface TypedValueInput { * loaded?" probe for the JS-side loader's structured diagnostics. */ export declare function version(): string +/** + * Opaque async-statement handle. + * + * 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()`. + * + * 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 declare class AsyncStatement { + /** + * 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 + /** + * 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. + */ + status(): Promise + /** Rows modified by the statement (UPDATE / INSERT / DELETE / MERGE). */ + numModifiedRows(): Promise + /** + * Server-supplied user-facing message (may contain SQL fragments — + * redact before centralised logging). + */ + displayMessage(): Promise + /** Server-supplied diagnostic detail. */ + diagnosticInfo(): Promise + /** Server-supplied structured error detail (JSON), when enabled. */ + errorDetailsJson(): Promise + /** + * 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. + */ + awaitResult(): Promise + /** + * 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`. + */ + cancel(): Promise + /** + * Explicit close. Idempotent — a second call on an + * already-closed handle returns `Ok(())`. On `Err`, the napi + * inner is already `None`, so a JS-side retry sees the + * closed-handle short-circuit and returns `Ok(())` without + * re-attempting the wire call. The kernel's own `Drop` + * fire-and-forget retry runs once in the background. + */ + close(): Promise +} +/** + * Opaque result-fetch handle returned by + * `AsyncStatement.awaitResult()`. Wraps a kernel `ResultStream` + * directly; structurally analogous to the sync `Statement`'s + * fetch-side surface (`fetchNextBatch` / `schema` / + * `statementId`). + * + * `cancel()` / `close()` are not exposed: the parent + * `AsyncStatement` owns server-side lifecycle. A `close()` here + * would create dual-ownership of the same statement_id with + * inconsistent close semantics. Callers `close()` the parent + * `AsyncStatement` after they're done fetching. + * + * Schema is cached at construction so it survives the underlying + * stream being drained; mirrors the sync `Statement.schema()` + * post-close contract. + */ +export declare class AsyncResultHandle { + /** + * Server-issued statement id. Cached at construction; readable + * for log correlation. Matches the parent `AsyncStatement`'s + * `statementId`. + */ + get statementId(): string + /** + * Pull the next batch of results. Returns `null` when the + * stream is exhausted. The returned `ArrowBatch.ipcBytes` is a + * complete Arrow IPC stream (schema header + 1 record-batch + * message), suitable for handing to `apache-arrow`'s + * `RecordBatchReader`. Byte-identical to the sync + * `Statement.fetchNextBatch()` payload for the same query. + */ + fetchNextBatch(): Promise + /** + * Result schema as an Arrow IPC payload (schema header only, + * no record-batch message). Available before any batches have + * been fetched. Sync because the body has no `.await` — + * `encode_ipc_stream` is pure CPU work over the cached + * `Arc`. + */ + schema(): ArrowSchema +} +/** + * 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 { + /** + * 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. + */ + get statementId(): string | null + /** + * Drive the blocking `execute()` and resolve to a `Statement` + * (identical to what `executeStatement` returns) once the kernel + * reaches a terminal state and the result stream is ready. + * + * Consumes the pending statement: a second `result()` call returns + * `KernelError(InvalidStatementHandle)`. The future is + * drop-cancel-safe — the kernel's per-execute `MidExecuteCancelState` + * guard fires a fire-and-forget `cancel_statement` if this future is + * dropped mid-flight (`Promise.race` / timeout loser), independently + * of an explicit `cancel()`. + * + * On a server-side cancel the kernel's blocking `execute()` currently + * surfaces `InvalidArgument` (a known kernel quirk — the async path + * returns `Cancelled`). When this handle's `cancel()` actually dispatched a + * server-side cancel, we normalise that into `Cancelled` here so JS callers + * can rely on a single cancelled-status code regardless of execution path. + * + * Three outcomes can race the blocking drive: (1) a natural terminal state + * → `Ok` or the genuine error; (2) an explicit `cancel()` that dispatched a + * server cancel → this `result()` rejects with a `Cancelled`-coded error + * (the normalisation above); (3) the future being **dropped** mid-flight + * (`Promise.race`/timeout loser) → the kernel's `MidExecuteCancelState` + * drop-guard fires a fire-and-forget `cancel_statement`, but there is no + * `result()` left to observe a code. Only (2) yields a `Cancelled` error. + */ + result(): Promise + /** + * Server-side cancel of the in-flight statement. + * + * Lock-free: fires the detached `StatementCanceller` captured at + * construction rather than taking the mutex `result()` holds, so it + * interrupts a still-running blocking `result()` instead of queueing + * behind it. No-op (returns `Ok`) if `result()` already finished + * successfully, or if no statement id has been observed yet (query still + * in its initial submit round-trip), and idempotent against a server + * already in a terminal state. + */ + cancel(): Promise +} +/** + * 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 { + /** + * Server-issued session id. Cached at construction; readable + * even after `close()` so JS-side log lines can correlate + * against kernel / server logs which key on the same id. + */ + get sessionId(): string + /** + * Execute a SQL statement and return a Statement handle that + * streams batches via `fetchNextBatch()`. + * + * Catalog / schema / sessionConf are session-level + * (`openSession`). Per-statement options on `ExecuteOptions`: + * - `statementConf` — per-statement Spark conf overlay + * - `queryTags` — serialised to a comma-separated `key:value` + * string and placed in `statement_conf["query_tags"]`, + * matching NodeJS Thrift's `serializeQueryTags` wire shape + * + * `options` is omitted/`None` for the no-options path; passing + * `{ statementConf: {} }` (an empty map) is treated the same as + * omission to keep the wire shape stable for the common case. + */ + executeStatement(sql: string, options?: ExecuteOptions | undefined | null): Promise + /** + * directResults execute — the Thrift/JDBC model. Sends ExecuteStatement + * with no `wait_timeout` field (server applies its ~10s default inline wait + * and auto-closes on success) and returns WITHOUT polling past it: + * + * - a **`Statement`** (left arm) when the query finished within the inline + * wait — terminal, result ready inline, `close()` is a clean release; + * - an **`AsyncStatement`** (right arm) when it did not — a poll/cancel + * handle the caller drives (`status()` / `awaitResult()` / `cancel()`). + * + * JS distinguishes the arms by feature-detecting `awaitResult` (present + * only on `AsyncStatement`). This is the path that gives mid-run cancel for + * long queries WITHOUT the eager-handle / close-drives workaround: the + * returned handle always corresponds to a server-owned statement. + * + * **Load-bearing contract:** the kernel's `DirectStatement::{Completed, + * Running}` discriminant cannot ride on these opaque `#[napi]` classes, so + * consumers MUST feature-detect via `awaitResult` (the only member unique to + * `AsyncStatement`). `Statement` (the Completed arm) MUST NOT gain an + * `awaitResult` member, or every consumer silently misroutes. The pyo3 + * binding makes the same `await_result`-probe assumption. + */ + executeStatementDirect(sql: string, options?: ExecuteOptions | undefined | null): Promise + /** + * Execute a SQL statement on the blocking (sync) path, but return a + * `CancellableExecution` handle so a concurrent JS task can cancel + * the query *while it is still running server-side*. + * + * `executeStatement` builds the kernel `Statement`, awaits the + * blocking `execute()`, and only then hands JS a `Statement` — so a + * query that runs for several seconds is uncancellable from JS on + * that path (there is no handle until the blocking call resolves). + * This method instead builds the statement, captures a detached + * `StatementCanceller` **before** dispatching `execute()`, and hands + * JS a `CancellableExecution` immediately. The caller drives the + * blocking execution via `result()` (resolves to the same + * `Statement` `executeStatement` returns) and can fire `cancel()` + * concurrently to interrupt a still-running query mid-COMPUTE. + * + * Option semantics are identical to `executeStatement`. + * Mirrors the pyo3 `Statement.canceller()` / `Statement.execute()` + * split (PR #121): obtain the canceller before the blocking drive. + */ + executeStatementCancellable(sql: string, options?: ExecuteOptions | undefined | null): Promise + /** + * Submit a SQL statement and return immediately with an + * `AsyncStatement` handle, without blocking until the query + * finishes. The kernel's `Statement::submit()` sends + * `wait_timeout=0s`, so the server responds as soon as it has a + * `statement_id` (state `Pending`/`Running`); JS drives polling + * via `AsyncStatement.status()` and materialises results with + * `AsyncStatement.awaitResult()`. + * + * This is the async-execution path the Thrift backend always + * uses (`runAsync: true`): the SEA backend submits, returns a + * pending operation handle, and polls to terminal during + * fetch. Option semantics (statementConf / queryTags / + * rowLimit / positional + named params) match `executeStatement`. + * Submit always sends `wait_timeout=0s` so the call returns + * immediately; the caller drives completion via `status()` / + * `awaitResult()`. Only the blocking-vs-pending return contract + * differs from `executeStatement`. + */ + submitStatement(sql: string, options?: ExecuteOptions | undefined | null): Promise + /** + * Explicit close. Awaits the server-side `DeleteSession` so the + * JS caller can observe failures (auth revoked mid-session, + * warehouse stopped, network error). Idempotent — a second call + * on an already-closed connection returns `Ok`. + * + * **Errors are terminal from the JS side.** The kernel session + * handle is consumed (`take()`) BEFORE the wire `DeleteSession` + * runs, because `Session::close` takes `self` by value. On `Err`, + * the napi `inner` is already `None`, so a JS-side retry sees a + * closed connection and returns `Ok(())` without re-attempting + * the wire call. The kernel's own `Drop` fire-and-forget retry + * runs once in the background — the JS caller can log the error + * but cannot drive a retry. If you need retry-on-failure + * semantics for `DeleteSession`, layer them above this method. + */ + close(): Promise + /** + * All catalogs visible to the session. + * + * JDBC `getCatalogs` shape: `TABLE_CAT: Utf8`. + */ + listCatalogs(): Promise + /** + * Schemas filtered by catalog (exact) and schema name pattern. + * + * JDBC `getSchemas` shape: `TABLE_SCHEM, TABLE_CATALOG`. + */ + listSchemas(catalog?: string | undefined | null, schemaPattern?: string | undefined | null): Promise + /** + * Tables filtered by catalog (exact), schema (pattern), table (pattern). + * + * 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. + */ + listTables(catalog?: string | undefined | null, schemaPattern?: string | undefined | null, tablePattern?: string | undefined | null, tableTypes?: Array | undefined | null): Promise + /** + * Columns of tables matching the filter. + * + * 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. + */ + listFunctions(catalog?: string | undefined | null, schemaPattern?: string | undefined | null, functionPattern?: string | undefined | null): Promise + /** + * Procedures visible to the session. `catalog` is exact; + * `schemaPattern` and `procedurePattern` are SQL LIKE. + */ + listProcedures(catalog?: string | undefined | null, schemaPattern?: string | undefined | null, procedurePattern?: string | undefined | null): Promise + /** + * All table types (`TABLE`, `VIEW`, `SYSTEM TABLE`, …). + * No wire call — static in-memory result. + */ + listTableTypes(): Promise + /** + * SQL data types supported by the workspace. + * No wire call — static in-memory result. + */ + listTypeInfo(): Promise + /** + * Primary keys for the given table. All three identifiers are + * exact — ODBC `SQLPrimaryKeys` does not support patterns. + */ + getPrimaryKeys(catalog: string, schema: string, table: string): Promise + /** + * Foreign-key relationships. The foreign side must be fully + * specified (catalog + schema + table); the parent side is + * optional. All identifiers are exact — no LIKE patterns. + */ + getCrossReference(parentCatalog: string | undefined | null, parentSchema: string | undefined | null, parentTable: string | undefined | null, foreignCatalog: string, foreignSchema: string, foreignTable: string): Promise +} +/** + * Opaque executed-statement handle. + * + * **Current concurrency shape** — every method takes `inner.lock()` + * and holds the guard across the kernel `.await`. tokio `Mutex` is + * FIFO, so cancel/close queue behind any in-flight `fetchNextBatch` + * until it returns naturally. This is a known limitation that exists + * because the napi shape has not yet been split into an + * `Arc` (for cancel/close, which the + * kernel exposes as `&self`-callable) plus a `Mutex>` only + * for the borrowed-mut fetch path. The lock-shape refactor needs a + * small kernel-side accessor and lands in a follow-up PR — see + * `sea-workflow/jira-candidates/2026-05-24-napi-cancel-during-fetch.md`. + * + * `schema` and `statement_id` are cached at construction so they + * survive `close()` — JS callers building error reports against a + * disposed statement can still read them. + */ +export declare class Statement { + /** + * 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 + /** + * Number of rows modified by the statement (UPDATE / INSERT / + * DELETE / MERGE). `null` for SELECT and on warehouses that don't + * surface the counter. Mirrors Thrift's + * `TGetOperationStatusResp.numModifiedRows`. + */ + numModifiedRows(): Promise + /** + * Server-supplied user-facing message. Mirrors Thrift's + * `TGetOperationStatusResp.displayMessage`. **PII / sensitive- + * data note:** may contain SQL fragments or parameter values — + * redact before centralised logging. + * + * Populated on `Succeeded` / `Closed` paths (incl. an empty `Closed`). + * On terminal-error states (`Failed` / `Cancelled`) the kernel returns + * an Error instead of a `Statement`, and the same field rides on the JS + * Error envelope under the same `displayMessage` key. + */ + displayMessage(): Promise + /** + * Server-supplied diagnostic detail — multi-line operator / + * stack context. Mirrors Thrift's + * `TGetOperationStatusResp.diagnosticInfo`. For support surfaces, + * not user-facing. Same reachability + PII caveats as + * `displayMessage`. + */ + diagnosticInfo(): Promise + /** + * Server-supplied JSON blob with extended error details. Mirrors + * Thrift's `TGetOperationStatusResp.errorDetailsJson`. + * Pass-through string — JS callers parse with `JSON.parse` if + * they need structured access. + * + * **Server-side gating:** populated only when the workspace has + * `spark.databricks.sql.errorDetailsJson.enabled = true` on the + * underlying SQL cluster. The flag is internal-only / default- + * false in the Databricks runtime, so for most JS callers this + * will return `null`. Admin-enabled workspaces return content + * shaped like `{"errorClass": "...", "messageTemplate": "..."}`. + * + * **Unbounded:** when populated, server can return a multi-MB + * blob; size before logging. + */ + errorDetailsJson(): Promise + /** + * Pull the next batch of results. Returns `null` when the stream + * is exhausted. The returned `ArrowBatch.ipcBytes` is a complete + * Arrow IPC stream (schema header + 1 record-batch message) + * suitable for handing to `apache-arrow`'s `RecordBatchReader`. + * + * On `Err`, the stream is in an unspecified state — call + * `close()` and discard the `Statement`. Subsequent + * `fetchNextBatch()` calls after an error are not guaranteed to + * succeed or fail consistently. + */ + fetchNextBatch(): Promise + /** + * Result schema as an Arrow IPC payload (schema header only, no + * record-batch message). Available before any batches have been + * fetched, and remains available after `close()` — the kernel + * materialises the schema eagerly so JS callers can build error + * reports against a disposed statement. + * + * Sync because the body has no `.await` — `encode_ipc_stream` is + * pure CPU work over an `Arc` already cached on the + * wrapper. Mirrors `pyo3/src/statement.rs::arrow_schema` (sync). + * napi-rs converts a panic in a sync `#[napi]` entry point into a + * thrown JS error via its own macro-expanded boundary, so the + * `util::guarded` `catch_unwind` wrapper that the `async fn` + * entry points use is not required for this method. + */ + schema(): ArrowSchema + /** + * Server-side cancel. + * + * For executed statements: short-circuits to `Ok(())` if + * `fetchNextBatch` has already returned `null` (stream + * naturally exhausted) — matches the JDBC `Statement.cancel()` + * no-op-after-completion contract, so JS callers can fire cancel + * defensively without distinguishing "real cancel" from "raced + * with natural completion." + * + * For metadata streams: no-op (the kernel has no in-flight + * cancellation surface for metadata calls today). + * + * Returns `KernelError(InvalidStatementHandle)` if the statement + * has been explicitly `close()`d. + */ + cancel(): Promise + /** + * Explicit close. + * + * For executed statements: awaits the server-side `CloseStatement` + * so the JS caller can observe failures (auth revoked mid-session, + * network error, server-side error). Idempotent — a second call + * on an already-closed statement returns `Ok`. + * + * **Errors are terminal from the JS side.** The kernel executed + * handle is taken out of `inner` BEFORE the wire `CloseStatement` + * runs (so `Drop` knows there's nothing left to clean up). On + * `Err`, the napi `inner` is already `None`, so a JS-side retry + * sees a closed statement and returns `Ok(())` without re- + * attempting the wire call. The kernel-level `ExecutedStatement` + * has been consumed at that point and the value is dropped on + * the way out of the closure — the kernel's `ExecutedStatement:: + * Drop` then fires-and-forgets a single retry on the captured + * runtime. The JS caller can log the error but cannot drive a + * further retry. If you need retry-on-failure semantics for + * `CloseStatement`, layer them above this method. + * + * For metadata streams: drops the stream (no server round-trip + * needed — metadata results have no in-flight server-side + * resource to release). + */ + close(): Promise +} diff --git a/native/kernel/index.js b/native/kernel/index.js index 45ea1ec8..ad50ecc9 100644 --- a/native/kernel/index.js +++ b/native/kernel/index.js @@ -1,713 +1,325 @@ -// prettier-ignore +/* tslint:disable */ /* eslint-disable */ -// @ts-nocheck +/* prettier-ignore */ + /* auto-generated by NAPI-RS */ -const { readFileSync } = require('fs') -let nativeBinding = null -const loadErrors = [] +const { existsSync, readFileSync } = require('fs') +const { join } = require('path') -const isMusl = () => { - let musl = false - if (process.platform === 'linux') { - musl = isMuslFromFilesystem() - if (musl === null) { - musl = isMuslFromReport() - } - if (musl === null) { - musl = isMuslFromChildProcess() - } - } - return musl -} +const { platform, arch } = process -const isFileMusl = (f) => f.includes('libc.musl-') || f.includes('ld-musl-') - -const isMuslFromFilesystem = () => { - try { - return readFileSync('/usr/bin/ldd', 'utf-8').includes('musl') - } catch { - return null - } -} +let nativeBinding = null +let localFileExisted = false +let loadError = null -const isMuslFromReport = () => { - let report = null - if (process.report && typeof process.report.getReport === 'function') { - process.report.excludeNetwork = true - report = process.report.getReport() - } - if (!report) { - return null - } - if (report.header && report.header.glibcVersionRuntime) { - return false - } - if (Array.isArray(report.sharedObjects)) { - if (report.sharedObjects.some(isFileMusl)) { +function isMusl() { + // For Node 10 + if (!process.report || typeof process.report.getReport !== 'function') { + try { + const lddPath = require('child_process').execSync('which ldd').toString().trim() + return readFileSync(lddPath, 'utf8').includes('musl') + } catch (e) { return true } - } - return false -} - -const isMuslFromChildProcess = () => { - try { - return require('child_process').execSync('ldd --version', { encoding: 'utf8' }).includes('musl') - } catch (e) { - // If we reach this case, we don't know if the system is musl or not, so is better to just fallback to false - return false + } else { + const { glibcVersionRuntime } = process.report.getReport().header + return !glibcVersionRuntime } } -function requireNative() { - if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) { - try { - return require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH); - } catch (err) { - loadErrors.push(err) - } - } else if (process.platform === 'android') { - if (process.arch === 'arm64') { - try { - return require('./index.android-arm64.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-android-arm64') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-android-arm64/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else if (process.arch === 'arm') { - try { - return require('./index.android-arm-eabi.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-android-arm-eabi') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-android-arm-eabi/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else { - loadErrors.push(new Error(`Unsupported architecture on Android ${process.arch}`)) - } - } else if (process.platform === 'win32') { - if (process.arch === 'x64') { - if ((process.config && process.config.variables && process.config.variables.shlib_suffix === 'dll.a') || (process.config && process.config.variables && process.config.variables.node_target_type === 'shared_library')) { - try { - return require('./index.win32-x64-gnu.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-win32-x64-gnu') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-win32-x64-gnu/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else { - try { - return require('./index.win32-x64-msvc.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-win32-x64-msvc') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-win32-x64-msvc/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } - } else if (process.arch === 'ia32') { - try { - return require('./index.win32-ia32-msvc.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-win32-ia32-msvc') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-win32-ia32-msvc/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else if (process.arch === 'arm64') { - try { - return require('./index.win32-arm64-msvc.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-win32-arm64-msvc') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-win32-arm64-msvc/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else { - loadErrors.push(new Error(`Unsupported architecture on Windows: ${process.arch}`)) - } - } else if (process.platform === 'darwin') { - try { - return require('./index.darwin-universal.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-darwin-universal') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-darwin-universal/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - if (process.arch === 'x64') { - try { - return require('./index.darwin-x64.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-darwin-x64') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-darwin-x64/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else if (process.arch === 'arm64') { - try { - return require('./index.darwin-arm64.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-darwin-arm64') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-darwin-arm64/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else { - loadErrors.push(new Error(`Unsupported architecture on macOS: ${process.arch}`)) - } - } else if (process.platform === 'freebsd') { - if (process.arch === 'x64') { - try { - return require('./index.freebsd-x64.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-freebsd-x64') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-freebsd-x64/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else if (process.arch === 'arm64') { - try { - return require('./index.freebsd-arm64.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-freebsd-arm64') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-freebsd-arm64/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else { - loadErrors.push(new Error(`Unsupported architecture on FreeBSD: ${process.arch}`)) - } - } else if (process.platform === 'linux') { - if (process.arch === 'x64') { - if (isMusl()) { +switch (platform) { + case 'android': + switch (arch) { + case 'arm64': + localFileExisted = existsSync(join(__dirname, 'index.android-arm64.node')) try { - return require('./index.linux-x64-musl.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-linux-x64-musl') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-linux-x64-musl/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (localFileExisted) { + nativeBinding = require('./index.android-arm64.node') + } else { + nativeBinding = require('@databricks/databricks-sql-kernel-android-arm64') } - return binding } catch (e) { - loadErrors.push(e) + loadError = e } - } else { + break + case 'arm': + localFileExisted = existsSync(join(__dirname, 'index.android-arm-eabi.node')) try { - return require('./index.linux-x64-gnu.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-linux-x64-gnu') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-linux-x64-gnu/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (localFileExisted) { + nativeBinding = require('./index.android-arm-eabi.node') + } else { + nativeBinding = require('@databricks/databricks-sql-kernel-android-arm-eabi') } - return binding - } catch (e) { - loadErrors.push(e) - } - } - } else if (process.arch === 'arm64') { - if (isMusl()) { - try { - return require('./index.linux-arm64-musl.node') } catch (e) { - loadErrors.push(e) + loadError = e } + break + default: + throw new Error(`Unsupported architecture on Android ${arch}`) + } + break + case 'win32': + switch (arch) { + case 'x64': + localFileExisted = existsSync( + join(__dirname, 'index.win32-x64-msvc.node') + ) try { - const binding = require('@databricks/databricks-sql-kernel-linux-arm64-musl') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-linux-arm64-musl/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (localFileExisted) { + nativeBinding = require('./index.win32-x64-msvc.node') + } else { + nativeBinding = require('@databricks/databricks-sql-kernel-win32-x64-msvc') } - return binding - } catch (e) { - loadErrors.push(e) - } - } else { - try { - return require('./index.linux-arm64-gnu.node') } catch (e) { - loadErrors.push(e) + loadError = e } + break + case 'ia32': + localFileExisted = existsSync( + join(__dirname, 'index.win32-ia32-msvc.node') + ) try { - const binding = require('@databricks/databricks-sql-kernel-linux-arm64-gnu') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-linux-arm64-gnu/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (localFileExisted) { + nativeBinding = require('./index.win32-ia32-msvc.node') + } else { + nativeBinding = require('@databricks/databricks-sql-kernel-win32-ia32-msvc') } - return binding } catch (e) { - loadErrors.push(e) + loadError = e } - } - } else if (process.arch === 'arm') { - if (isMusl()) { + break + case 'arm64': + localFileExisted = existsSync( + join(__dirname, 'index.win32-arm64-msvc.node') + ) try { - return require('./index.linux-arm-musleabihf.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-linux-arm-musleabihf') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-linux-arm-musleabihf/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (localFileExisted) { + nativeBinding = require('./index.win32-arm64-msvc.node') + } else { + nativeBinding = require('@databricks/databricks-sql-kernel-win32-arm64-msvc') } - return binding } catch (e) { - loadErrors.push(e) + loadError = e } + break + default: + throw new Error(`Unsupported architecture on Windows: ${arch}`) + } + break + case 'darwin': + localFileExisted = existsSync(join(__dirname, 'index.darwin-universal.node')) + try { + if (localFileExisted) { + nativeBinding = require('./index.darwin-universal.node') } else { - try { - return require('./index.linux-arm-gnueabihf.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-linux-arm-gnueabihf') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-linux-arm-gnueabihf/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } + nativeBinding = require('@databricks/databricks-sql-kernel-darwin-universal') } - } else if (process.arch === 'loong64') { - if (isMusl()) { - try { - return require('./index.linux-loong64-musl.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-linux-loong64-musl') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-linux-loong64-musl/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else { + break + } catch {} + switch (arch) { + case 'x64': + localFileExisted = existsSync(join(__dirname, 'index.darwin-x64.node')) try { - return require('./index.linux-loong64-gnu.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-linux-loong64-gnu') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-linux-loong64-gnu/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (localFileExisted) { + nativeBinding = require('./index.darwin-x64.node') + } else { + nativeBinding = require('@databricks/databricks-sql-kernel-darwin-x64') } - return binding - } catch (e) { - loadErrors.push(e) - } - } - } else if (process.arch === 'riscv64') { - if (isMusl()) { - try { - return require('./index.linux-riscv64-musl.node') } catch (e) { - loadErrors.push(e) + loadError = e } + break + case 'arm64': + localFileExisted = existsSync( + join(__dirname, 'index.darwin-arm64.node') + ) try { - const binding = require('@databricks/databricks-sql-kernel-linux-riscv64-musl') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-linux-riscv64-musl/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (localFileExisted) { + nativeBinding = require('./index.darwin-arm64.node') + } else { + nativeBinding = require('@databricks/databricks-sql-kernel-darwin-arm64') } - return binding } catch (e) { - loadErrors.push(e) + loadError = e } + break + default: + throw new Error(`Unsupported architecture on macOS: ${arch}`) + } + break + case 'freebsd': + if (arch !== 'x64') { + throw new Error(`Unsupported architecture on FreeBSD: ${arch}`) + } + localFileExisted = existsSync(join(__dirname, 'index.freebsd-x64.node')) + try { + if (localFileExisted) { + nativeBinding = require('./index.freebsd-x64.node') } else { - try { - return require('./index.linux-riscv64-gnu.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-linux-riscv64-gnu') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-linux-riscv64-gnu/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } - } else if (process.arch === 'ppc64') { - try { - return require('./index.linux-ppc64-gnu.node') - } catch (e) { - loadErrors.push(e) + nativeBinding = require('@databricks/databricks-sql-kernel-freebsd-x64') } - try { - const binding = require('@databricks/databricks-sql-kernel-linux-ppc64-gnu') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-linux-ppc64-gnu/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else if (process.arch === 's390x') { - try { - return require('./index.linux-s390x-gnu.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-linux-s390x-gnu') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-linux-s390x-gnu/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else { - loadErrors.push(new Error(`Unsupported architecture on Linux: ${process.arch}`)) + } catch (e) { + loadError = e } - } else if (process.platform === 'openharmony') { - if (process.arch === 'arm64') { - try { - return require('./index.openharmony-arm64.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-openharmony-arm64') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-openharmony-arm64/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else if (process.arch === 'x64') { - try { - return require('./index.openharmony-x64.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-openharmony-x64') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-openharmony-x64/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + break + case 'linux': + switch (arch) { + case 'x64': + if (isMusl()) { + localFileExisted = existsSync( + join(__dirname, 'index.linux-x64-musl.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./index.linux-x64-musl.node') + } else { + nativeBinding = require('@databricks/databricks-sql-kernel-linux-x64-musl') + } + } catch (e) { + loadError = e + } + } else { + localFileExisted = existsSync( + join(__dirname, 'index.linux-x64-gnu.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./index.linux-x64-gnu.node') + } else { + nativeBinding = require('@databricks/databricks-sql-kernel-linux-x64-gnu') + } + } catch (e) { + loadError = e + } } - return binding - } catch (e) { - loadErrors.push(e) - } - } else if (process.arch === 'arm') { - try { - return require('./index.openharmony-arm.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@databricks/databricks-sql-kernel-openharmony-arm') - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-openharmony-arm/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + break + case 'arm64': + if (isMusl()) { + localFileExisted = existsSync( + join(__dirname, 'index.linux-arm64-musl.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./index.linux-arm64-musl.node') + } else { + nativeBinding = require('@databricks/databricks-sql-kernel-linux-arm64-musl') + } + } catch (e) { + loadError = e + } + } else { + localFileExisted = existsSync( + join(__dirname, 'index.linux-arm64-gnu.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./index.linux-arm64-gnu.node') + } else { + nativeBinding = require('@databricks/databricks-sql-kernel-linux-arm64-gnu') + } + } catch (e) { + loadError = e + } } - return binding - } catch (e) { - loadErrors.push(e) - } - } else { - loadErrors.push(new Error(`Unsupported architecture on OpenHarmony: ${process.arch}`)) - } - } else { - loadErrors.push(new Error(`Unsupported OS: ${process.platform}, architecture: ${process.arch}`)) - } -} - -function createLoadErrorChain(errors) { - return errors.reduce((previous, current) => { - let message - try { - message = - current && typeof current.message === 'string' - ? current.message - : String(current) - } catch { - message = 'Unknown error' - } - const error = new Error(message) - error.cause = previous - return error - }, null) -} - -// NAPI_RS_FORCE_WASI is a tri-state flag: -// unset / any other value → native binding preferred, WASI is only a fallback -// 'true' → prefer WASI, but retain native as a lazy fallback -// 'error' → require WASI without initializing a native fallback -// Treating any non-empty string as truthy (the historical behavior) meant -// NAPI_RS_FORCE_WASI=false, NAPI_RS_FORCE_WASI=0, etc. inadvertently triggered -// the WASI path, causing ENOENT for packages shipped without a .wasi.cjs file. -// -// NAPI_RS_WASI_FLAVOR selects one exact generated flavor and implies strict -// WASI loading. It never crosses into another flavor or falls back to native. -const __napiWasiFlavors = ["wasm32-wasi"] -const __napiWasiFlavor = process.env.NAPI_RS_WASI_FLAVOR -const __napiWasiFlavorRequested = - typeof __napiWasiFlavor === 'string' && __napiWasiFlavor.length > 0 -if ( - __napiWasiFlavorRequested && - __napiWasiFlavors.indexOf(__napiWasiFlavor) === -1 -) { - throw new Error( - 'Unsupported WASI flavor "' + - __napiWasiFlavor + - '". Available flavors: ' + - __napiWasiFlavors.join(', '), - ) -} -const forceWasiError = process.env.NAPI_RS_FORCE_WASI === 'error' -const forceWasi = - process.env.NAPI_RS_FORCE_WASI === 'true' || - forceWasiError || - __napiWasiFlavorRequested - -if (!forceWasi) { - nativeBinding = requireNative() -} - -if (!nativeBinding || forceWasi) { - let wasiBinding = null - let wasiBindingLoaded = false - const wasiBindingErrors = [] - const __napiWasiResolveCandidate = (specifier, isPackage, localArtifacts) => { - try { - require.resolve(specifier) - } catch (resolveError) { - if (!resolveError || resolveError.code !== 'MODULE_NOT_FOUND') { - throw resolveError - } - if (isPackage) { - try { - require.resolve(specifier + '/package.json') - } catch (packageError) { - if (packageError && packageError.code === 'MODULE_NOT_FOUND') { - return resolveError + break + case 'arm': + if (isMusl()) { + localFileExisted = existsSync( + join(__dirname, 'index.linux-arm-musleabihf.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./index.linux-arm-musleabihf.node') + } else { + nativeBinding = require('@databricks/databricks-sql-kernel-linux-arm-musleabihf') + } + } catch (e) { + loadError = e + } + } else { + localFileExisted = existsSync( + join(__dirname, 'index.linux-arm-gnueabihf.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./index.linux-arm-gnueabihf.node') + } else { + nativeBinding = require('@databricks/databricks-sql-kernel-linux-arm-gnueabihf') + } + } catch (e) { + loadError = e } - // An exports restriction proves the package exists even when its - // package.json is not public. Preserve the root resolution failure. - throw resolveError } - // The package exists but its main/export target is broken. - throw resolveError - } - return resolveError - } - if (localArtifacts) { - let artifactError = null - for (let i = 0; i < localArtifacts.length; i++) { - try { - require.resolve(localArtifacts[i]) - return null - } catch (resolveError) { - if (!resolveError || resolveError.code !== 'MODULE_NOT_FOUND') { - throw resolveError + break + case 'riscv64': + if (isMusl()) { + localFileExisted = existsSync( + join(__dirname, 'index.linux-riscv64-musl.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./index.linux-riscv64-musl.node') + } else { + nativeBinding = require('@databricks/databricks-sql-kernel-linux-riscv64-musl') + } + } catch (e) { + loadError = e + } + } else { + localFileExisted = existsSync( + join(__dirname, 'index.linux-riscv64-gnu.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./index.linux-riscv64-gnu.node') + } else { + nativeBinding = require('@databricks/databricks-sql-kernel-linux-riscv64-gnu') + } + } catch (e) { + loadError = e } - artifactError = resolveError } - } - return artifactError - } - return null - } - if (!wasiBindingLoaded && (!__napiWasiFlavorRequested || __napiWasiFlavor === "wasm32-wasi")) { - let candidateError = null - let candidateFailed = false - try { - candidateError = __napiWasiResolveCandidate('./index.wasi.cjs', false, ["./index.wasm32-wasi.debug.wasm","./index.wasm32-wasi.wasm"]) - candidateFailed = candidateError !== null - if (!candidateFailed) { - wasiBinding = require('./index.wasi.cjs') - nativeBinding = wasiBinding - wasiBindingLoaded = true - } - } catch (err) { - candidateError = err - candidateFailed = true - } - if (candidateFailed) { - wasiBindingErrors.push(candidateError) - loadErrors.push(candidateError) - } - } - if (!wasiBindingLoaded && (!__napiWasiFlavorRequested || __napiWasiFlavor === "wasm32-wasi")) { - let candidateError = null - let candidateFailed = false - try { - candidateError = __napiWasiResolveCandidate('@databricks/databricks-sql-kernel-wasm32-wasi', true, undefined) - candidateFailed = candidateError !== null - if (!candidateFailed) { - if (process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - const bindingPackageVersion = require('@databricks/databricks-sql-kernel-wasm32-wasi/package.json').version - if (bindingPackageVersion !== '0.2.0') { - throw new Error(`WASI binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + break + case 's390x': + localFileExisted = existsSync( + join(__dirname, 'index.linux-s390x-gnu.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./index.linux-s390x-gnu.node') + } else { + nativeBinding = require('@databricks/databricks-sql-kernel-linux-s390x-gnu') } + } catch (e) { + loadError = e } - wasiBinding = require('@databricks/databricks-sql-kernel-wasm32-wasi') - nativeBinding = wasiBinding - wasiBindingLoaded = true - } - } catch (err) { - candidateError = err - candidateFailed = true + break + default: + throw new Error(`Unsupported architecture on Linux: ${arch}`) } - if (candidateFailed) { - wasiBindingErrors.push(candidateError) - loadErrors.push(candidateError) - } - } - if ( - !wasiBindingLoaded && - forceWasi && - !forceWasiError && - !__napiWasiFlavorRequested - ) { - nativeBinding = requireNative() - } - if ((forceWasiError || __napiWasiFlavorRequested) && !wasiBindingLoaded) { - const error = new Error( - __napiWasiFlavorRequested - ? 'WASI binding for flavor "' + __napiWasiFlavor + '" not found' - : 'WASI binding not found and NAPI_RS_FORCE_WASI is set to error', - ) - error.cause = createLoadErrorChain(wasiBindingErrors) - throw error - } + break + default: + throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`) } if (!nativeBinding) { - if (loadErrors.length > 0) { - const error = new Error( - `Cannot find native binding. ` + - `npm has a bug related to optional dependencies (https://github.com/npm/cli/issues/4828). ` + - 'Please try `npm i` again after removing both package-lock.json and node_modules directory.', - ) - // assign instead of the `new Error(message, { cause })` options form, - // which Node < 16.9 silently ignores - error.cause = createLoadErrorChain(loadErrors) - throw error + if (loadError) { + throw loadError } throw new Error(`Failed to load native binding`) } -module.exports = nativeBinding -module.exports.AsyncResultHandle = nativeBinding.AsyncResultHandle -module.exports.AsyncStatement = nativeBinding.AsyncStatement -module.exports.CancellableExecution = nativeBinding.CancellableExecution -module.exports.Connection = nativeBinding.Connection -module.exports.Statement = nativeBinding.Statement -module.exports.AuthMode = nativeBinding.AuthMode -module.exports.initKernelLogging = nativeBinding.initKernelLogging -module.exports.kernelLoggingStats = nativeBinding.kernelLoggingStats -module.exports.openSession = nativeBinding.openSession -module.exports.setKernelLogLevel = nativeBinding.setKernelLogLevel -module.exports.version = nativeBinding.version +const { AsyncStatement, AsyncResultHandle, CancellableExecution, Connection, AuthMode, openSession, initKernelLogging, kernelLoggingStats, setKernelLogLevel, Statement, version } = nativeBinding + +module.exports.AsyncStatement = AsyncStatement +module.exports.AsyncResultHandle = AsyncResultHandle +module.exports.CancellableExecution = CancellableExecution +module.exports.Connection = Connection +module.exports.AuthMode = AuthMode +module.exports.openSession = openSession +module.exports.initKernelLogging = initKernelLogging +module.exports.kernelLoggingStats = kernelLoggingStats +module.exports.setKernelLogLevel = setKernelLogLevel +module.exports.Statement = Statement +module.exports.version = version From 6aa346408e3ad769d4cc57377126add3de803ff8 Mon Sep 17 00:00:00 2001 From: "peco-engineer-bot[bot]" Date: Fri, 21 Aug 2026 22:59:49 +0000 Subject: [PATCH 11/14] ai: apply changes for #505 (1 review thread) Addresses: - #3834226992 at lib/kernel/KernelAuth.ts:782 Signed-off-by: peco-engineer-bot[bot] --- lib/kernel/KernelAuth.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/kernel/KernelAuth.ts b/lib/kernel/KernelAuth.ts index ed9dd4b3..ca4d796a 100644 --- a/lib/kernel/KernelAuth.ts +++ b/lib/kernel/KernelAuth.ts @@ -777,6 +777,12 @@ export function buildKernelConnectionOptions(options: ConnectionOptions): Kernel 'app-registration client id) alongside `oauthClientSecret`.', ); } + // `oauthScopes` is intentionally NOT forwarded here (and the `AzureSpM2m` + // union member has no such field), unlike the generic `OAuthM2m` arm below + // which honors an override. Entra service-principal tokens use a fixed + // `/.default` scope that the kernel derives from the Azure app id; + // a caller-supplied scope override is meaningless to that flow, so it is + // dropped by design rather than plumbed through. const azure = { ...base, authMode: 'AzureSpM2m' as const, From 324168cf7f4047cadb4978caf32556bef98086cb Mon Sep 17 00:00:00 2001 From: "peco-engineer-bot[bot]" Date: Fri, 21 Aug 2026 23:05:50 +0000 Subject: [PATCH 12/14] ai: apply changes for #505 (1 review thread) Addresses: - #3834265600 at lib/kernel/KernelAuth.ts:289 Signed-off-by: peco-engineer-bot[bot] --- tests/unit/kernel/auth-m2m.test.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/unit/kernel/auth-m2m.test.ts b/tests/unit/kernel/auth-m2m.test.ts index 99b67b99..d29a1524 100644 --- a/tests/unit/kernel/auth-m2m.test.ts +++ b/tests/unit/kernel/auth-m2m.test.ts @@ -173,6 +173,34 @@ describe('KernelAuth + KernelBackend — OAuth M2M auth flow', () => { }); }); + it('normalizes the host before Azure suffix matching (scheme, port, path, padding, case)', () => { + // `isAzureHost` strips a `http(s)://` scheme, drops any path and explicit + // `:port`, trims surrounding whitespace, and lowercases before matching the + // Azure suffixes. Feed each of those non-normalized shapes and assert the + // connection still routes to the Entra-direct `AzureSpM2m` path — a + // regression in the normalization would silently misroute to the in-house + // `OAuthM2m` and go uncaught otherwise. + const azureHostVariants = [ + 'https://adb-12345.0.azuredatabricks.net:443/sql/1.0/warehouses/abc', + 'http://adb-12345.0.azuredatabricks.net:8443', + ' ADB-12345.0.AzureDatabricks.NET ', + 'adb-12345.0.databricks.azure.us', + ]; + + for (const host of azureHostVariants) { + const opts: ConnectionOptions = { + host, + path: '/sql/1.0/warehouses/abc', + authType: 'databricks-oauth', + oauthClientId: 'entra-app-id', + oauthClientSecret: 'entra-secret', + }; + + const native = buildKernelConnectionOptions(opts); + expect(native.authMode, `for host=${JSON.stringify(host)}`).to.equal('AzureSpM2m'); + } + }); + it('treats a blank/reserved azureTenantId as omitted (kernel auto-discovers)', () => { // A tenant that resolves to `''`, whitespace, or the `'undefined'`/`'null'` // shell-export artifacts is degenerate: forwarding it verbatim would From 1aff27997cb3b7e15bd2eec94ffc2ee3d684aac8 Mon Sep 17 00:00:00 2001 From: "peco-engineer-bot[bot]" Date: Fri, 21 Aug 2026 23:14:51 +0000 Subject: [PATCH 13/14] ai: apply changes for #505 (1 review thread) Addresses: - #3834302149 at lib/kernel/KernelAuth.ts:288 Signed-off-by: peco-engineer-bot[bot] --- lib/kernel/KernelAuth.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/kernel/KernelAuth.ts b/lib/kernel/KernelAuth.ts index ca4d796a..f8c9529f 100644 --- a/lib/kernel/KernelAuth.ts +++ b/lib/kernel/KernelAuth.ts @@ -280,9 +280,12 @@ const AZURE_HOST_SUFFIXES = ['.azuredatabricks.net', '.databricks.azure.us', '.d /** * True when `host` is an Azure Databricks workspace host. Normalises the input - * the same way `getManager` does (trim surrounding whitespace, lowercase, strip - * scheme, then drop any path and explicit `:port`) so a caller passing a bare - * host, a padded string, or a full URL with a port is treated identically. + * more aggressively than the Thrift driver's `getManager` (which only + * lowercases and strips a leading `https://`): here we also trim surrounding + * whitespace, strip either scheme, then drop any path and explicit `:port`, so + * a caller passing a bare host, a padded string, or a full URL with a port is + * treated identically. The suffix set matches `getManager`, so routing stays a + * superset — not a byte-for-byte match — of Thrift's. */ function isAzureHost(host: string): boolean { const normalized = host From 16947e3ba3863439b22143b719d7704de719aa68 Mon Sep 17 00:00:00 2001 From: eric-wang-1990 Date: Fri, 21 Aug 2026 16:47:09 -0700 Subject: [PATCH 14/14] fix(kernel): regenerate native contract via build:native, not hand-copy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prior refresh (d74ffaa) hand-copied the kernel's committed napi/index.* into native/kernel/. Those are @napi-rs/cli v2-style output, but the kernel's napi crate is 3.11.0, so build:native runs @napi-rs/cli@3.8.2 (v3-style) — the format the committed contract actually uses. The hand-copy therefore regressed index.js's loader (dropped the NAPI_RS_NATIVE_LIBRARY_PATH override, the per-package version-consistency checks, and the win32-x64-gnu/darwin-universal targets) and reformatted index.d.ts wholesale. Regenerate authoritatively by running npm run build:native against the pinned KERNEL_REV (kernel main 5e5dea9) with @napi-rs/cli@3.8.2. Result: - index.js: byte-identical to the pre-d74ffaa baseline (a new auth mode does not change the platform-resolution loader) — net zero change vs the PR base. - index.d.ts: purely additive (+158/-2) — exposes AuthMode.AzureSpM2m and the azureClientId/azureClientSecret/azureTenantId/azureWorkspaceResourceId fields, plus the OAuthM2mJwt mode, tokenUrl/jwt* and telemetry/system-config fields that had also drifted out of the committed contract. Co-authored-by: Isaac Signed-off-by: eric-wang-1990 --- native/kernel/index.d.ts | 1747 +++++++++++++++++++------------------- native/kernel/index.js | 918 ++++++++++++++------ 2 files changed, 1540 insertions(+), 1125 deletions(-) diff --git a/native/kernel/index.d.ts b/native/kernel/index.d.ts index bc16c0d2..6fb596e2 100644 --- a/native/kernel/index.d.ts +++ b/native/kernel/index.d.ts @@ -1,356 +1,804 @@ -/* 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 +export declare class AsyncResultHandle { /** - * Server-side cap on the number of rows this statement returns - * (SEA `row_limit`), independent of any SQL `LIMIT`. Maps to - * `StatementSpec.row_limit`. Omitted ⇒ no driver-imposed cap. + * Server-issued statement id. Cached at construction; readable + * for log correlation. Matches the parent `AsyncStatement`'s + * `statementId`. */ - rowLimit?: number + get statementId(): string /** - * 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`]. + * 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. */ - positionalParams?: Array + fetchNextBatch(): Promise /** - * Named parameters (`:name` placeholders). Each carries its `name` - * alongside the `{ sqlType, value? }` pair. Mapped to a kernel - * `TypedValue` via the same [`parse_typed_value`] codec and bound with - * `StatementSpec::param_named`. Named is the SEA-spec-required public - * param form (`StatementParameter.name` is `openapi_required`); - * positional is the documented-undocumented variant. The two are - * mutually exclusive at the SQL level (`?` vs `:name`). + * 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`. */ - 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 + schema(): ArrowSchema } + /** - * 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'`, `'AzureSpM2m'`). + * Opaque async-statement handle. * - * 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. + * 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()`. + * + * 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 const enum AuthMode { - /** Personal access token (`token`). */ - Pat = 'Pat', - /** OAuth 2.0 machine-to-machine — `oauthClientId` + `oauthClientSecret`. */ - OAuthM2m = 'OAuthM2m', +export declare class AsyncStatement { /** - * OAuth 2.0 machine-to-machine with a JWT private-key client - * assertion — `oauthClientId` + `jwtKeyFile` + `jwtKid`. + * 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. */ - OAuthM2mJwt = 'OAuthM2mJwt', + get statementId(): string /** - * OAuth 2.0 user-to-machine (browser flow) — optional `oauthClientId` - * + `oauthRedirectPort`. + * 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. */ - OAuthU2m = 'OAuthU2m', + status(): Promise + /** Rows modified by the statement (UPDATE / INSERT / DELETE / MERGE). */ + numModifiedRows(): Promise /** - * Azure Entra service-principal M2M — `azureClientId` + - * `azureClientSecret` (+ optional `azureTenantId` / - * `azureWorkspaceResourceId`). The kernel builds the Entra token - * endpoint + `.default` scope, auto-discovers the tenant from the - * workspace when `azureTenantId` is omitted, and — when - * `azureWorkspaceResourceId` is set — additionally sends the SP - * management token for an RBAC-only service principal. + * Server-supplied user-facing message (may contain SQL fragments — + * redact before centralised logging). */ - AzureSpM2m = 'AzureSpM2m' + displayMessage(): Promise + /** Server-supplied diagnostic detail. */ + diagnosticInfo(): Promise + /** Server-supplied structured error detail (JSON), when enabled. */ + errorDetailsJson(): Promise + /** + * 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. + */ + awaitResult(): Promise + /** + * 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`. + */ + cancel(): Promise + /** + * Explicit close. Idempotent — a second call on an + * already-closed handle returns `Ok(())`. On `Err`, the napi + * inner is already `None`, so a JS-side retry sees the + * closed-handle short-circuit and returns `Ok(())` without + * re-attempting the wire call. The kernel's own `Drop` + * fire-and-forget retry runs once in the background. + */ + close(): Promise } + /** - * A single extra HTTP header as an explicit `{ name, value }` pair. + * 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. * - * 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"`). + * `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 interface ProxyInput { - url: string - username?: string - password?: string - bypassHosts?: string +export declare class CancellableExecution { + /** + * 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. + */ + get statementId(): string | null + /** + * Drive the blocking `execute()` and resolve to a `Statement` + * (identical to what `executeStatement` returns) once the kernel + * reaches a terminal state and the result stream is ready. + * + * Consumes the pending statement: a second `result()` call returns + * `KernelError(InvalidStatementHandle)`. The future is + * drop-cancel-safe — the kernel's per-execute `MidExecuteCancelState` + * guard fires a fire-and-forget `cancel_statement` if this future is + * dropped mid-flight (`Promise.race` / timeout loser), independently + * of an explicit `cancel()`. + * + * On a server-side cancel the kernel's blocking `execute()` currently + * surfaces `InvalidArgument` (a known kernel quirk — the async path + * returns `Cancelled`). When this handle's `cancel()` actually dispatched a + * server-side cancel, we normalise that into `Cancelled` here so JS callers + * can rely on a single cancelled-status code regardless of execution path. + * + * Three outcomes can race the blocking drive: (1) a natural terminal state + * → `Ok` or the genuine error; (2) an explicit `cancel()` that dispatched a + * server cancel → this `result()` rejects with a `Cancelled`-coded error + * (the normalisation above); (3) the future being **dropped** mid-flight + * (`Promise.race`/timeout loser) → the kernel's `MidExecuteCancelState` + * drop-guard fires a fire-and-forget `cancel_statement`, but there is no + * `result()` left to observe a code. Only (2) yields a `Cancelled` error. + */ + result(): Promise + /** + * Server-side cancel of the in-flight statement. + * + * Lock-free: fires the detached `StatementCanceller` captured at + * construction rather than taking the mutex `result()` holds, so it + * interrupts a still-running blocking `result()` instead of queueing + * behind it. No-op (returns `Ok`) if `result()` already finished + * successfully, or if no statement id has been observed yet (query still + * in its initial submit round-trip), and idempotent against a server + * already in a terminal state. + */ + cancel(): Promise } + /** - * JS-visible options for opening a Databricks SQL session. + * Opaque connection handle wrapping a kernel `Session`. * - * Authentication is selected by `authMode` (default [`AuthMode::Pat`]): - * - `Pat` — `token` required. - * - `OAuthM2m` — `oauthClientId` + `oauthClientSecret` required. - * - `OAuthU2m` — `oauthClientId` / `oauthRedirectPort` optional - * (defaults to the `databricks-sql-connector` client on port 8030). - * - `AzureSpM2m` — `azureClientId` + `azureClientSecret` required - * (+ optional `azureTenantId` / `azureWorkspaceResourceId`). + * `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. * - * 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** — 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 interface ConnectionOptions { +export declare class Connection { /** - * Workspace host, e.g. `adb-…azuredatabricks.net`. The kernel - * normalises this — bare hostnames get `https://` prepended. + * 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. */ - hostName: string + get sessionId(): string /** - * JDBC-style HTTP path, e.g. `/sql/1.0/warehouses/abc123`. The - * kernel parses out the warehouse id. + * 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. */ - httpPath: string + executeStatement(sql: string, options?: ExecuteOptions | undefined | null): Promise /** - * Authentication mode. Omitted ⇒ [`AuthMode::Pat`] (back-compat: - * existing PAT callers pass only `token`). + * directResults execute — the Thrift/JDBC model. Sends ExecuteStatement + * with no `wait_timeout` field (server applies its ~10s default inline wait + * and auto-closes on success) and returns WITHOUT polling past it: + * + * - a **`Statement`** (left arm) when the query finished within the inline + * wait — terminal, result ready inline, `close()` is a clean release; + * - an **`AsyncStatement`** (right arm) when it did not — a poll/cancel + * handle the caller drives (`status()` / `awaitResult()` / `cancel()`). + * + * JS distinguishes the arms by feature-detecting `awaitResult` (present + * only on `AsyncStatement`). This is the path that gives mid-run cancel for + * long queries WITHOUT the eager-handle / close-drives workaround: the + * returned handle always corresponds to a server-owned statement. + * + * **Load-bearing contract:** the kernel's `DirectStatement::{Completed, + * Running}` discriminant cannot ride on these opaque `#[napi]` classes, so + * consumers MUST feature-detect via `awaitResult` (the only member unique to + * `AsyncStatement`). `Statement` (the Completed arm) MUST NOT gain an + * `awaitResult` member, or every consumer silently misroutes. The pyo3 + * binding makes the same `await_result`-probe assumption. */ - authMode?: AuthMode + executeStatementDirect(sql: string, options?: ExecuteOptions | undefined | null): Promise /** - * Personal access token. Required (and non-empty) for - * [`AuthMode::Pat`]; ignored otherwise. + * Execute a SQL statement on the blocking (sync) path, but return a + * `CancellableExecution` handle so a concurrent JS task can cancel + * the query *while it is still running server-side*. + * + * `executeStatement` builds the kernel `Statement`, awaits the + * blocking `execute()`, and only then hands JS a `Statement` — so a + * query that runs for several seconds is uncancellable from JS on + * that path (there is no handle until the blocking call resolves). + * This method instead builds the statement, captures a detached + * `StatementCanceller` **before** dispatching `execute()`, and hands + * JS a `CancellableExecution` immediately. The caller drives the + * blocking execution via `result()` (resolves to the same + * `Statement` `executeStatement` returns) and can fire `cancel()` + * concurrently to interrupt a still-running query mid-COMPUTE. + * + * Option semantics are identical to `executeStatement`. + * Mirrors the pyo3 `Statement.canceller()` / `Statement.execute()` + * split (PR #121): obtain the canceller before the blocking drive. */ - token?: string + executeStatementCancellable(sql: string, options?: ExecuteOptions | undefined | null): Promise /** - * OAuth client id. Required for [`AuthMode::OAuthM2m`]; optional for - * [`AuthMode::OAuthU2m`] (defaults to `databricks-sql-connector`). + * 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`. */ - oauthClientId?: string - /** OAuth client secret. Required for [`AuthMode::OAuthM2m`]. */ - oauthClientSecret?: string + submitStatement(sql: string, options?: ExecuteOptions | undefined | null): Promise /** - * Localhost callback port for the [`AuthMode::OAuthU2m`] browser - * flow. Omitted ⇒ kernel default (8030). + * 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. */ - oauthRedirectPort?: number + close(): Promise /** - * OAuth scopes override (M2M / U2M). Omitted ⇒ kernel defaults - * (`["all-apis"]` for M2M; `["sql", "offline_access"]` for U2M). + * All catalogs visible to the session. + * + * JDBC `getCatalogs` shape: `TABLE_CAT: Utf8`. */ - oauthScopes?: Array + listCatalogs(): Promise /** - * OAuth token endpoint override. When omitted the kernel discovers the - * endpoint via the workspace's OIDC configuration. Set it to point the - * client-credentials / JWT client-assertion grant at the workspace's - * configured IdP token endpoint (e.g. Entra ID for Azure Databricks), - * which is required when that IdP — not Databricks-native OIDC — is the - * authority for [`AuthMode::OAuthM2m`] / [`AuthMode::OAuthM2mJwt`]. + * Schemas filtered by catalog (exact) and schema name pattern. + * + * JDBC `getSchemas` shape: `TABLE_SCHEM, TABLE_CATALOG`. */ - tokenUrl?: string + listSchemas(catalog?: string | undefined | null, schemaPattern?: string | undefined | null): Promise /** - * Path to the PEM private-key file. Required for - * [`AuthMode::OAuthM2mJwt`]. + * Tables filtered by catalog (**pattern**), schema (pattern), table + * (pattern). + * + * The catalog is an ODBC/JDBC LIKE pattern (`%` / `_`), matching + * Thrift `getTables`: a wildcard catalog matches multiple catalogs; + * a literal name (or an escaped `\_` / `\%`) takes the fast exact + * path. `undefined`/omitted catalog means "all catalogs", while an + * **empty string** means "match nothing" (zero rows) — pass + * `undefined`, not `""`, for all catalogs. The catalog pattern is + * validated (whitespace-only / NUL / >255 bytes are rejected with an + * error). + * + * JDBC `getTables` shape: 10 columns. `tableTypes`, when provided, + * filters rows by `TABLE_TYPE` kernel-side. + * + * `tableTypes` is an advisory filter. Databricks `SHOW TABLES` does + * NOT honour the table-type filter server-side; the kernel applies + * it client-side after the result returns. Callers expecting + * server-side rejection of off-type tables should not rely on this. */ - jwtKeyFile?: string - /** JWT key id (`kid`). Required for [`AuthMode::OAuthM2mJwt`]. */ - jwtKid?: string + listTables(catalog?: string | undefined | null, schemaPattern?: string | undefined | null, tablePattern?: string | undefined | null, tableTypes?: Array | undefined | null): Promise /** - * Passphrase for an encrypted PKCS#8 key. Optional for - * [`AuthMode::OAuthM2mJwt`] (omit for an unencrypted key). + * Columns of tables matching the filter. + * + * JDBC `getColumns` shape: 23 columns. */ - jwtPassphrase?: string + listColumns(catalog?: string | undefined | null, schemaPattern?: string | undefined | null, tablePattern?: string | undefined | null, columnPattern?: string | undefined | null): Promise /** - * JWT signing algorithm (`RS256`/`384`/`512`, `PS256`/`384`/`512`, - * `ES256`, `ES384`). Optional for [`AuthMode::OAuthM2mJwt`] - * (omitted ⇒ kernel default `RS256`). + * Functions visible to the session. `catalog` is exact; + * `schemaPattern` and `functionPattern` are SQL LIKE. */ - jwtAlgorithm?: string + listFunctions(catalog?: string | undefined | null, schemaPattern?: string | undefined | null, functionPattern?: string | undefined | null): Promise /** - * SP-wide Workload Identity Federation client id used during mandatory - * token exchange. Omitted selects BYOT / account-wide WIF. + * Procedures visible to the session. `catalog` is exact; + * `schemaPattern` and `procedurePattern` are SQL LIKE. */ - identityFederationClientId?: string + listProcedures(catalog?: string | undefined | null, schemaPattern?: string | undefined | null, procedurePattern?: string | undefined | null): Promise /** - * Azure Entra app-registration client id. Required for - * [`AuthMode::AzureSpM2m`]; ignored otherwise. + * All table types (`TABLE`, `VIEW`, `SYSTEM TABLE`, …). + * No wire call — static in-memory result. */ - azureClientId?: string + listTableTypes(): Promise /** - * Azure Entra app-registration client secret. Required for - * [`AuthMode::AzureSpM2m`]. + * SQL data types supported by the workspace. + * No wire call — static in-memory result. */ - azureClientSecret?: string + listTypeInfo(): Promise /** - * Azure Entra tenant id. Optional for [`AuthMode::AzureSpM2m`]; - * omitted ⇒ the kernel auto-discovers it from the workspace's - * `/aad/auth` redirect (matching the Thrift connector). + * Primary keys for the given table. All three identifiers are + * exact — ODBC `SQLPrimaryKeys` does not support patterns. */ - azureTenantId?: string + getPrimaryKeys(catalog: string, schema: string, table: string): Promise /** - * Azure workspace ARM resource id. Optional for [`AuthMode::AzureSpM2m`]; - * when set the kernel also sends the SP management token - * (`X-Databricks-Azure-SP-Management-Token`) + the - * `X-Databricks-Azure-Workspace-Resource-Id` header, so a service - * principal with an Azure RBAC role but no workspace membership can - * authenticate. Omitted ⇒ the data token authenticates alone. + * 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. */ - azureWorkspaceResourceId?: string + getCrossReference(parentCatalog: string | undefined | null, parentSchema: string | undefined | null, parentTable: string | undefined | null, foreignCatalog: string, foreignSchema: string, foreignTable: string): Promise +} + +/** + * Opaque executed-statement handle. + * + * **Current concurrency shape** — every method takes `inner.lock()` + * and holds the guard across the kernel `.await`. tokio `Mutex` is + * FIFO, so cancel/close queue behind any in-flight `fetchNextBatch` + * until it returns naturally. This is a known limitation that exists + * because the napi shape has not yet been split into an + * `Arc` (for cancel/close, which the + * kernel exposes as `&self`-callable) plus a `Mutex>` only + * for the borrowed-mut fetch path. The lock-shape refactor needs a + * small kernel-side accessor and lands in a follow-up PR — see + * `sea-workflow/jira-candidates/2026-05-24-napi-cancel-during-fetch.md`. + * + * `schema` and `statement_id` are cached at construction so they + * survive `close()` — JS callers building error reports against a + * disposed statement can still read them. + */ +export declare class Statement { /** - * Default catalog for statements executed on this session. - * Routed through the kernel's `DefaultOpts` and onto the SEA - * `CreateSession.catalog` wire field. + * 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. */ - catalog?: string + get statementId(): string /** - * Default schema for statements executed on this session. - * Routed through the kernel's `DefaultOpts` and onto the SEA - * `CreateSession.schema` wire field. + * 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`. */ - schema?: string + numModifiedRows(): Promise /** - * Server-bound session conf (Spark conf, `ANSI_MODE`, `TIMEZONE`, - * query-tag presets, …). Forwarded verbatim to SEA - * `session_confs`. Unknown keys are rejected server-side. + * 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. */ - sessionConf?: Record + displayMessage(): Promise /** - * Driver name reported in telemetry system configuration. Omitted ⇒ - * kernel default. + * 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`. */ - driverName?: string + diagnosticInfo(): Promise /** - * Driver version reported in telemetry system configuration. Omitted ⇒ - * kernel default. + * 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. */ - driverVersion?: string + errorDetailsJson(): Promise /** - * Runtime name reported in telemetry system configuration. Omitted ⇒ - * kernel default. + * 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. */ - runtimeName?: string + fetchNextBatch(): Promise /** - * Runtime version reported in telemetry system configuration. Omitted ⇒ - * kernel default. + * 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. */ - runtimeVersion?: string + schema(): ArrowSchema /** - * Runtime vendor reported in telemetry system configuration. Omitted ⇒ - * kernel default. + * 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. */ - runtimeVendor?: string + cancel(): Promise /** - * Operating system name reported in telemetry system configuration. - * Omitted ⇒ kernel default. + * 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). */ - osName?: string + close(): Promise +} + +/** + * A single Arrow IPC stream payload encoding one record batch (plus + * the schema header so the JS-side reader is stateless). + */ +export interface ArrowBatch { /** - * Operating system version reported in telemetry system configuration. - * Omitted ⇒ kernel default. + * Arrow IPC stream payload (schema header + 1 record-batch + * message). Decode with `apache-arrow`'s `RecordBatchReader`. */ - osVersion?: string + ipcBytes: Buffer +} + +/** + * An Arrow IPC stream payload encoding just the result schema (no + * record-batch messages). Returned by `Statement.schema()`. + */ +export interface ArrowSchema { /** - * Operating system architecture reported in telemetry system - * configuration. Omitted ⇒ kernel default. + * Arrow IPC stream payload (schema header only, no record-batch + * messages). Decode with `apache-arrow`'s `RecordBatchReader` — + * the reader will expose the schema and immediately end. */ - osArch?: string + ipcBytes: Buffer +} + +/** + * Authentication mode selector crossing the napi boundary. The string + * literals are what napi-rs emits from this `#[napi(string_enum)]` — the + * NodeJS SEA adapter (`KernelAuth`) matches them verbatim (`'Pat'`, + * `'OAuthM2m'`, `'OAuthU2m'`, `'AzureSpM2m'`). + * + * 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 const enum AuthMode { + /** Personal access token (`token`). */ + Pat = 'Pat', + /** OAuth 2.0 machine-to-machine — `oauthClientId` + `oauthClientSecret`. */ + OAuthM2m = 'OAuthM2m', /** - * Client application name reported in telemetry system configuration. - * Omitted ⇒ absent unless the kernel can infer a default. + * OAuth 2.0 machine-to-machine with a JWT private-key client + * assertion — `oauthClientId` + `jwtKeyFile` + `jwtKid`. */ - clientAppName?: string + OAuthM2mJwt = 'OAuthM2mJwt', /** - * Locale name reported in telemetry system configuration. Omitted ⇒ - * kernel default. + * OAuth 2.0 user-to-machine (browser flow) — optional `oauthClientId` + * + `oauthRedirectPort`. */ - localeName?: string + OAuthU2m = 'OAuthU2m', /** - * Character-set encoding reported in telemetry system configuration. - * Omitted ⇒ kernel default. - */ + * Azure Entra service-principal M2M — `azureClientId` + + * `azureClientSecret` (+ optional `azureTenantId` / + * `azureWorkspaceResourceId`). The kernel builds the Entra token + * endpoint + `.default` scope, auto-discovers the tenant from the + * workspace when `azureTenantId` is omitted, and — when + * `azureWorkspaceResourceId` is set — additionally sends the SP + * management token for an RBAC-only service principal. + */ + AzureSpM2m = 'AzureSpM2m' +} + +/** + * JS-visible options for opening a Databricks SQL session. + * + * Authentication is selected by `authMode` (default [`AuthMode::Pat`]): + * - `Pat` — `token` required. + * - `OAuthM2m` — `oauthClientId` + `oauthClientSecret` required. + * - `OAuthU2m` — `oauthClientId` / `oauthRedirectPort` optional + * (defaults to the `databricks-sql-connector` client on port 8030). + * - `AzureSpM2m` — `azureClientId` + `azureClientSecret` required + * (+ optional `azureTenantId` / `azureWorkspaceResourceId`). + * + * Catalog / schema / sessionConf are applied once at session creation + * and remain in effect for every statement run on the resulting + * `Connection`. The SEA wire protocol carries them on + * `CreateSession`, not on `ExecuteStatement` — so there is no + * per-statement override path on this binding. + */ +export interface ConnectionOptions { + /** + * 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 + /** + * Authentication mode. Omitted ⇒ [`AuthMode::Pat`] (back-compat: + * existing PAT callers pass only `token`). + */ + authMode?: AuthMode + /** + * Personal access token. Required (and non-empty) for + * [`AuthMode::Pat`]; ignored otherwise. + */ + token?: string + /** + * OAuth client id. Required for [`AuthMode::OAuthM2m`]; optional for + * [`AuthMode::OAuthU2m`] (defaults to `databricks-sql-connector`). + */ + oauthClientId?: string + /** OAuth client secret. Required for [`AuthMode::OAuthM2m`]. */ + oauthClientSecret?: string + /** + * 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). + */ + oauthScopes?: Array + /** + * OAuth token endpoint override. When omitted the kernel discovers the + * endpoint via the workspace's OIDC configuration. Set it to point the + * client-credentials / JWT client-assertion grant at the workspace's + * configured IdP token endpoint (e.g. Entra ID for Azure Databricks), + * which is required when that IdP — not Databricks-native OIDC — is the + * authority for [`AuthMode::OAuthM2m`] / [`AuthMode::OAuthM2mJwt`]. + */ + tokenUrl?: string + /** + * Path to the PEM private-key file. Required for + * [`AuthMode::OAuthM2mJwt`]. + */ + jwtKeyFile?: string + /** JWT key id (`kid`). Required for [`AuthMode::OAuthM2mJwt`]. */ + jwtKid?: string + /** + * Passphrase for an encrypted PKCS#8 key. Optional for + * [`AuthMode::OAuthM2mJwt`] (omit for an unencrypted key). + */ + jwtPassphrase?: string + /** + * JWT signing algorithm (`RS256`/`384`/`512`, `PS256`/`384`/`512`, + * `ES256`, `ES384`). Optional for [`AuthMode::OAuthM2mJwt`] + * (omitted ⇒ kernel default `RS256`). + */ + jwtAlgorithm?: string + /** + * SP-wide Workload Identity Federation client id used during mandatory + * token exchange. Omitted selects BYOT / account-wide WIF. + */ + identityFederationClientId?: string + /** + * Azure Entra app-registration client id. Required for + * [`AuthMode::AzureSpM2m`]; ignored otherwise. + */ + azureClientId?: string + /** + * Azure Entra app-registration client secret. Required for + * [`AuthMode::AzureSpM2m`]. + */ + azureClientSecret?: string + /** + * Azure Entra tenant id. Optional for [`AuthMode::AzureSpM2m`]; + * omitted ⇒ the kernel auto-discovers it from the workspace's + * `/aad/auth` redirect (matching the Thrift connector). + */ + azureTenantId?: string + /** + * Azure workspace ARM resource id. Optional for [`AuthMode::AzureSpM2m`]; + * when set the kernel also sends the SP management token + * (`X-Databricks-Azure-SP-Management-Token`) + the + * `X-Databricks-Azure-Workspace-Resource-Id` header, so a service + * principal with an Azure RBAC role but no workspace membership can + * authenticate. Omitted ⇒ the data token authenticates alone. + */ + azureWorkspaceResourceId?: string + /** + * Default catalog for statements executed on this session. + * Routed through the kernel's `DefaultOpts` and onto the SEA + * `CreateSession.catalog` wire field. + */ + catalog?: string + /** + * Default schema for statements executed on this session. + * Routed through the kernel's `DefaultOpts` and onto the SEA + * `CreateSession.schema` wire field. + */ + schema?: string + /** + * Server-bound session conf (Spark conf, `ANSI_MODE`, `TIMEZONE`, + * query-tag presets, …). Forwarded verbatim to SEA + * `session_confs`. Unknown keys are rejected server-side. + */ + sessionConf?: Record + /** + * Driver name reported in telemetry system configuration. Omitted ⇒ + * kernel default. + */ + driverName?: string + /** + * Driver version reported in telemetry system configuration. Omitted ⇒ + * kernel default. + */ + driverVersion?: string + /** + * Runtime name reported in telemetry system configuration. Omitted ⇒ + * kernel default. + */ + runtimeName?: string + /** + * Runtime version reported in telemetry system configuration. Omitted ⇒ + * kernel default. + */ + runtimeVersion?: string + /** + * Runtime vendor reported in telemetry system configuration. Omitted ⇒ + * kernel default. + */ + runtimeVendor?: string + /** + * Operating system name reported in telemetry system configuration. + * Omitted ⇒ kernel default. + */ + osName?: string + /** + * Operating system version reported in telemetry system configuration. + * Omitted ⇒ kernel default. + */ + osVersion?: string + /** + * Operating system architecture reported in telemetry system + * configuration. Omitted ⇒ kernel default. + */ + osArch?: string + /** + * Client application name reported in telemetry system configuration. + * Omitted ⇒ absent unless the kernel can infer a default. + */ + clientAppName?: string + /** + * Locale name reported in telemetry system configuration. Omitted ⇒ + * kernel default. + */ + localeName?: string + /** + * Character-set encoding reported in telemetry system configuration. + * Omitted ⇒ kernel default. + */ charSetEncoding?: string /** * Process name reported in telemetry system configuration. Omitted ⇒ @@ -568,44 +1016,130 @@ export interface ConnectionOptions { */ 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`]. + * Per-statement options for `Connection.executeStatement`. * - * 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. + * 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. * - * `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`. + * `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`.) * - * 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). + * **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 function initKernelLogging(callback: (err: Error | null, arg: Array) => any, level: string): void -/** - * Snapshot of the bridge's runtime state for observability. - * +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 + /** + * 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`]. + */ + positionalParams?: Array + /** + * 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`). + */ + 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 @@ -618,16 +1152,68 @@ export interface KernelLoggingStats { installed: boolean dropped: number } + /** - * Return the bridge's [`KernelLoggingStats`]. Safe to call before - * `initKernelLogging` (reports `installed: false`, `dropped: 0`). + * 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 declare function kernelLoggingStats(): KernelLoggingStats +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. * @@ -661,29 +1247,7 @@ export interface TypedValueInput { */ value?: string } -/** - * A single Arrow IPC stream payload encoding one record batch (plus - * the schema header so the JS-side reader is stateless). - */ -export interface ArrowBatch { - /** - * Arrow IPC stream payload (schema header + 1 record-batch - * message). Decode with `apache-arrow`'s `RecordBatchReader`. - */ - ipcBytes: Buffer -} -/** - * An Arrow IPC stream payload encoding just the result schema (no - * record-batch messages). Returned by `Statement.schema()`. - */ -export interface ArrowSchema { - /** - * Arrow IPC stream payload (schema header only, no record-batch - * messages). Decode with `apache-arrow`'s `RecordBatchReader` — - * the reader will expose the schema and immediately end. - */ - ipcBytes: Buffer -} + /** * Returns the native binding's crate version (`CARGO_PKG_VERSION`). * @@ -691,540 +1255,3 @@ export interface ArrowSchema { * loaded?" probe for the JS-side loader's structured diagnostics. */ export declare function version(): string -/** - * Opaque async-statement handle. - * - * 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()`. - * - * 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 declare class AsyncStatement { - /** - * 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 - /** - * 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. - */ - status(): Promise - /** Rows modified by the statement (UPDATE / INSERT / DELETE / MERGE). */ - numModifiedRows(): Promise - /** - * Server-supplied user-facing message (may contain SQL fragments — - * redact before centralised logging). - */ - displayMessage(): Promise - /** Server-supplied diagnostic detail. */ - diagnosticInfo(): Promise - /** Server-supplied structured error detail (JSON), when enabled. */ - errorDetailsJson(): Promise - /** - * 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. - */ - awaitResult(): Promise - /** - * 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`. - */ - cancel(): Promise - /** - * Explicit close. Idempotent — a second call on an - * already-closed handle returns `Ok(())`. On `Err`, the napi - * inner is already `None`, so a JS-side retry sees the - * closed-handle short-circuit and returns `Ok(())` without - * re-attempting the wire call. The kernel's own `Drop` - * fire-and-forget retry runs once in the background. - */ - close(): Promise -} -/** - * Opaque result-fetch handle returned by - * `AsyncStatement.awaitResult()`. Wraps a kernel `ResultStream` - * directly; structurally analogous to the sync `Statement`'s - * fetch-side surface (`fetchNextBatch` / `schema` / - * `statementId`). - * - * `cancel()` / `close()` are not exposed: the parent - * `AsyncStatement` owns server-side lifecycle. A `close()` here - * would create dual-ownership of the same statement_id with - * inconsistent close semantics. Callers `close()` the parent - * `AsyncStatement` after they're done fetching. - * - * Schema is cached at construction so it survives the underlying - * stream being drained; mirrors the sync `Statement.schema()` - * post-close contract. - */ -export declare class AsyncResultHandle { - /** - * Server-issued statement id. Cached at construction; readable - * for log correlation. Matches the parent `AsyncStatement`'s - * `statementId`. - */ - get statementId(): string - /** - * Pull the next batch of results. Returns `null` when the - * stream is exhausted. The returned `ArrowBatch.ipcBytes` is a - * complete Arrow IPC stream (schema header + 1 record-batch - * message), suitable for handing to `apache-arrow`'s - * `RecordBatchReader`. Byte-identical to the sync - * `Statement.fetchNextBatch()` payload for the same query. - */ - fetchNextBatch(): Promise - /** - * Result schema as an Arrow IPC payload (schema header only, - * no record-batch message). Available before any batches have - * been fetched. Sync because the body has no `.await` — - * `encode_ipc_stream` is pure CPU work over the cached - * `Arc`. - */ - schema(): ArrowSchema -} -/** - * 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 { - /** - * 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. - */ - get statementId(): string | null - /** - * Drive the blocking `execute()` and resolve to a `Statement` - * (identical to what `executeStatement` returns) once the kernel - * reaches a terminal state and the result stream is ready. - * - * Consumes the pending statement: a second `result()` call returns - * `KernelError(InvalidStatementHandle)`. The future is - * drop-cancel-safe — the kernel's per-execute `MidExecuteCancelState` - * guard fires a fire-and-forget `cancel_statement` if this future is - * dropped mid-flight (`Promise.race` / timeout loser), independently - * of an explicit `cancel()`. - * - * On a server-side cancel the kernel's blocking `execute()` currently - * surfaces `InvalidArgument` (a known kernel quirk — the async path - * returns `Cancelled`). When this handle's `cancel()` actually dispatched a - * server-side cancel, we normalise that into `Cancelled` here so JS callers - * can rely on a single cancelled-status code regardless of execution path. - * - * Three outcomes can race the blocking drive: (1) a natural terminal state - * → `Ok` or the genuine error; (2) an explicit `cancel()` that dispatched a - * server cancel → this `result()` rejects with a `Cancelled`-coded error - * (the normalisation above); (3) the future being **dropped** mid-flight - * (`Promise.race`/timeout loser) → the kernel's `MidExecuteCancelState` - * drop-guard fires a fire-and-forget `cancel_statement`, but there is no - * `result()` left to observe a code. Only (2) yields a `Cancelled` error. - */ - result(): Promise - /** - * Server-side cancel of the in-flight statement. - * - * Lock-free: fires the detached `StatementCanceller` captured at - * construction rather than taking the mutex `result()` holds, so it - * interrupts a still-running blocking `result()` instead of queueing - * behind it. No-op (returns `Ok`) if `result()` already finished - * successfully, or if no statement id has been observed yet (query still - * in its initial submit round-trip), and idempotent against a server - * already in a terminal state. - */ - cancel(): Promise -} -/** - * 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 { - /** - * Server-issued session id. Cached at construction; readable - * even after `close()` so JS-side log lines can correlate - * against kernel / server logs which key on the same id. - */ - get sessionId(): string - /** - * Execute a SQL statement and return a Statement handle that - * streams batches via `fetchNextBatch()`. - * - * Catalog / schema / sessionConf are session-level - * (`openSession`). Per-statement options on `ExecuteOptions`: - * - `statementConf` — per-statement Spark conf overlay - * - `queryTags` — serialised to a comma-separated `key:value` - * string and placed in `statement_conf["query_tags"]`, - * matching NodeJS Thrift's `serializeQueryTags` wire shape - * - * `options` is omitted/`None` for the no-options path; passing - * `{ statementConf: {} }` (an empty map) is treated the same as - * omission to keep the wire shape stable for the common case. - */ - executeStatement(sql: string, options?: ExecuteOptions | undefined | null): Promise - /** - * directResults execute — the Thrift/JDBC model. Sends ExecuteStatement - * with no `wait_timeout` field (server applies its ~10s default inline wait - * and auto-closes on success) and returns WITHOUT polling past it: - * - * - a **`Statement`** (left arm) when the query finished within the inline - * wait — terminal, result ready inline, `close()` is a clean release; - * - an **`AsyncStatement`** (right arm) when it did not — a poll/cancel - * handle the caller drives (`status()` / `awaitResult()` / `cancel()`). - * - * JS distinguishes the arms by feature-detecting `awaitResult` (present - * only on `AsyncStatement`). This is the path that gives mid-run cancel for - * long queries WITHOUT the eager-handle / close-drives workaround: the - * returned handle always corresponds to a server-owned statement. - * - * **Load-bearing contract:** the kernel's `DirectStatement::{Completed, - * Running}` discriminant cannot ride on these opaque `#[napi]` classes, so - * consumers MUST feature-detect via `awaitResult` (the only member unique to - * `AsyncStatement`). `Statement` (the Completed arm) MUST NOT gain an - * `awaitResult` member, or every consumer silently misroutes. The pyo3 - * binding makes the same `await_result`-probe assumption. - */ - executeStatementDirect(sql: string, options?: ExecuteOptions | undefined | null): Promise - /** - * Execute a SQL statement on the blocking (sync) path, but return a - * `CancellableExecution` handle so a concurrent JS task can cancel - * the query *while it is still running server-side*. - * - * `executeStatement` builds the kernel `Statement`, awaits the - * blocking `execute()`, and only then hands JS a `Statement` — so a - * query that runs for several seconds is uncancellable from JS on - * that path (there is no handle until the blocking call resolves). - * This method instead builds the statement, captures a detached - * `StatementCanceller` **before** dispatching `execute()`, and hands - * JS a `CancellableExecution` immediately. The caller drives the - * blocking execution via `result()` (resolves to the same - * `Statement` `executeStatement` returns) and can fire `cancel()` - * concurrently to interrupt a still-running query mid-COMPUTE. - * - * Option semantics are identical to `executeStatement`. - * Mirrors the pyo3 `Statement.canceller()` / `Statement.execute()` - * split (PR #121): obtain the canceller before the blocking drive. - */ - executeStatementCancellable(sql: string, options?: ExecuteOptions | undefined | null): Promise - /** - * Submit a SQL statement and return immediately with an - * `AsyncStatement` handle, without blocking until the query - * finishes. The kernel's `Statement::submit()` sends - * `wait_timeout=0s`, so the server responds as soon as it has a - * `statement_id` (state `Pending`/`Running`); JS drives polling - * via `AsyncStatement.status()` and materialises results with - * `AsyncStatement.awaitResult()`. - * - * This is the async-execution path the Thrift backend always - * uses (`runAsync: true`): the SEA backend submits, returns a - * pending operation handle, and polls to terminal during - * fetch. Option semantics (statementConf / queryTags / - * rowLimit / positional + named params) match `executeStatement`. - * Submit always sends `wait_timeout=0s` so the call returns - * immediately; the caller drives completion via `status()` / - * `awaitResult()`. Only the blocking-vs-pending return contract - * differs from `executeStatement`. - */ - submitStatement(sql: string, options?: ExecuteOptions | undefined | null): Promise - /** - * Explicit close. Awaits the server-side `DeleteSession` so the - * JS caller can observe failures (auth revoked mid-session, - * warehouse stopped, network error). Idempotent — a second call - * on an already-closed connection returns `Ok`. - * - * **Errors are terminal from the JS side.** The kernel session - * handle is consumed (`take()`) BEFORE the wire `DeleteSession` - * runs, because `Session::close` takes `self` by value. On `Err`, - * the napi `inner` is already `None`, so a JS-side retry sees a - * closed connection and returns `Ok(())` without re-attempting - * the wire call. The kernel's own `Drop` fire-and-forget retry - * runs once in the background — the JS caller can log the error - * but cannot drive a retry. If you need retry-on-failure - * semantics for `DeleteSession`, layer them above this method. - */ - close(): Promise - /** - * All catalogs visible to the session. - * - * JDBC `getCatalogs` shape: `TABLE_CAT: Utf8`. - */ - listCatalogs(): Promise - /** - * Schemas filtered by catalog (exact) and schema name pattern. - * - * JDBC `getSchemas` shape: `TABLE_SCHEM, TABLE_CATALOG`. - */ - listSchemas(catalog?: string | undefined | null, schemaPattern?: string | undefined | null): Promise - /** - * Tables filtered by catalog (exact), schema (pattern), table (pattern). - * - * 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. - */ - listTables(catalog?: string | undefined | null, schemaPattern?: string | undefined | null, tablePattern?: string | undefined | null, tableTypes?: Array | undefined | null): Promise - /** - * Columns of tables matching the filter. - * - * 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. - */ - listFunctions(catalog?: string | undefined | null, schemaPattern?: string | undefined | null, functionPattern?: string | undefined | null): Promise - /** - * Procedures visible to the session. `catalog` is exact; - * `schemaPattern` and `procedurePattern` are SQL LIKE. - */ - listProcedures(catalog?: string | undefined | null, schemaPattern?: string | undefined | null, procedurePattern?: string | undefined | null): Promise - /** - * All table types (`TABLE`, `VIEW`, `SYSTEM TABLE`, …). - * No wire call — static in-memory result. - */ - listTableTypes(): Promise - /** - * SQL data types supported by the workspace. - * No wire call — static in-memory result. - */ - listTypeInfo(): Promise - /** - * Primary keys for the given table. All three identifiers are - * exact — ODBC `SQLPrimaryKeys` does not support patterns. - */ - getPrimaryKeys(catalog: string, schema: string, table: string): Promise - /** - * Foreign-key relationships. The foreign side must be fully - * specified (catalog + schema + table); the parent side is - * optional. All identifiers are exact — no LIKE patterns. - */ - getCrossReference(parentCatalog: string | undefined | null, parentSchema: string | undefined | null, parentTable: string | undefined | null, foreignCatalog: string, foreignSchema: string, foreignTable: string): Promise -} -/** - * Opaque executed-statement handle. - * - * **Current concurrency shape** — every method takes `inner.lock()` - * and holds the guard across the kernel `.await`. tokio `Mutex` is - * FIFO, so cancel/close queue behind any in-flight `fetchNextBatch` - * until it returns naturally. This is a known limitation that exists - * because the napi shape has not yet been split into an - * `Arc` (for cancel/close, which the - * kernel exposes as `&self`-callable) plus a `Mutex>` only - * for the borrowed-mut fetch path. The lock-shape refactor needs a - * small kernel-side accessor and lands in a follow-up PR — see - * `sea-workflow/jira-candidates/2026-05-24-napi-cancel-during-fetch.md`. - * - * `schema` and `statement_id` are cached at construction so they - * survive `close()` — JS callers building error reports against a - * disposed statement can still read them. - */ -export declare class Statement { - /** - * 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 - /** - * Number of rows modified by the statement (UPDATE / INSERT / - * DELETE / MERGE). `null` for SELECT and on warehouses that don't - * surface the counter. Mirrors Thrift's - * `TGetOperationStatusResp.numModifiedRows`. - */ - numModifiedRows(): Promise - /** - * Server-supplied user-facing message. Mirrors Thrift's - * `TGetOperationStatusResp.displayMessage`. **PII / sensitive- - * data note:** may contain SQL fragments or parameter values — - * redact before centralised logging. - * - * Populated on `Succeeded` / `Closed` paths (incl. an empty `Closed`). - * On terminal-error states (`Failed` / `Cancelled`) the kernel returns - * an Error instead of a `Statement`, and the same field rides on the JS - * Error envelope under the same `displayMessage` key. - */ - displayMessage(): Promise - /** - * Server-supplied diagnostic detail — multi-line operator / - * stack context. Mirrors Thrift's - * `TGetOperationStatusResp.diagnosticInfo`. For support surfaces, - * not user-facing. Same reachability + PII caveats as - * `displayMessage`. - */ - diagnosticInfo(): Promise - /** - * Server-supplied JSON blob with extended error details. Mirrors - * Thrift's `TGetOperationStatusResp.errorDetailsJson`. - * Pass-through string — JS callers parse with `JSON.parse` if - * they need structured access. - * - * **Server-side gating:** populated only when the workspace has - * `spark.databricks.sql.errorDetailsJson.enabled = true` on the - * underlying SQL cluster. The flag is internal-only / default- - * false in the Databricks runtime, so for most JS callers this - * will return `null`. Admin-enabled workspaces return content - * shaped like `{"errorClass": "...", "messageTemplate": "..."}`. - * - * **Unbounded:** when populated, server can return a multi-MB - * blob; size before logging. - */ - errorDetailsJson(): Promise - /** - * Pull the next batch of results. Returns `null` when the stream - * is exhausted. The returned `ArrowBatch.ipcBytes` is a complete - * Arrow IPC stream (schema header + 1 record-batch message) - * suitable for handing to `apache-arrow`'s `RecordBatchReader`. - * - * On `Err`, the stream is in an unspecified state — call - * `close()` and discard the `Statement`. Subsequent - * `fetchNextBatch()` calls after an error are not guaranteed to - * succeed or fail consistently. - */ - fetchNextBatch(): Promise - /** - * Result schema as an Arrow IPC payload (schema header only, no - * record-batch message). Available before any batches have been - * fetched, and remains available after `close()` — the kernel - * materialises the schema eagerly so JS callers can build error - * reports against a disposed statement. - * - * Sync because the body has no `.await` — `encode_ipc_stream` is - * pure CPU work over an `Arc` already cached on the - * wrapper. Mirrors `pyo3/src/statement.rs::arrow_schema` (sync). - * napi-rs converts a panic in a sync `#[napi]` entry point into a - * thrown JS error via its own macro-expanded boundary, so the - * `util::guarded` `catch_unwind` wrapper that the `async fn` - * entry points use is not required for this method. - */ - schema(): ArrowSchema - /** - * Server-side cancel. - * - * For executed statements: short-circuits to `Ok(())` if - * `fetchNextBatch` has already returned `null` (stream - * naturally exhausted) — matches the JDBC `Statement.cancel()` - * no-op-after-completion contract, so JS callers can fire cancel - * defensively without distinguishing "real cancel" from "raced - * with natural completion." - * - * For metadata streams: no-op (the kernel has no in-flight - * cancellation surface for metadata calls today). - * - * Returns `KernelError(InvalidStatementHandle)` if the statement - * has been explicitly `close()`d. - */ - cancel(): Promise - /** - * Explicit close. - * - * For executed statements: awaits the server-side `CloseStatement` - * so the JS caller can observe failures (auth revoked mid-session, - * network error, server-side error). Idempotent — a second call - * on an already-closed statement returns `Ok`. - * - * **Errors are terminal from the JS side.** The kernel executed - * handle is taken out of `inner` BEFORE the wire `CloseStatement` - * runs (so `Drop` knows there's nothing left to clean up). On - * `Err`, the napi `inner` is already `None`, so a JS-side retry - * sees a closed statement and returns `Ok(())` without re- - * attempting the wire call. The kernel-level `ExecutedStatement` - * has been consumed at that point and the value is dropped on - * the way out of the closure — the kernel's `ExecutedStatement:: - * Drop` then fires-and-forgets a single retry on the captured - * runtime. The JS caller can log the error but cannot drive a - * further retry. If you need retry-on-failure semantics for - * `CloseStatement`, layer them above this method. - * - * For metadata streams: drops the stream (no server round-trip - * needed — metadata results have no in-flight server-side - * resource to release). - */ - close(): Promise -} 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