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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@

# support to 64bit integers: number, bigint or mixed
SAFE_INTEGER_MODE=number

# chinook database used in non-destructive tests
CHINOOK_DATABASE_URL="sqlitecloud://user:password@xxx.sqlite.cloud:8860/chinook.sqlite"

Expand Down
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.438",
"version": "1.0.507",
"description": "SQLiteCloud drivers for Typescript/Javascript in edge, web and node clients",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
Expand Down
4 changes: 2 additions & 2 deletions src/drivers/connection-tls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,11 @@ export class SQLiteCloudTlsConnection extends SQLiteCloudConnection {
this.socket = undefined
}, timeoutMs)

this.socket?.write(formattedCommands, 'utf-8', () => {
this.socket?.write(formattedCommands, () => {
clearTimeout(timeout) // Clear the timeout on successful write
})
} else {
this.socket?.write(formattedCommands, 'utf-8')
this.socket?.write(formattedCommands)
}

return this
Expand Down
2 changes: 1 addition & 1 deletion src/drivers/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* connection.ts - base abstract class for sqlitecloud server connections
*/

import { SQLiteCloudConfig, SQLiteCloudError, ErrorCallback, ResultsCallback, SQLiteCloudCommand } from './types'
import { SQLiteCloudConfig, SQLiteCloudError, ErrorCallback, ResultsCallback, SQLiteCloudCommand, SQLiteCloudDataTypes } from './types'
import { validateConfiguration } from './utilities'
import { OperationsQueue } from './queue'
import { anonimizeCommand, getUpdateResults } from './utilities'
Expand Down
1 change: 1 addition & 0 deletions src/drivers/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
SQLiteCloudArrayType,
SQLiteCloudCommand,
SQLiteCloudConfig,
SQLiteCloudDataTypes,
SQLiteCloudError
} from './types'
import { isBrowser, popCallback } from './utilities'
Expand Down
47 changes: 31 additions & 16 deletions src/drivers/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
// protocol.ts - low level protocol handling for SQLiteCloud transport
//

import { SQLiteCloudCommand, SQLiteCloudError, type SQLCloudRowsetMetadata, type SQLiteCloudDataTypes } from './types'
import { SQLiteCloudRowset } from './rowset'
import { SAFE_INTEGER_MODE, SQLiteCloudCommand, SQLiteCloudError, type SQLCloudRowsetMetadata, type SQLiteCloudDataTypes } from './types'

