From 6f4f79d8cc34be4b46205825af9eb2e75465abb5 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Fri, 22 Aug 2025 01:28:50 +0530 Subject: [PATCH] feat: migrate store to sqlite (#21078) * add store entity and migration * make store service take both isar and drift repos * migrate and switch store on beta timeline state change * chore: make drift variables final * dispose old store before switching repos * use store to update values for beta timeline * change log service to use the proper store * migrate store when beta already enabled * use isar repository to check beta timeline in store service * remove unused update method from store repo * dispose after create * change watchAll signature in store repo * fix test * rename init isar to initDB * request user to close and reopen on beta migration * fix tests * handle empty version in migration * wait for cache to be populated after migration --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex --- .../drift_schemas/main/drift_schema_v8.json | Bin 0 -> 34533 bytes .../test_utils/general_helper.dart | 14 +- mobile/lib/domain/services/log.service.dart | 8 +- mobile/lib/domain/services/store.service.dart | 24 +-- .../infrastructure/entities/store.entity.dart | 12 ++ .../entities/store.entity.drift.dart | Bin 0 -> 13357 bytes .../repositories/db.repository.dart | 7 +- .../repositories/db.repository.drift.dart | Bin 11951 -> 12257 bytes .../repositories/db.repository.steps.dart | Bin 92169 -> 102950 bytes .../repositories/store.repository.dart | 119 ++++++++++++-- .../repositories/user.repository.dart | 39 +++++ mobile/lib/main.dart | 14 +- .../pages/common/change_experience.page.dart | 151 +++++++++--------- .../providers/infrastructure/db.provider.dart | 9 +- mobile/lib/services/background.service.dart | 14 +- .../services/backup_verification.service.dart | 6 +- mobile/lib/utils/bootstrap.dart | 28 +++- mobile/lib/utils/isolate.dart | 11 +- mobile/lib/utils/migration.dart | 45 +++++- .../settings/beta_timeline_list_tile.dart | 1 - .../domain/services/log_service_test.dart | 4 +- .../domain/services/store_service_test.dart | 28 +++- mobile/test/drift/main/generated/schema.dart | Bin 1054 -> 1138 bytes .../test/drift/main/generated/schema_v8.dart | Bin 0 -> 215042 bytes .../repositories/store_repository_test.dart | 37 +++-- .../test/infrastructure/repository.mock.dart | 2 + 26 files changed, 409 insertions(+), 164 deletions(-) create mode 100644 mobile/drift_schemas/main/drift_schema_v8.json create mode 100644 mobile/lib/infrastructure/entities/store.entity.drift.dart create mode 100644 mobile/test/drift/main/generated/schema_v8.dart diff --git a/mobile/drift_schemas/main/drift_schema_v8.json b/mobile/drift_schemas/main/drift_schema_v8.json new file mode 100644 index 0000000000000000000000000000000000000000..6a4fe73fcb65d36046b12fffcdd44203c214b0b2 GIT binary patch literal 34533 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 loadApp(WidgetTester tester) async { await EasyLocalization.ensureInitialized(); // Clear all data from Isar (reuse existing instance if available) - final db = await Bootstrap.initIsar(); - final logDb = DriftLogger(); - await Bootstrap.initDomain(db, logDb); + final (isar, drift, logDb) = await Bootstrap.initDB(); + await Bootstrap.initDomain(isar, drift, logDb); await Store.clear(); - await db.writeTxn(() => db.clear()); + await isar.writeTxn(() => isar.clear()); // Load main Widget await tester.pumpWidget( ProviderScope( - overrides: [dbProvider.overrideWithValue(db), isarProvider.overrideWithValue(db)], + overrides: [ + dbProvider.overrideWithValue(isar), + isarProvider.overrideWithValue(isar), + driftProvider.overrideWith(driftOverride(drift)), + ], child: const app.MainWidget(), ), ); diff --git a/mobile/lib/domain/services/log.service.dart b/mobile/lib/domain/services/log.service.dart index 98cb24d9c..1053d5e54 100644 --- a/mobile/lib/domain/services/log.service.dart +++ b/mobile/lib/domain/services/log.service.dart @@ -15,7 +15,7 @@ import 'package:logging/logging.dart'; /// via [IStoreRepository] class LogService { final LogRepository _logRepository; - final IsarStoreRepository _storeRepository; + final IStoreRepository _storeRepository; final List _msgBuffer = []; @@ -38,7 +38,7 @@ class LogService { static Future init({ required LogRepository logRepository, - required IsarStoreRepository storeRepository, + required IStoreRepository storeRepository, bool shouldBuffer = true, }) async { _instance ??= await create( @@ -51,7 +51,7 @@ class LogService { static Future create({ required LogRepository logRepository, - required IsarStoreRepository storeRepository, + required IStoreRepository storeRepository, bool shouldBuffer = true, }) async { final instance = LogService._(logRepository, storeRepository, shouldBuffer); @@ -92,7 +92,7 @@ class LogService { } Future setLogLevel(LogLevel level) async { - await _storeRepository.insert(StoreKey.logLevel, level.index); + await _storeRepository.upsert(StoreKey.logLevel, level.index); Logger.root.level = level.toLevel(); } diff --git a/mobile/lib/domain/services/store.service.dart b/mobile/lib/domain/services/store.service.dart index dc845b70f..3347134ae 100644 --- a/mobile/lib/domain/services/store.service.dart +++ b/mobile/lib/domain/services/store.service.dart @@ -6,13 +6,13 @@ import 'package:immich_mobile/infrastructure/repositories/store.repository.dart' /// Provides access to a persistent key-value store with an in-memory cache. /// Listens for repository changes to keep the cache updated. class StoreService { - final IsarStoreRepository _storeRepository; + final IStoreRepository _storeRepository; /// In-memory cache. Keys are [StoreKey.id] final Map _cache = {}; - late final StreamSubscription _storeUpdateSubscription; + late final StreamSubscription> _storeUpdateSubscription; - StoreService._({required IsarStoreRepository storeRepository}) : _storeRepository = storeRepository; + StoreService._({required IStoreRepository isarStoreRepository}) : _storeRepository = isarStoreRepository; // TODO: Temporary typedef to make minimal changes. Remove this and make the presentation layer access store through a provider static StoreService? _instance; @@ -24,27 +24,29 @@ class StoreService { } // TODO: Replace the implementation with the one from create after removing the typedef - static Future init({required IsarStoreRepository storeRepository}) async { + static Future init({required IStoreRepository storeRepository}) async { _instance ??= await create(storeRepository: storeRepository); return _instance!; } - static Future create({required IsarStoreRepository storeRepository}) async { - final instance = StoreService._(storeRepository: storeRepository); - await instance._populateCache(); + static Future create({required IStoreRepository storeRepository}) async { + final instance = StoreService._(isarStoreRepository: storeRepository); + await instance.populateCache(); instance._storeUpdateSubscription = instance._listenForChange(); return instance; } - Future _populateCache() async { + Future populateCache() async { final storeValues = await _storeRepository.getAll(); for (StoreDto storeValue in storeValues) { _cache[storeValue.key.id] = storeValue.value; } } - StreamSubscription _listenForChange() => _storeRepository.watchAll().listen((event) { - _cache[event.key.id] = event.value; + StreamSubscription> _listenForChange() => _storeRepository.watchAll().listen((events) { + for (final event in events) { + _cache[event.key.id] = event.value; + } }); /// Disposes the store and cancels the subscription. To reuse the store call init() again @@ -69,7 +71,7 @@ class StoreService { /// Stores the [value] for the [key]. Skips write if value hasn't changed. Future put, T>(U key, T value) async { if (_cache[key.id] == value) return; - await _storeRepository.insert(key, value); + await _storeRepository.upsert(key, value); _cache[key.id] = value; } diff --git a/mobile/lib/infrastructure/entities/store.entity.dart b/mobile/lib/infrastructure/entities/store.entity.dart index 8d6d9a7d1..d4b3eec84 100644 --- a/mobile/lib/infrastructure/entities/store.entity.dart +++ b/mobile/lib/infrastructure/entities/store.entity.dart @@ -1,3 +1,5 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; import 'package:isar/isar.dart'; part 'store.entity.g.dart'; @@ -11,3 +13,13 @@ class StoreValue { const StoreValue(this.id, {this.intValue, this.strValue}); } + +class StoreEntity extends Table with DriftDefaultsMixin { + IntColumn get id => integer()(); + + TextColumn get stringValue => text().nullable()(); + IntColumn get intValue => integer().nullable()(); + + @override + Set get primaryKey => {id}; +} diff --git a/mobile/lib/infrastructure/entities/store.entity.drift.dart b/mobile/lib/infrastructure/entities/store.entity.drift.dart new file mode 100644 index 0000000000000000000000000000000000000000..327b0e95d9dd295ece27a7ada7ce9163294ff4b5 GIT binary patch literal 13357 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 Zf%Y%7i+JK!pp3@bhAaM`6l&aV{U2<5aD4y( literal 0 HcmV?d00001 diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart index c829b7c58..386de2269 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.dart @@ -18,6 +18,7 @@ import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity. import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/stack.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.steps.dart'; @@ -58,6 +59,7 @@ class IsarDatabaseRepository implements IDatabaseRepository { StackEntity, PersonEntity, AssetFaceEntity, + StoreEntity, ], include: {'package:immich_mobile/infrastructure/entities/merged_asset.drift'}, ) @@ -66,7 +68,7 @@ class Drift extends $Drift implements IDatabaseRepository { : super(executor ?? driftDatabase(name: 'immich', native: const DriftNativeOptions(shareAcrossIsolates: true))); @override - int get schemaVersion => 7; + int get schemaVersion => 8; @override MigrationStrategy get migration => MigrationStrategy( @@ -118,6 +120,9 @@ class Drift extends $Drift implements IDatabaseRepository { from6To7: (m, v7) async { await m.createIndex(v7.idxLatLng); }, + from7To8: (m, v8) async { + await m.create(v8.storeEntity); + }, ), ); diff --git a/mobile/lib/infrastructure/repositories/db.repository.drift.dart b/mobile/lib/infrastructure/repositories/db.repository.drift.dart index fd170fc22d733db2594458da807f02fa7dcc618e..456296e2d0fedb6a7b07afe6ebb4e656c05ee3d8 100644 GIT binary patch delta 206 zcmZ1<`!IgPL^jsqlKi67$p=|vCvRpGo_v5=ZSq7mB}R+MTi8q(EjPbpTgEnd11FEJ zp}C$)Fi5FuUP)$2Wk_ODPO1V}FGN7WRv{CvW^y8rD08Nv#pFr6I*gW+_wyP84gAO} z%4o5eosXFXq;2yrfz7hK3J^^YlP4EyicjWOXP1N9qF`&sr2qzCv*F4V1(0mkwC1Yi F0suEgLRJ6( delta 76 zcmaDDzdm-uM7GIq*pw!3W)q&Q$gad_vDt-v85?t^q4{KMJ{?Aj$r*fx;^un3sYU6j gDUQX(sUY!PX4IuIQfE+{Nx3d0!$VblMUVN z*`Y>F4)jr+l;HwZ%V=yc`GO`d3&<>pj1o{E@Al$2#utp!jpG^3gn)sNoRONFsHu=v zl%H!Jl5ep+GJ$b63tMtxajJs(^uA<9c}9!r>yjB0SU|GVKO{0LGg@qalfvl4gv*HO ZI_Zqp deleteAll(); + Stream>> watchAll(); + Future delete(StoreKey key); + Future upsert(StoreKey key, T value); + Future tryGet(StoreKey key); + Stream watch(StoreKey key); + Future>> getAll(); +} + +class IsarStoreRepository extends IsarDatabaseRepository implements IStoreRepository { final Isar _db; final validStoreKeys = StoreKey.values.map((e) => e.id).toSet(); IsarStoreRepository(super.db) : _db = db; + @override Future deleteAll() async { return await transaction(() async { await _db.storeValues.clear(); @@ -18,25 +32,29 @@ class IsarStoreRepository extends IsarDatabaseRepository { }); } - Stream> watchAll() { + @override + Stream>> watchAll() { return _db.storeValues .filter() .anyOf(validStoreKeys, (query, id) => query.idEqualTo(id)) .watch(fireImmediately: true) - .asyncExpand((entities) => Stream.fromFutures(entities.map((e) async => _toUpdateEvent(e)))); + .asyncMap((entities) => Future.wait(entities.map((entity) => _toUpdateEvent(entity)))); } + @override Future delete(StoreKey key) async { return await transaction(() async => await _db.storeValues.delete(key.id)); } - Future insert(StoreKey key, T value) async { + @override + Future upsert(StoreKey key, T value) async { return await transaction(() async { await _db.storeValues.put(await _fromValue(key, value)); return true; }); } + @override Future tryGet(StoreKey key) async { final entity = (await _db.storeValues.get(key.id)); if (entity == null) { @@ -45,13 +63,7 @@ class IsarStoreRepository extends IsarDatabaseRepository { return await _toValue(key, entity); } - Future update(StoreKey key, T value) async { - return await transaction(() async { - await _db.storeValues.put(await _fromValue(key, value)); - return true; - }); - } - + @override Stream watch(StoreKey key) async* { yield* _db.storeValues .watchObject(key.id, fireImmediately: true) @@ -88,8 +100,93 @@ class IsarStoreRepository extends IsarDatabaseRepository { return StoreValue(key.id, intValue: intValue, strValue: strValue); } + @override Future>> getAll() async { final entities = await _db.storeValues.filter().anyOf(validStoreKeys, (query, id) => query.idEqualTo(id)).findAll(); return Future.wait(entities.map((e) => _toUpdateEvent(e)).toList()); } } + +class DriftStoreRepository extends DriftDatabaseRepository implements IStoreRepository { + final Drift _db; + final validStoreKeys = StoreKey.values.map((e) => e.id).toSet(); + + DriftStoreRepository(super.db) : _db = db; + + @override + Future deleteAll() async { + await _db.storeEntity.deleteAll(); + return true; + } + + @override + Future>> getAll() async { + final query = _db.storeEntity.select()..where((entity) => entity.id.isIn(validStoreKeys)); + return query.asyncMap((entity) => _toUpdateEvent(entity)).get(); + } + + @override + Stream>> watchAll() { + final query = _db.storeEntity.select()..where((entity) => entity.id.isIn(validStoreKeys)); + + return query.asyncMap((entity) => _toUpdateEvent(entity)).watch(); + } + + @override + Future delete(StoreKey key) async { + await _db.storeEntity.deleteWhere((entity) => entity.id.equals(key.id)); + return; + } + + @override + Future upsert(StoreKey key, T value) async { + await _db.storeEntity.insertOnConflictUpdate(await _fromValue(key, value)); + return true; + } + + @override + Future tryGet(StoreKey key) async { + final entity = await _db.managers.storeEntity.filter((entity) => entity.id.equals(key.id)).getSingleOrNull(); + if (entity == null) { + return null; + } + return await _toValue(key, entity); + } + + @override + Stream watch(StoreKey key) async* { + final query = _db.storeEntity.select()..where((entity) => entity.id.equals(key.id)); + + yield* query.watchSingleOrNull().asyncMap((e) async => e == null ? null : await _toValue(key, e)); + } + + Future> _toUpdateEvent(StoreEntityData entity) async { + final key = StoreKey.values.firstWhere((e) => e.id == entity.id) as StoreKey; + final value = await _toValue(key, entity); + return StoreDto(key, value); + } + + Future _toValue(StoreKey key, StoreEntityData entity) async => + switch (key.type) { + const (int) => entity.intValue, + const (String) => entity.stringValue, + const (bool) => entity.intValue == 1, + const (DateTime) => entity.intValue == null ? null : DateTime.fromMillisecondsSinceEpoch(entity.intValue!), + const (UserDto) => + entity.stringValue == null ? null : await DriftUserRepository(_db).get(entity.stringValue!), + _ => null, + } + as T?; + + Future _fromValue(StoreKey key, T value) async { + final (int? intValue, String? strValue) = switch (key.type) { + const (int) => (value as int, null), + const (String) => (null, value as String), + const (bool) => ((value as bool) ? 1 : 0, null), + const (DateTime) => ((value as DateTime).millisecondsSinceEpoch, null), + const (UserDto) => (null, (await DriftUserRepository(_db).upsert(value as UserDto)).id), + _ => throw UnsupportedError("Unsupported primitive type: ${key.type} for key: ${key.name}"), + }; + return StoreEntityCompanion(id: Value(key.id), intValue: Value(intValue), stringValue: Value(strValue)); + } +} diff --git a/mobile/lib/infrastructure/repositories/user.repository.dart b/mobile/lib/infrastructure/repositories/user.repository.dart index 2c6d72139..1caab462c 100644 --- a/mobile/lib/infrastructure/repositories/user.repository.dart +++ b/mobile/lib/infrastructure/repositories/user.repository.dart @@ -1,6 +1,8 @@ +import 'package:drift/drift.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity; +import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:isar/isar.dart'; @@ -63,3 +65,40 @@ class IsarUserRepository extends IsarDatabaseRepository { return true; } } + +class DriftUserRepository extends DriftDatabaseRepository { + final Drift _db; + const DriftUserRepository(super.db) : _db = db; + + Future get(String id) => + _db.managers.userEntity.filter((user) => user.id.equals(id)).getSingleOrNull().then((user) => user?.toDto()); + + Future upsert(UserDto user) async { + await _db.userEntity.insertOnConflictUpdate( + UserEntityCompanion( + id: Value(user.id), + isAdmin: Value(user.isAdmin), + updatedAt: Value(user.updatedAt), + name: Value(user.name), + email: Value(user.email), + hasProfileImage: Value(user.hasProfileImage), + profileChangedAt: Value(user.profileChangedAt), + ), + ); + return user; + } +} + +extension on UserEntityData { + UserDto toDto() { + return UserDto( + id: id, + email: email, + name: name, + isAdmin: isAdmin, + updatedAt: updatedAt, + profileChangedAt: profileChangedAt, + hasProfileImage: hasProfileImage, + ); + } +} diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 8ac989183..0cab21748 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -14,7 +14,6 @@ import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/constants/locales.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/generated/codegen_loader.g.dart'; -import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart'; import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart'; import 'package:immich_mobile/providers/db.provider.dart'; @@ -41,18 +40,21 @@ import 'package:worker_manager/worker_manager.dart'; void main() async { ImmichWidgetsBinding(); - final db = await Bootstrap.initIsar(); - final logDb = DriftLogger(); - await Bootstrap.initDomain(db, logDb); + final (isar, drift, logDb) = await Bootstrap.initDB(); + await Bootstrap.initDomain(isar, drift, logDb); await initApp(); // Warm-up isolate pool for worker manager await workerManager.init(dynamicSpawning: true); - await migrateDatabaseIfNeeded(db); + await migrateDatabaseIfNeeded(isar, drift); HttpSSLOptions.apply(); runApp( ProviderScope( - overrides: [dbProvider.overrideWithValue(db), isarProvider.overrideWithValue(db)], + overrides: [ + dbProvider.overrideWithValue(isar), + isarProvider.overrideWithValue(isar), + driftProvider.overrideWith(driftOverride(drift)), + ], child: const MainWidget(), ), ); diff --git a/mobile/lib/pages/common/change_experience.page.dart b/mobile/lib/pages/common/change_experience.page.dart index 45392a38f..3e9747ce3 100644 --- a/mobile/lib/pages/common/change_experience.page.dart +++ b/mobile/lib/pages/common/change_experience.page.dart @@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; @@ -13,8 +14,8 @@ import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/utils/migration.dart'; +import 'package:logging/logging.dart'; import 'package:permission_handler/permission_handler.dart'; @RoutePage() @@ -28,7 +29,7 @@ class ChangeExperiencePage extends ConsumerStatefulWidget { } class _ChangeExperiencePageState extends ConsumerState { - bool hasMigrated = false; + AsyncValue hasMigrated = const AsyncValue.loading(); @override void initState() { @@ -37,46 +38,60 @@ class _ChangeExperiencePageState extends ConsumerState { } Future _handleMigration() async { - if (widget.switchingToBeta) { - final assetNotifier = ref.read(assetProvider.notifier); - if (assetNotifier.mounted) { - assetNotifier.dispose(); - } - final albumNotifier = ref.read(albumProvider.notifier); - if (albumNotifier.mounted) { - albumNotifier.dispose(); + try { + if (widget.switchingToBeta) { + final assetNotifier = ref.read(assetProvider.notifier); + if (assetNotifier.mounted) { + assetNotifier.dispose(); + } + final albumNotifier = ref.read(albumProvider.notifier); + if (albumNotifier.mounted) { + albumNotifier.dispose(); + } + + // Cancel uploads + await Store.put(StoreKey.backgroundBackup, false); + ref + .read(backupProvider.notifier) + .configureBackgroundBackup(enabled: false, onBatteryInfo: () {}, onError: (_) {}); + ref.read(backupProvider.notifier).setAutoBackup(false); + ref.read(backupProvider.notifier).cancelBackup(); + ref.read(manualUploadProvider.notifier).cancelBackup(); + // Start listening to new websocket events + ref.read(websocketProvider.notifier).stopListenToOldEvents(); + ref.read(websocketProvider.notifier).startListeningToBetaEvents(); + + final permission = await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); + + if (permission.isGranted) { + await ref.read(backgroundSyncProvider).syncLocal(full: true); + await migrateDeviceAssetToSqlite(ref.read(isarProvider), ref.read(driftProvider)); + await migrateBackupAlbumsToSqlite(ref.read(isarProvider), ref.read(driftProvider)); + await migrateStoreToSqlite(ref.read(isarProvider), ref.read(driftProvider)); + } + } else { + await ref.read(backgroundSyncProvider).cancel(); + ref.read(websocketProvider.notifier).stopListeningToBetaEvents(); + ref.read(websocketProvider.notifier).startListeningToOldEvents(); + await migrateStoreToIsar(ref.read(isarProvider), ref.read(driftProvider)); } - // Cancel uploads - await Store.put(StoreKey.backgroundBackup, false); - ref - .read(backupProvider.notifier) - .configureBackgroundBackup(enabled: false, onBatteryInfo: () {}, onError: (_) {}); - ref.read(backupProvider.notifier).setAutoBackup(false); - ref.read(backupProvider.notifier).cancelBackup(); - ref.read(manualUploadProvider.notifier).cancelBackup(); - // Start listening to new websocket events - ref.read(websocketProvider.notifier).stopListenToOldEvents(); - ref.read(websocketProvider.notifier).startListeningToBetaEvents(); + await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta); + await DriftStoreRepository(ref.read(driftProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta); - final permission = await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); - - if (permission.isGranted) { - await ref.read(backgroundSyncProvider).syncLocal(full: true); - await migrateDeviceAssetToSqlite(ref.read(isarProvider), ref.read(driftProvider)); - await migrateBackupAlbumsToSqlite(ref.read(isarProvider), ref.read(driftProvider)); + if (mounted) { + setState(() { + HapticFeedback.heavyImpact(); + hasMigrated = const AsyncValue.data(true); + }); + } + } catch (e, s) { + Logger("ChangeExperiencePage").severe("Error during migration", e, s); + if (mounted) { + setState(() { + hasMigrated = AsyncValue.error(e, s); + }); } - } else { - await ref.read(backgroundSyncProvider).cancel(); - ref.read(websocketProvider.notifier).stopListeningToBetaEvents(); - ref.read(websocketProvider.notifier).startListeningToOldEvents(); - } - - if (mounted) { - setState(() { - HapticFeedback.heavyImpact(); - hasMigrated = true; - }); } } @@ -89,44 +104,34 @@ class _ChangeExperiencePageState extends ConsumerState { children: [ AnimatedSwitcher( duration: Durations.long4, - child: hasMigrated - ? const Icon(Icons.check_circle_rounded, color: Colors.green, size: 48.0) - : const SizedBox(width: 50.0, height: 50.0, child: CircularProgressIndicator()), + child: hasMigrated.when( + data: (data) => const Icon(Icons.check_circle_rounded, color: Colors.green, size: 48.0), + error: (error, stackTrace) => const Icon(Icons.error, color: Colors.red, size: 48.0), + loading: () => const SizedBox(width: 50.0, height: 50.0, child: CircularProgressIndicator()), + ), ), const SizedBox(height: 16.0), - Center( - child: Column( - children: [ - SizedBox( - width: 300.0, - child: AnimatedSwitcher( - duration: Durations.long4, - child: hasMigrated - ? Text( - "Migration success!", - style: context.textTheme.titleMedium, - textAlign: TextAlign.center, - ) - : Text( - "Data migration in progress...\nPlease wait and don't close this page", - style: context.textTheme.titleMedium, - textAlign: TextAlign.center, - ), - ), + SizedBox( + width: 300.0, + child: AnimatedSwitcher( + duration: Durations.long4, + child: hasMigrated.when( + data: (data) => Text( + "Migration success!\nPlease close and reopen the app to apply changes", + style: context.textTheme.titleMedium, + textAlign: TextAlign.center, ), - if (hasMigrated) - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: ElevatedButton( - onPressed: () { - context.replaceRoute( - widget.switchingToBeta ? const TabShellRoute() : const TabControllerRoute(), - ); - }, - child: const Text("Continue"), - ), - ), - ], + error: (error, stackTrace) => Text( + "Migration failed!\nError: $error", + style: context.textTheme.titleMedium, + textAlign: TextAlign.center, + ), + loading: () => Text( + "Data migration in progress...\nPlease wait and don't close this page", + style: context.textTheme.titleMedium, + textAlign: TextAlign.center, + ), + ), ), ), ], diff --git a/mobile/lib/providers/infrastructure/db.provider.dart b/mobile/lib/providers/infrastructure/db.provider.dart index cdf934e50..d38bcbfb5 100644 --- a/mobile/lib/providers/infrastructure/db.provider.dart +++ b/mobile/lib/providers/infrastructure/db.provider.dart @@ -10,9 +10,12 @@ part 'db.provider.g.dart'; @Riverpod(keepAlive: true) Isar isar(Ref ref) => throw UnimplementedError('isar'); -final driftProvider = Provider((ref) { - final drift = Drift(); +Drift Function(Ref ref) driftOverride(Drift drift) => (ref) { ref.onDispose(() => unawaited(drift.close())); ref.keepAlive(); return drift; -}); +}; + +final driftProvider = Provider( + (ref) => throw UnimplementedError("driftProvider must be overridden in the isolate's ProviderContainer before use"), +); diff --git a/mobile/lib/services/background.service.dart b/mobile/lib/services/background.service.dart index 3bcc93f19..e6436df24 100644 --- a/mobile/lib/services/background.service.dart +++ b/mobile/lib/services/background.service.dart @@ -14,7 +14,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/backup_album.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart'; import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; @@ -331,11 +330,16 @@ class BackgroundService { } Future _onAssetsChanged() async { - final db = await Bootstrap.initIsar(); - final logDb = DriftLogger(); - await Bootstrap.initDomain(db, logDb); + final (isar, drift, logDb) = await Bootstrap.initDB(); + await Bootstrap.initDomain(isar, drift, logDb); - final ref = ProviderContainer(overrides: [dbProvider.overrideWithValue(db), isarProvider.overrideWithValue(db)]); + final ref = ProviderContainer( + overrides: [ + dbProvider.overrideWithValue(isar), + isarProvider.overrideWithValue(isar), + driftProvider.overrideWith(driftOverride(drift)), + ], + ); HttpSSLOptions.apply(); ref.read(apiServiceProvider).setAccessToken(Store.get(StoreKey.accessToken)); diff --git a/mobile/lib/services/backup_verification.service.dart b/mobile/lib/services/backup_verification.service.dart index 8f39fd17e..94c4721cc 100644 --- a/mobile/lib/services/backup_verification.service.dart +++ b/mobile/lib/services/backup_verification.service.dart @@ -11,7 +11,6 @@ import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart'; import 'package:immich_mobile/infrastructure/utils/exif.converter.dart'; import 'package:immich_mobile/providers/infrastructure/exif.provider.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; @@ -116,9 +115,8 @@ class BackupVerificationService { assert(tuple.deleteCandidates.length == tuple.originals.length); final List result = []; BackgroundIsolateBinaryMessenger.ensureInitialized(tuple.rootIsolateToken); - final db = await Bootstrap.initIsar(); - final logDb = DriftLogger(); - await Bootstrap.initDomain(db, logDb); + final (isar, drift, logDb) = await Bootstrap.initDB(); + await Bootstrap.initDomain(isar, drift, logDb); await tuple.fileMediaRepository.enableBackgroundAccess(); final ApiService apiService = ApiService(); apiService.setEndpoint(tuple.endpoint); diff --git a/mobile/lib/utils/bootstrap.dart b/mobile/lib/utils/bootstrap.dart index d2ad5ea16..480d918b4 100644 --- a/mobile/lib/utils/bootstrap.dart +++ b/mobile/lib/utils/bootstrap.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; +import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/entities/album.entity.dart'; @@ -14,6 +15,7 @@ import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; @@ -21,18 +23,23 @@ import 'package:isar/isar.dart'; import 'package:path_provider/path_provider.dart'; abstract final class Bootstrap { - static Future initIsar() async { - if (Isar.getInstance() != null) { - return Isar.getInstance()!; + static Future<(Isar isar, Drift drift, DriftLogger logDb)> initDB() async { + final drift = Drift(); + final logDb = DriftLogger(); + + Isar? isar = Isar.getInstance(); + + if (isar != null) { + return (isar, drift, logDb); } final dir = await getApplicationDocumentsDirectory(); - return await Isar.open( + isar = await Isar.open( [ StoreValueSchema, - ExifInfoSchema, AssetSchema, AlbumSchema, + ExifInfoSchema, UserSchema, BackupAlbumSchema, DuplicatedAssetSchema, @@ -45,14 +52,19 @@ abstract final class Bootstrap { maxSizeMiB: 2048, inspector: kDebugMode, ); + + return (isar, drift, logDb); } - static Future initDomain(Isar db, DriftLogger logDb, {bool shouldBufferLogs = true}) async { - await StoreService.init(storeRepository: IsarStoreRepository(db)); + static Future initDomain(Isar db, Drift drift, DriftLogger logDb, {bool shouldBufferLogs = true}) async { + final isBeta = await IsarStoreRepository(db).tryGet(StoreKey.betaTimeline) ?? false; + final IStoreRepository storeRepo = isBeta ? DriftStoreRepository(drift) : IsarStoreRepository(db); + + await StoreService.init(storeRepository: storeRepo); await LogService.init( logRepository: LogRepository(logDb), - storeRepository: IsarStoreRepository(db), + storeRepository: storeRepo, shouldBuffer: shouldBufferLogs, ); } diff --git a/mobile/lib/utils/isolate.dart b/mobile/lib/utils/isolate.dart index 2dfd9d4f5..58e7ad7f2 100644 --- a/mobile/lib/utils/isolate.dart +++ b/mobile/lib/utils/isolate.dart @@ -5,7 +5,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; -import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart'; import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; @@ -35,15 +34,15 @@ Cancelable runInIsolateGentle({ BackgroundIsolateBinaryMessenger.ensureInitialized(token); DartPluginRegistrant.ensureInitialized(); - final db = await Bootstrap.initIsar(); - final logDb = DriftLogger(); - await Bootstrap.initDomain(db, logDb, shouldBufferLogs: false); + final (isar, drift, logDb) = await Bootstrap.initDB(); + await Bootstrap.initDomain(isar, drift, logDb, shouldBufferLogs: false); final ref = ProviderContainer( overrides: [ // TODO: Remove once isar is removed - dbProvider.overrideWithValue(db), - isarProvider.overrideWithValue(db), + dbProvider.overrideWithValue(isar), + isarProvider.overrideWithValue(isar), cancellationProvider.overrideWithValue(cancelledChecker), + driftProvider.overrideWith(driftOverride(drift)), ], ); diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index e5c4bf6ef..9816986b9 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -20,6 +20,7 @@ import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; @@ -30,9 +31,10 @@ import 'package:logging/logging.dart'; // ignore: import_rule_photo_manager import 'package:photo_manager/photo_manager.dart'; -const int targetVersion = 13; +const int targetVersion = 14; -Future migrateDatabaseIfNeeded(Isar db) async { +Future migrateDatabaseIfNeeded(Isar db, Drift drift) async { + final hasVersion = Store.tryGet(StoreKey.version) != null; final int version = Store.get(StoreKey.version, targetVersion); if (version < 9) { @@ -58,6 +60,12 @@ Future migrateDatabaseIfNeeded(Isar db) async { await Store.put(StoreKey.photoManagerCustomFilter, true); } + // This means that the SQLite DB is just created and has no version + if (version < 14 || !hasVersion) { + await migrateStoreToSqlite(db, drift); + await Store.populateCache(); + } + if (targetVersion >= 12) { await Store.put(StoreKey.version, targetVersion); return; @@ -215,6 +223,39 @@ Future migrateBackupAlbumsToSqlite(Isar db, Drift drift) async { } } +Future migrateStoreToSqlite(Isar db, Drift drift) async { + try { + final isarStoreValues = await db.storeValues.where().findAll(); + await drift.batch((batch) { + for (final storeValue in isarStoreValues) { + final companion = StoreEntityCompanion( + id: Value(storeValue.id), + stringValue: Value(storeValue.strValue), + intValue: Value(storeValue.intValue), + ); + batch.insert(drift.storeEntity, companion, onConflict: DoUpdate((_) => companion)); + } + }); + } catch (error) { + debugPrint("[MIGRATION] Error while migrating store values to SQLite: $error"); + } +} + +Future migrateStoreToIsar(Isar db, Drift drift) async { + try { + final driftStoreValues = await drift.storeEntity + .select() + .map((entity) => StoreValue(entity.id, intValue: entity.intValue, strValue: entity.stringValue)) + .get(); + + await db.writeTxn(() async { + await db.storeValues.putAll(driftStoreValues); + }); + } catch (error) { + debugPrint("[MIGRATION] Error while migrating store values to Isar: $error"); + } +} + class _DeviceAsset { final String assetId; final List? hash; diff --git a/mobile/lib/widgets/settings/beta_timeline_list_tile.dart b/mobile/lib/widgets/settings/beta_timeline_list_tile.dart index 3d41094b7..a49814527 100644 --- a/mobile/lib/widgets/settings/beta_timeline_list_tile.dart +++ b/mobile/lib/widgets/settings/beta_timeline_list_tile.dart @@ -89,7 +89,6 @@ class _BetaTimelineListTileState extends ConsumerState wit ElevatedButton( onPressed: () async { Navigator.of(context).pop(); - await ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.betaTimeline, value); context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: value)]); }, child: Text("ok".t(context: context)), diff --git a/mobile/test/domain/services/log_service_test.dart b/mobile/test/domain/services/log_service_test.dart index b4feac4e2..95f677ba9 100644 --- a/mobile/test/domain/services/log_service_test.dart +++ b/mobile/test/domain/services/log_service_test.dart @@ -64,12 +64,12 @@ void main() { group("Log Service Set Level:", () { setUp(() async { - when(() => mockStoreRepo.insert(StoreKey.logLevel, any())).thenAnswer((_) async => true); + when(() => mockStoreRepo.upsert(StoreKey.logLevel, any())).thenAnswer((_) async => true); await sut.setLogLevel(LogLevel.shout); }); test('Updates the log level in store', () { - final index = verify(() => mockStoreRepo.insert(StoreKey.logLevel, captureAny())).captured.firstOrNull; + final index = verify(() => mockStoreRepo.upsert(StoreKey.logLevel, captureAny())).captured.firstOrNull; expect(index, LogLevel.shout.index); }); diff --git a/mobile/test/domain/services/store_service_test.dart b/mobile/test/domain/services/store_service_test.dart index d23913991..d03e49384 100644 --- a/mobile/test/domain/services/store_service_test.dart +++ b/mobile/test/domain/services/store_service_test.dart @@ -16,11 +16,13 @@ final _kBackupFailedSince = DateTime.utc(2023); void main() { late StoreService sut; late IsarStoreRepository mockStoreRepo; - late StreamController> controller; + late DriftStoreRepository mockDriftStoreRepo; + late StreamController>> controller; setUp(() async { - controller = StreamController>.broadcast(); + controller = StreamController>>.broadcast(); mockStoreRepo = MockStoreRepository(); + mockDriftStoreRepo = MockDriftStoreRepository(); // For generics, we need to provide fallback to each concrete type to avoid runtime errors registerFallbackValue(StoreKey.accessToken); registerFallbackValue(StoreKey.backupTriggerDelay); @@ -37,6 +39,16 @@ void main() { ); when(() => mockStoreRepo.watchAll()).thenAnswer((_) => controller.stream); + when(() => mockDriftStoreRepo.getAll()).thenAnswer( + (_) async => [ + const StoreDto(StoreKey.accessToken, _kAccessToken), + const StoreDto(StoreKey.backgroundBackup, _kBackgroundBackup), + const StoreDto(StoreKey.groupAssetsBy, _kGroupAssetsBy), + StoreDto(StoreKey.backupFailedSince, _kBackupFailedSince), + ], + ); + when(() => mockDriftStoreRepo.watchAll()).thenAnswer((_) => controller.stream); + sut = await StoreService.create(storeRepository: mockStoreRepo); }); @@ -58,7 +70,7 @@ void main() { test('Listens to stream of store updates', () async { final event = StoreDto(StoreKey.accessToken, _kAccessToken.toUpperCase()); - controller.add(event); + controller.add([event]); await pumpEventQueue(); @@ -83,18 +95,19 @@ void main() { group('Store Service put:', () { setUp(() { - when(() => mockStoreRepo.insert(any>(), any())).thenAnswer((_) async => true); + when(() => mockStoreRepo.upsert(any>(), any())).thenAnswer((_) async => true); + when(() => mockDriftStoreRepo.upsert(any>(), any())).thenAnswer((_) async => true); }); test('Skip insert when value is not modified', () async { await sut.put(StoreKey.accessToken, _kAccessToken); - verifyNever(() => mockStoreRepo.insert(StoreKey.accessToken, any())); + verifyNever(() => mockStoreRepo.upsert(StoreKey.accessToken, any())); }); test('Insert value when modified', () async { final newAccessToken = _kAccessToken.toUpperCase(); await sut.put(StoreKey.accessToken, newAccessToken); - verify(() => mockStoreRepo.insert(StoreKey.accessToken, newAccessToken)).called(1); + verify(() => mockStoreRepo.upsert(StoreKey.accessToken, newAccessToken)).called(1); expect(sut.tryGet(StoreKey.accessToken), newAccessToken); }); }); @@ -105,6 +118,7 @@ void main() { setUp(() { valueController = StreamController.broadcast(); when(() => mockStoreRepo.watch(any>())).thenAnswer((_) => valueController.stream); + when(() => mockDriftStoreRepo.watch(any>())).thenAnswer((_) => valueController.stream); }); tearDown(() async { @@ -129,6 +143,7 @@ void main() { group('Store Service delete:', () { setUp(() { when(() => mockStoreRepo.delete(any>())).thenAnswer((_) async => true); + when(() => mockDriftStoreRepo.delete(any>())).thenAnswer((_) async => true); }); test('Removes the value from the DB', () async { @@ -145,6 +160,7 @@ void main() { group('Store Service clear:', () { setUp(() { when(() => mockStoreRepo.deleteAll()).thenAnswer((_) async => true); + when(() => mockDriftStoreRepo.deleteAll()).thenAnswer((_) async => true); }); test('Clears all values from the store', () async { diff --git a/mobile/test/drift/main/generated/schema.dart b/mobile/test/drift/main/generated/schema.dart index 87de9194d30654de6a41431b443a343168c811f4..746206e453eced6a56f266847e3f980880bc5bcf 100644 GIT binary patch delta 94 zcmbQo@rh$YH=}H3Zb5!giGq4@az<)yVtko}UP@w7iMm2!u|k=}=1xX_Cbi_m;#36- qD=q~nC`v6UEy@E*>$xPBBqjl69ZSM2CigJ+a_A^n#9DLJasdFh2pu2* delta 22 ecmeywF^^+IH{<42jJ!;fUotnc##(dLasdEhI0twD diff --git a/mobile/test/drift/main/generated/schema_v8.dart b/mobile/test/drift/main/generated/schema_v8.dart new file mode 100644 index 0000000000000000000000000000000000000000..13520e6372fa5300910501d2174fb154e84b49a0 GIT binary patch literal 215042 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 TG%l}b+C_`uyPBs?|NizL0PCWz literal 0 HcmV?d00001 diff --git a/mobile/test/infrastructure/repositories/store_repository_test.dart b/mobile/test/infrastructure/repositories/store_repository_test.dart index 84d18ad95..f6424beab 100644 --- a/mobile/test/infrastructure/repositories/store_repository_test.dart +++ b/mobile/test/infrastructure/repositories/store_repository_test.dart @@ -44,7 +44,7 @@ void main() { test('converts int', () async { int? version = await sut.tryGet(StoreKey.version); expect(version, isNull); - await sut.insert(StoreKey.version, _kTestVersion); + await sut.upsert(StoreKey.version, _kTestVersion); version = await sut.tryGet(StoreKey.version); expect(version, _kTestVersion); }); @@ -52,7 +52,7 @@ void main() { test('converts string', () async { String? accessToken = await sut.tryGet(StoreKey.accessToken); expect(accessToken, isNull); - await sut.insert(StoreKey.accessToken, _kTestAccessToken); + await sut.upsert(StoreKey.accessToken, _kTestAccessToken); accessToken = await sut.tryGet(StoreKey.accessToken); expect(accessToken, _kTestAccessToken); }); @@ -60,7 +60,7 @@ void main() { test('converts datetime', () async { DateTime? backupFailedSince = await sut.tryGet(StoreKey.backupFailedSince); expect(backupFailedSince, isNull); - await sut.insert(StoreKey.backupFailedSince, _kTestBackupFailed); + await sut.upsert(StoreKey.backupFailedSince, _kTestBackupFailed); backupFailedSince = await sut.tryGet(StoreKey.backupFailedSince); expect(backupFailedSince, _kTestBackupFailed); }); @@ -68,7 +68,7 @@ void main() { test('converts bool', () async { bool? colorfulInterface = await sut.tryGet(StoreKey.colorfulInterface); expect(colorfulInterface, isNull); - await sut.insert(StoreKey.colorfulInterface, _kTestColorfulInterface); + await sut.upsert(StoreKey.colorfulInterface, _kTestColorfulInterface); colorfulInterface = await sut.tryGet(StoreKey.colorfulInterface); expect(colorfulInterface, _kTestColorfulInterface); }); @@ -76,7 +76,7 @@ void main() { test('converts user', () async { UserDto? user = await sut.tryGet(StoreKey.currentUser); expect(user, isNull); - await sut.insert(StoreKey.currentUser, _kTestUser); + await sut.upsert(StoreKey.currentUser, _kTestUser); user = await sut.tryGet(StoreKey.currentUser); expect(user, _kTestUser); }); @@ -108,10 +108,10 @@ void main() { await _populateStore(db); }); - test('update()', () async { + test('upsert()', () async { int? version = await sut.tryGet(StoreKey.version); expect(version, _kTestVersion); - await sut.update(StoreKey.version, _kTestVersion + 10); + await sut.upsert(StoreKey.version, _kTestVersion + 10); version = await sut.tryGet(StoreKey.version); expect(version, _kTestVersion + 10); }); @@ -126,22 +126,29 @@ void main() { final stream = sut.watch(StoreKey.version); expectLater(stream, emitsInOrder([_kTestVersion, _kTestVersion + 10])); await pumpEventQueue(); - await sut.update(StoreKey.version, _kTestVersion + 10); + await sut.upsert(StoreKey.version, _kTestVersion + 10); }); test('watchAll()', () async { final stream = sut.watchAll(); expectLater( stream, - emitsInAnyOrder([ - emits(const StoreDto(StoreKey.version, _kTestVersion)), - emits(StoreDto(StoreKey.backupFailedSince, _kTestBackupFailed)), - emits(const StoreDto(StoreKey.accessToken, _kTestAccessToken)), - emits(const StoreDto(StoreKey.colorfulInterface, _kTestColorfulInterface)), - emits(const StoreDto(StoreKey.version, _kTestVersion + 10)), + emitsInOrder([ + [ + const StoreDto(StoreKey.version, _kTestVersion), + StoreDto(StoreKey.backupFailedSince, _kTestBackupFailed), + const StoreDto(StoreKey.accessToken, _kTestAccessToken), + const StoreDto(StoreKey.colorfulInterface, _kTestColorfulInterface), + ], + [ + const StoreDto(StoreKey.version, _kTestVersion + 10), + StoreDto(StoreKey.backupFailedSince, _kTestBackupFailed), + const StoreDto(StoreKey.accessToken, _kTestAccessToken), + const StoreDto(StoreKey.colorfulInterface, _kTestColorfulInterface), + ], ]), ); - await sut.update(StoreKey.version, _kTestVersion + 10); + await sut.upsert(StoreKey.version, _kTestVersion + 10); }); }); } diff --git a/mobile/test/infrastructure/repository.mock.dart b/mobile/test/infrastructure/repository.mock.dart index 1fe3af689..1b66451dd 100644 --- a/mobile/test/infrastructure/repository.mock.dart +++ b/mobile/test/infrastructure/repository.mock.dart @@ -14,6 +14,8 @@ import 'package:mocktail/mocktail.dart'; class MockStoreRepository extends Mock implements IsarStoreRepository {} +class MockDriftStoreRepository extends Mock implements DriftStoreRepository {} + class MockLogRepository extends Mock implements LogRepository {} class MockIsarUserRepository extends Mock implements IsarUserRepository {}