This commit is contained in:
Santo Shakil 2026-08-15 12:41:05 +06:00 committed by GitHub
commit 5a1bc264c6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 128 additions and 3 deletions

View file

@ -224,6 +224,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
batch.insert(
_db.remoteAssetEntity,
companion.copyWith(id: Value(asset.id)),
mode: InsertMode.insertOrReplace,
onConflict: DoUpdate((_) => companion),
);
}
@ -263,6 +264,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
batch.insert(
_db.remoteAssetEntity,
companion.copyWith(id: Value(asset.id)),
mode: InsertMode.insertOrReplace,
onConflict: DoUpdate((_) => companion),
);
}

View file

@ -3,6 +3,7 @@ import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
@ -25,17 +26,18 @@ SyncAssetV1 _createAsset({
required String id,
required String checksum,
required String fileName,
String ownerId = 'user-1',
int? width,
int? height,
String? libraryId,
bool isFavorite = false,
}) {
return SyncAssetV1(
id: id,
checksum: checksum,
originalFileName: fileName,
type: AssetTypeEnum.IMAGE,
ownerId: ownerId,
isFavorite: false,
ownerId: 'user-1',
isFavorite: isFavorite,
fileCreatedAt: DateTime(2024, 1, 1),
fileModifiedAt: DateTime(2024, 1, 1),
createdAt: DateTime(2024, 1, 1),
@ -45,6 +47,31 @@ SyncAssetV1 _createAsset({
height: height,
deletedAt: null,
duration: null,
libraryId: libraryId,
livePhotoVideoId: null,
stackId: null,
thumbhash: null,
isEdited: false,
);
}
SyncAssetV2 _createAssetV2({required String id, required String checksum, required String fileName}) {
return SyncAssetV2(
id: id,
checksum: checksum,
originalFileName: fileName,
type: AssetTypeEnum.IMAGE,
ownerId: 'user-1',
isFavorite: false,
fileCreatedAt: DateTime(2024, 1, 1),
fileModifiedAt: DateTime(2024, 1, 1),
createdAt: DateTime(2024, 1, 1),
localDateTime: DateTime(2024, 1, 1),
visibility: AssetVisibility.timeline,
width: null,
height: null,
deletedAt: null,
duration: 0,
libraryId: null,
livePhotoVideoId: null,
stackId: null,
@ -240,4 +267,100 @@ void main() {
expect(after.backupSelection, equals(BackupSelection.none));
});
});
group('SyncStreamRepository - updateAssets upsert dedupe (#22522 #27186)', () {
Future<void> seedExif(String assetId) =>
db.remoteExifEntity.insertOne(RemoteExifEntityCompanion.insert(assetId: assetId));
Future<bool> exifExists(String assetId) async {
final rows = await (db.remoteExifEntity.select()..where((t) => t.assetId.equals(assetId))).get();
return rows.isNotEmpty;
}
test('same-id update keeps the child row and updates fields', () async {
await sut.updateUsersV1([_createUser()]);
final asset = _createAsset(id: 'a', checksum: 'AAA', fileName: 'photo.jpg');
await sut.updateAssetsV1([asset]);
await seedExif(asset.id);
final renamed = _createAsset(id: asset.id, checksum: asset.checksum, fileName: 'renamed.jpg', isFavorite: true);
await sut.updateAssetsV1([renamed]);
expect(await exifExists(asset.id), isTrue, reason: 'DO UPDATE keeps the row, the child survives');
final row = await (db.remoteAssetEntity.select()..where((t) => t.id.equals(asset.id))).getSingle();
expect(row.name, renamed.originalFileName);
expect(row.isFavorite, isTrue);
});
test('reupload with a new id replaces the stale row and cascades its child', () async {
await sut.updateUsersV1([_createUser()]);
final stale = _createAsset(id: 'stale', checksum: 'AAA', fileName: 'photo.jpg');
await sut.updateAssetsV1([stale]);
await seedExif(stale.id);
final fresh = _createAsset(id: 'fresh', checksum: stale.checksum, fileName: stale.originalFileName);
await sut.updateAssetsV1([fresh]);
final rows = await db.remoteAssetEntity.select().get();
expect(rows, hasLength(1), reason: 'no 2067, stale row replaced away');
expect(rows.single.id, fresh.id);
expect(await exifExists(stale.id), isFalse, reason: 'the stale child cascades with the replaced row');
// same scenario through V2
final staleV2 = _createAssetV2(id: 'stale2', checksum: 'BBB', fileName: 'photo2.jpg');
await sut.updateAssetsV2([staleV2]);
await seedExif(staleV2.id);
final freshV2 = _createAssetV2(id: 'fresh2', checksum: staleV2.checksum, fileName: staleV2.originalFileName);
await sut.updateAssetsV2([freshV2]);
final rows2 = await db.remoteAssetEntity.select().get();
expect(rows2.map((r) => r.id), containsAll([fresh.id, freshV2.id]));
expect(await exifExists(staleV2.id), isFalse);
});
test('library variant replaces only the matching library row', () async {
await sut.updateUsersV1([_createUser()]);
final staleLib = _createAsset(id: 'stale-lib', checksum: 'AAA', fileName: 'photo.jpg', libraryId: 'lib-1');
final keepNull = _createAsset(id: 'keep-null', checksum: staleLib.checksum, fileName: staleLib.originalFileName);
await sut.updateAssetsV1([staleLib, keepNull]);
final freshLib = _createAsset(
id: 'fresh-lib',
checksum: staleLib.checksum,
fileName: staleLib.originalFileName,
libraryId: staleLib.libraryId,
);
await sut.updateAssetsV1([freshLib]);
final rows = await db.remoteAssetEntity.select().get();
expect(rows.map((r) => r.id).toSet(), {
freshLib.id,
keepNull.id,
}, reason: 'library NULL and NOT NULL match different partial indexes');
});
test('batch-internal duplicates keep the last payload asset', () async {
await sut.updateUsersV1([_createUser()]);
final first = _createAsset(id: 'first-id', checksum: 'AAA', fileName: 'photo.jpg');
final last = _createAsset(id: 'last-id', checksum: first.checksum, fileName: first.originalFileName);
final firstLib = _createAsset(
id: 'first-lib',
checksum: 'BBB',
fileName: first.originalFileName,
libraryId: 'lib-1',
);
final lastLib = _createAsset(
id: 'last-lib',
checksum: firstLib.checksum,
fileName: firstLib.originalFileName,
libraryId: firstLib.libraryId,
);
await sut.updateAssetsV1([first, last, firstLib, lastLib]);
final rows = await db.remoteAssetEntity.select().get();
expect(rows, hasLength(2), reason: 'REPLACE makes batch-internal duplicates last-wins, no crash');
expect(rows.map((r) => r.id).toSet(), {last.id, lastLib.id});
});
});
}