Skip to content

Commit

Permalink
Merge pull request #141 from sqlitecloud/#122-implement-server-side-p…
Browse files Browse the repository at this point in the history
…repared-statement

feat(statement): server side prepared statement
  • Loading branch information
danielebriggi authored Dec 5, 2024
2 parents 5bdba10 + 75e489a commit 910b6c8
Show file tree
Hide file tree
Showing 23 changed files with 577 additions and 323 deletions.
12 changes: 12 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM oven/bun:debian

# Config Bun
ENV PATH="~/.bun/bin:${PATH}"
RUN ln -s /usr/local/bin/bun /usr/local/bin/node

# Update packages
RUN if [ "debian" == "alpine" ] ; then apk update ; else apt-get update ; fi

# Install Git
RUN if [ "debian" == "alpine" ] ; then apk add git ; else apt-get install -y git ; fi

19 changes: 19 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/marcosgomesneto/bun-devcontainers/tree/main/src/basic-bun
{
"name": "Bun",
"dockerFile": "Dockerfile",
// Configure tool-specific properties.
"customizations": {
// Configure properties specific to VS Code.
"vscode": {
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"oven.bun-vscode"
]
}
},
"features": {
"ghcr.io/devcontainers/features/node:1": {}
}
}
6 changes: 5 additions & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
{
"recommendations": ["kavod-io.vscode-jest-test-adapter", "esbenp.prettier-vscode", "github.vscode-github-actions", "hbenl.vscode-test-explorer"]
"recommendations": [
"esbenp.prettier-vscode",
"github.vscode-github-actions",
"hbenl.vscode-test-explorer"
]
}
15 changes: 2 additions & 13 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.333",
"version": "1.0.339",
"description": "SQLiteCloud drivers for Typescript/Javascript in edge, web and node clients",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
Expand Down
12 changes: 7 additions & 5 deletions src/drivers/connection-tls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* connection-tls.ts - connection via tls socket and sqlitecloud protocol
*/

