Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,9 @@ Dockerfile* text
#
.gitattributes export-ignore
.gitignore export-ignore

# napi-rs auto-generates this file from the kernel's `napi-binding/napi/`
# crate; regenerated by `npm run build:native`. Tell git/GitHub it's
# machine-generated so it collapses in diffs and is excluded from
# blame and language stats.
native/sea/index.d.ts linguist-generated=true
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,10 @@ coverage_unit
dist
*.DS_Store
lib/version.ts

# SEA native binding — copied/generated from kernel workspace by `npm run build:native`.
# The committed contract is `native/sea/index.d.ts` (TypeScript declarations).
# Everything else under native/sea/ is a build artifact and must not be committed.
native/sea/index.js
native/sea/index.node
native/sea/index.*.node
7 changes: 7 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
!dist/**/*
!thrift/**/*

# SEA napi-rs router shim + TypeScript declarations. The router (index.js)
# selects the per-platform `.node` artifact from `@databricks/sea-native-*`
# optionalDependencies (populated when the kernel CI publishes them);
# the .d.ts is the consumer-facing type contract.
!native/sea/index.js
!native/sea/index.d.ts

!LICENSE
!NOTICE
!package.json
Expand Down
6 changes: 6 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,9 @@ coverage
dist
thrift
package-lock.json

# Generated by napi-rs from the kernel's `napi-binding/napi/` crate;
# regenerated by `npm run build:native`. Format follows napi-rs's
# defaults (no semicolons), not this repo's prettier config.
native/sea/index.d.ts
native/sea/index.js
117 changes: 117 additions & 0 deletions lib/sea/SeaNativeLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Copyright (c) 2026 Databricks, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
* Lazy loader for the SEA (Statement Execution API) native binding.
*
* Mirrors the load-failure-tolerant pattern of `lib/utils/lz4.ts`: the
* `.node` artifact ships via per-platform optional dependencies
* (`@databricks/sea-native-<triple>`), so its absence must not crash
* a Thrift-only consumer of the driver. Callers that actually need
* SEA invoke `getSeaNative()`, which throws a structured error if
* the binding could not be loaded.
*/

import type {
Connection as NativeConnection,
Statement as NativeStatement,
ConnectionOptions,
ExecuteOptions,
ArrowBatch,
ArrowSchema,
} from '@sea-native';

export type { ConnectionOptions, ExecuteOptions, ArrowBatch, ArrowSchema };
export type Connection = NativeConnection;
export type Statement = NativeStatement;

export interface SeaNativeBinding {
version(): string;
openSession(options: ConnectionOptions): Promise<NativeConnection>;
Connection: typeof NativeConnection;
Statement: typeof NativeStatement;
}

const MIN_NODE_MAJOR = 18;

function detectNodeMajor(): number {
// `process.version` is `vX.Y.Z`; parseInt stops at the first non-digit.
return parseInt(process.version.slice(1), 10);
}

function loadFailureHint(err: NodeJS.ErrnoException): string {
const platform = `${process.platform}-${process.arch}`;
const installHint = `Install the matching optional dependency (e.g. @databricks/sea-native-${platform}).`;
if (err.code === 'MODULE_NOT_FOUND') {
return `SEA native binding not installed for platform ${platform} on Node ${process.version}. ${installHint}`;
}
if (err.code === 'ERR_DLOPEN_FAILED') {
return `SEA native binding present but failed to dlopen on platform ${platform} / Node ${process.version} — likely a libc or Node ABI mismatch. The binding requires Node >=${MIN_NODE_MAJOR}.`;
}
return `SEA native binding failed to load on platform ${platform} / Node ${process.version}: ${err.message}`;
}

let cached: SeaNativeBinding | null | undefined;
let cachedError: Error | undefined;

function tryLoad(): SeaNativeBinding | undefined {
const nodeMajor = detectNodeMajor();
if (Number.isFinite(nodeMajor) && nodeMajor < MIN_NODE_MAJOR) {
cachedError = new Error(
`SEA native binding requires Node >=${MIN_NODE_MAJOR}; running Node ${process.version}. Continue using the Thrift backend on this runtime.`,
);
return undefined;
}

try {
// The require path resolves to `native/sea/index.js` (the napi-rs
// router). `.js` is omitted so eslint's `import/extensions` rule
// accepts the call.
// eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
return require('../../native/sea') as SeaNativeBinding;
} catch (err) {
if (err instanceof Error && 'code' in err) {
cachedError = new Error(loadFailureHint(err as NodeJS.ErrnoException));
return undefined;
}
cachedError = new Error(`SEA native binding failed to load with non-standard error: ${String(err)}`);
return undefined;
}
}

