mirror of
https://github.com/nodejs/node.git
synced 2026-11-09 15:11:11 -05:00
Signed-off-by: anshikakalpana <anshikajain196872@gmail.com> PR-URL: https://github.com/nodejs/node/pull/63792 Fixes: https://github.com/nodejs/node/issues/63791 Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
351 lines
12 KiB
JavaScript
351 lines
12 KiB
JavaScript
'use strict';
|
|
// Flags: --expose-gc
|
|
|
|
const { skipIfSQLiteMissing } = require('../common');
|
|
skipIfSQLiteMissing();
|
|
|
|
const assert = require('assert');
|
|
const { DatabaseSync } = require('node:sqlite');
|
|
const { test, beforeEach } = require('node:test');
|
|
|
|
const db = new DatabaseSync(':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 DatabaseSync(':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 DatabaseSync(':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 DatabaseSync(':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 DatabaseSync(':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 DatabaseSync(':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 DatabaseSync 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('a tag store keeps the database alive by itself', () => {
|
|
const sql = new DatabaseSync(':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 + DatabaseSync pairs with circular references
|
|
for (let i = 0; i < 1000; i++) {
|
|
const sql = new DatabaseSync(':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);
|
|
});
|