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:
shenlong
2025-08-22 01:28:50 +05:30
committed by GitHub
parent ed3997d844
commit 6f4f79d8cc
26 changed files with 7907 additions and 169 deletions
@@ -64,12 +64,12 @@ void main() {
group("Log Service Set Level:", () {
setUp(() async {
when(() => mockStoreRepo.insert<int>(StoreKey.logLevel, any())).thenAnswer((_) async => true);
when(() => mockStoreRepo.upsert<int>(StoreKey.logLevel, any())).thenAnswer((_) async => true);
await sut.setLogLevel(LogLevel.shout);
});
test('Updates the log level in store', () {
final index = verify(() => mockStoreRepo.insert<int>(StoreKey.logLevel, captureAny())).captured.firstOrNull;
final index = verify(() => mockStoreRepo.upsert<int>(StoreKey.logLevel, captureAny())).captured.firstOrNull;
expect(index, LogLevel.shout.index);
});
@@ -16,11 +16,13 @@ final _kBackupFailedSince = DateTime.utc(2023);
void main() {
late StoreService sut;
late IsarStoreRepository mockStoreRepo;
late StreamController<StoreDto<Object>> controller;
late DriftStoreRepository mockDriftStoreRepo;
late StreamController<List<StoreDto<Object>>> controller;
setUp(() async {
controller = StreamController<StoreDto<Object>>.broadcast();
controller = StreamController<List<StoreDto<Object>>>.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<String>(any<StoreKey<String>>(), any())).thenAnswer((_) async => true);
when(() => mockStoreRepo.upsert<String>(any<StoreKey<String>>(), any())).thenAnswer((_) async => true);
when(() => mockDriftStoreRepo.upsert<String>(any<StoreKey<String>>(), any())).thenAnswer((_) async => true);
});
test('Skip insert when value is not modified', () async {
await sut.put(StoreKey.accessToken, _kAccessToken);
verifyNever(() => mockStoreRepo.insert<String>(StoreKey.accessToken, any()));
verifyNever(() => mockStoreRepo.upsert<String>(StoreKey.accessToken, any()));
});
test('Insert value when modified', () async {
final newAccessToken = _kAccessToken.toUpperCase();
await sut.put(StoreKey.accessToken, newAccessToken);
verify(() => mockStoreRepo.insert<String>(StoreKey.accessToken, newAccessToken)).called(1);
verify(() => mockStoreRepo.upsert<String>(StoreKey.accessToken, newAccessToken)).called(1);
expect(sut.tryGet(StoreKey.accessToken), newAccessToken);
});
});
@@ -105,6 +118,7 @@ void main() {
setUp(() {
valueController = StreamController<String?>.broadcast();
when(() => mockStoreRepo.watch<String>(any<StoreKey<String>>())).thenAnswer((_) => valueController.stream);
when(() => mockDriftStoreRepo.watch<String>(any<StoreKey<String>>())).thenAnswer((_) => valueController.stream);
});
tearDown(() async {
@@ -129,6 +143,7 @@ void main() {
group('Store Service delete:', () {
setUp(() {
when(() => mockStoreRepo.delete<String>(any<StoreKey<String>>())).thenAnswer((_) async => true);
when(() => mockDriftStoreRepo.delete<String>(any<StoreKey<String>>())).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 {