/**
* Returns the loaded native binding. Throws a structured error if
* the binding is unavailable on this platform / Node version.
*/
export function getSeaNative(): SeaNativeBinding {
if (cached === undefined) {
cached = tryLoad() ?? null;
}
if (cached === null) {
throw cachedError ?? new Error('SEA native binding unavailable');
}
return cached;
}

/**
* Returns the loaded binding or `undefined` if it could not be
* loaded. Use this for capability-detection at startup; use
* `getSeaNative()` at the point where SEA is actually required.
*/
export function tryGetSeaNative(): SeaNativeBinding | undefined {
if (cached === undefined) {
cached = tryLoad() ?? null;
}
return cached ?? undefined;
}
62 changes: 62 additions & 0 deletions native/sea/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# `native/sea/` — consumer-side directory for the Rust napi binding

**The Rust binding source lives in the kernel repo** at
`databricks-sql-kernel/napi-binding/napi/`. Building it requires a
local checkout of that repo — see "Build for local dev" below.

## Workspace topology

The napi crate is a **standalone Cargo workspace** (`[workspace]
members = ["."]` in `napi-binding/napi/Cargo.toml`), **not** a
sibling of `pyo3/` in the kernel root workspace.

The reason is Cargo feature unification. pyo3 builds the kernel with
the default `tls-native` feature (system OpenSSL via `native-tls`).
The napi crate has to opt INTO `tls-rustls` instead: napi modules are
loaded into Node.js processes that statically link OpenSSL 3.x, and
dynamically linking the system's OpenSSL 1.1 (which `native-tls`
pulls in on Linux) collides with Node's symbols at module-load time
and segfaults the process before any Rust code runs. `rustls` is
pure Rust + `ring` and avoids the conflict entirely.

If napi lived in the same workspace as pyo3, `cargo build
--workspace` would unify the kernel's feature set to `tls-native ∪
tls-rustls`, link both TLS stacks into the resulting napi cdylib,
and reintroduce the same clash. Standalone-workspace is the fix.

## What lives in this directory

- `index.d.ts` — TypeScript declarations consumed by `lib/sea/`.
Generated by napi-rs from the Rust source; checked in as the
consumer-facing type contract.
- `index.js` — napi-rs's per-platform router shim. Gitignored;
populated by `npm run build:native` for local dev. In published
tarballs it ships alongside the `.d.ts` and `require()`s the
right `@databricks/sea-native-<triple>` optional dependency.
- `index.*.node` — the actual native binary, one per platform.
Gitignored. In production these live in the per-triple optional
dependencies (`@databricks/sea-native-linux-x64-gnu`, etc.); for
local dev `npm run build:native` copies one into this directory.

## Build for local dev

```bash
# From the nodejs repo root:
export DATABRICKS_SQL_KERNEL_REPO=/path/to/your/databricks-sql-kernel/napi-binding
npm run build:native # release build (default)
BUILD_PROFILE= npm run build:native # debug build (empty BUILD_PROFILE drops --release)
```

`DATABRICKS_SQL_KERNEL_REPO` is required when your kernel checkout
isn't at `../../databricks-sql-kernel/napi-binding` relative to the
nodejs repo.

## Production load path

At release time the kernel's CI publishes
`@databricks/sea-native-<triple>` npm packages — one per supported
platform — each containing a single `.node` binary. The nodejs
driver lists them as `optionalDependencies`; npm installs only the
one matching the consumer's `process.platform` / `process.arch`.
`native/sea/index.js` (the napi-rs router) then `require()`s the
installed package at load time.
144 changes: 144 additions & 0 deletions native/sea/index.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"test": "nyc --report-dir=${NYC_REPORT_DIR:-coverage_unit} mocha --config tests/unit/.mocharc.js",
"update-version": "node bin/update-version.js && prettier --write ./lib/version.ts",
"build": "npm run update-version && tsc --project tsconfig.build.json",
"build:native": "bash -c 'cd ${DATABRICKS_SQL_KERNEL_REPO:-../../databricks-sql-kernel/napi-binding}/napi && npx --no-install @napi-rs/cli build --platform ${BUILD_PROFILE:---release} && cp index.* $OLDPWD/native/sea/'",
"watch": "tsc --project tsconfig.build.json --watch",
"type-check": "tsc --noEmit",
"prettier": "prettier . --check",
Expand Down Expand Up @@ -47,6 +48,7 @@
],
"license": "Apache 2.0",
"devDependencies": {
"@napi-rs/cli": "2.18.4",
"@types/chai": "^4.3.14",
"@types/http-proxy": "^1.17.14",
"@types/lz4": "^0.6.4",
Expand Down Expand Up @@ -89,6 +91,7 @@
"winston": "^3.8.2"
},
"optionalDependencies": {
"lz4": "^0.6.5"
"lz4": "^0.6.5",
"@databricks/sea-native-linux-x64-gnu": "0.1.0"
}
}
Loading
Loading