Skip to content

Commit cd5d4bf

Browse files
committed
sea-execution: executeStatement + openSession via SeaSessionBackend
SeaSessionBackend wraps the napi Connection handle. executeStatement passes through to napi.executeStatement and returns an IOperationBackend (SeaOperationBackend in sea-results feature). Session config + initialCatalog/initialSchema flow to napi openSession. M0 stops at executeStatement; metadata methods + per-stmt overrides defer to M1. No new dependencies. Reuses existing ConnectionOptions / Session config shapes. Co-authored-by: Isaac
1 parent e5bf5dd commit cd5d4bf

8 files changed

Lines changed: 1168 additions & 274 deletions

File tree

lib/DBSQLClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ export default class DBSQLClient extends EventEmitter implements IDBSQLClient, I
243243
// pattern (see databricks-sql-python/src/databricks/sql/session.py).
244244
const internalOptions = options as ConnectionOptions & InternalConnectionOptions;
245245
this.backend = internalOptions.useSEA
246-
? new SeaBackend()
246+
? new SeaBackend({ context: this })
247247
: new ThriftBackend({
248248
context: this,
249249
onConnectionEvent: (event, payload) => this.forwardConnectionEvent(event, payload),

lib/sea/SeaBackend.ts

Lines changed: 88 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -14,156 +14,123 @@
1414

1515
import IBackend from '../contracts/IBackend';
1616
import ISessionBackend from '../contracts/ISessionBackend';
17-
import IOperationBackend from '../contracts/IOperationBackend';
17+
import IClientContext from '../contracts/IClientContext';
1818
import { ConnectionOptions, OpenSessionRequest } from '../contracts/IDBSQLClient';
19-
import {
20-
ExecuteStatementOptions,
21-
TypeInfoRequest,
22-
CatalogsRequest,
23-
SchemasRequest,
24-
TablesRequest,
25-
TableTypesRequest,
26-
ColumnsRequest,
27-
FunctionsRequest,
28-
PrimaryKeysRequest,
29-
CrossReferenceRequest,
30-
} from '../contracts/IDBSQLSession';
31-
import Status from '../dto/Status';
32-
import InfoValue from '../dto/InfoValue';
3319
import HiveDriverError from '../errors/HiveDriverError';
34-
import { getSeaNative, SeaNativeBinding } from './SeaNativeLoader';
20+
import {
21+
getSeaNative,
22+
SeaNativeBinding,
23+
SeaNativeConnection,
24+
} from './SeaNativeLoader';
25+
import { mapKernelErrorToJsError, KernelErrorShape } from './SeaErrorMapping';
3526
import { buildSeaConnectionOptions, SeaNativeConnectionOptions } from './SeaAuth';
36-
37-
const NOT_IMPLEMENTED_SESSION =
38-
'SEA session backend: method not implemented in sea-auth (M0); lands in sea-execution/sea-operation.';
39-
40-
/**
41-
* Opaque handle to the napi binding's `Connection` class. The exact
42-
* shape lives in `native/sea/index.d.ts` (auto-generated). We type it as
43-
* a structural minimum here so the loader's pass-through typing doesn't
44-
* leak into every call site.
45-
*/
46-
interface NativeConnection {
47-
close(): Promise<void>;
48-
}
27+
import SeaSessionBackend from './SeaSessionBackend';
4928

5029
/**
51-
* Minimal `ISessionBackend` that wraps the napi-binding's `Connection`.
52-
*
53-
* For M0 (sea-auth) only `id` and `close()` are functional — they're the
54-
* subset required to round-trip a connect-open-close cycle. Every other
55-
* method throws a clear "not implemented in M0" `HiveDriverError`.
56-
*
57-
* The `id` field is currently a synthetic counter-based string; the kernel
58-
* exposes a real session-id through a follow-on getter that
59-
* `sea-execution` will wire through.
30+
* Sentinel string the napi binding uses on `Error.reason` JSON envelopes.
31+
* Keep in sync with `native/sea/src/error.rs` (`SENTINEL`).
6032
*/
61-
export class SeaSessionBackend implements ISessionBackend {
62-
private static seq = 0;
63-
64-
public readonly id: string;
65-
66-
private readonly connection: NativeConnection;
67-
68-
constructor(connection: NativeConnection) {
69-
this.connection = connection;
70-
SeaSessionBackend.seq += 1;
71-
this.id = `sea-session-${SeaSessionBackend.seq}`;
72-
}
73-
74-
/* eslint-disable @typescript-eslint/no-unused-vars */
75-
public async getInfo(_infoType: number): Promise<InfoValue> {
76-
throw new HiveDriverError(NOT_IMPLEMENTED_SESSION);
77-
}
78-
79-
public async executeStatement(
80-
_statement: string,
81-
_options: ExecuteStatementOptions,
82-
): Promise<IOperationBackend> {
83-
throw new HiveDriverError(NOT_IMPLEMENTED_SESSION);
84-
}
85-
86-
public async getTypeInfo(_request: TypeInfoRequest): Promise<IOperationBackend> {
87-
throw new HiveDriverError(NOT_IMPLEMENTED_SESSION);
88-
}
89-
90-
public async getCatalogs(_request: CatalogsRequest): Promise<IOperationBackend> {
91-
throw new HiveDriverError(NOT_IMPLEMENTED_SESSION);
92-
}
93-
94-
public async getSchemas(_request: SchemasRequest): Promise<IOperationBackend> {
95-
throw new HiveDriverError(NOT_IMPLEMENTED_SESSION);
96-
}
97-
98-
public async getTables(_request: TablesRequest): Promise<IOperationBackend> {
99-
throw new HiveDriverError(NOT_IMPLEMENTED_SESSION);
100-
}
101-
102-
public async getTableTypes(_request: TableTypesRequest): Promise<IOperationBackend> {
103-
throw new HiveDriverError(NOT_IMPLEMENTED_SESSION);
104-
}
105-
106-
public async getColumns(_request: ColumnsRequest): Promise<IOperationBackend> {
107-
throw new HiveDriverError(NOT_IMPLEMENTED_SESSION);
108-
}
109-
110-
public async getFunctions(_request: FunctionsRequest): Promise<IOperationBackend> {
111-
throw new HiveDriverError(NOT_IMPLEMENTED_SESSION);
112-
}
113-
114-
public async getPrimaryKeys(_request: PrimaryKeysRequest): Promise<IOperationBackend> {
115-
throw new HiveDriverError(NOT_IMPLEMENTED_SESSION);
116-
}
117-
118-
public async getCrossReference(_request: CrossReferenceRequest): Promise<IOperationBackend> {
119-
throw new HiveDriverError(NOT_IMPLEMENTED_SESSION);
33+
const KERNEL_ERROR_SENTINEL = '__databricks_error__:';
34+
35+
function rethrowKernelError(err: unknown): never {
36+
if (err && typeof err === 'object' && 'message' in err) {
37+
const reason = (err as { reason?: unknown }).reason;
38+
if (typeof reason === 'string' && reason.startsWith(KERNEL_ERROR_SENTINEL)) {
39+
try {
40+
const payload = JSON.parse(reason.slice(KERNEL_ERROR_SENTINEL.length)) as KernelErrorShape;
41+
throw mapKernelErrorToJsError(payload);
42+
} catch (parseErr) {
43+
if (parseErr !== err) {
44+
throw parseErr;
45+
}
46+
}
47+
}
12048
}
121-
/* eslint-enable @typescript-eslint/no-unused-vars */
49+
throw err;
50+
}
12251

123-
public async close(): Promise<Status> {
124-
await this.connection.close();
125-
return Status.success();
126-
}
52+
export interface SeaBackendOptions {
53+
context: IClientContext;
54+
/**
55+
* Optional injection seam for unit tests. When provided, replaces the
56+
* default `getSeaNative()` call so tests can swap in a mock napi
57+
* binding without loading the `.node` artifact.
58+
*/
59+
nativeBinding?: SeaNativeBinding;
12760
}
12861

12962
/**
130-
* M0 SeaBackend — wires PAT auth + napi `openSession` end-to-end.
63+
* SEA-backed implementation of `IBackend`.
13164
*
132-
* Connect is a no-op at this layer (the napi binding has no notion of a
133-
* standalone "connect"; a session is opened directly). We capture the
134-
* validated PAT options and hand them to `openSession()` on demand.
65+
* **M0 dispatch model:** the napi binding's `openSession()` already
66+
* builds a kernel `Session` from PAT + hostname + httpPath, so there is
67+
* no "connect" round-trip before `openSession` — `connect()` only
68+
* captures the `ConnectionOptions` and validates that PAT auth is in
69+
* use. The actual session open happens inside `openSession()`.
13570
*
136-
* Subsequent milestones (`sea-execution`, `sea-operation`) replace the
137-
* stubbed `ISessionBackend` / `IOperationBackend` methods with real
138-
* napi-binding calls.
71+
* **Auth validation:** delegates to `buildSeaConnectionOptions` from
72+
* `SeaAuth`, which mirrors the existing DBSQLClient PAT validation
73+
* pattern (slash-prepended httpPath, AuthenticationError on missing
74+
* token, HiveDriverError on non-PAT authType naming M1 modes).
75+
*
76+
* **Why we don't use IClientContext's connectionProvider here:** that
77+
* provider is the Thrift HTTP transport. The kernel owns its own
78+
* reqwest+rustls stack inside the native binding, so there is no
79+
* NodeJS-level connection state to manage on the SEA path. The
80+
* `IClientContext` is still useful for logger + config access.
13981
*/
14082
export default class SeaBackend implements IBackend {
141-
private nativeOptions?: SeaNativeConnectionOptions;
83+
private readonly context: IClientContext;
14284

143-
private readonly native: SeaNativeBinding;
85+
private readonly binding: SeaNativeBinding;
14486

145-
constructor(native: SeaNativeBinding = getSeaNative()) {
146-
this.native = native;
87+
private nativeOptions?: SeaNativeConnectionOptions;
88+
89+
constructor(options?: SeaBackendOptions) {
90+
this.context = options?.context as IClientContext;
91+
this.binding = options?.nativeBinding ?? getSeaNative();
14792
}
14893

14994
public async connect(options: ConnectionOptions): Promise<void> {
15095
// Validate PAT auth + capture the napi-binding option shape.
151-
// Any non-PAT mode (or a missing token) throws here, before we ever
152-
// touch the native binding.
96+
// Any non-PAT mode (or a missing/empty token) throws here, before
97+
// we ever touch the native binding.
15398
this.nativeOptions = buildSeaConnectionOptions(options);
15499
}
155100

156-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
157-
public async openSession(_request: OpenSessionRequest): Promise<ISessionBackend> {
101+
public async openSession(request: OpenSessionRequest): Promise<ISessionBackend> {
158102
if (!this.nativeOptions) {
159-
throw new HiveDriverError('SeaBackend: connect() must be called before openSession().');
103+
throw new HiveDriverError('SeaBackend: not connected. Call connect() first.');
104+
}
105+
106+
let nativeConnection: SeaNativeConnection;
107+
try {
108+
nativeConnection = (await this.binding.openSession(this.nativeOptions)) as SeaNativeConnection;
109+
} catch (err) {
110+
rethrowKernelError(err);
160111
}
161-
const connection = (await this.native.openSession(this.nativeOptions)) as NativeConnection;
162-
return new SeaSessionBackend(connection);
112+
113+
// Merge `request.configuration` (the existing public field for Spark
114+
// conf) with any backend-specific session config. The SEA wire
115+
// protocol applies these per-statement, but we capture them at
116+
// session-open time and forward with every executeStatement to
117+
// preserve session-config semantics.
118+
const sessionConfig = request.configuration ? { ...request.configuration } : undefined;
119+
120+
return new SeaSessionBackend({
121+
connection: nativeConnection!,
122+
context: this.context,
123+
defaults: {
124+
initialCatalog: request.initialCatalog,
125+
initialSchema: request.initialSchema,
126+
sessionConfig,
127+
},
128+
});
163129
}
164130

165131
public async close(): Promise<void> {
166-
// Connection-level resources are owned by the session wrapper. No-op here.
132+
// No backend-level resources to release — each `SeaSessionBackend`
133+
// owns its own napi `Connection` lifecycle.
167134
this.nativeOptions = undefined;
168135
}
169136
}

lib/sea/SeaNativeLoader.ts

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,27 +35,64 @@
3535
// eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-dynamic-require, global-require
3636
const native = require('../../native/sea/index.js');
3737

38+
/**
39+
* JS-visible per-execute options carried over the napi binding boundary.
40+
* Mirrors the `ExecuteOptions` shape generated by napi-rs into
41+
* `native/sea/index.d.ts`. Re-declared here so the JS adapter layer
42+
* isn't tied to the binding-generated types.
43+
*/
44+
export interface SeaExecuteOptions {
45+
initialCatalog?: string;
46+
initialSchema?: string;
47+
sessionConfig?: Record<string, string>;
48+
}
49+
50+
/**
51+
* Arrow IPC payload returned by `Statement.fetchNextBatch()`. Carries a
52+
* complete Arrow IPC stream (schema header + 1 record-batch message).
53+
*/
54+
export interface SeaArrowBatch {
55+
ipcBytes: Buffer;
56+
}
57+
58+
/**
59+
* Arrow IPC payload returned by `Statement.schema()` (schema header only).
60+
*/
61+
export interface SeaArrowSchema {
62+
ipcBytes: Buffer;
63+
}
64+
65+
/**
66+
* Typed surface for the opaque napi `Statement` handle. Method signatures
67+
* match `native/sea/index.d.ts` exactly so the JS-side wrappers can
68+
* `await` them without `any` casts.
69+
*/
70+
export interface SeaNativeStatement {
71+
fetchNextBatch(): Promise<SeaArrowBatch | null>;
72+
schema(): Promise<SeaArrowSchema>;
73+
cancel(): Promise<void>;
74+
close(): Promise<void>;
75+
}
76+
77+
/**
78+
* Typed surface for the opaque napi `Connection` handle.
79+
*/
80+
export interface SeaNativeConnection {
81+
executeStatement(sql: string, options: SeaExecuteOptions): Promise<SeaNativeStatement>;
82+
close(): Promise<void>;
83+
}
84+
3885
/**
3986
* Public surface of the native binding exposed to the rest of the
4087
* NodeJS driver. Round 2 lands `openSession` + opaque `Connection` /
4188
* `Statement` classes (the binding-generated `.d.ts` is the source of
4289
* 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.
4990
*/
5091
export interface SeaNativeBinding {
5192
/** Returns the native crate version (smoke test for the binding's load path). */
5293
version(): string;
5394
/** 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>;
95+
openSession(opts: { hostName: string; httpPath: string; token: string }): Promise<SeaNativeConnection>;
5996
/** Opaque Connection class — instance methods on the binding-generated d.ts. */
6097
Connection: Function;
6198
/** Opaque Statement class — instance methods on the binding-generated d.ts. */

0 commit comments

Comments
 (0)