mirror of
https://github.com/nodejs/node.git
synced 2026-11-09 15:11:11 -05:00
DatabaseSync::DeleteSessions() deleted native sqlite3_session handles without clearing the pointers held by their Session wrappers. Reopening the database allowed stale session handles to be used, crashing the process. Track Session wrappers and delete sessions through Session::Delete() so the wrappers are marked closed. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: codex:gpt-5.6-sol PR-URL: https://github.com/nodejs/node/pull/64783 Fixes: https://github.com/nodejs/node/issues/64782 Reviewed-By: Aviv Keller <me@aviv.sh>
723 lines
23 KiB
JavaScript
723 lines
23 KiB
JavaScript
// Flags: --expose-gc --experimental-sqlite
|
|
'use strict';
|
|
const { skipIfSQLiteMissing } = require('../common');
|
|
skipIfSQLiteMissing();
|
|
const {
|
|
DatabaseSync,
|
|
constants,
|
|
} = require('node:sqlite');
|
|
const { test, suite } = require('node:test');
|
|
const { nextDb } = require('../sqlite/next-db.js');
|
|
const { Worker } = require('worker_threads');
|
|
const { once } = require('events');
|
|
|
|
/**
|
|
* Convenience wrapper around assert.deepStrictEqual that sets a null
|
|
* prototype to the expected object.
|
|
* @returns {boolean}
|
|
*/
|
|
function deepStrictEqual(t) {
|
|
return (actual, expected, message) => {
|
|
if (Array.isArray(expected)) {
|
|
expected = expected.map((obj) => ({ ...obj, __proto__: null }));
|
|
} else if (typeof expected === 'object') {
|
|
expected = { ...expected, __proto__: null };
|
|
}
|
|
t.assert.deepStrictEqual(actual, expected, message);
|
|
};
|
|
}
|
|
|
|
test('creating and applying a changeset', (t) => {
|
|
const createDataTableSql = `
|
|
CREATE TABLE data(
|
|
key INTEGER PRIMARY KEY,
|
|
value TEXT
|
|
) STRICT`;
|
|
|
|
const createDatabase = () => {
|
|
const database = new DatabaseSync(':memory:');
|
|
database.exec(createDataTableSql);
|
|
return database;
|
|
};
|
|
|
|
const databaseFrom = createDatabase();
|
|
const session = databaseFrom.createSession();
|
|
|
|
const select = 'SELECT * FROM data ORDER BY key';
|
|
|
|
const insert = databaseFrom.prepare('INSERT INTO data (key, value) VALUES (?, ?)');
|
|
insert.run(1, 'hello');
|
|
insert.run(2, 'world');
|
|
|
|
const databaseTo = createDatabase();
|
|
|
|
t.assert.strictEqual(databaseTo.applyChangeset(session.changeset()), true);
|
|
deepStrictEqual(t)(
|
|
databaseFrom.prepare(select).all(),
|
|
databaseTo.prepare(select).all()
|
|
);
|
|
});
|
|
|
|
test('database.createSession() - closed database results in exception', (t) => {
|
|
const database = new DatabaseSync(':memory:');
|
|
database.close();
|
|
t.assert.throws(() => {
|
|
database.createSession();
|
|
}, {
|
|
name: 'Error',
|
|
message: 'database is not open',
|
|
});
|
|
});
|
|
|
|
test('session.changeset() - closed database results in exception', (t) => {
|
|
const database = new DatabaseSync(':memory:');
|
|
const session = database.createSession();
|
|
database.close();
|
|
t.assert.throws(() => {
|
|
session.changeset();
|
|
}, {
|
|
name: 'Error',
|
|
message: 'database is not open',
|
|
});
|
|
});
|
|
|
|
test('session methods - reopened database results in exception', (t) => {
|
|
for (const method of ['changeset', 'close']) {
|
|
const database = new DatabaseSync(':memory:');
|
|
const session = database.createSession();
|
|
database.close();
|
|
database.open();
|
|
|
|
t.assert.throws(() => {
|
|
session[method]();
|
|
}, {
|
|
name: 'Error',
|
|
message: 'session is not open',
|
|
});
|
|
}
|
|
});
|
|
|
|
test('database.applyChangeset() - closed database results in exception', (t) => {
|
|
const database = new DatabaseSync(':memory:');
|
|
const session = database.createSession();
|
|
const changeset = session.changeset();
|
|
database.close();
|
|
t.assert.throws(() => {
|
|
database.applyChangeset(changeset);
|
|
}, {
|
|
name: 'Error',
|
|
message: 'database is not open',
|
|
});
|
|
});
|
|
|
|
test('database.createSession() - use table option to track specific table', (t) => {
|
|
const database1 = new DatabaseSync(':memory:');
|
|
const database2 = new DatabaseSync(':memory:');
|
|
|
|
const createData1TableSql = `CREATE TABLE data1 (
|
|
key INTEGER PRIMARY KEY,
|
|
value TEXT
|
|
) STRICT
|
|
`;
|
|
const createData2TableSql = `CREATE TABLE data2 (
|
|
key INTEGER PRIMARY KEY,
|
|
value TEXT
|
|
) STRICT
|
|
`;
|
|
database1.exec(createData1TableSql);
|
|
database1.exec(createData2TableSql);
|
|
database2.exec(createData1TableSql);
|
|
database2.exec(createData2TableSql);
|
|
|
|
const session = database1.createSession({
|
|
table: 'data1'
|
|
});
|
|
const insert1 = database1.prepare('INSERT INTO data1 (key, value) VALUES (?, ?)');
|
|
insert1.run(1, 'hello');
|
|
insert1.run(2, 'world');
|
|
const insert2 = database1.prepare('INSERT INTO data2 (key, value) VALUES (?, ?)');
|
|
insert2.run(1, 'hello');
|
|
insert2.run(2, 'world');
|
|
const select1 = 'SELECT * FROM data1 ORDER BY key';
|
|
const select2 = 'SELECT * FROM data2 ORDER BY key';
|
|
t.assert.strictEqual(database2.applyChangeset(session.changeset()), true);
|
|
deepStrictEqual(t)(
|
|
database1.prepare(select1).all(),
|
|
database2.prepare(select1).all()); // data1 table should be equal
|
|
deepStrictEqual(t)(database2.prepare(select2).all(), []); // data2 should be empty in database2
|
|
t.assert.strictEqual(database1.prepare(select2).all().length, 2); // data1 should have values in database1
|
|
});
|
|
|
|
suite('conflict resolution', () => {
|
|
const createDataTableSql = `CREATE TABLE data (
|
|
key INTEGER PRIMARY KEY,
|
|
value TEXT UNIQUE
|
|
) STRICT`;
|
|
|
|
const prepareConflict = () => {
|
|
const database1 = new DatabaseSync(':memory:');
|
|
const database2 = new DatabaseSync(':memory:');
|
|
|
|
database1.exec(createDataTableSql);
|
|
database2.exec(createDataTableSql);
|
|
|
|
const insertSql = 'INSERT INTO data (key, value) VALUES (?, ?)';
|
|
const session = database1.createSession();
|
|
database1.prepare(insertSql).run(1, 'hello');
|
|
database1.prepare(insertSql).run(2, 'foo');
|
|
database2.prepare(insertSql).run(1, 'world');
|
|
return {
|
|
database2,
|
|
changeset: session.changeset()
|
|
};
|
|
};
|
|
|
|
const prepareDataConflict = () => {
|
|
const database1 = new DatabaseSync(':memory:');
|
|
const database2 = new DatabaseSync(':memory:');
|
|
|
|
database1.exec(createDataTableSql);
|
|
database2.exec(createDataTableSql);
|
|
|
|
const insertSql = 'INSERT INTO data (key, value) VALUES (?, ?)';
|
|
database1.prepare(insertSql).run(1, 'hello');
|
|
database2.prepare(insertSql).run(1, 'othervalue');
|
|
const session = database1.createSession();
|
|
database1.prepare('UPDATE data SET value = ? WHERE key = ?').run('foo', 1);
|
|
return {
|
|
database2,
|
|
changeset: session.changeset()
|
|
};
|
|
};
|
|
|
|
const prepareNotFoundConflict = () => {
|
|
const database1 = new DatabaseSync(':memory:');
|
|
const database2 = new DatabaseSync(':memory:');
|
|
|
|
database1.exec(createDataTableSql);
|
|
database2.exec(createDataTableSql);
|
|
|
|
const insertSql = 'INSERT INTO data (key, value) VALUES (?, ?)';
|
|
database1.prepare(insertSql).run(1, 'hello');
|
|
const session = database1.createSession();
|
|
database1.prepare('DELETE FROM data WHERE key = 1').run();
|
|
return {
|
|
database2,
|
|
changeset: session.changeset()
|
|
};
|
|
};
|
|
|
|
const prepareFkConflict = () => {
|
|
const database1 = new DatabaseSync(':memory:');
|
|
const database2 = new DatabaseSync(':memory:');
|
|
|
|
database1.exec(createDataTableSql);
|
|
database2.exec(createDataTableSql);
|
|
const fkTableSql = `CREATE TABLE other (
|
|
key INTEGER PRIMARY KEY,
|
|
ref REFERENCES data(key)
|
|
)`;
|
|
database1.exec(fkTableSql);
|
|
database2.exec(fkTableSql);
|
|
|
|
const insertDataSql = 'INSERT INTO data (key, value) VALUES (?, ?)';
|
|
const insertOtherSql = 'INSERT INTO other (key, ref) VALUES (?, ?)';
|
|
database1.prepare(insertDataSql).run(1, 'hello');
|
|
database2.prepare(insertDataSql).run(1, 'hello');
|
|
database1.prepare(insertOtherSql).run(1, 1);
|
|
database2.prepare(insertOtherSql).run(1, 1);
|
|
|
|
database1.exec('DELETE FROM other WHERE key = 1'); // So we don't get a fk violation in database1
|
|
const session = database1.createSession();
|
|
database1.prepare('DELETE FROM data WHERE key = 1').run(); // Changeset with fk violation
|
|
database2.exec('PRAGMA foreign_keys = ON'); // Needs to be supported, otherwise will fail here
|
|
|
|
return {
|
|
database2,
|
|
changeset: session.changeset()
|
|
};
|
|
};
|
|
|
|
const prepareConstraintConflict = () => {
|
|
const database1 = new DatabaseSync(':memory:');
|
|
const database2 = new DatabaseSync(':memory:');
|
|
|
|
database1.exec(createDataTableSql);
|
|
database2.exec(createDataTableSql);
|
|
|
|
const insertSql = 'INSERT INTO data (key, value) VALUES (?, ?)';
|
|
const session = database1.createSession();
|
|
database1.prepare(insertSql).run(1, 'hello');
|
|
database2.prepare(insertSql).run(2, 'hello'); // database2 already contains hello
|
|
|
|
return {
|
|
database2,
|
|
changeset: session.changeset()
|
|
};
|
|
};
|
|
|
|
test('database.applyChangeset() - SQLITE_CHANGESET_CONFLICT conflict with default behavior (abort)', (t) => {
|
|
const { database2, changeset } = prepareConflict();
|
|
// When changeset is aborted due to a conflict, applyChangeset should return false
|
|
t.assert.strictEqual(database2.applyChangeset(changeset), false);
|
|
deepStrictEqual(t)(
|
|
database2.prepare('SELECT value from data').all(),
|
|
[{ value: 'world' }]); // unchanged
|
|
});
|
|
|
|
test('database.applyChangeset() - SQLITE_CHANGESET_CONFLICT conflict handled with SQLITE_CHANGESET_ABORT', (t) => {
|
|
const { database2, changeset } = prepareConflict();
|
|
let conflictType = null;
|
|
const result = database2.applyChangeset(changeset, {
|
|
onConflict: (conflictType_) => {
|
|
conflictType = conflictType_;
|
|
return constants.SQLITE_CHANGESET_ABORT;
|
|
}
|
|
});
|
|
// When changeset is aborted due to a conflict, applyChangeset should return false
|
|
t.assert.strictEqual(result, false);
|
|
t.assert.strictEqual(conflictType, constants.SQLITE_CHANGESET_CONFLICT);
|
|
deepStrictEqual(t)(
|
|
database2.prepare('SELECT value from data').all(),
|
|
[{ value: 'world' }]); // unchanged
|
|
});
|
|
|
|
test('database.applyChangeset() - SQLITE_CHANGESET_DATA conflict handled with SQLITE_CHANGESET_REPLACE', (t) => {
|
|
const { database2, changeset } = prepareDataConflict();
|
|
let conflictType = null;
|
|
const result = database2.applyChangeset(changeset, {
|
|
onConflict: (conflictType_) => {
|
|
conflictType = conflictType_;
|
|
return constants.SQLITE_CHANGESET_REPLACE;
|
|
}
|
|
});
|
|
// Not aborted due to conflict, so should return true
|
|
t.assert.strictEqual(result, true);
|
|
t.assert.strictEqual(conflictType, constants.SQLITE_CHANGESET_DATA);
|
|
deepStrictEqual(t)(
|
|
database2.prepare('SELECT value from data ORDER BY key').all(),
|
|
[{ value: 'foo' }]); // replaced
|
|
});
|
|
|
|
test('database.applyChangeset() - SQLITE_CHANGESET_NOTFOUND conflict with SQLITE_CHANGESET_OMIT', (t) => {
|
|
const { database2, changeset } = prepareNotFoundConflict();
|
|
let conflictType = null;
|
|
const result = database2.applyChangeset(changeset, {
|
|
onConflict: (conflictType_) => {
|
|
conflictType = conflictType_;
|
|
return constants.SQLITE_CHANGESET_OMIT;
|
|
}
|
|
});
|
|
// Not aborted due to conflict, so should return true
|
|
t.assert.strictEqual(result, true);
|
|
t.assert.strictEqual(conflictType, constants.SQLITE_CHANGESET_NOTFOUND);
|
|
deepStrictEqual(t)(database2.prepare('SELECT value from data').all(), []);
|
|
});
|
|
|
|
test('database.applyChangeset() - SQLITE_CHANGESET_FOREIGN_KEY conflict', (t) => {
|
|
const { database2, changeset } = prepareFkConflict();
|
|
let conflictType = null;
|
|
const result = database2.applyChangeset(changeset, {
|
|
onConflict: (conflictType_) => {
|
|
conflictType = conflictType_;
|
|
return constants.SQLITE_CHANGESET_OMIT;
|
|
}
|
|
});
|
|
// Not aborted due to conflict, so should return true
|
|
t.assert.strictEqual(result, true);
|
|
t.assert.strictEqual(conflictType, constants.SQLITE_CHANGESET_FOREIGN_KEY);
|
|
deepStrictEqual(t)(database2.prepare('SELECT value from data').all(), []);
|
|
});
|
|
|
|
test('database.applyChangeset() - SQLITE_CHANGESET_CONSTRAINT conflict', (t) => {
|
|
const { database2, changeset } = prepareConstraintConflict();
|
|
let conflictType = null;
|
|
const result = database2.applyChangeset(changeset, {
|
|
onConflict: (conflictType_) => {
|
|
conflictType = conflictType_;
|
|
return constants.SQLITE_CHANGESET_OMIT;
|
|
}
|
|
});
|
|
// Not aborted due to conflict, so should return true
|
|
t.assert.strictEqual(result, true);
|
|
t.assert.strictEqual(conflictType, constants.SQLITE_CHANGESET_CONSTRAINT);
|
|
deepStrictEqual(t)(database2.prepare('SELECT key, value from data').all(), [{ key: 2, value: 'hello' }]);
|
|
});
|
|
|
|
test('conflict resolution handler returns invalid value', (t) => {
|
|
const invalidHandlers = [
|
|
() => -1,
|
|
() => ({}),
|
|
() => null,
|
|
async () => constants.SQLITE_CHANGESET_ABORT,
|
|
];
|
|
|
|
for (const invalidHandler of invalidHandlers) {
|
|
const { database2, changeset } = prepareConflict();
|
|
t.assert.throws(() => {
|
|
database2.applyChangeset(changeset, {
|
|
onConflict: invalidHandler
|
|
});
|
|
}, {
|
|
name: 'Error',
|
|
message: 'bad parameter or other API misuse',
|
|
errcode: 21,
|
|
code: 'ERR_SQLITE_ERROR'
|
|
}, `Did not throw expected exception when using invalid onConflict handler: ${invalidHandler}`);
|
|
}
|
|
});
|
|
|
|
test('conflict resolution handler throws', (t) => {
|
|
const { database2, changeset } = prepareConflict();
|
|
t.assert.throws(() => {
|
|
database2.applyChangeset(changeset, {
|
|
onConflict: () => {
|
|
throw new Error('some error');
|
|
}
|
|
});
|
|
}, {
|
|
name: 'Error',
|
|
message: 'some error'
|
|
});
|
|
});
|
|
});
|
|
|
|
test('filter handler throws', (t) => {
|
|
const database1 = new DatabaseSync(':memory:');
|
|
const database2 = new DatabaseSync(':memory:');
|
|
const createTableSql = 'CREATE TABLE data1(key INTEGER PRIMARY KEY); CREATE TABLE data2(key INTEGER PRIMARY KEY);';
|
|
database1.exec(createTableSql);
|
|
database2.exec(createTableSql);
|
|
|
|
const session = database1.createSession();
|
|
|
|
database1.exec('INSERT INTO data1 (key) VALUES (1), (2), (3)');
|
|
database1.exec('INSERT INTO data2 (key) VALUES (1), (2), (3), (4), (5)');
|
|
|
|
t.assert.throws(() => {
|
|
database2.applyChangeset(session.changeset(), {
|
|
filter: (tableName) => {
|
|
throw new Error(`Error filtering table ${tableName}`);
|
|
}
|
|
});
|
|
}, {
|
|
name: 'Error',
|
|
message: 'Error filtering table data1'
|
|
});
|
|
});
|
|
|
|
test('database.createSession() - filter changes', (t) => {
|
|
const database1 = new DatabaseSync(':memory:');
|
|
const database2 = new DatabaseSync(':memory:');
|
|
const createTableSql = 'CREATE TABLE data1(key INTEGER PRIMARY KEY); CREATE TABLE data2(key INTEGER PRIMARY KEY);';
|
|
database1.exec(createTableSql);
|
|
database2.exec(createTableSql);
|
|
|
|
const session = database1.createSession();
|
|
|
|
database1.exec('INSERT INTO data1 (key) VALUES (1), (2), (3)');
|
|
database1.exec('INSERT INTO data2 (key) VALUES (1), (2), (3), (4), (5)');
|
|
|
|
database2.applyChangeset(session.changeset(), {
|
|
filter: (tableName) => tableName === 'data2'
|
|
});
|
|
|
|
const data1Rows = database2.prepare('SELECT * FROM data1').all();
|
|
const data2Rows = database2.prepare('SELECT * FROM data2').all();
|
|
|
|
// Expect no rows since all changes were filtered out
|
|
t.assert.strictEqual(data1Rows.length, 0);
|
|
// Expect 5 rows since these changes were not filtered out
|
|
t.assert.strictEqual(data2Rows.length, 5);
|
|
});
|
|
|
|
test('database.createSession() - specify other database', (t) => {
|
|
const database = new DatabaseSync(':memory:');
|
|
const session = database.createSession();
|
|
const sessionMain = database.createSession({
|
|
db: 'main'
|
|
});
|
|
const sessionTest = database.createSession({
|
|
db: 'test'
|
|
});
|
|
database.exec('CREATE TABLE data (key INTEGER PRIMARY KEY)');
|
|
database.exec('INSERT INTO data (key) VALUES (1)');
|
|
t.assert.notStrictEqual(session.changeset().length, 0);
|
|
t.assert.notStrictEqual(sessionMain.changeset().length, 0);
|
|
// Since this session is attached to a different database, its changeset should be empty
|
|
t.assert.strictEqual(sessionTest.changeset().length, 0);
|
|
});
|
|
|
|
test('database.createSession() - wrong arguments', (t) => {
|
|
const database = new DatabaseSync(':memory:');
|
|
t.assert.throws(() => {
|
|
database.createSession(null);
|
|
}, {
|
|
name: 'TypeError',
|
|
message: 'The "options" argument must be an object.'
|
|
});
|
|
|
|
t.assert.throws(() => {
|
|
database.createSession({
|
|
table: 123
|
|
});
|
|
}, {
|
|
name: 'TypeError',
|
|
message: 'The "options.table" argument must be a string.'
|
|
});
|
|
|
|
t.assert.throws(() => {
|
|
database.createSession({
|
|
db: 123
|
|
});
|
|
}, {
|
|
name: 'TypeError',
|
|
message: 'The "options.db" argument must be a string.'
|
|
});
|
|
});
|
|
|
|
test('database.applyChangeset() - wrong arguments', (t) => {
|
|
const database = new DatabaseSync(':memory:');
|
|
const session = database.createSession();
|
|
t.assert.throws(() => {
|
|
database.applyChangeset(null);
|
|
}, {
|
|
name: 'TypeError',
|
|
message: 'The "changeset" argument must be a Uint8Array.'
|
|
});
|
|
|
|
t.assert.throws(() => {
|
|
database.applyChangeset(session.changeset(), null);
|
|
}, {
|
|
name: 'TypeError',
|
|
message: 'The "options" argument must be an object.'
|
|
});
|
|
|
|
t.assert.throws(() => {
|
|
database.applyChangeset(session.changeset(), {
|
|
filter: null
|
|
}, null);
|
|
}, {
|
|
name: 'TypeError',
|
|
message: 'The "options.filter" argument must be a function.'
|
|
});
|
|
|
|
t.assert.throws(() => {
|
|
database.applyChangeset(session.changeset(), {
|
|
onConflict: null
|
|
}, null);
|
|
}, {
|
|
name: 'TypeError',
|
|
message: 'The "options.onConflict" argument must be a function.'
|
|
});
|
|
});
|
|
|
|
test('database.applyChangeset() - malformed changeset returns SQLITE_CORRUPT', {
|
|
skip: process.config.variables.node_shared_sqlite ?
|
|
'requires the bundled SQLite session fix' : false,
|
|
}, (t) => {
|
|
const database = new DatabaseSync(':memory:');
|
|
database.exec('CREATE TABLE t1(a INTEGER PRIMARY KEY, b, c, d)');
|
|
|
|
const changeset = Buffer.from(
|
|
'540401000000743100177e0072286565286565',
|
|
'hex');
|
|
|
|
t.assert.throws(() => {
|
|
database.applyChangeset(changeset);
|
|
}, {
|
|
name: 'Error',
|
|
message: 'database disk image is malformed',
|
|
errcode: 11,
|
|
code: 'ERR_SQLITE_ERROR',
|
|
});
|
|
});
|
|
|
|
test('session.patchset()', (t) => {
|
|
const database = new DatabaseSync(':memory:');
|
|
database.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, value TEXT)');
|
|
|
|
database.exec("INSERT INTO data VALUES ('1', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.')");
|
|
|
|
const session = database.createSession();
|
|
database.exec("UPDATE data SET value = 'hi' WHERE key = 1");
|
|
|
|
const patchset = session.patchset();
|
|
const changeset = session.changeset();
|
|
|
|
t.assert.ok(patchset instanceof Uint8Array);
|
|
t.assert.ok(changeset instanceof Uint8Array);
|
|
|
|
t.assert.deepStrictEqual(patchset, session.patchset());
|
|
t.assert.deepStrictEqual(changeset, session.changeset());
|
|
|
|
t.assert.ok(
|
|
patchset.length < changeset.length,
|
|
'expected patchset to be smaller than changeset');
|
|
});
|
|
|
|
test('session.close() - using session after close throws exception', (t) => {
|
|
const database = new DatabaseSync(':memory:');
|
|
database.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, value TEXT)');
|
|
|
|
database.exec("INSERT INTO data VALUES ('1', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.')");
|
|
|
|
const session = database.createSession();
|
|
database.exec("UPDATE data SET value = 'hi' WHERE key = 1");
|
|
session.close();
|
|
|
|
database.exec("UPDATE data SET value = 'world' WHERE key = 1");
|
|
t.assert.throws(() => {
|
|
session.changeset();
|
|
}, {
|
|
name: 'Error',
|
|
message: 'session is not open'
|
|
});
|
|
});
|
|
|
|
test('session.close() - after closing database throws exception', (t) => {
|
|
const database = new DatabaseSync(':memory:');
|
|
database.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, value TEXT)');
|
|
|
|
database.exec("INSERT INTO data VALUES ('1', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.')");
|
|
|
|
const session = database.createSession();
|
|
database.close();
|
|
|
|
t.assert.throws(() => {
|
|
session.close();
|
|
}, {
|
|
name: 'Error',
|
|
message: 'database is not open'
|
|
});
|
|
});
|
|
|
|
test('session.close() - closing twice', (t) => {
|
|
const database = new DatabaseSync(':memory:');
|
|
const session = database.createSession();
|
|
session.close();
|
|
|
|
t.assert.throws(() => {
|
|
session.close();
|
|
}, {
|
|
name: 'Error',
|
|
message: 'session is not open'
|
|
});
|
|
});
|
|
|
|
test('session - keeps its database alive after the db handle is dropped', async (t) => {
|
|
const { gcUntil, onGC } = require('../common/gc');
|
|
|
|
// The DatabaseSync handle is created in a nested scope and never referenced
|
|
// again, so the returned session is the only thing keeping it reachable.
|
|
let dbCollected = false;
|
|
const session = (() => {
|
|
const database = new DatabaseSync(':memory:');
|
|
database.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, value TEXT)');
|
|
onGC(database, { ongc: () => { dbCollected = true; } });
|
|
const s = database.createSession();
|
|
database.exec("INSERT INTO data VALUES (1, 'hello')");
|
|
return s;
|
|
})();
|
|
|
|
// The session must keep the database alive across GC. Previously it held
|
|
// only a weak reference, so the database could be collected and using the
|
|
// session afterwards dereferenced a dangling pointer and crashed.
|
|
await gcUntil('database is collected', () => dbCollected, 5).then(
|
|
() => { throw new Error('session did not keep its database alive'); },
|
|
() => {}, // Expected: the database is never collected, so gcUntil rejects.
|
|
);
|
|
t.assert.strictEqual(dbCollected, false);
|
|
|
|
// The database is still open and usable through the still-alive session.
|
|
const changeset = session.changeset();
|
|
t.assert.ok(changeset.byteLength > 0);
|
|
session.close();
|
|
});
|
|
|
|
test('session supports ERM', (t) => {
|
|
const database = new DatabaseSync(':memory:');
|
|
let afterDisposeSession;
|
|
{
|
|
using session = database.createSession();
|
|
afterDisposeSession = session;
|
|
const changeset = session.changeset();
|
|
t.assert.ok(changeset instanceof Uint8Array);
|
|
t.assert.strictEqual(changeset.length, 0);
|
|
}
|
|
t.assert.throws(() => afterDisposeSession.changeset(), {
|
|
message: /session is not open/,
|
|
});
|
|
});
|
|
|
|
test('concurrent applyChangeset with workers', async (t) => {
|
|
// Before adding this test, the callbacks were stored in static variables
|
|
// this could result in a crash
|
|
// this test is a regression test for that scenario
|
|
|
|
function modeToString(mode) {
|
|
if (mode === constants.SQLITE_CHANGESET_ABORT) return 'SQLITE_CHANGESET_ABORT';
|
|
if (mode === constants.SQLITE_CHANGESET_OMIT) return 'SQLITE_CHANGESET_OMIT';
|
|
}
|
|
|
|
const dbPath = nextDb();
|
|
const db1 = new DatabaseSync(dbPath);
|
|
const db2 = new DatabaseSync(':memory:');
|
|
const createTable = `
|
|
CREATE TABLE data(
|
|
key INTEGER PRIMARY KEY,
|
|
value TEXT
|
|
) STRICT`;
|
|
db1.exec(createTable);
|
|
db2.exec(createTable);
|
|
db1.prepare('INSERT INTO data (key, value) VALUES (?, ?)').run(1, 'hello');
|
|
db1.close();
|
|
const session = db2.createSession();
|
|
db2.prepare('INSERT INTO data (key, value) VALUES (?, ?)').run(1, 'world');
|
|
const changeset = session.changeset(); // Changeset with conflict (for db1)
|
|
|
|
const iterations = 10;
|
|
for (let i = 0; i < iterations; i++) {
|
|
const workers = [];
|
|
const expectedResults = new Map([
|
|
[constants.SQLITE_CHANGESET_ABORT, false],
|
|
[constants.SQLITE_CHANGESET_OMIT, true]]
|
|
);
|
|
|
|
// Launch two workers (abort and omit modes)
|
|
for (const mode of [constants.SQLITE_CHANGESET_ABORT, constants.SQLITE_CHANGESET_OMIT]) {
|
|
const worker = new Worker(`${__dirname}/../sqlite/worker.js`, {
|
|
workerData: {
|
|
dbPath,
|
|
changeset,
|
|
mode
|
|
},
|
|
});
|
|
workers.push(worker);
|
|
}
|
|
|
|
const results = await Promise.all(workers.map(async (worker) => {
|
|
const [message] = await once(worker, 'message');
|
|
return message;
|
|
}));
|
|
|
|
// Verify each result
|
|
for (const res of results) {
|
|
if (res.errorMessage) {
|
|
if (res.errcode === 5) { // SQLITE_BUSY
|
|
break; // ignore
|
|
}
|
|
t.assert.fail(`Worker error: ${res.error.message}`);
|
|
}
|
|
const expected = expectedResults.get(res.mode);
|
|
t.assert.strictEqual(
|
|
res.result,
|
|
expected,
|
|
`Iteration ${i}: Worker (${modeToString(res.mode)}) expected ${expected} but got ${res.result}`
|
|
);
|
|
}
|
|
|
|
workers.forEach((worker) => worker.terminate()); // Cleanup
|
|
}
|
|
});
|