mirror of
https://github.com/nodejs/node.git
synced 2024-11-21 21:19:50 +01:00
746b17e1a5
PR-URL: https://github.com/nodejs/node/pull/54181 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
385 lines
12 KiB
JavaScript
385 lines
12 KiB
JavaScript
// Flags: --experimental-sqlite
|
|
'use strict';
|
|
require('../common');
|
|
const {
|
|
DatabaseSync,
|
|
SQLITE_CHANGESET_OMIT,
|
|
SQLITE_CHANGESET_REPLACE,
|
|
SQLITE_CHANGESET_ABORT
|
|
} = require('node:sqlite');
|
|
const { test, suite } = require('node:test');
|
|
|
|
/**
|
|
* Convenience wrapper around assert.deepStrictEqual that sets a null
|
|
* prototype to the expected object.
|
|
*/
|
|
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('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 prepareConflict = () => {
|
|
const database1 = new DatabaseSync(':memory:');
|
|
const database2 = new DatabaseSync(':memory:');
|
|
|
|
const createDataTableSql = `CREATE TABLE data (
|
|
key INTEGER PRIMARY KEY,
|
|
value TEXT
|
|
) STRICT
|
|
`;
|
|
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()
|
|
};
|
|
};
|
|
|
|
test('database.applyChangeset() - 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() - conflict with SQLITE_CHANGESET_ABORT', (t) => {
|
|
const { database2, changeset } = prepareConflict();
|
|
const result = database2.applyChangeset(changeset, {
|
|
onConflict: SQLITE_CHANGESET_ABORT
|
|
});
|
|
// When changeset is aborted due to a conflict, applyChangeset should return false
|
|
t.assert.strictEqual(result, false);
|
|
deepStrictEqual(t)(
|
|
database2.prepare('SELECT value from data').all(),
|
|
[{ value: 'world' }]); // unchanged
|
|
});
|
|
|
|
test('database.applyChangeset() - conflict with SQLITE_CHANGESET_REPLACE', (t) => {
|
|
const { database2, changeset } = prepareConflict();
|
|
const result = database2.applyChangeset(changeset, {
|
|
onConflict: SQLITE_CHANGESET_REPLACE
|
|
});
|
|
// Not aborted due to conflict, so should return true
|
|
t.assert.strictEqual(result, true);
|
|
deepStrictEqual(t)(
|
|
database2.prepare('SELECT value from data ORDER BY key').all(),
|
|
[{ value: 'hello' }, { value: 'foo' }]); // replaced
|
|
});
|
|
|
|
test('database.applyChangeset() - conflict with SQLITE_CHANGESET_OMIT', (t) => {
|
|
const { database2, changeset } = prepareConflict();
|
|
const result = database2.applyChangeset(changeset, {
|
|
onConflict: SQLITE_CHANGESET_OMIT
|
|
});
|
|
// Not aborted due to conflict, so should return true
|
|
t.assert.strictEqual(result, true);
|
|
deepStrictEqual(t)(
|
|
database2.prepare('SELECT value from data ORDER BY key ASC').all(),
|
|
[{ value: 'world' }, { value: 'foo' }]); // Conflicting change omitted
|
|
});
|
|
});
|
|
|
|
test('session related constants are defined', (t) => {
|
|
t.assert.strictEqual(SQLITE_CHANGESET_OMIT, 0);
|
|
t.assert.strictEqual(SQLITE_CHANGESET_REPLACE, 1);
|
|
t.assert.strictEqual(SQLITE_CHANGESET_ABORT, 2);
|
|
});
|
|
|
|
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 number.'
|
|
});
|
|
});
|
|
|
|
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'
|
|
});
|
|
});
|