mirror of
https://github.com/nodejs/node.git
synced 2026-11-09 15:11:11 -05:00
Rename the DatabaseSync and StatementSync classes to Database and Statement, and the internal DatabaseSyncLimits helper to DatabaseLimits. The old names are kept as aliases of the new classes and are Documentation-only deprecated (DEP0210, DEP0211). Assisted-by: Claude Code Signed-off-by: Guilherme Araújo <arauujogui@gmail.com> PR-URL: https://github.com/nodejs/node/pull/65988 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Robert Nagy <ronagy@icloud.com> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
391 lines
13 KiB
JavaScript
391 lines
13 KiB
JavaScript
'use strict';
|
|
// Flags: --expose-gc
|
|
|
|
const { skipIfSQLiteMissing } = require('../common');
|
|
skipIfSQLiteMissing();
|
|
|
|
const assert = require('assert');
|
|
const { Database } = require('node:sqlite');
|
|
const { test, beforeEach } = require('node:test');
|
|
|
|
const db = new Database(':memory:');
|
|
const sql = db.createTagStore(10);
|
|
|
|
beforeEach(() => {
|
|
db.exec('DROP TABLE IF EXISTS foo');
|
|
db.exec('CREATE TABLE foo (id INTEGER PRIMARY KEY, text TEXT)');
|
|
sql.clear();
|
|
});
|
|
|
|
test('throws error if database is not open', () => {
|
|
const db = new Database(':memory:', { open: false });
|
|
|
|
assert.throws(() => {
|
|
db.createTagStore(10);
|
|
}, {
|
|
code: 'ERR_INVALID_STATE',
|
|
message: 'database is not open'
|
|
});
|
|
});
|
|
|
|
test('sql.run inserts data', () => {
|
|
assert.strictEqual(sql.run`INSERT INTO foo (text) VALUES (${'bob'})`.changes, 1);
|
|
assert.strictEqual(sql.run`INSERT INTO foo (text) VALUES (${'mac'})`.changes, 1);
|
|
assert.strictEqual(sql.run`INSERT INTO foo (text) VALUES (${'alice'})`.changes, 1);
|
|
|
|
const count = db.prepare('SELECT COUNT(*) as count FROM foo').get().count;
|
|
assert.strictEqual(count, 3);
|
|
});
|
|
|
|
test('sql.get retrieves a single row', () => {
|
|
assert.strictEqual(sql.run`INSERT INTO foo (text) VALUES (${'bob'})`.changes, 1);
|
|
const first = sql.get`SELECT * FROM foo ORDER BY id ASC`;
|
|
assert.ok(first);
|
|
assert.strictEqual(first.text, 'bob');
|
|
assert.strictEqual(first.id, 1);
|
|
assert.strictEqual(Object.getPrototypeOf(first), null);
|
|
});
|
|
|
|
test('sql.all retrieves all rows', () => {
|
|
assert.strictEqual(sql.run`INSERT INTO foo (text) VALUES (${'bob'})`.changes, 1);
|
|
assert.strictEqual(sql.run`INSERT INTO foo (text) VALUES (${'mac'})`.changes, 1);
|
|
assert.strictEqual(sql.run`INSERT INTO foo (text) VALUES (${'alice'})`.changes, 1);
|
|
|
|
const all = sql.all`SELECT * FROM foo ORDER BY id ASC`;
|
|
assert.strictEqual(Array.isArray(all), true);
|
|
assert.strictEqual(all.length, 3);
|
|
for (const row of all) {
|
|
assert.strictEqual(Object.getPrototypeOf(row), null);
|
|
}
|
|
assert.deepStrictEqual(all.map((r) => r.text), ['bob', 'mac', 'alice']);
|
|
});
|
|
|
|
test('sql.iterate retrieves rows via iterator', () => {
|
|
assert.strictEqual(sql.run`INSERT INTO foo (text) VALUES (${'bob'})`.changes, 1);
|
|
assert.strictEqual(sql.run`INSERT INTO foo (text) VALUES (${'mac'})`.changes, 1);
|
|
assert.strictEqual(sql.run`INSERT INTO foo (text) VALUES (${'alice'})`.changes, 1);
|
|
|
|
const iter = sql.iterate`SELECT * FROM foo ORDER BY id ASC`;
|
|
const iterRows = [];
|
|
for (const row of iter) {
|
|
assert.ok(row);
|
|
assert.strictEqual(Object.getPrototypeOf(row), null);
|
|
iterRows.push(row.text);
|
|
}
|
|
assert.deepStrictEqual(iterRows, ['bob', 'mac', 'alice']);
|
|
});
|
|
|
|
test('queries with no results', () => {
|
|
const none = sql.get`SELECT * FROM foo WHERE text = ${'notfound'}`;
|
|
assert.strictEqual(none, undefined);
|
|
|
|
const empty = sql.all`SELECT * FROM foo WHERE text = ${'notfound'}`;
|
|
assert.deepStrictEqual(empty, []);
|
|
|
|
let count = 0;
|
|
// eslint-disable-next-line no-unused-vars
|
|
for (const _ of sql.iterate`SELECT * FROM foo WHERE text = ${'notfound'}`) {
|
|
count++;
|
|
}
|
|
assert.strictEqual(count, 0);
|
|
});
|
|
|
|
test('rejects parameters outside of template expressions', () => {
|
|
const ldb = new Database(':memory:');
|
|
const lsql = ldb.createTagStore();
|
|
ldb.exec(`
|
|
CREATE TABLE secrets(owner TEXT, token TEXT);
|
|
INSERT INTO secrets VALUES ('victim', 'secret');
|
|
CREATE TABLE transfers(from_user TEXT, amount INTEGER);
|
|
`);
|
|
|
|
const expectedError = {
|
|
code: 'ERR_INVALID_ARG_VALUE',
|
|
message: /must be bound using template literal placeholders/,
|
|
};
|
|
|
|
for (const method of ['get', 'all', 'iterate']) {
|
|
// Prime the cached statement with a bound value before each attempt.
|
|
// eslint-disable-next-line no-unused-expressions
|
|
lsql.all`SELECT token FROM secrets WHERE owner = ${'victim'}`;
|
|
assert.throws(() => {
|
|
// eslint-disable-next-line no-unused-expressions
|
|
lsql[method]`SELECT token FROM secrets WHERE owner = ?`;
|
|
}, expectedError);
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-expressions
|
|
lsql.run`INSERT INTO transfers VALUES (${'victim'},${100})`;
|
|
assert.throws(() => {
|
|
// eslint-disable-next-line no-unused-expressions
|
|
lsql.run`INSERT INTO transfers VALUES (?,?)`;
|
|
}, expectedError);
|
|
assert.strictEqual(
|
|
ldb.prepare('SELECT COUNT(*) AS count FROM transfers').get().count,
|
|
1);
|
|
|
|
ldb.close();
|
|
});
|
|
|
|
test('TagStore capacity, size, and clear', () => {
|
|
assert.strictEqual(sql.capacity, 10);
|
|
assert.strictEqual(sql.size, 0);
|
|
|
|
assert.strictEqual(sql.run`INSERT INTO foo (text) VALUES (${'one'})`.changes, 1);
|
|
assert.strictEqual(sql.size, 1);
|
|
|
|
assert.ok(sql.get`SELECT * FROM foo WHERE text = ${'one'}`);
|
|
assert.strictEqual(sql.size, 2);
|
|
|
|
// Using the same template string shouldn't increase the size
|
|
assert.strictEqual(sql.get`SELECT * FROM foo WHERE text = ${'two'}`, undefined);
|
|
assert.strictEqual(sql.size, 2);
|
|
|
|
assert.strictEqual(sql.all`SELECT * FROM foo`.length, 1);
|
|
assert.strictEqual(sql.size, 3);
|
|
|
|
sql.clear();
|
|
assert.strictEqual(sql.size, 0);
|
|
assert.strictEqual(sql.capacity, 10);
|
|
});
|
|
|
|
test('iterator is invalidated when the cached statement is reset', () => {
|
|
const ldb = new Database(':memory:');
|
|
const lsql = ldb.createTagStore();
|
|
ldb.exec('CREATE TABLE foo (id INTEGER PRIMARY KEY, text TEXT)');
|
|
for (let i = 0; i < 5; i++) {
|
|
// eslint-disable-next-line no-unused-expressions
|
|
lsql.run`INSERT INTO foo (text) VALUES (${String(i)})`;
|
|
}
|
|
|
|
// Invalidated by sql.get on the same tagged literal
|
|
let it = lsql.iterate`SELECT * FROM foo ORDER BY id ASC`;
|
|
it.next();
|
|
// eslint-disable-next-line no-unused-expressions
|
|
lsql.get`SELECT * FROM foo ORDER BY id ASC`;
|
|
assert.throws(() => { it.next(); }, {
|
|
code: 'ERR_INVALID_STATE',
|
|
message: /iterator was invalidated/,
|
|
});
|
|
|
|
// Invalidated by sql.all on the same tagged literal
|
|
it = lsql.iterate`SELECT * FROM foo ORDER BY id ASC`;
|
|
it.next();
|
|
// eslint-disable-next-line no-unused-expressions
|
|
lsql.all`SELECT * FROM foo ORDER BY id ASC`;
|
|
assert.throws(() => { it.next(); }, {
|
|
code: 'ERR_INVALID_STATE',
|
|
message: /iterator was invalidated/,
|
|
});
|
|
|
|
// Invalidated by sql.run on the same tagged literal
|
|
it = lsql.iterate`SELECT * FROM foo ORDER BY id ASC`;
|
|
it.next();
|
|
// eslint-disable-next-line no-unused-expressions
|
|
lsql.run`SELECT * FROM foo ORDER BY id ASC`;
|
|
assert.throws(() => { it.next(); }, {
|
|
code: 'ERR_INVALID_STATE',
|
|
message: /iterator was invalidated/,
|
|
});
|
|
|
|
// Invalidated by a new sql.iterate on the same tagged literal
|
|
it = lsql.iterate`SELECT * FROM foo ORDER BY id ASC`;
|
|
it.next();
|
|
const it2 = lsql.iterate`SELECT * FROM foo ORDER BY id ASC`;
|
|
assert.throws(() => { it.next(); }, {
|
|
code: 'ERR_INVALID_STATE',
|
|
message: /iterator was invalidated/,
|
|
});
|
|
|
|
// The fresh iterator still works.
|
|
assert.strictEqual(it2.next().done, false);
|
|
|
|
ldb.close();
|
|
});
|
|
|
|
test('a stale iterator cannot replay a victim-bound write', () => {
|
|
const bank = new Database(':memory:');
|
|
const tx = bank.createTagStore();
|
|
bank.exec(`
|
|
CREATE TABLE acct(user TEXT PRIMARY KEY, balance INTEGER);
|
|
CREATE TABLE transfers(id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
from_user TEXT, to_user TEXT, amount INTEGER);
|
|
CREATE TRIGGER debit AFTER INSERT ON transfers
|
|
BEGIN
|
|
UPDATE acct SET balance = balance - NEW.amount WHERE user = NEW.from_user;
|
|
UPDATE acct SET balance = balance + NEW.amount WHERE user = NEW.to_user;
|
|
END;
|
|
INSERT INTO acct VALUES ('victim', 1000);
|
|
INSERT INTO acct VALUES ('attacker', 0);
|
|
`);
|
|
|
|
// Attacker arms an iterator without stepping it. The static parts of this
|
|
// tagged literal must be byte-identical to the victim's below so they share
|
|
// the same cached statement.
|
|
const armed = tx.iterate`INSERT INTO transfers(from_user, to_user, amount) VALUES (${'attacker'}, ${'attacker'}, ${0}) RETURNING id`;
|
|
|
|
// Victim makes one legitimate transfer through the same tagged literal.
|
|
// eslint-disable-next-line no-unused-expressions
|
|
tx.get`INSERT INTO transfers(from_user, to_user, amount) VALUES (${'victim'}, ${'attacker'}, ${100}) RETURNING id`;
|
|
|
|
// The stale iterator must not be able to replay the victim's write.
|
|
assert.throws(() => { armed.next(); }, {
|
|
code: 'ERR_INVALID_STATE',
|
|
message: /iterator was invalidated/,
|
|
});
|
|
|
|
const balances = bank.prepare('SELECT user, balance FROM acct ORDER BY user').all();
|
|
assert.deepStrictEqual(balances, [
|
|
{ __proto__: null, user: 'attacker', balance: 100 },
|
|
{ __proto__: null, user: 'victim', balance: 900 },
|
|
]);
|
|
const count = bank.prepare('SELECT COUNT(*) AS c FROM transfers').get().c;
|
|
assert.strictEqual(count, 1);
|
|
|
|
bank.close();
|
|
});
|
|
|
|
test('a finished iterator stays done and does not restart', () => {
|
|
// eslint-disable-next-line no-unused-expressions
|
|
sql.run`INSERT INTO foo (text) VALUES (${'bob'})`;
|
|
|
|
const iter = sql.iterate`SELECT * FROM foo ORDER BY id ASC`;
|
|
assert.strictEqual(iter.next().done, false);
|
|
assert.strictEqual(iter.next().done, true);
|
|
// A subsequent next() must not restart the statement.
|
|
assert.strictEqual(iter.next().done, true);
|
|
assert.strictEqual(iter.next().done, true);
|
|
});
|
|
|
|
test('createTagStore throws on invalid maxSize', () => {
|
|
const db = new Database(':memory:');
|
|
|
|
assert.throws(() => db.createTagStore(0), {
|
|
code: 'ERR_OUT_OF_RANGE',
|
|
message: /maxSize/,
|
|
});
|
|
|
|
assert.throws(() => db.createTagStore(-1), {
|
|
code: 'ERR_OUT_OF_RANGE',
|
|
message: /maxSize/,
|
|
});
|
|
|
|
assert.throws(() => db.createTagStore(NaN), {
|
|
code: 'ERR_OUT_OF_RANGE',
|
|
message: /maxSize/,
|
|
});
|
|
|
|
assert.throws(() => db.createTagStore(1.5), {
|
|
code: 'ERR_OUT_OF_RANGE',
|
|
message: /maxSize/,
|
|
});
|
|
|
|
assert.throws(() => db.createTagStore('abc'), {
|
|
code: 'ERR_INVALID_ARG_TYPE',
|
|
message: /maxSize/,
|
|
});
|
|
|
|
assert.throws(() => db.createTagStore(Number.MAX_SAFE_INTEGER), {
|
|
code: 'ERR_OUT_OF_RANGE',
|
|
message: /maxSize/,
|
|
});
|
|
});
|
|
|
|
test('sql.db returns the associated Database instance', () => {
|
|
assert.strictEqual(sql.db, db);
|
|
});
|
|
|
|
test('sql error messages are descriptive', () => {
|
|
assert.strictEqual(sql.run`INSERT INTO foo (text) VALUES (${'test'})`.changes, 1);
|
|
|
|
// Test with non-existent column
|
|
assert.throws(() => {
|
|
const result = sql.get`SELECT nonexistent_column FROM foo`;
|
|
assert.fail(`Expected error, got: ${JSON.stringify(result)}`);
|
|
}, {
|
|
code: 'ERR_SQLITE_ERROR',
|
|
message: /no such column/i,
|
|
});
|
|
|
|
// Test with non-existent table
|
|
assert.throws(() => {
|
|
const result = sql.get`SELECT * FROM nonexistent_table`;
|
|
assert.fail(`Expected error, got: ${JSON.stringify(result)}`);
|
|
}, {
|
|
code: 'ERR_SQLITE_ERROR',
|
|
message: /no such table/i,
|
|
});
|
|
});
|
|
|
|
test('rejects SQL that contains no statements', () => {
|
|
const expectedError = {
|
|
code: 'ERR_INVALID_ARG_VALUE',
|
|
message: /contains no statements/,
|
|
};
|
|
|
|
for (const method of ['run', 'get', 'all', 'iterate']) {
|
|
assert.throws(() => {
|
|
// eslint-disable-next-line no-unused-expressions
|
|
sql[method]`-- comment`;
|
|
}, expectedError);
|
|
|
|
assert.throws(() => {
|
|
// eslint-disable-next-line no-unused-expressions
|
|
sql[method]``;
|
|
}, expectedError);
|
|
}
|
|
|
|
// A rejected statement must not be cached, so a later valid query with the
|
|
// same tag store still works.
|
|
assert.strictEqual(sql.run`INSERT INTO foo (text) VALUES (${'bob'})`.changes, 1);
|
|
});
|
|
|
|
test('a tag store keeps the database alive by itself', () => {
|
|
const sql = new Database(':memory:').createTagStore();
|
|
|
|
sql.db.exec('CREATE TABLE test (data INTEGER)');
|
|
|
|
global.gc();
|
|
|
|
// eslint-disable-next-line no-unused-expressions
|
|
sql.run`INSERT INTO test (data) VALUES (1)`;
|
|
});
|
|
|
|
test('tag store prevents circular reference leaks', async () => {
|
|
const { gcUntil } = require('../common/gc');
|
|
|
|
const before = process.memoryUsage().heapUsed;
|
|
|
|
// Create many SQLTagStore + Database pairs with circular references
|
|
for (let i = 0; i < 1000; i++) {
|
|
const sql = new Database(':memory:').createTagStore();
|
|
sql.db.exec('CREATE TABLE test (data INTEGER)');
|
|
// eslint-disable-next-line no-void
|
|
sql.db.setAuthorizer(() => void sql.db);
|
|
}
|
|
|
|
// GC until memory stabilizes or give up after 20 attempts
|
|
await gcUntil('tag store leak check', () => {
|
|
const after = process.memoryUsage().heapUsed;
|
|
// Memory should not grow significantly (allow 50% margin for noise)
|
|
return after < before * 1.5;
|
|
}, 20);
|
|
});
|
|
|
|
test('cached statements are finalized when the database is closed', () => {
|
|
const db = new Database(':memory:');
|
|
const sql = db.createTagStore();
|
|
|
|
db.exec('CREATE TABLE foo (id INTEGER PRIMARY KEY)');
|
|
db.exec('INSERT INTO foo (id) VALUES (1)');
|
|
assert.deepStrictEqual(sql.all`SELECT id FROM foo`, [{ __proto__: null, id: 1 }]);
|
|
|
|
db.close();
|
|
db.open();
|
|
|
|
assert.throws(() => sql.all`SELECT id FROM foo`, {
|
|
code: 'ERR_SQLITE_ERROR',
|
|
message: /no such table/i,
|
|
});
|
|
});
|