Skip to content
Open
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
8 changes: 8 additions & 0 deletions lib/DBSQLParameter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ export enum DBSQLParameterType {
STRING = 'STRING',
DATE = 'DATE',
TIMESTAMP = 'TIMESTAMP',
// Timezone-explicit timestamp variants. A bare `Date` value defaults to
// `TIMESTAMP`; set one of these explicitly to bind a TIMESTAMP_NTZ
// (no timezone, wall-clock) or TIMESTAMP_LTZ (local timezone) parameter.
// The Thrift wire only has `TIMESTAMP`; these are SEA-path types the kernel
// param codec accepts — without them a migrating caller silently coerces
// NTZ/LTZ columns to TIMESTAMP.
TIMESTAMP_NTZ = 'TIMESTAMP_NTZ',
TIMESTAMP_LTZ = 'TIMESTAMP_LTZ',
FLOAT = 'FLOAT',
DECIMAL = 'DECIMAL',
DOUBLE = 'DOUBLE',
Expand Down
14 changes: 14 additions & 0 deletions lib/contracts/IDBSQLSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,20 @@ export type ExecuteStatementOptions = {
* These tags apply only to this statement and do not persist across queries.
*/
queryTags?: Record<string, string | null | undefined>;
/**
* Server-side cap on the number of rows the statement returns (SEA path only).
* Maps to the kernel's `row_limit` / SEA `row_limit`. The Thrift backend has no
* execute-time server cap, so this is a no-op there; use `maxRows` for the
* client-side per-fetch chunk size on both backends.
*/
rowLimit?: number;
/**
* Arbitrary per-statement configuration overlay (SEA path only). Maps to the
* kernel's `statement_conf` / SEA `statement_conf`, the same mechanism the
* Thrift backend exposes as `confOverlay`. `queryTags` are merged into this map
* under the `query_tags` key, mirroring the Thrift wire shape.
*/
statementConf?: Record<string, string>;
};

export type TypeInfoRequest = {
Expand Down
49 changes: 29 additions & 20 deletions lib/sea/SeaSessionBackend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,19 +117,17 @@ export default class SeaSessionBackend implements ISessionBackend {
/**
* Execute a SQL statement through the napi binding.
*
* Catalog / schema / sessionConf were applied at session open, so
* there are no per-statement options to thread through.
* Catalog / schema / sessionConf were applied at session open. The
* per-statement options threaded here mirror the Thrift backend:
* `ordinalParameters` / `namedParameters` (bound params), `queryTimeout`
* (server wait timeout), `queryTags` (serialised into the conf overlay's
* `query_tags` key), `statementConf` (arbitrary conf overlay), and
* `rowLimit` (SEA-only server-side row cap).
*
* M0 intentionally rejects `queryTimeout`, `namedParameters`, and
* `ordinalParameters` with explicit deferred-to-M1 errors. `useCloudFetch`
* is a no-op on the SEA path — the kernel hardcodes the SEA
* `disposition` to `INLINE_OR_EXTERNAL_LINKS`, and per-statement
* conf overrides have no reader on the kernel; cloud-fetch behaviour
* is governed entirely by the kernel's `ResultConfig` (M1 binding
* surface).
*
* The Thrift backend remains the path for consumers that need any
* of those today.
* `useCloudFetch` is a no-op on the SEA path — the kernel hardcodes the
* SEA `disposition` to `INLINE_OR_EXTERNAL_LINKS`; cloud-fetch behaviour
* is governed by the kernel's `ResultConfig`. `maxRows` is the
* client-side per-fetch chunk size, applied by the facade, not here.
*/
public async executeStatement(statement: string, options: ExecuteStatementOptions): Promise<IOperationBackend> {
this.failIfClosed();
Expand Down Expand Up @@ -175,15 +173,26 @@ export default class SeaSessionBackend implements ISessionBackend {
if (options.queryTimeout !== undefined) {
nativeOptions.queryTimeoutSecs = Number(options.queryTimeout);
}
// Query tags: serialise JS-side into the conf overlay's `query_tags` key
// (the same wire shape the Thrift backend produces via `serializeQueryTags`
// → `confOverlay`). Not forwarded via the napi `queryTags` field: that's a
// `HashMap<String,String>` which can't represent a null-valued tag, and the
// kernel rejects setting both the field and a `query_tags` conf key. A
// null-valued tag therefore round-trips as a key-only segment.
// Server-side row cap (SEA `row_limit`). SEA-only — the Thrift backend has
// no execute-time server cap, so there is no parity obligation here.
if (options.rowLimit !== undefined) {
nativeOptions.rowLimit = Number(options.rowLimit);
}
// Per-statement conf overlay (`statement_conf`) plus query tags. Tags are
// serialised JS-side into the `query_tags` key (the same wire shape the
// Thrift backend produces via `serializeQueryTags` → `confOverlay`), rather
// than via the napi `queryTags` field: napi's `HashMap<String,String>`
// can't represent a null-valued tag, and the kernel rejects setting both
// the `queryTags` field and a `query_tags` conf key.
const serializedQueryTags = serializeQueryTags(options.queryTags);
if (serializedQueryTags !== undefined) {
nativeOptions.statementConf = { query_tags: serializedQueryTags };
if (options.statementConf !== undefined || serializedQueryTags !== undefined) {
const statementConf: Record<string, string> = { ...(options.statementConf ?? {}) };
if (serializedQueryTags !== undefined) {
statementConf.query_tags = serializedQueryTags;
}
if (Object.keys(statementConf).length > 0) {
nativeOptions.statementConf = statementConf;
}
}
const hasOptions = Object.keys(nativeOptions).length > 0;

Expand Down
31 changes: 31 additions & 0 deletions tests/unit/sea/execution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ import { ConnectionOptions } from '../../../lib/contracts/IDBSQLClient';
// -----------------------------------------------------------------------------

class FakeNativeStatement implements SeaNativeStatement {
public readonly statementId = 'fake-statement-id';

public closed = false;

public cancelled = false;
Expand Down Expand Up @@ -98,6 +100,8 @@ class FakeNativeAsyncStatement implements SeaNativeAsyncStatement {
}

class FakeNativeConnection implements SeaNativeConnection {
public readonly sessionId = 'fake-session-id';

public closed = false;

public lastSql?: string;
Expand Down Expand Up @@ -506,6 +510,33 @@ describe('SeaSessionBackend', () => {
expect(connection.lastListSchemasArgs).to.deep.equal([undefined, '%']);
});

it('executeStatement forwards rowLimit as napi rowLimit', async () => {
const connection = new FakeNativeConnection();
const session = makeSession(connection);
await session.executeStatement('SELECT 1', { rowLimit: 500 });
expect(connection.lastOptions?.rowLimit).to.equal(500);
});

it('executeStatement forwards statementConf verbatim as napi statementConf', async () => {
const connection = new FakeNativeConnection();
const session = makeSession(connection);
await session.executeStatement('SELECT 1', { statementConf: { 'spark.sql.ansi.enabled': 'true' } });
expect(connection.lastOptions?.statementConf).to.deep.equal({ 'spark.sql.ansi.enabled': 'true' });
});

it('executeStatement merges queryTags into a provided statementConf', async () => {
const connection = new FakeNativeConnection();
const session = makeSession(connection);
await session.executeStatement('SELECT 1', {
statementConf: { 'spark.sql.ansi.enabled': 'true' },
queryTags: { team: 'data' },
});
expect(connection.lastOptions?.statementConf).to.deep.equal({
'spark.sql.ansi.enabled': 'true',
query_tags: 'team:data',
});
});

it('executeStatement uses the no-options fast path when nothing is bound', async () => {
const connection = new FakeNativeConnection();
const session = makeSession(connection);
Expand Down
3 changes: 3 additions & 0 deletions tests/unit/sea/metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import HiveDriverError from '../../../lib/errors/HiveDriverError';
// ─── Fakes ───────────────────────────────────────────────────────────────────

class FakeNativeStatement implements SeaNativeStatement {
public readonly statementId = 'fake-statement-id';
public async fetchNextBatch() { return null; }
public async schema() { return { ipcBytes: Buffer.alloc(0) }; }
public async cancel() {}
Expand All @@ -47,6 +48,8 @@ interface RecordedMetadataCall {
* wrapping path.
*/
class FakeMetadataConnection implements SeaNativeConnection {
public readonly sessionId = 'fake-session-id';

public readonly calls: RecordedMetadataCall[] = [];

public throwNextCall: unknown = null;
Expand Down
19 changes: 19 additions & 0 deletions tests/unit/sea/positionalParams.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,25 @@ describe('SeaPositionalParams.buildSeaPositionalParams', () => {
{ sqlType: 'TIMESTAMP', value: '2024-01-15 10:30:00' },
]);
});

it('honours explicit TIMESTAMP_NTZ / TIMESTAMP_LTZ types (kernel param codec)', () => {
expect(
buildSeaPositionalParams([
new DBSQLParameter({ type: DBSQLParameterType.TIMESTAMP_NTZ, value: '2024-01-15 10:30:00' }),
new DBSQLParameter({ type: DBSQLParameterType.TIMESTAMP_LTZ, value: '2024-01-15 10:30:00' }),
]),
).to.deep.equal([
{ sqlType: 'TIMESTAMP_NTZ', value: '2024-01-15 10:30:00' },
{ sqlType: 'TIMESTAMP_LTZ', value: '2024-01-15 10:30:00' },
]);
});

it('routes a Date with explicit TIMESTAMP_NTZ type as NTZ (not the default TIMESTAMP)', () => {
const d = new Date('2024-01-15T10:30:00.000Z');
expect(
buildSeaPositionalParams([new DBSQLParameter({ type: DBSQLParameterType.TIMESTAMP_NTZ, value: d })]),
).to.deep.equal([{ sqlType: 'TIMESTAMP_NTZ', value: d.toISOString() }]);
});
});

describe('SeaPositionalParams.buildSeaNamedParams', () => {
Expand Down
Loading