Skip to content

Commit ac72ab5

Browse files
feat: cache parameter bindings
1 parent e528fa8 commit ac72ab5

File tree

7 files changed

+141
-52
lines changed

7 files changed

+141
-52
lines changed

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
20.18.2
1+
22.18.0

bench/insert-blob.bench.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { Buffer } from 'node:buffer';
22
import { bench, describe } from 'vitest';
33

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

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

2527
sdb.exec(PREPARE);
2628
bdb.exec(PREPARE);
29+
ndb.exec(PREPARE);
2730

2831
const sinsert = sdb.prepare(INSERT);
2932
const binsert = bdb.prepare(INSERT);
33+
const ninsert = ndb.prepare(INSERT);
3034

3135
bench(
3236
'@signalapp/sqlcipher',
@@ -51,4 +55,16 @@ describe('INSERT INTO t', () => {
5155
},
5256
},
5357
);
58+
59+
bench(
60+
'node:sqlite',
61+
() => {
62+
ninsert.run({ b: BLOB });
63+
},
64+
{
65+
teardown: () => {
66+
ndb.exec(DELETE);
67+
},
68+
},
69+
);
5470
});

bench/insert.bench.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { bench, describe } from 'vitest';
22

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

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

2830
sdb.exec(PREPARE);
2931
bdb.exec(PREPARE);
32+
ndb.exec(PREPARE);
3033

3134
const sinsert = sdb.prepare(INSERT);
3235
const binsert = bdb.prepare(INSERT);
36+
const ninsert = ndb.prepare(INSERT);
3337

3438
bench(
3539
'@signalapp/sqlcipher',
@@ -54,4 +58,16 @@ describe('INSERT INTO t', () => {
5458
},
5559
},
5660
);
61+
62+
bench(
63+
'node:sqlite',
64+
() => {
65+
ninsert.run({ a1: 1, a2: 2, a3: 3, b1: 'b1', b2: 'b2', b3: 'b3' });
66+
},
67+
{
68+
teardown: () => {
69+
ndb.exec(DELETE);
70+
},
71+
},
72+
);
5773
});

bench/select.bench.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { bench, describe } from 'vitest';
22

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

