Skip to content

Commit 9dcb28c

Browse files
committed
feat(sea): wire rowLimit + statementConf + TIMESTAMP_NTZ/LTZ params
Three statement-option / param-type additions where the kernel + napi were already ready but the node SEA layer didn't expose them: - rowLimit: new `ExecuteStatementOptions.rowLimit` → napi `rowLimit` (SEA `row_limit`). SEA-only server-side cap; Thrift has no execute-time cap. - statementConf: new `ExecuteStatementOptions.statementConf` → napi `statementConf` (SEA `statement_conf`), the Thrift `confOverlay` equivalent. Generalises the existing query_tags serialisation so a caller-supplied statementConf and queryTags merge into one conf map (queryTags already forwarded upstream). - TIMESTAMP_NTZ / TIMESTAMP_LTZ: added to `DBSQLParameterType` so callers can bind timezone-explicit timestamp params. `toSparkParameter` already honours an explicit type and `SeaPositionalParams` passes the SQL type verbatim to the kernel codec (which has the NTZ/LTZ arms). Co-authored-by: Isaac Signed-off-by: Madhavendra Rathore <madhavendra.rathore@databricks.com>
1 parent 71bfc5e commit 9dcb28c

5 files changed

Lines changed: 97 additions & 20 deletions

File tree

