// 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 });
});
});