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
83 changes: 83 additions & 0 deletions lib/sea/SeaAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// 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.

import { ConnectionOptions } from '../contracts/IDBSQLClient';
import AuthenticationError from '../errors/AuthenticationError';
import HiveDriverError from '../errors/HiveDriverError';

/**
* Shape consumed by the napi-binding's `openSession()` (see
* `native/sea/index.d.ts`). M0 supports PAT only — `token` is required.
*
* Mirrors `ConnectionOptions` in the binding's `.d.ts`; declared locally
* to avoid coupling the JS-side adapter to the auto-generated TS file.
*/
export interface SeaNativeConnectionOptions {
hostName: string;
httpPath: string;
token: string;
}

function prependSlash(str: string): string {
if (str.length > 0 && str.charAt(0) !== '/') {
return `/${str}`;
}
return str;
}

/**
* Validate that the user-supplied `ConnectionOptions` describe a PAT auth
* configuration and build the napi-binding's connection-options shape.
*
* M0 SCOPE: PAT only.
* - Accepts `authType: 'access-token'` and the undefined-authType default
* (which already means PAT throughout the existing driver — see
* `DBSQLClient.createAuthProvider`).
* - Rejects every other `authType` discriminant with a clear
* "M0 supports only PAT" message so callers know OAuth / Federation /
* custom providers land in M1.
*
* Throws:
* - `AuthenticationError` when the auth mode is PAT but `token` is missing
* or empty.
* - `HiveDriverError` when the auth mode is anything other than PAT.
*/
export function buildSeaConnectionOptions(options: ConnectionOptions): SeaNativeConnectionOptions {
const { authType } = options as { authType?: string };

if (authType !== undefined && authType !== 'access-token') {
throw new HiveDriverError(
`SEA backend (M0) supports only PAT auth (authType: 'access-token'); ` +
`got authType: '${authType}'. Other auth modes (databricks-oauth, ` +
`token-provider, external-token, static-token, custom) will land in M1.`,
);
}

// PAT path — at this point `options` is structurally the access-token branch
// of `AuthOptions`, which guarantees a `token` field at the type level. We
// still defensively re-check because the public ConnectionOptions type
// permits `authType: undefined` with no token at runtime.
const { token } = options as { token?: string };
if (typeof token !== 'string' || token.length === 0) {
throw new AuthenticationError(
'SEA backend: a non-empty PAT must be supplied via `token` when using `authType: \'access-token\'`.',
);
}

return {
hostName: options.host,
httpPath: prependSlash(options.path),
token,
};
}
163 changes: 157 additions & 6 deletions lib/sea/SeaBackend.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,169 @@
// 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.

import IBackend from '../contracts/IBackend';
import ISessionBackend from '../contracts/ISessionBackend';
import IOperationBackend from '../contracts/IOperationBackend';
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 { getSeaNative, SeaNativeBinding } from './SeaNativeLoader';
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 {
close(): Promise<void>;
}

/**
* 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`.
*
* The `id` field is currently a synthetic counter-based string; the kernel
* exposes a real session-id through a follow-on getter that
* `sea-execution` will wire through.
*/
export class SeaSessionBackend implements ISessionBackend {
private static seq = 0;

const NOT_IMPLEMENTED = 'SEA backend not implemented yet — wired in sea-napi-binding feature';
public readonly id: string;

private readonly connection: NativeConnection;

constructor(connection: NativeConnection) {
this.connection = connection;
SeaSessionBackend.seq += 1;
this.id = `sea-session-${SeaSessionBackend.seq}`;
}

/* 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);
}
/* eslint-enable @typescript-eslint/no-unused-vars */

public async close(): Promise<Status> {
await this.connection.close();
return Status.success();
}
}

/**
* M0 SeaBackend — wires PAT auth + napi `openSession` end-to-end.
*
* 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.
*
* Subsequent milestones (`sea-execution`, `sea-operation`) replace the
* stubbed `ISessionBackend` / `IOperationBackend` methods with real
* napi-binding calls.
*/
export default class SeaBackend implements IBackend {
public async connect(): Promise<void> {
throw new Error(NOT_IMPLEMENTED);
private nativeOptions?: SeaNativeConnectionOptions;

private readonly native: SeaNativeBinding;

constructor(native: SeaNativeBinding = getSeaNative()) {
this.native = native;
}

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.
this.nativeOptions = buildSeaConnectionOptions(options);
}

public async openSession(): Promise<ISessionBackend> {
throw new Error(NOT_IMPLEMENTED);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public async openSession(_request: OpenSessionRequest): Promise<ISessionBackend> {
if (!this.nativeOptions) {
throw new HiveDriverError('SeaBackend: connect() must be called before openSession().');
}
const connection = (await this.native.openSession(this.nativeOptions)) as NativeConnection;
return new SeaSessionBackend(connection);
}

public async close(): Promise<void> {
throw new Error(NOT_IMPLEMENTED);
// Connection-level resources are owned by the session wrapper. No-op here.
this.nativeOptions = undefined;
}
}
11 changes: 11 additions & 0 deletions tests/integration/.mocharc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use strict';

const allSpecs = 'tests/integration/**/*.test.ts';

const argvSpecs = process.argv.slice(4);

module.exports = {
spec: argvSpecs.length > 0 ? argvSpecs : allSpecs,
timeout: '300000',
require: ['ts-node/register'],
};
75 changes: 75 additions & 0 deletions tests/integration/sea/auth-pat-e2e.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// 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.

import { expect } from 'chai';
import { DBSQLClient } from '../../../lib';

/**
* sea-auth M0 end-to-end:
* 1. Construct a DBSQLClient.
* 2. `connect({ useSEA: true, token })` against pecotesting.
* 3. `openSession()` — round-trips through the napi binding.
* 4. Close the session, then the client.
*
* No query is executed here — execution is the responsibility of the
* sea-execution feature's own e2e. This test exists solely to confirm
* the PAT round-trips end-to-end and the napi binding's `openSession`
* surface is reachable from `DBSQLClient`.
*
* Required env (exported by `~/.zshrc` on the developer machine):
* - DATABRICKS_PECOTESTING_SERVER_HOSTNAME
* - DATABRICKS_PECOTESTING_HTTP_PATH
* - DATABRICKS_PECOTESTING_TOKEN_PERSONAL (preferred — personal PAT)
* - DATABRICKS_PECOTESTING_TOKEN (fallback — shared PAT)
*
* If any of the three required env vars is missing, the suite is skipped
* so CI machines without secrets don't fail-flap.
*/
describe('sea-auth e2e — PAT through DBSQLClient ↔ SeaBackend ↔ napi binding', function suite() {
const host = process.env.DATABRICKS_PECOTESTING_SERVER_HOSTNAME;
const path = process.env.DATABRICKS_PECOTESTING_HTTP_PATH;
const token =
process.env.DATABRICKS_PECOTESTING_TOKEN_PERSONAL || process.env.DATABRICKS_PECOTESTING_TOKEN;

this.timeout(120_000);

before(function gate() {
if (!host || !path || !token) {
// eslint-disable-next-line no-invalid-this
this.skip();
}
});

it('connects, opens a session, closes the session, closes the client', async () => {
const client = new DBSQLClient();

const connected = await client.connect({
host: host as string,
path: path as string,
token: token as string,
useSEA: true,
});
expect(connected).to.equal(client);

const session = await client.openSession();
expect(session).to.exist;
expect(session.id).to.be.a('string');
expect(session.id.length).to.be.greaterThan(0);

const status = await session.close();
expect(status.isSuccess).to.equal(true);

await client.close();
});
});
Loading
Loading