Skip to content

Commit

Permalink
test: split out sqlite session test and fix null __proto__
Browse files Browse the repository at this point in the history
  • Loading branch information
louwers committed Nov 2, 2024
1 parent 763da58 commit 40837f2
Show file tree
Hide file tree
Showing 2 changed files with 368 additions and 350 deletions.
368 changes: 368 additions & 0 deletions test/parallel/test-sqlite-session.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,368 @@
// Flags: --experimental-sqlite
'use strict';
const {
DatabaseSync,
SQLITE_CHANGESET_OMIT,
SQLITE_CHANGESET_REPLACE,
SQLITE_CHANGESET_ABORT
} = require('node:sqlite');
const { test } = require('node:test');

/**
* Convenience wrapper around assert.deepStrictEqual that sets a null
* prototype to the expected object.
*/
function deepStrictEqual(t) {
return (actual, expected, message) => {
expected.__proto__ = null;
}
}

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
});

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);
console.log("----------------", database2.prepare('SELECT value from data').all());
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");

t.assert.ok(
session.patchset().length < session.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'
});
});
Loading

0 comments on commit 40837f2

Please sign in to comment.