|
13 | 13 | // limitations under the License. |
14 | 14 |
|
15 | 15 | /** |
16 | | - * Loader for the SEA (Statement Execution API) native binding. |
| 16 | + * Lazy loader for the SEA (Statement Execution API) native binding. |
17 | 17 | * |
18 | | - * Round 1b: minimal pass-through to the napi-rs auto-generated |
19 | | - * `index.js` shim in `native/sea/`. The shim itself picks the right |
20 | | - * per-platform `.node` artifact (linux-x64-gnu today; more triples in |
21 | | - * the bundling feature). |
22 | | - * |
23 | | - * Round 2+ will extend this with: lazy require to defer the `.node` |
24 | | - * load until the first SEA call, structured load-error diagnostics |
25 | | - * (which platform/arch was attempted, whether the package was |
26 | | - * installed at all), and a JS-side `DBSQLLogger` install path that |
27 | | - * forwards to the binding's `installLogger()` once that surface lands. |
| 18 | + * Mirrors the load-failure-tolerant pattern of `lib/utils/lz4.ts`: the |
| 19 | + * `.node` artifact ships via per-platform optional dependencies |
| 20 | + * (`@databricks/sea-native-<triple>`), so its absence must not crash |
| 21 | + * a Thrift-only consumer of the driver. Callers that actually need |
| 22 | + * SEA invoke `getSeaNative()`, which throws a structured error if |
| 23 | + * the binding could not be loaded. |
28 | 24 | */ |
29 | 25 |
|
30 | | -// The path is relative to this file at runtime (`dist/sea/SeaNativeLoader.js`) |
31 | | -// resolving to `dist/sea/../../native/sea/index.js` once `tsc` has emitted |
32 | | -// to `dist/`. We use a require-time path resolution because the napi |
33 | | -// shim is plain CommonJS and not part of the TS source tree. |
34 | | -// |
35 | | -// eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-dynamic-require, global-require |
36 | | -const native = require('../../native/sea/index.js'); |
| 26 | +import type { |
| 27 | + Connection as NativeConnection, |
| 28 | + Statement as NativeStatement, |
| 29 | + ConnectionOptions, |
| 30 | + ExecuteOptions, |
| 31 | + ArrowBatch, |
| 32 | + ArrowSchema, |
| 33 | +} from '@sea-native'; |
| 34 | + |
| 35 | +export type { ConnectionOptions, ExecuteOptions, ArrowBatch, ArrowSchema }; |
| 36 | +export type Connection = NativeConnection; |
| 37 | +export type Statement = NativeStatement; |
37 | 38 |
|
38 | | -/** |
39 | | - * Public surface of the native binding exposed to the rest of the |
40 | | - * NodeJS driver. Round 2 lands `openSession` + opaque `Connection` / |
41 | | - * `Statement` classes (the binding-generated `.d.ts` is the source of |
42 | | - * truth for their method signatures — see `native/sea/index.d.ts`). |
43 | | - * |
44 | | - * We deliberately keep this typed loosely (`unknown` for the class |
45 | | - * shapes) so the loader layer doesn't have to import the binding's |
46 | | - * generated types and the JS adapter layer can introduce its own |
47 | | - * higher-level wrappers without conflicting with the binding's TS |
48 | | - * declarations. |
49 | | - */ |
50 | 39 | export interface SeaNativeBinding { |
51 | | - /** Returns the native crate version (smoke test for the binding's load path). */ |
52 | 40 | version(): string; |
53 | | - /** Open a session over PAT auth. Returns an opaque Connection. */ |
54 | | - openSession(opts: { |
55 | | - hostName: string; |
56 | | - httpPath: string; |
57 | | - token: string; |
58 | | - }): Promise<unknown>; |
59 | | - /** Opaque Connection class — instance methods on the binding-generated d.ts. */ |
60 | | - Connection: Function; |
61 | | - /** Opaque Statement class — instance methods on the binding-generated d.ts. */ |
62 | | - Statement: Function; |
| 41 | + openSession(options: ConnectionOptions): Promise<NativeConnection>; |
| 42 | + Connection: typeof NativeConnection; |
| 43 | + Statement: typeof NativeStatement; |
| 44 | +} |
| 45 | + |
| 46 | +const MIN_NODE_MAJOR = 18; |
| 47 | + |
| 48 | +function detectNodeMajor(): number { |
| 49 | + // `process.version` is `vX.Y.Z`; parseInt stops at the first non-digit. |
| 50 | + return parseInt(process.version.slice(1), 10); |
| 51 | +} |
| 52 | + |
| 53 | +function loadFailureHint(err: NodeJS.ErrnoException): string { |
| 54 | + const platform = `${process.platform}-${process.arch}`; |
| 55 | + const installHint = `Install the matching optional dependency (e.g. @databricks/sea-native-${platform}).`; |
| 56 | + if (err.code === 'MODULE_NOT_FOUND') { |
| 57 | + return `SEA native binding not installed for platform ${platform} on Node ${process.version}. ${installHint}`; |
| 58 | + } |
| 59 | + if (err.code === 'ERR_DLOPEN_FAILED') { |
| 60 | + 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}.`; |
| 61 | + } |
| 62 | + return `SEA native binding failed to load on platform ${platform} / Node ${process.version}: ${err.message}`; |
| 63 | +} |
| 64 | + |
| 65 | +let cached: SeaNativeBinding | null | undefined; |
| 66 | +let cachedError: Error | undefined; |
| 67 | + |
| 68 | +function tryLoad(): SeaNativeBinding | undefined { |
| 69 | + const nodeMajor = detectNodeMajor(); |
| 70 | + if (Number.isFinite(nodeMajor) && nodeMajor < MIN_NODE_MAJOR) { |
| 71 | + cachedError = new Error( |
| 72 | + `SEA native binding requires Node >=${MIN_NODE_MAJOR}; running Node ${process.version}. Continue using the Thrift backend on this runtime.`, |
| 73 | + ); |
| 74 | + return undefined; |
| 75 | + } |
| 76 | + |
| 77 | + try { |
| 78 | + // The require path resolves to `native/sea/index.js` (the napi-rs |
| 79 | + // router). `.js` is omitted so eslint's `import/extensions` rule |
| 80 | + // accepts the call. |
| 81 | + // eslint-disable-next-line @typescript-eslint/no-var-requires, global-require |
| 82 | + return require('../../native/sea') as SeaNativeBinding; |
| 83 | + } catch (err) { |
| 84 | + if (err instanceof Error && 'code' in err) { |
| 85 | + cachedError = new Error(loadFailureHint(err as NodeJS.ErrnoException)); |
| 86 | + return undefined; |
| 87 | + } |
| 88 | + cachedError = new Error(`SEA native binding failed to load with non-standard error: ${String(err)}`); |
| 89 | + return undefined; |
| 90 | + } |
63 | 91 | } |
64 | 92 |
|
65 | 93 | /** |
66 | | - * Returns the loaded native binding. Throws if the platform-specific |
67 | | - * `.node` artifact cannot be found (napi-rs's auto-generated shim |
68 | | - * surfaces a descriptive error in that case). |
| 94 | + * Returns the loaded native binding. Throws a structured error if |
| 95 | + * the binding is unavailable on this platform / Node version. |
69 | 96 | */ |
70 | 97 | export function getSeaNative(): SeaNativeBinding { |
71 | | - return native as SeaNativeBinding; |
| 98 | + if (cached === undefined) { |
| 99 | + cached = tryLoad() ?? null; |
| 100 | + } |
| 101 | + if (cached === null) { |
| 102 | + throw cachedError ?? new Error('SEA native binding unavailable'); |
| 103 | + } |
| 104 | + return cached; |
72 | 105 | } |
73 | 106 |
|
74 | 107 | /** |
75 | | - * Convenience accessor for the smoke-test path. Equivalent to |
76 | | - * `getSeaNative().version()` but reads more naturally in tests and |
77 | | - * REPLs. |
| 108 | + * Returns the loaded binding or `undefined` if it could not be |
| 109 | + * loaded. Use this for capability-detection at startup; use |
| 110 | + * `getSeaNative()` at the point where SEA is actually required. |
78 | 111 | */ |
79 | | -export function version(): string { |
80 | | - return getSeaNative().version(); |
| 112 | +export function tryGetSeaNative(): SeaNativeBinding | undefined { |
| 113 | + if (cached === undefined) { |
| 114 | + cached = tryLoad() ?? null; |
| 115 | + } |
| 116 | + return cached ?? undefined; |
81 | 117 | } |
0 commit comments