|
14 | 14 |
|
15 | 15 | import IBackend from '../contracts/IBackend'; |
16 | 16 | import ISessionBackend from '../contracts/ISessionBackend'; |
17 | | -import IOperationBackend from '../contracts/IOperationBackend'; |
| 17 | +import IClientContext from '../contracts/IClientContext'; |
18 | 18 | 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'; |
33 | 19 | 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'; |
35 | 26 | 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'; |
49 | 28 |
|
50 | 29 | /** |
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`). |
60 | 32 | */ |
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 | + } |
120 | 48 | } |
121 | | - /* eslint-enable @typescript-eslint/no-unused-vars */ |
| 49 | + throw err; |
| 50 | +} |
122 | 51 |
|
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; |
127 | 60 | } |
128 | 61 |
|
129 | 62 | /** |
130 | | - * M0 SeaBackend — wires PAT auth + napi `openSession` end-to-end. |
| 63 | + * SEA-backed implementation of `IBackend`. |
131 | 64 | * |
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()`. |
135 | 70 | * |
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. |
139 | 81 | */ |
140 | 82 | export default class SeaBackend implements IBackend { |
141 | | - private nativeOptions?: SeaNativeConnectionOptions; |
| 83 | + private readonly context: IClientContext; |
142 | 84 |
|
143 | | - private readonly native: SeaNativeBinding; |
| 85 | + private readonly binding: SeaNativeBinding; |
144 | 86 |
|
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(); |
147 | 92 | } |
148 | 93 |
|
149 | 94 | public async connect(options: ConnectionOptions): Promise<void> { |
150 | 95 | // 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. |
153 | 98 | this.nativeOptions = buildSeaConnectionOptions(options); |
154 | 99 | } |
155 | 100 |
|
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> { |
158 | 102 | 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); |
160 | 111 | } |
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 | + }); |
163 | 129 | } |
164 | 130 |
|
165 | 131 | 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. |
167 | 134 | this.nativeOptions = undefined; |
168 | 135 | } |
169 | 136 | } |
0 commit comments