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 <alex.tran1502@gmail.com>
This commit is contained in:
@@ -1,16 +1,30 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.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/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
class IsarStoreRepository extends IsarDatabaseRepository {
|
||||
// Temporary interface until Isar is removed to make the service work with both Isar and Sqlite
|
||||
abstract class IStoreRepository {
|
||||
Future<bool> deleteAll();
|
||||
Stream<List<StoreDto<Object>>> watchAll();
|
||||
Future<void> delete<T>(StoreKey<T> key);
|
||||
Future<bool> upsert<T>(StoreKey<T> key, T value);
|
||||
Future<T?> tryGet<T>(StoreKey<T> key);
|
||||
Stream<T?> watch<T>(StoreKey<T> key);
|
||||
Future<List<StoreDto<Object>>> 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<bool> deleteAll() async {
|
||||
return await transaction(() async {
|
||||
await _db.storeValues.clear();
|
||||
@@ -18,25 +32,29 @@ class IsarStoreRepository extends IsarDatabaseRepository {
|
||||
});
|
||||
}
|
||||
|
||||
Stream<StoreDto<Object>> watchAll() {
|
||||
@override
|
||||
Stream<List<StoreDto<Object>>> 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<void> delete<T>(StoreKey<T> key) async {
|
||||
return await transaction(() async => await _db.storeValues.delete(key.id));
|
||||
}
|
||||
|
||||
Future<bool> insert<T>(StoreKey<T> key, T value) async {
|
||||
@override
|
||||
Future<bool> upsert<T>(StoreKey<T> key, T value) async {
|
||||
return await transaction(() async {
|
||||
await _db.storeValues.put(await _fromValue(key, value));
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<T?> tryGet<T>(StoreKey<T> 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<bool> update<T>(StoreKey<T> key, T value) async {
|
||||
return await transaction(() async {
|
||||
await _db.storeValues.put(await _fromValue(key, value));
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<T?> watch<T>(StoreKey<T> 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<List<StoreDto<Object>>> 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<bool> deleteAll() async {
|
||||
await _db.storeEntity.deleteAll();
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<StoreDto<Object>>> getAll() async {
|
||||
final query = _db.storeEntity.select()..where((entity) => entity.id.isIn(validStoreKeys));
|
||||
return query.asyncMap((entity) => _toUpdateEvent(entity)).get();
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<List<StoreDto<Object>>> watchAll() {
|
||||
final query = _db.storeEntity.select()..where((entity) => entity.id.isIn(validStoreKeys));
|
||||
|
||||
return query.asyncMap((entity) => _toUpdateEvent(entity)).watch();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> delete<T>(StoreKey<T> key) async {
|
||||
await _db.storeEntity.deleteWhere((entity) => entity.id.equals(key.id));
|
||||
return;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> upsert<T>(StoreKey<T> key, T value) async {
|
||||
await _db.storeEntity.insertOnConflictUpdate(await _fromValue(key, value));
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<T?> tryGet<T>(StoreKey<T> 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<T?> watch<T>(StoreKey<T> 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<StoreDto<Object>> _toUpdateEvent(StoreEntityData entity) async {
|
||||
final key = StoreKey.values.firstWhere((e) => e.id == entity.id) as StoreKey<Object>;
|
||||
final value = await _toValue(key, entity);
|
||||
return StoreDto(key, value);
|
||||
}
|
||||
|
||||
Future<T?> _toValue<T>(StoreKey<T> 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<StoreEntityCompanion> _fromValue<T>(StoreKey<T> 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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user