diff --git a/lib/core/storage/folder_repository.dart b/lib/core/storage/folder_repository.dart index 96afaad0..4a89636b 100644 --- a/lib/core/storage/folder_repository.dart +++ b/lib/core/storage/folder_repository.dart @@ -55,6 +55,32 @@ class FolderRepository { await db.delete('folders'); } + /// Makes the stored folders exactly [names], in one transaction: a failure + /// leaves the old list. Folders that stay keep their connections; the ones + /// that go release theirs first (see [removeFolder]). + Future replaceFolders(List names) async { + final wanted = { + for (final name in names) + if (name.trim().isNotEmpty) name.trim(), + }; + final db = await _open(); + await db.transaction((txn) async { + final rows = await txn.query('folders', columns: ['name']); + final existing = {for (final r in rows) r['name'] as String}; + for (final name in existing.difference(wanted)) { + await txn.rawUpdate( + 'UPDATE connections SET folder_id = NULL WHERE folder_id IN ' + '(SELECT id FROM folders WHERE name = ?)', + [name], + ); + await txn.delete('folders', where: 'name = ?', whereArgs: [name]); + } + for (final name in wanted.difference(existing)) { + await txn.insert('folders', {'name': name}); + } + }); + } + Future getFolderIdByName(String name) async { final db = await _open(); final rows = await db.query('folders', diff --git a/lib/core/storage/folders_storage.dart b/lib/core/storage/folders_storage.dart index 9f6fd44e..c8b4a565 100644 --- a/lib/core/storage/folders_storage.dart +++ b/lib/core/storage/folders_storage.dart @@ -26,11 +26,13 @@ class FoldersStorage { try { await _migrateFromLegacyIfNeeded(); _folders = await LocalDb.instance.getFolders(); + _loaded = true; } catch (e) { + // Not marked loaded: the next call reads again instead of caching an + // empty list for the rest of the session. debugPrint('FoldersStorage.load: $e'); _folders = []; } - _loaded = true; return folders; } @@ -64,11 +66,8 @@ class FoldersStorage { } Future save(List folders) async { - _folders = List.from(folders); - await LocalDb.instance.clearFolders(); - for (final name in _folders) { - await LocalDb.instance.addFolder(name); - } + await LocalDb.instance.replaceFolders(folders); + _folders = await LocalDb.instance.getFolders(); } Future add(String name) async { diff --git a/lib/core/storage/local_db.dart b/lib/core/storage/local_db.dart index 323b477e..18887c8b 100644 --- a/lib/core/storage/local_db.dart +++ b/lib/core/storage/local_db.dart @@ -135,6 +135,9 @@ class LocalDb { Future clearFolders() => folders.clearFolders(); + Future replaceFolders(List names) => + folders.replaceFolders(names); + Future getFolderIdByName(String name) => folders.getFolderIdByName(name); diff --git a/test/core/storage/storage_repositories_test.dart b/test/core/storage/storage_repositories_test.dart index 6983c08d..c87e4889 100644 --- a/test/core/storage/storage_repositories_test.dart +++ b/test/core/storage/storage_repositories_test.dart @@ -51,6 +51,33 @@ void main() { expect(rows.single['folder_id'], isNull); }); + test('folders: replaceFolders is all-or-nothing and keeps the connections', + () async { + final folders = FolderRepository(open); + await folders.addFolder('work'); + await folders.addFolder('home'); + final workId = await folders.getFolderIdByName('work'); + final homeId = await folders.getFolderIdByName('home'); + final inWork = await insertConnection(folderId: workId); + final inHome = await insertConnection(folderId: homeId); + + await folders.replaceFolders(['work', ' new ', '', 'work']); + + expect(await folders.getFolders(), ['new', 'work']); + expect(await folders.getFolderIdByName('work'), workId); + final kept = await db.query('connections', where: 'id = ?', whereArgs: [inWork]); + expect(kept.single['folder_id'], workId); + final released = await db.query('connections', where: 'id = ?', whereArgs: [inHome]); + expect(released.single['folder_id'], isNull); + + // A name the table refuses mid-way rolls the whole replace back. + await db.execute( + 'CREATE TRIGGER no_boom BEFORE INSERT ON folders ' + "WHEN NEW.name = 'boom' BEGIN SELECT RAISE(ABORT, 'boom'); END"); + await expectLater(folders.replaceFolders(['zzz', 'boom']), throwsA(anything)); + expect(await folders.getFolders(), ['new', 'work']); + }); + test('app settings: set, replace, delete and ER layouts per scope', () async { final settings = AppSettingsRepository(open); expect(await settings.getAppSetting('k'), isNull);