Skip to content

Commit ea6e4df

Browse files
author
Daniele Briggi
committed
feat(statement): server side prepared statement
1 parent 5bdba10 commit ea6e4df

20 files changed

+483
-302
lines changed

.devcontainer/Dockerfile

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
FROM oven/bun:debian
2+
3+
# Config Bun
4+
ENV PATH="~/.bun/bin:${PATH}"
5+
RUN ln -s /usr/local/bin/bun /usr/local/bin/node
6+
7+
# Update packages
8+
RUN if [ "debian" == "alpine" ] ; then apk update ; else apt-get update ; fi
9+
10+
# Install Git
11+
RUN if [ "debian" == "alpine" ] ; then apk add git ; else apt-get install -y git ; fi
12+

.devcontainer/devcontainer.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
2+
// README at: https://github.com/marcosgomesneto/bun-devcontainers/tree/main/src/basic-bun
3+
{
4+
"name": "Bun",
5+
"dockerFile": "Dockerfile",
6+
// Configure tool-specific properties.
7+
"customizations": {
8+
// Configure properties specific to VS Code.
9+
"vscode": {
10+
// Add the IDs of extensions you want installed when the container is created.
11+
"extensions": [
12+
"oven.bun-vscode"
13+
]
14+
}
15+
},
16+
"features": {
17+
"ghcr.io/devcontainers/features/node:1": {}
18+
}
19+
}

.vscode/extensions.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
{
2-
"recommendations": ["kavod-io.vscode-jest-test-adapter", "esbenp.prettier-vscode", "github.vscode-github-actions", "hbenl.vscode-test-explorer"]
2+
"recommendations": [
3+
"esbenp.prettier-vscode",
4+
"github.vscode-github-actions",
5+
"hbenl.vscode-test-explorer"
6+
]
37
}

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/drivers/connection-tls.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* connection-tls.ts - connection via tls socket and sqlitecloud protocol
33
*/
44