lib/DBSQLParameter.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ export enum DBSQLParameterType {
88
STRING = 'STRING',
99
DATE = 'DATE',
1010
TIMESTAMP = 'TIMESTAMP',
11+
// Timezone-explicit timestamp variants. A bare `Date` value defaults to
12+
// `TIMESTAMP`; set one of these explicitly to bind a TIMESTAMP_NTZ
13+
// (no timezone, wall-clock) or TIMESTAMP_LTZ (local timezone) parameter.
14+
// The Thrift wire only has `TIMESTAMP`; these are SEA-path types the kernel
15+
// param codec accepts — without them a migrating caller silently coerces
16+
// NTZ/LTZ columns to TIMESTAMP.
17+
TIMESTAMP_NTZ = 'TIMESTAMP_NTZ',
18+
TIMESTAMP_LTZ = 'TIMESTAMP_LTZ',
1119
FLOAT = 'FLOAT',
1220
DECIMAL = 'DECIMAL',
1321
DOUBLE = 'DOUBLE',

lib/contracts/IDBSQLSession.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,20 @@ export type ExecuteStatementOptions = {
2727
* These tags apply only to this statement and do not persist across queries.
2828
*/
2929
queryTags?: Record<string, string | null | undefined>;
30+
/**
31+
* Server-side cap on the number of rows the statement returns (SEA path only).
32+
* Maps to the kernel's `row_limit` / SEA `row_limit`. The Thrift backend has no
33+
* execute-time server cap, so this is a no-op there; use `maxRows` for the
34+
* client-side per-fetch chunk size on both backends.
35+
*/
36+
rowLimit?: number;
37+
/**
38+
* Arbitrary per-statement configuration overlay (SEA path only). Maps to the
39+
* kernel's `statement_conf` / SEA `statement_conf`, the same mechanism the
40+
* Thrift backend exposes as `confOverlay`. `queryTags` are merged into this map
41+
* under the `query_tags` key, mirroring the Thrift wire shape.
42+
*/
43+
statementConf?: Record<string, string>;
3044
};
3145

3246
export type TypeInfoRequest = {

lib/sea/SeaSessionBackend.ts

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -117,19 +117,17 @@ export default class SeaSessionBackend implements ISessionBackend {
117117
/**
118118
* Execute a SQL statement through the napi binding.
119119
*
120-
* Catalog / schema / sessionConf were applied at session open, so
121-
* there are no per-statement options to thread through.
120+
* Catalog / schema / sessionConf were applied at session open. The
121+
* per-statement options threaded here mirror the Thrift backend:
122+
* `ordinalParameters` / `namedParameters` (bound params), `queryTimeout`
123+
* (server wait timeout), `queryTags` (serialised into the conf overlay's
124+
* `query_tags` key), `statementConf` (arbitrary conf overlay), and
125+
* `rowLimit` (SEA-only server-side row cap).
122126
*
123-
* M0 intentionally rejects `queryTimeout`, `namedParameters`, and
124-
* `ordinalParameters` with explicit deferred-to-M1 errors. `useCloudFetch`
125-
* is a no-op on the SEA path — the kernel hardcodes the SEA
126-
* `disposition` to `INLINE_OR_EXTERNAL_LINKS`, and per-statement
127-
* conf overrides have no reader on the kernel; cloud-fetch behaviour
128-
* is governed entirely by the kernel's `ResultConfig` (M1 binding
129-
* surface).
130-
*
131-
* The Thrift backend remains the path for consumers that need any
132-
* of those today.
127+
* `useCloudFetch` is a no-op on the SEA path — the kernel hardcodes the
128+
* SEA `disposition` to `INLINE_OR_EXTERNAL_LINKS`; cloud-fetch behaviour
129+
* is governed by the kernel's `ResultConfig`. `maxRows` is the
130+
* client-side per-fetch chunk size, applied by the facade, not here.
133131
*/
134132
public async executeStatement(statement: string, options: ExecuteStatementOptions): Promise<IOperationBackend> {
135133
this.failIfClosed();
@@ -175,15 +173,26 @@ export default class SeaSessionBackend implements ISessionBackend {
175173
if (options.queryTimeout !== undefined) {
176174
nativeOptions.queryTimeoutSecs = Number(options.queryTimeout);
177175
}
178-
// Query tags: serialise JS-side into the conf overlay's `query_tags` key
179-
// (the same wire shape the Thrift backend produces via `serializeQueryTags`
180-
// → `confOverlay`). Not forwarded via the napi `queryTags` field: that's a
181-
// `HashMap<String,String>` which can't represent a null-valued tag, and the
182-
// kernel rejects setting both the field and a `query_tags` conf key. A
183-
// null-valued tag therefore round-trips as a key-only segment.
176+
// Server-side row cap (SEA `row_limit`). SEA-only — the Thrift backend has
177+
// no execute-time server cap, so there is no parity obligation here.
178+
if (options.rowLimit !== undefined) {
179+
nativeOptions.rowLimit = Number(options.rowLimit);
180+
}
181+
// Per-statement conf overlay (`statement_conf`) plus query tags. Tags are
182+
// serialised JS-side into the `query_tags` key (the same wire shape the
183+
// Thrift backend produces via `serializeQueryTags` → `confOverlay`), rather
184+
// than via the napi `queryTags` field: napi's `HashMap<String,String>`
185+
// can't represent a null-valued tag, and the kernel rejects setting both
186+
// the `queryTags` field and a `query_tags` conf key.
184187
const serializedQueryTags = serializeQueryTags(options.queryTags);
185-
if (serializedQueryTags !== undefined) {
186-
nativeOptions.statementConf = { query_tags: serializedQueryTags };
188+
if (options.statementConf !== undefined || serializedQueryTags !== undefined) {
189+
const statementConf: Record<string, string> = { ...(options.statementConf ?? {}) };
190+
if (serializedQueryTags !== undefined) {
191+
statementConf.query_tags = serializedQueryTags;
192+
}
193+
if (Object.keys(statementConf).length > 0) {
194+
nativeOptions.statementConf = statementConf;
195+
}
187196
}
188197
const hasOptions = Object.keys(nativeOptions).length > 0;
189198

tests/unit/sea/execution.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,33 @@ describe('SeaSessionBackend', () => {
506506
expect(connection.lastListSchemasArgs).to.deep.equal([undefined, '%']);
507507
});
508508

509+
it('executeStatement forwards rowLimit as napi rowLimit', async () => {
510+
const connection = new FakeNativeConnection();
511+
const session = makeSession(connection);
512+
await session.executeStatement('SELECT 1', { rowLimit: 500 });
513+
expect(connection.lastOptions?.rowLimit).to.equal(500);
514+
});
515+
516+
it('executeStatement forwards statementConf verbatim as napi statementConf', async () => {
517+
const connection = new FakeNativeConnection();
518+
const session = makeSession(connection);
519+
await session.executeStatement('SELECT 1', { statementConf: { 'spark.sql.ansi.enabled': 'true' } });
520+
expect(connection.lastOptions?.statementConf).to.deep.equal({ 'spark.sql.ansi.enabled': 'true' });
521+
});
522+
523+
it('executeStatement merges queryTags into a provided statementConf', async () => {
524+
const connection = new FakeNativeConnection();
525+
const session = makeSession(connection);
526+
await session.executeStatement('SELECT 1', {
527+
statementConf: { 'spark.sql.ansi.enabled': 'true' },
528+
queryTags: { team: 'data' },
529+
});
530+
expect(connection.lastOptions?.statementConf).to.deep.equal({
531+
'spark.sql.ansi.enabled': 'true',
532+
query_tags: 'team:data',
533+
});
534+
});
535+
509536
it('executeStatement uses the no-options fast path when nothing is bound', async () => {
510537
const connection = new FakeNativeConnection();
511538
const session = makeSession(connection);

tests/unit/sea/positionalParams.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,25 @@ describe('SeaPositionalParams.buildSeaPositionalParams', () => {
5454
{ sqlType: 'TIMESTAMP', value: '2024-01-15 10:30:00' },
5555
]);
5656
});
57+
58+
it('honours explicit TIMESTAMP_NTZ / TIMESTAMP_LTZ types (kernel param codec)', () => {
59+
expect(
60+
buildSeaPositionalParams([
61+
new DBSQLParameter({ type: DBSQLParameterType.TIMESTAMP_NTZ, value: '2024-01-15 10:30:00' }),
62+
new DBSQLParameter({ type: DBSQLParameterType.TIMESTAMP_LTZ, value: '2024-01-15 10:30:00' }),
63+
]),
64+
).to.deep.equal([
65+
{ sqlType: 'TIMESTAMP_NTZ', value: '2024-01-15 10:30:00' },
66+
{ sqlType: 'TIMESTAMP_LTZ', value: '2024-01-15 10:30:00' },
67+
]);
68+
});
69+
70+
it('routes a Date with explicit TIMESTAMP_NTZ type as NTZ (not the default TIMESTAMP)', () => {
71+
const d = new Date('2024-01-15T10:30:00.000Z');
72+
expect(
73+
buildSeaPositionalParams([new DBSQLParameter({ type: DBSQLParameterType.TIMESTAMP_NTZ, value: d })]),
74+
).to.deep.equal([{ sqlType: 'TIMESTAMP_NTZ', value: d.toISOString() }]);
75+
});
5776
});
5877

5978
describe('SeaPositionalParams.buildSeaNamedParams', () => {

0 commit comments

Comments
 (0)