diff --git a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart index 24d8877c7b..00b3304087 100644 --- a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart @@ -198,9 +198,15 @@ class SyncStreamRepository extends DriftDatabaseRepository { Future updateAssetsV1(Iterable data, {String debugLabel = 'user'}) async { try { await _db.batch((batch) { + // Keep only the last asset per partial-index key; server order is authoritative + final deduped = <(String, String, String?), SyncAssetV1>{}; for (final asset in data) { - // Avoid SqliteException(2067) when server re-issues a new id for - // the same (ownerId, checksum). #22522 #27186 + deduped[(asset.ownerId, asset.checksum, asset.libraryId)] = asset; + } + + // Avoid SqliteException(2067) when server re-issues a new id for + // the same (ownerId, checksum). #22522 #27186 + for (final asset in deduped.values) { _enqueueRemoteAssetDedupe( batch, id: asset.id, @@ -208,7 +214,9 @@ class SyncStreamRepository extends DriftDatabaseRepository { checksum: asset.checksum, libraryId: asset.libraryId, ); + } + for (final asset in deduped.values) { final companion = RemoteAssetEntityCompanion( name: Value(asset.originalFileName), type: Value(asset.type.toAssetType()), @@ -247,8 +255,14 @@ class SyncStreamRepository extends DriftDatabaseRepository { Future updateAssetsV2(Iterable data, {String debugLabel = 'user'}) async { try { await _db.batch((batch) { + // Keep only the last asset per partial-index key; server order is authoritative + final deduped = <(String, String, String?), SyncAssetV2>{}; for (final asset in data) { - // See updateAssetsV1 for why this dedupe is required. #22522 #27186 + deduped[(asset.ownerId, asset.checksum, asset.libraryId)] = asset; + } + + // See updateAssetsV1 for why this dedupe is required. #22522 #27186 + for (final asset in deduped.values) { _enqueueRemoteAssetDedupe( batch, id: asset.id, @@ -256,7 +270,9 @@ class SyncStreamRepository extends DriftDatabaseRepository { checksum: asset.checksum, libraryId: asset.libraryId, ); + } + for (final asset in deduped.values) { final companion = RemoteAssetEntityCompanion( name: Value(asset.originalFileName), type: Value(asset.type.toAssetType()), diff --git a/mobile/test/domain/repositories/sync_stream_repository_test.dart b/mobile/test/domain/repositories/sync_stream_repository_test.dart index fd593b7542..9761b1c9cc 100644 --- a/mobile/test/domain/repositories/sync_stream_repository_test.dart +++ b/mobile/test/domain/repositories/sync_stream_repository_test.dart @@ -349,5 +349,30 @@ void main() { expect(rows, hasLength(1)); expect(rows.single.id, equals('new-id')); }); + + test('one payload with duplicate (ownerId, checksum) and null library keeps the last asset', () async { + await sut.updateUsersV1([_createUser()]); + await sut.updateAssetsV1([ + _createAsset(id: 'first-id', checksum: 'AAA', fileName: 'photo.jpg'), + _createAsset(id: 'last-id', checksum: 'AAA', fileName: 'photo.jpg'), + ]); + + final rows = await db.remoteAssetEntity.select().get(); + expect(rows, hasLength(1)); + expect(rows.single.id, equals('last-id')); + }); + + test('one payload with duplicate (ownerId, libraryId, checksum) keeps the last asset', () async { + await sut.updateUsersV1([_createUser()]); + await sut.updateAssetsV1([ + _createAsset(id: 'first-id', checksum: 'AAA', fileName: 'photo.jpg', libraryId: 'lib-1'), + _createAsset(id: 'last-id', checksum: 'AAA', fileName: 'photo.jpg', libraryId: 'lib-1'), + ]); + + final rows = await db.remoteAssetEntity.select().get(); + expect(rows, hasLength(1)); + expect(rows.single.id, equals('last-id')); + expect(rows.single.libraryId, equals('lib-1')); + }); }); }