mirror of
https://github.com/immich-app/immich
synced 2026-08-15 13:03:57 +00:00
Merge 281d88c034 into f9c05af45f
This commit is contained in:
commit
5a1bc264c6
2 changed files with 128 additions and 3 deletions
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue