Skip to content

Commit 779055d

Browse files
committed
feat: add options for custom bigint and timestamp conversions in fetchRow and fetchAll
1 parent 19bcdaa commit 779055d

7 files changed

Lines changed: 137 additions & 34 deletions

File tree

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,8 @@ function fetchRow(
209209
```
210210
- Streams each row to `options.onEachRow`.
211211
- Use `format: 'JSON_OBJECT'` to map rows into schema-based objects.
212+
- Use `encodeBigInt` to customize BIGINT/LONG conversions when using `JSON_OBJECT`.
213+
- Use `encodeTimestamp` to customize TIMESTAMP* conversions when using `JSON_OBJECT`.
212214
- Supports `INLINE` results or `JSON_ARRAY` formatted `EXTERNAL_LINKS` only.
213215
- If only a subset of external links is returned, missing chunk metadata is fetched by index.
214216

@@ -274,15 +276,19 @@ type ExecuteStatementOptions = {
274276
275277
type FetchRowsOptions = {
276278
signal?: AbortSignal
277-
onEachRow?: (row: RowArray | RowObject) => void
278279
format?: 'JSON_ARRAY' | 'JSON_OBJECT'
279280
logger?: Logger
281+
onEachRow?: (row: RowArray | RowObject) => void
282+
encodeBigInt?: (value: bigint) => unknown
283+
encodeTimestamp?: (value: string) => unknown
280284
}
281285
282286
type FetchAllOptions = {
283287
signal?: AbortSignal
284288
format?: 'JSON_ARRAY' | 'JSON_OBJECT'
285289
logger?: Logger
290+
encodeBigInt?: (value: bigint) => unknown
291+
encodeTimestamp?: (value: string) => unknown
286292
}
287293
288294
type FetchStreamOptions = {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@bitofsky/databricks-sql",
3-
"version": "1.0.4",
3+
"version": "1.0.5",
44
"description": "Databricks SQL client for Node.js - Direct REST API without SDK",
55
"main": "dist/index.cjs",
66
"module": "dist/index.js",

src/api/fetchAll.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ export async function fetchAll(
4141
if (options.logger)
4242
fetchOptions.logger = options.logger
4343

44+
if (options.encodeBigInt)
45+
fetchOptions.encodeBigInt = options.encodeBigInt
46+
47+
if (options.encodeTimestamp)
48+
fetchOptions.encodeTimestamp = options.encodeTimestamp
49+
4450
await fetchRow(statementResult, auth, fetchOptions)
4551
logger?.info?.(`fetchAll fetched ${rows.length} rows for statement ${statementId}.`, {
4652
...logContext,

src/api/fetchRow.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ export async function fetchRow(
3030
const statementId = statementResult.statement_id
3131
const logContext = { statementId, manifest, requestedFormat: format }
3232
// Map JSON_ARRAY rows to JSON_OBJECT when requested.
33-
const mapRow = createRowMapper(manifest, format)
33+
const mapRow = createRowMapper(manifest, format, {
34+
...options.encodeBigInt ? { encodeBigInt: options.encodeBigInt } : {},
35+
...options.encodeTimestamp ? { encodeTimestamp: options.encodeTimestamp } : {},
36+
})
3437

3538
logger?.info?.(`fetchRow fetching rows for statement ${statementId}.`, {
3639
...logContext,

src/createRowMapper.ts

Lines changed: 58 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
FetchRowsOptions,
55
RowArray,
66
RowObject,
7+
RowMapperOptions,
78
StatementManifest,
89
} from './types.js'
910

@@ -30,12 +31,10 @@ const INTEGER_TYPES = new Set(['TINYINT', 'SMALLINT', 'INT'])
3031
const BIGINT_TYPES = new Set(['BIGINT', 'LONG'])
3132
const FLOAT_TYPES = new Set(['FLOAT', 'DOUBLE'])
3233
const BOOLEAN_TYPES = new Set(['BOOLEAN'])
34+
const TIMESTAMP_TYPES = new Set(['TIMESTAMP', 'TIMESTAMP_NTZ', 'TIMESTAMP_LTZ'])
3335
const STRING_TYPES = new Set([
3436
'STRING',
3537
'DATE',
36-
'TIMESTAMP',
37-
'TIMESTAMP_NTZ',
38-
'TIMESTAMP_LTZ',
3938
'TIME',
4039
])
4140

@@ -46,15 +45,16 @@ const STRING_TYPES = new Set([
4645
*/
4746
export function createRowMapper(
4847
manifest: StatementManifest,
49-
format: FetchRowsOptions['format']
48+
format: FetchRowsOptions['format'],
49+
options: RowMapperOptions = {}
5050
): RowMapper {
5151
if (format !== 'JSON_OBJECT')
5252
return (row) => row
5353

5454
// Precompute per-column converters for fast row mapping.
5555
const columnConverters = manifest.schema.columns.map((column: ColumnInfo) => ({
5656
name: column.name,
57-
convert: createColumnConverter(column),
57+
convert: createColumnConverter(column, options),
5858
}))
5959

6060
return (row) => {
@@ -72,9 +72,12 @@ export function createRowMapper(
7272
}
7373
}
7474

75-
function createColumnConverter(column: ColumnInfo): (value: unknown) => unknown {
75+
function createColumnConverter(
76+
column: ColumnInfo,
77+
options: RowMapperOptions
78+
): (value: unknown) => unknown {
7679
const descriptor = parseColumnType(column)
77-
return (value) => convertValue(descriptor, value)
80+
return (value) => convertValue(descriptor, value, options)
7881
}
7982

8083
function parseColumnType(column: ColumnInfo): TypeDescriptor {
@@ -255,19 +258,23 @@ function stripNotNull(typeText: string): string {
255258
return trimmed
256259
}
257260

258-
function convertValue(descriptor: TypeDescriptor, value: unknown): unknown {
261+
function convertValue(
262+
descriptor: TypeDescriptor,
263+
value: unknown,
264+
options: RowMapperOptions
265+
): unknown {
259266
if (value === null || value === undefined)
260267
return value
261268

262269
if (descriptor.typeName === 'STRUCT' && descriptor.fields)
263270
// STRUCT values are JSON strings in JSON_ARRAY format.
264-
return convertStructValue(descriptor.fields, value)
271+
return convertStructValue(descriptor.fields, value, options)
265272

266273
if (descriptor.typeName === 'ARRAY' && descriptor.elementType)
267-
return convertArrayValue(descriptor.elementType, value)
274+
return convertArrayValue(descriptor.elementType, value, options)
268275

269276
if (descriptor.typeName === 'MAP' && descriptor.keyType && descriptor.valueType)
270-
return convertMapValue(descriptor.keyType, descriptor.valueType, value)
277+
return convertMapValue(descriptor.keyType, descriptor.valueType, value, options)
271278

272279
if (descriptor.typeName === 'DECIMAL')
273280
return convertNumber(value)
@@ -276,45 +283,57 @@ function convertValue(descriptor: TypeDescriptor, value: unknown): unknown {
276283
return convertNumber(value)
277284

278285
if (BIGINT_TYPES.has(descriptor.typeName))
279-
return convertInteger(value)
286+
return convertInteger(value, options.encodeBigInt)
280287

281288
if (FLOAT_TYPES.has(descriptor.typeName))
282289
return convertNumber(value)
283290

284291
if (BOOLEAN_TYPES.has(descriptor.typeName))
285292
return convertBoolean(value)
286293

294+
if (TIMESTAMP_TYPES.has(descriptor.typeName))
295+
return convertTimestamp(value, options.encodeTimestamp)
296+
287297
if (STRING_TYPES.has(descriptor.typeName))
288298
return value
289299

290300
return value
291301
}
292302

293-
function convertStructValue(fields: StructField[], value: unknown): unknown {
303+
function convertStructValue(
304+
fields: StructField[],
305+
value: unknown,
306+
options: RowMapperOptions
307+
): unknown {
294308
const raw = parseStructValue(value)
295309
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
296310
return value
297311

298312
// Apply nested field conversions based on the parsed STRUCT schema.
299313
const mapped: RowObject = {}
300314
for (const field of fields)
301-
mapped[field.name] = convertValue(field.type, (raw as RowObject)[field.name])
315+
mapped[field.name] = convertValue(field.type, (raw as RowObject)[field.name], options)
302316

303317
return mapped
304318
}
305319

306-
function convertArrayValue(elementType: TypeDescriptor, value: unknown): unknown {
320+
function convertArrayValue(
321+
elementType: TypeDescriptor,
322+
value: unknown,
323+
options: RowMapperOptions
324+
): unknown {
307325
const raw = parseJsonValue(value)
308326
if (!Array.isArray(raw))
309327
return value
310328

311-
return raw.map((entry) => convertValue(elementType, entry))
329+
return raw.map((entry) => convertValue(elementType, entry, options))
312330
}
313331

314332
function convertMapValue(
315333
keyType: TypeDescriptor,
316334
valueType: TypeDescriptor,
317-
value: unknown
335+
value: unknown,
336+
options: RowMapperOptions
318337
): unknown {
319338
const raw = parseJsonValue(value)
320339
if (!raw || typeof raw !== 'object')
@@ -325,16 +344,16 @@ function convertMapValue(
325344
for (const entry of raw) {
326345
if (!Array.isArray(entry) || entry.length < 2)
327346
continue
328-
const convertedKey = convertValue(keyType, entry[0])
329-
mapped[String(convertedKey)] = convertValue(valueType, entry[1])
347+
const convertedKey = convertValue(keyType, entry[0], options)
348+
mapped[String(convertedKey)] = convertValue(valueType, entry[1], options)
330349
}
331350
return mapped
332351
}
333352

334353
const mapped: RowObject = {}
335354
for (const [key, entryValue] of Object.entries(raw)) {
336-
const convertedKey = convertValue(keyType, key)
337-
mapped[String(convertedKey)] = convertValue(valueType, entryValue)
355+
const convertedKey = convertValue(keyType, key, options)
356+
mapped[String(convertedKey)] = convertValue(valueType, entryValue, options)
338357
}
339358

340359
return mapped
@@ -372,20 +391,23 @@ function convertNumber(value: unknown): unknown {
372391
return value
373392
}
374393

375-
function convertInteger(value: unknown): unknown {
394+
function convertInteger(value: unknown, encodeBigInt?: (value: bigint) => unknown): unknown {
376395
if (typeof value === 'bigint')
377-
return value
396+
return encodeBigInt ? encodeBigInt(value) : value
378397

379398
if (typeof value === 'number') {
380-
if (Number.isInteger(value))
381-
return BigInt(value)
399+
if (Number.isInteger(value)) {
400+
const bigintValue = BigInt(value)
401+
return encodeBigInt ? encodeBigInt(bigintValue) : bigintValue
402+
}
382403
return value
383404
}
384405

385406
if (typeof value === 'string') {
386407
try {
387408
// Preserve integer semantics for BIGINT/DECIMAL(scale=0) by returning bigint.
388-
return BigInt(value)
409+
const bigintValue = BigInt(value)
410+
return encodeBigInt ? encodeBigInt(bigintValue) : bigintValue
389411
} catch {
390412
return value
391413
}
@@ -394,6 +416,16 @@ function convertInteger(value: unknown): unknown {
394416
return value
395417
}
396418

419+
function convertTimestamp(
420+
value: unknown,
421+
encodeTimestamp?: (value: string) => unknown
422+
): unknown {
423+
if (typeof value !== 'string')
424+
return value
425+
426+
return encodeTimestamp ? encodeTimestamp(value) : value
427+
}
428+
397429
function convertBoolean(value: unknown): unknown {
398430
if (typeof value === 'boolean')
399431
return value

src/types.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,14 @@ export type RowArray = unknown[]
235235
/** Row data as JSON object */
236236
export type RowObject = Record<string, unknown>
237237

238+
/** Options for row mapping */
239+
export type RowMapperOptions = {
240+
/** Hook to transform bigint values (e.g., to string or number) */
241+
encodeBigInt?: (value: bigint) => unknown
242+
/** Hook to transform timestamp-like values (e.g., to Date) */
243+
encodeTimestamp?: (value: string) => unknown
244+
}
245+
238246
/** Format for fetchRow/fetchAll */
239247
export type FetchRowFormat = 'JSON_ARRAY' | 'JSON_OBJECT'
240248

@@ -248,12 +256,16 @@ export type FetchStreamOptions = SignalOptions & {
248256

249257
/** Options for fetchRow */
250258
export type FetchRowsOptions = SignalOptions & {
251-
/** Callback for each row */
252-
onEachRow?: (row: RowArray | RowObject) => void
253259
/** Row format (default: JSON_ARRAY) */
254260
format?: FetchRowFormat
255261
/** Optional logger for lifecycle events */
256262
logger?: Logger
263+
/** Callback for each row */
264+
onEachRow?: (row: RowArray | RowObject) => void
265+
/** Customize bigint conversion for JSON_OBJECT rows */
266+
encodeBigInt?: RowMapperOptions['encodeBigInt']
267+
/** Customize TIMESTAMP* conversion for JSON_OBJECT rows */
268+
encodeTimestamp?: RowMapperOptions['encodeTimestamp']
257269
}
258270

259271
/** Options for fetchAll */
@@ -262,6 +274,10 @@ export type FetchAllOptions = SignalOptions & {
262274
format?: FetchRowFormat
263275
/** Optional logger for lifecycle events */
264276
logger?: Logger
277+
/** Customize bigint conversion for JSON_OBJECT rows */
278+
encodeBigInt?: RowMapperOptions['encodeBigInt']
279+
/** Customize TIMESTAMP* conversion for JSON_OBJECT rows */
280+
encodeTimestamp?: RowMapperOptions['encodeTimestamp']
265281
}
266282

267283
/** Result from mergeStreamToExternalLink callback */

test/createRowMapper.spec.ts

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, it, expect } from 'vitest'
22
import { createRowMapper } from '../src/createRowMapper.js'
3-
import type { FetchRowsOptions, StatementManifest } from '../src/types.js'
3+
import type { FetchRowsOptions, RowMapperOptions, StatementManifest } from '../src/types.js'
44

55
type Column = StatementManifest['schema']['columns'][number]
66

@@ -23,10 +23,11 @@ function buildManifest(columns: Column[]): StatementManifest {
2323
function mapRow(
2424
columns: Column[],
2525
row: unknown[],
26-
format: FetchRowsOptions['format'] = 'JSON_OBJECT'
26+
format: FetchRowsOptions['format'] = 'JSON_OBJECT',
27+
options?: RowMapperOptions
2728
) {
2829
const manifest = buildManifest(columns)
29-
const mapper = createRowMapper(manifest, format)
30+
const mapper = createRowMapper(manifest, format, options)
3031
return mapper(row)
3132
}
3233

@@ -53,6 +54,21 @@ describe('createRowMapper', () => {
5354
})
5455
})
5556

57+
it('applies encodeBigInt to bigint values', () => {
58+
const columns: Column[] = [
59+
{ name: 'big_col', type_text: 'BIGINT', type_name: 'BIGINT', position: 0 },
60+
{ name: 'nested', type_text: 'STRUCT<inner: BIGINT>', type_name: 'STRUCT', position: 1 },
61+
]
62+
const row = ['9007199254740993', '{"inner":"42"}']
63+
const options: RowMapperOptions = {
64+
encodeBigInt: (value) => value.toString(),
65+
}
66+
expect(mapRow(columns, row, 'JSON_OBJECT', options)).toEqual({
67+
big_col: '9007199254740993',
68+
nested: { inner: '42' },
69+
})
70+
})
71+
5672
it('converts float and double values to numbers', () => {
5773
const columns: Column[] = [
5874
{ name: 'float_col', type_text: 'FLOAT', type_name: 'FLOAT', position: 0 },
@@ -91,6 +107,30 @@ describe('createRowMapper', () => {
91107
})
92108
})
93109

110+
it('applies encodeTimestamp to TIMESTAMP types only', () => {
111+
const columns: Column[] = [
112+
{ name: 'd', type_text: 'DATE', type_name: 'DATE', position: 0 },
113+
{ name: 't', type_text: 'TIME', type_name: 'TIME', position: 1 },
114+
{ name: 'ts', type_text: 'TIMESTAMP', type_name: 'TIMESTAMP', position: 2 },
115+
{ name: 'ts_ltz', type_text: 'TIMESTAMP_LTZ', type_name: 'TIMESTAMP_LTZ', position: 3 },
116+
]
117+
const row = [
118+
'2024-01-02',
119+
'03:04:05',
120+
'2024-01-02T03:04:05.123Z',
121+
'2024-01-02T03:04:05.123Z',
122+
]
123+
const options: RowMapperOptions = {
124+
encodeTimestamp: (value) => `ts:${value}`,
125+
}
126+
expect(mapRow(columns, row, 'JSON_OBJECT', options)).toEqual({
127+
d: '2024-01-02',
128+
t: '03:04:05',
129+
ts: 'ts:2024-01-02T03:04:05.123Z',
130+
ts_ltz: 'ts:2024-01-02T03:04:05.123Z',
131+
})
132+
})
133+
94134
it('converts boolean string values', () => {
95135
const columns: Column[] = [
96136
{ name: 'flag', type_text: 'BOOLEAN', type_name: 'BOOLEAN', position: 0 },

0 commit comments

Comments
 (0)