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
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
20.18.2
22.18.0
18 changes: 17 additions & 1 deletion bench/insert-blob.bench.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { Buffer } from 'node:buffer';
import { bench, describe } from 'vitest';

import BDatabase from '@signalapp/better-sqlite3';
import Database from '../lib/index.js';
import { DatabaseSync as NDatabase } from 'node:sqlite';
import Database from '../dist/index.mjs';

const PREPARE = `
CREATE TABLE t (
Expand All @@ -21,12 +22,15 @@ const DELETE = 'DELETE FROM t';
describe('INSERT INTO t', () => {
const sdb = new Database(':memory:', { cacheStatements: true });
const bdb = new BDatabase(':memory:');
const ndb = new NDatabase(':memory:');

sdb.exec(PREPARE);
bdb.exec(PREPARE);
ndb.exec(PREPARE);

const sinsert = sdb.prepare(INSERT);
const binsert = bdb.prepare(INSERT);
const ninsert = ndb.prepare(INSERT);

bench(
'@signalapp/sqlcipher',
Expand All @@ -51,4 +55,16 @@ describe('INSERT INTO t', () => {
},
},
);

bench(
'node:sqlite',
() => {
ninsert.run({ b: BLOB });
},
{
teardown: () => {
ndb.exec(DELETE);
},
},
);
});
18 changes: 17 additions & 1 deletion bench/insert.bench.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { bench, describe } from 'vitest';

import BDatabase from '@signalapp/better-sqlite3';
import Database from '../lib/index.js';
import { DatabaseSync as NDatabase } from 'node:sqlite';
import Database from '../dist/index.mjs';

const PREPARE = `
CREATE TABLE t (
Expand All @@ -24,12 +25,15 @@ const DELETE = 'DELETE FROM t';
describe('INSERT INTO t', () => {
const sdb = new Database(':memory:', { cacheStatements: true });
const bdb = new BDatabase(':memory:');
const ndb = new NDatabase(':memory:');

sdb.exec(PREPARE);
bdb.exec(PREPARE);
ndb.exec(PREPARE);

const sinsert = sdb.prepare(INSERT);
const binsert = bdb.prepare(INSERT);
const ninsert = ndb.prepare(INSERT);

bench(
'@signalapp/sqlcipher',
Expand All @@ -54,4 +58,16 @@ describe('INSERT INTO t', () => {
},
},
);

bench(
'node:sqlite',
() => {
ninsert.run({ a1: 1, a2: 2, a3: 3, b1: 'b1', b2: 'b2', b3: 'b3' });
},
{
teardown: () => {
ndb.exec(DELETE);
},
},
);
});
18 changes: 17 additions & 1 deletion bench/select.bench.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { bench, describe } from 'vitest';

import BDatabase from '@signalapp/better-sqlite3';
import Database from '../lib/index.js';
import { DatabaseSync as NDatabase } from 'node:sqlite';
import Database from '../dist/index.mjs';

const PREPARE = `
CREATE TABLE t (
Expand Down Expand Up @@ -36,12 +37,15 @@ const SELECT = 'SELECT * FROM t LIMIT 1000';
describe('SELECT * FROM t', () => {
const sdb = new Database(':memory:', { cacheStatements: true });
const bdb = new BDatabase(':memory:');
const ndb = new NDatabase(':memory:');

sdb.exec(PREPARE);
bdb.exec(PREPARE);
ndb.exec(PREPARE);

const sinsert = sdb.prepare(INSERT);
const binsert = bdb.prepare(INSERT);
const ninsert = ndb.prepare(INSERT);

sdb.transaction(() => {
for (const value of VALUES) {
Expand All @@ -55,6 +59,12 @@ describe('SELECT * FROM t', () => {
}
})();

ndb.exec('BEGIN');
for (const value of VALUES) {
ninsert.run(value);
}
ndb.exec('COMMIT');

const sselect = sdb.prepare(SELECT);
const bselect = bdb.prepare(SELECT);

Expand All @@ -65,4 +75,10 @@ describe('SELECT * FROM t', () => {
bench('@signalapp/better-sqlite', () => {
bselect.all();
});

bench('node:sqlite', () => {
// Node.js seems to finalize the statement after `.all()`
const nselect = ndb.prepare(SELECT);
nselect.all();
});
});
72 changes: 60 additions & 12 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,16 @@ const addon = bindings<{
persistent: boolean,
pluck: boolean,
bigint: boolean,
paramNames: Array<string | null>,
): NativeStatement;
statementRun<Options extends StatementOptions>(
stmt: NativeStatement,
params: StatementParameters<Options> | undefined,
params: NativeParameters<Options> | undefined,
result: [number, number],
): void;
statementStep<Options extends StatementOptions>(
stmt: NativeStatement,
params: StatementParameters<Options> | null | undefined,
params: NativeParameters<Options> | null | undefined,
cache: Array<SqliteValue<Options>> | undefined,
isGet: boolean,
): Array<SqliteValue<Options>>;
Expand Down Expand Up @@ -85,11 +86,15 @@ export type StatementOptions = Readonly<{
bigint?: true;
}>;

export type NativeParameters<Options extends StatementOptions> = ReadonlyArray<
SqliteValue<Options>
>;

/**
* Parameters accepted by `.run()`/`.get()`/`.all()` methods of the statement.
*/
export type StatementParameters<Options extends StatementOptions> =
| ReadonlyArray<SqliteValue<Options>>
| NativeParameters<Options>
| Readonly<Record<string, SqliteValue<Options>>>;

/**
Expand Down Expand Up @@ -119,6 +124,9 @@ class Statement<Options extends StatementOptions = object> {

#cache: Array<SqliteValue<Options>> | undefined;
#createRow: undefined | ((result: unknown) => RowType<Options>);
#translateParams: (
params: StatementParameters<Options>,
) => NativeParameters<Options>;
#native: NativeStatement | undefined;
#onClose: (() => void) | undefined;

Expand All @@ -131,14 +139,47 @@ class Statement<Options extends StatementOptions = object> {
) {
this.#needsTranslation = persistent === true && !pluck;

const paramNames = new Array<string | null>();

this.#native = addon.statementNew(
db,
query,
persistent === true,
pluck === true,
bigint === true,
paramNames,
);

const isArrayParams = paramNames.every((name) => name === null);
const isObjectParams =
!isArrayParams && paramNames.every((name) => typeof name === 'string');

if (!isArrayParams && !isObjectParams) {
throw new TypeError('Cannot mix named and anonymous params in query');
}

if (isArrayParams) {
this.#translateParams = (params) => {
if (!Array.isArray(params)) {
throw new TypeError('Query requires an array of anonymous params');
}
return params;
};
} else {
this.#translateParams = runInThisContext(`
(function translateParams(params) {
if (Array.isArray(params)) {
throw new TypeError('Query requires an object of named params');
}
return [
${paramNames
.map((name) => `params[${JSON.stringify(name)}]`)
.join(',\n')}
];
})
`);
}

this.#onClose = onClose;
}

Expand All @@ -154,8 +195,8 @@ class Statement<Options extends StatementOptions = object> {
throw new Error('Statement closed');
}
const result: [number, number] = [0, 0];
this.#checkParams(params);
addon.statementRun(this.#native, params, result);
const nativeParams = this.#checkParams(params);
addon.statementRun(this.#native, nativeParams, result);
return { changes: result[0], lastInsertRowid: result[1] };
}

Expand All @@ -174,8 +215,13 @@ class Statement<Options extends StatementOptions = object> {
if (this.#native === undefined) {
throw new Error('Statement closed');
}
this.#checkParams(params);
const result = addon.statementStep(this.#native, params, this.#cache, true);
const nativeParams = this.#checkParams(params);
const result = addon.statementStep(
this.#native,
nativeParams,
this.#cache,
true,
);
if (result === undefined) {
return undefined;
}
Expand All @@ -202,9 +248,8 @@ class Statement<Options extends StatementOptions = object> {
throw new Error('Statement closed');
}
const result = [];
this.#checkParams(params);
let singleUseParams: StatementParameters<Options> | undefined | null =
params;
const nativeParams = this.#checkParams(params);
let singleUseParams: typeof nativeParams | undefined | null = nativeParams;
while (true) {
const single = addon.statementStep(
this.#native,
Expand Down Expand Up @@ -282,16 +327,19 @@ class Statement<Options extends StatementOptions = object> {
}

/** @internal */
#checkParams(params: StatementParameters<Options> | undefined): void {
#checkParams(
params: StatementParameters<Options> | undefined,
): NativeParameters<Options> | undefined {
if (params === undefined) {
return;
return undefined;
}
if (typeof params !== 'object') {
throw new TypeError('Params must be either object or array');
}
if (params === null) {
throw new TypeError('Params cannot be null');
}
return this.#translateParams(params);
}
}

Expand Down
50 changes: 23 additions & 27 deletions src/addon.cc
Original file line number Diff line number Diff line change
Expand Up @@ -406,12 +406,14 @@ Napi::Value Statement::New(const Napi::CallbackInfo& info) {
auto is_persistent = info[2].As<Napi::Boolean>();
auto is_pluck = info[3].As<Napi::Boolean>();
auto is_bigint = info[4].As<Napi::Boolean>();
auto param_names = info[5].As<Napi::Array>();

assert(db_external.IsExternal());
assert(query.IsString());
assert(is_persistent.IsBoolean());
assert(is_pluck.IsBoolean());
assert(is_bigint.IsBoolean());
assert(param_names.IsArray());

auto db = db_external.Data();

Expand Down Expand Up @@ -440,6 +442,18 @@ Napi::Value Statement::New(const Napi::CallbackInfo& info) {
auto stmt = new Statement(db, db_external, handle, is_persistent, is_pluck,
is_bigint);

int key_count = sqlite3_bind_parameter_count(handle);

for (int i = 1; i <= key_count; i++) {
auto name = sqlite3_bind_parameter_name(handle, i);
if (name == nullptr) {
param_names[i - 1] = env.Null();
} else {
// Skip "$"
param_names[i - 1] = name + 1;
}
}

return Napi::External<Statement>::New(
env, stmt, [](Napi::Env env, Statement* stmt) { delete stmt; });
}
Expand Down Expand Up @@ -733,36 +747,18 @@ bool Statement::BindParams(Napi::Env env, Napi::Value params) {

for (int i = 1; i <= list_len; i++) {
auto name = sqlite3_bind_parameter_name(handle_, i);
if (name != nullptr) {
NAPI_THROW(FormatError(env, "Unexpected named param %s at %d", name, i),
false);
}

auto error = BindParam(env, i, list[i - 1]);
if (error != nullptr) {
NAPI_THROW(
FormatError(env, "Failed to bind param %d, error %s", i, error),
false);
}
}
} else {
auto obj = params.As<Napi::Object>();

for (int i = 1; i <= key_count; i++) {
auto name = sqlite3_bind_parameter_name(handle_, i);
if (name == nullptr) {
NAPI_THROW(FormatError(env, "Unexpected anonymous param at %d", i),
false);
}

// Skip "$"
name = name + 1;
auto value = obj[name];
auto error = BindParam(env, i, value);
if (error != nullptr) {
NAPI_THROW(
FormatError(env, "Failed to bind param %s, error %s", name, error),
false);
if (name == nullptr) {
NAPI_THROW(
FormatError(env, "Failed to bind param %d, error %s", i, error),
false);
} else {
NAPI_THROW(FormatError(env, "Failed to bind param %s, error %s",
name + 1, error),
false);
}
}
}
}
Expand Down
Loading
Loading