improved tab parent handling
This commit is contained in:
@@ -0,0 +1,441 @@
|
|||||||
|
{
|
||||||
|
"_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": "is_private",
|
||||||
|
"getter_name": "isPrivate",
|
||||||
|
"moor_type": "bool",
|
||||||
|
"nullable": true,
|
||||||
|
"customConstraints": "",
|
||||||
|
"default_dart": null,
|
||||||
|
"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": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"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": 3,
|
||||||
|
"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": 4,
|
||||||
|
"references": [
|
||||||
|
1,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"type": "trigger",
|
||||||
|
"data": {
|
||||||
|
"on": 1,
|
||||||
|
"references_in_body": [
|
||||||
|
1,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"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": 5,
|
||||||
|
"references": [
|
||||||
|
1,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"type": "trigger",
|
||||||
|
"data": {
|
||||||
|
"on": 1,
|
||||||
|
"references_in_body": [
|
||||||
|
1,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"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": 6,
|
||||||
|
"references": [
|
||||||
|
1,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"type": "trigger",
|
||||||
|
"data": {
|
||||||
|
"on": 1,
|
||||||
|
"references_in_body": [
|
||||||
|
1,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"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, \"is_private\" INTEGER, \"is_probably_readerable\" INTEGER, \"extracted_content_markdown\" TEXT, \"extracted_content_plain\" TEXT, \"full_content_markdown\" TEXT, \"full_content_plain\" TEXT, \"timestamp\" INTEGER NOT NULL);"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -118,14 +118,35 @@ class TabRepository extends _$TabRepository {
|
|||||||
selectTabId: selectTabId,
|
selectTabId: selectTabId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Build sets for validation
|
||||||
|
final creatingTabIds = createdTabIds.toSet();
|
||||||
|
final parentIdsToValidate = tabs
|
||||||
|
.map((tab) => tab.parentId)
|
||||||
|
.whereType<String>()
|
||||||
|
.where((id) => !creatingTabIds.contains(id))
|
||||||
|
.toSet();
|
||||||
|
|
||||||
|
// Batch validate parent IDs that aren't in the current creation batch
|
||||||
|
final existingParentIds =
|
||||||
|
await tabDao.getExistingTabIds(parentIdsToValidate).get().then((ids) => ids.toSet());
|
||||||
|
|
||||||
// Upsert all tabs in the database
|
// Upsert all tabs in the database
|
||||||
for (var i = 0; i < createdTabIds.length; i++) {
|
for (var i = 0; i < createdTabIds.length; i++) {
|
||||||
final tabId = createdTabIds[i];
|
final tabId = createdTabIds[i];
|
||||||
final tab = tabs[i];
|
final tab = tabs[i];
|
||||||
|
|
||||||
|
// Validate parent exists in either the batch being created or database
|
||||||
|
String? validatedParentId;
|
||||||
|
if (tab.parentId != null) {
|
||||||
|
if (creatingTabIds.contains(tab.parentId) ||
|
||||||
|
existingParentIds.contains(tab.parentId)) {
|
||||||
|
validatedParentId = tab.parentId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await tabDao.insertTab(
|
await tabDao.insertTab(
|
||||||
tabId,
|
tabId,
|
||||||
parentId: Value(tab.parentId),
|
parentId: Value(validatedParentId),
|
||||||
source: TabSource.manual,
|
source: TabSource.manual,
|
||||||
containerId: Value(container?.value?.id),
|
containerId: Value(container?.value?.id),
|
||||||
isPrivate: Value(tab.private),
|
isPrivate: Value(tab.private),
|
||||||
@@ -539,7 +560,7 @@ class TabRepository extends _$TabRepository {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
final tabStateDebouncer = Debouncer(const Duration(seconds: 3));
|
final tabStateDebouncer = Debouncer(const Duration(seconds: 1));
|
||||||
Map<String, TabState>? debounceStartValue;
|
Map<String, TabState>? debounceStartValue;
|
||||||
|
|
||||||
ref.listen(
|
ref.listen(
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ final class TabRepositoryProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$tabRepositoryHash() => r'ea1079570ed2c3424fef6168a7633a10cfb9ac6a';
|
String _$tabRepositoryHash() => r'6a66c2f1f8d00767d19e6125d4eafc6d93381c8a';
|
||||||
|
|
||||||
abstract class _$TabRepository extends $Notifier<void> {
|
abstract class _$TabRepository extends $Notifier<void> {
|
||||||
void build();
|
void build();
|
||||||
|
|||||||
@@ -61,6 +61,15 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
|
|||||||
return query.map((row) => row.read(db.tab.id)!);
|
return query.map((row) => row.read(db.tab.id)!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Validates multiple tab IDs and returns only those that exist in the database.
|
||||||
|
Selectable<String> getExistingTabIds(Iterable<String> tabIds) {
|
||||||
|
final query = selectOnly(db.tab)
|
||||||
|
..addColumns([db.tab.id])
|
||||||
|
..where(db.tab.id.isIn(tabIds));
|
||||||
|
|
||||||
|
return query.map((row) => row.read(db.tab.id)!);
|
||||||
|
}
|
||||||
|
|
||||||
Selectable<TabData> getTabsFifo({int limit = 25}) {
|
Selectable<TabData> getTabsFifo({int limit = 25}) {
|
||||||
return select(db.tab)
|
return select(db.tab)
|
||||||
..limit(limit)
|
..limit(limit)
|
||||||
@@ -242,33 +251,72 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
|
|||||||
Future<void> updateTabs(
|
Future<void> updateTabs(
|
||||||
Map<String, TabState>? previous,
|
Map<String, TabState>? previous,
|
||||||
Map<String, TabState> next,
|
Map<String, TabState> next,
|
||||||
) async {
|
) {
|
||||||
await batch((batch) {
|
return db.transaction(() async {
|
||||||
|
// Collect parent IDs that need database validation
|
||||||
|
final parentIdsToValidate = <String>{};
|
||||||
|
final validatedParentIds = <String, String?>{};
|
||||||
|
|
||||||
for (final state in next.values) {
|
for (final state in next.values) {
|
||||||
final previousState = previous?[state.id];
|
final previousState = previous?[state.id];
|
||||||
|
|
||||||
if (previousState == null ||
|
if (previousState?.parentId != state.parentId && state.parentId != null) {
|
||||||
previousState.url != state.url ||
|
if (next.containsKey(state.parentId)) {
|
||||||
previousState.title != state.title) {
|
// Parent exists in current state
|
||||||
batch.update(
|
validatedParentIds[state.id] = state.parentId;
|
||||||
db.tab,
|
} else {
|
||||||
TabCompanion(
|
// Need to validate against database
|
||||||
parentId: (previousState?.parentId != state.parentId)
|
parentIdsToValidate.add(state.parentId!);
|
||||||
? Value(
|
}
|
||||||
next.containsKey(state.parentId) ? state.parentId : null,
|
|
||||||
)
|
|
||||||
: const Value.absent(),
|
|
||||||
url: (previousState?.url != state.url)
|
|
||||||
? Value(state.url)
|
|
||||||
: const Value.absent(),
|
|
||||||
title: (previousState?.title != state.title)
|
|
||||||
? Value(state.title)
|
|
||||||
: const Value.absent(),
|
|
||||||
),
|
|
||||||
where: (t) => t.id.equals(state.id),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Batch validate parent IDs that aren't in the current state
|
||||||
|
final existingParentIds =
|
||||||
|
await getExistingTabIds(parentIdsToValidate).get().then((ids) => ids.toSet());
|
||||||
|
|
||||||
|
// Complete validation map
|
||||||
|
for (final state in next.values) {
|
||||||
|
final previousState = previous?[state.id];
|
||||||
|
|
||||||
|
if (previousState?.parentId != state.parentId) {
|
||||||
|
if (!validatedParentIds.containsKey(state.id)) {
|
||||||
|
// This parent ID needed database validation
|
||||||
|
if (state.parentId != null && existingParentIds.contains(state.parentId)) {
|
||||||
|
validatedParentIds[state.id] = state.parentId;
|
||||||
|
} else {
|
||||||
|
validatedParentIds[state.id] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await batch((batch) {
|
||||||
|
for (final state in next.values) {
|
||||||
|
final previousState = previous?[state.id];
|
||||||
|
|
||||||
|
if (previousState == null ||
|
||||||
|
previousState.url != state.url ||
|
||||||
|
previousState.title != state.title ||
|
||||||
|
previousState.parentId != state.parentId) {
|
||||||
|
batch.update(
|
||||||
|
db.tab,
|
||||||
|
TabCompanion(
|
||||||
|
parentId: validatedParentIds.containsKey(state.id)
|
||||||
|
? Value(validatedParentIds[state.id])
|
||||||
|
: const Value.absent(),
|
||||||
|
url: (previousState?.url != state.url)
|
||||||
|
? Value(state.url)
|
||||||
|
: const Value.absent(),
|
||||||
|
title: (previousState?.title != state.title)
|
||||||
|
? Value(state.title)
|
||||||
|
: const Value.absent(),
|
||||||
|
),
|
||||||
|
where: (t) => t.id.equals(state.id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 = 4;
|
final int schemaVersion = 5;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final int ftsTokenLimit = 10;
|
final int ftsTokenLimit = 10;
|
||||||
@@ -95,5 +95,9 @@ class TabDatabase extends $TabDatabase with TrigramQueryBuilderMixin {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
from4To5: (m, schema) async {
|
||||||
|
await m.drop(schema.tabMaintainParentChainOnDelete);
|
||||||
|
await m.create(schema.tabMaintainParentChainOnDelete);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -383,9 +383,88 @@ i1.GeneratedColumn<int> _column_16(String aliasedName) =>
|
|||||||
type: i1.DriftSqlType.int,
|
type: i1.DriftSqlType.int,
|
||||||
$customConstraints: 'NOT NULL',
|
$customConstraints: 'NOT NULL',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final class Schema5 extends i0.VersionedSchema {
|
||||||
|
Schema5({required super.database}) : super(version: 5);
|
||||||
|
@override
|
||||||
|
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||||
|
container,
|
||||||
|
tab,
|
||||||
|
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 Shape3 tab = Shape3(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'tab',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_16,
|
||||||
|
_column_4,
|
||||||
|
_column_5,
|
||||||
|
_column_6,
|
||||||
|
_column_7,
|
||||||
|
_column_8,
|
||||||
|
_column_9,
|
||||||
|
_column_10,
|
||||||
|
_column_11,
|
||||||
|
_column_12,
|
||||||
|
_column_13,
|
||||||
|
_column_14,
|
||||||
|
_column_15,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
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',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
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,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema5 schema) from4To5,
|
||||||
}) {
|
}) {
|
||||||
return (currentVersion, database) async {
|
return (currentVersion, database) async {
|
||||||
switch (currentVersion) {
|
switch (currentVersion) {
|
||||||
@@ -399,6 +478,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||||||
final migrator = i1.Migrator(database, schema);
|
final migrator = i1.Migrator(database, schema);
|
||||||
await from3To4(migrator, schema);
|
await from3To4(migrator, schema);
|
||||||
return 4;
|
return 4;
|
||||||
|
case 4:
|
||||||
|
final schema = Schema5(database: database);
|
||||||
|
final migrator = i1.Migrator(database, schema);
|
||||||
|
await from4To5(migrator, schema);
|
||||||
|
return 5;
|
||||||
default:
|
default:
|
||||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||||
}
|
}
|
||||||
@@ -408,6 +492,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||||||
i1.OnUpgrade stepByStep({
|
i1.OnUpgrade stepByStep({
|
||||||
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,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema5 schema) from4To5,
|
||||||
}) => i0.VersionedSchema.stepByStepHelper(
|
}) => i0.VersionedSchema.stepByStepHelper(
|
||||||
step: migrationSteps(from2To3: from2To3, from3To4: from3To4),
|
step: migrationSteps(
|
||||||
|
from2To3: from2To3,
|
||||||
|
from3To4: from3To4,
|
||||||
|
from4To5: from4To5,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -42,9 +42,15 @@ CREATE VIRTUAL TABLE tab_fts
|
|||||||
|
|
||||||
-- Create trigger to handle parent deletion
|
-- Create trigger to handle parent deletion
|
||||||
CREATE TRIGGER tab_maintain_parent_chain_on_delete BEFORE DELETE ON tab BEGIN
|
CREATE TRIGGER tab_maintain_parent_chain_on_delete BEFORE DELETE ON tab BEGIN
|
||||||
-- Update all children of the deleted row to point to its parent
|
-- Update all children of the deleted row to point to its parent (grandparent)
|
||||||
UPDATE tab
|
-- Only if the grandparent exists, otherwise set to NULL
|
||||||
SET parent_id = OLD.parent_id
|
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;
|
WHERE parent_id = OLD.id;
|
||||||
END;
|
END;
|
||||||
|
|
||||||
|
|||||||
@@ -2195,7 +2195,7 @@ class TabFtsCompanion extends i0.UpdateCompanion<i3.TabFt> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
i0.Trigger get tabMaintainParentChainOnDelete => i0.Trigger(
|
i0.Trigger get tabMaintainParentChainOnDelete => i0.Trigger(
|
||||||
'CREATE TRIGGER tab_maintain_parent_chain_on_delete BEFORE DELETE ON tab BEGIN UPDATE tab SET parent_id = OLD.parent_id WHERE parent_id = OLD.id;END',
|
'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',
|
'tab_maintain_parent_chain_on_delete',
|
||||||
);
|
);
|
||||||
i0.Trigger get tabAfterInsert => i0.Trigger(
|
i0.Trigger get tabAfterInsert => i0.Trigger(
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:drift/internal/migrations.dart';
|
|||||||
import 'schema_v2.dart' as v2;
|
import 'schema_v2.dart' as v2;
|
||||||
import 'schema_v3.dart' as v3;
|
import 'schema_v3.dart' as v3;
|
||||||
import 'schema_v4.dart' as v4;
|
import 'schema_v4.dart' as v4;
|
||||||
|
import 'schema_v5.dart' as v5;
|
||||||
|
|
||||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||||
@override
|
@override
|
||||||
@@ -17,10 +18,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
|||||||
return v3.DatabaseAtV3(db);
|
return v3.DatabaseAtV3(db);
|
||||||
case 4:
|
case 4:
|
||||||
return v4.DatabaseAtV4(db);
|
return v4.DatabaseAtV4(db);
|
||||||
|
case 5:
|
||||||
|
return v5.DatabaseAtV5(db);
|
||||||
default:
|
default:
|
||||||
throw MissingSchemaException(version, versions);
|
throw MissingSchemaException(version, versions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static const versions = const [2, 3, 4];
|
static const versions = const [2, 3, 4, 5];
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user