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
4 changes: 2 additions & 2 deletions lib/DBSQLClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,14 +232,14 @@ export default class DBSQLClient extends EventEmitter implements IDBSQLClient, I
// doesn't ship in the public `.d.ts`. Mirrors Python's `kwargs.get("use_sea")`
// pattern (see databricks-sql-python/src/databricks/sql/session.py).
const internalOptions = options as ConnectionOptions & InternalConnectionOptions;

if (internalOptions.useSEA) {
// The SEA backend authenticates inside the native binding; the
// Thrift auth/connection providers are never read on this path, so
// we don't build them (avoids validating the PAT twice and
// constructing a throwaway OAuth provider for an OAuth+useSEA call).
// The backend reads logger/config off the IClientContext it's given.
this.logger.log(LogLevel.info, 'Connecting via the SEA (native) backend');
this.backend = new SeaBackend(undefined, this.logger);
this.backend = new SeaBackend({ context: this });
} else {
this.authProvider = this.createAuthProvider(options, authProvider);
this.connectionProvider = this.createConnectionProvider(options);
Expand Down
236 changes: 88 additions & 148 deletions lib/sea/SeaBackend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,183 +14,123 @@

import IBackend from '../contracts/IBackend';
import ISessionBackend from '../contracts/ISessionBackend';
import IOperationBackend from '../contracts/IOperationBackend';
import IClientContext from '../contracts/IClientContext';
import { ConnectionOptions, OpenSessionRequest } from '../contracts/IDBSQLClient';
import {
ExecuteStatementOptions,
TypeInfoRequest,
CatalogsRequest,
SchemasRequest,
TablesRequest,
TableTypesRequest,
ColumnsRequest,
FunctionsRequest,
PrimaryKeysRequest,
CrossReferenceRequest,
} from '../contracts/IDBSQLSession';
import Status from '../dto/Status';
import InfoValue from '../dto/InfoValue';
import HiveDriverError from '../errors/HiveDriverError';
import IDBSQLLogger, { LogLevel } from '../contracts/IDBSQLLogger';
import { getSeaNative, SeaNativeBinding } from './SeaNativeLoader';
import {
getSeaNative,
SeaNativeBinding,
SeaNativeConnection,
} from './SeaNativeLoader';
import { mapKernelErrorToJsError, KernelErrorShape } from './SeaErrorMapping';
import { buildSeaConnectionOptions, SeaNativeConnectionOptions } from './SeaAuth';

const NOT_IMPLEMENTED_SESSION =
'SEA session backend: method not implemented in sea-auth (M0); lands in sea-execution/sea-operation.';

/**
* Opaque handle to the napi binding's `Connection` class. The exact
* shape lives in `native/sea/index.d.ts` (auto-generated). We type it as
* a structural minimum here so the loader's pass-through typing doesn't
* leak into every call site.
*/
interface NativeConnection {
/** Server-issued session id (kernel `Connection.sessionId` getter). */
readonly sessionId: string;
close(): Promise<void>;
}
import SeaSessionBackend from './SeaSessionBackend';

/**
* Minimal `ISessionBackend` that wraps the napi-binding's `Connection`.
*
* For M0 (sea-auth) only `id` and `close()` are functional — they're the
* subset required to round-trip a connect-open-close cycle. Every other
* method throws a clear "not implemented in M0" `HiveDriverError`.
*
* `id` is the server-issued session id read straight off the kernel
* `Connection` (its `sessionId` getter, readable even after close()), so
* the value logged by `DBSQLSession` correlates with kernel / server logs
* rather than being a process-local synthetic counter.
* Sentinel string the napi binding uses on `Error.reason` JSON envelopes.
* Keep in sync with `native/sea/src/error.rs` (`SENTINEL`).
*/
export class SeaSessionBackend implements ISessionBackend {
public readonly id: string;

private readonly connection: NativeConnection;

private readonly logger?: IDBSQLLogger;

constructor(connection: NativeConnection, logger?: IDBSQLLogger) {
this.connection = connection;
this.logger = logger;
this.id = connection.sessionId;
}

/* eslint-disable @typescript-eslint/no-unused-vars */
public async getInfo(_infoType: number): Promise<InfoValue> {
throw new HiveDriverError(NOT_IMPLEMENTED_SESSION);
}

public async executeStatement(
_statement: string,
_options: ExecuteStatementOptions,
): Promise<IOperationBackend> {
throw new HiveDriverError(NOT_IMPLEMENTED_SESSION);
}

public async getTypeInfo(_request: TypeInfoRequest): Promise<IOperationBackend> {
throw new HiveDriverError(NOT_IMPLEMENTED_SESSION);
}

public async getCatalogs(_request: CatalogsRequest): Promise<IOperationBackend> {
throw new HiveDriverError(NOT_IMPLEMENTED_SESSION);
}

public async getSchemas(_request: SchemasRequest): Promise<IOperationBackend> {
throw new HiveDriverError(NOT_IMPLEMENTED_SESSION);
}

public async getTables(_request: TablesRequest): Promise<IOperationBackend> {
throw new HiveDriverError(NOT_IMPLEMENTED_SESSION);
}

public async getTableTypes(_request: TableTypesRequest): Promise<IOperationBackend> {
throw new HiveDriverError(NOT_IMPLEMENTED_SESSION);
}

public async getColumns(_request: ColumnsRequest): Promise<IOperationBackend> {
throw new HiveDriverError(NOT_IMPLEMENTED_SESSION);
}

public async getFunctions(_request: FunctionsRequest): Promise<IOperationBackend> {
throw new HiveDriverError(NOT_IMPLEMENTED_SESSION);
}

public async getPrimaryKeys(_request: PrimaryKeysRequest): Promise<IOperationBackend> {
throw new HiveDriverError(NOT_IMPLEMENTED_SESSION);
}

public async getCrossReference(_request: CrossReferenceRequest): Promise<IOperationBackend> {
throw new HiveDriverError(NOT_IMPLEMENTED_SESSION);
const KERNEL_ERROR_SENTINEL = '__databricks_error__:';

function rethrowKernelError(err: unknown): never {
if (err && typeof err === 'object' && 'message' in err) {
const reason = (err as { reason?: unknown }).reason;
if (typeof reason === 'string' && reason.startsWith(KERNEL_ERROR_SENTINEL)) {
try {
const payload = JSON.parse(reason.slice(KERNEL_ERROR_SENTINEL.length)) as KernelErrorShape;
throw mapKernelErrorToJsError(payload);
} catch (parseErr) {
if (parseErr !== err) {
throw parseErr;
}
}
}
}
/* eslint-enable @typescript-eslint/no-unused-vars */
throw err;
}

public async close(): Promise<Status> {
this.logger?.log(LogLevel.debug, `SEA session closing with id: ${this.id}`);
await this.connection.close();
return Status.success();
}
export interface SeaBackendOptions {
context: IClientContext;
/**
* Optional injection seam for unit tests. When provided, replaces the
* default `getSeaNative()` call so tests can swap in a mock napi
* binding without loading the `.node` artifact.
*/
nativeBinding?: SeaNativeBinding;
}

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

private readonly injectedNative?: SeaNativeBinding;
private readonly binding: SeaNativeBinding;

private cachedNative?: SeaNativeBinding;

private readonly logger?: IDBSQLLogger;

// `native` is injectable (tests pass a fake); production leaves it
// undefined and the binding is resolved lazily on first use so that
// constructing a SeaBackend never throws on a platform without the
// optional `.node` — the clearer auth/option validation in connect()
// runs first.
constructor(native?: SeaNativeBinding, logger?: IDBSQLLogger) {
this.injectedNative = native;
this.logger = logger;
}
private nativeOptions?: SeaNativeConnectionOptions;

private get native(): SeaNativeBinding {
if (!this.cachedNative) {
this.cachedNative = this.injectedNative ?? getSeaNative();
}
return this.cachedNative;
constructor(options?: SeaBackendOptions) {
this.context = options?.context as IClientContext;
this.binding = options?.nativeBinding ?? getSeaNative();
}

public async connect(options: ConnectionOptions): Promise<void> {
// Validate PAT auth + capture the napi-binding option shape. Any
// non-PAT mode (or a missing token) throws here, before we ever touch
// the native binding. NOTE: unlike Thrift, this performs no network
// round-trip — the session is opened lazily in openSession(), so a
// resolved connect() does not by itself prove the endpoint is
// reachable or the credential is valid.
// Validate PAT auth + capture the napi-binding option shape.
// Any non-PAT mode (or a missing/empty token) throws here, before
// we ever touch the native binding.
this.nativeOptions = buildSeaConnectionOptions(options);
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
public async openSession(_request: OpenSessionRequest): Promise<ISessionBackend> {
public async openSession(request: OpenSessionRequest): Promise<ISessionBackend> {
if (!this.nativeOptions) {
throw new HiveDriverError('SeaBackend: connect() must be called before openSession().');
throw new HiveDriverError('SeaBackend: not connected. Call connect() first.');
}

let nativeConnection: SeaNativeConnection;
try {
nativeConnection = (await this.binding.openSession(this.nativeOptions)) as SeaNativeConnection;
} catch (err) {
rethrowKernelError(err);
}
const connection = (await this.native.openSession(this.nativeOptions)) as NativeConnection;
const session = new SeaSessionBackend(connection, this.logger);
this.logger?.log(LogLevel.info, `SEA session opened with id: ${session.id}`);
return session;

// Merge `request.configuration` (the existing public field for Spark
// conf) with any backend-specific session config. The SEA wire
// protocol applies these per-statement, but we capture them at
// session-open time and forward with every executeStatement to
// preserve session-config semantics.
const sessionConfig = request.configuration ? { ...request.configuration } : undefined;

return new SeaSessionBackend({
connection: nativeConnection!,
context: this.context,
defaults: {
initialCatalog: request.initialCatalog,
initialSchema: request.initialSchema,
sessionConfig,
},
});
}

public async close(): Promise<void> {
// Connection-level resources are owned by the session wrapper. No-op here.
// No backend-level resources to release — each `SeaSessionBackend`
// owns its own napi `Connection` lifecycle.
this.nativeOptions = undefined;
}
}
17 changes: 17 additions & 0 deletions lib/sea/SeaNativeLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,23 @@ export type SeaArrowSchema = NativeArrowSchema;
export type SeaConnection = NativeConnection;
export type SeaStatement = NativeStatement;

// Back-compat aliases for the downstream SEA stack branches that landed
// against the pre-rename loader. The merged kernel (@databricks/sql-kernel)
// moved per-statement catalog/schema/sessionConfig to session-level
// `openSession`, so `ExecuteOptions` no longer exists on the binding;
// `SeaExecuteOptions` is kept as a deprecated shim describing the old
// per-statement shape so the stack keeps compiling. Per-statement options
// are now applied at session creation — see native/sea/README.md.
export type SeaNativeConnection = NativeConnection;
export type SeaNativeStatement = NativeStatement;
export type SeaNativeConnectionOptions = NativeConnectionOptions;
/** @deprecated per-statement options moved to session-level `openSession`. */
export interface SeaExecuteOptions {
initialCatalog?: string;
initialSchema?: string;
sessionConfig?: Record<string, string>;
}

/**
* The full native binding surface, derived from the generated module
* so it can never drift from the `.d.ts` contract: when the kernel
Expand Down
Loading
Loading