mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 18:25:13 +08:00
The oversized-string cases allocate hundreds of megabytes and can make the ordinary statement test time out under parallel suite load. Move all five cases unchanged to the sequential pummel suite, which also allows more time for intensive tests. Refs: https://github.com/nodejs/reliability/issues/1690 Signed-off-by: Filip Skokan <panva.ip@gmail.com> Assisted-by: Codex PR-URL: https://github.com/nodejs/node/pull/66722 Refs: https://github.com/nodejs/reliability/issues/1686 Refs: https://github.com/nodejs/reliability/issues/1687 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Guilherme Araújo <arauujogui@gmail.com>
1448 lines
48 KiB
JavaScript
1448 lines
48 KiB
JavaScript
// Flags: --expose-gc
|
|
'use strict';
|
|
const { skipIfSQLiteMissing } = require('../common');
|
|
skipIfSQLiteMissing();
|
|
const {
|
|
Database,
|
|
Statement,
|
|
} = require('node:sqlite');
|
|
const { suite, test } = require('node:test');
|
|
|
|
suite('Statement() constructor', () => {
|
|
test('Statement cannot be constructed directly', (t) => {
|
|
t.assert.throws(() => {
|
|
new Statement();
|
|
}, {
|
|
code: 'ERR_ILLEGAL_CONSTRUCTOR',
|
|
message: /Illegal constructor/,
|
|
});
|
|
});
|
|
});
|
|
|
|
suite('Statement.prototype.get()', () => {
|
|
test('executes a query and returns undefined on no results', (t) => {
|
|
using db = new Database(':memory:');
|
|
let stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
|
|
t.assert.strictEqual(stmt.get(), undefined);
|
|
stmt = db.prepare('SELECT * FROM storage');
|
|
t.assert.strictEqual(stmt.get(), undefined);
|
|
});
|
|
|
|
test('executes a query and returns the first result', (t) => {
|
|
using db = new Database(':memory:');
|
|
let stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
|
|
t.assert.strictEqual(stmt.get(), undefined);
|
|
stmt = db.prepare('INSERT INTO storage (key, val) VALUES (?, ?)');
|
|
t.assert.strictEqual(stmt.get('key1', 'val1'), undefined);
|
|
t.assert.strictEqual(stmt.get('key2', 'val2'), undefined);
|
|
stmt = db.prepare('SELECT * FROM storage ORDER BY key');
|
|
t.assert.deepStrictEqual(stmt.get(), { __proto__: null, key: 'key1', val: 'val1' });
|
|
});
|
|
|
|
test('executes a query that returns special columns', (t) => {
|
|
using db = new Database(':memory:');
|
|
const stmt = db.prepare('SELECT 1 as __proto__, 2 as constructor, 3 as toString');
|
|
t.assert.deepStrictEqual(stmt.get(), { __proto__: null, ['__proto__']: 1, constructor: 2, toString: 3 });
|
|
});
|
|
|
|
test('reflects an added column after the schema changes', (t) => {
|
|
using db = new Database(':memory:');
|
|
db.exec('CREATE TABLE storage(key TEXT, val TEXT)');
|
|
db.prepare('INSERT INTO storage (key, val) VALUES (?, ?)').run('key1', 'val1');
|
|
const stmt = db.prepare('SELECT * FROM storage ORDER BY key');
|
|
db.exec("ALTER TABLE storage ADD COLUMN extra TEXT DEFAULT 'def'");
|
|
t.assert.deepStrictEqual(stmt.get(), {
|
|
__proto__: null, key: 'key1', val: 'val1', extra: 'def',
|
|
});
|
|
});
|
|
|
|
test('reflects a dropped column after the schema changes', (t) => {
|
|
using db = new Database(':memory:');
|
|
db.exec('CREATE TABLE storage(key TEXT, val TEXT, extra TEXT)');
|
|
db.prepare('INSERT INTO storage (key, val, extra) VALUES (?, ?, ?)')
|
|
.run('key1', 'val1', 'x');
|
|
const stmt = db.prepare('SELECT * FROM storage ORDER BY key');
|
|
db.exec('ALTER TABLE storage DROP COLUMN extra');
|
|
t.assert.deepStrictEqual(stmt.get(), {
|
|
__proto__: null, key: 'key1', val: 'val1',
|
|
});
|
|
});
|
|
|
|
test('throws if the statement is already finalized', (t) => {
|
|
using db = new Database(':memory:');
|
|
const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
|
|
stmt.close();
|
|
t.assert.throws(() => {
|
|
stmt.get();
|
|
}, {
|
|
code: 'ERR_INVALID_STATE',
|
|
message: /statement has been finalized/,
|
|
});
|
|
});
|
|
|
|
test('surfaces a deferred SQLite error from reset() even though a row was already built', (t) => {
|
|
using db = new Database(':memory:');
|
|
db.exec(`
|
|
PRAGMA foreign_keys = ON;
|
|
PRAGMA defer_foreign_keys = ON;
|
|
CREATE TABLE parent(id INTEGER PRIMARY KEY);
|
|
CREATE TABLE child(id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parent(id));
|
|
`);
|
|
// The FK check is deferred until the implicit transaction commits, which
|
|
// happens inside reset() here because RETURNING leaves the statement's
|
|
// VDBE running after the row is produced.
|
|
const stmt = db.prepare(
|
|
'INSERT INTO child (parent_id) VALUES (999) RETURNING id'
|
|
);
|
|
t.assert.throws(() => {
|
|
stmt.get();
|
|
}, {
|
|
code: 'ERR_SQLITE_ERROR',
|
|
message: /FOREIGN KEY constraint failed/,
|
|
});
|
|
});
|
|
});
|
|
|
|
suite('Statement.prototype.all()', () => {
|
|
test('executes a query and returns an empty array on no results', (t) => {
|
|
using db = new Database(':memory:');
|
|
const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
|
|
t.assert.deepStrictEqual(stmt.all(), []);
|
|
});
|
|
|
|
test('executes a query and returns all results', (t) => {
|
|
using db = new Database(':memory:');
|
|
let stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
|
|
t.assert.deepStrictEqual(stmt.run(), { changes: 0, lastInsertRowid: 0 });
|
|
stmt = db.prepare('INSERT INTO storage (key, val) VALUES (?, ?)');
|
|
t.assert.deepStrictEqual(
|
|
stmt.run('key1', 'val1'),
|
|
{ changes: 1, lastInsertRowid: 1 },
|
|
);
|
|
t.assert.deepStrictEqual(
|
|
stmt.run('key2', 'val2'),
|
|
{ changes: 1, lastInsertRowid: 2 },
|
|
);
|
|
stmt = db.prepare('SELECT * FROM storage ORDER BY key');
|
|
t.assert.deepStrictEqual(stmt.all(), [
|
|
{ __proto__: null, key: 'key1', val: 'val1' },
|
|
{ __proto__: null, key: 'key2', val: 'val2' },
|
|
]);
|
|
});
|
|
|
|
test('reflects an added column after the schema changes', (t) => {
|
|
using db = new Database(':memory:');
|
|
db.exec('CREATE TABLE storage(key TEXT, val TEXT)');
|
|
db.prepare('INSERT INTO storage (key, val) VALUES (?, ?)').run('key1', 'val1');
|
|
const stmt = db.prepare('SELECT * FROM storage ORDER BY key');
|
|
db.exec("ALTER TABLE storage ADD COLUMN extra TEXT DEFAULT 'def'");
|
|
t.assert.deepStrictEqual(stmt.all(), [
|
|
{ __proto__: null, key: 'key1', val: 'val1', extra: 'def' },
|
|
]);
|
|
});
|
|
|
|
test('reflects a dropped column after the schema changes', (t) => {
|
|
using db = new Database(':memory:');
|
|
db.exec('CREATE TABLE storage(key TEXT, val TEXT, extra TEXT)');
|
|
db.prepare('INSERT INTO storage (key, val, extra) VALUES (?, ?, ?)')
|
|
.run('key1', 'val1', 'x');
|
|
const stmt = db.prepare('SELECT * FROM storage ORDER BY key');
|
|
db.exec('ALTER TABLE storage DROP COLUMN extra');
|
|
t.assert.deepStrictEqual(stmt.all(), [
|
|
{ __proto__: null, key: 'key1', val: 'val1' },
|
|
]);
|
|
});
|
|
|
|
test('throws if the statement is already finalized', (t) => {
|
|
using db = new Database(':memory:');
|
|
const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
|
|
stmt.close();
|
|
t.assert.throws(() => {
|
|
stmt.all();
|
|
}, {
|
|
code: 'ERR_INVALID_STATE',
|
|
message: /statement has been finalized/,
|
|
});
|
|
});
|
|
|
|
test('surfaces a deferred SQLite error from reset() even though the array was already built', (t) => {
|
|
using db = new Database(':memory:');
|
|
db.exec(`
|
|
PRAGMA foreign_keys = ON;
|
|
PRAGMA defer_foreign_keys = ON;
|
|
CREATE TABLE parent(id INTEGER PRIMARY KEY);
|
|
CREATE TABLE child(id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parent(id));
|
|
`);
|
|
const stmt = db.prepare(
|
|
'INSERT INTO child (parent_id) VALUES (999) RETURNING id'
|
|
);
|
|
t.assert.throws(() => {
|
|
stmt.all();
|
|
}, {
|
|
code: 'ERR_SQLITE_ERROR',
|
|
message: /FOREIGN KEY constraint failed/,
|
|
});
|
|
});
|
|
});
|
|
|
|
suite('Statement.prototype.iterate()', () => {
|
|
test('executes a query and returns an empty iterator on no results', (t) => {
|
|
using db = new Database(':memory:');
|
|
const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
|
|
const iter = stmt.iterate();
|
|
t.assert.strictEqual(iter instanceof globalThis.Iterator, true);
|
|
t.assert.ok(iter[Symbol.iterator]);
|
|
t.assert.deepStrictEqual(iter.toArray(), []);
|
|
});
|
|
|
|
test('executes a query and returns all results', (t) => {
|
|
using db = new Database(':memory:');
|
|
let stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
|
|
t.assert.deepStrictEqual(stmt.run(), { changes: 0, lastInsertRowid: 0 });
|
|
stmt = db.prepare('INSERT INTO storage (key, val) VALUES (?, ?)');
|
|
t.assert.deepStrictEqual(
|
|
stmt.run('key1', 'val1'),
|
|
{ changes: 1, lastInsertRowid: 1 },
|
|
);
|
|
t.assert.deepStrictEqual(
|
|
stmt.run('key2', 'val2'),
|
|
{ changes: 1, lastInsertRowid: 2 },
|
|
);
|
|
|
|
const items = [
|
|
{ __proto__: null, key: 'key1', val: 'val1' },
|
|
{ __proto__: null, key: 'key2', val: 'val2' },
|
|
];
|
|
|
|
stmt = db.prepare('SELECT * FROM storage ORDER BY key');
|
|
t.assert.deepStrictEqual(stmt.iterate().toArray(), items);
|
|
|
|
const itemsLoop = items.slice();
|
|
for (const item of stmt.iterate()) {
|
|
t.assert.deepStrictEqual(item, itemsLoop.shift());
|
|
}
|
|
});
|
|
|
|
test('reflects an added column after the schema changes', (t) => {
|
|
using db = new Database(':memory:');
|
|
db.exec('CREATE TABLE storage(key TEXT, val TEXT)');
|
|
db.prepare('INSERT INTO storage (key, val) VALUES (?, ?)').run('key1', 'val1');
|
|
const stmt = db.prepare('SELECT * FROM storage ORDER BY key');
|
|
db.exec("ALTER TABLE storage ADD COLUMN extra TEXT DEFAULT 'def'");
|
|
t.assert.deepStrictEqual(stmt.iterate().toArray(), [
|
|
{ __proto__: null, key: 'key1', val: 'val1', extra: 'def' },
|
|
]);
|
|
});
|
|
|
|
test('reflects a dropped column after the schema changes', (t) => {
|
|
using db = new Database(':memory:');
|
|
db.exec('CREATE TABLE storage(key TEXT, val TEXT, extra TEXT)');
|
|
db.prepare('INSERT INTO storage (key, val, extra) VALUES (?, ?, ?)')
|
|
.run('key1', 'val1', 'x');
|
|
const stmt = db.prepare('SELECT * FROM storage ORDER BY key');
|
|
db.exec('ALTER TABLE storage DROP COLUMN extra');
|
|
t.assert.deepStrictEqual(stmt.iterate().toArray(), [
|
|
{ __proto__: null, key: 'key1', val: 'val1' },
|
|
]);
|
|
});
|
|
|
|
test('iterator keeps the prepared statement from being collected', (t) => {
|
|
const db = new Database(':memory:');
|
|
db.exec(`
|
|
CREATE TABLE test(key TEXT, val TEXT);
|
|
INSERT INTO test (key, val) VALUES ('key1', 'val1');
|
|
INSERT INTO test (key, val) VALUES ('key2', 'val2');
|
|
`);
|
|
// Do not keep an explicit reference to the prepared statement.
|
|
const iterator = db.prepare('SELECT * FROM test').iterate();
|
|
const results = [];
|
|
|
|
global.gc();
|
|
|
|
for (const item of iterator) {
|
|
results.push(item);
|
|
}
|
|
|
|
t.assert.deepStrictEqual(results, [
|
|
{ __proto__: null, key: 'key1', val: 'val1' },
|
|
{ __proto__: null, key: 'key2', val: 'val2' },
|
|
]);
|
|
});
|
|
|
|
test('iterator can be exited early', (t) => {
|
|
const db = new Database(':memory:');
|
|
db.exec(`
|
|
CREATE TABLE test(key TEXT, val TEXT);
|
|
INSERT INTO test (key, val) VALUES ('key1', 'val1');
|
|
INSERT INTO test (key, val) VALUES ('key2', 'val2');
|
|
`);
|
|
const iterator = db.prepare('SELECT * FROM test').iterate();
|
|
const results = [];
|
|
|
|
for (const item of iterator) {
|
|
results.push(item);
|
|
break;
|
|
}
|
|
|
|
t.assert.deepStrictEqual(results, [
|
|
{ __proto__: null, key: 'key1', val: 'val1' },
|
|
]);
|
|
t.assert.deepStrictEqual(
|
|
iterator.next(),
|
|
{ __proto__: null, done: true, value: null },
|
|
);
|
|
});
|
|
|
|
test('iterator is invalidated when statement is reset by get/all/run/iterate', (t) => {
|
|
const db = new Database(':memory:');
|
|
db.exec('CREATE TABLE test (value INTEGER NOT NULL)');
|
|
for (let i = 0; i < 5; i++) {
|
|
db.prepare('INSERT INTO test (value) VALUES (?)').run(i);
|
|
}
|
|
const stmt = db.prepare('SELECT * FROM test');
|
|
|
|
// Invalidated by stmt.get()
|
|
let it = stmt.iterate();
|
|
it.next();
|
|
stmt.get();
|
|
t.assert.throws(() => { it.next(); }, {
|
|
code: 'ERR_INVALID_STATE',
|
|
message: /iterator was invalidated/,
|
|
});
|
|
|
|
// Invalidated by stmt.all()
|
|
it = stmt.iterate();
|
|
it.next();
|
|
stmt.all();
|
|
t.assert.throws(() => { it.next(); }, {
|
|
code: 'ERR_INVALID_STATE',
|
|
message: /iterator was invalidated/,
|
|
});
|
|
|
|
// Invalidated by stmt.run()
|
|
it = stmt.iterate();
|
|
it.next();
|
|
stmt.run();
|
|
t.assert.throws(() => { it.next(); }, {
|
|
code: 'ERR_INVALID_STATE',
|
|
message: /iterator was invalidated/,
|
|
});
|
|
|
|
// Invalidated by a new stmt.iterate()
|
|
it = stmt.iterate();
|
|
it.next();
|
|
const it2 = stmt.iterate();
|
|
t.assert.throws(() => { it.next(); }, {
|
|
code: 'ERR_INVALID_STATE',
|
|
message: /iterator was invalidated/,
|
|
});
|
|
|
|
// New iterator works fine
|
|
t.assert.strictEqual(it2.next().done, false);
|
|
|
|
// Reset on a different statement does NOT invalidate this iterator
|
|
const stmt2 = db.prepare('SELECT * FROM test');
|
|
it = stmt.iterate();
|
|
it.next();
|
|
stmt2.get();
|
|
it.next();
|
|
});
|
|
|
|
test('throws if the statement is already finalized', (t) => {
|
|
using db = new Database(':memory:');
|
|
const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
|
|
stmt.close();
|
|
t.assert.throws(() => {
|
|
stmt.iterate();
|
|
}, {
|
|
code: 'ERR_INVALID_STATE',
|
|
message: /statement has been finalized/,
|
|
});
|
|
});
|
|
|
|
test('does not replay results after the iterator is naturally exhausted', (t) => {
|
|
using db = new Database(':memory:');
|
|
db.exec(`
|
|
CREATE TABLE test(key TEXT);
|
|
INSERT INTO test (key) VALUES ('key1');
|
|
`);
|
|
const it = db.prepare('SELECT * FROM test').iterate();
|
|
t.assert.deepStrictEqual(it.next(), {
|
|
__proto__: null, done: false, value: { __proto__: null, key: 'key1' },
|
|
});
|
|
t.assert.deepStrictEqual(
|
|
it.next(), { __proto__: null, done: true, value: null });
|
|
// Calling next() again on an exhausted iterator must keep reporting
|
|
// done, not silently reset the statement and replay from row 1.
|
|
t.assert.deepStrictEqual(
|
|
it.next(), { __proto__: null, done: true, value: null });
|
|
});
|
|
|
|
test('propagates a pending exception when the loop body throws mid-iteration', (t) => {
|
|
using db = new Database(':memory:');
|
|
db.exec(`
|
|
CREATE TABLE test(key TEXT);
|
|
INSERT INTO test (key) VALUES ('key1');
|
|
INSERT INTO test (key) VALUES ('key2');
|
|
`);
|
|
const stmt = db.prepare('SELECT * FROM test');
|
|
const userError = new Error('boom');
|
|
t.assert.throws(() => {
|
|
// eslint-disable-next-line no-unused-vars
|
|
for (const row of stmt.iterate()) {
|
|
throw userError;
|
|
}
|
|
}, (err) => err === userError);
|
|
});
|
|
});
|
|
|
|
suite('Statement.prototype.run()', () => {
|
|
test('executes a query and returns change metadata', (t) => {
|
|
using db = new Database(':memory:');
|
|
const setup = db.exec(`
|
|
CREATE TABLE storage(key TEXT, val TEXT);
|
|
INSERT INTO storage (key, val) VALUES ('foo', 'bar');
|
|
`);
|
|
t.assert.strictEqual(setup, undefined);
|
|
const stmt = db.prepare('SELECT * FROM storage');
|
|
t.assert.deepStrictEqual(stmt.run(), { changes: 1, lastInsertRowid: 1 });
|
|
});
|
|
|
|
test('SQLite throws when trying to bind too many parameters', (t) => {
|
|
using db = new Database(':memory:');
|
|
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.throws(() => {
|
|
stmt.run(1, 2, 3);
|
|
}, {
|
|
code: 'ERR_SQLITE_ERROR',
|
|
message: 'column index out of range',
|
|
errcode: 25,
|
|
errstr: 'column index out of range',
|
|
});
|
|
});
|
|
|
|
test('SQLite defaults to NULL for unbound parameters', (t) => {
|
|
using db = new Database(':memory:');
|
|
const setup = db.exec(
|
|
'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER NOT NULL) STRICT;'
|
|
);
|
|
t.assert.strictEqual(setup, undefined);
|
|
const stmt = db.prepare('INSERT INTO data (key, val) VALUES (?, ?)');
|
|
t.assert.throws(() => {
|
|
stmt.run(1);
|
|
}, {
|
|
code: 'ERR_SQLITE_ERROR',
|
|
message: 'NOT NULL constraint failed: data.val',
|
|
errcode: 1299,
|
|
errstr: 'constraint failed',
|
|
});
|
|
});
|
|
|
|
test('returns correct metadata when using RETURNING', (t) => {
|
|
const db = new Database(':memory:');
|
|
const setup = db.exec(
|
|
'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER NOT NULL) STRICT;'
|
|
);
|
|
t.assert.strictEqual(setup, undefined);
|
|
const sql = 'INSERT INTO data (key, val) VALUES ($k, $v) RETURNING key';
|
|
const stmt = db.prepare(sql);
|
|
t.assert.deepStrictEqual(
|
|
stmt.run({ k: 1, v: 10 }), { changes: 1, lastInsertRowid: 1 }
|
|
);
|
|
t.assert.deepStrictEqual(
|
|
stmt.run({ k: 2, v: 20 }), { changes: 1, lastInsertRowid: 2 }
|
|
);
|
|
t.assert.deepStrictEqual(
|
|
stmt.run({ k: 3, v: 30 }), { changes: 1, lastInsertRowid: 3 }
|
|
);
|
|
});
|
|
|
|
test('SQLite defaults unbound ?NNN parameters', (t) => {
|
|
using db = new Database(':memory:');
|
|
const setup = db.exec(
|
|
'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER NOT NULL) STRICT;'
|
|
);
|
|
t.assert.strictEqual(setup, undefined);
|
|
const stmt = db.prepare('INSERT INTO data (key, val) VALUES (?1, ?3)');
|
|
|
|
t.assert.throws(() => {
|
|
stmt.run(1);
|
|
}, {
|
|
code: 'ERR_SQLITE_ERROR',
|
|
message: 'NOT NULL constraint failed: data.val',
|
|
errcode: 1299,
|
|
errstr: 'constraint failed',
|
|
});
|
|
});
|
|
|
|
test('binds ?NNN params by position', (t) => {
|
|
using db = new Database(':memory:');
|
|
const setup = db.exec(
|
|
'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER NOT NULL) STRICT;'
|
|
);
|
|
t.assert.strictEqual(setup, undefined);
|
|
const stmt = db.prepare('INSERT INTO data (key, val) VALUES (?1, ?2)');
|
|
t.assert.deepStrictEqual(stmt.run(1, 2), { changes: 1, lastInsertRowid: 1 });
|
|
});
|
|
|
|
test('throws if the statement is already finalized', (t) => {
|
|
using db = new Database(':memory:');
|
|
const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
|
|
stmt.close();
|
|
t.assert.throws(() => {
|
|
stmt.run();
|
|
}, {
|
|
code: 'ERR_INVALID_STATE',
|
|
message: /statement has been finalized/,
|
|
});
|
|
});
|
|
});
|
|
|
|
suite('Statement.prototype.sourceSQL', () => {
|
|
test('equals input SQL', (t) => {
|
|
using db = new Database(':memory:');
|
|
const setup = db.exec(
|
|
'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
|
|
);
|
|
t.assert.strictEqual(setup, undefined);
|
|
const sql = 'INSERT INTO types (key, val) VALUES ($k, $v)';
|
|
const stmt = db.prepare(sql);
|
|
t.assert.strictEqual(stmt.sourceSQL, sql);
|
|
});
|
|
|
|
test('throws if the statement is already finalized', (t) => {
|
|
using db = new Database(':memory:');
|
|
const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
|
|
stmt.close();
|
|
t.assert.throws(() => stmt.sourceSQL, {
|
|
code: 'ERR_INVALID_STATE',
|
|
message: /statement has been finalized/,
|
|
});
|
|
});
|
|
});
|
|
|
|
suite('Statement.prototype.expandedSQL', () => {
|
|
test('equals expanded SQL', (t) => {
|
|
using db = new Database(':memory:');
|
|
const setup = db.exec(
|
|
'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
|
|
);
|
|
t.assert.strictEqual(setup, undefined);
|
|
const sql = 'INSERT INTO types (key, val) VALUES ($k, ?)';
|
|
const expanded = 'INSERT INTO types (key, val) VALUES (\'33\', \'42\')';
|
|
const stmt = db.prepare(sql);
|
|
t.assert.deepStrictEqual(
|
|
stmt.run({ $k: '33' }, '42'),
|
|
{ changes: 1, lastInsertRowid: 33 },
|
|
);
|
|
t.assert.strictEqual(stmt.expandedSQL, expanded);
|
|
});
|
|
|
|
test('throws if the statement is already finalized', (t) => {
|
|
using db = new Database(':memory:');
|
|
const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
|
|
stmt.close();
|
|
t.assert.throws(() => stmt.expandedSQL, {
|
|
code: 'ERR_INVALID_STATE',
|
|
message: /statement has been finalized/,
|
|
});
|
|
});
|
|
});
|
|
|
|
suite('Statement.prototype.stat()', () => {
|
|
const counters = [
|
|
'fullscanStep', 'sort', 'autoindex', 'vmStep', 'reprepare',
|
|
'run', 'memused',
|
|
];
|
|
|
|
// 'filterMiss' and 'filterHit' map to SQLITE_STMTSTATUS_FILTER_MISS and
|
|
// SQLITE_STMTSTATUS_FILTER_HIT, which were added in SQLite 3.38.0. Builds
|
|
// using an older shared SQLite do not expose them.
|
|
const [major, minor] = process.versions.sqlite.split('.').map(Number);
|
|
const hasFilterCounters = major > 3 || (major === 3 && minor >= 38);
|
|
|
|
if (hasFilterCounters) {
|
|
counters.push('filterMiss', 'filterHit');
|
|
}
|
|
|
|
test('returns a number for every valid counter', (t) => {
|
|
using db = new Database(':memory:');
|
|
db.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;');
|
|
const stmt = db.prepare('SELECT * FROM data');
|
|
for (const counter of counters) {
|
|
t.assert.strictEqual(typeof stmt.stat(counter), 'number');
|
|
}
|
|
});
|
|
|
|
test('counts virtual machine steps and runs after execution', (t) => {
|
|
using db = new Database(':memory:');
|
|
db.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;');
|
|
const insert = db.prepare('INSERT INTO data (key, val) VALUES (?, ?)');
|
|
for (let i = 1; i <= 5; i++) {
|
|
insert.run(i, `val-${i}`);
|
|
}
|
|
const stmt = db.prepare('SELECT * FROM data');
|
|
t.assert.strictEqual(stmt.stat('run'), 0);
|
|
t.assert.strictEqual(stmt.stat('vmStep'), 0);
|
|
stmt.all();
|
|
t.assert.strictEqual(stmt.stat('run'), 1);
|
|
t.assert.ok(stmt.stat('vmStep') > 0);
|
|
t.assert.ok(stmt.stat('memused') > 0);
|
|
});
|
|
|
|
test('detects full table scans', (t) => {
|
|
using db = new Database(':memory:');
|
|
db.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;');
|
|
const insert = db.prepare('INSERT INTO data (key, val) VALUES (?, ?)');
|
|
for (let i = 1; i <= 10; i++) {
|
|
insert.run(i, `val-${i}`);
|
|
}
|
|
|
|
// Filtering on a non-indexed column forces a full table scan.
|
|
const scan = db.prepare('SELECT * FROM data WHERE val = ?');
|
|
scan.all('val-5');
|
|
t.assert.ok(scan.stat('fullscanStep') > 0);
|
|
|
|
// Filtering on the primary key uses the index; no full scan occurs.
|
|
const indexed = db.prepare('SELECT * FROM data WHERE key = ?');
|
|
indexed.all(5);
|
|
t.assert.strictEqual(indexed.stat('fullscanStep'), 0);
|
|
});
|
|
|
|
test('reading a counter does not reset it', (t) => {
|
|
using db = new Database(':memory:');
|
|
db.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;');
|
|
const stmt = db.prepare('SELECT * FROM data');
|
|
stmt.all();
|
|
const first = stmt.stat('run');
|
|
t.assert.strictEqual(stmt.stat('run'), first);
|
|
});
|
|
|
|
test('throws if the counter argument is not a string', (t) => {
|
|
using db = new Database(':memory:');
|
|
const stmt = db.prepare('SELECT 1');
|
|
t.assert.throws(() => stmt.stat(), {
|
|
code: 'ERR_INVALID_ARG_TYPE',
|
|
message: /The "counter" argument must be a string/,
|
|
});
|
|
t.assert.throws(() => stmt.stat(42), {
|
|
code: 'ERR_INVALID_ARG_TYPE',
|
|
message: /The "counter" argument must be a string/,
|
|
});
|
|
});
|
|
|
|
test('throws if the counter name is unknown', (t) => {
|
|
using db = new Database(':memory:');
|
|
const stmt = db.prepare('SELECT 1');
|
|
t.assert.throws(() => stmt.stat('nope'), {
|
|
code: 'ERR_INVALID_ARG_VALUE',
|
|
message: /The "counter" argument is not a valid statistic name/,
|
|
});
|
|
});
|
|
|
|
test('throws if the statement is finalized', (t) => {
|
|
const db = new Database(':memory:');
|
|
const stmt = db.prepare('SELECT 1');
|
|
db.close();
|
|
t.assert.throws(() => stmt.stat('run'), {
|
|
code: 'ERR_INVALID_STATE',
|
|
message: /statement has been finalized/,
|
|
});
|
|
});
|
|
});
|
|
|
|
suite('Statement.prototype.resetStats()', () => {
|
|
test('returns undefined', (t) => {
|
|
using db = new Database(':memory:');
|
|
const stmt = db.prepare('SELECT 1');
|
|
t.assert.strictEqual(stmt.resetStats(), undefined);
|
|
});
|
|
|
|
// The column name cache is keyed on the reprepare counter, which
|
|
// resetStats() zeroes. A later re-prepare must not be able to make the
|
|
// counter match the cached generation again and reuse stale names.
|
|
test('invalidates cached iterator column names', (t) => {
|
|
using db = new Database(':memory:');
|
|
db.exec('CREATE TABLE data(a); INSERT INTO data VALUES (1)');
|
|
const stmt = db.prepare('SELECT * FROM data');
|
|
|
|
db.exec('ALTER TABLE data RENAME COLUMN a TO b');
|
|
stmt.iterate().toArray();
|
|
stmt.resetStats();
|
|
db.exec('ALTER TABLE data RENAME COLUMN b TO c');
|
|
|
|
t.assert.deepStrictEqual(stmt.iterate().toArray(), [
|
|
{ __proto__: null, c: 1 },
|
|
]);
|
|
});
|
|
|
|
test('invalidates the cache when the column count grows', (t) => {
|
|
using db = new Database(':memory:');
|
|
db.exec('CREATE TABLE t(a); INSERT INTO t VALUES (1)');
|
|
const stmt = db.prepare('SELECT * FROM t');
|
|
|
|
db.exec('ALTER TABLE t ADD COLUMN b DEFAULT 2');
|
|
stmt.iterate().toArray();
|
|
stmt.resetStats();
|
|
db.exec('ALTER TABLE t ADD COLUMN c DEFAULT 3');
|
|
|
|
t.assert.deepStrictEqual(stmt.iterate().toArray(), [
|
|
{ __proto__: null, a: 1, b: 2, c: 3 },
|
|
]);
|
|
});
|
|
|
|
test('does not reset memused', (t) => {
|
|
using db = new Database(':memory:');
|
|
db.exec('CREATE TABLE t(a); INSERT INTO t VALUES (1),(2),(3)');
|
|
const stmt = db.prepare('SELECT * FROM t ORDER BY a');
|
|
stmt.all();
|
|
|
|
// The memused counter reports current memory usage rather than an
|
|
// accumulated total, so SQLite ignores the reset flag for it.
|
|
const before = stmt.stat('memused');
|
|
t.assert.ok(before > 0);
|
|
stmt.resetStats();
|
|
t.assert.strictEqual(stmt.stat('memused'), before);
|
|
});
|
|
|
|
test('clears every counter', (t) => {
|
|
using db = new Database(':memory:');
|
|
db.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;');
|
|
const insert = db.prepare('INSERT INTO data (key, val) VALUES (?, ?)');
|
|
for (let i = 1; i <= 5; i++) {
|
|
insert.run(i, `val-${i}`);
|
|
}
|
|
|
|
// Force a full table scan so more than one counter is non-zero.
|
|
const stmt = db.prepare('SELECT * FROM data WHERE val = ?');
|
|
stmt.all('val-3');
|
|
t.assert.ok(stmt.stat('run') > 0);
|
|
t.assert.ok(stmt.stat('vmStep') > 0);
|
|
t.assert.ok(stmt.stat('fullscanStep') > 0);
|
|
|
|
stmt.resetStats();
|
|
t.assert.strictEqual(stmt.stat('run'), 0);
|
|
t.assert.strictEqual(stmt.stat('vmStep'), 0);
|
|
t.assert.strictEqual(stmt.stat('fullscanStep'), 0);
|
|
});
|
|
|
|
test('counters accumulate again after a reset', (t) => {
|
|
using db = new Database(':memory:');
|
|
db.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;');
|
|
const stmt = db.prepare('SELECT * FROM data');
|
|
stmt.all();
|
|
stmt.resetStats();
|
|
t.assert.strictEqual(stmt.stat('run'), 0);
|
|
stmt.all();
|
|
t.assert.strictEqual(stmt.stat('run'), 1);
|
|
});
|
|
|
|
test('is a no-op when no counters have been incremented', (t) => {
|
|
using db = new Database(':memory:');
|
|
const stmt = db.prepare('SELECT 1');
|
|
stmt.resetStats();
|
|
t.assert.strictEqual(stmt.stat('run'), 0);
|
|
});
|
|
|
|
test('throws if the statement is finalized', (t) => {
|
|
const db = new Database(':memory:');
|
|
const stmt = db.prepare('SELECT 1');
|
|
db.close();
|
|
t.assert.throws(() => stmt.resetStats(), {
|
|
code: 'ERR_INVALID_STATE',
|
|
message: /statement has been finalized/,
|
|
});
|
|
});
|
|
});
|
|
|
|
suite('Statement.prototype.setReadBigInts()', () => {
|
|
test('BigInts support can be toggled', (t) => {
|
|
using db = new Database(':memory:');
|
|
const setup = db.exec(`
|
|
CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;
|
|
INSERT INTO data (key, val) VALUES (1, 42);
|
|
`);
|
|
t.assert.strictEqual(setup, undefined);
|
|
|
|
const query = db.prepare('SELECT val FROM data');
|
|
t.assert.deepStrictEqual(query.get(), { __proto__: null, val: 42 });
|
|
t.assert.strictEqual(query.setReadBigInts(true), undefined);
|
|
t.assert.deepStrictEqual(query.get(), { __proto__: null, val: 42n });
|
|
t.assert.strictEqual(query.setReadBigInts(false), undefined);
|
|
t.assert.deepStrictEqual(query.get(), { __proto__: null, val: 42 });
|
|
|
|
const insert = db.prepare('INSERT INTO data (key) VALUES (?)');
|
|
t.assert.deepStrictEqual(
|
|
insert.run(10),
|
|
{ changes: 1, lastInsertRowid: 10 },
|
|
);
|
|
t.assert.strictEqual(insert.setReadBigInts(true), undefined);
|
|
t.assert.deepStrictEqual(
|
|
insert.run(20),
|
|
{ changes: 1n, lastInsertRowid: 20n },
|
|
);
|
|
t.assert.strictEqual(insert.setReadBigInts(false), undefined);
|
|
t.assert.deepStrictEqual(
|
|
insert.run(30),
|
|
{ changes: 1, lastInsertRowid: 30 },
|
|
);
|
|
});
|
|
|
|
test('throws when input is not a boolean', (t) => {
|
|
using db = new Database(':memory:');
|
|
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)');
|
|
t.assert.throws(() => {
|
|
stmt.setReadBigInts();
|
|
}, {
|
|
code: 'ERR_INVALID_ARG_TYPE',
|
|
message: /The "readBigInts" argument must be a boolean/,
|
|
});
|
|
});
|
|
|
|
test('BigInt is required for reading large integers', (t) => {
|
|
using db = new Database(':memory:');
|
|
const bad = db.prepare(`SELECT ${Number.MAX_SAFE_INTEGER} + 1`);
|
|
t.assert.throws(() => {
|
|
bad.get();
|
|
}, {
|
|
code: 'ERR_OUT_OF_RANGE',
|
|
message: /^Value is too large to be represented as a JavaScript number: 9007199254740992$/,
|
|
});
|
|
const good = db.prepare(`SELECT ${Number.MAX_SAFE_INTEGER} + 1`);
|
|
good.setReadBigInts(true);
|
|
t.assert.deepStrictEqual(good.get(), {
|
|
__proto__: null,
|
|
[`${Number.MAX_SAFE_INTEGER} + 1`]: 2n ** 53n,
|
|
});
|
|
});
|
|
|
|
test('BigInt is required for reading large last insert row IDs', (t) => {
|
|
using db = new Database(':memory:');
|
|
db.exec('CREATE TABLE data(key INTEGER PRIMARY KEY) STRICT');
|
|
const insert = db.prepare('INSERT INTO data VALUES (?)');
|
|
|
|
t.assert.throws(() => {
|
|
insert.run(9007199254740993n);
|
|
}, {
|
|
code: 'ERR_OUT_OF_RANGE',
|
|
message: /^Value is too large to be represented as a JavaScript number: 9007199254740993$/,
|
|
});
|
|
|
|
insert.setReadBigInts(true);
|
|
t.assert.deepStrictEqual(insert.run(9007199254740995n), {
|
|
changes: 1n,
|
|
lastInsertRowid: 9007199254740995n,
|
|
});
|
|
});
|
|
|
|
test('throws if the statement is already finalized', (t) => {
|
|
using db = new Database(':memory:');
|
|
const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
|
|
stmt.close();
|
|
t.assert.throws(() => {
|
|
stmt.setReadBigInts(true);
|
|
}, {
|
|
code: 'ERR_INVALID_STATE',
|
|
message: /statement has been finalized/,
|
|
});
|
|
});
|
|
});
|
|
|
|
suite('Statement.prototype.setReturnArrays()', () => {
|
|
test('throws when input is not a boolean', (t) => {
|
|
using db = new Database(':memory:');
|
|
const setup = db.exec(
|
|
'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
|
|
);
|
|
t.assert.strictEqual(setup, undefined);
|
|
const stmt = db.prepare('SELECT key, val FROM data');
|
|
t.assert.throws(() => {
|
|
stmt.setReturnArrays();
|
|
}, {
|
|
code: 'ERR_INVALID_ARG_TYPE',
|
|
message: /The "returnArrays" argument must be a boolean/,
|
|
});
|
|
});
|
|
|
|
test('throws if the statement is already finalized', (t) => {
|
|
using db = new Database(':memory:');
|
|
const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
|
|
stmt.close();
|
|
t.assert.throws(() => {
|
|
stmt.setReturnArrays(true);
|
|
}, {
|
|
code: 'ERR_INVALID_STATE',
|
|
message: /statement has been finalized/,
|
|
});
|
|
});
|
|
});
|
|
|
|
suite('Statement.prototype.get() with array output', () => {
|
|
test('returns array row when setReturnArrays is true', (t) => {
|
|
using db = new Database(':memory:');
|
|
const setup = db.exec(`
|
|
CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
|
|
INSERT INTO data (key, val) VALUES (1, 'one');
|
|
`);
|
|
t.assert.strictEqual(setup, undefined);
|
|
|
|
const query = db.prepare('SELECT key, val FROM data WHERE key = 1');
|
|
t.assert.deepStrictEqual(query.get(), { __proto__: null, key: 1, val: 'one' });
|
|
|
|
query.setReturnArrays(true);
|
|
t.assert.deepStrictEqual(query.get(), [1, 'one']);
|
|
|
|
query.setReturnArrays(false);
|
|
t.assert.deepStrictEqual(query.get(), { __proto__: null, key: 1, val: 'one' });
|
|
});
|
|
|
|
test('returns array rows with BigInts when both flags are set', (t) => {
|
|
const expected = [1n, 9007199254740992n];
|
|
using db = new Database(':memory:');
|
|
const setup = db.exec(`
|
|
CREATE TABLE big_data(id INTEGER, big_num INTEGER);
|
|
INSERT INTO big_data VALUES (1, 9007199254740992);
|
|
`);
|
|
t.assert.strictEqual(setup, undefined);
|
|
|
|
const query = db.prepare('SELECT id, big_num FROM big_data');
|
|
query.setReturnArrays(true);
|
|
query.setReadBigInts(true);
|
|
|
|
const row = query.get();
|
|
t.assert.deepStrictEqual(row, expected);
|
|
});
|
|
});
|
|
|
|
suite('Statement.prototype.all() with array output', () => {
|
|
test('returns array rows when setReturnArrays is true', (t) => {
|
|
using db = new Database(':memory:');
|
|
const setup = db.exec(`
|
|
CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
|
|
INSERT INTO data (key, val) VALUES (1, 'one');
|
|
INSERT INTO data (key, val) VALUES (2, 'two');
|
|
`);
|
|
t.assert.strictEqual(setup, undefined);
|
|
|
|
const query = db.prepare('SELECT key, val FROM data ORDER BY key');
|
|
t.assert.deepStrictEqual(query.all(), [
|
|
{ __proto__: null, key: 1, val: 'one' },
|
|
{ __proto__: null, key: 2, val: 'two' },
|
|
]);
|
|
|
|
query.setReturnArrays(true);
|
|
t.assert.deepStrictEqual(query.all(), [
|
|
[1, 'one'],
|
|
[2, 'two'],
|
|
]);
|
|
|
|
query.setReturnArrays(false);
|
|
t.assert.deepStrictEqual(query.all(), [
|
|
{ __proto__: null, key: 1, val: 'one' },
|
|
{ __proto__: null, key: 2, val: 'two' },
|
|
]);
|
|
});
|
|
|
|
test('handles array rows with many columns', (t) => {
|
|
const expected = [
|
|
1,
|
|
'text1',
|
|
1.1,
|
|
new Uint8Array([0xde, 0xad, 0xbe, 0xef]),
|
|
5,
|
|
'text2',
|
|
2.2,
|
|
new Uint8Array([0xbe, 0xef, 0xca, 0xfe]),
|
|
9,
|
|
'text3',
|
|
];
|
|
using db = new Database(':memory:');
|
|
const setup = db.exec(`
|
|
CREATE TABLE wide_table(
|
|
col1 INTEGER, col2 TEXT, col3 REAL, col4 BLOB, col5 INTEGER,
|
|
col6 TEXT, col7 REAL, col8 BLOB, col9 INTEGER, col10 TEXT
|
|
);
|
|
INSERT INTO wide_table VALUES (
|
|
1, 'text1', 1.1, X'DEADBEEF', 5,
|
|
'text2', 2.2, X'BEEFCAFE', 9, 'text3'
|
|
);
|
|
`);
|
|
t.assert.strictEqual(setup, undefined);
|
|
|
|
const query = db.prepare('SELECT * FROM wide_table');
|
|
query.setReturnArrays(true);
|
|
|
|
const results = query.all();
|
|
t.assert.strictEqual(results.length, 1);
|
|
t.assert.deepStrictEqual(results[0], expected);
|
|
});
|
|
});
|
|
|
|
suite('Statement.prototype.iterate() with array output', () => {
|
|
test('iterates array rows when setReturnArrays is true', (t) => {
|
|
using db = new Database(':memory:');
|
|
const setup = db.exec(`
|
|
CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
|
|
INSERT INTO data (key, val) VALUES (1, 'one');
|
|
INSERT INTO data (key, val) VALUES (2, 'two');
|
|
`);
|
|
t.assert.strictEqual(setup, undefined);
|
|
|
|
const query = db.prepare('SELECT key, val FROM data ORDER BY key');
|
|
|
|
// Test with objects first
|
|
const objectRows = [];
|
|
for (const row of query.iterate()) {
|
|
objectRows.push(row);
|
|
}
|
|
t.assert.deepStrictEqual(objectRows, [
|
|
{ __proto__: null, key: 1, val: 'one' },
|
|
{ __proto__: null, key: 2, val: 'two' },
|
|
]);
|
|
|
|
// Test with arrays
|
|
query.setReturnArrays(true);
|
|
const arrayRows = [];
|
|
for (const row of query.iterate()) {
|
|
arrayRows.push(row);
|
|
}
|
|
t.assert.deepStrictEqual(arrayRows, [
|
|
[1, 'one'],
|
|
[2, 'two'],
|
|
]);
|
|
|
|
// Test toArray() method
|
|
t.assert.deepStrictEqual(query.iterate().toArray(), [
|
|
[1, 'one'],
|
|
[2, 'two'],
|
|
]);
|
|
});
|
|
|
|
test('iterator can be exited early with array rows', (t) => {
|
|
const db = new Database(':memory:');
|
|
db.exec(`
|
|
CREATE TABLE test(key TEXT, val TEXT);
|
|
INSERT INTO test (key, val) VALUES ('key1', 'val1');
|
|
INSERT INTO test (key, val) VALUES ('key2', 'val2');
|
|
`);
|
|
const stmt = db.prepare('SELECT key, val FROM test');
|
|
stmt.setReturnArrays(true);
|
|
|
|
const iterator = stmt.iterate();
|
|
const results = [];
|
|
|
|
for (const item of iterator) {
|
|
results.push(item);
|
|
break;
|
|
}
|
|
|
|
t.assert.deepStrictEqual(results, [
|
|
['key1', 'val1'],
|
|
]);
|
|
t.assert.deepStrictEqual(
|
|
iterator.next(),
|
|
{ __proto__: null, done: true, value: null },
|
|
);
|
|
});
|
|
});
|
|
|
|
suite('Statement.prototype.setAllowBareNamedParameters()', () => {
|
|
test('bare named parameter support can be toggled', (t) => {
|
|
using db = new Database(':memory:');
|
|
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 ($k, $v)');
|
|
t.assert.deepStrictEqual(
|
|
stmt.run({ k: 1, v: 2 }),
|
|
{ changes: 1, lastInsertRowid: 1 },
|
|
);
|
|
t.assert.strictEqual(stmt.setAllowBareNamedParameters(false), undefined);
|
|
t.assert.throws(() => {
|
|
stmt.run({ k: 2, v: 4 });
|
|
}, {
|
|
code: 'ERR_INVALID_STATE',
|
|
message: /Unknown named parameter 'k'/,
|
|
});
|
|
t.assert.strictEqual(stmt.setAllowBareNamedParameters(true), undefined);
|
|
t.assert.deepStrictEqual(
|
|
stmt.run({ k: 3, v: 6 }),
|
|
{ changes: 1, lastInsertRowid: 3 },
|
|
);
|
|
});
|
|
|
|
test('throws when input is not a boolean', (t) => {
|
|
using db = new Database(':memory:');
|
|
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 ($k, $v)');
|
|
t.assert.throws(() => {
|
|
stmt.setAllowBareNamedParameters();
|
|
}, {
|
|
code: 'ERR_INVALID_ARG_TYPE',
|
|
message: /The "allowBareNamedParameters" argument must be a boolean/,
|
|
});
|
|
});
|
|
|
|
test('throws if the statement is already finalized', (t) => {
|
|
using db = new Database(':memory:');
|
|
const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
|
|
stmt.close();
|
|
t.assert.throws(() => {
|
|
stmt.setAllowBareNamedParameters(true);
|
|
}, {
|
|
code: 'ERR_INVALID_STATE',
|
|
message: /statement has been finalized/,
|
|
});
|
|
});
|
|
});
|
|
|
|
suite('options.readBigInts', () => {
|
|
test('BigInts are returned when input is true', (t) => {
|
|
using db = new Database(':memory:');
|
|
const setup = db.exec(`
|
|
CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;
|
|
INSERT INTO data (key, val) VALUES (1, 42);
|
|
`);
|
|
t.assert.strictEqual(setup, undefined);
|
|
|
|
const query = db.prepare('SELECT val FROM data', { readBigInts: true });
|
|
t.assert.deepStrictEqual(query.get(), { __proto__: null, val: 42n });
|
|
});
|
|
|
|
test('numbers are returned when input is false', (t) => {
|
|
using db = new Database(':memory:');
|
|
const setup = db.exec(`
|
|
CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;
|
|
INSERT INTO data (key, val) VALUES (1, 42);
|
|
`);
|
|
t.assert.strictEqual(setup, undefined);
|
|
|
|
const query = db.prepare('SELECT val FROM data', { readBigInts: false });
|
|
t.assert.deepStrictEqual(query.get(), { __proto__: null, val: 42 });
|
|
});
|
|
|
|
test('throws when input is not a boolean', (t) => {
|
|
using db = new Database(':memory:');
|
|
const setup = db.exec(
|
|
'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
|
|
);
|
|
t.assert.strictEqual(setup, undefined);
|
|
t.assert.throws(() => {
|
|
db.prepare('SELECT val FROM data', { readBigInts: 'true' });
|
|
}, {
|
|
code: 'ERR_INVALID_ARG_TYPE',
|
|
message: /The "options\.readBigInts" argument must be a boolean/,
|
|
});
|
|
});
|
|
|
|
test('setReadBigInts can override prepare option', (t) => {
|
|
using db = new Database(':memory:');
|
|
const setup = db.exec(`
|
|
CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;
|
|
INSERT INTO data (key, val) VALUES (1, 42);
|
|
`);
|
|
t.assert.strictEqual(setup, undefined);
|
|
|
|
const query = db.prepare('SELECT val FROM data', { readBigInts: true });
|
|
t.assert.deepStrictEqual(query.get(), { __proto__: null, val: 42n });
|
|
t.assert.strictEqual(query.setReadBigInts(false), undefined);
|
|
t.assert.deepStrictEqual(query.get(), { __proto__: null, val: 42 });
|
|
});
|
|
});
|
|
|
|
suite('options.returnArrays', () => {
|
|
test('arrays are returned when input is true', (t) => {
|
|
using db = new Database(':memory:');
|
|
const setup = db.exec(`
|
|
CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
|
|
INSERT INTO data (key, val) VALUES (1, 'one');
|
|
`);
|
|
t.assert.strictEqual(setup, undefined);
|
|
|
|
const query = db.prepare(
|
|
'SELECT key, val FROM data WHERE key = 1',
|
|
{ returnArrays: true }
|
|
);
|
|
t.assert.deepStrictEqual(query.get(), [1, 'one']);
|
|
});
|
|
|
|
test('objects are returned when input is false', (t) => {
|
|
using db = new Database(':memory:');
|
|
const setup = db.exec(`
|
|
CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
|
|
INSERT INTO data (key, val) VALUES (1, 'one');
|
|
`);
|
|
t.assert.strictEqual(setup, undefined);
|
|
|
|
const query = db.prepare(
|
|
'SELECT key, val FROM data WHERE key = 1',
|
|
{ returnArrays: false }
|
|
);
|
|
t.assert.deepStrictEqual(query.get(), { __proto__: null, key: 1, val: 'one' });
|
|
});
|
|
|
|
test('throws when input is not a boolean', (t) => {
|
|
using db = new Database(':memory:');
|
|
const setup = db.exec(
|
|
'CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;'
|
|
);
|
|
t.assert.strictEqual(setup, undefined);
|
|
t.assert.throws(() => {
|
|
db.prepare('SELECT key, val FROM data', { returnArrays: 'true' });
|
|
}, {
|
|
code: 'ERR_INVALID_ARG_TYPE',
|
|
message: /The "options\.returnArrays" argument must be a boolean/,
|
|
});
|
|
});
|
|
|
|
test('setReturnArrays can override prepare option', (t) => {
|
|
using db = new Database(':memory:');
|
|
const setup = db.exec(`
|
|
CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
|
|
INSERT INTO data (key, val) VALUES (1, 'one');
|
|
`);
|
|
t.assert.strictEqual(setup, undefined);
|
|
|
|
const query = db.prepare(
|
|
'SELECT key, val FROM data WHERE key = 1',
|
|
{ returnArrays: true }
|
|
);
|
|
t.assert.deepStrictEqual(query.get(), [1, 'one']);
|
|
t.assert.strictEqual(query.setReturnArrays(false), undefined);
|
|
t.assert.deepStrictEqual(query.get(), { __proto__: null, key: 1, val: 'one' });
|
|
});
|
|
|
|
test('all() returns arrays when input is true', (t) => {
|
|
using db = new Database(':memory:');
|
|
const setup = db.exec(`
|
|
CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
|
|
INSERT INTO data (key, val) VALUES (1, 'one');
|
|
INSERT INTO data (key, val) VALUES (2, 'two');
|
|
`);
|
|
t.assert.strictEqual(setup, undefined);
|
|
|
|
const query = db.prepare(
|
|
'SELECT key, val FROM data ORDER BY key',
|
|
{ returnArrays: true }
|
|
);
|
|
t.assert.deepStrictEqual(query.all(), [
|
|
[1, 'one'],
|
|
[2, 'two'],
|
|
]);
|
|
});
|
|
|
|
test('iterate() returns arrays when input is true', (t) => {
|
|
using db = new Database(':memory:');
|
|
const setup = db.exec(`
|
|
CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
|
|
INSERT INTO data (key, val) VALUES (1, 'one');
|
|
INSERT INTO data (key, val) VALUES (2, 'two');
|
|
`);
|
|
t.assert.strictEqual(setup, undefined);
|
|
|
|
const query = db.prepare(
|
|
'SELECT key, val FROM data ORDER BY key',
|
|
{ returnArrays: true }
|
|
);
|
|
t.assert.deepStrictEqual(query.iterate().toArray(), [
|
|
[1, 'one'],
|
|
[2, 'two'],
|
|
]);
|
|
});
|
|
});
|
|
|
|
suite('options.allowBareNamedParameters', () => {
|
|
test('bare named parameters are allowed when input is true', (t) => {
|
|
using db = new Database(':memory:');
|
|
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 ($k, $v)',
|
|
{ allowBareNamedParameters: true }
|
|
);
|
|
t.assert.deepStrictEqual(
|
|
stmt.run({ k: 1, v: 2 }),
|
|
{ changes: 1, lastInsertRowid: 1 },
|
|
);
|
|
});
|
|
|
|
test('bare named parameters throw when input is false', (t) => {
|
|
using db = new Database(':memory:');
|
|
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 ($k, $v)',
|
|
{ allowBareNamedParameters: false }
|
|
);
|
|
t.assert.throws(() => {
|
|
stmt.run({ k: 1, v: 2 });
|
|
}, {
|
|
code: 'ERR_INVALID_STATE',
|
|
message: /Unknown named parameter 'k'/,
|
|
});
|
|
});
|
|
|
|
test('throws when input is not a boolean', (t) => {
|
|
using db = new Database(':memory:');
|
|
const setup = db.exec(
|
|
'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
|
|
);
|
|
t.assert.strictEqual(setup, undefined);
|
|
t.assert.throws(() => {
|
|
db.prepare(
|
|
'INSERT INTO data (key, val) VALUES ($k, $v)',
|
|
{ allowBareNamedParameters: 'true' }
|
|
);
|
|
}, {
|
|
code: 'ERR_INVALID_ARG_TYPE',
|
|
message: /The "options\.allowBareNamedParameters" argument must be a boolean/,
|
|
});
|
|
});
|
|
|
|
test('setAllowBareNamedParameters can override prepare option', (t) => {
|
|
using db = new Database(':memory:');
|
|
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 ($k, $v)',
|
|
{ allowBareNamedParameters: false }
|
|
);
|
|
t.assert.throws(() => {
|
|
stmt.run({ k: 1, v: 2 });
|
|
}, {
|
|
code: 'ERR_INVALID_STATE',
|
|
message: /Unknown named parameter 'k'/,
|
|
});
|
|
t.assert.strictEqual(stmt.setAllowBareNamedParameters(true), undefined);
|
|
t.assert.deepStrictEqual(
|
|
stmt.run({ k: 2, v: 4 }),
|
|
{ changes: 1, lastInsertRowid: 2 },
|
|
);
|
|
});
|
|
});
|
|
|
|
|
|
suite('Statement.prototype.close()', () => {
|
|
test('finalizes an open statement', (t) => {
|
|
using db = new Database(':memory:');
|
|
db.exec('CREATE TABLE storage(key TEXT, val TEXT)');
|
|
const stmt = db.prepare('SELECT * FROM storage');
|
|
t.assert.strictEqual(stmt.close(), undefined);
|
|
t.assert.throws(() => stmt.get(), {
|
|
code: 'ERR_INVALID_STATE',
|
|
message: /statement has been finalized/,
|
|
});
|
|
});
|
|
|
|
test('throws if the statement is already finalized', (t) => {
|
|
using db = new Database(':memory:');
|
|
const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
|
|
stmt.close();
|
|
t.assert.throws(() => {
|
|
stmt.close();
|
|
}, {
|
|
code: 'ERR_INVALID_STATE',
|
|
message: /statement has been finalized/,
|
|
});
|
|
});
|
|
});
|
|
|
|
suite('Statement.prototype[Symbol.dispose]()', () => {
|
|
test('finalizes an open statement', (t) => {
|
|
using db = new Database(':memory:');
|
|
db.exec('CREATE TABLE storage(key TEXT, val TEXT)');
|
|
const stmt = db.prepare('SELECT * FROM storage');
|
|
stmt[Symbol.dispose]();
|
|
t.assert.throws(() => stmt.get(), {
|
|
code: 'ERR_INVALID_STATE',
|
|
message: /statement has been finalized/,
|
|
});
|
|
});
|
|
|
|
test('does not throw on an already-finalized statement', () => {
|
|
using db = new Database(':memory:');
|
|
const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
|
|
stmt[Symbol.dispose]();
|
|
stmt[Symbol.dispose]();
|
|
});
|
|
|
|
test('works with a using declaration', (t) => {
|
|
using db = new Database(':memory:');
|
|
db.exec('CREATE TABLE storage(key TEXT, val TEXT)');
|
|
let captured;
|
|
{
|
|
using stmt = db.prepare('SELECT * FROM storage');
|
|
captured = stmt;
|
|
t.assert.deepStrictEqual(stmt.all(), []);
|
|
}
|
|
t.assert.throws(() => captured.get(), {
|
|
code: 'ERR_INVALID_STATE',
|
|
message: /statement has been finalized/,
|
|
});
|
|
});
|
|
|
|
test('closing the database after dispose does not double-finalize', () => {
|
|
using db = new Database(':memory:');
|
|
const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
|
|
stmt[Symbol.dispose]();
|
|
db.close();
|
|
});
|
|
});
|
|
|
|
suite('options.persistent', () => {
|
|
test('statement executes correctly when persistent is true', (t) => {
|
|
using db = new Database(':memory:');
|
|
db.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;');
|
|
db.exec('INSERT INTO data (key, val) VALUES (1, 42);');
|
|
using stmt = db.prepare('SELECT val FROM data', { persistent: true });
|
|
t.assert.deepStrictEqual(stmt.get(), { __proto__: null, val: 42 });
|
|
});
|
|
|
|
test('statement executes correctly when persistent is false', (t) => {
|
|
using db = new Database(':memory:');
|
|
db.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;');
|
|
db.exec('INSERT INTO data (key, val) VALUES (1, 42);');
|
|
using stmt = db.prepare('SELECT val FROM data', { persistent: false });
|
|
t.assert.deepStrictEqual(stmt.get(), { __proto__: null, val: 42 });
|
|
});
|
|
|
|
test('throws when input is not a boolean', (t) => {
|
|
using db = new Database(':memory:');
|
|
t.assert.throws(() => {
|
|
db.prepare('SELECT 1', { persistent: 'yes' });
|
|
}, {
|
|
code: 'ERR_INVALID_ARG_TYPE',
|
|
message: /The "options\.persistent" argument must be a boolean/,
|
|
});
|
|
});
|
|
|
|
test('can be combined with other options', (t) => {
|
|
using db = new Database(':memory:');
|
|
db.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;');
|
|
db.exec('INSERT INTO data (key, val) VALUES (1, 42);');
|
|
using stmt = db.prepare(
|
|
'SELECT val FROM data',
|
|
{ persistent: true, readBigInts: true }
|
|
);
|
|
t.assert.deepStrictEqual(stmt.get(), { __proto__: null, val: 42n });
|
|
});
|
|
});
|