Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: marcelomendoncasoares <6807059+marcelomendoncasoares@users.noreply.github.com>
395 lines
12 KiB
Dart
395 lines
12 KiB
Dart
import 'package:drift_offline_sync/src/database/triggers.dart';
|
|
import 'package:drift_offline_sync/src/migrator.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
import '../utils/database.dart';
|
|
import '../utils/user.dart';
|
|
|
|
void main() {
|
|
group('Given a SQLite3 database with a table', () {
|
|
late OfflineSyncMigrator migrator;
|
|
|
|
setUp(() {
|
|
migrator = database.createMigrator();
|
|
});
|
|
|
|
group('when generating trigger statements', () {
|
|
late List<String> triggers;
|
|
|
|
setUp(() {
|
|
final generator = Sqlite3OfflineSyncTriggers(migrator.crdtDb);
|
|
triggers = generator.generateCreateTriggerStatements(database.todosTable);
|
|
});
|
|
|
|
test('then one trigger is created for each operation.', () {
|
|
expect(triggers, hasLength(3));
|
|
expect(triggers, anyElement(contains('AFTER INSERT ON "todos"')));
|
|
expect(triggers, anyElement(contains('AFTER UPDATE ON "todos"')));
|
|
expect(triggers, anyElement(contains('AFTER DELETE ON "todos"')));
|
|
});
|
|
|
|
test('then the insert trigger statement is correct.', () {
|
|
final insertStatement = triggers.firstWhere(
|
|
(trigger) => trigger.contains('AFTER INSERT ON "todos"'),
|
|
);
|
|
|
|
expect(
|
|
insertStatement,
|
|
'''\
|
|
CREATE TRIGGER IF NOT EXISTS "__crdt__todos__insert"
|
|
AFTER INSERT ON "todos"
|
|
WHEN (
|
|
SELECT MAX("crdt_triggers_on")
|
|
FROM "__crdt_control"
|
|
WHERE ("user_id" = '${migrator.userId}')
|
|
AND ("node_id" = '${migrator.nodeId}')
|
|
) = TRUE
|
|
BEGIN
|
|
INSERT INTO "${Sqlite3OfflineSyncTriggers.hlcCanonicalViewName}" ("user_id")
|
|
VALUES ('${migrator.userId}');
|
|
|
|
INSERT INTO "__crdt_data" (
|
|
"user_id", "table_name", "column_name", "row_id", "hlc_timestamp", "raw_value"
|
|
)
|
|
SELECT
|
|
'${migrator.userId}' AS "user_id",
|
|
'todos' AS "table_name",
|
|
"column_name",
|
|
NEW."id" AS "row_id",
|
|
(
|
|
SELECT (printf('%015d', "last_timestamp") || '-' || printf('%04X', "counter") || '-' || '${migrator.nodeId}')
|
|
FROM "${migrator.crdtDb.sqlBuilder.hlcStateTableName}"
|
|
WHERE "user_id" = '${migrator.userId}'
|
|
) AS "hlc_timestamp",
|
|
raw_value
|
|
FROM (
|
|
SELECT
|
|
'id' AS "column_name",
|
|
NEW."id" AS "raw_value"
|
|
UNION ALL
|
|
SELECT
|
|
'title' AS "column_name",
|
|
NEW."title" AS "raw_value"
|
|
UNION ALL
|
|
SELECT
|
|
'content' AS "column_name",
|
|
NEW."content" AS "raw_value"
|
|
UNION ALL
|
|
SELECT
|
|
'target_date' AS "column_name",
|
|
NEW."target_date" AS "raw_value"
|
|
UNION ALL
|
|
SELECT
|
|
'category' AS "column_name",
|
|
NEW."category" AS "raw_value"
|
|
UNION ALL
|
|
SELECT
|
|
'status' AS "column_name",
|
|
NEW."status" AS "raw_value"
|
|
UNION ALL
|
|
SELECT
|
|
'__crdt_is_deleted' AS "column_name",
|
|
FALSE AS "raw_value"
|
|
);
|
|
END;''',
|
|
);
|
|
});
|
|
|
|
test('then the update trigger statement is correct.', () {
|
|
final updateStatement = triggers.firstWhere(
|
|
(trigger) => trigger.contains('AFTER UPDATE ON "todos"'),
|
|
);
|
|
|
|
expect(
|
|
updateStatement,
|
|
'''\
|
|
CREATE TRIGGER IF NOT EXISTS "__crdt__todos__update"
|
|
AFTER UPDATE ON "todos"
|
|
WHEN (
|
|
SELECT MAX("crdt_triggers_on")
|
|
FROM "__crdt_control"
|
|
WHERE ("user_id" = '${migrator.userId}')
|
|
AND ("node_id" = '${migrator.nodeId}')
|
|
) = TRUE
|
|
BEGIN
|
|
INSERT INTO "${Sqlite3OfflineSyncTriggers.hlcCanonicalViewName}" ("user_id")
|
|
VALUES ('${migrator.userId}');
|
|
|
|
INSERT INTO "__crdt_data" (
|
|
"user_id", "table_name", "column_name", "row_id", "hlc_timestamp", "raw_value"
|
|
)
|
|
SELECT
|
|
'${migrator.userId}' AS "user_id",
|
|
'todos' AS "table_name",
|
|
"column_name",
|
|
OLD."id" AS "row_id",
|
|
(
|
|
SELECT (printf('%015d', "last_timestamp") || '-' || printf('%04X', "counter") || '-' || '${migrator.nodeId}')
|
|
FROM "${migrator.crdtDb.sqlBuilder.hlcStateTableName}"
|
|
WHERE "user_id" = '${migrator.userId}'
|
|
) AS "hlc_timestamp",
|
|
raw_value
|
|
FROM (
|
|
SELECT
|
|
'id' AS "column_name",
|
|
NEW."id" AS "raw_value",
|
|
NEW."id" IS NOT OLD."id" AS "value_changed"
|
|
UNION ALL
|
|
SELECT
|
|
'title' AS "column_name",
|
|
NEW."title" AS "raw_value",
|
|
NEW."title" IS NOT OLD."title" AS "value_changed"
|
|
UNION ALL
|
|
SELECT
|
|
'content' AS "column_name",
|
|
NEW."content" AS "raw_value",
|
|
NEW."content" IS NOT OLD."content" AS "value_changed"
|
|
UNION ALL
|
|
SELECT
|
|
'target_date' AS "column_name",
|
|
NEW."target_date" AS "raw_value",
|
|
NEW."target_date" IS NOT OLD."target_date" AS "value_changed"
|
|
UNION ALL
|
|
SELECT
|
|
'category' AS "column_name",
|
|
NEW."category" AS "raw_value",
|
|
NEW."category" IS NOT OLD."category" AS "value_changed"
|
|
UNION ALL
|
|
SELECT
|
|
'status' AS "column_name",
|
|
NEW."status" AS "raw_value",
|
|
NEW."status" IS NOT OLD."status" AS "value_changed"
|
|
)
|
|
WHERE ("value_changed" = TRUE);
|
|
END;''',
|
|
);
|
|
});
|
|
|
|
test('then the delete trigger statement is correct.', () {
|
|
final deleteStatement = triggers.firstWhere(
|
|
(trigger) => trigger.contains('AFTER DELETE ON "todos"'),
|
|
);
|
|
|
|
expect(
|
|
deleteStatement,
|
|
'''\
|
|
CREATE TRIGGER IF NOT EXISTS "__crdt__todos__delete"
|
|
AFTER DELETE ON "todos"
|
|
WHEN (
|
|
SELECT MAX("crdt_triggers_on")
|
|
FROM "__crdt_control"
|
|
WHERE ("user_id" = '${migrator.userId}')
|
|
AND ("node_id" = '${migrator.nodeId}')
|
|
) = TRUE
|
|
BEGIN
|
|
INSERT INTO "${Sqlite3OfflineSyncTriggers.hlcCanonicalViewName}" ("user_id")
|
|
VALUES ('${migrator.userId}');
|
|
|
|
INSERT INTO "__crdt_data" (
|
|
"user_id", "table_name", "column_name", "row_id", "hlc_timestamp", "raw_value"
|
|
)
|
|
SELECT
|
|
'${migrator.userId}' AS "user_id",
|
|
'todos' AS "table_name",
|
|
"column_name",
|
|
OLD."id" AS "row_id",
|
|
(
|
|
SELECT (printf('%015d', "last_timestamp") || '-' || printf('%04X', "counter") || '-' || '${migrator.nodeId}')
|
|
FROM "${migrator.crdtDb.sqlBuilder.hlcStateTableName}"
|
|
WHERE "user_id" = '${migrator.userId}'
|
|
) AS "hlc_timestamp",
|
|
raw_value
|
|
FROM (
|
|
SELECT
|
|
'__crdt_is_deleted' AS "column_name",
|
|
TRUE AS "raw_value"
|
|
);
|
|
END;''',
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
test('Given a SQLite3 database with a table that has only primary key columns '
|
|
'when generating trigger statements '
|
|
'then the triggers are generated with success.', () {
|
|
final migrator = database.createMigrator();
|
|
final generator = Sqlite3OfflineSyncTriggers(migrator.crdtDb);
|
|
final triggers = generator.generateCreateTriggerStatements(database.pureDefaults);
|
|
|
|
expect(triggers, hasLength(3));
|
|
});
|
|
|
|
group(
|
|
'Given a SQLite3 database with a table that has multiple primary key columns',
|
|
() {
|
|
late OfflineSyncMigrator migrator;
|
|
|
|
setUp(() {
|
|
migrator = database.createMigrator();
|
|
});
|
|
|
|
group('when generating trigger statements', () {
|
|
late List<String> triggers;
|
|
|
|
setUp(() {
|
|
final generator = Sqlite3OfflineSyncTriggers(migrator.crdtDb);
|
|
triggers = generator.generateCreateTriggerStatements(database.sharedTodos);
|
|
});
|
|
|
|
test('then the triggers are generated with success.', () {
|
|
expect(triggers, hasLength(3));
|
|
});
|
|
|
|
group('then the insert trigger', () {
|
|
late final insertStatement = triggers.firstWhere(
|
|
(trigger) => trigger.contains('AFTER INSERT ON "shared_todos"'),
|
|
);
|
|
|
|
test(
|
|
'has the "row_id" as a "CONCAT_WS" of all primary key columns.',
|
|
() {
|
|
expect(
|
|
insertStatement,
|
|
contains("CONCAT_WS('||', NEW.\"todo\", NEW.\"user\") AS \"row_id\""),
|
|
);
|
|
},
|
|
);
|
|
|
|
test('has the primary key columns also present as individual entries.', () {
|
|
expect(insertStatement, contains("'todo' AS \"column_name\""));
|
|
expect(insertStatement, contains("'user' AS \"column_name\""));
|
|
});
|
|
});
|
|
|
|
group('then the update trigger', () {
|
|
late final updateStatement = triggers.firstWhere(
|
|
(trigger) => trigger.contains('AFTER UPDATE ON "shared_todos"'),
|
|
);
|
|
|
|
test('has the "row_id" as a "CONCAT_WS" of all primary key columns.', () {
|
|
expect(
|
|
updateStatement,
|
|
contains("CONCAT_WS('||', OLD.\"todo\", OLD.\"user\") AS \"row_id\""),
|
|
);
|
|
});
|
|
|
|
test('has the primary key columns also present as individual entries.', () {
|
|
expect(updateStatement, contains("'todo' AS \"column_name\""));
|
|
expect(updateStatement, contains("'user' AS \"column_name\""));
|
|
});
|
|
});
|
|
|
|
test(
|
|
'then the delete trigger has the "row_id" as a "CONCAT_WS" of all primary key columns.',
|
|
() {
|
|
final deleteStatement = triggers.firstWhere(
|
|
(trigger) => trigger.contains('AFTER DELETE ON "shared_todos"'),
|
|
);
|
|
|
|
expect(
|
|
deleteStatement,
|
|
contains("CONCAT_WS('||', OLD.\"todo\", OLD.\"user\") AS \"row_id\""),
|
|
);
|
|
},
|
|
);
|
|
});
|
|
},
|
|
);
|
|
|
|
group('Given a SQLite3 database with a table that has generated columns', () {
|
|
late OfflineSyncMigrator migrator;
|
|
|
|
setUp(() {
|
|
migrator = database.createMigrator();
|
|
});
|
|
|
|
group('when generating trigger statements', () {
|
|
late List<String> triggers;
|
|
|
|
setUp(() {
|
|
final generator = Sqlite3OfflineSyncTriggers(migrator.crdtDb);
|
|
triggers = generator.generateCreateTriggerStatements(database.categories);
|
|
|
|
// Pin the column name to avoid having the `isNot(contains())` tests passing
|
|
// with no real test if the column is removed or renamed.
|
|
expect(
|
|
database.categories.descriptionInUpperCase.$name,
|
|
'description_in_upper_case',
|
|
);
|
|
});
|
|
|
|
test('then the insert trigger excludes generated columns.', () {
|
|
final insertStatement = triggers.firstWhere(
|
|
(trigger) => trigger.contains('AFTER INSERT ON "categories"'),
|
|
);
|
|
|
|
expect(insertStatement, isNot(contains("'description_in_upper_case'")));
|
|
});
|
|
|
|
test('then the update trigger excludes generated columns.', () {
|
|
final updateStatement = triggers.firstWhere(
|
|
(trigger) => trigger.contains('AFTER UPDATE ON "categories"'),
|
|
);
|
|
|
|
expect(updateStatement, isNot(contains("'description_in_upper_case'")));
|
|
});
|
|
});
|
|
});
|
|
|
|
// This test assures that the above tests are for [ConflictingBias.deleteWins]
|
|
// and avoids the need for duplicating all tests.
|
|
test('Given a SQLite3 migrator with no specified conflicting bias '
|
|
'then it generates the triggers with the deleteWins conflicting bias.', () {
|
|
final migrator = database.createMigrator();
|
|
expect(migrator.conflictingBias, ConflictingBias.deleteWins);
|
|
});
|
|
|
|
group('Given a SQLite3 database with a table and updateWins conflicting bias', () {
|
|
late OfflineSyncMigrator migrator;
|
|
late String isDeletedColumnName;
|
|
|
|
setUp(() {
|
|
migrator = OfflineSyncMigrator(
|
|
database,
|
|
userId: testUserId,
|
|
nodeId: testNodeId,
|
|
conflictingBias: ConflictingBias.updateWins,
|
|
synchronizedTables: [
|
|
database.todosTable,
|
|
],
|
|
);
|
|
|
|
isDeletedColumnName = migrator.crdtDb.sqlBuilder.isDeletedColumnName;
|
|
});
|
|
|
|
group('when generating trigger statements', () {
|
|
late List<String> triggers;
|
|
|
|
setUp(() {
|
|
final generator = Sqlite3OfflineSyncTriggers(
|
|
migrator.crdtDb,
|
|
ConflictingBias.updateWins,
|
|
);
|
|
triggers = generator.generateCreateTriggerStatements(database.todosTable);
|
|
});
|
|
|
|
test('then the update trigger includes the deleted flag column.', () {
|
|
final updateStatement = triggers.firstWhere(
|
|
(trigger) => trigger.contains('AFTER UPDATE ON "todos"'),
|
|
);
|
|
|
|
expect(
|
|
updateStatement,
|
|
contains('''
|
|
UNION ALL
|
|
SELECT
|
|
'$isDeletedColumnName' AS "column_name",
|
|
FALSE AS "raw_value",
|
|
NEW."__crdt_is_deleted" IS NOT OLD."__crdt_is_deleted" AS "value_changed"
|
|
'''),
|
|
);
|
|
});
|
|
});
|
|
});
|
|
}
|