feat: Basic implementation of the OfflineSyncCrdt methods on SQLite3
This commit is contained in:
parent
f74f94edea
commit
f078e66a51
6 changed files with 927 additions and 100 deletions
72
test/crdt/crdt_canonical_time_test.dart
Normal file
72
test/crdt/crdt_canonical_time_test.dart
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import 'package:drift/drift.dart' hide isNotNull, isNull;
|
||||
import 'package:drift_offline_first/drift_offline_first.dart';
|
||||
import 'package:drift_offline_first/src/hlc/stateful.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../utils/crdt_context.dart';
|
||||
import '../utils/database.dart';
|
||||
|
||||
void main() {
|
||||
group('Given CRDT for an empty database', () {
|
||||
late OfflineSyncCrdt crdt;
|
||||
late String nodeId;
|
||||
|
||||
setUp(() async {
|
||||
(crdt, _, nodeId) = database.crdtContext;
|
||||
});
|
||||
|
||||
test(
|
||||
'when getting the canonical time '
|
||||
'then it returns Hlc.zero for the nodeId.', () async {
|
||||
expect(crdt.canonicalTime, equals(Hlc.zero(nodeId)));
|
||||
});
|
||||
});
|
||||
|
||||
group('Given CRDT for a database with data', () {
|
||||
late OfflineSyncCrdt crdt;
|
||||
late CrdtDatabase crdtDb;
|
||||
late Hlc createdHlc;
|
||||
|
||||
setUp(() async {
|
||||
await database.managers.todosTable.create(
|
||||
(t) => t(content: 'test', targetDate: Value(DateTime.now())),
|
||||
);
|
||||
|
||||
(crdt, crdtDb, _) = database.crdtContext;
|
||||
|
||||
final allCrdtDataEntries = await crdtDb.managers.crdtDataTable.get();
|
||||
createdHlc = allCrdtDataEntries.first.hlcTimestamp;
|
||||
});
|
||||
|
||||
test(
|
||||
'when getting the canonical time '
|
||||
'then it returns the HLC timestamp of the created data.', () async {
|
||||
expect(crdt.canonicalTime, equals(createdHlc));
|
||||
});
|
||||
});
|
||||
|
||||
group('Given CRDT for a database with data and a new data entry', () {
|
||||
late OfflineSyncCrdt crdt;
|
||||
|
||||
setUp(() async {
|
||||
await database.managers.todosTable.create(
|
||||
(t) => t(content: 'test', targetDate: Value(DateTime.now())),
|
||||
);
|
||||
|
||||
(crdt, _, _) = database.crdtContext;
|
||||
|
||||
await database.managers.todosTable.create(
|
||||
(t) => t(content: 'test2', targetDate: Value(DateTime.now())),
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'when getting the canonical time '
|
||||
'then the canonical time matches the cached HLC for the nodeId.', () async {
|
||||
expect(
|
||||
crdt.canonicalTime,
|
||||
equals(StatefulHlc.cached(crdt.userId, crdt.nodeId).lastHlc),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
76
test/crdt/crdt_database_extract_test.dart
Normal file
76
test/crdt/crdt_database_extract_test.dart
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import 'package:drift/drift.dart' hide isNotNull, isNull;
|
||||
import 'package:drift_offline_first/drift_offline_first.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../utils/crdt_context.dart';
|
||||
import '../utils/database.dart';
|
||||
import '../utils/tables.dart';
|
||||
|
||||
void main() {
|
||||
group('Given CRDT database for a database with data with every column type', () {
|
||||
late CrdtDatabase crdtDb;
|
||||
late String rowId;
|
||||
late TableWithEveryColumnTypeData createdData;
|
||||
|
||||
setUp(() async {
|
||||
final createdRowId = await database.managers.tableWithEveryColumnType.create(
|
||||
(t) => t(
|
||||
aBool: const Value(true),
|
||||
aDateTime: Value(DateTime.now()),
|
||||
aText: const Value('test'),
|
||||
anInt: const Value(1),
|
||||
anInt64: Value(BigInt.one),
|
||||
aReal: const Value(1.0),
|
||||
aBlob: Value(Uint8List.fromList([1, 2, 3])),
|
||||
anIntEnum: const Value(TodoStatus.open),
|
||||
aTextWithConverter: Value(MyCustomObject('test')),
|
||||
aUuid: Value(UuidValue.fromString('123e4567-e89b-12d3-a456-426614174000')),
|
||||
),
|
||||
);
|
||||
|
||||
(_, crdtDb, _) = database.crdtContext;
|
||||
rowId = createdRowId.toString();
|
||||
createdData = await database.managers.tableWithEveryColumnType.getSingle();
|
||||
});
|
||||
|
||||
test(
|
||||
'when getting the data from the CRDT data table of the CRDT database '
|
||||
'then it rebuilds the data perfectly.', () async {
|
||||
final data = await crdtDb.getSingleFromCrdtData<TableWithEveryColumnType,
|
||||
TableWithEveryColumnTypeData>(rowId);
|
||||
|
||||
expect(data, equals(createdData));
|
||||
});
|
||||
|
||||
test(
|
||||
'when trying to get the data from the CRDT data table with a non-existent row id '
|
||||
'then it returns null.', () async {
|
||||
final data = await crdtDb.getSingleFromCrdtData<TableWithEveryColumnType,
|
||||
TableWithEveryColumnTypeData>('non-existent-row-id');
|
||||
|
||||
expect(data, isNull);
|
||||
});
|
||||
|
||||
test(
|
||||
'when trying to get the data from the CRDT data table with multiple row ids '
|
||||
'then it returns the data for the existing row ids.', () async {
|
||||
final data = await crdtDb
|
||||
.getFromCrdtData<TableWithEveryColumnType, TableWithEveryColumnTypeData>(
|
||||
[rowId, 'non-existent-row-id'],
|
||||
);
|
||||
|
||||
expect(data, equals([createdData]));
|
||||
});
|
||||
|
||||
test(
|
||||
'when trying to get the data from the CRDT data table for a non-synchronized table '
|
||||
'then it throws an error.', () async {
|
||||
expect(
|
||||
() => crdtDb.getFromCrdtData<TableWithoutPK, TableWithEveryColumnTypeData>(
|
||||
[rowId],
|
||||
),
|
||||
throwsA(isA<ArgumentError>()),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
142
test/crdt/crdt_get_changeset_test.dart
Normal file
142
test/crdt/crdt_get_changeset_test.dart
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
import 'package:drift/drift.dart' hide isNotNull, isNull;
|
||||
import 'package:drift_offline_first/drift_offline_first.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../utils/crdt_context.dart';
|
||||
import '../utils/database.dart';
|
||||
|
||||
void main() {
|
||||
group('Given CRDT for an empty database', () {
|
||||
late OfflineSyncCrdt crdt;
|
||||
|
||||
setUp(() async {
|
||||
(crdt, _, _) = database.crdtContext;
|
||||
});
|
||||
|
||||
test(
|
||||
'when getting a changeset '
|
||||
'then it returns an empty changeset.', () async {
|
||||
final changeset = await crdt.getChangeset();
|
||||
|
||||
expect(changeset, isEmpty);
|
||||
});
|
||||
|
||||
test(
|
||||
'when adding data to the database '
|
||||
'then the changeset contains the data.', () async {
|
||||
await database.managers.todosTable.create(
|
||||
(t) => t(content: 'test'),
|
||||
);
|
||||
|
||||
final changeset = await crdt.getChangeset();
|
||||
expect(changeset, isNotEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
group('Given CRDT for a database with data', () {
|
||||
late OfflineSyncCrdt crdt;
|
||||
late CrdtDatabase crdtDb;
|
||||
late String nodeId;
|
||||
late String rowId;
|
||||
late Hlc createdHlc;
|
||||
late TodoEntry createdData;
|
||||
|
||||
setUp(() async {
|
||||
final createdRowId = await database.managers.todosTable.create(
|
||||
(t) => t(content: 'test', targetDate: Value(DateTime.now())),
|
||||
);
|
||||
|
||||
(crdt, crdtDb, nodeId) = database.crdtContext;
|
||||
|
||||
final allCrdtDataEntries = await crdtDb.managers.crdtDataTable.get();
|
||||
createdHlc = allCrdtDataEntries.first.hlcTimestamp;
|
||||
rowId = createdRowId.toString();
|
||||
createdData = await database.managers.todosTable.getSingle();
|
||||
});
|
||||
|
||||
group('when getting a changeset', () {
|
||||
group('without any filters', () {
|
||||
test(
|
||||
'then it returns a changeset with one entry per column of the created data.',
|
||||
() async {
|
||||
final changeset = await crdt.getChangeset();
|
||||
final changesetColumns = changeset.map((e) => e.columnName).toSet();
|
||||
final expectedColumns = database.todosTable.$columns.map((c) => c.$name);
|
||||
|
||||
expect(changeset.length, expectedColumns.length);
|
||||
expect(changesetColumns, equals(expectedColumns));
|
||||
});
|
||||
|
||||
test('then the raw values are set correctly.', () async {
|
||||
final changeset = await crdt.getChangeset();
|
||||
final rebuiltData = database.todosTable.map({
|
||||
for (final c in database.todosTable.$columns)
|
||||
c.$name: changeset
|
||||
.firstWhere((e) => e.rowId == rowId && e.columnName == c.$name)
|
||||
.rawValue
|
||||
?.rawSqlValue,
|
||||
});
|
||||
|
||||
expect(rebuiltData, equals(createdData));
|
||||
});
|
||||
});
|
||||
|
||||
test(
|
||||
'with onlyNodeId set to the nodeId '
|
||||
'then it returns a changeset with the data for the nodeId.', () async {
|
||||
final changeset = await crdt.getChangeset(onlyNodeId: nodeId);
|
||||
final nodeIds = changeset.map((e) => e.hlcTimestamp.nodeId).toSet();
|
||||
|
||||
expect(changeset, isNotEmpty);
|
||||
expect(nodeIds, equals({nodeId}));
|
||||
});
|
||||
|
||||
test(
|
||||
'with onlyNodeId set to a different nodeId '
|
||||
'then it returns an empty changeset.', () async {
|
||||
final changeset = await crdt.getChangeset(onlyNodeId: 'wrong-node');
|
||||
|
||||
expect(changeset, isEmpty);
|
||||
});
|
||||
|
||||
test(
|
||||
'with exceptNodeId set to the nodeId '
|
||||
'then it returns an empty changeset.', () async {
|
||||
final changeset = await crdt.getChangeset(exceptNodeId: nodeId);
|
||||
|
||||
expect(changeset, isEmpty);
|
||||
});
|
||||
|
||||
test(
|
||||
'with exceptNodeId set to a different nodeId '
|
||||
'then it returns a changeset with data from other nodes.', () async {
|
||||
const otherNodeId = 'other-node';
|
||||
final changeset = await crdt.getChangeset(exceptNodeId: otherNodeId);
|
||||
final nodeIds = changeset.map((e) => e.hlcTimestamp.nodeId).toSet();
|
||||
|
||||
expect(changeset, isNotEmpty);
|
||||
expect(nodeIds, isNot(contains(otherNodeId)));
|
||||
});
|
||||
|
||||
test(
|
||||
'with modifiedAfter set to a timestamp before the created data '
|
||||
'then it returns a changeset with the entries from the created data.',
|
||||
() async {
|
||||
final earlierHlc = Hlc.zero(nodeId);
|
||||
final changeset = await crdt.getChangeset(modifiedAfter: earlierHlc);
|
||||
|
||||
expect(changeset, isNotEmpty);
|
||||
expect(changeset.map((e) => e.rowId).toSet(), equals({rowId}));
|
||||
});
|
||||
|
||||
test(
|
||||
'with modifiedAfter set to a timestamp after the created data '
|
||||
'then it returns an empty changeset.', () async {
|
||||
final futureHlc = createdHlc.increment();
|
||||
final changeset = await crdt.getChangeset(modifiedAfter: futureHlc);
|
||||
|
||||
expect(changeset, isEmpty);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
78
test/crdt/crdt_last_modified_timestamp_test.dart
Normal file
78
test/crdt/crdt_last_modified_timestamp_test.dart
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import 'package:drift/drift.dart' hide isNotNull, isNull;
|
||||
import 'package:drift_offline_first/drift_offline_first.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../utils/crdt_context.dart';
|
||||
import '../utils/database.dart';
|
||||
|
||||
void main() {
|
||||
group('Given CRDT for an empty database', () {
|
||||
late OfflineSyncCrdt crdt;
|
||||
|
||||
setUp(() async {
|
||||
(crdt, _, _) = database.crdtContext;
|
||||
});
|
||||
|
||||
test(
|
||||
'when getting the last modified timestamp '
|
||||
'then it returns null since the database is empty.', () async {
|
||||
final lastModified = await crdt.getLastModified();
|
||||
expect(lastModified, isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('Given CRDT for a database with data', () {
|
||||
late OfflineSyncCrdt crdt;
|
||||
late CrdtDatabase crdtDb;
|
||||
late String nodeId;
|
||||
late Hlc createdHlc;
|
||||
|
||||
setUp(() async {
|
||||
await database.managers.todosTable.create(
|
||||
(t) => t(content: 'test', targetDate: Value(DateTime.now())),
|
||||
);
|
||||
|
||||
(crdt, crdtDb, nodeId) = database.crdtContext;
|
||||
|
||||
final allCrdtDataEntries = await crdtDb.managers.crdtDataTable.get();
|
||||
createdHlc = allCrdtDataEntries.first.hlcTimestamp;
|
||||
});
|
||||
|
||||
group('when getting the last modified timestamp', () {
|
||||
test(
|
||||
'without any filters '
|
||||
'then it returns the HLC timestamp of the created data.', () async {
|
||||
final lastModified = await crdt.getLastModified();
|
||||
expect(lastModified, equals(createdHlc));
|
||||
});
|
||||
|
||||
test(
|
||||
'with onlyNodeId set to the nodeId '
|
||||
'then it returns the HLC timestamp of the created data.', () async {
|
||||
final lastModified = await crdt.getLastModified(onlyNodeId: nodeId);
|
||||
expect(lastModified, equals(createdHlc));
|
||||
});
|
||||
|
||||
test(
|
||||
'with onlyNodeId set to a different nodeId '
|
||||
'then it returns null.', () async {
|
||||
final lastModified = await crdt.getLastModified(onlyNodeId: 'other-node');
|
||||
expect(lastModified, isNull);
|
||||
});
|
||||
|
||||
test(
|
||||
'with exceptNodeId set to the nodeId '
|
||||
'then it returns null.', () async {
|
||||
final lastModified = await crdt.getLastModified(exceptNodeId: nodeId);
|
||||
expect(lastModified, isNull);
|
||||
});
|
||||
|
||||
test(
|
||||
'with exceptNodeId set to a different nodeId '
|
||||
'then it returns the HLC timestamp of the created data.', () async {
|
||||
final lastModified = await crdt.getLastModified(exceptNodeId: 'other-node');
|
||||
expect(lastModified, equals(createdHlc));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
126
test/crdt/crdt_merge_changeset_test.dart
Normal file
126
test/crdt/crdt_merge_changeset_test.dart
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
import 'package:drift/drift.dart' hide isNotNull, isNull;
|
||||
import 'package:drift_offline_first/drift_offline_first.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../utils/crdt_context.dart';
|
||||
import '../utils/database.dart';
|
||||
import '../utils/tables.dart';
|
||||
|
||||
void main() {
|
||||
late OfflineSyncCrdt crdt;
|
||||
late CrdtDatabase crdtDb;
|
||||
late String nodeId;
|
||||
|
||||
group('Given CRDT for an empty database and a pending changeset', () {
|
||||
late Iterable<CrdtDataEntry> changeset;
|
||||
|
||||
setUp(() async {
|
||||
(crdt, crdtDb, nodeId) = database.crdtContext;
|
||||
|
||||
final todo = TodoEntry(
|
||||
id: const RowId(1),
|
||||
content: 'test',
|
||||
targetDate: DateTime.now(),
|
||||
);
|
||||
|
||||
changeset = todo.toCrdtDataEntry(nodeId);
|
||||
});
|
||||
|
||||
test(
|
||||
'when merging a changeset '
|
||||
'then the changeset is saved to the database.', () async {
|
||||
await crdt.merge(changeset);
|
||||
|
||||
expect(await crdtDb.managers.crdtDataTable.get(), changeset);
|
||||
});
|
||||
});
|
||||
|
||||
group(
|
||||
'Given CRDT for a database with data and a pending changeset with conflicting data and older HLC',
|
||||
() {
|
||||
late Iterable<CrdtDataEntry> firstChangeset;
|
||||
late Iterable<CrdtDataEntry> lateOldChangeset;
|
||||
|
||||
setUp(() async {
|
||||
(crdt, crdtDb, nodeId) = database.crdtContext;
|
||||
|
||||
final todo = TodoEntry(
|
||||
id: const RowId(1),
|
||||
content: 'test',
|
||||
targetDate: DateTime.now(),
|
||||
);
|
||||
|
||||
firstChangeset = todo.toCrdtDataEntry(nodeId);
|
||||
await crdt.merge(firstChangeset);
|
||||
|
||||
lateOldChangeset = todo.toCrdtDataEntry(nodeId, hlc: Hlc.zero(nodeId));
|
||||
});
|
||||
|
||||
test(
|
||||
'when merging a changeset with older HLC '
|
||||
'then no changes are saved to the database.', () async {
|
||||
await crdt.merge(lateOldChangeset);
|
||||
|
||||
expect(await crdtDb.managers.crdtDataTable.get(), firstChangeset);
|
||||
});
|
||||
});
|
||||
|
||||
group(
|
||||
'Given CRDT for a database with data and a pending changeset with conflicting data and some entries with newer HLC',
|
||||
() {
|
||||
late Iterable<CrdtDataEntry> firstChangeset;
|
||||
late Iterable<CrdtDataEntry> newerChangeset;
|
||||
|
||||
setUp(() async {
|
||||
(crdt, crdtDb, nodeId) = database.crdtContext;
|
||||
|
||||
final todo = TodoEntry(
|
||||
id: const RowId(1),
|
||||
content: 'test',
|
||||
targetDate: DateTime.now(),
|
||||
);
|
||||
|
||||
firstChangeset = todo.toCrdtDataEntry(nodeId);
|
||||
await crdt.merge(firstChangeset);
|
||||
|
||||
newerChangeset =
|
||||
todo.toCrdtDataEntry(nodeId).where((e) => e.columnName == 'content');
|
||||
});
|
||||
|
||||
test(
|
||||
'when merging a changeset with some entries with newer HLC '
|
||||
'then the entries with newer HLC are saved to the database.', () async {
|
||||
await crdt.merge(newerChangeset);
|
||||
|
||||
final expectedResultingChangeset = [
|
||||
for (final e in firstChangeset)
|
||||
if (e.columnName == 'content') newerChangeset.first else e,
|
||||
];
|
||||
|
||||
expect(await crdtDb.managers.crdtDataTable.get(), expectedResultingChangeset);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
extension on TodoEntry {
|
||||
Iterable<CrdtDataEntry> toCrdtDataEntry(String nodeId, {Hlc? hlc}) {
|
||||
final hlcTimestamp = hlc ?? Hlc.now(nodeId);
|
||||
|
||||
return database.todosTable.$columns.map(
|
||||
(c) => CrdtDataEntry(
|
||||
userId: 'test',
|
||||
tblName: 'todos',
|
||||
columnName: c.$name,
|
||||
rowId: id.toString(),
|
||||
rawValue: (toJson()[c.$name] as Object?)?.toDriftAny(),
|
||||
hlcTimestamp: hlcTimestamp,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension on Object {
|
||||
DriftAny? toDriftAny() {
|
||||
return DriftAny(this);
|
||||
}
|
||||
}
|
||||
164
test/crdt/crdt_save_changeset_test.dart
Normal file
164
test/crdt/crdt_save_changeset_test.dart
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
import 'package:drift/drift.dart' hide isNotNull, isNull;
|
||||
import 'package:drift_offline_first/drift_offline_first.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../utils/crdt_context.dart';
|
||||
import '../utils/database.dart';
|
||||
import '../utils/tables.dart';
|
||||
|
||||
void main() {
|
||||
late OfflineSyncCrdt crdt;
|
||||
late CrdtDatabase crdtDb;
|
||||
late String otherNodeId;
|
||||
|
||||
group('Given CRDT for an empty database and a pending changeset', () {
|
||||
late Iterable<CrdtDataEntry> changeset;
|
||||
late Hlc lastHlc;
|
||||
|
||||
setUp(() async {
|
||||
(crdt, crdtDb, _) = database.crdtContext;
|
||||
otherNodeId = const Uuid().v4();
|
||||
|
||||
final todo = TodoEntry(
|
||||
id: const RowId(1),
|
||||
content: 'test',
|
||||
targetDate: DateTime.now(),
|
||||
);
|
||||
|
||||
changeset = todo.toCrdtDataEntry(otherNodeId);
|
||||
lastHlc = Hlc.now(otherNodeId);
|
||||
});
|
||||
|
||||
group('when saving a changeset', () {
|
||||
setUp(() async {
|
||||
await crdt.saveChangeset(changeset, [lastHlc]);
|
||||
});
|
||||
|
||||
test('then the changeset is saved to the database.', () async {
|
||||
final savedChangeset = await crdtDb.managers.crdtDataTable.get();
|
||||
expect(savedChangeset, changeset);
|
||||
});
|
||||
|
||||
test('then the merged HLCs are saved to the database.', () async {
|
||||
final savedMergedHlcs = await crdtDb.managers.crdtMergeHlcTable.get();
|
||||
expect(savedMergedHlcs.map((e) => e.lastReceivedHlc).single, lastHlc);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group(
|
||||
'Given CRDT for a database with data and a pending changeset with conflicting data and older HLC',
|
||||
() {
|
||||
late Iterable<CrdtDataEntry> firstChangeset;
|
||||
late Iterable<CrdtDataEntry> lateOldChangeset;
|
||||
late Hlc lastHlc;
|
||||
late Hlc lateOldHlc;
|
||||
|
||||
setUp(() async {
|
||||
(crdt, crdtDb, _) = database.crdtContext;
|
||||
otherNodeId = const Uuid().v4();
|
||||
|
||||
final todo = TodoEntry(
|
||||
id: const RowId(1),
|
||||
content: 'test',
|
||||
targetDate: DateTime.now(),
|
||||
);
|
||||
|
||||
lastHlc = Hlc.now(otherNodeId);
|
||||
firstChangeset = todo.toCrdtDataEntry(otherNodeId, lastHlc);
|
||||
await crdt.saveChangeset(firstChangeset, [lastHlc]);
|
||||
|
||||
lateOldHlc = Hlc.zero(otherNodeId);
|
||||
lateOldChangeset = todo.toCrdtDataEntry(otherNodeId, lateOldHlc);
|
||||
});
|
||||
|
||||
group('when saving a changeset with older HLC', () {
|
||||
setUp(() async {
|
||||
await crdt.saveChangeset(lateOldChangeset, [lateOldHlc]);
|
||||
});
|
||||
|
||||
test('then no changes are saved to the database.', () async {
|
||||
final savedChangeset = await crdtDb.managers.crdtDataTable.get();
|
||||
expect(savedChangeset, firstChangeset);
|
||||
});
|
||||
|
||||
test('then the last received HLC is not updated in the database.', () async {
|
||||
final savedMergedHlcs = await crdtDb.managers.crdtMergeHlcTable.get();
|
||||
expect(savedMergedHlcs.map((e) => e.lastReceivedHlc).single, lastHlc);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group(
|
||||
'Given CRDT for a database with data and a pending changeset with conflicting data and some entries with newer HLC',
|
||||
() {
|
||||
late Iterable<CrdtDataEntry> firstChangeset;
|
||||
late Iterable<CrdtDataEntry> newerChangeset;
|
||||
late Hlc lastHlc;
|
||||
late Hlc newerHlc;
|
||||
|
||||
setUp(() async {
|
||||
(crdt, crdtDb, _) = database.crdtContext;
|
||||
otherNodeId = const Uuid().v4();
|
||||
|
||||
final todo = TodoEntry(
|
||||
id: const RowId(1),
|
||||
content: 'test',
|
||||
targetDate: DateTime.now(),
|
||||
);
|
||||
|
||||
lastHlc = Hlc.now(otherNodeId);
|
||||
firstChangeset = todo.toCrdtDataEntry(otherNodeId);
|
||||
await crdt.saveChangeset(firstChangeset, [lastHlc]);
|
||||
|
||||
newerHlc = Hlc.now(otherNodeId);
|
||||
newerChangeset = todo
|
||||
.toCrdtDataEntry(otherNodeId, newerHlc)
|
||||
.where((e) => e.columnName == 'content');
|
||||
});
|
||||
|
||||
group('when saving a changeset with some entries with newer HLC', () {
|
||||
setUp(() async {
|
||||
await crdt.saveChangeset(newerChangeset, [newerHlc]);
|
||||
});
|
||||
|
||||
test('then the entries with newer HLC are saved to the database.', () async {
|
||||
final expectedResultingChangeset = [
|
||||
for (final e in firstChangeset)
|
||||
if (e.columnName == 'content') newerChangeset.first else e,
|
||||
];
|
||||
|
||||
expect(await crdtDb.managers.crdtDataTable.get(), expectedResultingChangeset);
|
||||
});
|
||||
|
||||
test('then the last received HLC is updated in the database.', () async {
|
||||
final savedMergedHlcs = await crdtDb.managers.crdtMergeHlcTable.get();
|
||||
expect(savedMergedHlcs.map((e) => e.lastReceivedHlc).single, newerHlc);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
extension on TodoEntry {
|
||||
Iterable<CrdtDataEntry> toCrdtDataEntry(String nodeId, [Hlc? hlc]) {
|
||||
final hlcTimestamp = hlc ?? Hlc.now(nodeId);
|
||||
|
||||
return database.todosTable.$columns.map(
|
||||
(c) => CrdtDataEntry(
|
||||
userId: 'test',
|
||||
tblName: 'todos',
|
||||
columnName: c.$name,
|
||||
rowId: id.toString(),
|
||||
rawValue: (toJson()[c.$name] as Object?)?.toDriftAny(),
|
||||
hlcTimestamp: hlcTimestamp,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension on Object {
|
||||
DriftAny? toDriftAny() {
|
||||
return DriftAny(this);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue