Skip to content

Commit bfb2dec

Browse files
author
Daniele Briggi
committed
feat: implement safe integer mode for SQLiteCloud connection string
1 parent b7f5b5c commit bfb2dec

File tree

9 files changed

+112
-30
lines changed

9 files changed

+112
-30
lines changed

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@sqlitecloud/drivers",
3-
"version": "1.0.779",
3+
"version": "1.0.834",
44
"description": "SQLiteCloud drivers for Typescript/Javascript in edge, web and node clients",
55
"main": "./lib/index.js",
66
"types": "./lib/index.d.ts",

src/drivers/connection-tls.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -221,17 +221,17 @@ export class SQLiteCloudTlsConnection extends SQLiteCloudConnection {
221221
this.processCommandsData(Buffer.alloc(0))
222222
return
223223
} else {
224-
const { data } = popData(decompressResults.buffer)
224+
const { data } = popData(decompressResults.buffer, this.config.safe_integer_mode)
225225
this.processCommandsFinish?.call(this, null, data)
226226
}
227227
} else {
228228
if (dataType !== CMD_ROWSET_CHUNK) {
229-
const { data } = popData(this.buffer)
229+
const { data } = popData(this.buffer, this.config.safe_integer_mode)
230230
this.processCommandsFinish?.call(this, null, data)
231231
} else {
232232
const completeChunk = bufferEndsWith(this.buffer, ROWSET_CHUNKS_END)
233233
if (completeChunk) {
234-
const parsedData = parseRowsetChunks([...this.pendingChunks, this.buffer])
234+
const parsedData = parseRowsetChunks([...this.pendingChunks, this.buffer], this.config.safe_integer_mode)
235235
this.processCommandsFinish?.call(this, null, parsedData)
236236
}
237237
}
@@ -241,7 +241,7 @@ export class SQLiteCloudTlsConnection extends SQLiteCloudConnection {
241241
// command with no explicit len so make sure that the final character is a space
242242
const lastChar = this.buffer.subarray(this.buffer.length - 1, this.buffer.length).toString('utf8')
243243
if (lastChar == ' ') {
244-
const { data } = popData(this.buffer)
244+
const { data } = popData(this.buffer, this.config.safe_integer_mode)
245245
this.processCommandsFinish?.call(this, null, data)
246246
}
247247
}

src/drivers/protocol.ts

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@
33
//
44

55
import { SQLiteCloudRowset } from './rowset'
6-
import { SAFE_INTEGER_MODE, SQLiteCloudCommand, SQLiteCloudError, type SQLCloudRowsetMetadata, type SQLiteCloudDataTypes } from './types'
6+
import {
7+
SAFE_INTEGER_MODE,
8+
SQLiteCloudCommand,
9+
SQLiteCloudError,
10+
type SQLCloudRowsetMetadata,
11+
type SQLiteCloudDataTypes,
12+
type SQLiteCloudSafeIntegerMode
13+
} from './types'
714
import { getSafeBuffer } from './safe-imports'
815

916
// explicitly importing buffer library to allow cross-platform support by replacing it
@@ -125,15 +132,15 @@ export function parseError(buffer: Buffer, spaceIndex: number): never {
125132
}
126133

127134
/** Parse an array of items (each of which will be parsed by type separately) */
128-
export function parseArray(buffer: Buffer, spaceIndex: number): SQLiteCloudDataTypes[] {
135+
export function parseArray(buffer: Buffer, spaceIndex: number, safeIntegerMode: SQLiteCloudSafeIntegerMode = SAFE_INTEGER_MODE): SQLiteCloudDataTypes[] {
129136
const parsedData = []
130137

131138
const array = buffer.subarray(spaceIndex + 1, buffer.length)
132139
const numberOfItems = parseInt(array.subarray(0, spaceIndex - 2).toString('utf8'))
133140
let arrayItems = array.subarray(array.indexOf(' ') + 1, array.length)
134141

135142
for (let i = 0; i < numberOfItems; i++) {
136-
const { data, fwdBuffer: buffer } = popData(arrayItems)
143+
const { data, fwdBuffer: buffer } = popData(arrayItems, safeIntegerMode)
137144
parsedData.push(data)
138145
arrayItems = buffer
139146
}
@@ -165,9 +172,9 @@ export function parseRowsetHeader(buffer: Buffer): { index: number; metadata: SQ
165172
}
166173

167174
/** Extract column names and, optionally, more metadata out of a rowset's header */
168-
function parseRowsetColumnsMetadata(buffer: Buffer, metadata: SQLCloudRowsetMetadata): Buffer {
175+
function parseRowsetColumnsMetadata(buffer: Buffer, metadata: SQLCloudRowsetMetadata, safeIntegerMode: SQLiteCloudSafeIntegerMode): Buffer {
169176
function popForward() {
170-
const { data, fwdBuffer: fwdBuffer } = popData(buffer) // buffer in parent scope
177+
const { data, fwdBuffer: fwdBuffer } = popData(buffer, safeIntegerMode) // buffer in parent scope
171178
buffer = fwdBuffer
172179
return data
173180
}
@@ -192,16 +199,16 @@ function parseRowsetColumnsMetadata(buffer: Buffer, metadata: SQLCloudRowsetMeta
192199
}
193200

194201
/** Parse a regular rowset (no chunks) */
195-
function parseRowset(buffer: Buffer, spaceIndex: number): SQLiteCloudRowset {
202+
function parseRowset(buffer: Buffer, spaceIndex: number, safeIntegerMode: SQLiteCloudSafeIntegerMode): SQLiteCloudRowset {
196203
buffer = buffer.subarray(spaceIndex + 1, buffer.length)
197204

198205
const { metadata, fwdBuffer } = parseRowsetHeader(buffer)
199-
buffer = parseRowsetColumnsMetadata(fwdBuffer, metadata)
206+
buffer = parseRowsetColumnsMetadata(fwdBuffer, metadata, safeIntegerMode)
200207

201208
// decode each rowset item
202209
const data = []
203210
for (let j = 0; j < metadata.numberOfRows * metadata.numberOfColumns; j++) {
204-
const { data: rowData, fwdBuffer } = popData(buffer)
211+
const { data: rowData, fwdBuffer } = popData(buffer, safeIntegerMode)
205212
data.push(rowData)
206213
buffer = fwdBuffer
207214
}
@@ -223,7 +230,7 @@ export function bufferEndsWith(buffer: Buffer, suffix: string): boolean {
223230
* *LEN 0:VERS NROWS NCOLS DATA
224231
* @see https://github.com/sqlitecloud/sdk/blob/master/PROTOCOL.md#scsp-rowset-chunk
225232
*/
226-
export function parseRowsetChunks(buffers: Buffer[]): SQLiteCloudRowset {
233+
export function parseRowsetChunks(buffers: Buffer[], safeIntegerMode: SQLiteCloudSafeIntegerMode = SAFE_INTEGER_MODE): SQLiteCloudRowset {
227234
let buffer = Buffer.concat(buffers)
228235
if (!bufferStartsWith(buffer, CMD_ROWSET_CHUNK) || !bufferEndsWith(buffer, ROWSET_CHUNKS_END)) {
229236
throw new Error('SQLiteCloudConnection.parseRowsetChunks - invalid chunks buffer')
@@ -245,14 +252,14 @@ export function parseRowsetChunks(buffers: Buffer[]): SQLiteCloudRowset {
245252
// first chunk? extract columns metadata
246253
if (chunkIndex === 1) {
247254
metadata = chunkMetadata
248-
buffer = parseRowsetColumnsMetadata(buffer, metadata)
255+
buffer = parseRowsetColumnsMetadata(buffer, metadata, safeIntegerMode)
249256
} else {
250257
metadata.numberOfRows += chunkMetadata.numberOfRows
251258
}
252259

253260
// extract single rowset row
254261
for (let k = 0; k < chunkMetadata.numberOfRows * metadata.numberOfColumns; k++) {
255-
const { data: itemData, fwdBuffer } = popData(buffer)
262+
const { data: itemData, fwdBuffer } = popData(buffer, safeIntegerMode)
256263
data.push(itemData)
257264
buffer = fwdBuffer
258265
}
@@ -276,7 +283,10 @@ function popIntegers(buffer: Buffer, numberOfIntegers = 1): { data: number[]; fw
276283
}
277284

278285
/** Parse command, extract its data, return the data and the buffer moved to the first byte after the command */
279-
export function popData(buffer: Buffer): { data: SQLiteCloudDataTypes | SQLiteCloudRowset; fwdBuffer: Buffer } {
286+
export function popData(
287+
buffer: Buffer,
288+
safeIntegerMode: SQLiteCloudSafeIntegerMode = SAFE_INTEGER_MODE
289+
): { data: SQLiteCloudDataTypes | SQLiteCloudRowset; fwdBuffer: Buffer } {
280290
function popResults(data: any) {
281291
const fwdBuffer = buffer.subarray(commandEnd)
282292
return { data, fwdBuffer }
@@ -307,10 +317,10 @@ export function popData(buffer: Buffer): { data: SQLiteCloudDataTypes | SQLiteCl
307317
case CMD_INT:
308318
// SQLite uses 64-bit INTEGER, but JS uses 53-bit Number
309319
const value = BigInt(buffer.subarray(1, spaceIndex).toString())
310-
if (SAFE_INTEGER_MODE === 'bigint') {
320+
if (safeIntegerMode === 'bigint') {
311321
return popResults(value)
312322
}
313-
if (SAFE_INTEGER_MODE === 'mixed') {
323+
if (safeIntegerMode === 'mixed') {
314324
if (value <= BigInt(Number.MIN_SAFE_INTEGER) || BigInt(Number.MAX_SAFE_INTEGER) <= value) {
315325
return popResults(value)
316326
}
@@ -333,9 +343,9 @@ export function popData(buffer: Buffer): { data: SQLiteCloudDataTypes | SQLiteCl
333343
case CMD_BLOB:
334344
return popResults(buffer.subarray(spaceIndex + 1, commandEnd))
335345
case CMD_ARRAY:
336-
return popResults(parseArray(buffer, spaceIndex))
346+
return popResults(parseArray(buffer, spaceIndex, safeIntegerMode))
337347
case CMD_ROWSET:
338-
return popResults(parseRowset(buffer, spaceIndex))
348+
return popResults(parseRowset(buffer, spaceIndex, safeIntegerMode))
339349
case CMD_ERROR:
340350
parseError(buffer, spaceIndex) // throws custom error
341351
break

src/drivers/types.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,14 @@ export const DEFAULT_PORT = 8860
1919
* (inlcuding `lastID` from WRITE statements)
2020
* mixed - use BigInt and Number types depending on the value size
2121
*/
22-
export let SAFE_INTEGER_MODE = 'number'
22+
export type SQLiteCloudSafeIntegerMode = 'number' | 'bigint' | 'mixed'
23+
24+
export let SAFE_INTEGER_MODE: SQLiteCloudSafeIntegerMode = 'number'
2325
if (typeof process !== 'undefined') {
24-
SAFE_INTEGER_MODE = process.env['SAFE_INTEGER_MODE']?.toLowerCase() || 'number'
26+
const mode = process.env['SAFE_INTEGER_MODE']?.toLowerCase()
27+
if (mode === 'bigint' || mode === 'mixed' || mode === 'number') {
28+
SAFE_INTEGER_MODE = mode
29+
}
2530
}
2631
if (SAFE_INTEGER_MODE == 'bigint') {
2732
console.debug('BigInt mode: Using Number for all INTEGER values from SQLite, including meta information from WRITE statements.')
@@ -79,6 +84,8 @@ export interface SQLiteCloudConfig {
7984
maxrows?: number
8085
/** Server should limit total number of rows in a set to maxRowset */
8186
maxrowset?: number
87+
/** How SQLite 64-bit INTEGER values are returned: number, bigint or mixed. Defaults to SAFE_INTEGER_MODE env var, then number */
88+
safe_integer_mode?: SQLiteCloudSafeIntegerMode
8289

8390
/** Custom options and configurations for tls socket, eg: additional certificates */
8491
tlsoptions?: tls.ConnectionOptions

src/drivers/utilities.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,16 @@
22
// utilities.ts - utility methods to manipulate SQL statements
33
//
44

5-
import { DEFAULT_PORT, DEFAULT_TIMEOUT, SQLiteCloudArrayType, SQLiteCloudConfig, SQLiteCloudDataTypes, SQLiteCloudError } from './types'
5+
import {
6+
DEFAULT_PORT,
7+
DEFAULT_TIMEOUT,
8+
SAFE_INTEGER_MODE,
9+
SQLiteCloudArrayType,
10+
SQLiteCloudConfig,
11+
SQLiteCloudDataTypes,
12+
SQLiteCloudError,
13+
SQLiteCloudSafeIntegerMode
14+
} from './types'
615
import { getSafeURL } from './safe-imports'
716

817
// explicitly importing these libraries to allow cross-platform support by replacing them
@@ -174,6 +183,7 @@ export function validateConfiguration(config: SQLiteCloudConfig): SQLiteCloudCon
174183
config.verbose = parseBoolean(config.verbose)
175184
config.noblob = parseBoolean(config.noblob)
176185
config.compression = config.compression != undefined && config.compression != null ? parseBoolean(config.compression) : true // default: true
186+
config.safe_integer_mode = parseSafeIntegerMode(config.safe_integer_mode || SAFE_INTEGER_MODE)
177187

178188
config.create = parseBoolean(config.create)
179189
config.non_linearizable = parseBoolean(config.non_linearizable)
@@ -242,6 +252,7 @@ export function parseconnectionstring(connectionstring: string): SQLiteCloudConf
242252
maxdata: options.maxdata ? parseInt(options.maxdata) : undefined,
243253
maxrows: options.maxrows ? parseInt(options.maxrows) : undefined,
244254
maxrowset: options.maxrowset ? parseInt(options.maxrowset) : undefined,
255+
safe_integer_mode: options.safe_integer_mode ? parseSafeIntegerMode(options.safe_integer_mode) : undefined,
245256
usewebsocket: options.usewebsocket ? parseBoolean(options.usewebsocket) : undefined,
246257
verbose: options.verbose ? parseBoolean(options.verbose) : undefined
247258
}
@@ -278,3 +289,12 @@ export function parseBooleanToZeroOne(value: string | boolean | null | undefined
278289
}
279290
return value ? 1 : 0
280291
}
292+
293+
/** Parse 64-bit integer handling mode */
294+
export function parseSafeIntegerMode(value: string | SQLiteCloudSafeIntegerMode | null | undefined): SQLiteCloudSafeIntegerMode {
295+
const mode = value?.toLowerCase()
296+
if (mode === 'number' || mode === 'bigint' || mode === 'mixed') {
297+
return mode
298+
}
299+
return 'number'
300+
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export { Database } from './drivers/database'
1111
export { SQLiteCloudConnection } from './drivers/connection'
1212
export {
1313
type SQLiteCloudConfig,
14+
type SQLiteCloudSafeIntegerMode,
1415
type SQLCloudRowsetMetadata,
1516
SQLiteCloudError,
1617
type ResultsCallback,

test/protocol.test.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// protocol.test.ts
33
//
44

5-
import { formatCommand, parseRowsetChunks } from '../src/drivers/protocol'
5+
import { formatCommand, parseRowsetChunks, popData } from '../src/drivers/protocol'
66
import { SQLiteCloudCommand } from '../src/drivers/types'
77

88
// response sent by the server when we TEST ROWSET_CHUNK
@@ -31,6 +31,30 @@ describe('parseRowsetChunks', () => {
3131
})
3232
})
3333

34+
describe('Safe integer mode', () => {
35+
it('should return numbers by default', () => {
36+
const { data } = popData(Buffer.from(':9007199254740992 '))
37+
expect(data).toBe(9007199254740992)
38+
expect(typeof data).toBe('number')
39+
})
40+
41+
it('should return bigint when mode is bigint', () => {
42+
const { data } = popData(Buffer.from(':42 '), 'bigint')
43+
expect(data).toBe(BigInt(42))
44+
expect(typeof data).toBe('bigint')
45+
})
46+
47+
it('should return bigint only for unsafe integers when mode is mixed', () => {
48+
const small = popData(Buffer.from(':42 '), 'mixed')
49+
const large = popData(Buffer.from(':9007199254740992 '), 'mixed')
50+
51+
expect(small.data).toBe(42)
52+
expect(typeof small.data).toBe('number')
53+
expect(large.data).toBe(BigInt('9007199254740992'))
54+
expect(typeof large.data).toBe('bigint')
55+
})
56+
})
57+
3458
const testCases = [
3559
{ query: "SELECT 'hello world'", parameters: [], expected: "+20 SELECT 'hello world'" },
3660
{

test/utilities.test.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
//
44

55
import { SQLiteCloudError } from '../src/index'
6-
import { getInitializationCommands, parseconnectionstring, sanitizeSQLiteIdentifier } from '../src/drivers/utilities'
6+
import { getInitializationCommands, parseconnectionstring, sanitizeSQLiteIdentifier, validateConfiguration } from '../src/drivers/utilities'
77
import { getTestingDatabaseName } from './shared'
88

99
import { expect, describe, it } from '@jest/globals'
@@ -163,6 +163,13 @@ describe('parseconnectionstring', () => {
163163
expect(config.timeout).toBe(123)
164164
})
165165

166+
it('should parse connection with safe integer mode', () => {
167+
const connectionstring = `sqlitecloud://host:1234/database?apikey=xxx&safe_integer_mode=bigint`
168+
const config = parseconnectionstring(connectionstring)
169+
170+
expect(config.safe_integer_mode).toBe('bigint')
171+
})
172+
166173
it('expect error when both user/pass and api key are set', () => {
167174
const connectionstring = 'sqlitecloud://user:password@host:1234/database?apikey=yyy'
168175
expect(() => parseconnectionstring(connectionstring)).toThrowError('Choose between apikey, token or username/password')
@@ -179,6 +186,19 @@ describe('parseconnectionstring', () => {
179186
})
180187
})
181188

189+
describe('validateConfiguration()', () => {
190+
it('should use safe integer mode from config', () => {
191+
const config = validateConfiguration({
192+
username: 'user',
193+
password: 'password',
194+
host: 'host',
195+
safe_integer_mode: 'mixed'
196+
})
197+
198+
expect(config.safe_integer_mode).toBe('mixed')
199+
})
200+
})
201+
182202
describe('getTestingDatabaseName', () => {
183203
it('should generate readable database names', () => {
184204
const database = getTestingDatabaseName('benchkmark')
@@ -218,4 +238,4 @@ describe('getInitializationCommands()', () => {
218238
expect(result).toContain('AUTH TOKEN mytoken;')
219239
expect(result).not.toContain('AUTH APIKEY')
220240
})
221-
})
241+
})

0 commit comments

Comments
 (0)