mirror of
https://github.com/nodejs/node.git
synced 2026-11-09 15:11:11 -05:00
Since SQLite 3.54, sqlite3_clear_bindings() invalidates the query plan of the statement, so a statement was re-prepared on every run. Bind NULL only to the parameters that were not bound in this run instead, and skip clearing in the tag store, which always binds every parameter. Refs: https://github.com/nodejs/node/issues/66800 Signed-off-by: dualfroz <me@dualfroz.com> PR-URL: https://github.com/nodejs/node/pull/66983 Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
288 lines
8.6 KiB
JavaScript
288 lines
8.6 KiB
JavaScript
'use strict';
|
|
const { skipIfSQLiteMissing } = require('../common');
|
|
skipIfSQLiteMissing();
|
|
const { Database } = require('node:sqlite');
|
|
const { suite, test } = require('node:test');
|
|
|
|
suite('data binding and mapping', () => {
|
|
test('supported data types', (t) => {
|
|
const u8a = new TextEncoder().encode('a☃b☃c');
|
|
const db = new Database(':memory:');
|
|
t.after(() => { db.close(); });
|
|
const setup = db.exec(`
|
|
CREATE TABLE types(
|
|
key INTEGER PRIMARY KEY,
|
|
int INTEGER,
|
|
double REAL,
|
|
text TEXT,
|
|
buf BLOB
|
|
) STRICT;
|
|
`);
|
|
t.assert.strictEqual(setup, undefined);
|
|
const stmt = db.prepare('INSERT INTO types (key, int, double, text, buf) ' +
|
|
'VALUES (?, ?, ?, ?, ?)');
|
|
t.assert.deepStrictEqual(
|
|
stmt.run(1, 42, 3.14159, 'foo', u8a),
|
|
{ changes: 1, lastInsertRowid: 1 },
|
|
);
|
|
t.assert.deepStrictEqual(
|
|
stmt.run(2, null, null, null, null),
|
|
{ changes: 1, lastInsertRowid: 2 }
|
|
);
|
|
t.assert.deepStrictEqual(
|
|
stmt.run(3, Number(8), Number(2.718), String('bar'), Buffer.from('x☃y☃')),
|
|
{ changes: 1, lastInsertRowid: 3 },
|
|
);
|
|
t.assert.deepStrictEqual(
|
|
stmt.run(4, 99n, 0xf, '', new Uint8Array()),
|
|
{ changes: 1, lastInsertRowid: 4 },
|
|
);
|
|
|
|
const query = db.prepare('SELECT * FROM types WHERE key = ?');
|
|
t.assert.deepStrictEqual(query.get(1), {
|
|
__proto__: null,
|
|
key: 1,
|
|
int: 42,
|
|
double: 3.14159,
|
|
text: 'foo',
|
|
buf: u8a,
|
|
});
|
|
t.assert.deepStrictEqual(query.get(2), {
|
|
__proto__: null,
|
|
key: 2,
|
|
int: null,
|
|
double: null,
|
|
text: null,
|
|
buf: null,
|
|
});
|
|
t.assert.deepStrictEqual(query.get(3), {
|
|
__proto__: null,
|
|
key: 3,
|
|
int: 8,
|
|
double: 2.718,
|
|
text: 'bar',
|
|
buf: new TextEncoder().encode('x☃y☃'),
|
|
});
|
|
t.assert.deepStrictEqual(query.get(4), {
|
|
__proto__: null,
|
|
key: 4,
|
|
int: 99,
|
|
double: 0xf,
|
|
text: '',
|
|
buf: new Uint8Array(),
|
|
});
|
|
|
|
t.assert.deepStrictEqual(
|
|
stmt.run(5, true, false, true, null),
|
|
{ changes: 1, lastInsertRowid: 5 }
|
|
);
|
|
t.assert.deepStrictEqual(
|
|
query.get(5),
|
|
{ __proto__: null, key: 5, int: 1, double: 0, text: '1', buf: null }
|
|
);
|
|
|
|
t.assert.deepStrictEqual(
|
|
stmt.run(6, undefined, undefined, undefined, undefined),
|
|
{ changes: 1, lastInsertRowid: 6 }
|
|
);
|
|
t.assert.deepStrictEqual(
|
|
query.get(6),
|
|
{ __proto__: null, key: 6, int: null, double: null, text: null, buf: null }
|
|
);
|
|
});
|
|
|
|
test('undefined is bound as NULL', (t) => {
|
|
const db = new Database(':memory:');
|
|
t.after(() => { db.close(); });
|
|
const setup = db.exec(
|
|
'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
|
|
);
|
|
t.assert.strictEqual(setup, undefined);
|
|
const stmt = db.prepare('INSERT INTO types (key, val) VALUES ($k, $v)');
|
|
const query = db.prepare('SELECT * FROM types WHERE key = ?');
|
|
|
|
// An explicit `undefined` and an omitted named parameter are equivalent.
|
|
t.assert.deepStrictEqual(
|
|
stmt.run({ k: 1, v: undefined }),
|
|
{ changes: 1, lastInsertRowid: 1 }
|
|
);
|
|
t.assert.deepStrictEqual(
|
|
stmt.run({ k: 2 }),
|
|
{ changes: 1, lastInsertRowid: 2 }
|
|
);
|
|
t.assert.deepStrictEqual(
|
|
query.get(1),
|
|
{ __proto__: null, key: 1, val: null }
|
|
);
|
|
t.assert.deepStrictEqual(
|
|
query.get(2),
|
|
{ __proto__: null, key: 2, val: null }
|
|
);
|
|
|
|
t.assert.deepStrictEqual(
|
|
db.prepare('SELECT ? AS a').get(undefined),
|
|
{ __proto__: null, a: null }
|
|
);
|
|
});
|
|
|
|
test('undefined is not treated as the named parameters argument', (t) => {
|
|
const db = new Database(':memory:');
|
|
t.after(() => { db.close(); });
|
|
|
|
// `undefined` is not an object, so it is bound as an anonymous parameter
|
|
// rather than being treated as the named parameters argument.
|
|
t.assert.throws(() => {
|
|
db.prepare('SELECT $k AS a').get(undefined);
|
|
}, {
|
|
code: 'ERR_SQLITE_ERROR',
|
|
message: /column index out of range/,
|
|
});
|
|
|
|
t.assert.throws(() => {
|
|
db.prepare('SELECT ? AS a').get(1, undefined);
|
|
}, {
|
|
code: 'ERR_SQLITE_ERROR',
|
|
message: /column index out of range/,
|
|
});
|
|
});
|
|
|
|
test('large strings are bound correctly', (t) => {
|
|
const db = new Database(':memory:');
|
|
t.after(() => { db.close(); });
|
|
const setup = db.exec(
|
|
'CREATE TABLE data(key INTEGER PRIMARY KEY, text TEXT) STRICT;'
|
|
);
|
|
t.assert.strictEqual(setup, undefined);
|
|
|
|
t.assert.deepStrictEqual(
|
|
db.prepare('INSERT INTO data (key, text) VALUES (?, ?)').run(1, ''),
|
|
{ changes: 1, lastInsertRowid: 1 },
|
|
);
|
|
|
|
const update = db.prepare('UPDATE data SET text = ? WHERE key = 1');
|
|
|
|
// > 1024 bytes so `Utf8Value` uses heap storage internally.
|
|
const largeAscii = 'a'.repeat(8 * 1024);
|
|
// Force a non-one-byte string path through UTF-8 conversion.
|
|
const largeUnicode = '\u2603'.repeat(2048);
|
|
|
|
const res = update.run(largeAscii);
|
|
t.assert.strictEqual(res.changes, 1);
|
|
|
|
t.assert.strictEqual(
|
|
db.prepare('SELECT text FROM data WHERE key = 1').get().text,
|
|
largeAscii,
|
|
);
|
|
|
|
t.assert.strictEqual(update.run(largeUnicode).changes, 1);
|
|
t.assert.strictEqual(
|
|
db.prepare('SELECT text FROM data WHERE key = 1').get().text,
|
|
largeUnicode,
|
|
);
|
|
});
|
|
|
|
test('unsupported data types', (t) => {
|
|
const db = new Database(':memory:');
|
|
t.after(() => { db.close(); });
|
|
const setup = db.exec(
|
|
'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
|
|
);
|
|
t.assert.strictEqual(setup, undefined);
|
|
|
|
[
|
|
() => {},
|
|
Symbol(),
|
|
/foo/,
|
|
Promise.resolve(),
|
|
new Map(),
|
|
new Set(),
|
|
].forEach((val) => {
|
|
t.assert.throws(() => {
|
|
db.prepare('INSERT INTO types (key, val) VALUES (?, ?)').run(1, val);
|
|
}, {
|
|
code: 'ERR_INVALID_ARG_TYPE',
|
|
message: /Provided value cannot be bound to SQLite parameter 2/,
|
|
});
|
|
});
|
|
|
|
t.assert.throws(() => {
|
|
const stmt = db.prepare('INSERT INTO types (key, val) VALUES ($k, $v)');
|
|
stmt.run({ $k: 1, $v: () => {} });
|
|
}, {
|
|
code: 'ERR_INVALID_ARG_TYPE',
|
|
message: /Provided value cannot be bound to SQLite parameter 2/,
|
|
});
|
|
});
|
|
|
|
test('throws when binding a BigInt that is too large', (t) => {
|
|
const max = 9223372036854775807n; // Largest 64-bit signed integer value.
|
|
const db = new Database(':memory:');
|
|
t.after(() => { db.close(); });
|
|
const setup = db.exec(
|
|
'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
|
|
);
|
|
t.assert.strictEqual(setup, undefined);
|
|
const stmt = db.prepare('INSERT INTO types (key, val) VALUES (?, ?)');
|
|
t.assert.deepStrictEqual(
|
|
stmt.run(1, max),
|
|
{ changes: 1, lastInsertRowid: 1 },
|
|
);
|
|
t.assert.throws(() => {
|
|
stmt.run(1, max + 1n);
|
|
}, {
|
|
code: 'ERR_INVALID_ARG_VALUE',
|
|
message: /BigInt value is too large to bind/,
|
|
});
|
|
});
|
|
|
|
test('statements are unbound on each call', (t) => {
|
|
const db = new Database(':memory:');
|
|
t.after(() => { db.close(); });
|
|
const setup = db.exec(
|
|
'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
|
|
);
|
|
t.assert.strictEqual(setup, undefined);
|
|
const stmt = db.prepare('INSERT INTO data (key, val) VALUES (?, ?)');
|
|
t.assert.deepStrictEqual(
|
|
stmt.run(1, 5),
|
|
{ changes: 1, lastInsertRowid: 1 },
|
|
);
|
|
t.assert.deepStrictEqual(
|
|
stmt.run(),
|
|
{ changes: 1, lastInsertRowid: 2 },
|
|
);
|
|
t.assert.deepStrictEqual(
|
|
db.prepare('SELECT * FROM data ORDER BY key').all(),
|
|
[{ __proto__: null, key: 1, val: 5 }, { __proto__: null, key: 2, val: null }],
|
|
);
|
|
});
|
|
|
|
test('omitted named parameters are unbound on each call', (t) => {
|
|
const db = new Database(':memory:');
|
|
t.after(() => { db.close(); });
|
|
const setup = db.exec(
|
|
'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
|
|
);
|
|
t.assert.strictEqual(setup, undefined);
|
|
const named = db.prepare('INSERT INTO data (key, val) VALUES ($k, $v)');
|
|
const mixed = db.prepare('INSERT INTO data (key, val) VALUES ($k, ?)');
|
|
named.run({ k: 1, v: 5 });
|
|
named.run({ k: 2 });
|
|
named.run({ k: 3, v: 6 });
|
|
named.run({ k: 4, $k: 4 });
|
|
mixed.run({ k: 5 }, 7);
|
|
mixed.run({ k: 6 });
|
|
t.assert.deepStrictEqual(
|
|
db.prepare('SELECT * FROM data ORDER BY key').all(),
|
|
[
|
|
{ __proto__: null, key: 1, val: 5 },
|
|
{ __proto__: null, key: 2, val: null },
|
|
{ __proto__: null, key: 3, val: 6 },
|
|
{ __proto__: null, key: 4, val: null },
|
|
{ __proto__: null, key: 5, val: 7 },
|
|
{ __proto__: null, key: 6, val: null },
|
|
],
|
|
);
|
|
});
|
|
});
|