// Flags: --expose-gc
'use strict';
const { mustCall, skipIfSQLiteMissing } = require('../common');
skipIfSQLiteMissing();
const assert = require('node:assert');
const dc = require('node:diagnostics_channel');
const { Database } = require('node:sqlite');
const { suite, it } = require('node:test');
const { gcUntil } = require('../common/gc');
suite('sqlite.db.query diagnostics channel', () => {
it('subscriber receives SQL string for exec() statements', (t) => {
const calls = [];
using db = new Database(':memory:');
const handler = (msg) => calls.push(msg);
dc.subscribe('sqlite.db.query', handler);
t.after(() => dc.unsubscribe('sqlite.db.query', handler));
db.exec('CREATE TABLE t (x INTEGER)');
db.exec('INSERT INTO t VALUES (1)');
assert.strictEqual(calls.length, 2);
assert.strictEqual(calls[0].sql, 'CREATE TABLE t (x INTEGER)');
assert.strictEqual(calls[1].sql, 'INSERT INTO t VALUES (1)');
});
it('subscriber receives SQL string for prepared INSERT statements', (t) => {
let calls = [];
using db = new Database(':memory:');
const handler = (msg) => calls.push(msg);
dc.subscribe('sqlite.db.query', handler);
t.after(() => dc.unsubscribe('sqlite.db.query', handler));
db.exec('CREATE TABLE t (x INTEGER)');
calls = []; // reset after setup
using stmt = db.prepare('INSERT INTO t VALUES (?)');
stmt.run(42);
assert.strictEqual(calls.length, 1);
assert.strictEqual(calls[0].sql, 'INSERT INTO t VALUES (42.0)');
});
it('subscriber receives SQL string for prepared SELECT statements', (t) => {
let calls = [];
using db = new Database(':memory:');
const handler = (msg) => calls.push(msg);
dc.subscribe('sqlite.db.query', handler);
t.after(() => dc.unsubscribe('sqlite.db.query', handler));
db.exec('CREATE TABLE t (x INTEGER)');
db.exec('INSERT INTO t VALUES (1)');
calls = []; // reset after setup
using stmt = db.prepare('SELECT x FROM t WHERE x = ?');
stmt.get(1);
assert.strictEqual(calls.length, 1);
assert.strictEqual(calls[0].sql, 'SELECT x FROM t WHERE x = 1.0');
});
it('subscriber receives SQL string for prepared UPDATE statements', (t) => {
let calls = [];
using db = new Database(':memory:');
const handler = (msg) => calls.push(msg);
dc.subscribe('sqlite.db.query', handler);
t.after(() => dc.unsubscribe('sqlite.db.query', handler));
db.exec('CREATE TABLE t (x INTEGER)');
db.exec('INSERT INTO t VALUES (1)');
calls = []; // reset after setup
using stmt = db.prepare('UPDATE t SET x = ? WHERE x = ?');
stmt.run(2, 1);
assert.strictEqual(calls.length, 1);
assert.strictEqual(calls[0].sql, 'UPDATE t SET x = 2.0 WHERE x = 1.0');
});
it('subscriber receives SQL string for prepared DELETE statements', (t) => {
let calls = [];
using db = new Database(':memory:');
const handler = (msg) => calls.push(msg);
dc.subscribe('sqlite.db.query', handler);
t.after(() => dc.unsubscribe('sqlite.db.query', handler));
db.exec('CREATE TABLE t (x INTEGER)');
db.exec('INSERT INTO t VALUES (1)');
calls = []; // reset after setup
using stmt = db.prepare('DELETE FROM t WHERE x = ?');
stmt.run(1);
assert.strictEqual(calls.length, 1);
assert.strictEqual(calls[0].sql, 'DELETE FROM t WHERE x = 1.0');
});
it('no calls received after unsubscribe', (t) => {
const calls = [];
using db = new Database(':memory:');
const handler = (msg) => calls.push(msg);
dc.subscribe('sqlite.db.query', handler);
db.exec('CREATE TABLE t (x INTEGER)');
assert.strictEqual(calls.length, 1);
dc.unsubscribe('sqlite.db.query', handler);
db.exec('INSERT INTO t VALUES (1)');
assert.strictEqual(calls.length, 1); // No new calls after unsubscribe
});
it('falls back to source SQL when expansion fails', (t) => {
let calls = [];
using db = new Database(':memory:', { limits: { length: 1000 } });
const handler = (msg) => calls.push(msg);
dc.subscribe('sqlite.db.query', handler);
t.after(() => dc.unsubscribe('sqlite.db.query', handler));
db.exec('CREATE TABLE t (x TEXT)');
calls = []; // reset after setup
using stmt = db.prepare('INSERT INTO t VALUES (?)');
const longValue = 'a'.repeat(977);
stmt.run(longValue);
assert.strictEqual(calls.length, 1);
// Falls back to source SQL with unexpanded '?' placeholder
assert.strictEqual(calls[0].sql, 'INSERT INTO t VALUES (?)');
});
it('database property identifies the correct database', (t) => {
const calls = [];
using db1 = new Database(':memory:');
using db2 = new Database(':memory:');
const handler = (msg) => calls.push(msg);
dc.subscribe('sqlite.db.query', handler);
t.after(() => dc.unsubscribe('sqlite.db.query', handler));
db1.exec('CREATE TABLE t (x INTEGER)');
db2.exec('CREATE TABLE t (x INTEGER)');
assert.strictEqual(calls.length, 2);
assert.strictEqual(calls[0].database, db1);
assert.strictEqual(calls[1].database, db2);
assert.notStrictEqual(calls[0].database, calls[1].database);
});
it('duration is a number', (t) => {
const calls = [];
using db = new Database(':memory:');
const handler = (msg) => calls.push(msg);
dc.subscribe('sqlite.db.query', handler);
t.after(() => dc.unsubscribe('sqlite.db.query', handler));
db.exec('CREATE TABLE t (x INTEGER)');
assert.strictEqual(calls.length, 1);
assert.strictEqual(typeof calls[0].duration, 'number');
});
it('duration is non-negative', (t) => {
const calls = [];
using db = new Database(':memory:');
const handler = (msg) => calls.push(msg);
dc.subscribe('sqlite.db.query', handler);
t.after(() => dc.unsubscribe('sqlite.db.query', handler));
db.exec('CREATE TABLE t (x INTEGER)');
assert.strictEqual(calls.length, 1);
assert.ok(calls[0].duration >= 0);
});
it('does not publish when an unfinished statement is collected', async (t) => {
let calls = 0;
const handler = () => calls++;
dc.subscribe('sqlite.db.query', handler);
t.after(() => dc.unsubscribe('sqlite.db.query', handler));
let collected = false;
const registry = new FinalizationRegistry(() => { collected = true; });
(() => {
const db = new Database(':memory:');
db.exec('CREATE TABLE t (x INTEGER)');
for (let i = 0; i < 10; i++) {
db.exec(`INSERT INTO t VALUES (${i})`);
}
const stmt = db.prepare('SELECT x FROM t');
registry.register(stmt);
stmt.iterate().next(); // Leave the statement unfinished.
})();
calls = 0; // reset after setup
await gcUntil('unfinished statement is collected', () => collected);
assert.strictEqual(calls, 0);
});
it('subscriber cannot close the database or statement', (t) => {
using db = new Database(':memory:');
db.exec('CREATE TABLE t (x INTEGER)');
using stmt = db.prepare('INSERT INTO t VALUES (?)');
const handler = mustCall(() => {
assert.throws(() => db.close(), { code: 'ERR_INVALID_STATE' });
assert.throws(() => stmt.close(), { code: 'ERR_INVALID_STATE' });
assert.throws(() => stmt[Symbol.dispose](), {
code: 'ERR_INVALID_STATE',
});
});
dc.subscribe('sqlite.db.query', handler);
t.after(() => dc.unsubscribe('sqlite.db.query', handler));
stmt.run(1);
dc.unsubscribe('sqlite.db.query', handler);
assert.deepStrictEqual(db.prepare('SELECT x FROM t').all(), [
{ __proto__: null, x: 1 },
]);
});
});