67
const PREPARE = `
78
CREATE TABLE t (
@@ -36,12 +37,15 @@ const SELECT = 'SELECT * FROM t LIMIT 1000';
3637
describe('SELECT * FROM t', () => {
3738
const sdb = new Database(':memory:', { cacheStatements: true });
3839
const bdb = new BDatabase(':memory:');
40+
const ndb = new NDatabase(':memory:');
3941

4042
sdb.exec(PREPARE);
4143
bdb.exec(PREPARE);
44+
ndb.exec(PREPARE);
4245

4346
const sinsert = sdb.prepare(INSERT);
4447
const binsert = bdb.prepare(INSERT);
48+
const ninsert = ndb.prepare(INSERT);
4549

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

62+
ndb.exec('BEGIN');
63+
for (const value of VALUES) {
64+
ninsert.run(value);
65+
}
66+
ndb.exec('COMMIT');
67+
5868
const sselect = sdb.prepare(SELECT);
5969
const bselect = bdb.prepare(SELECT);
6070

@@ -65,4 +75,10 @@ describe('SELECT * FROM t', () => {
6575
bench('@signalapp/better-sqlite', () => {
6676
bselect.all();
6777
});
78+
79+
bench('node:sqlite', () => {
80+
// Node.js seems to finalize the statement after `.all()`
81+
const nselect = ndb.prepare(SELECT);
82+
nselect.all();
83+
});
6884
});

lib/index.ts

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,16 @@ const addon = bindings<{
2727
persistent: boolean,
2828
pluck: boolean,
2929
bigint: boolean,
30+
paramNames: Array<string | null>,
3031
): NativeStatement;
3132
statementRun<Options extends StatementOptions>(
3233
stmt: NativeStatement,
33-
params: StatementParameters<Options> | undefined,
34+
params: NativeParameters<Options> | undefined,
3435
result: [number, number],
3536
): void;
3637
statementStep<Options extends StatementOptions>(
3738
stmt: NativeStatement,
38-
params: StatementParameters<Options> | null | undefined,
39+
params: NativeParameters<Options> | null | undefined,
3940
cache: Array<SqliteValue<Options>> | undefined,
4041
isGet: boolean,
4142
): Array<SqliteValue<Options>>;
@@ -85,11 +86,15 @@ export type StatementOptions = Readonly<{
8586
bigint?: true;
8687
}>;
8788

89+
export type NativeParameters<Options extends StatementOptions> = ReadonlyArray<
90+
SqliteValue<Options>
91+
>;
92+
8893
/**
8994
* Parameters accepted by `.run()`/`.get()`/`.all()` methods of the statement.
9095
*/
9196
export type StatementParameters<Options extends StatementOptions> =
92-
| ReadonlyArray<SqliteValue<Options>>
97+
| NativeParameters<Options>
9398
| Readonly<Record<string, SqliteValue<Options>>>;
9499

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

120125
#cache: Array<SqliteValue<Options>> | undefined;
121126
#createRow: undefined | ((result: unknown) => RowType<Options>);
127+
#translateParams: (
128+
params: StatementParameters<Options>,
129+
) => NativeParameters<Options>;
122130
#native: NativeStatement | undefined;
123131
#onClose: (() => void) | undefined;
124132

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

142+
const paramNames = new Array<string | null>();
143+
134144
this.#native = addon.statementNew(
135145
db,
136146
query,
137147
persistent === true,
138148
pluck === true,
139149
bigint === true,
150+
paramNames,
140151
);
141152

153+
const isArrayParams = paramNames.every((name) => name === null);
154+
const isObjectParams =
155+
!isArrayParams && paramNames.every((name) => typeof name === 'string');
156+
157+
if (!isArrayParams && !isObjectParams) {
158+
throw new TypeError('Cannot mix named and anonymous params in query');
159+
}
160+
161+
if (isArrayParams) {
162+
this.#translateParams = (params) => {
163+
if (!Array.isArray(params)) {
164+
throw new TypeError('Query requires an array of anonymous params');
165+
}
166+
return params;
167+
};
168+
} else {
169+
this.#translateParams = runInThisContext(`
170+
(function translateParams(params) {
171+
if (Array.isArray(params)) {
172+
throw new TypeError('Query requires an object of named params');
173+
}
174+
return [
175+
${paramNames
176+
.map((name) => `params[${JSON.stringify(name)}]`)
177+
.join(',\n')}
178+
];
179+
})
180+
`);
181+
}
182+
142183
this.#onClose = onClose;
143184
}
144185

@@ -154,8 +195,8 @@ class Statement<Options extends StatementOptions = object> {
154195
throw new Error('Statement closed');
155196
}
156197
const result: [number, number] = [0, 0];
157-
this.#checkParams(params);
158-
addon.statementRun(this.#native, params, result);
198+
const nativeParams = this.#checkParams(params);
199+
addon.statementRun(this.#native, nativeParams, result);
159200
return { changes: result[0], lastInsertRowid: result[1] };
160201
}
161202

@@ -174,8 +215,13 @@ class Statement<Options extends StatementOptions = object> {
174215
if (this.#native === undefined) {
175216
throw new Error('Statement closed');
176217
}
177-
this.#checkParams(params);
178-
const result = addon.statementStep(this.#native, params, this.#cache, true);
218+
const nativeParams = this.#checkParams(params);
219+
const result = addon.statementStep(
220+
this.#native,
221+
nativeParams,
222+
this.#cache,
223+
true,
224+
);
179225
if (result === undefined) {
180226
return undefined;
181227
}
@@ -202,9 +248,8 @@ class Statement<Options extends StatementOptions = object> {
202248
throw new Error('Statement closed');
203249
}
204250
const result = [];
205-
this.#checkParams(params);
206-
let singleUseParams: StatementParameters<Options> | undefined | null =
207-
params;
251+
const nativeParams = this.#checkParams(params);
252+
let singleUseParams: typeof nativeParams | undefined | null = nativeParams;
208253
while (true) {
209254
const single = addon.statementStep(
210255
this.#native,
@@ -282,16 +327,19 @@ class Statement<Options extends StatementOptions = object> {
282327
}
283328

284329
/** @internal */
285-
#checkParams(params: StatementParameters<Options> | undefined): void {
330+
#checkParams(
331+
params: StatementParameters<Options> | undefined,
332+
): NativeParameters<Options> | undefined {
286333
if (params === undefined) {
287-
return;
334+
return undefined;
288335
}
289336
if (typeof params !== 'object') {
290337
throw new TypeError('Params must be either object or array');
291338
}
292339
if (params === null) {
293340
throw new TypeError('Params cannot be null');
294341
}
342+
return this.#translateParams(params);
295343
}
296344
}
297345

src/addon.cc

Lines changed: 23 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -406,12 +406,14 @@ Napi::Value Statement::New(const Napi::CallbackInfo& info) {
406406
auto is_persistent = info[2].As<Napi::Boolean>();
407407
auto is_pluck = info[3].As<Napi::Boolean>();
408408
auto is_bigint = info[4].As<Napi::Boolean>();
409+
auto param_names = info[5].As<Napi::Array>();
409410

410411
assert(db_external.IsExternal());
411412
assert(query.IsString());
412413
assert(is_persistent.IsBoolean());
413414
assert(is_pluck.IsBoolean());
414415
assert(is_bigint.IsBoolean());
416+
assert(param_names.IsArray());
415417

416418
auto db = db_external.Data();
417419

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

445+
int key_count = sqlite3_bind_parameter_count(handle);
446+
447+
for (int i = 1; i <= key_count; i++) {
448+
auto name = sqlite3_bind_parameter_name(handle, i);
449+
if (name == nullptr) {
450+
param_names[i - 1] = env.Null();
451+
} else {
452+
// Skip "$"
453+
param_names[i - 1] = name + 1;
454+
}
455+
}
456+
443457
return Napi::External<Statement>::New(
444458
env, stmt, [](Napi::Env env, Statement* stmt) { delete stmt; });
445459
}
@@ -733,36 +747,18 @@ bool Statement::BindParams(Napi::Env env, Napi::Value params) {
733747

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

741751
auto error = BindParam(env, i, list[i - 1]);
742752
if (error != nullptr) {
743-
NAPI_THROW(
744-
FormatError(env, "Failed to bind param %d, error %s", i, error),
745-
false);
746-
}
747-
}
748-
} else {
749-
auto obj = params.As<Napi::Object>();
750-
751-
for (int i = 1; i <= key_count; i++) {
752-
auto name = sqlite3_bind_parameter_name(handle_, i);
753-
if (name == nullptr) {
754-
NAPI_THROW(FormatError(env, "Unexpected anonymous param at %d", i),
755-
false);
756-
}
757-
758-
// Skip "$"
759-
name = name + 1;
760-
auto value = obj[name];
761-
auto error = BindParam(env, i, value);
762-
if (error != nullptr) {
763-
NAPI_THROW(
764-
FormatError(env, "Failed to bind param %s, error %s", name, error),
765-
false);
753+
if (name == nullptr) {
754+
NAPI_THROW(
755+
FormatError(env, "Failed to bind param %d, error %s", i, error),
756+
false);
757+
} else {
758+
NAPI_THROW(FormatError(env, "Failed to bind param %s, error %s",
759+
name + 1, error),
760+
false);
761+
}
766762
}
767763
}
768764
}

0 commit comments

Comments
 (0)