make tab restore more resilient

This commit is contained in:
Fabian Freund
2026-05-02 05:58:38 +02:00
parent 69f21bb7d8
commit 063decd451
11 changed files with 2861 additions and 29 deletions
@@ -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>(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>(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"
}
]
}
]
}
@@ -52,6 +52,10 @@ part 'tab.g.dart';
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
class TabRepository extends _$TabRepository { class TabRepository extends _$TabRepository {
final _tabsService = GeckoTabService(); final _tabsService = GeckoTabService();
final _sessionStartedAt = DateTime.now();
bool _didPruneTombstones = false;
bool _reclosing = false;
bool _suppressNextReclose = false;
final _tabFromIntent = <String>{}; final _tabFromIntent = <String>{};
final _closeLock = Lock(); final _closeLock = Lock();
@@ -550,38 +554,28 @@ class TabRepository extends _$TabRepository {
} }
} }
Future<void> closeTab(String tabId) { Future<void> _closeTabsInternal(
List<String> tabIds, {
required bool recordTombstones,
}) {
return _closeLock.synchronized(() async { return _closeLock.synchronized(() async {
// Collect isolation context before close if (tabIds.isEmpty) {
final isolationContextId = ref return;
.read(tabStatesProvider)[tabId]
?.isolationContextId;
if (ref.read(selectedTabProvider) == tabId) {
await _selectNextTab(tabId);
} }
await _preservePromotedChildOrderOnClose([tabId]); if (recordTombstones) {
await ref
await _tabsService.removeTab(tabId: tabId); .read(tabDatabaseProvider)
.tabDao
// Queue isolation cleanup — actual cleanup runs after syncTabs .addClosedTabTombstones(tabIds);
// deletes the DB row, so the count check is accurate.
if (isolationContextId != null) {
_pendingIsolationCleanup.add(isolationContextId);
} }
});
}
Future<void> closeTabs(List<String> tabIds) {
return _closeLock.synchronized(() async {
// Collect isolation contexts from tabs being closed
for (final tabId in tabIds) { for (final tabId in tabIds) {
final contextId = ref final isolationContextId = ref
.read(tabStatesProvider)[tabId] .read(tabStatesProvider)[tabId]
?.isolationContextId; ?.isolationContextId;
if (contextId != null) { if (isolationContextId != null) {
_pendingIsolationCleanup.add(contextId); _pendingIsolationCleanup.add(isolationContextId);
} }
} }
@@ -592,10 +586,29 @@ class TabRepository extends _$TabRepository {
await _preservePromotedChildOrderOnClose(tabIds); 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<void> closeTab(String tabId) {
return _closeTabsInternal([tabId], recordTombstones: true);
}
Future<void> closeTabs(List<String> tabIds) {
return _closeTabsInternal(tabIds, recordTombstones: true);
}
Future<void> _clearTombstonesForCurrentTabs(List<String> tabIds) {
return ref
.read(tabDatabaseProvider)
.tabDao
.deleteClosedTabTombstones(tabIds);
}
Future<void> _preservePromotedChildOrderOnClose(List<String> tabIds) { Future<void> _preservePromotedChildOrderOnClose(List<String> tabIds) {
return ref return ref
.read(tabDatabaseProvider) .read(tabDatabaseProvider)
@@ -662,9 +675,49 @@ class TabRepository extends _$TabRepository {
} }
Future<void> undoClose() { Future<void> 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(); return _tabsService.undo();
} }
Future<bool> _recloseRestoredClosedTabs(List<String> 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. /// Cleans up isolation contexts from previous crashed sessions.
/// Called once after tab list stabilizes on startup. /// Called once after tab list stabilizes on startup.
// Future<void> _cleanupOrphanedIsolationContexts() async { // Future<void> _cleanupOrphanedIsolationContexts() async {
@@ -845,6 +898,15 @@ class TabRepository extends _$TabRepository {
ref.listen( ref.listen(
tabListProvider, tabListProvider,
(previous, next) async { (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 //Only sync tabs if there has been a previous value or is not empty
final shouldSyncTabs = final shouldSyncTabs =
next.value.isNotEmpty || (previous?.value.isNotEmpty ?? false); next.value.isNotEmpty || (previous?.value.isNotEmpty ?? false);
@@ -41,7 +41,7 @@ final class TabRepositoryProvider
} }
} }
String _$tabRepositoryHash() => r'4c5bda4d0ddcc66cfa420d3c2ca97db42b73b878'; String _$tabRepositoryHash() => r'84ecfed72f17c367fc7bf0100f6367d4e7609596';
abstract class _$TabRepository extends $Notifier<void> { abstract class _$TabRepository extends $Notifier<void> {
void build(); void build();
@@ -47,6 +47,8 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
final _undoHistory = <String, TabData>{}; final _undoHistory = <String, TabData>{};
Timer? _clearHistoryTimer; Timer? _clearHistoryTimer;
static const closedTabTombstoneTtl = Duration(hours: 24);
TabDao(super.db); TabDao(super.db);
UpdateStatement<Tab, TabData> _updateByIdStatement(String id) => UpdateStatement<Tab, TabData> _updateByIdStatement(String id) =>
@@ -317,6 +319,103 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
return statement.write(TabCompanion(containerId: Value(containerId))); return statement.write(TabCompanion(containerId: Value(containerId)));
} }
Future<void> addClosedTabTombstones(
Iterable<String> 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<void> deleteClosedTabTombstones(Iterable<String> tabIds) {
final uniqueIds = tabIds.toSet();
if (uniqueIds.isEmpty) {
return Future.value();
}
return (db.closedTabTombstone.delete()
..where((t) => t.tabId.isIn(uniqueIds)))
.go();
}
Future<void> 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<Set<String>> getStartupRestoredClosedTabIds(
Iterable<String> tabIds, {
required DateTime sessionStartedAt,
Duration ttl = closedTabTombstoneTtl,
DateTime? now,
}) async {
final uniqueIds = tabIds.toSet();
if (uniqueIds.isEmpty) {
return const <String>{};
}
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<Map<String, String>> _containerIdsByContextualIdentity(
Iterable<String> contextIds,
) async {
final uniqueContextIds = contextIds.toSet();
if (uniqueContextIds.isEmpty) {
return const <String, String>{};
}
final rows = await db.definitionsDrift
.containerIdsByContextualIdentities(
contextIds: uniqueContextIds.toList(growable: false),
)
.get();
return {for (final row in rows) row.contextualIdentity: row.id};
}
Future<void> reorderTabs({ Future<void> reorderTabs({
required List<String> movingTabIds, required List<String> movingTabIds,
required String? previousTabId, required String? previousTabId,
@@ -633,6 +732,32 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
parentIdsToValidate, parentIdsToValidate,
).get().then((ids) => ids.toSet()); ).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 <String, String?>{}
: 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 = <String, String>{
for (final entry in containerRepairCandidates.entries)
if (currentContainerIds[entry.key] == null)
if (containerIdsByContext[entry.value] case final containerId?)
entry.key: containerId,
};
// Complete validation map // Complete validation map
for (final state in next.values) { for (final state in next.values) {
final previousState = previous?[state.id]; final previousState = previous?[state.id];
@@ -658,13 +783,17 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
previousState.url != state.url || previousState.url != state.url ||
previousState.title != state.title || previousState.title != state.title ||
previousState.parentId != state.parentId || previousState.parentId != state.parentId ||
previousState.tabMode != state.tabMode) { previousState.tabMode != state.tabMode ||
repairedContainerIds.containsKey(state.id)) {
batch.update( batch.update(
db.tab, db.tab,
TabCompanion( TabCompanion(
parentId: validatedParentIds.containsKey(state.id) parentId: validatedParentIds.containsKey(state.id)
? Value(validatedParentIds[state.id]) ? Value(validatedParentIds[state.id])
: const Value.absent(), : const Value.absent(),
containerId:
repairedContainerIds[state.id].mapNotNull(Value.new) ??
const Value.absent(),
url: (previousState?.url != state.url) url: (previousState?.url != state.url)
? Value(state.url) ? Value(state.url)
: const Value.absent(), : const Value.absent(),
@@ -31,7 +31,7 @@ import 'package:weblibre/features/search/domain/fts_tokenizer.dart';
@DriftDatabase(include: {'definitions.drift'}, daos: [ContainerDao, TabDao]) @DriftDatabase(include: {'definitions.drift'}, daos: [ContainerDao, TabDao])
class TabDatabase extends $TabDatabase with TrigramQueryBuilderMixin { class TabDatabase extends $TabDatabase with TrigramQueryBuilderMixin {
@override @override
final int schemaVersion = 8; final int schemaVersion = 9;
@override @override
final int ftsTokenLimit = 10; final int ftsTokenLimit = 10;
@@ -137,5 +137,8 @@ class TabDatabase extends $TabDatabase with TrigramQueryBuilderMixin {
await m.createIndex(schema.idxTabParentContainer); await m.createIndex(schema.idxTabParentContainer);
}, },
from8To9: (m, schema) async {
await m.create(schema.closedTabTombstone);
},
); );
} }
@@ -17,6 +17,9 @@ abstract class $TabDatabase extends i0.GeneratedDatabase {
$TabDatabaseManager get managers => $TabDatabaseManager(this); $TabDatabaseManager get managers => $TabDatabaseManager(this);
late final i1.Container container = i1.Container(this); late final i1.Container container = i1.Container(this);
late final i1.Tab tab = i1.Tab(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 i1.TabFts tabFts = i1.TabFts(this);
late final i2.ContainerDao containerDao = i2.ContainerDao( late final i2.ContainerDao containerDao = i2.ContainerDao(
this as i3.TabDatabase, this as i3.TabDatabase,
@@ -32,6 +35,7 @@ abstract class $TabDatabase extends i0.GeneratedDatabase {
List<i0.DatabaseSchemaEntity> get allSchemaEntities => [ List<i0.DatabaseSchemaEntity> get allSchemaEntities => [
container, container,
tab, tab,
closedTabTombstone,
i1.idxTabParentContainer, i1.idxTabParentContainer,
tabFts, tabFts,
i1.tabMaintainParentChainOnDelete, i1.tabMaintainParentChainOnDelete,
@@ -86,6 +90,8 @@ class $TabDatabaseManager {
i1.$ContainerTableManager get container => i1.$ContainerTableManager get container =>
i1.$ContainerTableManager(_db, _db.container); i1.$ContainerTableManager(_db, _db.container);
i1.$TabTableManager get tab => i1.$TabTableManager(_db, _db.tab); 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); i1.$TabFtsTableManager get tabFts => i1.$TabFtsTableManager(_db, _db.tabFts);
} }
@@ -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<i1.DatabaseSchemaEntity> 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<String> get tabId =>
columnsByName['tab_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get closedAt =>
columnsByName['closed_at']! as i1.GeneratedColumn<int>;
}
i1.GeneratedColumn<String> _column_20(String aliasedName) =>
i1.GeneratedColumn<String>(
'tab_id',
aliasedName,
false,
type: i1.DriftSqlType.string,
$customConstraints: 'PRIMARY KEY NOT NULL',
);
i1.GeneratedColumn<int> _column_21(String aliasedName) =>
i1.GeneratedColumn<int>(
'closed_at',
aliasedName,
false,
type: i1.DriftSqlType.int,
$customConstraints: 'NOT NULL',
);
i0.MigrationStepWithVersion migrationSteps({ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3, required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4, required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
@@ -816,6 +938,7 @@ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6, required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6,
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7, required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8, required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
}) { }) {
return (currentVersion, database) async { return (currentVersion, database) async {
switch (currentVersion) { switch (currentVersion) {
@@ -849,6 +972,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema); final migrator = i1.Migrator(database, schema);
await from7To8(migrator, schema); await from7To8(migrator, schema);
return 8; return 8;
case 8:
final schema = Schema9(database: database);
final migrator = i1.Migrator(database, schema);
await from8To9(migrator, schema);
return 9;
default: default:
throw ArgumentError.value('Unknown migration from $currentVersion'); throw ArgumentError.value('Unknown migration from $currentVersion');
} }
@@ -862,6 +990,7 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6, required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6,
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7, required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8, required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
}) => i0.VersionedSchema.stepByStepHelper( }) => i0.VersionedSchema.stepByStepHelper(
step: migrationSteps( step: migrationSteps(
from2To3: from2To3, from2To3: from2To3,
@@ -870,5 +999,6 @@ i1.OnUpgrade stepByStep({
from5To6: from5To6, from5To6: from5To6,
from6To7: from6To7, from6To7: from6To7,
from7To8: from7To8, from7To8: from7To8,
from8To9: from8To9,
), ),
); );
@@ -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 -- Composite index used by `tabsWithRootAndDepth` (the seed checks
-- `parent_id IS NULL OR NOT EXISTS(... WHERE p.id = parent_id AND -- `parent_id IS NULL OR NOT EXISTS(... WHERE p.id = parent_id AND
-- p.container_id IS :container_id)`) and by `lastChildTabId` -- p.container_id IS :container_id)`) and by `lastChildTabId`
@@ -390,6 +395,13 @@ containerByContextualIdentity:
WHERE container.metadata ->> '$.contextualIdentity' = :contextId WHERE container.metadata ->> '$.contextualIdentity' = :contextId
LIMIT 1; 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: containersToClearOnExit:
SELECT container.metadata ->> '$.contextualIdentity' AS contextual_identity SELECT container.metadata ->> '$.contextualIdentity' AS contextual_identity
FROM container FROM container
@@ -882,6 +882,159 @@ typedef $TabProcessedTableManager =
i3.TabData, i3.TabData,
i0.PrefetchHooks Function({bool containerId}) i0.PrefetchHooks Function({bool containerId})
>; >;
typedef $ClosedTabTombstoneCreateCompanionBuilder =
i3.ClosedTabTombstoneCompanion Function({
required String tabId,
required DateTime closedAt,
i0.Value<int> rowid,
});
typedef $ClosedTabTombstoneUpdateCompanionBuilder =
i3.ClosedTabTombstoneCompanion Function({
i0.Value<String> tabId,
i0.Value<DateTime> closedAt,
i0.Value<int> rowid,
});
class $ClosedTabTombstoneFilterComposer
extends i0.Composer<i0.GeneratedDatabase, i3.ClosedTabTombstone> {
$ClosedTabTombstoneFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnFilters<String> get tabId => $composableBuilder(
column: $table.tabId,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<DateTime> get closedAt => $composableBuilder(
column: $table.closedAt,
builder: (column) => i0.ColumnFilters(column),
);
}
class $ClosedTabTombstoneOrderingComposer
extends i0.Composer<i0.GeneratedDatabase, i3.ClosedTabTombstone> {
$ClosedTabTombstoneOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnOrderings<String> get tabId => $composableBuilder(
column: $table.tabId,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<DateTime> get closedAt => $composableBuilder(
column: $table.closedAt,
builder: (column) => i0.ColumnOrderings(column),
);
}
class $ClosedTabTombstoneAnnotationComposer
extends i0.Composer<i0.GeneratedDatabase, i3.ClosedTabTombstone> {
$ClosedTabTombstoneAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.GeneratedColumn<String> get tabId =>
$composableBuilder(column: $table.tabId, builder: (column) => column);
i0.GeneratedColumn<DateTime> 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<String> tabId = const i0.Value.absent(),
i0.Value<DateTime> closedAt = const i0.Value.absent(),
i0.Value<int> rowid = const i0.Value.absent(),
}) => i3.ClosedTabTombstoneCompanion(
tabId: tabId,
closedAt: closedAt,
rowid: rowid,
),
createCompanionCallback:
({
required String tabId,
required DateTime closedAt,
i0.Value<int> 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 = typedef $TabFtsCreateCompanionBuilder =
i3.TabFtsCompanion Function({ i3.TabFtsCompanion Function({
required String title, required String title,
@@ -2060,6 +2213,196 @@ class TabCompanion extends i0.UpdateCompanion<i3.TabData> {
} }
} }
class ClosedTabTombstone extends i0.Table
with i0.TableInfo<ClosedTabTombstone, i3.ClosedTabTombstoneData> {
@override
final i0.GeneratedDatabase attachedDatabase;
final String? _alias;
ClosedTabTombstone(this.attachedDatabase, [this._alias]);
late final i0.GeneratedColumn<String> tabId = i0.GeneratedColumn<String>(
'tab_id',
aliasedName,
false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
$customConstraints: 'PRIMARY KEY NOT NULL',
);
late final i0.GeneratedColumn<DateTime> closedAt =
i0.GeneratedColumn<DateTime>(
'closed_at',
aliasedName,
false,
type: i0.DriftSqlType.dateTime,
requiredDuringInsert: true,
$customConstraints: 'NOT NULL',
);
@override
List<i0.GeneratedColumn> get $columns => [tabId, closedAt];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'closed_tab_tombstone';
@override
Set<i0.GeneratedColumn> get $primaryKey => {tabId};
@override
i3.ClosedTabTombstoneData map(
Map<String, dynamic> 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<i3.ClosedTabTombstoneData> {
final String tabId;
final DateTime closedAt;
const ClosedTabTombstoneData({required this.tabId, required this.closedAt});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['tab_id'] = i0.Variable<String>(tabId);
map['closed_at'] = i0.Variable<DateTime>(closedAt);
return map;
}
factory ClosedTabTombstoneData.fromJson(
Map<String, dynamic> json, {
i0.ValueSerializer? serializer,
}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return ClosedTabTombstoneData(
tabId: serializer.fromJson<String>(json['tab_id']),
closedAt: serializer.fromJson<DateTime>(json['closed_at']),
);
}
@override
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'tab_id': serializer.toJson<String>(tabId),
'closed_at': serializer.toJson<DateTime>(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<i3.ClosedTabTombstoneData> {
final i0.Value<String> tabId;
final i0.Value<DateTime> closedAt;
final i0.Value<int> 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<i3.ClosedTabTombstoneData> custom({
i0.Expression<String>? tabId,
i0.Expression<DateTime>? closedAt,
i0.Expression<int>? 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<String>? tabId,
i0.Value<DateTime>? closedAt,
i0.Value<int>? rowid,
}) {
return i3.ClosedTabTombstoneCompanion(
tabId: tabId ?? this.tabId,
closedAt: closedAt ?? this.closedAt,
rowid: rowid ?? this.rowid,
);
}
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
if (tabId.present) {
map['tab_id'] = i0.Variable<String>(tabId.value);
}
if (closedAt.present) {
map['closed_at'] = i0.Variable<DateTime>(closedAt.value);
}
if (rowid.present) {
map['rowid'] = i0.Variable<int>(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( i0.Index get idxTabParentContainer => i0.Index(
'idx_tab_parent_container', 'idx_tab_parent_container',
'CREATE INDEX idx_tab_parent_container ON tab (parent_id, container_id)', 'CREATE INDEX idx_tab_parent_container ON tab (parent_id, container_id)',
@@ -2643,6 +2986,23 @@ class DefinitionsDrift extends i9.ModularAccessor {
).asyncMap(container.mapFromRow); ).asyncMap(container.mapFromRow);
} }
i0.Selectable<ContainerIdsByContextualIdentitiesResult>
containerIdsByContextualIdentities({required List<String> 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<String>($)],
readsFrom: {container},
).map(
(i0.QueryRow row) => ContainerIdsByContextualIdentitiesResult(
id: row.read<String>('id'),
contextualIdentity: row.read<String>('contextual_identity'),
),
);
}
i0.Selectable<String?> containersToClearOnExit() { i0.Selectable<String?> containersToClearOnExit() {
return customSelect( 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', '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}); UnorderedTabDescendantsResult({required this.id, this.parentId});
} }
class ContainerIdsByContextualIdentitiesResult {
final String id;
final String contextualIdentity;
ContainerIdsByContextualIdentitiesResult({
required this.id,
required this.contextualIdentity,
});
}
class IsolatedContextContainerPairsResult { class IsolatedContextContainerPairsResult {
final String? isolationContextId; final String? isolationContextId;
final String? containerId; final String? containerId;
@@ -11,6 +11,7 @@ import 'schema_v5.dart' as v5;
import 'schema_v6.dart' as v6; import 'schema_v6.dart' as v6;
import 'schema_v7.dart' as v7; import 'schema_v7.dart' as v7;
import 'schema_v8.dart' as v8; import 'schema_v8.dart' as v8;
import 'schema_v9.dart' as v9;
class GeneratedHelper implements SchemaInstantiationHelper { class GeneratedHelper implements SchemaInstantiationHelper {
@override @override
@@ -30,10 +31,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
return v7.DatabaseAtV7(db); return v7.DatabaseAtV7(db);
case 8: case 8:
return v8.DatabaseAtV8(db); return v8.DatabaseAtV8(db);
case 9:
return v9.DatabaseAtV9(db);
default: default:
throw MissingSchemaException(version, versions); 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];
} }
File diff suppressed because it is too large Load Diff