From 6620de2e4e49da709921dfe62f5214c975541213 Mon Sep 17 00:00:00 2001 From: Daniele Briggi <=> Date: Thu, 16 Apr 2026 16:23:35 +0200 Subject: [PATCH] feat: bigint support to ws --- package-lock.json | 4 +- package.json | 2 +- src/drivers/connection-ws.ts | 41 +++++++++++------- src/drivers/utilities.ts | 55 ++++++++++++++++++++++- src/index.ts | 10 ++++- test/connection-ws-unit.test.ts | 61 ++++++++++++++++++++++++++ test/utilities.test.ts | 77 +++++++++++++++++++++++++++++++-- 7 files changed, 226 insertions(+), 24 deletions(-) create mode 100644 test/connection-ws-unit.test.ts diff --git a/package-lock.json b/package-lock.json index c6960f5..13aef8b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@sqlitecloud/drivers", - "version": "1.0.834", + "version": "1.0.837", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@sqlitecloud/drivers", - "version": "1.0.834", + "version": "1.0.837", "license": "MIT", "dependencies": { "buffer": "^6.0.3", diff --git a/package.json b/package.json index 0673e19..e2d430f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sqlitecloud/drivers", - "version": "1.0.834", + "version": "1.0.837", "description": "SQLiteCloud drivers for Typescript/Javascript in edge, web and node clients", "main": "./lib/index.js", "types": "./lib/index.d.ts", diff --git a/src/drivers/connection-ws.ts b/src/drivers/connection-ws.ts index 381c939..5f3cf33 100644 --- a/src/drivers/connection-ws.ts +++ b/src/drivers/connection-ws.ts @@ -6,6 +6,7 @@ import { io, Socket } from 'socket.io-client' import { SQLiteCloudConnection } from './connection' import { SQLiteCloudRowset } from './rowset' import { ErrorCallback, ResultsCallback, SQLiteCloudCommand, SQLiteCloudConfig, SQLiteCloudError } from './types' +import { decodeBigIntMarkers, encodeBigIntMarkers } from './utilities' /** * Implementation of TransportConnection that connects to the database indirectly @@ -80,24 +81,34 @@ export class SQLiteCloudWebsocketConnection extends SQLiteCloudConnection { commands = { query: commands } } - this.socket.emit('GET /v2/weblite/sql', { sql: commands.query, bind: commands.parameters, row: 'array' }, (response: any) => { - if (response?.error) { - const error = new SQLiteCloudError(response.error.detail, { ...response.error }) - callback?.call(this, error) - } else { - const { data, metadata } = response - if (data && metadata) { - if (metadata.numberOfRows !== undefined && metadata.numberOfColumns !== undefined && metadata.columns !== undefined) { - console.assert(Array.isArray(data), 'SQLiteCloudWebsocketConnection.transportCommands - data is not an array') - // we can recreate a SQLiteCloudRowset from the response which we know to be an array of arrays - const rowset = new SQLiteCloudRowset(metadata, data.flat()) - callback?.call(this, null, rowset) - return + this.socket.emit( + 'GET /v2/weblite/sql', + { + sql: commands.query, + bind: encodeBigIntMarkers(commands.parameters), + row: 'array', + safe_integer_mode: this.config.safe_integer_mode + }, + (response: any) => { + if (response?.error) { + const error = new SQLiteCloudError(response.error.detail, { ...response.error }) + callback?.call(this, error) + } else { + const { metadata } = response + const data = decodeBigIntMarkers(response?.data, this.config.safe_integer_mode) + if (data && metadata) { + if (metadata.numberOfRows !== undefined && metadata.numberOfColumns !== undefined && metadata.columns !== undefined) { + console.assert(Array.isArray(data), 'SQLiteCloudWebsocketConnection.transportCommands - data is not an array') + // we can recreate a SQLiteCloudRowset from the response which we know to be an array of arrays + const rowset = new SQLiteCloudRowset(metadata, data.flat()) + callback?.call(this, null, rowset) + return + } } + callback?.call(this, null, data) } - callback?.call(this, null, response?.data) } - }) + ) return this } diff --git a/src/drivers/utilities.ts b/src/drivers/utilities.ts index c56649f..ad956dc 100644 --- a/src/drivers/utilities.ts +++ b/src/drivers/utilities.ts @@ -13,6 +13,7 @@ import { SQLiteCloudSafeIntegerMode } from './types' import { getSafeURL } from './safe-imports' +import { Buffer } from 'buffer' // explicitly importing these libraries to allow cross-platform support by replacing them // In React Native: Metro resolves 'whatwg-url' to 'react-native-url-polyfill' via package.json react-native field @@ -168,9 +169,11 @@ export function popCallback( export function validateConfiguration(config: SQLiteCloudConfig): SQLiteCloudConfig { console.assert(config, 'SQLiteCloudConnection.validateConfiguration - missing config') if (config.connectionstring) { + const configOverrides = Object.fromEntries(Object.entries(config).filter(([, value]) => value !== undefined)) + const connectionStringConfig = parseconnectionstring(config.connectionstring) config = { - ...config, - ...parseconnectionstring(config.connectionstring), + ...connectionStringConfig, + ...configOverrides, connectionstring: config.connectionstring // keep original connection string } } @@ -298,3 +301,51 @@ export function parseSafeIntegerMode(value: string | SQLiteCloudSafeIntegerMode } return 'number' } + +const BIGINT_MARKER_RE = /^-?\d+n$/ + +/** Convert values that JSON cannot represent losslessly into sqlitecloud-js bigint markers. */ +export function encodeBigIntMarkers(value: any): any { + if (typeof value === 'bigint') { + return `${value.toString()}n` + } + + if (Array.isArray(value)) { + return value.map(item => encodeBigIntMarkers(item)) + } + + if (value && typeof value === 'object' && !Buffer.isBuffer(value)) { + const result: Record = {} + Object.entries(value).forEach(([key, item]) => { + result[key] = encodeBigIntMarkers(item) + }) + return result + } + + return value +} + +/** Convert sqlitecloud-js bigint markers back into BigInt values for lossless integer modes. */ +export function decodeBigIntMarkers(value: any, safeIntegerMode?: SQLiteCloudSafeIntegerMode): any { + if (safeIntegerMode !== 'bigint' && safeIntegerMode !== 'mixed') { + return value + } + + if (typeof value === 'string' && BIGINT_MARKER_RE.test(value)) { + return BigInt(value.slice(0, -1)) + } + + if (Array.isArray(value)) { + return value.map(item => decodeBigIntMarkers(item, safeIntegerMode)) + } + + if (value && typeof value === 'object' && !Buffer.isBuffer(value)) { + const result: Record = {} + Object.entries(value).forEach(([key, item]) => { + result[key] = decodeBigIntMarkers(item, safeIntegerMode) + }) + return result + } + + return value +} diff --git a/src/index.ts b/src/index.ts index 64de62a..577086c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,5 +19,13 @@ export { type SQLiteCloudDataTypes } from './drivers/types' export { SQLiteCloudRowset, SQLiteCloudRow } from './drivers/rowset' -export { parseconnectionstring, validateConfiguration, getInitializationCommands, sanitizeSQLiteIdentifier } from './drivers/utilities' +export { + parseconnectionstring, + validateConfiguration, + getInitializationCommands, + sanitizeSQLiteIdentifier, + parseSafeIntegerMode, + encodeBigIntMarkers, + decodeBigIntMarkers +} from './drivers/utilities' export * as protocol from './drivers/protocol' diff --git a/test/connection-ws-unit.test.ts b/test/connection-ws-unit.test.ts new file mode 100644 index 0000000..bc93aeb --- /dev/null +++ b/test/connection-ws-unit.test.ts @@ -0,0 +1,61 @@ +/** + * connection-ws-unit.test.ts - websocket transport helpers + */ + +import { describe, expect, it, jest } from '@jest/globals' +import { SQLiteCloudWebsocketConnection } from '../src/drivers/connection-ws' +import { decodeBigIntMarkers, encodeBigIntMarkers } from '../src/drivers/utilities' + +describe('websocket bigint markers', () => { + it('should encode bigint values before sending JSON payloads', () => { + expect( + encodeBigIntMarkers({ + id: BigInt('9223372036854775807'), + values: [1, BigInt(2)] + }) + ).toEqual({ + id: '9223372036854775807n', + values: [1, '2n'] + }) + }) + + it('should decode bigint markers when safe integer mode is bigint or mixed', () => { + expect(decodeBigIntMarkers('9223372036854775807n', 'bigint')).toBe(BigInt('9223372036854775807')) + expect(decodeBigIntMarkers({ id: '9223372036854775807n' }, 'mixed')).toEqual({ + id: BigInt('9223372036854775807') + }) + }) + + it('should not decode bigint markers when safe integer mode is number', () => { + expect(decodeBigIntMarkers('9223372036854775807n', 'number')).toBe('9223372036854775807n') + }) + + it('should send safe integer mode and encoded bind parameters to the gateway', done => { + const connection = Object.create(SQLiteCloudWebsocketConnection.prototype) as any + const emit = jest.fn((_event: string, _payload: any, callback: (response: any) => void) => { + callback({ data: '123n' }) + }) + connection.socket = { connected: true, emit } + connection.config = { safe_integer_mode: 'bigint' } + + connection.transportCommands({ query: 'SELECT ?', parameters: [BigInt(123)] }, (error: Error | null, results: any) => { + try { + expect(error).toBeNull() + expect(results).toBe(BigInt(123)) + expect(emit).toHaveBeenCalledWith( + 'GET /v2/weblite/sql', + { + sql: 'SELECT ?', + bind: ['123n'], + row: 'array', + safe_integer_mode: 'bigint' + }, + expect.any(Function) + ) + done() + } catch (error) { + done(error as Error) + } + }) + }) +}) diff --git a/test/utilities.test.ts b/test/utilities.test.ts index 0da8df8..6ab458f 100644 --- a/test/utilities.test.ts +++ b/test/utilities.test.ts @@ -3,7 +3,15 @@ // import { SQLiteCloudError } from '../src/index' -import { getInitializationCommands, parseconnectionstring, sanitizeSQLiteIdentifier, validateConfiguration } from '../src/drivers/utilities' +import { + decodeBigIntMarkers, + encodeBigIntMarkers, + getInitializationCommands, + parseconnectionstring, + parseSafeIntegerMode, + sanitizeSQLiteIdentifier, + validateConfiguration +} from '../src/drivers/utilities' import { getTestingDatabaseName } from './shared' import { expect, describe, it } from '@jest/globals' @@ -197,6 +205,69 @@ describe('validateConfiguration()', () => { expect(config.safe_integer_mode).toBe('mixed') }) + + it('should use safe integer mode from config when connection string is provided', () => { + const connectionstring = 'sqlitecloud://host:1234/database?apikey=xxx' + const config = validateConfiguration({ + connectionstring, + safe_integer_mode: 'mixed' + }) + + expect(config.safe_integer_mode).toBe('mixed') + expect(config.connectionstring).toBe(connectionstring) + }) + + it('should use safe integer mode from connection string params', () => { + const config = validateConfiguration({ + connectionstring: 'sqlitecloud://host:1234/database?apikey=xxx&safe_integer_mode=bigint' + }) + + expect(config.safe_integer_mode).toBe('bigint') + }) + + it('should prefer config safe integer mode over connection string params', () => { + const config = validateConfiguration({ + connectionstring: 'sqlitecloud://host:1234/database?apikey=xxx&safe_integer_mode=bigint', + safe_integer_mode: 'mixed' + }) + + expect(config.safe_integer_mode).toBe('mixed') + }) + + it('should prefer all explicit config values over connection string params', () => { + const config = validateConfiguration({ + connectionstring: 'sqlitecloud://host:1234/database?apikey=xxx&timeout=123&insecure=1&maxrows=42', + timeout: 456, + insecure: false, + maxrows: 84 + }) + + expect(config.timeout).toBe(456) + expect(config.insecure).toBe(false) + expect(config.maxrows).toBe(84) + }) +}) + +describe('safe integer marker utilities', () => { + it('should parse safe integer mode', () => { + expect(parseSafeIntegerMode('bigint')).toBe('bigint') + expect(parseSafeIntegerMode('mixed')).toBe('mixed') + expect(parseSafeIntegerMode('number')).toBe('number') + expect(parseSafeIntegerMode('invalid')).toBe('number') + }) + + it('should encode bigint values as marker strings', () => { + expect(encodeBigIntMarkers({ id: BigInt('9223372036854775807'), values: [1, BigInt(2)] })).toEqual({ + id: '9223372036854775807n', + values: [1, '2n'] + }) + }) + + it('should decode bigint markers only in lossless modes', () => { + expect(decodeBigIntMarkers('9223372036854775807n', 'bigint')).toBe(BigInt('9223372036854775807')) + expect(decodeBigIntMarkers({ id: '9223372036854775807n' }, 'mixed')).toEqual({ id: BigInt('9223372036854775807') }) + expect(decodeBigIntMarkers('9223372036854775807n', 'number')).toBe('9223372036854775807n') + }) }) describe('getTestingDatabaseName', () => { @@ -214,7 +285,7 @@ describe('sanitizeSQLiteIdentifier()', () => { }) it('valid indentifier', () => { - const identifier = "a_colName1" + const identifier = 'a_colName1' const sanitized = sanitizeSQLiteIdentifier(identifier) expect(sanitized).toBe('"a_colName1"') }) @@ -230,7 +301,7 @@ describe('getInitializationCommands()', () => { it('should return commands with auth token command', () => { const config = { token: 'mytoken', - database: 'mydb', + database: 'mydb' } const result = getInitializationCommands(config)