import { type SQLiteCloudConfig, SQLiteCloudError, type ErrorCallback, type ResultsCallback } from './types'
import { type SQLiteCloudConfig, SQLiteCloudError, type ErrorCallback, type ResultsCallback, SQLiteCloudCommand } from './types'
import { SQLiteCloudConnection } from './connection'
import { getInitializationCommands } from './utilities'
import {
Expand All @@ -23,8 +23,6 @@ import { Buffer } from 'buffer'

import * as tls from 'tls'

import fs from 'fs'

/**
* Implementation of SQLiteCloudConnection that connects to the database using specific tls APIs
* that connect to native sockets or tls sockets and communicates via raw, binary protocol.
Expand Down Expand Up @@ -104,13 +102,17 @@ export class SQLiteCloudTlsConnection extends SQLiteCloudConnection {
}

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

if (typeof commands === 'string') {
commands = { query: commands } as SQLiteCloudCommand
}

// reset buffer and rowset chunks, define response callback
this.buffer = Buffer.alloc(0)
this.startedOn = new Date()
Expand Down Expand Up @@ -148,7 +150,7 @@ export class SQLiteCloudTlsConnection extends SQLiteCloudConnection {
// buffer to accumulate incoming data until an whole command is received and can be parsed
private buffer: Buffer = Buffer.alloc(0)
private startedOn: Date = new Date()
private executingCommands?: string
private executingCommands?: SQLiteCloudCommand

// callback to be called when a command is finished processing
private processCallback?: ResultsCallback
Expand Down
10 changes: 7 additions & 3 deletions src/drivers/connection-ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* transport-ws.ts - handles low level communication with sqlitecloud server via socket.io websocket
*/

import { SQLiteCloudConfig, SQLiteCloudError, ErrorCallback, ResultsCallback } from './types'
import { SQLiteCloudConfig, SQLiteCloudError, ErrorCallback, ResultsCallback, SQLiteCloudCommand, SQLiteCloudDataTypes } from './types'
import { SQLiteCloudRowset } from './rowset'
import { SQLiteCloudConnection } from './connection'
import { io, Socket } from 'socket.io-client'
Expand Down Expand Up @@ -41,14 +41,18 @@ export class SQLiteCloudWebsocketConnection extends SQLiteCloudConnection {
}

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

if (typeof commands === 'string') {
commands = { query: commands }
}

this.socket.emit('v1/sql', { sql: commands, row: 'array' }, (response: any) => {
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)
Expand Down
37 changes: 19 additions & 18 deletions src/drivers/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
* connection.ts - base abstract class for sqlitecloud server connections
*/

import { SQLiteCloudConfig, SQLiteCloudError, ErrorCallback, ResultsCallback } from './types'
import { validateConfiguration, prepareSql } from './utilities'
import { SQLiteCloudConfig, SQLiteCloudError, ErrorCallback, ResultsCallback, SQLiteCloudCommand } from './types'
import { validateConfiguration } from './utilities'
import { OperationsQueue } from './queue'
import { anonimizeCommand, getUpdateResults } from './utilities'

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

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

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

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

// sql is a TemplateStringsArray, the 'raw' property is specific to TemplateStringsArray
if (Array.isArray(sql) && 'raw' in sql) {
let query = ''
sql.forEach((string, i) => {
preparedSql += string + (i < values.length ? '?' : '')
// TemplateStringsArray splits the string before each variable
// used in the template. Add the question mark
// to the end of the string for the number of used variables.
query += string + (i < values.length ? '?' : '')
})
preparedSql = prepareSql(preparedSql, ...values)
commands = { query, parameters: values }
} else if (typeof sql === 'string') {
commands = { query: sql, parameters: values }
} else if (typeof sql === 'object') {
commands = sql as SQLiteCloudCommand
} else {
if (typeof sql === 'string') {
if (values?.length > 0) {
preparedSql = prepareSql(sql, ...values)
} else {
preparedSql = sql
}
} else {
throw new Error('Invalid sql')
}
throw new Error('Invalid sql')
}

return new Promise((resolve, reject) => {
this.sendCommands(preparedSql, (error, results) => {
this.sendCommands(commands, (error, results) => {
if (error) {
reject(error)
} else {
Expand Down
54 changes: 26 additions & 28 deletions src/drivers/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@

import { SQLiteCloudConnection } from './connection'
import { SQLiteCloudRowset } from './rowset'
import { SQLiteCloudConfig, SQLiteCloudError, RowCountCallback, SQLiteCloudArrayType } from './types'
import { prepareSql, popCallback } from './utilities'
import { Statement } from './statement'
import { SQLiteCloudConfig, SQLiteCloudError, RowCountCallback, SQLiteCloudArrayType, SQLiteCloudCommand } from './types'
import { popCallback } from './utilities'
import { ErrorCallback, ResultsCallback, RowCallback, RowsCallback } from './types'
import EventEmitter from 'eventemitter3'
import { isBrowser } from './utilities'
import { PubSub } from './pubsub'
import { Statement } from './statement'

// Uses eventemitter3 instead of node events for browser compatibility
// https://github.com/primus/eventemitter3
Expand Down Expand Up @@ -204,12 +204,12 @@ export class Database extends EventEmitter {
public run<T>(sql: string, params: any, callback?: ResultsCallback<T>): this
public run(sql: string, ...params: any[]): this {
const { args, callback } = popCallback<ResultsCallback>(params)
const preparedSql = args?.length > 0 ? prepareSql(sql, ...args) : sql
const command: SQLiteCloudCommand = { query: sql, parameters: args }
this.getConnection((error, connection) => {
if (error || !connection) {
this.handleError(null, error as Error, callback)
} else {
connection.sendCommands(preparedSql, (error, results) => {
connection.sendCommands(command, (error, results) => {
if (error) {
this.handleError(connection, error, callback)
} else {
Expand Down Expand Up @@ -237,12 +237,12 @@ export class Database extends EventEmitter {
public get<T>(sql: string, params: any, callback?: RowCallback<T>): this
public get(sql: string, ...params: any[]): this {
const { args, callback } = popCallback<RowCallback>(params)
const preparedSql = args?.length > 0 ? prepareSql(sql, ...args) : sql
const command: SQLiteCloudCommand = { query: sql, parameters: args }
this.getConnection((error, connection) => {
if (error || !connection) {
this.handleError(null, error as Error, callback)
} else {
connection.sendCommands(preparedSql, (error, results) => {
connection.sendCommands(command, (error, results) => {
if (error) {
this.handleError(connection, error, callback)
} else {
Expand Down Expand Up @@ -275,12 +275,12 @@ export class Database extends EventEmitter {
public all<T>(sql: string, params: any, callback?: RowsCallback<T>): this
public all(sql: string, ...params: any[]): this {
const { args, callback } = popCallback<RowsCallback>(params)
const preparedSql = args?.length > 0 ? prepareSql(sql, ...args) : sql
const command: SQLiteCloudCommand = { query: sql, parameters: args }
this.getConnection((error, connection) => {
if (error || !connection) {
this.handleError(null, error as Error, callback)
} else {
connection.sendCommands(preparedSql, (error, results) => {
connection.sendCommands(command, (error, results) => {
if (error) {
this.handleError(connection, error, callback)
} else {
Expand Down Expand Up @@ -316,12 +316,12 @@ export class Database extends EventEmitter {
// extract optional parameters and one or two callbacks
const { args, callback, complete } = popCallback<RowCallback>(params)

const preparedSql = args?.length > 0 ? prepareSql(sql, ...args) : sql
const command: SQLiteCloudCommand = { query: sql, parameters: args }
this.getConnection((error, connection) => {
if (error || !connection) {
this.handleError(null, error as Error, callback)
} else {
connection.sendCommands(preparedSql, (error, rowset) => {
connection.sendCommands(command, (error, rowset) => {
if (error) {
this.handleError(connection, error, callback)
} else {
Expand Down Expand Up @@ -352,8 +352,7 @@ export class Database extends EventEmitter {
* they are bound to the prepared statement before calling the callback.
*/
public prepare<T = any>(sql: string, ...params: any[]): Statement<T> {
const { args, callback } = popCallback(params)
return new Statement(this, sql, ...args, callback)
return new Statement(this, sql, ...params)
}

/**
Expand Down Expand Up @@ -444,34 +443,33 @@ export class Database extends EventEmitter {
* @returns An array of rows in case of selections or an object with
* metadata in case of insert, update, delete.
*/

public async sql(sql: TemplateStringsArray | string, ...values: any[]): Promise<any> {
let preparedSql = ''
public async sql(sql: TemplateStringsArray | string | SQLiteCloudCommand, ...values: any[]): Promise<any> {
let commands = { query: '' } as SQLiteCloudCommand

// sql is a TemplateStringsArray, the 'raw' property is specific to TemplateStringsArray
if (Array.isArray(sql) && 'raw' in sql) {
let query = ''
sql.forEach((string, i) => {
preparedSql += string + (i < values.length ? '?' : '')
// TemplateStringsArray splits the string before each variable
// used in the template. Add the question mark
// to the end of the string for the number of used variables.
query += string + (i < values.length ? '?' : '')
})
preparedSql = prepareSql(preparedSql, ...values)
commands = { query, parameters: values }
} else if (typeof sql === 'string') {
commands = { query: sql, parameters: values }
} else if (typeof sql === 'object') {
commands = sql as SQLiteCloudCommand
} else {
if (typeof sql === 'string') {
if (values?.length > 0) {
preparedSql = prepareSql(sql, ...values)
} else {
preparedSql = sql
}
} else {
throw new Error('Invalid sql')
}
throw new Error('Invalid sql')
}

return new Promise((resolve, reject) => {
this.getConnection((error, connection) => {
if (error || !connection) {
reject(error)
} else {
connection.sendCommands(preparedSql, (error, results) => {
connection.sendCommands(commands, (error, results) => {
if (error) {
reject(error)
} else {
Expand Down
Loading

0 comments on commit 910b6c8

Please sign in to comment.