5-
import { type SQLiteCloudConfig, SQLiteCloudError, type ErrorCallback, type ResultsCallback } from './types'
5+
import { type SQLiteCloudConfig, SQLiteCloudError, type ErrorCallback, type ResultsCallback, SQLiteCloudCommand } from './types'
66
import { SQLiteCloudConnection } from './connection'
77
import { getInitializationCommands } from './utilities'
88
import {
@@ -23,8 +23,6 @@ import { Buffer } from 'buffer'
2323

2424
import * as tls from 'tls'
2525

26-
import fs from 'fs'
27-
2826
/**
2927
* Implementation of SQLiteCloudConnection that connects to the database using specific tls APIs
3028
* that connect to native sockets or tls sockets and communicates via raw, binary protocol.
@@ -104,13 +102,17 @@ export class SQLiteCloudTlsConnection extends SQLiteCloudConnection {
104102
}
105103

106104
/** Will send a command immediately (no queueing), return the rowset/result or throw an error */
107-
transportCommands(commands: string, callback?: ResultsCallback): this {
105+
transportCommands(commands: string | SQLiteCloudCommand, callback?: ResultsCallback): this {
108106
// connection needs to be established?
109107
if (!this.socket) {
110108
callback?.call(this, new SQLiteCloudError('Connection not established', { errorCode: 'ERR_CONNECTION_NOT_ESTABLISHED' }))
111109
return this
112110
}
113111

112+
if (typeof commands === 'string') {
113+
commands = { query: commands } as SQLiteCloudCommand
114+
}
115+
114116
// reset buffer and rowset chunks, define response callback
115117
this.buffer = Buffer.alloc(0)
116118
this.startedOn = new Date()
@@ -148,7 +150,7 @@ export class SQLiteCloudTlsConnection extends SQLiteCloudConnection {
148150
// buffer to accumulate incoming data until an whole command is received and can be parsed
149151
private buffer: Buffer = Buffer.alloc(0)
150152
private startedOn: Date = new Date()
151-
private executingCommands?: string
153+
private executingCommands?: SQLiteCloudCommand
152154

153155
// callback to be called when a command is finished processing
154156
private processCallback?: ResultsCallback

src/drivers/connection-ws.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* transport-ws.ts - handles low level communication with sqlitecloud server via socket.io websocket
33
*/
44

5-
import { SQLiteCloudConfig, SQLiteCloudError, ErrorCallback, ResultsCallback } from './types'
5+
import { SQLiteCloudConfig, SQLiteCloudError, ErrorCallback, ResultsCallback, SQLiteCloudCommand } from './types'
66
import { SQLiteCloudRowset } from './rowset'
77
import { SQLiteCloudConnection } from './connection'
88
import { io, Socket } from 'socket.io-client'
@@ -41,7 +41,7 @@ export class SQLiteCloudWebsocketConnection extends SQLiteCloudConnection {
4141
}
4242

4343
/** Will send a command immediately (no queueing), return the rowset/result or throw an error */
44-
transportCommands(commands: string, callback?: ResultsCallback): this {
44+
transportCommands(commands: string | SQLiteCloudCommand, callback?: ResultsCallback): this {
4545
// connection needs to be established?
4646
if (!this.socket) {
4747
callback?.call(this, new SQLiteCloudError('Connection not established', { errorCode: 'ERR_CONNECTION_NOT_ESTABLISHED' }))

src/drivers/connection.ts

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
* connection.ts - base abstract class for sqlitecloud server connections
33
*/
44

5-
import { SQLiteCloudConfig, SQLiteCloudError, ErrorCallback, ResultsCallback } from './types'
6-
import { validateConfiguration, prepareSql } from './utilities'
5+
import { SQLiteCloudConfig, SQLiteCloudError, ErrorCallback, ResultsCallback, SQLiteCloudCommand } from './types'
6+
import { validateConfiguration } from './utilities'
77
import { OperationsQueue } from './queue'
88
import { anonimizeCommand, getUpdateResults } from './utilities'
99

@@ -62,7 +62,7 @@ export abstract class SQLiteCloudConnection {
6262
protected abstract connectTransport(config: SQLiteCloudConfig, callback?: ErrorCallback): this
6363

6464
/** Send a command, return the rowset/result or throw an error */
65-
protected abstract transportCommands(commands: string, callback?: ResultsCallback): this
65+
protected abstract transportCommands(commands: string | SQLiteCloudCommand, callback?: ResultsCallback): this
6666

6767
/** Will log to console if verbose mode is enabled */
6868
protected log(message: string, ...optionalParams: any[]): void {
@@ -85,7 +85,7 @@ export abstract class SQLiteCloudConnection {
8585
}
8686

8787
/** Will enquee a command to be executed and callback with the resulting rowset/result/error */
88-
public sendCommands(commands: string, callback?: ResultsCallback): this {
88+
public sendCommands(commands: string | SQLiteCloudCommand, callback?: ResultsCallback): this {
8989
this.operations.enqueue(done => {
9090
if (!this.connected) {
9191
const error = new SQLiteCloudError('Connection not established', { errorCode: 'ERR_CONNECTION_NOT_ESTABLISHED' })
@@ -108,32 +108,33 @@ export abstract class SQLiteCloudConnection {
108108
* using backticks and parameters in ${parameter} format. These parameters
109109
* will be properly escaped and quoted like when using a prepared statement.
110110
* @param sql A sql string or a template string in `backticks` format
111+
* A SQLiteCloudCommand when the query is defined with question marks and bindings.
111112
* @returns An array of rows in case of selections or an object with
112113
* metadata in case of insert, update, delete.
113114
*/
114-
public async sql(sql: TemplateStringsArray | string, ...values: any[]): Promise<any> {
115-
let preparedSql = ''
115+
public async sql(sql: TemplateStringsArray | string | SQLiteCloudCommand, ...values: any[]): Promise<any> {
116+
let commands = { query: '' } as SQLiteCloudCommand
116117

117118
// sql is a TemplateStringsArray, the 'raw' property is specific to TemplateStringsArray
118119
if (Array.isArray(sql) && 'raw' in sql) {
120+
let query = ''
119121
sql.forEach((string, i) => {
120-
preparedSql += string + (i < values.length ? '?' : '')
122+
// TemplateStringsArray splits the string before each variable
123+
// used in the template. Add the question mark
124+
// to the end of the string for the number of used variables.
125+
query += string + (i < values.length ? '?' : '')
121126
})
122-
preparedSql = prepareSql(preparedSql, ...values)
127+
commands = { query, parameters: values }
128+
} else if (typeof sql === 'string') {
129+
commands = { query: sql, parameters: values }
130+
} else if (typeof sql === 'object') {
131+
commands = sql as SQLiteCloudCommand
123132
} else {
124-
if (typeof sql === 'string') {
125-
if (values?.length > 0) {
126-
preparedSql = prepareSql(sql, ...values)
127-
} else {
128-
preparedSql = sql
129-
}
130-
} else {
131-
throw new Error('Invalid sql')
132-
}
133+
throw new Error('Invalid sql')
133134
}
134135

135136
return new Promise((resolve, reject) => {
136-
this.sendCommands(preparedSql, (error, results) => {
137+
this.sendCommands(commands, (error, results) => {
137138
if (error) {
138139
reject(error)
139140
} else {

src/drivers/database.ts

Lines changed: 26 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@
1212

1313
import { SQLiteCloudConnection } from './connection'
1414
import { SQLiteCloudRowset } from './rowset'
15-
import { SQLiteCloudConfig, SQLiteCloudError, RowCountCallback, SQLiteCloudArrayType } from './types'
16-
import { prepareSql, popCallback } from './utilities'
17-
import { Statement } from './statement'
15+
import { SQLiteCloudConfig, SQLiteCloudError, RowCountCallback, SQLiteCloudArrayType, SQLiteCloudCommand } from './types'
16+
import { popCallback } from './utilities'
1817
import { ErrorCallback, ResultsCallback, RowCallback, RowsCallback } from './types'
1918
import EventEmitter from 'eventemitter3'
2019
import { isBrowser } from './utilities'
2120
import { PubSub } from './pubsub'
21+
import { Statement } from './statement'
2222

2323
// Uses eventemitter3 instead of node events for browser compatibility
2424
// https://github.com/primus/eventemitter3
@@ -204,12 +204,12 @@ export class Database extends EventEmitter {
204204
public run<T>(sql: string, params: any, callback?: ResultsCallback<T>): this
205205
public run(sql: string, ...params: any[]): this {
206206
const { args, callback } = popCallback<ResultsCallback>(params)
207-
const preparedSql = args?.length > 0 ? prepareSql(sql, ...args) : sql
207+
const command: SQLiteCloudCommand = { query: sql, parameters: args?.flat() }
208208
this.getConnection((error, connection) => {
209209
if (error || !connection) {
210210
this.handleError(null, error as Error, callback)
211211
} else {
212-
connection.sendCommands(preparedSql, (error, results) => {
212+
connection.sendCommands(command, (error, results) => {
213213
if (error) {
214214
this.handleError(connection, error, callback)
215215
} else {
@@ -237,12 +237,12 @@ export class Database extends EventEmitter {
237237
public get<T>(sql: string, params: any, callback?: RowCallback<T>): this
238238
public get(sql: string, ...params: any[]): this {
239239
const { args, callback } = popCallback<RowCallback>(params)
240-
const preparedSql = args?.length > 0 ? prepareSql(sql, ...args) : sql
240+
const command: SQLiteCloudCommand = { query: sql, parameters: args?.flat() }
241241
this.getConnection((error, connection) => {
242242
if (error || !connection) {
243243
this.handleError(null, error as Error, callback)
244244
} else {
245-
connection.sendCommands(preparedSql, (error, results) => {
245+
connection.sendCommands(command, (error, results) => {
246246
if (error) {
247247
this.handleError(connection, error, callback)
248248
} else {
@@ -275,12 +275,12 @@ export class Database extends EventEmitter {
275275
public all<T>(sql: string, params: any, callback?: RowsCallback<T>): this
276276
public all(sql: string, ...params: any[]): this {
277277
const { args, callback } = popCallback<RowsCallback>(params)
278-
const preparedSql = args?.length > 0 ? prepareSql(sql, ...args) : sql
278+
const command: SQLiteCloudCommand = { query: sql, parameters: args?.flat() }
279279
this.getConnection((error, connection) => {
280280
if (error || !connection) {
281281
this.handleError(null, error as Error, callback)
282282
} else {
283-
connection.sendCommands(preparedSql, (error, results) => {
283+
connection.sendCommands(command, (error, results) => {
284284
if (error) {
285285
this.handleError(connection, error, callback)
286286
} else {
@@ -316,12 +316,12 @@ export class Database extends EventEmitter {
316316
// extract optional parameters and one or two callbacks
317317
const { args, callback, complete } = popCallback<RowCallback>(params)
318318

319-
const preparedSql = args?.length > 0 ? prepareSql(sql, ...args) : sql
319+
const command: SQLiteCloudCommand = { query: sql, parameters: args?.flat() }
320320
this.getConnection((error, connection) => {
321321
if (error || !connection) {
322322
this.handleError(null, error as Error, callback)
323323
} else {
324-
connection.sendCommands(preparedSql, (error, rowset) => {
324+
connection.sendCommands(command, (error, rowset) => {
325325
if (error) {
326326
this.handleError(connection, error, callback)
327327
} else {
@@ -352,8 +352,7 @@ export class Database extends EventEmitter {
352352
* they are bound to the prepared statement before calling the callback.
353353
*/
354354
public prepare<T = any>(sql: string, ...params: any[]): Statement<T> {
355-
const { args, callback } = popCallback(params)
356-
return new Statement(this, sql, ...args, callback)
355+
return new Statement(this, sql, ...params)
357356
}
358357

359358
/**
@@ -444,34 +443,33 @@ export class Database extends EventEmitter {
444443
* @returns An array of rows in case of selections or an object with
445444
* metadata in case of insert, update, delete.
446445
*/
447-
448-
public async sql(sql: TemplateStringsArray | string, ...values: any[]): Promise<any> {
449-
let preparedSql = ''
446+
public async sql(sql: TemplateStringsArray | string | SQLiteCloudCommand, ...values: any[]): Promise<any> {
447+
let commands = { query: '' } as SQLiteCloudCommand
450448

451449
// sql is a TemplateStringsArray, the 'raw' property is specific to TemplateStringsArray
452450
if (Array.isArray(sql) && 'raw' in sql) {
451+
let query = ''
453452
sql.forEach((string, i) => {
454-
preparedSql += string + (i < values.length ? '?' : '')
453+
// TemplateStringsArray splits the string before each variable
454+
// used in the template. Add the question mark
455+
// to the end of the string for the number of used variables.
456+
query += string + (i < values.length ? '?' : '')
455457
})
456-
preparedSql = prepareSql(preparedSql, ...values)
458+
commands = { query, parameters: values }
459+
} else if (typeof sql === 'string') {
460+
commands = { query: sql, parameters: values }
461+
} else if (typeof sql === 'object') {
462+
commands = sql as SQLiteCloudCommand
457463
} else {
458-
if (typeof sql === 'string') {
459-
if (values?.length > 0) {
460-
preparedSql = prepareSql(sql, ...values)
461-
} else {
462-
preparedSql = sql
463-
}
464-
} else {
465-
throw new Error('Invalid sql')
466-
}
464+
throw new Error('Invalid sql')
467465
}
468466

469467
return new Promise((resolve, reject) => {
470468
this.getConnection((error, connection) => {
471469
if (error || !connection) {
472470
reject(error)
473471
} else {
474-
connection.sendCommands(preparedSql, (error, results) => {
472+
connection.sendCommands(commands, (error, results) => {
475473
if (error) {
476474
reject(error)
477475
} else {

0 commit comments

Comments
 (0)