fix: Preserve harness transactions for non-synced row updates (#133)
This commit is contained in:
parent
1f5501ff97
commit
4789f650be
4 changed files with 626 additions and 0 deletions
|
|
@ -785,4 +785,142 @@ void main() {
|
|||
});
|
||||
},
|
||||
);
|
||||
|
||||
group('Given two rows on a table not tracked by CRDT,', () {
|
||||
late OfflineSyncIntegrityViolation violation;
|
||||
late OfflineSyncIntegrityViolation otherViolation;
|
||||
|
||||
setUp(() async {
|
||||
violation = await OfflineSyncIntegrityViolation.db.insertRow(
|
||||
session,
|
||||
OfflineSyncIntegrityViolation(
|
||||
type: OfflineSyncViolationType.ownershipCollision,
|
||||
domainTableName: Person.t.tableName,
|
||||
uuidRowId: const Uuid().v7obj(),
|
||||
incomingSpaceUuid: testCrdtUserId,
|
||||
operation: OfflineSyncViolationOperation.mergeInsert,
|
||||
firstSeenAt: DateTime.utc(2026),
|
||||
lastSeenAt: DateTime.utc(2026),
|
||||
occurrences: 1,
|
||||
),
|
||||
);
|
||||
otherViolation = await OfflineSyncIntegrityViolation.db.insertRow(
|
||||
session,
|
||||
violation.copyWith(id: null, uuidRowId: const Uuid().v7obj()),
|
||||
);
|
||||
});
|
||||
|
||||
group('when deleting one row with deleteRow,', () {
|
||||
late OfflineSyncIntegrityViolation deleted;
|
||||
|
||||
setUp(() async {
|
||||
deleted = await OfflineSyncIntegrityViolation.db.deleteRow(session, violation);
|
||||
});
|
||||
|
||||
test('then the deleted row is returned.', () {
|
||||
expect(deleted.id, violation.id);
|
||||
expect(deleted.uuidRowId, violation.uuidRowId);
|
||||
});
|
||||
|
||||
test('then only that row is physically removed.', () async {
|
||||
expect(
|
||||
await OfflineSyncIntegrityViolation.db.findById(testSession, violation.id!),
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
await OfflineSyncIntegrityViolation.db.findById(
|
||||
testSession,
|
||||
otherViolation.id!,
|
||||
),
|
||||
isNotNull,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('when deleting one row with delete and noReturn,', () {
|
||||
late List<OfflineSyncIntegrityViolation> deleted;
|
||||
|
||||
setUp(() async {
|
||||
deleted = await OfflineSyncIntegrityViolation.db.delete(
|
||||
session,
|
||||
[violation],
|
||||
noReturn: true,
|
||||
);
|
||||
});
|
||||
|
||||
test('then an empty list is returned.', () {
|
||||
expect(deleted, isEmpty);
|
||||
});
|
||||
|
||||
test('then only that row is physically removed.', () async {
|
||||
expect(
|
||||
await OfflineSyncIntegrityViolation.db.findById(testSession, violation.id!),
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
await OfflineSyncIntegrityViolation.db.findById(
|
||||
testSession,
|
||||
otherViolation.id!,
|
||||
),
|
||||
isNotNull,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('when deleting one row with deleteWhere and noReturn,', () {
|
||||
late List<OfflineSyncIntegrityViolation> deleted;
|
||||
|
||||
setUp(() async {
|
||||
deleted = await OfflineSyncIntegrityViolation.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.id.equals(violation.id),
|
||||
noReturn: true,
|
||||
);
|
||||
});
|
||||
|
||||
test('then an empty list is returned.', () {
|
||||
expect(deleted, isEmpty);
|
||||
});
|
||||
|
||||
test('then only the matching row is physically removed.', () async {
|
||||
expect(
|
||||
await OfflineSyncIntegrityViolation.db.findById(testSession, violation.id!),
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
await OfflineSyncIntegrityViolation.db.findById(
|
||||
testSession,
|
||||
otherViolation.id!,
|
||||
),
|
||||
isNotNull,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('when deleteRow runs in an explicit transaction that rolls back,', () {
|
||||
setUp(() async {
|
||||
final rollback = StateError('rollback');
|
||||
await expectLater(
|
||||
session.db.transaction((tx) async {
|
||||
await OfflineSyncIntegrityViolation.db.deleteRow(
|
||||
session,
|
||||
violation,
|
||||
transaction: tx,
|
||||
);
|
||||
throw rollback;
|
||||
}),
|
||||
throwsA(same(rollback)),
|
||||
);
|
||||
});
|
||||
|
||||
test('then the deleted row is restored.', () async {
|
||||
final stored = await OfflineSyncIntegrityViolation.db.findById(
|
||||
testSession,
|
||||
violation.id!,
|
||||
);
|
||||
expect(stored?.uuidRowId, violation.uuidRowId);
|
||||
expect(stored?.occurrences, 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -545,4 +545,112 @@ void main() {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('Given an empty table not tracked by CRDT,', () {
|
||||
late OfflineSyncIntegrityViolation violation;
|
||||
|
||||
setUp(() {
|
||||
violation = OfflineSyncIntegrityViolation(
|
||||
type: OfflineSyncViolationType.ownershipCollision,
|
||||
domainTableName: Person.t.tableName,
|
||||
uuidRowId: const Uuid().v7obj(),
|
||||
incomingSpaceUuid: testCrdtUserId,
|
||||
operation: OfflineSyncViolationOperation.mergeInsert,
|
||||
firstSeenAt: DateTime.utc(2026),
|
||||
lastSeenAt: DateTime.utc(2026),
|
||||
occurrences: 1,
|
||||
);
|
||||
});
|
||||
|
||||
group('when inserting a row with insertRow,', () {
|
||||
late OfflineSyncIntegrityViolation inserted;
|
||||
|
||||
setUp(() async {
|
||||
inserted = await OfflineSyncIntegrityViolation.db.insertRow(session, violation);
|
||||
});
|
||||
|
||||
test('then the returned row has a generated id and the supplied values.', () {
|
||||
expect(inserted.id, isNotNull);
|
||||
expect(inserted.uuidRowId, violation.uuidRowId);
|
||||
expect(inserted.occurrences, 1);
|
||||
});
|
||||
|
||||
test('then the row is stored in the database.', () async {
|
||||
final stored = await OfflineSyncIntegrityViolation.db.findById(
|
||||
session,
|
||||
inserted.id!,
|
||||
);
|
||||
expect(stored?.uuidRowId, violation.uuidRowId);
|
||||
expect(stored?.occurrences, 1);
|
||||
});
|
||||
});
|
||||
|
||||
group('when inserting rows with noReturn,', () {
|
||||
late List<OfflineSyncIntegrityViolation> inserted;
|
||||
|
||||
setUp(() async {
|
||||
inserted = await OfflineSyncIntegrityViolation.db.insert(
|
||||
session,
|
||||
[violation],
|
||||
noReturn: true,
|
||||
);
|
||||
});
|
||||
|
||||
test('then an empty list is returned.', () {
|
||||
expect(inserted, isEmpty);
|
||||
});
|
||||
|
||||
test('then the row is stored in the database.', () async {
|
||||
final stored = await OfflineSyncIntegrityViolation.db.findFirstRow(
|
||||
session,
|
||||
where: (t) => t.uuidRowId.equals(violation.uuidRowId),
|
||||
);
|
||||
expect(stored?.occurrences, 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('Given an existing row on a table not tracked by CRDT,', () {
|
||||
late OfflineSyncIntegrityViolation violation;
|
||||
|
||||
setUp(() async {
|
||||
violation = await OfflineSyncIntegrityViolation.db.insertRow(
|
||||
session,
|
||||
OfflineSyncIntegrityViolation(
|
||||
type: OfflineSyncViolationType.ownershipCollision,
|
||||
domainTableName: Person.t.tableName,
|
||||
uuidRowId: const Uuid().v7obj(),
|
||||
incomingSpaceUuid: testCrdtUserId,
|
||||
operation: OfflineSyncViolationOperation.mergeInsert,
|
||||
firstSeenAt: DateTime.utc(2026),
|
||||
lastSeenAt: DateTime.utc(2026),
|
||||
occurrences: 1,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
group('when inserting the same primary key with ignoreConflicts,', () {
|
||||
late List<OfflineSyncIntegrityViolation> inserted;
|
||||
|
||||
setUp(() async {
|
||||
inserted = await OfflineSyncIntegrityViolation.db.insert(
|
||||
session,
|
||||
[violation.copyWith(occurrences: 2)],
|
||||
ignoreConflicts: true,
|
||||
);
|
||||
});
|
||||
|
||||
test('then an empty list is returned.', () {
|
||||
expect(inserted, isEmpty);
|
||||
});
|
||||
|
||||
test('then the original row is preserved.', () async {
|
||||
final stored = await OfflineSyncIntegrityViolation.db.findById(
|
||||
session,
|
||||
violation.id!,
|
||||
);
|
||||
expect(stored?.occurrences, 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -728,5 +728,132 @@ void main() {
|
|||
expect(row?.occurrences, 3);
|
||||
});
|
||||
});
|
||||
|
||||
group('when updating the row with updateRow,', () {
|
||||
late OfflineSyncIntegrityViolation updated;
|
||||
|
||||
setUp(() async {
|
||||
updated = await OfflineSyncIntegrityViolation.db.updateRow(
|
||||
session,
|
||||
violation.copyWith(occurrences: 2),
|
||||
);
|
||||
});
|
||||
|
||||
test('then the updated row is returned.', () {
|
||||
expect(updated.id, violation.id);
|
||||
expect(updated.occurrences, 2);
|
||||
});
|
||||
|
||||
test('then the row reflects the new values.', () async {
|
||||
final stored = await OfflineSyncIntegrityViolation.db.findById(
|
||||
session,
|
||||
violation.id!,
|
||||
);
|
||||
expect(stored?.occurrences, 2);
|
||||
});
|
||||
});
|
||||
|
||||
group('when updateRow selects only the occurrences column,', () {
|
||||
late OfflineSyncIntegrityViolation updated;
|
||||
|
||||
setUp(() async {
|
||||
updated = await OfflineSyncIntegrityViolation.db.updateRow(
|
||||
session,
|
||||
violation.copyWith(occurrences: 2, lastSeenAt: DateTime.utc(2027)),
|
||||
columns: (t) => [t.occurrences],
|
||||
);
|
||||
});
|
||||
|
||||
test('then the returned row preserves the unselected timestamp.', () {
|
||||
expect(updated.occurrences, 2);
|
||||
expect(updated.lastSeenAt, violation.lastSeenAt);
|
||||
});
|
||||
|
||||
test('then only the selected column changes in the database.', () async {
|
||||
final stored = await OfflineSyncIntegrityViolation.db.findById(
|
||||
session,
|
||||
violation.id!,
|
||||
);
|
||||
expect(stored?.occurrences, 2);
|
||||
expect(stored?.lastSeenAt, violation.lastSeenAt);
|
||||
});
|
||||
});
|
||||
|
||||
group('when update selects only the occurrences column,', () {
|
||||
late List<OfflineSyncIntegrityViolation> updated;
|
||||
|
||||
setUp(() async {
|
||||
updated = await OfflineSyncIntegrityViolation.db.update(
|
||||
session,
|
||||
[violation.copyWith(occurrences: 2, lastSeenAt: DateTime.utc(2027))],
|
||||
columns: (t) => [t.occurrences],
|
||||
);
|
||||
});
|
||||
|
||||
test('then the returned row preserves the unselected timestamp.', () {
|
||||
expect(updated.single.id, violation.id);
|
||||
expect(updated.single.occurrences, 2);
|
||||
expect(updated.single.lastSeenAt, violation.lastSeenAt);
|
||||
});
|
||||
|
||||
test('then only the selected column changes in the database.', () async {
|
||||
final stored = await OfflineSyncIntegrityViolation.db.findById(
|
||||
session,
|
||||
violation.id!,
|
||||
);
|
||||
expect(stored?.occurrences, 2);
|
||||
expect(stored?.lastSeenAt, violation.lastSeenAt);
|
||||
});
|
||||
});
|
||||
|
||||
group('when updateRow runs in an explicit transaction that rolls back,', () {
|
||||
setUp(() async {
|
||||
final rollback = StateError('rollback');
|
||||
await expectLater(
|
||||
session.db.transaction((tx) async {
|
||||
await OfflineSyncIntegrityViolation.db.updateRow(
|
||||
session,
|
||||
violation.copyWith(occurrences: 2),
|
||||
transaction: tx,
|
||||
);
|
||||
throw rollback;
|
||||
}),
|
||||
throwsA(same(rollback)),
|
||||
);
|
||||
});
|
||||
|
||||
test('then the original row is preserved.', () async {
|
||||
final stored = await OfflineSyncIntegrityViolation.db.findById(
|
||||
session,
|
||||
violation.id!,
|
||||
);
|
||||
expect(stored?.occurrences, 1);
|
||||
});
|
||||
});
|
||||
|
||||
group('when updateById changes the occurrences column,', () {
|
||||
late OfflineSyncIntegrityViolation? updated;
|
||||
|
||||
setUp(() async {
|
||||
updated = await OfflineSyncIntegrityViolation.db.updateById(
|
||||
session,
|
||||
violation.id!,
|
||||
columnValues: (t) => [t.occurrences(2)],
|
||||
);
|
||||
});
|
||||
|
||||
test('then the updated row is returned.', () {
|
||||
expect(updated?.id, violation.id);
|
||||
expect(updated?.occurrences, 2);
|
||||
});
|
||||
|
||||
test('then the row reflects the new values.', () async {
|
||||
final stored = await OfflineSyncIntegrityViolation.db.findById(
|
||||
session,
|
||||
violation.id!,
|
||||
);
|
||||
expect(stored?.occurrences, 2);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -638,4 +638,139 @@ void main() {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('Given an existing row on a table not tracked by CRDT,', () {
|
||||
late OfflineSyncIntegrityViolation violation;
|
||||
|
||||
setUp(() async {
|
||||
violation = await OfflineSyncIntegrityViolation.db.insertRow(
|
||||
session,
|
||||
OfflineSyncIntegrityViolation(
|
||||
type: OfflineSyncViolationType.ownershipCollision,
|
||||
domainTableName: Person.t.tableName,
|
||||
uuidRowId: const Uuid().v7obj(),
|
||||
incomingSpaceUuid: testCrdtUserId,
|
||||
operation: OfflineSyncViolationOperation.mergeInsert,
|
||||
firstSeenAt: DateTime.utc(2026),
|
||||
lastSeenAt: DateTime.utc(2026),
|
||||
occurrences: 1,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
group('when upserting a new row without an id,', () {
|
||||
late UuidValue rowUuid;
|
||||
late OfflineSyncIntegrityViolation? inserted;
|
||||
|
||||
setUp(() async {
|
||||
rowUuid = const Uuid().v7obj();
|
||||
inserted = await OfflineSyncIntegrityViolation.db.upsertRow(
|
||||
session,
|
||||
violation.copyWith(id: null, uuidRowId: rowUuid, occurrences: 2),
|
||||
conflictColumns: (t) => [t.id],
|
||||
);
|
||||
});
|
||||
|
||||
test('then the returned row has a generated id and the supplied values.', () {
|
||||
expect(inserted?.id, isNotNull);
|
||||
expect(inserted?.id, isNot(violation.id));
|
||||
expect(inserted?.uuidRowId, rowUuid);
|
||||
expect(inserted?.occurrences, 2);
|
||||
});
|
||||
|
||||
test('then both rows are stored with their respective values.', () async {
|
||||
final stored = await OfflineSyncIntegrityViolation.db.findFirstRow(
|
||||
session,
|
||||
where: (t) => t.uuidRowId.equals(rowUuid),
|
||||
);
|
||||
final original = await OfflineSyncIntegrityViolation.db.findById(
|
||||
session,
|
||||
violation.id!,
|
||||
);
|
||||
expect(stored?.occurrences, 2);
|
||||
expect(original?.occurrences, 1);
|
||||
});
|
||||
});
|
||||
|
||||
group(
|
||||
'when upserting a conflicting row with only occurrences selected for update,',
|
||||
() {
|
||||
late OfflineSyncIntegrityViolation? updated;
|
||||
|
||||
setUp(() async {
|
||||
updated = await OfflineSyncIntegrityViolation.db.upsertRow(
|
||||
session,
|
||||
violation.copyWith(occurrences: 2, lastSeenAt: DateTime.utc(2027)),
|
||||
conflictColumns: (t) => [t.id],
|
||||
updateColumns: (t) => [t.occurrences],
|
||||
);
|
||||
});
|
||||
|
||||
test('then the returned row preserves its id and unselected timestamp.', () {
|
||||
expect(updated?.id, violation.id);
|
||||
expect(updated?.occurrences, 2);
|
||||
expect(updated?.lastSeenAt, violation.lastSeenAt);
|
||||
});
|
||||
|
||||
test('then only the selected column changes in the database.', () async {
|
||||
final stored = await OfflineSyncIntegrityViolation.db.findById(
|
||||
session,
|
||||
violation.id!,
|
||||
);
|
||||
expect(stored?.occurrences, 2);
|
||||
expect(stored?.lastSeenAt, violation.lastSeenAt);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
group('when upserting a conflicting row with a non-matching updateWhere,', () {
|
||||
late OfflineSyncIntegrityViolation? updated;
|
||||
|
||||
setUp(() async {
|
||||
updated = await OfflineSyncIntegrityViolation.db.upsertRow(
|
||||
session,
|
||||
violation.copyWith(occurrences: 2),
|
||||
conflictColumns: (t) => [t.id],
|
||||
updateWhere: (t) => t.occurrences.equals(99),
|
||||
);
|
||||
});
|
||||
|
||||
test('then no row is returned.', () {
|
||||
expect(updated, isNull);
|
||||
});
|
||||
|
||||
test('then the original row is preserved.', () async {
|
||||
final stored = await OfflineSyncIntegrityViolation.db.findById(
|
||||
session,
|
||||
violation.id!,
|
||||
);
|
||||
expect(stored?.occurrences, 1);
|
||||
});
|
||||
});
|
||||
|
||||
group('when upserting a conflicting row with noReturn,', () {
|
||||
late List<OfflineSyncIntegrityViolation> updated;
|
||||
|
||||
setUp(() async {
|
||||
updated = await OfflineSyncIntegrityViolation.db.upsert(
|
||||
session,
|
||||
[violation.copyWith(occurrences: 2)],
|
||||
conflictColumns: (t) => [t.id],
|
||||
noReturn: true,
|
||||
);
|
||||
});
|
||||
|
||||
test('then an empty list is returned.', () {
|
||||
expect(updated, isEmpty);
|
||||
});
|
||||
|
||||
test('then the row reflects the new values.', () async {
|
||||
final stored = await OfflineSyncIntegrityViolation.db.findById(
|
||||
session,
|
||||
violation.id!,
|
||||
);
|
||||
expect(stored?.occurrences, 2);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue