Skip to content
Merged
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
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
41 changes: 26 additions & 15 deletions src/drivers/connection-ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
55 changes: 53 additions & 2 deletions src/drivers/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -168,9 +169,11 @@ export function popCallback<T extends ErrorCallback = ErrorCallback>(
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
}
}
Expand Down Expand Up @@ -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<string, any> = {}
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<string, any> = {}
Object.entries(value).forEach(([key, item]) => {
result[key] = decodeBigIntMarkers(item, safeIntegerMode)
})
return result
}

return value
}
10 changes: 9 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
61 changes: 61 additions & 0 deletions test/connection-ws-unit.test.ts
Original file line number Diff line number Diff line change
@@ -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)
}
})
})
})
77 changes: 74 additions & 3 deletions test/utilities.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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', () => {
Expand All @@ -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"')
})
Expand All @@ -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)
Expand Down
Loading