// explicitly importing buffer library to allow cross-platform support by replacing it
import { Buffer } from 'buffer'
Expand Down Expand Up @@ -302,7 +302,17 @@ export function popData(buffer: Buffer): { data: SQLiteCloudDataTypes | SQLiteCl
// console.debug(`popData - dataType: ${dataType}, spaceIndex: ${spaceIndex}, commandLength: ${commandLength}, commandEnd: ${commandEnd}`)
switch (dataType) {
case CMD_INT:
return popResults(parseInt(buffer.subarray(1, spaceIndex).toString()))
// SQLite uses 64-bit INTEGER, but JS uses 53-bit Number
const value = BigInt(buffer.subarray(1, spaceIndex).toString())
if (SAFE_INTEGER_MODE === 'bigint') {
return popResults(value)
}
if (SAFE_INTEGER_MODE === 'mixed') {
if (value <= BigInt(Number.MIN_SAFE_INTEGER) || BigInt(Number.MAX_SAFE_INTEGER) <= value) {
return popResults(value)
}
}
return popResults(Number(value))
case CMD_FLOAT:
return popResults(parseFloat(buffer.subarray(1, spaceIndex).toString()))
case CMD_NULL:
Expand Down Expand Up @@ -334,7 +344,7 @@ export function popData(buffer: Buffer): { data: SQLiteCloudDataTypes | SQLiteCl
}

/** Format a command to be sent via SCSP protocol */
export function formatCommand(command: SQLiteCloudCommand): string {
export function formatCommand(command: SQLiteCloudCommand): Buffer {
// core returns null if there's a space after the semi column
// we want to maintain a compatibility with the standard sqlite3 driver
command.query = command.query.trim()
Expand All @@ -346,22 +356,23 @@ export function formatCommand(command: SQLiteCloudCommand): string {
return serializeData(command.query, false)
}

function serializeCommand(data: SQLiteCloudDataTypes[], zeroString: boolean = false): string {
function serializeCommand(data: SQLiteCloudDataTypes[], zeroString: boolean = false): Buffer {
const n = data.length
let serializedData = `${n} `
let serializedData = Buffer.from(`${n} `)

for (let i = 0; i < n; i++) {
// the first string is the sql and it must be zero-terminated
const zs = i == 0 || zeroString
serializedData += serializeData(data[i], zs)
serializedData = Buffer.concat([serializedData, serializeData(data[i], zs)])
}

const bytesTotal = Buffer.byteLength(serializedData, 'utf-8')
const header = `${CMD_ARRAY}${bytesTotal} `
return header + serializedData
const bytesTotal = serializedData.byteLength
const header = Buffer.from(`${CMD_ARRAY}${bytesTotal} `)

return Buffer.concat([header, serializedData])
}

function serializeData(data: SQLiteCloudDataTypes, zeroString: boolean = false): string {
function serializeData(data: SQLiteCloudDataTypes, zeroString: boolean = false): Buffer {
if (typeof data === 'string') {
let cmd = CMD_STRING
if (zeroString) {
Expand All @@ -370,24 +381,28 @@ function serializeData(data: SQLiteCloudDataTypes, zeroString: boolean = false):
}

const header = `${cmd}${Buffer.byteLength(data, 'utf-8')} `
return header + data
return Buffer.from(header + data)
}

if (typeof data === 'number') {
if (Number.isInteger(data)) {
return `${CMD_INT}${data} `
return Buffer.from(`${CMD_INT}${data} `)
} else {
return `${CMD_FLOAT}${data} `
return Buffer.from(`${CMD_FLOAT}${data} `)
}
}

if (typeof data === 'bigint') {
return Buffer.from(`${CMD_INT}${data} `)
}

if (Buffer.isBuffer(data)) {
const header = `${CMD_BLOB}${data.length} `
return header + data.toString('utf-8')
const header = `${CMD_BLOB}${data.byteLength} `
return Buffer.concat([Buffer.from(header), data])
}

if (data === null || data === undefined) {
return `${CMD_NULL} `
return Buffer.from(`${CMD_NULL} `)
}

if (Array.isArray(data)) {
Expand Down
22 changes: 22 additions & 0 deletions src/drivers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,26 @@ export const DEFAULT_TIMEOUT = 300 * 1000
/** Default tls connection port */
export const DEFAULT_PORT = 8860

/**
* Support to SQLite 64bit integer
*
* number - (default) always return Number type (max: 2^53 - 1)
* Precision is lost when selecting greater numbers from SQLite
* bigint - always return BigInt type (max: 2^63 - 1) for all numbers from SQLite
* (inlcuding `lastID` from WRITE statements)
* mixed - use BigInt and Number types depending on the value size
*/
export let SAFE_INTEGER_MODE = 'number'
if (typeof process !== 'undefined') {
SAFE_INTEGER_MODE = process.env['SAFE_INTEGER_MODE']?.toLowerCase() || 'number'
}
if (SAFE_INTEGER_MODE == 'bigint') {
console.debug('BigInt mode: Using Number for all INTEGER values from SQLite, including meta information from WRITE statements.')
}
if (SAFE_INTEGER_MODE == 'mixed') {
console.debug('Mixed mode: Using BigInt for INTEGER values from SQLite (including meta information from WRITE statements) bigger then 2^53, Number otherwise.')
}

/**
* Configuration for SQLite cloud connection
* @note Options are all lowecase so they 1:1 compatible with C SDK
Expand All @@ -26,6 +46,8 @@ export interface SQLiteCloudConfig {
password_hashed?: boolean
/** API key can be provided instead of username and password */
apikey?: string
/** Access Token provided in place of API Key or username/password */
token?: string

/** Host name is required unless connectionstring is provided, eg: xxx.sqlitecloud.io */
host?: string
Expand Down
65 changes: 32 additions & 33 deletions src/drivers/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@
// utilities.ts - utility methods to manipulate SQL statements
//

import { SQLiteCloudConfig, SQLiteCloudError, SQLiteCloudDataTypes, DEFAULT_PORT, DEFAULT_TIMEOUT } from './types'
import { SQLiteCloudArrayType } from './types'
import { DEFAULT_PORT, DEFAULT_TIMEOUT, SQLiteCloudArrayType, SQLiteCloudConfig, SQLiteCloudDataTypes, SQLiteCloudError } from './types'

// explicitly importing these libraries to allow cross-platform support by replacing them
import { URL } from 'whatwg-url'
import { Buffer } from 'buffer'

//
// determining running environment, thanks to browser-or-node
Expand Down Expand Up @@ -42,45 +40,47 @@ export function anonimizeError(error: Error): Error {
export function getInitializationCommands(config: SQLiteCloudConfig): string {
// we check the credentials using non linearizable so we're quicker
// then we bring back linearizability unless specified otherwise
let commands = 'SET CLIENT KEY NONLINEARIZABLE TO 1; '
let commands = 'SET CLIENT KEY NONLINEARIZABLE TO 1;'

// first user authentication, then all other commands
if (config.apikey) {
commands += `AUTH APIKEY ${config.apikey}; `
commands += `AUTH APIKEY ${config.apikey};`
} else if (config.token) {
commands += `AUTH TOKEN ${config.token};`
} else {
commands += `AUTH USER ${config.username || ''} ${config.password_hashed ? 'HASH' : 'PASSWORD'} ${config.password || ''}; `
commands += `AUTH USER ${config.username || ''} ${config.password_hashed ? 'HASH' : 'PASSWORD'} ${config.password || ''};`
}

if (config.compression) {
commands += 'SET CLIENT KEY COMPRESSION TO 1; '
commands += 'SET CLIENT KEY COMPRESSION TO 1;'
}
if (config.zerotext) {
commands += 'SET CLIENT KEY ZEROTEXT TO 1; '
commands += 'SET CLIENT KEY ZEROTEXT TO 1;'
}
if (config.noblob) {
commands += 'SET CLIENT KEY NOBLOB TO 1; '
commands += 'SET CLIENT KEY NOBLOB TO 1;'
}
if (config.maxdata) {
commands += `SET CLIENT KEY MAXDATA TO ${config.maxdata}; `
commands += `SET CLIENT KEY MAXDATA TO ${config.maxdata};`
}
if (config.maxrows) {
commands += `SET CLIENT KEY MAXROWS TO ${config.maxrows}; `
commands += `SET CLIENT KEY MAXROWS TO ${config.maxrows};`
}
if (config.maxrowset) {
commands += `SET CLIENT KEY MAXROWSET TO ${config.maxrowset}; `
commands += `SET CLIENT KEY MAXROWSET TO ${config.maxrowset};`
}

// we ALWAYS set non linearizable to 1 when we start so we can be quicker on login
// but then we need to put it back to its default value if "linearizable" unless set
if (!config.non_linearizable) {
commands += 'SET CLIENT KEY NONLINEARIZABLE TO 0; '
commands += 'SET CLIENT KEY NONLINEARIZABLE TO 0;'
}

if (config.database) {
if (config.create && !config.memory) {
commands += `CREATE DATABASE ${config.database} IF NOT EXISTS; `
commands += `CREATE DATABASE ${config.database} IF NOT EXISTS;`
}
commands += `USE DATABASE ${config.database}; `
commands += `USE DATABASE ${config.database};`
}

return commands
Expand Down Expand Up @@ -109,13 +109,12 @@ export function getUpdateResults(results?: any): Record<string, any> | undefined
switch (results[0]) {
case SQLiteCloudArrayType.ARRAY_TYPE_SQLITE_EXEC:
return {
type: results[0],
index: results[1],
type: Number(results[0]),
index: Number(results[1]),
lastID: results[2], // ROWID (sqlite3_last_insert_rowid)
changes: results[3], // CHANGES(sqlite3_changes)
totalChanges: results[4], // TOTAL_CHANGES (sqlite3_total_changes)
finalized: results[5], // FINALIZED
//
finalized: Number(results[5]), // FINALIZED
rowId: results[2] // same as lastId
}
}
Expand Down Expand Up @@ -177,16 +176,19 @@ export function validateConfiguration(config: SQLiteCloudConfig): SQLiteCloudCon
config.non_linearizable = parseBoolean(config.non_linearizable)
config.insecure = parseBoolean(config.insecure)

const hasCredentials = (config.username && config.password) || config.apikey
const hasCredentials = (config.username && config.password) || config.apikey || config.token
if (!config.host || !hasCredentials) {
console.error('SQLiteCloudConnection.validateConfiguration - missing arguments', config)
throw new SQLiteCloudError('The user, password and host arguments or the ?apikey= must be specified.', { errorCode: 'ERR_MISSING_ARGS' })
throw new SQLiteCloudError('The user, password and host arguments, the ?apikey= or the ?token= must be specified.', { errorCode: 'ERR_MISSING_ARGS' })
}

if (!config.connectionstring) {
// build connection string from configuration, values are already validated
config.connectionstring = `sqlitecloud://${config.host}:${config.port}/${config.database || ''}`
if (config.apikey) {
config.connectionstring = `sqlitecloud://${config.host}:${config.port}/${config.database || ''}?apikey=${config.apikey}`
config.connectionstring += `?apikey=${config.apikey}`
} else if (config.token) {
config.connectionstring += `?token=${config.token}`
} else {
config.connectionstring = `sqlitecloud://${encodeURIComponent(config.username || '')}:${encodeURIComponent(config.password || '')}@${config.host}:${
config.port
Expand Down Expand Up @@ -215,13 +217,13 @@ export function parseconnectionstring(connectionstring: string): SQLiteCloudConf
// all lowecase options
const options: { [key: string]: string } = {}
url.searchParams.forEach((value, key) => {
options[key.toLowerCase().replace(/-/g, '_')] = value
options[key.toLowerCase().replace(/-/g, '_')] = value.trim()
})

const config: SQLiteCloudConfig = {
...options,
username: decodeURIComponent(url.username),
password: decodeURIComponent(url.password),
username: url.username ? decodeURIComponent(url.username) : undefined,
password: url.password ? decodeURIComponent(url.password) : undefined,
password_hashed: options.password_hashed ? parseBoolean(options.password_hashed) : undefined,
host: url.hostname,
// type cast values
Expand All @@ -241,13 +243,10 @@ export function parseconnectionstring(connectionstring: string): SQLiteCloudConf
verbose: options.verbose ? parseBoolean(options.verbose) : undefined
}

// either you use an apikey or username and password
if (config.apikey) {
if (config.username || config.password) {
console.warn('SQLiteCloudConnection.parseconnectionstring - apikey and username/password are both specified, using apikey')
}
delete config.username
delete config.password
// either you use an apikey, token or username and password
if (Number(!!config.apikey) + Number(!!config.token) + Number(!!(config.username || config.password)) > 1) {
console.error('SQLiteCloudConnection.parseconnectionstring - choose between apikey, token or username/password')
throw new SQLiteCloudError('Choose between apikey, token or username/password')
}

const database = url.pathname.replace('/', '') // pathname is database name, remove the leading slash
Expand All @@ -257,7 +256,7 @@ export function parseconnectionstring(connectionstring: string): SQLiteCloudConf

return config
} catch (error) {
throw new SQLiteCloudError(`Invalid connection string: ${connectionstring}`)
throw new SQLiteCloudError(`Invalid connection string: ${connectionstring} - error: ${error}`)
}
}

Expand Down
Loading
Loading