From 063decd4512cd7d8d8d4c8ff5b3b00b896b8817a Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Sat, 2 May 2026 05:58:38 +0200 Subject: [PATCH] make tab restore more resilient --- .../drift_schemas/tabs/drift_schema_v9.json | 535 ++++++ .../geckoview/domain/repositories/tab.dart | 112 +- .../geckoview/domain/repositories/tab.g.dart | 2 +- .../features/tabs/data/database/daos/tab.dart | 131 +- .../features/tabs/data/database/database.dart | 5 +- .../tabs/data/database/database.drift.dart | 6 + .../tabs/data/database/database.steps.dart | 130 ++ .../tabs/data/database/definitions.drift | 12 + .../tabs/data/database/definitions.drift.dart | 369 ++++ .../test/drift/tabs/generated/schema.dart | 5 +- .../test/drift/tabs/generated/schema_v9.dart | 1583 +++++++++++++++++ 11 files changed, 2861 insertions(+), 29 deletions(-) create mode 100644 apps/weblibre/drift_schemas/tabs/drift_schema_v9.json create mode 100644 apps/weblibre/test/drift/tabs/generated/schema_v9.dart diff --git a/apps/weblibre/drift_schemas/tabs/drift_schema_v9.json b/apps/weblibre/drift_schemas/tabs/drift_schema_v9.json new file mode 100644 index 00000000..0283a642 --- /dev/null +++ b/apps/weblibre/drift_schemas/tabs/drift_schema_v9.json @@ -0,0 +1,535 @@ +{ + "_meta": { + "description": "This file contains a serialized version of schema entities for drift.", + "version": "1.3.0" + }, + "options": { + "store_date_time_values_as_text": false + }, + "entities": [ + { + "id": 0, + "references": [], + "type": "table", + "data": { + "name": "container", + "was_declared_in_moor": true, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": "PRIMARY KEY NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "primary-key" + ] + }, + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": true, + "customConstraints": "", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "color", + "getter_name": "color", + "moor_type": "int", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const ColorConverter()", + "dart_type_name": "Color" + } + }, + { + "name": "metadata", + "getter_name": "metadata", + "moor_type": "string", + "nullable": true, + "customConstraints": "", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const ContainerMetadataConverter()", + "dart_type_name": "ContainerMetadata" + } + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 1, + "references": [ + 0 + ], + "type": "table", + "data": { + "name": "tab", + "was_declared_in_moor": true, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": "PRIMARY KEY NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "primary-key" + ] + }, + { + "name": "source", + "getter_name": "source", + "moor_type": "int", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(TabSource.values)", + "dart_type_name": "TabSource" + } + }, + { + "name": "parent_id", + "getter_name": "parentId", + "moor_type": "string", + "nullable": true, + "customConstraints": "REFERENCES tab(id)ON DELETE SET NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "container_id", + "getter_name": "containerId", + "moor_type": "string", + "nullable": true, + "customConstraints": "REFERENCES container(id)ON DELETE CASCADE", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "container", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "order_key", + "getter_name": "orderKey", + "moor_type": "string", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "url", + "getter_name": "url", + "moor_type": "string", + "nullable": true, + "customConstraints": "", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const UriConverterNullable()", + "dart_type_name": "Uri?" + } + }, + { + "name": "title", + "getter_name": "title", + "moor_type": "string", + "nullable": true, + "customConstraints": "", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "tab_mode", + "getter_name": "tabMode", + "moor_type": "int", + "nullable": false, + "customConstraints": "NOT NULL DEFAULT 0", + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(TabModeDbValue.values)", + "dart_type_name": "TabModeDbValue" + } + }, + { + "name": "isolation_context_id", + "getter_name": "isolationContextId", + "moor_type": "string", + "nullable": true, + "customConstraints": "", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_pinned", + "getter_name": "isPinned", + "moor_type": "bool", + "nullable": false, + "customConstraints": "NOT NULL DEFAULT 0", + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_probably_readerable", + "getter_name": "isProbablyReaderable", + "moor_type": "bool", + "nullable": true, + "customConstraints": "", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "extracted_content_markdown", + "getter_name": "extractedContentMarkdown", + "moor_type": "string", + "nullable": true, + "customConstraints": "", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "extracted_content_plain", + "getter_name": "extractedContentPlain", + "moor_type": "string", + "nullable": true, + "customConstraints": "", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "full_content_markdown", + "getter_name": "fullContentMarkdown", + "moor_type": "string", + "nullable": true, + "customConstraints": "", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "full_content_plain", + "getter_name": "fullContentPlain", + "moor_type": "string", + "nullable": true, + "customConstraints": "", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "timestamp", + "getter_name": "timestamp", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [ + "CHECK((tab_mode = 2 AND isolation_context_id IS NOT NULL)OR(tab_mode != 2 AND isolation_context_id IS NULL))" + ] + } + }, + { + "id": 2, + "references": [], + "type": "table", + "data": { + "name": "closed_tab_tombstone", + "was_declared_in_moor": true, + "columns": [ + { + "name": "tab_id", + "getter_name": "tabId", + "moor_type": "string", + "nullable": false, + "customConstraints": "PRIMARY KEY NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "primary-key" + ] + }, + { + "name": "closed_at", + "getter_name": "closedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 3, + "references": [ + 1 + ], + "type": "index", + "data": { + "on": 1, + "name": "idx_tab_parent_container", + "sql": "CREATE INDEX idx_tab_parent_container ON tab(parent_id, container_id);", + "unique": false, + "columns": [] + } + }, + { + "id": 4, + "references": [ + 1 + ], + "type": "table", + "data": { + "name": "tab_fts", + "was_declared_in_moor": true, + "columns": [ + { + "name": "title", + "getter_name": "title", + "moor_type": "string", + "nullable": false, + "customConstraints": "", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "url", + "getter_name": "url", + "moor_type": "string", + "nullable": false, + "customConstraints": "", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "extracted_content_plain", + "getter_name": "extractedContentPlain", + "moor_type": "string", + "nullable": false, + "customConstraints": "", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "full_content_plain", + "getter_name": "fullContentPlain", + "moor_type": "string", + "nullable": false, + "customConstraints": "", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": true, + "create_virtual_stmt": "CREATE VIRTUAL TABLE \"tab_fts\" USING fts5(title, url, extracted_content_plain, full_content_plain, content=tab, tokenize=\"trigram\")", + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 5, + "references": [ + 1 + ], + "type": "trigger", + "data": { + "on": 1, + "references_in_body": [ + 1 + ], + "name": "tab_maintain_parent_chain_on_delete", + "sql": "CREATE TRIGGER tab_maintain_parent_chain_on_delete BEFORE DELETE ON tab BEGIN\n -- Update all children of the deleted row to point to its parent (grandparent)\n -- Only if the grandparent exists, otherwise set to NULL\n UPDATE tab\n SET parent_id = CASE\n WHEN OLD.parent_id IS NOT NULL AND\n EXISTS(SELECT 1 FROM tab WHERE id = OLD.parent_id)\n THEN OLD.parent_id\n ELSE NULL\n END\n WHERE parent_id = OLD.id;\nEND;" + } + }, + { + "id": 6, + "references": [ + 1, + 4 + ], + "type": "trigger", + "data": { + "on": 1, + "references_in_body": [ + 1, + 4 + ], + "name": "tab_after_insert", + "sql": "CREATE TRIGGER tab_after_insert AFTER INSERT ON tab BEGIN\n INSERT INTO \n tab_fts(rowid, title, url, extracted_content_plain, full_content_plain) \n VALUES (new.rowid, new.title, new.url, new.extracted_content_plain, new.full_content_plain);\nEND;" + } + }, + { + "id": 7, + "references": [ + 1, + 4 + ], + "type": "trigger", + "data": { + "on": 1, + "references_in_body": [ + 1, + 4 + ], + "name": "tab_after_delete", + "sql": "CREATE TRIGGER tab_after_delete AFTER DELETE ON tab BEGIN\n INSERT INTO \n tab_fts(tab_fts, rowid, title, url, extracted_content_plain, full_content_plain) \n VALUES('delete', old.rowid, old.title, old.url, old.extracted_content_plain, old.full_content_plain);\nEND;" + } + }, + { + "id": 8, + "references": [ + 1, + 4 + ], + "type": "trigger", + "data": { + "on": 1, + "references_in_body": [ + 1, + 4 + ], + "name": "tab_after_update", + "sql": "CREATE TRIGGER tab_after_update AFTER UPDATE ON tab BEGIN\n INSERT INTO \n tab_fts(tab_fts, rowid, title, url, extracted_content_plain, full_content_plain) \n VALUES('delete', old.rowid, old.title, old.url, old.extracted_content_plain, old.full_content_plain);\n INSERT INTO \n tab_fts(rowid, title, url, extracted_content_plain, full_content_plain) \n VALUES (new.rowid, new.title, new.url, new.extracted_content_plain, new.full_content_plain);\nEND;" + } + } + ], + "fixed_sql": [ + { + "name": "container", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"container\" (\"id\" TEXT PRIMARY KEY NOT NULL, \"name\" TEXT, \"color\" INTEGER NOT NULL, \"metadata\" TEXT);" + } + ] + }, + { + "name": "tab", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"tab\" (\"id\" TEXT PRIMARY KEY NOT NULL, \"source\" INTEGER NOT NULL, \"parent_id\" TEXT REFERENCES tab(id)ON DELETE SET NULL, \"container_id\" TEXT REFERENCES container(id)ON DELETE CASCADE, \"order_key\" TEXT NOT NULL, \"url\" TEXT, \"title\" TEXT, \"tab_mode\" INTEGER NOT NULL DEFAULT 0, \"isolation_context_id\" TEXT, \"is_pinned\" INTEGER NOT NULL DEFAULT 0, \"is_probably_readerable\" INTEGER, \"extracted_content_markdown\" TEXT, \"extracted_content_plain\" TEXT, \"full_content_markdown\" TEXT, \"full_content_plain\" TEXT, \"timestamp\" INTEGER NOT NULL, CHECK((tab_mode = 2 AND isolation_context_id IS NOT NULL)OR(tab_mode != 2 AND isolation_context_id IS NULL)));" + } + ] + }, + { + "name": "closed_tab_tombstone", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"closed_tab_tombstone\" (\"tab_id\" TEXT PRIMARY KEY NOT NULL, \"closed_at\" INTEGER NOT NULL);" + } + ] + }, + { + "name": "idx_tab_parent_container", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX idx_tab_parent_container ON tab (parent_id, container_id)" + } + ] + }, + { + "name": "tab_fts", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE VIRTUAL TABLE IF NOT EXISTS \"tab_fts\" USING fts5(title, url, extracted_content_plain, full_content_plain, content=tab, tokenize=\"trigram\");" + } + ] + }, + { + "name": "tab_maintain_parent_chain_on_delete", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TRIGGER tab_maintain_parent_chain_on_delete BEFORE DELETE ON tab BEGIN UPDATE tab SET parent_id = CASE WHEN OLD.parent_id IS NOT NULL AND EXISTS (SELECT 1 FROM tab WHERE id = OLD.parent_id) THEN OLD.parent_id ELSE NULL END WHERE parent_id = OLD.id;END" + } + ] + }, + { + "name": "tab_after_insert", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TRIGGER tab_after_insert AFTER INSERT ON tab BEGIN INSERT INTO tab_fts (\"rowid\", title, url, extracted_content_plain, full_content_plain) VALUES (new.\"rowid\", new.title, new.url, new.extracted_content_plain, new.full_content_plain);END" + } + ] + }, + { + "name": "tab_after_delete", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TRIGGER tab_after_delete AFTER DELETE ON tab BEGIN INSERT INTO tab_fts (tab_fts, \"rowid\", title, url, extracted_content_plain, full_content_plain) VALUES ('delete', old.\"rowid\", old.title, old.url, old.extracted_content_plain, old.full_content_plain);END" + } + ] + }, + { + "name": "tab_after_update", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TRIGGER tab_after_update AFTER UPDATE ON tab BEGIN INSERT INTO tab_fts (tab_fts, \"rowid\", title, url, extracted_content_plain, full_content_plain) VALUES ('delete', old.\"rowid\", old.title, old.url, old.extracted_content_plain, old.full_content_plain);INSERT INTO tab_fts (\"rowid\", title, url, extracted_content_plain, full_content_plain) VALUES (new.\"rowid\", new.title, new.url, new.extracted_content_plain, new.full_content_plain);END" + } + ] + } + ] +} \ No newline at end of file diff --git a/apps/weblibre/lib/features/geckoview/domain/repositories/tab.dart b/apps/weblibre/lib/features/geckoview/domain/repositories/tab.dart index 619928fb..85c83ba2 100644 --- a/apps/weblibre/lib/features/geckoview/domain/repositories/tab.dart +++ b/apps/weblibre/lib/features/geckoview/domain/repositories/tab.dart @@ -52,6 +52,10 @@ part 'tab.g.dart'; @Riverpod(keepAlive: true) class TabRepository extends _$TabRepository { final _tabsService = GeckoTabService(); + final _sessionStartedAt = DateTime.now(); + bool _didPruneTombstones = false; + bool _reclosing = false; + bool _suppressNextReclose = false; final _tabFromIntent = {}; final _closeLock = Lock(); @@ -550,38 +554,28 @@ class TabRepository extends _$TabRepository { } } - Future closeTab(String tabId) { + Future _closeTabsInternal( + List tabIds, { + required bool recordTombstones, + }) { return _closeLock.synchronized(() async { - // Collect isolation context before close - final isolationContextId = ref - .read(tabStatesProvider)[tabId] - ?.isolationContextId; - - if (ref.read(selectedTabProvider) == tabId) { - await _selectNextTab(tabId); + if (tabIds.isEmpty) { + return; } - await _preservePromotedChildOrderOnClose([tabId]); - - await _tabsService.removeTab(tabId: tabId); - - // Queue isolation cleanup — actual cleanup runs after syncTabs - // deletes the DB row, so the count check is accurate. - if (isolationContextId != null) { - _pendingIsolationCleanup.add(isolationContextId); + if (recordTombstones) { + await ref + .read(tabDatabaseProvider) + .tabDao + .addClosedTabTombstones(tabIds); } - }); - } - Future closeTabs(List tabIds) { - return _closeLock.synchronized(() async { - // Collect isolation contexts from tabs being closed for (final tabId in tabIds) { - final contextId = ref + final isolationContextId = ref .read(tabStatesProvider)[tabId] ?.isolationContextId; - if (contextId != null) { - _pendingIsolationCleanup.add(contextId); + if (isolationContextId != null) { + _pendingIsolationCleanup.add(isolationContextId); } } @@ -592,10 +586,29 @@ class TabRepository extends _$TabRepository { await _preservePromotedChildOrderOnClose(tabIds); - await _tabsService.removeTabs(ids: tabIds); + if (tabIds.length == 1) { + await _tabsService.removeTab(tabId: tabIds.single); + } else { + await _tabsService.removeTabs(ids: tabIds); + } }); } + Future closeTab(String tabId) { + return _closeTabsInternal([tabId], recordTombstones: true); + } + + Future closeTabs(List tabIds) { + return _closeTabsInternal(tabIds, recordTombstones: true); + } + + Future _clearTombstonesForCurrentTabs(List tabIds) { + return ref + .read(tabDatabaseProvider) + .tabDao + .deleteClosedTabTombstones(tabIds); + } + Future _preservePromotedChildOrderOnClose(List tabIds) { return ref .read(tabDatabaseProvider) @@ -662,9 +675,49 @@ class TabRepository extends _$TabRepository { } Future undoClose() { + // Suppress the next reclose pass: undo can resurrect a tab whose + // tombstone is still on disk (from a previous session); without this + // flag the listener would immediately re-close it. + _suppressNextReclose = true; return _tabsService.undo(); } + Future _recloseRestoredClosedTabs(List tabIds) async { + if (_reclosing) { + return false; + } + _reclosing = true; + try { + final tabDao = ref.read(tabDatabaseProvider).tabDao; + + if (!_didPruneTombstones) { + await tabDao.pruneExpiredClosedTabTombstones(); + _didPruneTombstones = true; + } + + final restoredClosedTabIds = await tabDao.getStartupRestoredClosedTabIds( + tabIds, + sessionStartedAt: _sessionStartedAt, + ); + + if (restoredClosedTabIds.isEmpty) { + return false; + } + + // recordTombstones: false — the tombstone already exists from the + // original close; rewriting it would bump closed_at into this session + // and disable the resurrection check on the next emission. + await _closeTabsInternal( + restoredClosedTabIds.toList(growable: false), + recordTombstones: false, + ); + + return true; + } finally { + _reclosing = false; + } + } + /// Cleans up isolation contexts from previous crashed sessions. /// Called once after tab list stabilizes on startup. // Future _cleanupOrphanedIsolationContexts() async { @@ -845,6 +898,15 @@ class TabRepository extends _$TabRepository { ref.listen( tabListProvider, (previous, next) async { + if (_suppressNextReclose) { + _suppressNextReclose = false; + // Drop tombstones for the tabs that just came back via undo so + // future emissions don't treat them as resurrections. + await _clearTombstonesForCurrentTabs(next.value); + } else if (await _recloseRestoredClosedTabs(next.value)) { + return; + } + //Only sync tabs if there has been a previous value or is not empty final shouldSyncTabs = next.value.isNotEmpty || (previous?.value.isNotEmpty ?? false); diff --git a/apps/weblibre/lib/features/geckoview/domain/repositories/tab.g.dart b/apps/weblibre/lib/features/geckoview/domain/repositories/tab.g.dart index 96178a72..c540608d 100644 --- a/apps/weblibre/lib/features/geckoview/domain/repositories/tab.g.dart +++ b/apps/weblibre/lib/features/geckoview/domain/repositories/tab.g.dart @@ -41,7 +41,7 @@ final class TabRepositoryProvider } } -String _$tabRepositoryHash() => r'4c5bda4d0ddcc66cfa420d3c2ca97db42b73b878'; +String _$tabRepositoryHash() => r'84ecfed72f17c367fc7bf0100f6367d4e7609596'; abstract class _$TabRepository extends $Notifier { void build(); diff --git a/apps/weblibre/lib/features/geckoview/features/tabs/data/database/daos/tab.dart b/apps/weblibre/lib/features/geckoview/features/tabs/data/database/daos/tab.dart index 1487af8a..c56e2c2a 100644 --- a/apps/weblibre/lib/features/geckoview/features/tabs/data/database/daos/tab.dart +++ b/apps/weblibre/lib/features/geckoview/features/tabs/data/database/daos/tab.dart @@ -47,6 +47,8 @@ class TabDao extends DatabaseAccessor with $TabDaoMixin { final _undoHistory = {}; Timer? _clearHistoryTimer; + static const closedTabTombstoneTtl = Duration(hours: 24); + TabDao(super.db); UpdateStatement _updateByIdStatement(String id) => @@ -317,6 +319,103 @@ class TabDao extends DatabaseAccessor with $TabDaoMixin { return statement.write(TabCompanion(containerId: Value(containerId))); } + Future addClosedTabTombstones( + Iterable tabIds, { + DateTime? closedAt, + }) async { + final uniqueIds = tabIds.toSet(); + if (uniqueIds.isEmpty) { + return; + } + + final effectiveClosedAt = closedAt ?? DateTime.now(); + + await batch((batch) { + for (final tabId in uniqueIds) { + batch.insert( + db.closedTabTombstone, + ClosedTabTombstoneCompanion.insert( + tabId: tabId, + closedAt: effectiveClosedAt, + ), + onConflict: DoUpdate( + (_) => + ClosedTabTombstoneCompanion(closedAt: Value(effectiveClosedAt)), + ), + ); + } + }); + } + + Future deleteClosedTabTombstones(Iterable tabIds) { + final uniqueIds = tabIds.toSet(); + if (uniqueIds.isEmpty) { + return Future.value(); + } + + return (db.closedTabTombstone.delete() + ..where((t) => t.tabId.isIn(uniqueIds))) + .go(); + } + + Future pruneExpiredClosedTabTombstones({ + Duration ttl = closedTabTombstoneTtl, + DateTime? now, + }) { + final cutoff = (now ?? DateTime.now()).subtract(ttl); + + return (db.closedTabTombstone.delete() + ..where((t) => t.closedAt.isSmallerOrEqualValue(cutoff))) + .go(); + } + + Future> getStartupRestoredClosedTabIds( + Iterable tabIds, { + required DateTime sessionStartedAt, + Duration ttl = closedTabTombstoneTtl, + DateTime? now, + }) async { + final uniqueIds = tabIds.toSet(); + if (uniqueIds.isEmpty) { + return const {}; + } + + final freshnessCutoff = (now ?? DateTime.now()).subtract(ttl); + final query = selectOnly(db.closedTabTombstone) + ..addColumns([db.closedTabTombstone.tabId]) + ..where( + db.closedTabTombstone.tabId.isIn(uniqueIds) & + db.closedTabTombstone.closedAt.isSmallerThanValue( + sessionStartedAt, + ) & + db.closedTabTombstone.closedAt.isBiggerOrEqualValue( + freshnessCutoff, + ), + ); + + return query + .map((row) => row.read(db.closedTabTombstone.tabId)!) + .get() + .then((rows) => rows.toSet()); + } + + Future> _containerIdsByContextualIdentity( + Iterable contextIds, + ) async { + final uniqueContextIds = contextIds.toSet(); + if (uniqueContextIds.isEmpty) { + return const {}; + } + + final rows = await db.definitionsDrift + .containerIdsByContextualIdentities( + contextIds: uniqueContextIds.toList(growable: false), + ) + .get(); + + return {for (final row in rows) row.contextualIdentity: row.id}; + } + Future reorderTabs({ required List movingTabIds, required String? previousTabId, @@ -633,6 +732,32 @@ class TabDao extends DatabaseAccessor with $TabDaoMixin { parentIdsToValidate, ).get().then((ids) => ids.toSet()); + final containerRepairCandidates = { + for (final state in next.values) + if (state.contextId != null && + (previous?[state.id]?.contextId != state.contextId || + previous?[state.id] == null)) + state.id: state.contextId!, + }; + final currentContainerIds = containerRepairCandidates.isEmpty + ? const {} + : await getTabsContainerId( + containerRepairCandidates.keys, + ).get().then(Map.fromEntries); + final repairableContexts = containerRepairCandidates.entries + .where((entry) => currentContainerIds[entry.key] == null) + .map((entry) => entry.value) + .toSet(); + final containerIdsByContext = await _containerIdsByContextualIdentity( + repairableContexts, + ); + final repairedContainerIds = { + for (final entry in containerRepairCandidates.entries) + if (currentContainerIds[entry.key] == null) + if (containerIdsByContext[entry.value] case final containerId?) + entry.key: containerId, + }; + // Complete validation map for (final state in next.values) { final previousState = previous?[state.id]; @@ -658,13 +783,17 @@ class TabDao extends DatabaseAccessor with $TabDaoMixin { previousState.url != state.url || previousState.title != state.title || previousState.parentId != state.parentId || - previousState.tabMode != state.tabMode) { + previousState.tabMode != state.tabMode || + repairedContainerIds.containsKey(state.id)) { batch.update( db.tab, TabCompanion( parentId: validatedParentIds.containsKey(state.id) ? Value(validatedParentIds[state.id]) : const Value.absent(), + containerId: + repairedContainerIds[state.id].mapNotNull(Value.new) ?? + const Value.absent(), url: (previousState?.url != state.url) ? Value(state.url) : const Value.absent(), diff --git a/apps/weblibre/lib/features/geckoview/features/tabs/data/database/database.dart b/apps/weblibre/lib/features/geckoview/features/tabs/data/database/database.dart index f8e50044..df23b2df 100644 --- a/apps/weblibre/lib/features/geckoview/features/tabs/data/database/database.dart +++ b/apps/weblibre/lib/features/geckoview/features/tabs/data/database/database.dart @@ -31,7 +31,7 @@ import 'package:weblibre/features/search/domain/fts_tokenizer.dart'; @DriftDatabase(include: {'definitions.drift'}, daos: [ContainerDao, TabDao]) class TabDatabase extends $TabDatabase with TrigramQueryBuilderMixin { @override - final int schemaVersion = 8; + final int schemaVersion = 9; @override final int ftsTokenLimit = 10; @@ -137,5 +137,8 @@ class TabDatabase extends $TabDatabase with TrigramQueryBuilderMixin { await m.createIndex(schema.idxTabParentContainer); }, + from8To9: (m, schema) async { + await m.create(schema.closedTabTombstone); + }, ); } diff --git a/apps/weblibre/lib/features/geckoview/features/tabs/data/database/database.drift.dart b/apps/weblibre/lib/features/geckoview/features/tabs/data/database/database.drift.dart index fc5e08ef..a56537f9 100644 --- a/apps/weblibre/lib/features/geckoview/features/tabs/data/database/database.drift.dart +++ b/apps/weblibre/lib/features/geckoview/features/tabs/data/database/database.drift.dart @@ -17,6 +17,9 @@ abstract class $TabDatabase extends i0.GeneratedDatabase { $TabDatabaseManager get managers => $TabDatabaseManager(this); late final i1.Container container = i1.Container(this); late final i1.Tab tab = i1.Tab(this); + late final i1.ClosedTabTombstone closedTabTombstone = i1.ClosedTabTombstone( + this, + ); late final i1.TabFts tabFts = i1.TabFts(this); late final i2.ContainerDao containerDao = i2.ContainerDao( this as i3.TabDatabase, @@ -32,6 +35,7 @@ abstract class $TabDatabase extends i0.GeneratedDatabase { List get allSchemaEntities => [ container, tab, + closedTabTombstone, i1.idxTabParentContainer, tabFts, i1.tabMaintainParentChainOnDelete, @@ -86,6 +90,8 @@ class $TabDatabaseManager { i1.$ContainerTableManager get container => i1.$ContainerTableManager(_db, _db.container); i1.$TabTableManager get tab => i1.$TabTableManager(_db, _db.tab); + i1.$ClosedTabTombstoneTableManager get closedTabTombstone => + i1.$ClosedTabTombstoneTableManager(_db, _db.closedTabTombstone); i1.$TabFtsTableManager get tabFts => i1.$TabFtsTableManager(_db, _db.tabFts); } diff --git a/apps/weblibre/lib/features/geckoview/features/tabs/data/database/database.steps.dart b/apps/weblibre/lib/features/geckoview/features/tabs/data/database/database.steps.dart index e012b20b..ea914b40 100644 --- a/apps/weblibre/lib/features/geckoview/features/tabs/data/database/database.steps.dart +++ b/apps/weblibre/lib/features/geckoview/features/tabs/data/database/database.steps.dart @@ -809,6 +809,128 @@ final class Schema8 extends i0.VersionedSchema { ); } +final class Schema9 extends i0.VersionedSchema { + Schema9({required super.database}) : super(version: 9); + @override + late final List entities = [ + container, + tab, + closedTabTombstone, + idxTabParentContainer, + tabFts, + tabMaintainParentChainOnDelete, + tabAfterInsert, + tabAfterDelete, + tabAfterUpdate, + ]; + late final Shape0 container = Shape0( + source: i0.VersionedTable( + entityName: 'container', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [_column_0, _column_1, _column_2, _column_3], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape5 tab = Shape5( + source: i0.VersionedTable( + entityName: 'tab', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'CHECK((tab_mode = 2 AND isolation_context_id IS NOT NULL)OR(tab_mode != 2 AND isolation_context_id IS NULL))', + ], + columns: [ + _column_0, + _column_16, + _column_4, + _column_5, + _column_6, + _column_7, + _column_8, + _column_17, + _column_18, + _column_19, + _column_10, + _column_11, + _column_12, + _column_13, + _column_14, + _column_15, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape6 closedTabTombstone = Shape6( + source: i0.VersionedTable( + entityName: 'closed_tab_tombstone', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [_column_20, _column_21], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxTabParentContainer = i1.Index( + 'idx_tab_parent_container', + 'CREATE INDEX idx_tab_parent_container ON tab (parent_id, container_id)', + ); + late final Shape2 tabFts = Shape2( + source: i0.VersionedVirtualTable( + entityName: 'tab_fts', + moduleAndArgs: + 'fts5(title, url, extracted_content_plain, full_content_plain, content=tab, tokenize="trigram")', + columns: [_column_8, _column_7, _column_12, _column_14], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Trigger tabMaintainParentChainOnDelete = i1.Trigger( + 'CREATE TRIGGER tab_maintain_parent_chain_on_delete BEFORE DELETE ON tab BEGIN UPDATE tab SET parent_id = CASE WHEN OLD.parent_id IS NOT NULL AND EXISTS (SELECT 1 FROM tab WHERE id = OLD.parent_id) THEN OLD.parent_id ELSE NULL END WHERE parent_id = OLD.id;END', + 'tab_maintain_parent_chain_on_delete', + ); + final i1.Trigger tabAfterInsert = i1.Trigger( + 'CREATE TRIGGER tab_after_insert AFTER INSERT ON tab BEGIN INSERT INTO tab_fts ("rowid", title, url, extracted_content_plain, full_content_plain) VALUES (new."rowid", new.title, new.url, new.extracted_content_plain, new.full_content_plain);END', + 'tab_after_insert', + ); + final i1.Trigger tabAfterDelete = i1.Trigger( + 'CREATE TRIGGER tab_after_delete AFTER DELETE ON tab BEGIN INSERT INTO tab_fts (tab_fts, "rowid", title, url, extracted_content_plain, full_content_plain) VALUES (\'delete\', old."rowid", old.title, old.url, old.extracted_content_plain, old.full_content_plain);END', + 'tab_after_delete', + ); + final i1.Trigger tabAfterUpdate = i1.Trigger( + 'CREATE TRIGGER tab_after_update AFTER UPDATE ON tab BEGIN INSERT INTO tab_fts (tab_fts, "rowid", title, url, extracted_content_plain, full_content_plain) VALUES (\'delete\', old."rowid", old.title, old.url, old.extracted_content_plain, old.full_content_plain);INSERT INTO tab_fts ("rowid", title, url, extracted_content_plain, full_content_plain) VALUES (new."rowid", new.title, new.url, new.extracted_content_plain, new.full_content_plain);END', + 'tab_after_update', + ); +} + +class Shape6 extends i0.VersionedTable { + Shape6({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get tabId => + columnsByName['tab_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get closedAt => + columnsByName['closed_at']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_20(String aliasedName) => + i1.GeneratedColumn( + 'tab_id', + aliasedName, + false, + type: i1.DriftSqlType.string, + $customConstraints: 'PRIMARY KEY NOT NULL', + ); +i1.GeneratedColumn _column_21(String aliasedName) => + i1.GeneratedColumn( + 'closed_at', + aliasedName, + false, + type: i1.DriftSqlType.int, + $customConstraints: 'NOT NULL', + ); i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema3 schema) from2To3, required Future Function(i1.Migrator m, Schema4 schema) from3To4, @@ -816,6 +938,7 @@ i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema6 schema) from5To6, required Future Function(i1.Migrator m, Schema7 schema) from6To7, required Future Function(i1.Migrator m, Schema8 schema) from7To8, + required Future Function(i1.Migrator m, Schema9 schema) from8To9, }) { return (currentVersion, database) async { switch (currentVersion) { @@ -849,6 +972,11 @@ i0.MigrationStepWithVersion migrationSteps({ final migrator = i1.Migrator(database, schema); await from7To8(migrator, schema); return 8; + case 8: + final schema = Schema9(database: database); + final migrator = i1.Migrator(database, schema); + await from8To9(migrator, schema); + return 9; default: throw ArgumentError.value('Unknown migration from $currentVersion'); } @@ -862,6 +990,7 @@ i1.OnUpgrade stepByStep({ required Future Function(i1.Migrator m, Schema6 schema) from5To6, required Future Function(i1.Migrator m, Schema7 schema) from6To7, required Future Function(i1.Migrator m, Schema8 schema) from7To8, + required Future Function(i1.Migrator m, Schema9 schema) from8To9, }) => i0.VersionedSchema.stepByStepHelper( step: migrationSteps( from2To3: from2To3, @@ -870,5 +999,6 @@ i1.OnUpgrade stepByStep({ from5To6: from5To6, from6To7: from6To7, from7To8: from7To8, + from8To9: from8To9, ), ); diff --git a/apps/weblibre/lib/features/geckoview/features/tabs/data/database/definitions.drift b/apps/weblibre/lib/features/geckoview/features/tabs/data/database/definitions.drift index cc3a6910..ced61f9c 100644 --- a/apps/weblibre/lib/features/geckoview/features/tabs/data/database/definitions.drift +++ b/apps/weblibre/lib/features/geckoview/features/tabs/data/database/definitions.drift @@ -37,6 +37,11 @@ CREATE TABLE tab( ) ); +CREATE TABLE closed_tab_tombstone( + tab_id TEXT PRIMARY KEY NOT NULL, + closed_at DATETIME NOT NULL +); + -- Composite index used by `tabsWithRootAndDepth` (the seed checks -- `parent_id IS NULL OR NOT EXISTS(... WHERE p.id = parent_id AND -- p.container_id IS :container_id)`) and by `lastChildTabId` @@ -390,6 +395,13 @@ containerByContextualIdentity: WHERE container.metadata ->> '$.contextualIdentity' = :contextId LIMIT 1; +containerIdsByContextualIdentities: + SELECT + id, + CAST(container.metadata ->> '$.contextualIdentity' AS TEXT) AS contextual_identity + FROM container + WHERE CAST(container.metadata ->> '$.contextualIdentity' AS TEXT) IN :contextIds; + containersToClearOnExit: SELECT container.metadata ->> '$.contextualIdentity' AS contextual_identity FROM container diff --git a/apps/weblibre/lib/features/geckoview/features/tabs/data/database/definitions.drift.dart b/apps/weblibre/lib/features/geckoview/features/tabs/data/database/definitions.drift.dart index 42ac9f16..e7cc496d 100644 --- a/apps/weblibre/lib/features/geckoview/features/tabs/data/database/definitions.drift.dart +++ b/apps/weblibre/lib/features/geckoview/features/tabs/data/database/definitions.drift.dart @@ -882,6 +882,159 @@ typedef $TabProcessedTableManager = i3.TabData, i0.PrefetchHooks Function({bool containerId}) >; +typedef $ClosedTabTombstoneCreateCompanionBuilder = + i3.ClosedTabTombstoneCompanion Function({ + required String tabId, + required DateTime closedAt, + i0.Value rowid, + }); +typedef $ClosedTabTombstoneUpdateCompanionBuilder = + i3.ClosedTabTombstoneCompanion Function({ + i0.Value tabId, + i0.Value closedAt, + i0.Value rowid, + }); + +class $ClosedTabTombstoneFilterComposer + extends i0.Composer { + $ClosedTabTombstoneFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnFilters get tabId => $composableBuilder( + column: $table.tabId, + builder: (column) => i0.ColumnFilters(column), + ); + + i0.ColumnFilters get closedAt => $composableBuilder( + column: $table.closedAt, + builder: (column) => i0.ColumnFilters(column), + ); +} + +class $ClosedTabTombstoneOrderingComposer + extends i0.Composer { + $ClosedTabTombstoneOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnOrderings get tabId => $composableBuilder( + column: $table.tabId, + builder: (column) => i0.ColumnOrderings(column), + ); + + i0.ColumnOrderings get closedAt => $composableBuilder( + column: $table.closedAt, + builder: (column) => i0.ColumnOrderings(column), + ); +} + +class $ClosedTabTombstoneAnnotationComposer + extends i0.Composer { + $ClosedTabTombstoneAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.GeneratedColumn get tabId => + $composableBuilder(column: $table.tabId, builder: (column) => column); + + i0.GeneratedColumn get closedAt => + $composableBuilder(column: $table.closedAt, builder: (column) => column); +} + +class $ClosedTabTombstoneTableManager + extends + i0.RootTableManager< + i0.GeneratedDatabase, + i3.ClosedTabTombstone, + i3.ClosedTabTombstoneData, + i3.$ClosedTabTombstoneFilterComposer, + i3.$ClosedTabTombstoneOrderingComposer, + i3.$ClosedTabTombstoneAnnotationComposer, + $ClosedTabTombstoneCreateCompanionBuilder, + $ClosedTabTombstoneUpdateCompanionBuilder, + ( + i3.ClosedTabTombstoneData, + i0.BaseReferences< + i0.GeneratedDatabase, + i3.ClosedTabTombstone, + i3.ClosedTabTombstoneData + >, + ), + i3.ClosedTabTombstoneData, + i0.PrefetchHooks Function() + > { + $ClosedTabTombstoneTableManager( + i0.GeneratedDatabase db, + i3.ClosedTabTombstone table, + ) : super( + i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + i3.$ClosedTabTombstoneFilterComposer($db: db, $table: table), + createOrderingComposer: () => + i3.$ClosedTabTombstoneOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + i3.$ClosedTabTombstoneAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + i0.Value tabId = const i0.Value.absent(), + i0.Value closedAt = const i0.Value.absent(), + i0.Value rowid = const i0.Value.absent(), + }) => i3.ClosedTabTombstoneCompanion( + tabId: tabId, + closedAt: closedAt, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String tabId, + required DateTime closedAt, + i0.Value rowid = const i0.Value.absent(), + }) => i3.ClosedTabTombstoneCompanion.insert( + tabId: tabId, + closedAt: closedAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), i0.BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + ), + ); +} + +typedef $ClosedTabTombstoneProcessedTableManager = + i0.ProcessedTableManager< + i0.GeneratedDatabase, + i3.ClosedTabTombstone, + i3.ClosedTabTombstoneData, + i3.$ClosedTabTombstoneFilterComposer, + i3.$ClosedTabTombstoneOrderingComposer, + i3.$ClosedTabTombstoneAnnotationComposer, + $ClosedTabTombstoneCreateCompanionBuilder, + $ClosedTabTombstoneUpdateCompanionBuilder, + ( + i3.ClosedTabTombstoneData, + i0.BaseReferences< + i0.GeneratedDatabase, + i3.ClosedTabTombstone, + i3.ClosedTabTombstoneData + >, + ), + i3.ClosedTabTombstoneData, + i0.PrefetchHooks Function() + >; typedef $TabFtsCreateCompanionBuilder = i3.TabFtsCompanion Function({ required String title, @@ -2060,6 +2213,196 @@ class TabCompanion extends i0.UpdateCompanion { } } +class ClosedTabTombstone extends i0.Table + with i0.TableInfo { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + ClosedTabTombstone(this.attachedDatabase, [this._alias]); + late final i0.GeneratedColumn tabId = i0.GeneratedColumn( + 'tab_id', + aliasedName, + false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'PRIMARY KEY NOT NULL', + ); + late final i0.GeneratedColumn closedAt = + i0.GeneratedColumn( + 'closed_at', + aliasedName, + false, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + @override + List get $columns => [tabId, closedAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'closed_tab_tombstone'; + @override + Set get $primaryKey => {tabId}; + @override + i3.ClosedTabTombstoneData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i3.ClosedTabTombstoneData( + tabId: attachedDatabase.typeMapping.read( + i0.DriftSqlType.string, + data['${effectivePrefix}tab_id'], + )!, + closedAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, + data['${effectivePrefix}closed_at'], + )!, + ); + } + + @override + ClosedTabTombstone createAlias(String alias) { + return ClosedTabTombstone(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class ClosedTabTombstoneData extends i0.DataClass + implements i0.Insertable { + final String tabId; + final DateTime closedAt; + const ClosedTabTombstoneData({required this.tabId, required this.closedAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['tab_id'] = i0.Variable(tabId); + map['closed_at'] = i0.Variable(closedAt); + return map; + } + + factory ClosedTabTombstoneData.fromJson( + Map json, { + i0.ValueSerializer? serializer, + }) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return ClosedTabTombstoneData( + tabId: serializer.fromJson(json['tab_id']), + closedAt: serializer.fromJson(json['closed_at']), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'tab_id': serializer.toJson(tabId), + 'closed_at': serializer.toJson(closedAt), + }; + } + + i3.ClosedTabTombstoneData copyWith({String? tabId, DateTime? closedAt}) => + i3.ClosedTabTombstoneData( + tabId: tabId ?? this.tabId, + closedAt: closedAt ?? this.closedAt, + ); + ClosedTabTombstoneData copyWithCompanion( + i3.ClosedTabTombstoneCompanion data, + ) { + return ClosedTabTombstoneData( + tabId: data.tabId.present ? data.tabId.value : this.tabId, + closedAt: data.closedAt.present ? data.closedAt.value : this.closedAt, + ); + } + + @override + String toString() { + return (StringBuffer('ClosedTabTombstoneData(') + ..write('tabId: $tabId, ') + ..write('closedAt: $closedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(tabId, closedAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i3.ClosedTabTombstoneData && + other.tabId == this.tabId && + other.closedAt == this.closedAt); +} + +class ClosedTabTombstoneCompanion + extends i0.UpdateCompanion { + final i0.Value tabId; + final i0.Value closedAt; + final i0.Value rowid; + const ClosedTabTombstoneCompanion({ + this.tabId = const i0.Value.absent(), + this.closedAt = const i0.Value.absent(), + this.rowid = const i0.Value.absent(), + }); + ClosedTabTombstoneCompanion.insert({ + required String tabId, + required DateTime closedAt, + this.rowid = const i0.Value.absent(), + }) : tabId = i0.Value(tabId), + closedAt = i0.Value(closedAt); + static i0.Insertable custom({ + i0.Expression? tabId, + i0.Expression? closedAt, + i0.Expression? rowid, + }) { + return i0.RawValuesInsertable({ + if (tabId != null) 'tab_id': tabId, + if (closedAt != null) 'closed_at': closedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + i3.ClosedTabTombstoneCompanion copyWith({ + i0.Value? tabId, + i0.Value? closedAt, + i0.Value? rowid, + }) { + return i3.ClosedTabTombstoneCompanion( + tabId: tabId ?? this.tabId, + closedAt: closedAt ?? this.closedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (tabId.present) { + map['tab_id'] = i0.Variable(tabId.value); + } + if (closedAt.present) { + map['closed_at'] = i0.Variable(closedAt.value); + } + if (rowid.present) { + map['rowid'] = i0.Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ClosedTabTombstoneCompanion(') + ..write('tabId: $tabId, ') + ..write('closedAt: $closedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + i0.Index get idxTabParentContainer => i0.Index( 'idx_tab_parent_container', 'CREATE INDEX idx_tab_parent_container ON tab (parent_id, container_id)', @@ -2643,6 +2986,23 @@ class DefinitionsDrift extends i9.ModularAccessor { ).asyncMap(container.mapFromRow); } + i0.Selectable + containerIdsByContextualIdentities({required List contextIds}) { + var $arrayStartIndex = 1; + final expandedcontextIds = $expandVar($arrayStartIndex, contextIds.length); + $arrayStartIndex += contextIds.length; + return customSelect( + 'SELECT id, CAST(container.metadata ->> \'\$.contextualIdentity\' AS TEXT) AS contextual_identity FROM container WHERE CAST(container.metadata ->> \'\$.contextualIdentity\' AS TEXT) IN ($expandedcontextIds)', + variables: [for (var $ in contextIds) i0.Variable($)], + readsFrom: {container}, + ).map( + (i0.QueryRow row) => ContainerIdsByContextualIdentitiesResult( + id: row.read('id'), + contextualIdentity: row.read('contextual_identity'), + ), + ); + } + i0.Selectable containersToClearOnExit() { return customSelect( 'SELECT container.metadata ->> \'\$.contextualIdentity\' AS contextual_identity FROM container WHERE json_extract(container.metadata, \'\$.clearDataOnExit\') = 1 AND container.metadata ->> \'\$.contextualIdentity\' IS NOT NULL', @@ -2727,6 +3087,15 @@ class UnorderedTabDescendantsResult { UnorderedTabDescendantsResult({required this.id, this.parentId}); } +class ContainerIdsByContextualIdentitiesResult { + final String id; + final String contextualIdentity; + ContainerIdsByContextualIdentitiesResult({ + required this.id, + required this.contextualIdentity, + }); +} + class IsolatedContextContainerPairsResult { final String? isolationContextId; final String? containerId; diff --git a/apps/weblibre/test/drift/tabs/generated/schema.dart b/apps/weblibre/test/drift/tabs/generated/schema.dart index 87fbc35b..2fe66d7a 100644 --- a/apps/weblibre/test/drift/tabs/generated/schema.dart +++ b/apps/weblibre/test/drift/tabs/generated/schema.dart @@ -11,6 +11,7 @@ import 'schema_v5.dart' as v5; import 'schema_v6.dart' as v6; import 'schema_v7.dart' as v7; import 'schema_v8.dart' as v8; +import 'schema_v9.dart' as v9; class GeneratedHelper implements SchemaInstantiationHelper { @override @@ -30,10 +31,12 @@ class GeneratedHelper implements SchemaInstantiationHelper { return v7.DatabaseAtV7(db); case 8: return v8.DatabaseAtV8(db); + case 9: + return v9.DatabaseAtV9(db); default: throw MissingSchemaException(version, versions); } } - static const versions = const [2, 3, 4, 5, 6, 7, 8]; + static const versions = const [2, 3, 4, 5, 6, 7, 8, 9]; } diff --git a/apps/weblibre/test/drift/tabs/generated/schema_v9.dart b/apps/weblibre/test/drift/tabs/generated/schema_v9.dart new file mode 100644 index 00000000..b544d0e1 --- /dev/null +++ b/apps/weblibre/test/drift/tabs/generated/schema_v9.dart @@ -0,0 +1,1583 @@ +// dart format width=80 +// GENERATED BY drift_dev, DO NOT MODIFY. +// ignore_for_file: type=lint,unused_import +// +import 'package:drift/drift.dart'; + +class Container extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Container(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'PRIMARY KEY NOT NULL', + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: '', + ); + late final GeneratedColumn color = GeneratedColumn( + 'color', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn metadata = GeneratedColumn( + 'metadata', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: '', + ); + @override + List get $columns => [id, name, color, metadata]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'container'; + @override + Set get $primaryKey => {id}; + @override + ContainerData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ContainerData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + ), + color: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}color'], + )!, + metadata: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}metadata'], + ), + ); + } + + @override + Container createAlias(String alias) { + return Container(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class ContainerData extends DataClass implements Insertable { + final String id; + final String? name; + final int color; + final String? metadata; + const ContainerData({ + required this.id, + this.name, + required this.color, + this.metadata, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + if (!nullToAbsent || name != null) { + map['name'] = Variable(name); + } + map['color'] = Variable(color); + if (!nullToAbsent || metadata != null) { + map['metadata'] = Variable(metadata); + } + return map; + } + + factory ContainerData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ContainerData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + color: serializer.fromJson(json['color']), + metadata: serializer.fromJson(json['metadata']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'color': serializer.toJson(color), + 'metadata': serializer.toJson(metadata), + }; + } + + ContainerData copyWith({ + String? id, + Value name = const Value.absent(), + int? color, + Value metadata = const Value.absent(), + }) => ContainerData( + id: id ?? this.id, + name: name.present ? name.value : this.name, + color: color ?? this.color, + metadata: metadata.present ? metadata.value : this.metadata, + ); + ContainerData copyWithCompanion(ContainerCompanion data) { + return ContainerData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + color: data.color.present ? data.color.value : this.color, + metadata: data.metadata.present ? data.metadata.value : this.metadata, + ); + } + + @override + String toString() { + return (StringBuffer('ContainerData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('color: $color, ') + ..write('metadata: $metadata') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, name, color, metadata); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ContainerData && + other.id == this.id && + other.name == this.name && + other.color == this.color && + other.metadata == this.metadata); +} + +class ContainerCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value color; + final Value metadata; + final Value rowid; + const ContainerCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.color = const Value.absent(), + this.metadata = const Value.absent(), + this.rowid = const Value.absent(), + }); + ContainerCompanion.insert({ + required String id, + this.name = const Value.absent(), + required int color, + this.metadata = const Value.absent(), + this.rowid = const Value.absent(), + }) : id = Value(id), + color = Value(color); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? color, + Expression? metadata, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (color != null) 'color': color, + if (metadata != null) 'metadata': metadata, + if (rowid != null) 'rowid': rowid, + }); + } + + ContainerCompanion copyWith({ + Value? id, + Value? name, + Value? color, + Value? metadata, + Value? rowid, + }) { + return ContainerCompanion( + id: id ?? this.id, + name: name ?? this.name, + color: color ?? this.color, + metadata: metadata ?? this.metadata, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (color.present) { + map['color'] = Variable(color.value); + } + if (metadata.present) { + map['metadata'] = Variable(metadata.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ContainerCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('color: $color, ') + ..write('metadata: $metadata, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Tab extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Tab(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'PRIMARY KEY NOT NULL', + ); + late final GeneratedColumn source = GeneratedColumn( + 'source', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn parentId = GeneratedColumn( + 'parent_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'REFERENCES tab(id)ON DELETE SET NULL', + ); + late final GeneratedColumn containerId = GeneratedColumn( + 'container_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'REFERENCES container(id)ON DELETE CASCADE', + ); + late final GeneratedColumn orderKey = GeneratedColumn( + 'order_key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn url = GeneratedColumn( + 'url', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: '', + ); + late final GeneratedColumn title = GeneratedColumn( + 'title', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: '', + ); + late final GeneratedColumn tabMode = GeneratedColumn( + 'tab_mode', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn isolationContextId = + GeneratedColumn( + 'isolation_context_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: '', + ); + late final GeneratedColumn isPinned = GeneratedColumn( + 'is_pinned', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn isProbablyReaderable = GeneratedColumn( + 'is_probably_readerable', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: '', + ); + late final GeneratedColumn extractedContentMarkdown = + GeneratedColumn( + 'extracted_content_markdown', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: '', + ); + late final GeneratedColumn extractedContentPlain = + GeneratedColumn( + 'extracted_content_plain', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: '', + ); + late final GeneratedColumn fullContentMarkdown = + GeneratedColumn( + 'full_content_markdown', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: '', + ); + late final GeneratedColumn fullContentPlain = GeneratedColumn( + 'full_content_plain', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: '', + ); + late final GeneratedColumn timestamp = GeneratedColumn( + 'timestamp', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + @override + List get $columns => [ + id, + source, + parentId, + containerId, + orderKey, + url, + title, + tabMode, + isolationContextId, + isPinned, + isProbablyReaderable, + extractedContentMarkdown, + extractedContentPlain, + fullContentMarkdown, + fullContentPlain, + timestamp, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'tab'; + @override + Set get $primaryKey => {id}; + @override + TabData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return TabData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + source: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}source'], + )!, + parentId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}parent_id'], + ), + containerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}container_id'], + ), + orderKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}order_key'], + )!, + url: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}url'], + ), + title: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}title'], + ), + tabMode: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}tab_mode'], + )!, + isolationContextId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}isolation_context_id'], + ), + isPinned: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_pinned'], + )!, + isProbablyReaderable: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_probably_readerable'], + ), + extractedContentMarkdown: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}extracted_content_markdown'], + ), + extractedContentPlain: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}extracted_content_plain'], + ), + fullContentMarkdown: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}full_content_markdown'], + ), + fullContentPlain: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}full_content_plain'], + ), + timestamp: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}timestamp'], + )!, + ); + } + + @override + Tab createAlias(String alias) { + return Tab(attachedDatabase, alias); + } + + @override + List get customConstraints => const [ + 'CHECK((tab_mode = 2 AND isolation_context_id IS NOT NULL)OR(tab_mode != 2 AND isolation_context_id IS NULL))', + ]; + @override + bool get dontWriteConstraints => true; +} + +class TabData extends DataClass implements Insertable { + final String id; + final int source; + final String? parentId; + final String? containerId; + final String orderKey; + final String? url; + final String? title; + final int tabMode; + final String? isolationContextId; + final int isPinned; + final int? isProbablyReaderable; + final String? extractedContentMarkdown; + final String? extractedContentPlain; + final String? fullContentMarkdown; + final String? fullContentPlain; + final int timestamp; + const TabData({ + required this.id, + required this.source, + this.parentId, + this.containerId, + required this.orderKey, + this.url, + this.title, + required this.tabMode, + this.isolationContextId, + required this.isPinned, + this.isProbablyReaderable, + this.extractedContentMarkdown, + this.extractedContentPlain, + this.fullContentMarkdown, + this.fullContentPlain, + required this.timestamp, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['source'] = Variable(source); + if (!nullToAbsent || parentId != null) { + map['parent_id'] = Variable(parentId); + } + if (!nullToAbsent || containerId != null) { + map['container_id'] = Variable(containerId); + } + map['order_key'] = Variable(orderKey); + if (!nullToAbsent || url != null) { + map['url'] = Variable(url); + } + if (!nullToAbsent || title != null) { + map['title'] = Variable(title); + } + map['tab_mode'] = Variable(tabMode); + if (!nullToAbsent || isolationContextId != null) { + map['isolation_context_id'] = Variable(isolationContextId); + } + map['is_pinned'] = Variable(isPinned); + if (!nullToAbsent || isProbablyReaderable != null) { + map['is_probably_readerable'] = Variable(isProbablyReaderable); + } + if (!nullToAbsent || extractedContentMarkdown != null) { + map['extracted_content_markdown'] = Variable( + extractedContentMarkdown, + ); + } + if (!nullToAbsent || extractedContentPlain != null) { + map['extracted_content_plain'] = Variable(extractedContentPlain); + } + if (!nullToAbsent || fullContentMarkdown != null) { + map['full_content_markdown'] = Variable(fullContentMarkdown); + } + if (!nullToAbsent || fullContentPlain != null) { + map['full_content_plain'] = Variable(fullContentPlain); + } + map['timestamp'] = Variable(timestamp); + return map; + } + + factory TabData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return TabData( + id: serializer.fromJson(json['id']), + source: serializer.fromJson(json['source']), + parentId: serializer.fromJson(json['parentId']), + containerId: serializer.fromJson(json['containerId']), + orderKey: serializer.fromJson(json['orderKey']), + url: serializer.fromJson(json['url']), + title: serializer.fromJson(json['title']), + tabMode: serializer.fromJson(json['tabMode']), + isolationContextId: serializer.fromJson( + json['isolationContextId'], + ), + isPinned: serializer.fromJson(json['isPinned']), + isProbablyReaderable: serializer.fromJson( + json['isProbablyReaderable'], + ), + extractedContentMarkdown: serializer.fromJson( + json['extractedContentMarkdown'], + ), + extractedContentPlain: serializer.fromJson( + json['extractedContentPlain'], + ), + fullContentMarkdown: serializer.fromJson( + json['fullContentMarkdown'], + ), + fullContentPlain: serializer.fromJson(json['fullContentPlain']), + timestamp: serializer.fromJson(json['timestamp']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'source': serializer.toJson(source), + 'parentId': serializer.toJson(parentId), + 'containerId': serializer.toJson(containerId), + 'orderKey': serializer.toJson(orderKey), + 'url': serializer.toJson(url), + 'title': serializer.toJson(title), + 'tabMode': serializer.toJson(tabMode), + 'isolationContextId': serializer.toJson(isolationContextId), + 'isPinned': serializer.toJson(isPinned), + 'isProbablyReaderable': serializer.toJson(isProbablyReaderable), + 'extractedContentMarkdown': serializer.toJson( + extractedContentMarkdown, + ), + 'extractedContentPlain': serializer.toJson( + extractedContentPlain, + ), + 'fullContentMarkdown': serializer.toJson(fullContentMarkdown), + 'fullContentPlain': serializer.toJson(fullContentPlain), + 'timestamp': serializer.toJson(timestamp), + }; + } + + TabData copyWith({ + String? id, + int? source, + Value parentId = const Value.absent(), + Value containerId = const Value.absent(), + String? orderKey, + Value url = const Value.absent(), + Value title = const Value.absent(), + int? tabMode, + Value isolationContextId = const Value.absent(), + int? isPinned, + Value isProbablyReaderable = const Value.absent(), + Value extractedContentMarkdown = const Value.absent(), + Value extractedContentPlain = const Value.absent(), + Value fullContentMarkdown = const Value.absent(), + Value fullContentPlain = const Value.absent(), + int? timestamp, + }) => TabData( + id: id ?? this.id, + source: source ?? this.source, + parentId: parentId.present ? parentId.value : this.parentId, + containerId: containerId.present ? containerId.value : this.containerId, + orderKey: orderKey ?? this.orderKey, + url: url.present ? url.value : this.url, + title: title.present ? title.value : this.title, + tabMode: tabMode ?? this.tabMode, + isolationContextId: isolationContextId.present + ? isolationContextId.value + : this.isolationContextId, + isPinned: isPinned ?? this.isPinned, + isProbablyReaderable: isProbablyReaderable.present + ? isProbablyReaderable.value + : this.isProbablyReaderable, + extractedContentMarkdown: extractedContentMarkdown.present + ? extractedContentMarkdown.value + : this.extractedContentMarkdown, + extractedContentPlain: extractedContentPlain.present + ? extractedContentPlain.value + : this.extractedContentPlain, + fullContentMarkdown: fullContentMarkdown.present + ? fullContentMarkdown.value + : this.fullContentMarkdown, + fullContentPlain: fullContentPlain.present + ? fullContentPlain.value + : this.fullContentPlain, + timestamp: timestamp ?? this.timestamp, + ); + TabData copyWithCompanion(TabCompanion data) { + return TabData( + id: data.id.present ? data.id.value : this.id, + source: data.source.present ? data.source.value : this.source, + parentId: data.parentId.present ? data.parentId.value : this.parentId, + containerId: data.containerId.present + ? data.containerId.value + : this.containerId, + orderKey: data.orderKey.present ? data.orderKey.value : this.orderKey, + url: data.url.present ? data.url.value : this.url, + title: data.title.present ? data.title.value : this.title, + tabMode: data.tabMode.present ? data.tabMode.value : this.tabMode, + isolationContextId: data.isolationContextId.present + ? data.isolationContextId.value + : this.isolationContextId, + isPinned: data.isPinned.present ? data.isPinned.value : this.isPinned, + isProbablyReaderable: data.isProbablyReaderable.present + ? data.isProbablyReaderable.value + : this.isProbablyReaderable, + extractedContentMarkdown: data.extractedContentMarkdown.present + ? data.extractedContentMarkdown.value + : this.extractedContentMarkdown, + extractedContentPlain: data.extractedContentPlain.present + ? data.extractedContentPlain.value + : this.extractedContentPlain, + fullContentMarkdown: data.fullContentMarkdown.present + ? data.fullContentMarkdown.value + : this.fullContentMarkdown, + fullContentPlain: data.fullContentPlain.present + ? data.fullContentPlain.value + : this.fullContentPlain, + timestamp: data.timestamp.present ? data.timestamp.value : this.timestamp, + ); + } + + @override + String toString() { + return (StringBuffer('TabData(') + ..write('id: $id, ') + ..write('source: $source, ') + ..write('parentId: $parentId, ') + ..write('containerId: $containerId, ') + ..write('orderKey: $orderKey, ') + ..write('url: $url, ') + ..write('title: $title, ') + ..write('tabMode: $tabMode, ') + ..write('isolationContextId: $isolationContextId, ') + ..write('isPinned: $isPinned, ') + ..write('isProbablyReaderable: $isProbablyReaderable, ') + ..write('extractedContentMarkdown: $extractedContentMarkdown, ') + ..write('extractedContentPlain: $extractedContentPlain, ') + ..write('fullContentMarkdown: $fullContentMarkdown, ') + ..write('fullContentPlain: $fullContentPlain, ') + ..write('timestamp: $timestamp') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + source, + parentId, + containerId, + orderKey, + url, + title, + tabMode, + isolationContextId, + isPinned, + isProbablyReaderable, + extractedContentMarkdown, + extractedContentPlain, + fullContentMarkdown, + fullContentPlain, + timestamp, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TabData && + other.id == this.id && + other.source == this.source && + other.parentId == this.parentId && + other.containerId == this.containerId && + other.orderKey == this.orderKey && + other.url == this.url && + other.title == this.title && + other.tabMode == this.tabMode && + other.isolationContextId == this.isolationContextId && + other.isPinned == this.isPinned && + other.isProbablyReaderable == this.isProbablyReaderable && + other.extractedContentMarkdown == this.extractedContentMarkdown && + other.extractedContentPlain == this.extractedContentPlain && + other.fullContentMarkdown == this.fullContentMarkdown && + other.fullContentPlain == this.fullContentPlain && + other.timestamp == this.timestamp); +} + +class TabCompanion extends UpdateCompanion { + final Value id; + final Value source; + final Value parentId; + final Value containerId; + final Value orderKey; + final Value url; + final Value title; + final Value tabMode; + final Value isolationContextId; + final Value isPinned; + final Value isProbablyReaderable; + final Value extractedContentMarkdown; + final Value extractedContentPlain; + final Value fullContentMarkdown; + final Value fullContentPlain; + final Value timestamp; + final Value rowid; + const TabCompanion({ + this.id = const Value.absent(), + this.source = const Value.absent(), + this.parentId = const Value.absent(), + this.containerId = const Value.absent(), + this.orderKey = const Value.absent(), + this.url = const Value.absent(), + this.title = const Value.absent(), + this.tabMode = const Value.absent(), + this.isolationContextId = const Value.absent(), + this.isPinned = const Value.absent(), + this.isProbablyReaderable = const Value.absent(), + this.extractedContentMarkdown = const Value.absent(), + this.extractedContentPlain = const Value.absent(), + this.fullContentMarkdown = const Value.absent(), + this.fullContentPlain = const Value.absent(), + this.timestamp = const Value.absent(), + this.rowid = const Value.absent(), + }); + TabCompanion.insert({ + required String id, + required int source, + this.parentId = const Value.absent(), + this.containerId = const Value.absent(), + required String orderKey, + this.url = const Value.absent(), + this.title = const Value.absent(), + this.tabMode = const Value.absent(), + this.isolationContextId = const Value.absent(), + this.isPinned = const Value.absent(), + this.isProbablyReaderable = const Value.absent(), + this.extractedContentMarkdown = const Value.absent(), + this.extractedContentPlain = const Value.absent(), + this.fullContentMarkdown = const Value.absent(), + this.fullContentPlain = const Value.absent(), + required int timestamp, + this.rowid = const Value.absent(), + }) : id = Value(id), + source = Value(source), + orderKey = Value(orderKey), + timestamp = Value(timestamp); + static Insertable custom({ + Expression? id, + Expression? source, + Expression? parentId, + Expression? containerId, + Expression? orderKey, + Expression? url, + Expression? title, + Expression? tabMode, + Expression? isolationContextId, + Expression? isPinned, + Expression? isProbablyReaderable, + Expression? extractedContentMarkdown, + Expression? extractedContentPlain, + Expression? fullContentMarkdown, + Expression? fullContentPlain, + Expression? timestamp, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (source != null) 'source': source, + if (parentId != null) 'parent_id': parentId, + if (containerId != null) 'container_id': containerId, + if (orderKey != null) 'order_key': orderKey, + if (url != null) 'url': url, + if (title != null) 'title': title, + if (tabMode != null) 'tab_mode': tabMode, + if (isolationContextId != null) + 'isolation_context_id': isolationContextId, + if (isPinned != null) 'is_pinned': isPinned, + if (isProbablyReaderable != null) + 'is_probably_readerable': isProbablyReaderable, + if (extractedContentMarkdown != null) + 'extracted_content_markdown': extractedContentMarkdown, + if (extractedContentPlain != null) + 'extracted_content_plain': extractedContentPlain, + if (fullContentMarkdown != null) + 'full_content_markdown': fullContentMarkdown, + if (fullContentPlain != null) 'full_content_plain': fullContentPlain, + if (timestamp != null) 'timestamp': timestamp, + if (rowid != null) 'rowid': rowid, + }); + } + + TabCompanion copyWith({ + Value? id, + Value? source, + Value? parentId, + Value? containerId, + Value? orderKey, + Value? url, + Value? title, + Value? tabMode, + Value? isolationContextId, + Value? isPinned, + Value? isProbablyReaderable, + Value? extractedContentMarkdown, + Value? extractedContentPlain, + Value? fullContentMarkdown, + Value? fullContentPlain, + Value? timestamp, + Value? rowid, + }) { + return TabCompanion( + id: id ?? this.id, + source: source ?? this.source, + parentId: parentId ?? this.parentId, + containerId: containerId ?? this.containerId, + orderKey: orderKey ?? this.orderKey, + url: url ?? this.url, + title: title ?? this.title, + tabMode: tabMode ?? this.tabMode, + isolationContextId: isolationContextId ?? this.isolationContextId, + isPinned: isPinned ?? this.isPinned, + isProbablyReaderable: isProbablyReaderable ?? this.isProbablyReaderable, + extractedContentMarkdown: + extractedContentMarkdown ?? this.extractedContentMarkdown, + extractedContentPlain: + extractedContentPlain ?? this.extractedContentPlain, + fullContentMarkdown: fullContentMarkdown ?? this.fullContentMarkdown, + fullContentPlain: fullContentPlain ?? this.fullContentPlain, + timestamp: timestamp ?? this.timestamp, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (source.present) { + map['source'] = Variable(source.value); + } + if (parentId.present) { + map['parent_id'] = Variable(parentId.value); + } + if (containerId.present) { + map['container_id'] = Variable(containerId.value); + } + if (orderKey.present) { + map['order_key'] = Variable(orderKey.value); + } + if (url.present) { + map['url'] = Variable(url.value); + } + if (title.present) { + map['title'] = Variable(title.value); + } + if (tabMode.present) { + map['tab_mode'] = Variable(tabMode.value); + } + if (isolationContextId.present) { + map['isolation_context_id'] = Variable(isolationContextId.value); + } + if (isPinned.present) { + map['is_pinned'] = Variable(isPinned.value); + } + if (isProbablyReaderable.present) { + map['is_probably_readerable'] = Variable(isProbablyReaderable.value); + } + if (extractedContentMarkdown.present) { + map['extracted_content_markdown'] = Variable( + extractedContentMarkdown.value, + ); + } + if (extractedContentPlain.present) { + map['extracted_content_plain'] = Variable( + extractedContentPlain.value, + ); + } + if (fullContentMarkdown.present) { + map['full_content_markdown'] = Variable( + fullContentMarkdown.value, + ); + } + if (fullContentPlain.present) { + map['full_content_plain'] = Variable(fullContentPlain.value); + } + if (timestamp.present) { + map['timestamp'] = Variable(timestamp.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('TabCompanion(') + ..write('id: $id, ') + ..write('source: $source, ') + ..write('parentId: $parentId, ') + ..write('containerId: $containerId, ') + ..write('orderKey: $orderKey, ') + ..write('url: $url, ') + ..write('title: $title, ') + ..write('tabMode: $tabMode, ') + ..write('isolationContextId: $isolationContextId, ') + ..write('isPinned: $isPinned, ') + ..write('isProbablyReaderable: $isProbablyReaderable, ') + ..write('extractedContentMarkdown: $extractedContentMarkdown, ') + ..write('extractedContentPlain: $extractedContentPlain, ') + ..write('fullContentMarkdown: $fullContentMarkdown, ') + ..write('fullContentPlain: $fullContentPlain, ') + ..write('timestamp: $timestamp, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class ClosedTabTombstone extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + ClosedTabTombstone(this.attachedDatabase, [this._alias]); + late final GeneratedColumn tabId = GeneratedColumn( + 'tab_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'PRIMARY KEY NOT NULL', + ); + late final GeneratedColumn closedAt = GeneratedColumn( + 'closed_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + @override + List get $columns => [tabId, closedAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'closed_tab_tombstone'; + @override + Set get $primaryKey => {tabId}; + @override + ClosedTabTombstoneData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ClosedTabTombstoneData( + tabId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}tab_id'], + )!, + closedAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}closed_at'], + )!, + ); + } + + @override + ClosedTabTombstone createAlias(String alias) { + return ClosedTabTombstone(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class ClosedTabTombstoneData extends DataClass + implements Insertable { + final String tabId; + final int closedAt; + const ClosedTabTombstoneData({required this.tabId, required this.closedAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['tab_id'] = Variable(tabId); + map['closed_at'] = Variable(closedAt); + return map; + } + + factory ClosedTabTombstoneData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ClosedTabTombstoneData( + tabId: serializer.fromJson(json['tabId']), + closedAt: serializer.fromJson(json['closedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'tabId': serializer.toJson(tabId), + 'closedAt': serializer.toJson(closedAt), + }; + } + + ClosedTabTombstoneData copyWith({String? tabId, int? closedAt}) => + ClosedTabTombstoneData( + tabId: tabId ?? this.tabId, + closedAt: closedAt ?? this.closedAt, + ); + ClosedTabTombstoneData copyWithCompanion(ClosedTabTombstoneCompanion data) { + return ClosedTabTombstoneData( + tabId: data.tabId.present ? data.tabId.value : this.tabId, + closedAt: data.closedAt.present ? data.closedAt.value : this.closedAt, + ); + } + + @override + String toString() { + return (StringBuffer('ClosedTabTombstoneData(') + ..write('tabId: $tabId, ') + ..write('closedAt: $closedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(tabId, closedAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ClosedTabTombstoneData && + other.tabId == this.tabId && + other.closedAt == this.closedAt); +} + +class ClosedTabTombstoneCompanion + extends UpdateCompanion { + final Value tabId; + final Value closedAt; + final Value rowid; + const ClosedTabTombstoneCompanion({ + this.tabId = const Value.absent(), + this.closedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + ClosedTabTombstoneCompanion.insert({ + required String tabId, + required int closedAt, + this.rowid = const Value.absent(), + }) : tabId = Value(tabId), + closedAt = Value(closedAt); + static Insertable custom({ + Expression? tabId, + Expression? closedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (tabId != null) 'tab_id': tabId, + if (closedAt != null) 'closed_at': closedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + ClosedTabTombstoneCompanion copyWith({ + Value? tabId, + Value? closedAt, + Value? rowid, + }) { + return ClosedTabTombstoneCompanion( + tabId: tabId ?? this.tabId, + closedAt: closedAt ?? this.closedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (tabId.present) { + map['tab_id'] = Variable(tabId.value); + } + if (closedAt.present) { + map['closed_at'] = Variable(closedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ClosedTabTombstoneCompanion(') + ..write('tabId: $tabId, ') + ..write('closedAt: $closedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class TabFts extends Table + with TableInfo, VirtualTableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + TabFts(this.attachedDatabase, [this._alias]); + late final GeneratedColumn title = GeneratedColumn( + 'title', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: '', + ); + late final GeneratedColumn url = GeneratedColumn( + 'url', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: '', + ); + late final GeneratedColumn extractedContentPlain = + GeneratedColumn( + 'extracted_content_plain', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: '', + ); + late final GeneratedColumn fullContentPlain = GeneratedColumn( + 'full_content_plain', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: '', + ); + @override + List get $columns => [ + title, + url, + extractedContentPlain, + fullContentPlain, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'tab_fts'; + @override + Set get $primaryKey => const {}; + @override + TabFtsData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return TabFtsData( + title: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}title'], + ), + url: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}url'], + ), + extractedContentPlain: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}extracted_content_plain'], + ), + fullContentPlain: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}full_content_plain'], + ), + ); + } + + @override + TabFts createAlias(String alias) { + return TabFts(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; + @override + String get moduleAndArgs => + 'fts5(title, url, extracted_content_plain, full_content_plain, content=tab, tokenize="trigram")'; +} + +class TabFtsData extends DataClass implements Insertable { + final String? title; + final String? url; + final String? extractedContentPlain; + final String? fullContentPlain; + const TabFtsData({ + this.title, + this.url, + this.extractedContentPlain, + this.fullContentPlain, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (!nullToAbsent || title != null) { + map['title'] = Variable(title); + } + if (!nullToAbsent || url != null) { + map['url'] = Variable(url); + } + if (!nullToAbsent || extractedContentPlain != null) { + map['extracted_content_plain'] = Variable(extractedContentPlain); + } + if (!nullToAbsent || fullContentPlain != null) { + map['full_content_plain'] = Variable(fullContentPlain); + } + return map; + } + + factory TabFtsData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return TabFtsData( + title: serializer.fromJson(json['title']), + url: serializer.fromJson(json['url']), + extractedContentPlain: serializer.fromJson( + json['extractedContentPlain'], + ), + fullContentPlain: serializer.fromJson(json['fullContentPlain']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'title': serializer.toJson(title), + 'url': serializer.toJson(url), + 'extractedContentPlain': serializer.toJson( + extractedContentPlain, + ), + 'fullContentPlain': serializer.toJson(fullContentPlain), + }; + } + + TabFtsData copyWith({ + Value title = const Value.absent(), + Value url = const Value.absent(), + Value extractedContentPlain = const Value.absent(), + Value fullContentPlain = const Value.absent(), + }) => TabFtsData( + title: title.present ? title.value : this.title, + url: url.present ? url.value : this.url, + extractedContentPlain: extractedContentPlain.present + ? extractedContentPlain.value + : this.extractedContentPlain, + fullContentPlain: fullContentPlain.present + ? fullContentPlain.value + : this.fullContentPlain, + ); + TabFtsData copyWithCompanion(TabFtsCompanion data) { + return TabFtsData( + title: data.title.present ? data.title.value : this.title, + url: data.url.present ? data.url.value : this.url, + extractedContentPlain: data.extractedContentPlain.present + ? data.extractedContentPlain.value + : this.extractedContentPlain, + fullContentPlain: data.fullContentPlain.present + ? data.fullContentPlain.value + : this.fullContentPlain, + ); + } + + @override + String toString() { + return (StringBuffer('TabFtsData(') + ..write('title: $title, ') + ..write('url: $url, ') + ..write('extractedContentPlain: $extractedContentPlain, ') + ..write('fullContentPlain: $fullContentPlain') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(title, url, extractedContentPlain, fullContentPlain); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TabFtsData && + other.title == this.title && + other.url == this.url && + other.extractedContentPlain == this.extractedContentPlain && + other.fullContentPlain == this.fullContentPlain); +} + +class TabFtsCompanion extends UpdateCompanion { + final Value title; + final Value url; + final Value extractedContentPlain; + final Value fullContentPlain; + final Value rowid; + const TabFtsCompanion({ + this.title = const Value.absent(), + this.url = const Value.absent(), + this.extractedContentPlain = const Value.absent(), + this.fullContentPlain = const Value.absent(), + this.rowid = const Value.absent(), + }); + TabFtsCompanion.insert({ + this.title = const Value.absent(), + this.url = const Value.absent(), + this.extractedContentPlain = const Value.absent(), + this.fullContentPlain = const Value.absent(), + this.rowid = const Value.absent(), + }); + static Insertable custom({ + Expression? title, + Expression? url, + Expression? extractedContentPlain, + Expression? fullContentPlain, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (title != null) 'title': title, + if (url != null) 'url': url, + if (extractedContentPlain != null) + 'extracted_content_plain': extractedContentPlain, + if (fullContentPlain != null) 'full_content_plain': fullContentPlain, + if (rowid != null) 'rowid': rowid, + }); + } + + TabFtsCompanion copyWith({ + Value? title, + Value? url, + Value? extractedContentPlain, + Value? fullContentPlain, + Value? rowid, + }) { + return TabFtsCompanion( + title: title ?? this.title, + url: url ?? this.url, + extractedContentPlain: + extractedContentPlain ?? this.extractedContentPlain, + fullContentPlain: fullContentPlain ?? this.fullContentPlain, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (title.present) { + map['title'] = Variable(title.value); + } + if (url.present) { + map['url'] = Variable(url.value); + } + if (extractedContentPlain.present) { + map['extracted_content_plain'] = Variable( + extractedContentPlain.value, + ); + } + if (fullContentPlain.present) { + map['full_content_plain'] = Variable(fullContentPlain.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('TabFtsCompanion(') + ..write('title: $title, ') + ..write('url: $url, ') + ..write('extractedContentPlain: $extractedContentPlain, ') + ..write('fullContentPlain: $fullContentPlain, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV9 extends GeneratedDatabase { + DatabaseAtV9(QueryExecutor e) : super(e); + late final Container container = Container(this); + late final Tab tab = Tab(this); + late final ClosedTabTombstone closedTabTombstone = ClosedTabTombstone(this); + late final Index idxTabParentContainer = Index( + 'idx_tab_parent_container', + 'CREATE INDEX idx_tab_parent_container ON tab (parent_id, container_id)', + ); + late final TabFts tabFts = TabFts(this); + late final Trigger tabMaintainParentChainOnDelete = Trigger( + 'CREATE TRIGGER tab_maintain_parent_chain_on_delete BEFORE DELETE ON tab BEGIN UPDATE tab SET parent_id = CASE WHEN OLD.parent_id IS NOT NULL AND EXISTS (SELECT 1 FROM tab WHERE id = OLD.parent_id) THEN OLD.parent_id ELSE NULL END WHERE parent_id = OLD.id;END', + 'tab_maintain_parent_chain_on_delete', + ); + late final Trigger tabAfterInsert = Trigger( + 'CREATE TRIGGER tab_after_insert AFTER INSERT ON tab BEGIN INSERT INTO tab_fts ("rowid", title, url, extracted_content_plain, full_content_plain) VALUES (new."rowid", new.title, new.url, new.extracted_content_plain, new.full_content_plain);END', + 'tab_after_insert', + ); + late final Trigger tabAfterDelete = Trigger( + 'CREATE TRIGGER tab_after_delete AFTER DELETE ON tab BEGIN INSERT INTO tab_fts (tab_fts, "rowid", title, url, extracted_content_plain, full_content_plain) VALUES (\'delete\', old."rowid", old.title, old.url, old.extracted_content_plain, old.full_content_plain);END', + 'tab_after_delete', + ); + late final Trigger tabAfterUpdate = Trigger( + 'CREATE TRIGGER tab_after_update AFTER UPDATE ON tab BEGIN INSERT INTO tab_fts (tab_fts, "rowid", title, url, extracted_content_plain, full_content_plain) VALUES (\'delete\', old."rowid", old.title, old.url, old.extracted_content_plain, old.full_content_plain);INSERT INTO tab_fts ("rowid", title, url, extracted_content_plain, full_content_plain) VALUES (new."rowid", new.title, new.url, new.extracted_content_plain, new.full_content_plain);END', + 'tab_after_update', + ); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + container, + tab, + closedTabTombstone, + idxTabParentContainer, + tabFts, + tabMaintainParentChainOnDelete, + tabAfterInsert, + tabAfterDelete, + tabAfterUpdate, + ]; + @override + StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules([ + WritePropagation( + on: TableUpdateQuery.onTableName( + 'container', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('tab', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'tab', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('tab', kind: UpdateKind.update)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'tab', + limitUpdateKind: UpdateKind.insert, + ), + result: [TableUpdate('tab_fts', kind: UpdateKind.insert)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'tab', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('tab_fts', kind: UpdateKind.insert)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'tab', + limitUpdateKind: UpdateKind.update, + ), + result: [TableUpdate('tab_fts', kind: UpdateKind.insert)], + ), + ]); + @override + int get schemaVersion => 9; +}