From 1b8eaf2c7677b2a4559e91e8baa034e835b6eb8f Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Tue, 26 May 2026 16:15:14 +0200 Subject: [PATCH] per-containder indexing opt out --- .../drift_schemas/tabs/drift_schema_v14.json | 1203 +++++++ .../geckoview/domain/repositories/tab.g.dart | 2 +- .../features/tabs/data/database/database.dart | 17 +- .../tabs/data/database/database.drift.dart | 22 + .../tabs/data/database/database.steps.dart | 224 ++ .../tabs/data/database/definitions.drift | 188 +- .../tabs/data/database/definitions.drift.dart | 12 +- .../tabs/data/models/container_data.dart | 10 + .../tabs/data/models/container_data.g.dart | 16 + .../features/tabs/domain/providers.g.dart | 2 +- .../tabs/domain/repositories/tab.g.dart | 2 +- .../presentation/screens/container_edit.dart | 16 + .../test/drift/tabs/generated/schema.dart | 5 +- .../test/drift/tabs/generated/schema_v14.dart | 3198 +++++++++++++++++ .../drift/tabs/local_history_index_test.dart | 257 ++ .../test/drift/tabs/migration_test.dart | 5 +- 16 files changed, 5164 insertions(+), 15 deletions(-) create mode 100644 apps/weblibre/drift_schemas/tabs/drift_schema_v14.json create mode 100644 apps/weblibre/test/drift/tabs/generated/schema_v14.dart create mode 100644 apps/weblibre/test/drift/tabs/local_history_index_test.dart diff --git a/apps/weblibre/drift_schemas/tabs/drift_schema_v14.json b/apps/weblibre/drift_schemas/tabs/drift_schema_v14.json new file mode 100644 index 00000000..dbe266cf --- /dev/null +++ b/apps/weblibre/drift_schemas/tabs/drift_schema_v14.json @@ -0,0 +1,1203 @@ +{ + "_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": "order_key", + "getter_name": "orderKey", + "moor_type": "string", + "nullable": false, + "customConstraints": "NOT NULL", + "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": "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": [] + }, + { + "name": "content_hash", + "getter_name": "contentHash", + "moor_type": "int", + "nullable": true, + "customConstraints": "GENERATED ALWAYS AS (generate_content_hash(title, extracted_content_plain, full_content_plain)) VIRTUAL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "generated_as": { + "dart_expression": { + "elements": [ + "const ", + { + "lexeme": "CustomExpression", + "import_uri": "package:drift/drift.dart" + }, + "('generate_content_hash(title, extracted_content_plain, full_content_plain)')" + ] + }, + "stored": false + } + } + ] + } + ], + "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": "capture_tab", + "was_declared_in_moor": true, + "columns": [ + { + "name": "tab_id", + "getter_name": "tabId", + "moor_type": "string", + "nullable": false, + "customConstraints": "NOT NULL PRIMARY KEY REFERENCES tab(id)ON DELETE CASCADE", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "primary-key", + { + "foreign_key": { + "to": { + "table": "tab", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "capture_id", + "getter_name": "captureId", + "moor_type": "string", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "source_url", + "getter_name": "sourceUrl", + "moor_type": "string", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "status", + "getter_name": "status", + "moor_type": "string", + "nullable": false, + "customConstraints": "NOT NULL DEFAULT 'pending'", + "default_dart": "const CustomExpression('\\'pending\\'')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "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": 5, + "references": [ + 4 + ], + "type": "index", + "data": { + "on": 4, + "name": "idx_capture_tab_capture_id", + "sql": "CREATE INDEX idx_capture_tab_capture_id ON capture_tab(capture_id);", + "unique": false, + "columns": [] + } + }, + { + "id": 6, + "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": 7, + "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": 8, + "references": [ + 1, + 6 + ], + "type": "trigger", + "data": { + "on": 1, + "references_in_body": [ + 1, + 6 + ], + "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": 9, + "references": [ + 1, + 6 + ], + "type": "trigger", + "data": { + "on": 1, + "references_in_body": [ + 1, + 6 + ], + "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": 10, + "references": [ + 1, + 6 + ], + "type": "trigger", + "data": { + "on": 1, + "references_in_body": [ + 1, + 6 + ], + "name": "tab_after_update", + "sql": "CREATE TRIGGER tab_after_update AFTER UPDATE OF\n title, url, extracted_content_plain, full_content_plain\nON tab\nWHEN OLD.content_hash IS NOT NEW.content_hash\n OR OLD.url IS NOT NEW.url\nBEGIN\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;" + } + }, + { + "id": 11, + "references": [], + "type": "table", + "data": { + "name": "local_index_setting", + "was_declared_in_moor": true, + "columns": [ + { + "name": "key", + "getter_name": "key", + "moor_type": "string", + "nullable": false, + "customConstraints": "PRIMARY KEY NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "primary-key" + ] + }, + { + "name": "value", + "getter_name": "value", + "moor_type": "int", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "strict": true + } + }, + { + "id": 12, + "references": [], + "type": "table", + "data": { + "name": "history", + "was_declared_in_moor": true, + "columns": [ + { + "name": "url_canonical", + "getter_name": "urlCanonical", + "moor_type": "string", + "nullable": false, + "customConstraints": "PRIMARY KEY NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "primary-key" + ] + }, + { + "name": "url_host", + "getter_name": "urlHost", + "moor_type": "string", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "url_path", + "getter_name": "urlPath", + "moor_type": "string", + "nullable": true, + "customConstraints": "", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "title", + "getter_name": "title", + "moor_type": "string", + "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": "content_hash", + "getter_name": "contentHash", + "moor_type": "int", + "nullable": true, + "customConstraints": "", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "observed_at", + "getter_name": "observedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": "NOT NULL", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "observed_count", + "getter_name": "observedCount", + "moor_type": "int", + "nullable": false, + "customConstraints": "NOT NULL DEFAULT 1", + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 13, + "references": [ + 12 + ], + "type": "index", + "data": { + "on": 12, + "name": "idx_history_host", + "sql": "CREATE INDEX idx_history_host ON history(url_host);", + "unique": false, + "columns": [] + } + }, + { + "id": 14, + "references": [ + 12 + ], + "type": "index", + "data": { + "on": 12, + "name": "idx_history_observed", + "sql": "CREATE INDEX idx_history_observed ON history(observed_at DESC);", + "unique": false, + "columns": [] + } + }, + { + "id": 15, + "references": [ + 12 + ], + "type": "table", + "data": { + "name": "history_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_host", + "getter_name": "urlHost", + "moor_type": "string", + "nullable": false, + "customConstraints": "", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "url_path", + "getter_name": "urlPath", + "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 \"history_fts\" USING fts5(title, url_host, url_path, extracted_content_plain, full_content_plain, content=history, tokenize=\"trigram\")", + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 16, + "references": [ + 12, + 15 + ], + "type": "trigger", + "data": { + "on": 12, + "references_in_body": [ + 12, + 15 + ], + "name": "history_after_insert", + "sql": "CREATE TRIGGER history_after_insert AFTER INSERT ON history BEGIN\n INSERT INTO\n history_fts(rowid, title, url_host, url_path, extracted_content_plain, full_content_plain)\n VALUES (new.rowid, new.title, new.url_host, new.url_path, new.extracted_content_plain, new.full_content_plain);\nEND;" + } + }, + { + "id": 17, + "references": [ + 12, + 15 + ], + "type": "trigger", + "data": { + "on": 12, + "references_in_body": [ + 12, + 15 + ], + "name": "history_after_delete", + "sql": "CREATE TRIGGER history_after_delete AFTER DELETE ON history BEGIN\n INSERT INTO\n history_fts(history_fts, rowid, title, url_host, url_path, extracted_content_plain, full_content_plain)\n VALUES('delete', old.rowid, old.title, old.url_host, old.url_path, old.extracted_content_plain, old.full_content_plain);\nEND;" + } + }, + { + "id": 18, + "references": [ + 12, + 15 + ], + "type": "trigger", + "data": { + "on": 12, + "references_in_body": [ + 12, + 15 + ], + "name": "history_after_update", + "sql": "CREATE TRIGGER history_after_update AFTER UPDATE OF\n title, url_host, url_path, extracted_content_plain, full_content_plain\nON history BEGIN\n INSERT INTO\n history_fts(history_fts, rowid, title, url_host, url_path, extracted_content_plain, full_content_plain)\n VALUES('delete', old.rowid, old.title, old.url_host, old.url_path, old.extracted_content_plain, old.full_content_plain);\n INSERT INTO\n history_fts(rowid, title, url_host, url_path, extracted_content_plain, full_content_plain)\n VALUES (new.rowid, new.title, new.url_host, new.url_path, new.extracted_content_plain, new.full_content_plain);\nEND;" + } + }, + { + "id": 19, + "references": [ + 1, + 11, + 0, + 12 + ], + "type": "trigger", + "data": { + "on": 1, + "references_in_body": [ + 1, + 11, + 0, + 12 + ], + "name": "tab_to_history_on_insert", + "sql": "CREATE TRIGGER tab_to_history_on_insert AFTER INSERT ON tab\nWHEN NEW.url IS NOT NULL\n AND url_indexable(CAST(NEW.url AS TEXT)) = 1\n AND (SELECT value FROM local_index_setting WHERE \"key\" = 'enabled') = 1\n AND (NEW.tab_mode != 1\n OR (SELECT value FROM local_index_setting WHERE \"key\" = 'index_private') = 1)\n AND NOT EXISTS (\n SELECT 1 FROM container\n WHERE container.id = NEW.container_id\n AND json_extract(container.metadata, '$.excludeFromIndex') = 1\n )\nBEGIN\n INSERT INTO history (\n url_canonical, url_host, url_path, title, is_probably_readerable,\n extracted_content_markdown, extracted_content_plain,\n full_content_markdown, full_content_plain,\n content_hash, observed_at, observed_count\n ) VALUES (\n url_canonical(CAST(NEW.url AS TEXT)),\n url_host(CAST(NEW.url AS TEXT)),\n url_path(CAST(NEW.url AS TEXT)),\n NEW.title,\n NEW.is_probably_readerable,\n NEW.extracted_content_markdown,\n NEW.extracted_content_plain,\n NEW.full_content_markdown,\n NEW.full_content_plain,\n NEW.content_hash,\n strftime('%s','now') * 1000,\n 1\n )\n ON CONFLICT(url_canonical) DO UPDATE SET\n title = COALESCE(excluded.title, history.title),\n is_probably_readerable = excluded.is_probably_readerable,\n extracted_content_markdown = excluded.extracted_content_markdown,\n extracted_content_plain = excluded.extracted_content_plain,\n full_content_markdown = excluded.full_content_markdown,\n full_content_plain = excluded.full_content_plain,\n content_hash = excluded.content_hash,\n observed_at = excluded.observed_at,\n observed_count = history.observed_count + 1\n WHERE history.content_hash IS NOT excluded.content_hash;\nEND;" + } + }, + { + "id": 20, + "references": [ + 1, + 11, + 0, + 12 + ], + "type": "trigger", + "data": { + "on": 1, + "references_in_body": [ + 1, + 11, + 0, + 12 + ], + "name": "tab_to_history_on_update", + "sql": "CREATE TRIGGER tab_to_history_on_update AFTER UPDATE OF\n title, url, extracted_content_plain, extracted_content_markdown,\n full_content_plain, full_content_markdown, is_probably_readerable\nON tab\nWHEN NEW.url IS NOT NULL\n AND url_indexable(CAST(NEW.url AS TEXT)) = 1\n AND (OLD.content_hash IS NOT NEW.content_hash\n OR OLD.url IS NOT NEW.url)\n AND (SELECT value FROM local_index_setting WHERE \"key\" = 'enabled') = 1\n AND (NEW.tab_mode != 1\n OR (SELECT value FROM local_index_setting WHERE \"key\" = 'index_private') = 1)\n AND NOT EXISTS (\n SELECT 1 FROM container\n WHERE container.id = NEW.container_id\n AND json_extract(container.metadata, '$.excludeFromIndex') = 1\n )\nBEGIN\n INSERT INTO history (\n url_canonical, url_host, url_path, title, is_probably_readerable,\n extracted_content_markdown, extracted_content_plain,\n full_content_markdown, full_content_plain,\n content_hash, observed_at, observed_count\n ) VALUES (\n url_canonical(CAST(NEW.url AS TEXT)),\n url_host(CAST(NEW.url AS TEXT)),\n url_path(CAST(NEW.url AS TEXT)),\n NEW.title,\n NEW.is_probably_readerable,\n NEW.extracted_content_markdown,\n NEW.extracted_content_plain,\n NEW.full_content_markdown,\n NEW.full_content_plain,\n NEW.content_hash,\n strftime('%s','now') * 1000,\n 1\n )\n ON CONFLICT(url_canonical) DO UPDATE SET\n title = COALESCE(excluded.title, history.title),\n is_probably_readerable = excluded.is_probably_readerable,\n extracted_content_markdown = excluded.extracted_content_markdown,\n extracted_content_plain = excluded.extracted_content_plain,\n full_content_markdown = excluded.full_content_markdown,\n full_content_plain = excluded.full_content_plain,\n content_hash = excluded.content_hash,\n observed_at = excluded.observed_at,\n observed_count = history.observed_count + 1\n WHERE history.content_hash IS NOT excluded.content_hash;\nEND;" + } + }, + { + "id": 21, + "references": [ + 1, + 11, + 12, + 0 + ], + "type": "trigger", + "data": { + "on": 1, + "references_in_body": [ + 1, + 11, + 12, + 0 + ], + "name": "tab_to_history_on_container_update", + "sql": "CREATE TRIGGER tab_to_history_on_container_update AFTER UPDATE OF container_id\nON tab\nWHEN NEW.url IS NOT NULL\n AND url_indexable(CAST(NEW.url AS TEXT)) = 1\n AND (SELECT value FROM local_index_setting WHERE \"key\" = 'enabled') = 1\nBEGIN\n DELETE FROM history\n WHERE url_canonical = url_canonical(CAST(NEW.url AS TEXT))\n AND NOT EXISTS (\n SELECT 1\n FROM tab AS candidate\n WHERE candidate.url IS NOT NULL\n AND url_indexable(CAST(candidate.url AS TEXT)) = 1\n AND url_canonical(CAST(candidate.url AS TEXT)) = history.url_canonical\n AND (candidate.tab_mode != 1\n OR (SELECT value FROM local_index_setting WHERE \"key\" = 'index_private') = 1)\n AND NOT EXISTS (\n SELECT 1 FROM container\n WHERE container.id = candidate.container_id\n AND json_extract(container.metadata, '$.excludeFromIndex') = 1\n )\n );\n\n INSERT INTO history (\n url_canonical, url_host, url_path, title, is_probably_readerable,\n extracted_content_markdown, extracted_content_plain,\n full_content_markdown, full_content_plain,\n content_hash, observed_at, observed_count\n )\n SELECT\n url_canonical(CAST(candidate.url AS TEXT)),\n url_host(CAST(candidate.url AS TEXT)),\n url_path(CAST(candidate.url AS TEXT)),\n candidate.title,\n candidate.is_probably_readerable,\n candidate.extracted_content_markdown,\n candidate.extracted_content_plain,\n candidate.full_content_markdown,\n candidate.full_content_plain,\n candidate.content_hash,\n strftime('%s','now') * 1000,\n 1\n FROM tab AS candidate\n WHERE candidate.url IS NOT NULL\n AND url_indexable(CAST(candidate.url AS TEXT)) = 1\n AND url_canonical(CAST(candidate.url AS TEXT)) = url_canonical(CAST(NEW.url AS TEXT))\n AND (candidate.tab_mode != 1\n OR (SELECT value FROM local_index_setting WHERE \"key\" = 'index_private') = 1)\n AND NOT EXISTS (\n SELECT 1 FROM container\n WHERE container.id = candidate.container_id\n AND json_extract(container.metadata, '$.excludeFromIndex') = 1\n )\n ORDER BY candidate.timestamp DESC, candidate.rowid DESC\n LIMIT 1\n ON CONFLICT(url_canonical) DO UPDATE SET\n title = COALESCE(excluded.title, history.title),\n is_probably_readerable = excluded.is_probably_readerable,\n extracted_content_markdown = excluded.extracted_content_markdown,\n extracted_content_plain = excluded.extracted_content_plain,\n full_content_markdown = excluded.full_content_markdown,\n full_content_plain = excluded.full_content_plain,\n content_hash = excluded.content_hash,\n observed_at = excluded.observed_at,\n observed_count = history.observed_count + 1;\nEND;" + } + }, + { + "id": 22, + "references": [ + 0, + 11, + 12, + 1 + ], + "type": "trigger", + "data": { + "on": 0, + "references_in_body": [ + 0, + 11, + 12, + 1 + ], + "name": "container_to_history_on_metadata_update", + "sql": "CREATE TRIGGER container_to_history_on_metadata_update AFTER UPDATE OF metadata\nON container\nWHEN COALESCE(json_extract(OLD.metadata, '$.excludeFromIndex') = 1, 0)\n != COALESCE(json_extract(NEW.metadata, '$.excludeFromIndex') = 1, 0)\n AND (SELECT value FROM local_index_setting WHERE \"key\" = 'enabled') = 1\nBEGIN\n DELETE FROM history\n WHERE url_canonical IN (\n SELECT DISTINCT url_canonical(CAST(affected.url AS TEXT))\n FROM tab AS affected\n WHERE affected.container_id = NEW.id\n AND affected.url IS NOT NULL\n AND url_indexable(CAST(affected.url AS TEXT)) = 1\n )\n AND NOT EXISTS (\n SELECT 1\n FROM tab AS candidate\n WHERE candidate.url IS NOT NULL\n AND url_indexable(CAST(candidate.url AS TEXT)) = 1\n AND url_canonical(CAST(candidate.url AS TEXT)) = history.url_canonical\n AND (candidate.tab_mode != 1\n OR (SELECT value FROM local_index_setting WHERE \"key\" = 'index_private') = 1)\n AND NOT EXISTS (\n SELECT 1 FROM container\n WHERE container.id = candidate.container_id\n AND json_extract(container.metadata, '$.excludeFromIndex') = 1\n )\n );\n\n INSERT INTO history (\n url_canonical, url_host, url_path, title, is_probably_readerable,\n extracted_content_markdown, extracted_content_plain,\n full_content_markdown, full_content_plain,\n content_hash, observed_at, observed_count\n )\n SELECT\n url_canonical(CAST(candidate.url AS TEXT)),\n url_host(CAST(candidate.url AS TEXT)),\n url_path(CAST(candidate.url AS TEXT)),\n candidate.title,\n candidate.is_probably_readerable,\n candidate.extracted_content_markdown,\n candidate.extracted_content_plain,\n candidate.full_content_markdown,\n candidate.full_content_plain,\n candidate.content_hash,\n strftime('%s','now') * 1000,\n 1\n FROM tab AS candidate\n WHERE candidate.url IS NOT NULL\n AND url_indexable(CAST(candidate.url AS TEXT)) = 1\n AND url_canonical(CAST(candidate.url AS TEXT)) IN (\n SELECT DISTINCT url_canonical(CAST(affected.url AS TEXT))\n FROM tab AS affected\n WHERE affected.container_id = NEW.id\n AND affected.url IS NOT NULL\n AND url_indexable(CAST(affected.url AS TEXT)) = 1\n )\n AND (candidate.tab_mode != 1\n OR (SELECT value FROM local_index_setting WHERE \"key\" = 'index_private') = 1)\n AND NOT EXISTS (\n SELECT 1 FROM container\n WHERE container.id = candidate.container_id\n AND json_extract(container.metadata, '$.excludeFromIndex') = 1\n )\n AND NOT EXISTS (\n SELECT 1\n FROM tab AS newer\n WHERE newer.url IS NOT NULL\n AND url_indexable(CAST(newer.url AS TEXT)) = 1\n AND url_canonical(CAST(newer.url AS TEXT)) = url_canonical(CAST(candidate.url AS TEXT))\n AND (newer.tab_mode != 1\n OR (SELECT value FROM local_index_setting WHERE \"key\" = 'index_private') = 1)\n AND NOT EXISTS (\n SELECT 1 FROM container\n WHERE container.id = newer.container_id\n AND json_extract(container.metadata, '$.excludeFromIndex') = 1\n )\n AND (\n newer.timestamp > candidate.timestamp\n OR (newer.timestamp = candidate.timestamp AND newer.rowid > candidate.rowid)\n )\n )\n ON CONFLICT(url_canonical) DO UPDATE SET\n title = COALESCE(excluded.title, history.title),\n is_probably_readerable = excluded.is_probably_readerable,\n extracted_content_markdown = excluded.extracted_content_markdown,\n extracted_content_plain = excluded.extracted_content_plain,\n full_content_markdown = excluded.full_content_markdown,\n full_content_plain = excluded.full_content_plain,\n content_hash = excluded.content_hash,\n observed_at = excluded.observed_at,\n observed_count = history.observed_count + 1;\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, \"order_key\" TEXT NOT NULL, \"is_pinned\" INTEGER NOT NULL DEFAULT 0, \"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, \"content_hash\" INTEGER GENERATED ALWAYS AS (generate_content_hash(title, extracted_content_plain, full_content_plain)) VIRTUAL, 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": "capture_tab", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"capture_tab\" (\"tab_id\" TEXT NOT NULL PRIMARY KEY REFERENCES tab(id)ON DELETE CASCADE, \"capture_id\" TEXT NOT NULL, \"source_url\" TEXT NOT NULL, \"status\" TEXT NOT NULL DEFAULT 'pending', \"created_at\" INTEGER NOT NULL);" + } + ] + }, + { + "name": "idx_capture_tab_capture_id", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX idx_capture_tab_capture_id ON capture_tab (capture_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 OF title, url, extracted_content_plain, full_content_plain ON tab WHEN OLD.content_hash IS NOT NEW.content_hash OR OLD.url IS NOT NEW.url 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" + } + ] + }, + { + "name": "local_index_setting", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"local_index_setting\" (\"key\" TEXT PRIMARY KEY NOT NULL, \"value\" INTEGER NOT NULL) STRICT;" + } + ] + }, + { + "name": "history", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"history\" (\"url_canonical\" TEXT PRIMARY KEY NOT NULL, \"url_host\" TEXT NOT NULL, \"url_path\" TEXT, \"title\" TEXT, \"is_probably_readerable\" INTEGER, \"extracted_content_markdown\" TEXT, \"extracted_content_plain\" TEXT, \"full_content_markdown\" TEXT, \"full_content_plain\" TEXT, \"content_hash\" INTEGER, \"observed_at\" INTEGER NOT NULL, \"observed_count\" INTEGER NOT NULL DEFAULT 1);" + } + ] + }, + { + "name": "idx_history_host", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX idx_history_host ON history (url_host)" + } + ] + }, + { + "name": "idx_history_observed", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX idx_history_observed ON history (observed_at DESC)" + } + ] + }, + { + "name": "history_fts", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE VIRTUAL TABLE IF NOT EXISTS \"history_fts\" USING fts5(title, url_host, url_path, extracted_content_plain, full_content_plain, content=history, tokenize=\"trigram\");" + } + ] + }, + { + "name": "history_after_insert", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TRIGGER history_after_insert AFTER INSERT ON history BEGIN INSERT INTO history_fts (\"rowid\", title, url_host, url_path, extracted_content_plain, full_content_plain) VALUES (new.\"rowid\", new.title, new.url_host, new.url_path, new.extracted_content_plain, new.full_content_plain);END" + } + ] + }, + { + "name": "history_after_delete", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TRIGGER history_after_delete AFTER DELETE ON history BEGIN INSERT INTO history_fts (history_fts, \"rowid\", title, url_host, url_path, extracted_content_plain, full_content_plain) VALUES ('delete', old.\"rowid\", old.title, old.url_host, old.url_path, old.extracted_content_plain, old.full_content_plain);END" + } + ] + }, + { + "name": "history_after_update", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TRIGGER history_after_update AFTER UPDATE OF title, url_host, url_path, extracted_content_plain, full_content_plain ON history BEGIN INSERT INTO history_fts (history_fts, \"rowid\", title, url_host, url_path, extracted_content_plain, full_content_plain) VALUES ('delete', old.\"rowid\", old.title, old.url_host, old.url_path, old.extracted_content_plain, old.full_content_plain);INSERT INTO history_fts (\"rowid\", title, url_host, url_path, extracted_content_plain, full_content_plain) VALUES (new.\"rowid\", new.title, new.url_host, new.url_path, new.extracted_content_plain, new.full_content_plain);END" + } + ] + }, + { + "name": "tab_to_history_on_insert", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TRIGGER tab_to_history_on_insert AFTER INSERT ON tab WHEN NEW.url IS NOT NULL AND url_indexable(CAST(NEW.url AS TEXT)) = 1 AND (SELECT value FROM local_index_setting WHERE \"key\" = 'enabled') = 1 AND(NEW.tab_mode != 1 OR (SELECT value FROM local_index_setting WHERE \"key\" = 'index_private') = 1)AND NOT EXISTS (SELECT 1 FROM container WHERE container.id = NEW.container_id AND json_extract(container.metadata, '$.excludeFromIndex') = 1) BEGIN INSERT INTO history (url_canonical, url_host, url_path, title, is_probably_readerable, extracted_content_markdown, extracted_content_plain, full_content_markdown, full_content_plain, content_hash, observed_at, observed_count) VALUES (url_canonical(CAST(NEW.url AS TEXT)), url_host(CAST(NEW.url AS TEXT)), url_path(CAST(NEW.url AS TEXT)), NEW.title, NEW.is_probably_readerable, NEW.extracted_content_markdown, NEW.extracted_content_plain, NEW.full_content_markdown, NEW.full_content_plain, NEW.content_hash, strftime('%s', 'now') * 1000, 1) ON CONFLICT (url_canonical) DO UPDATE SET title = COALESCE(excluded.title, history.title), is_probably_readerable = excluded.is_probably_readerable, extracted_content_markdown = excluded.extracted_content_markdown, extracted_content_plain = excluded.extracted_content_plain, full_content_markdown = excluded.full_content_markdown, full_content_plain = excluded.full_content_plain, content_hash = excluded.content_hash, observed_at = excluded.observed_at, observed_count = history.observed_count + 1 WHERE history.content_hash IS NOT excluded.content_hash;END" + } + ] + }, + { + "name": "tab_to_history_on_update", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TRIGGER tab_to_history_on_update AFTER UPDATE OF title, url, extracted_content_plain, extracted_content_markdown, full_content_plain, full_content_markdown, is_probably_readerable ON tab WHEN NEW.url IS NOT NULL AND url_indexable(CAST(NEW.url AS TEXT)) = 1 AND(OLD.content_hash IS NOT NEW.content_hash OR OLD.url IS NOT NEW.url)AND (SELECT value FROM local_index_setting WHERE \"key\" = 'enabled') = 1 AND(NEW.tab_mode != 1 OR (SELECT value FROM local_index_setting WHERE \"key\" = 'index_private') = 1)AND NOT EXISTS (SELECT 1 FROM container WHERE container.id = NEW.container_id AND json_extract(container.metadata, '$.excludeFromIndex') = 1) BEGIN INSERT INTO history (url_canonical, url_host, url_path, title, is_probably_readerable, extracted_content_markdown, extracted_content_plain, full_content_markdown, full_content_plain, content_hash, observed_at, observed_count) VALUES (url_canonical(CAST(NEW.url AS TEXT)), url_host(CAST(NEW.url AS TEXT)), url_path(CAST(NEW.url AS TEXT)), NEW.title, NEW.is_probably_readerable, NEW.extracted_content_markdown, NEW.extracted_content_plain, NEW.full_content_markdown, NEW.full_content_plain, NEW.content_hash, strftime('%s', 'now') * 1000, 1) ON CONFLICT (url_canonical) DO UPDATE SET title = COALESCE(excluded.title, history.title), is_probably_readerable = excluded.is_probably_readerable, extracted_content_markdown = excluded.extracted_content_markdown, extracted_content_plain = excluded.extracted_content_plain, full_content_markdown = excluded.full_content_markdown, full_content_plain = excluded.full_content_plain, content_hash = excluded.content_hash, observed_at = excluded.observed_at, observed_count = history.observed_count + 1 WHERE history.content_hash IS NOT excluded.content_hash;END" + } + ] + }, + { + "name": "tab_to_history_on_container_update", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TRIGGER tab_to_history_on_container_update AFTER UPDATE OF container_id ON tab WHEN NEW.url IS NOT NULL AND url_indexable(CAST(NEW.url AS TEXT)) = 1 AND (SELECT value FROM local_index_setting WHERE \"key\" = 'enabled') = 1 BEGIN DELETE FROM history WHERE url_canonical = url_canonical(CAST(NEW.url AS TEXT)) AND NOT EXISTS (SELECT 1 FROM tab AS candidate WHERE candidate.url IS NOT NULL AND url_indexable(CAST(candidate.url AS TEXT)) = 1 AND url_canonical(CAST(candidate.url AS TEXT)) = history.url_canonical AND(candidate.tab_mode != 1 OR (SELECT value FROM local_index_setting WHERE \"key\" = 'index_private') = 1)AND NOT EXISTS (SELECT 1 FROM container WHERE container.id = candidate.container_id AND json_extract(container.metadata, '$.excludeFromIndex') = 1));INSERT INTO history (url_canonical, url_host, url_path, title, is_probably_readerable, extracted_content_markdown, extracted_content_plain, full_content_markdown, full_content_plain, content_hash, observed_at, observed_count) SELECT url_canonical(CAST(candidate.url AS TEXT)), url_host(CAST(candidate.url AS TEXT)), url_path(CAST(candidate.url AS TEXT)), candidate.title, candidate.is_probably_readerable, candidate.extracted_content_markdown, candidate.extracted_content_plain, candidate.full_content_markdown, candidate.full_content_plain, candidate.content_hash, strftime('%s', 'now') * 1000, 1 FROM tab AS candidate WHERE candidate.url IS NOT NULL AND url_indexable(CAST(candidate.url AS TEXT)) = 1 AND url_canonical(CAST(candidate.url AS TEXT)) = url_canonical(CAST(NEW.url AS TEXT)) AND(candidate.tab_mode != 1 OR (SELECT value FROM local_index_setting WHERE \"key\" = 'index_private') = 1)AND NOT EXISTS (SELECT 1 FROM container WHERE container.id = candidate.container_id AND json_extract(container.metadata, '$.excludeFromIndex') = 1) ORDER BY candidate.timestamp DESC, candidate.\"rowid\" DESC LIMIT 1 ON CONFLICT (url_canonical) DO UPDATE SET title = COALESCE(excluded.title, history.title), is_probably_readerable = excluded.is_probably_readerable, extracted_content_markdown = excluded.extracted_content_markdown, extracted_content_plain = excluded.extracted_content_plain, full_content_markdown = excluded.full_content_markdown, full_content_plain = excluded.full_content_plain, content_hash = excluded.content_hash, observed_at = excluded.observed_at, observed_count = history.observed_count + 1;END" + } + ] + }, + { + "name": "container_to_history_on_metadata_update", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TRIGGER container_to_history_on_metadata_update AFTER UPDATE OF metadata ON container WHEN COALESCE(json_extract(OLD.metadata, '$.excludeFromIndex') = 1, 0) != COALESCE(json_extract(NEW.metadata, '$.excludeFromIndex') = 1, 0) AND (SELECT value FROM local_index_setting WHERE \"key\" = 'enabled') = 1 BEGIN DELETE FROM history WHERE url_canonical IN (SELECT DISTINCT url_canonical(CAST(affected.url AS TEXT)) FROM tab AS affected WHERE affected.container_id = NEW.id AND affected.url IS NOT NULL AND url_indexable(CAST(affected.url AS TEXT)) = 1) AND NOT EXISTS (SELECT 1 FROM tab AS candidate WHERE candidate.url IS NOT NULL AND url_indexable(CAST(candidate.url AS TEXT)) = 1 AND url_canonical(CAST(candidate.url AS TEXT)) = history.url_canonical AND(candidate.tab_mode != 1 OR (SELECT value FROM local_index_setting WHERE \"key\" = 'index_private') = 1)AND NOT EXISTS (SELECT 1 FROM container WHERE container.id = candidate.container_id AND json_extract(container.metadata, '$.excludeFromIndex') = 1));INSERT INTO history (url_canonical, url_host, url_path, title, is_probably_readerable, extracted_content_markdown, extracted_content_plain, full_content_markdown, full_content_plain, content_hash, observed_at, observed_count) SELECT url_canonical(CAST(candidate.url AS TEXT)), url_host(CAST(candidate.url AS TEXT)), url_path(CAST(candidate.url AS TEXT)), candidate.title, candidate.is_probably_readerable, candidate.extracted_content_markdown, candidate.extracted_content_plain, candidate.full_content_markdown, candidate.full_content_plain, candidate.content_hash, strftime('%s', 'now') * 1000, 1 FROM tab AS candidate WHERE candidate.url IS NOT NULL AND url_indexable(CAST(candidate.url AS TEXT)) = 1 AND url_canonical(CAST(candidate.url AS TEXT)) IN (SELECT DISTINCT url_canonical(CAST(affected.url AS TEXT)) FROM tab AS affected WHERE affected.container_id = NEW.id AND affected.url IS NOT NULL AND url_indexable(CAST(affected.url AS TEXT)) = 1) AND(candidate.tab_mode != 1 OR (SELECT value FROM local_index_setting WHERE \"key\" = 'index_private') = 1)AND NOT EXISTS (SELECT 1 FROM container WHERE container.id = candidate.container_id AND json_extract(container.metadata, '$.excludeFromIndex') = 1) AND NOT EXISTS (SELECT 1 FROM tab AS newer WHERE newer.url IS NOT NULL AND url_indexable(CAST(newer.url AS TEXT)) = 1 AND url_canonical(CAST(newer.url AS TEXT)) = url_canonical(CAST(candidate.url AS TEXT)) AND(newer.tab_mode != 1 OR (SELECT value FROM local_index_setting WHERE \"key\" = 'index_private') = 1)AND NOT EXISTS (SELECT 1 FROM container WHERE container.id = newer.container_id AND json_extract(container.metadata, '$.excludeFromIndex') = 1) AND(newer.timestamp > candidate.timestamp OR(newer.timestamp = candidate.timestamp AND newer.\"rowid\" > candidate.\"rowid\"))) ON CONFLICT (url_canonical) DO UPDATE SET title = COALESCE(excluded.title, history.title), is_probably_readerable = excluded.is_probably_readerable, extracted_content_markdown = excluded.extracted_content_markdown, extracted_content_plain = excluded.extracted_content_plain, full_content_markdown = excluded.full_content_markdown, full_content_plain = excluded.full_content_plain, content_hash = excluded.content_hash, observed_at = excluded.observed_at, observed_count = history.observed_count + 1;END" + } + ] + } + ] +} \ No newline at end of file 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 8d57f5cb..433e3e8f 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'2cd5b6bf5bb9a62c05b038b28acef59cecbeae4e'; +String _$tabRepositoryHash() => r'7d41038ac5a20ecf6de0645b99231fb8b2f6963a'; abstract class _$TabRepository extends $Notifier { void build(); 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 63e28166..0439ef4c 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 @@ -40,7 +40,7 @@ import 'package:weblibre/features/search/domain/fts_tokenizer.dart'; ) class TabDatabase extends $TabDatabase with TrigramQueryBuilderMixin { @override - final int schemaVersion = 13; + final int schemaVersion = 14; @override final int ftsTokenLimit = 10; @@ -63,7 +63,9 @@ class TabDatabase extends $TabDatabase with TrigramQueryBuilderMixin { await customStatement('PRAGMA foreign_keys = ON'); await definitionsDrift.optimizeFtsIndex(); - await definitionsDrift.optimizeHistoryFtsIndex(); + if (details.versionNow >= 12) { + await definitionsDrift.optimizeHistoryFtsIndex(); + } }, onUpgrade: (m, from, to) async { // Following the advice from https://drift.simonbinder.eu/Migrations/api/#general-tips @@ -234,5 +236,16 @@ class TabDatabase extends $TabDatabase with TrigramQueryBuilderMixin { rank = rank.genNext(); } }, + from13To14: (m, schema) async { + // Add per-container exclude-from-index gate to the tab→history + // triggers, plus re-evaluation triggers for container assignment and + // exclude-flag changes. + await m.drop(schema.tabToHistoryOnInsert); + await m.create(schema.tabToHistoryOnInsert); + await m.drop(schema.tabToHistoryOnUpdate); + await m.create(schema.tabToHistoryOnUpdate); + await m.create(schema.tabToHistoryOnContainerUpdate); + await m.create(schema.containerToHistoryOnMetadataUpdate); + }, ); } 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 bb418bd0..84de99a6 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 @@ -68,6 +68,8 @@ abstract class $TabDatabase extends i0.GeneratedDatabase { i1.historyAfterUpdate, i1.tabToHistoryOnInsert, i1.tabToHistoryOnUpdate, + i1.tabToHistoryOnContainerUpdate, + i1.containerToHistoryOnMetadataUpdate, ]; @override i0.StreamQueryUpdateRules get streamUpdateRules => @@ -149,6 +151,26 @@ abstract class $TabDatabase extends i0.GeneratedDatabase { ), result: [i0.TableUpdate('history', kind: i0.UpdateKind.insert)], ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName( + 'tab', + limitUpdateKind: i0.UpdateKind.update, + ), + result: [ + i0.TableUpdate('history', kind: i0.UpdateKind.delete), + i0.TableUpdate('history', kind: i0.UpdateKind.insert), + ], + ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName( + 'container', + limitUpdateKind: i0.UpdateKind.update, + ), + result: [ + i0.TableUpdate('history', kind: i0.UpdateKind.delete), + i0.TableUpdate('history', kind: i0.UpdateKind.insert), + ], + ), ]); } 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 22b74393..947ba0cd 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 @@ -1823,6 +1823,222 @@ class Shape12 extends i0.VersionedTable { columnsByName['metadata']! as i1.GeneratedColumn; } +final class Schema14 extends i0.VersionedSchema { + Schema14({required super.database}) : super(version: 14); + @override + late final List entities = [ + container, + tab, + closedTabTombstone, + idxTabParentContainer, + captureTab, + idxCaptureTabCaptureId, + tabFts, + tabMaintainParentChainOnDelete, + tabAfterInsert, + tabAfterDelete, + tabAfterUpdate, + localIndexSetting, + history, + idxHistoryHost, + idxHistoryObserved, + historyFts, + historyAfterInsert, + historyAfterDelete, + historyAfterUpdate, + tabToHistoryOnInsert, + tabToHistoryOnUpdate, + tabToHistoryOnContainerUpdate, + containerToHistoryOnMetadataUpdate, + ]; + late final Shape12 container = Shape12( + source: i0.VersionedTable( + entityName: 'container', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_1, + _column_2, + _column_6, + _column_19, + _column_3, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape8 tab = Shape8( + 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, + _column_27, + ], + 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 Shape7 captureTab = Shape7( + source: i0.VersionedTable( + entityName: 'capture_tab', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [_column_22, _column_23, _column_24, _column_25, _column_26], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxCaptureTabCaptureId = i1.Index( + 'idx_capture_tab_capture_id', + 'CREATE INDEX idx_capture_tab_capture_id ON capture_tab (capture_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 OF title, url, extracted_content_plain, full_content_plain ON tab WHEN OLD.content_hash IS NOT NEW.content_hash OR OLD.url IS NOT NEW.url 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', + ); + late final Shape9 localIndexSetting = Shape9( + source: i0.VersionedTable( + entityName: 'local_index_setting', + withoutRowId: false, + isStrict: true, + tableConstraints: [], + columns: [_column_28, _column_29], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape10 history = Shape10( + source: i0.VersionedTable( + entityName: 'history', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_30, + _column_31, + _column_32, + _column_8, + _column_10, + _column_11, + _column_12, + _column_13, + _column_14, + _column_33, + _column_34, + _column_35, + ], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxHistoryHost = i1.Index( + 'idx_history_host', + 'CREATE INDEX idx_history_host ON history (url_host)', + ); + final i1.Index idxHistoryObserved = i1.Index( + 'idx_history_observed', + 'CREATE INDEX idx_history_observed ON history (observed_at DESC)', + ); + late final Shape11 historyFts = Shape11( + source: i0.VersionedVirtualTable( + entityName: 'history_fts', + moduleAndArgs: + 'fts5(title, url_host, url_path, extracted_content_plain, full_content_plain, content=history, tokenize="trigram")', + columns: [_column_8, _column_36, _column_32, _column_12, _column_14], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Trigger historyAfterInsert = i1.Trigger( + 'CREATE TRIGGER history_after_insert AFTER INSERT ON history BEGIN INSERT INTO history_fts ("rowid", title, url_host, url_path, extracted_content_plain, full_content_plain) VALUES (new."rowid", new.title, new.url_host, new.url_path, new.extracted_content_plain, new.full_content_plain);END', + 'history_after_insert', + ); + final i1.Trigger historyAfterDelete = i1.Trigger( + 'CREATE TRIGGER history_after_delete AFTER DELETE ON history BEGIN INSERT INTO history_fts (history_fts, "rowid", title, url_host, url_path, extracted_content_plain, full_content_plain) VALUES (\'delete\', old."rowid", old.title, old.url_host, old.url_path, old.extracted_content_plain, old.full_content_plain);END', + 'history_after_delete', + ); + final i1.Trigger historyAfterUpdate = i1.Trigger( + 'CREATE TRIGGER history_after_update AFTER UPDATE OF title, url_host, url_path, extracted_content_plain, full_content_plain ON history BEGIN INSERT INTO history_fts (history_fts, "rowid", title, url_host, url_path, extracted_content_plain, full_content_plain) VALUES (\'delete\', old."rowid", old.title, old.url_host, old.url_path, old.extracted_content_plain, old.full_content_plain);INSERT INTO history_fts ("rowid", title, url_host, url_path, extracted_content_plain, full_content_plain) VALUES (new."rowid", new.title, new.url_host, new.url_path, new.extracted_content_plain, new.full_content_plain);END', + 'history_after_update', + ); + final i1.Trigger tabToHistoryOnInsert = i1.Trigger( + 'CREATE TRIGGER tab_to_history_on_insert AFTER INSERT ON tab WHEN NEW.url IS NOT NULL AND url_indexable(CAST(NEW.url AS TEXT)) = 1 AND (SELECT value FROM local_index_setting WHERE "key" = \'enabled\') = 1 AND(NEW.tab_mode != 1 OR (SELECT value FROM local_index_setting WHERE "key" = \'index_private\') = 1)AND NOT EXISTS (SELECT 1 FROM container WHERE container.id = NEW.container_id AND json_extract(container.metadata, \'\$.excludeFromIndex\') = 1) BEGIN INSERT INTO history (url_canonical, url_host, url_path, title, is_probably_readerable, extracted_content_markdown, extracted_content_plain, full_content_markdown, full_content_plain, content_hash, observed_at, observed_count) VALUES (url_canonical(CAST(NEW.url AS TEXT)), url_host(CAST(NEW.url AS TEXT)), url_path(CAST(NEW.url AS TEXT)), NEW.title, NEW.is_probably_readerable, NEW.extracted_content_markdown, NEW.extracted_content_plain, NEW.full_content_markdown, NEW.full_content_plain, NEW.content_hash, strftime(\'%s\', \'now\') * 1000, 1) ON CONFLICT (url_canonical) DO UPDATE SET title = COALESCE(excluded.title, history.title), is_probably_readerable = excluded.is_probably_readerable, extracted_content_markdown = excluded.extracted_content_markdown, extracted_content_plain = excluded.extracted_content_plain, full_content_markdown = excluded.full_content_markdown, full_content_plain = excluded.full_content_plain, content_hash = excluded.content_hash, observed_at = excluded.observed_at, observed_count = history.observed_count + 1 WHERE history.content_hash IS NOT excluded.content_hash;END', + 'tab_to_history_on_insert', + ); + final i1.Trigger tabToHistoryOnUpdate = i1.Trigger( + 'CREATE TRIGGER tab_to_history_on_update AFTER UPDATE OF title, url, extracted_content_plain, extracted_content_markdown, full_content_plain, full_content_markdown, is_probably_readerable ON tab WHEN NEW.url IS NOT NULL AND url_indexable(CAST(NEW.url AS TEXT)) = 1 AND(OLD.content_hash IS NOT NEW.content_hash OR OLD.url IS NOT NEW.url)AND (SELECT value FROM local_index_setting WHERE "key" = \'enabled\') = 1 AND(NEW.tab_mode != 1 OR (SELECT value FROM local_index_setting WHERE "key" = \'index_private\') = 1)AND NOT EXISTS (SELECT 1 FROM container WHERE container.id = NEW.container_id AND json_extract(container.metadata, \'\$.excludeFromIndex\') = 1) BEGIN INSERT INTO history (url_canonical, url_host, url_path, title, is_probably_readerable, extracted_content_markdown, extracted_content_plain, full_content_markdown, full_content_plain, content_hash, observed_at, observed_count) VALUES (url_canonical(CAST(NEW.url AS TEXT)), url_host(CAST(NEW.url AS TEXT)), url_path(CAST(NEW.url AS TEXT)), NEW.title, NEW.is_probably_readerable, NEW.extracted_content_markdown, NEW.extracted_content_plain, NEW.full_content_markdown, NEW.full_content_plain, NEW.content_hash, strftime(\'%s\', \'now\') * 1000, 1) ON CONFLICT (url_canonical) DO UPDATE SET title = COALESCE(excluded.title, history.title), is_probably_readerable = excluded.is_probably_readerable, extracted_content_markdown = excluded.extracted_content_markdown, extracted_content_plain = excluded.extracted_content_plain, full_content_markdown = excluded.full_content_markdown, full_content_plain = excluded.full_content_plain, content_hash = excluded.content_hash, observed_at = excluded.observed_at, observed_count = history.observed_count + 1 WHERE history.content_hash IS NOT excluded.content_hash;END', + 'tab_to_history_on_update', + ); + final i1.Trigger tabToHistoryOnContainerUpdate = i1.Trigger( + 'CREATE TRIGGER tab_to_history_on_container_update AFTER UPDATE OF container_id ON tab WHEN NEW.url IS NOT NULL AND url_indexable(CAST(NEW.url AS TEXT)) = 1 AND (SELECT value FROM local_index_setting WHERE "key" = \'enabled\') = 1 BEGIN DELETE FROM history WHERE url_canonical = url_canonical(CAST(NEW.url AS TEXT)) AND NOT EXISTS (SELECT 1 FROM tab AS candidate WHERE candidate.url IS NOT NULL AND url_indexable(CAST(candidate.url AS TEXT)) = 1 AND url_canonical(CAST(candidate.url AS TEXT)) = history.url_canonical AND(candidate.tab_mode != 1 OR (SELECT value FROM local_index_setting WHERE "key" = \'index_private\') = 1)AND NOT EXISTS (SELECT 1 FROM container WHERE container.id = candidate.container_id AND json_extract(container.metadata, \'\$.excludeFromIndex\') = 1));INSERT INTO history (url_canonical, url_host, url_path, title, is_probably_readerable, extracted_content_markdown, extracted_content_plain, full_content_markdown, full_content_plain, content_hash, observed_at, observed_count) SELECT url_canonical(CAST(candidate.url AS TEXT)), url_host(CAST(candidate.url AS TEXT)), url_path(CAST(candidate.url AS TEXT)), candidate.title, candidate.is_probably_readerable, candidate.extracted_content_markdown, candidate.extracted_content_plain, candidate.full_content_markdown, candidate.full_content_plain, candidate.content_hash, strftime(\'%s\', \'now\') * 1000, 1 FROM tab AS candidate WHERE candidate.url IS NOT NULL AND url_indexable(CAST(candidate.url AS TEXT)) = 1 AND url_canonical(CAST(candidate.url AS TEXT)) = url_canonical(CAST(NEW.url AS TEXT)) AND(candidate.tab_mode != 1 OR (SELECT value FROM local_index_setting WHERE "key" = \'index_private\') = 1)AND NOT EXISTS (SELECT 1 FROM container WHERE container.id = candidate.container_id AND json_extract(container.metadata, \'\$.excludeFromIndex\') = 1) ORDER BY candidate.timestamp DESC, candidate."rowid" DESC LIMIT 1 ON CONFLICT (url_canonical) DO UPDATE SET title = COALESCE(excluded.title, history.title), is_probably_readerable = excluded.is_probably_readerable, extracted_content_markdown = excluded.extracted_content_markdown, extracted_content_plain = excluded.extracted_content_plain, full_content_markdown = excluded.full_content_markdown, full_content_plain = excluded.full_content_plain, content_hash = excluded.content_hash, observed_at = excluded.observed_at, observed_count = history.observed_count + 1;END', + 'tab_to_history_on_container_update', + ); + final i1.Trigger containerToHistoryOnMetadataUpdate = i1.Trigger( + 'CREATE TRIGGER container_to_history_on_metadata_update AFTER UPDATE OF metadata ON container WHEN COALESCE(json_extract(OLD.metadata, \'\$.excludeFromIndex\') = 1, 0) != COALESCE(json_extract(NEW.metadata, \'\$.excludeFromIndex\') = 1, 0) AND (SELECT value FROM local_index_setting WHERE "key" = \'enabled\') = 1 BEGIN DELETE FROM history WHERE url_canonical IN (SELECT DISTINCT url_canonical(CAST(affected.url AS TEXT)) FROM tab AS affected WHERE affected.container_id = NEW.id AND affected.url IS NOT NULL AND url_indexable(CAST(affected.url AS TEXT)) = 1) AND NOT EXISTS (SELECT 1 FROM tab AS candidate WHERE candidate.url IS NOT NULL AND url_indexable(CAST(candidate.url AS TEXT)) = 1 AND url_canonical(CAST(candidate.url AS TEXT)) = history.url_canonical AND(candidate.tab_mode != 1 OR (SELECT value FROM local_index_setting WHERE "key" = \'index_private\') = 1)AND NOT EXISTS (SELECT 1 FROM container WHERE container.id = candidate.container_id AND json_extract(container.metadata, \'\$.excludeFromIndex\') = 1));INSERT INTO history (url_canonical, url_host, url_path, title, is_probably_readerable, extracted_content_markdown, extracted_content_plain, full_content_markdown, full_content_plain, content_hash, observed_at, observed_count) SELECT url_canonical(CAST(candidate.url AS TEXT)), url_host(CAST(candidate.url AS TEXT)), url_path(CAST(candidate.url AS TEXT)), candidate.title, candidate.is_probably_readerable, candidate.extracted_content_markdown, candidate.extracted_content_plain, candidate.full_content_markdown, candidate.full_content_plain, candidate.content_hash, strftime(\'%s\', \'now\') * 1000, 1 FROM tab AS candidate WHERE candidate.url IS NOT NULL AND url_indexable(CAST(candidate.url AS TEXT)) = 1 AND url_canonical(CAST(candidate.url AS TEXT)) IN (SELECT DISTINCT url_canonical(CAST(affected.url AS TEXT)) FROM tab AS affected WHERE affected.container_id = NEW.id AND affected.url IS NOT NULL AND url_indexable(CAST(affected.url AS TEXT)) = 1) AND(candidate.tab_mode != 1 OR (SELECT value FROM local_index_setting WHERE "key" = \'index_private\') = 1)AND NOT EXISTS (SELECT 1 FROM container WHERE container.id = candidate.container_id AND json_extract(container.metadata, \'\$.excludeFromIndex\') = 1) AND NOT EXISTS (SELECT 1 FROM tab AS newer WHERE newer.url IS NOT NULL AND url_indexable(CAST(newer.url AS TEXT)) = 1 AND url_canonical(CAST(newer.url AS TEXT)) = url_canonical(CAST(candidate.url AS TEXT)) AND(newer.tab_mode != 1 OR (SELECT value FROM local_index_setting WHERE "key" = \'index_private\') = 1)AND NOT EXISTS (SELECT 1 FROM container WHERE container.id = newer.container_id AND json_extract(container.metadata, \'\$.excludeFromIndex\') = 1) AND(newer.timestamp > candidate.timestamp OR(newer.timestamp = candidate.timestamp AND newer."rowid" > candidate."rowid"))) ON CONFLICT (url_canonical) DO UPDATE SET title = COALESCE(excluded.title, history.title), is_probably_readerable = excluded.is_probably_readerable, extracted_content_markdown = excluded.extracted_content_markdown, extracted_content_plain = excluded.extracted_content_plain, full_content_markdown = excluded.full_content_markdown, full_content_plain = excluded.full_content_plain, content_hash = excluded.content_hash, observed_at = excluded.observed_at, observed_count = history.observed_count + 1;END', + 'container_to_history_on_metadata_update', + ); +} + i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema3 schema) from2To3, required Future Function(i1.Migrator m, Schema4 schema) from3To4, @@ -1835,6 +2051,7 @@ i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema11 schema) from10To11, required Future Function(i1.Migrator m, Schema12 schema) from11To12, required Future Function(i1.Migrator m, Schema13 schema) from12To13, + required Future Function(i1.Migrator m, Schema14 schema) from13To14, }) { return (currentVersion, database) async { switch (currentVersion) { @@ -1893,6 +2110,11 @@ i0.MigrationStepWithVersion migrationSteps({ final migrator = i1.Migrator(database, schema); await from12To13(migrator, schema); return 13; + case 13: + final schema = Schema14(database: database); + final migrator = i1.Migrator(database, schema); + await from13To14(migrator, schema); + return 14; default: throw ArgumentError.value('Unknown migration from $currentVersion'); } @@ -1911,6 +2133,7 @@ i1.OnUpgrade stepByStep({ required Future Function(i1.Migrator m, Schema11 schema) from10To11, required Future Function(i1.Migrator m, Schema12 schema) from11To12, required Future Function(i1.Migrator m, Schema13 schema) from12To13, + required Future Function(i1.Migrator m, Schema14 schema) from13To14, }) => i0.VersionedSchema.stepByStepHelper( step: migrationSteps( from2To3: from2To3, @@ -1924,5 +2147,6 @@ i1.OnUpgrade stepByStep({ from10To11: from10To11, from11To12: from11To12, from12To13: from12To13, + from13To14: from13To14, ), ); 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 ed49c363..cb959e79 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 @@ -297,17 +297,28 @@ historyUrlsPage: deleteHistoryByCanonicalUrls: DELETE FROM history WHERE url_canonical IN :canonicalUrls; --- Tab → history fan-out. Fires when content-bearing columns change AND the --- content actually changed (content_hash gate). Settings are read from --- local_index_setting at SQL level so the trigger is the single source of --- truth for indexing eligibility. tab_mode = 1 is the private tab marker; --- private tabs are skipped unless `index_private` is enabled. +-- Tab -> history fan-out. Content triggers fire when content-bearing columns +-- change AND the content actually changed (content_hash gate). Re-evaluation +-- triggers fire when indexing eligibility changes without a content update +-- (container assignment or container exclude flag changes). Settings are read +-- from local_index_setting at SQL level so these triggers are the single source +-- of truth for indexing eligibility. tab_mode = 1 is the private tab marker; +-- private tabs are skipped unless `index_private` is enabled. Per-container +-- opt-out lives in container.metadata.excludeFromIndex (JSON); tabs without +-- a container fall through (NULL container_id -> no matching row -> indexable). +-- Re-evaluation must refresh observed_at even when content_hash is unchanged, +-- because the winning eligible tab for a canonical URL may have changed. CREATE TRIGGER tab_to_history_on_insert AFTER INSERT ON tab WHEN NEW.url IS NOT NULL AND url_indexable(CAST(NEW.url AS TEXT)) = 1 AND (SELECT value FROM local_index_setting WHERE "key" = 'enabled') = 1 AND (NEW.tab_mode != 1 OR (SELECT value FROM local_index_setting WHERE "key" = 'index_private') = 1) + AND NOT EXISTS ( + SELECT 1 FROM container + WHERE container.id = NEW.container_id + AND json_extract(container.metadata, '$.excludeFromIndex') = 1 + ) BEGIN INSERT INTO history ( url_canonical, url_host, url_path, title, is_probably_readerable, @@ -352,6 +363,11 @@ WHEN NEW.url IS NOT NULL AND (SELECT value FROM local_index_setting WHERE "key" = 'enabled') = 1 AND (NEW.tab_mode != 1 OR (SELECT value FROM local_index_setting WHERE "key" = 'index_private') = 1) + AND NOT EXISTS ( + SELECT 1 FROM container + WHERE container.id = NEW.container_id + AND json_extract(container.metadata, '$.excludeFromIndex') = 1 + ) BEGIN INSERT INTO history ( url_canonical, url_host, url_path, title, is_probably_readerable, @@ -385,6 +401,168 @@ BEGIN WHERE history.content_hash IS NOT excluded.content_hash; END; +CREATE TRIGGER tab_to_history_on_container_update AFTER UPDATE OF container_id +ON tab +WHEN NEW.url IS NOT NULL + AND url_indexable(CAST(NEW.url AS TEXT)) = 1 + AND (SELECT value FROM local_index_setting WHERE "key" = 'enabled') = 1 +BEGIN + DELETE FROM history + WHERE url_canonical = url_canonical(CAST(NEW.url AS TEXT)) + AND NOT EXISTS ( + SELECT 1 + FROM tab AS candidate + WHERE candidate.url IS NOT NULL + AND url_indexable(CAST(candidate.url AS TEXT)) = 1 + AND url_canonical(CAST(candidate.url AS TEXT)) = history.url_canonical + AND (candidate.tab_mode != 1 + OR (SELECT value FROM local_index_setting WHERE "key" = 'index_private') = 1) + AND NOT EXISTS ( + SELECT 1 FROM container + WHERE container.id = candidate.container_id + AND json_extract(container.metadata, '$.excludeFromIndex') = 1 + ) + ); + + INSERT INTO history ( + url_canonical, url_host, url_path, title, is_probably_readerable, + extracted_content_markdown, extracted_content_plain, + full_content_markdown, full_content_plain, + content_hash, observed_at, observed_count + ) + SELECT + url_canonical(CAST(candidate.url AS TEXT)), + url_host(CAST(candidate.url AS TEXT)), + url_path(CAST(candidate.url AS TEXT)), + candidate.title, + candidate.is_probably_readerable, + candidate.extracted_content_markdown, + candidate.extracted_content_plain, + candidate.full_content_markdown, + candidate.full_content_plain, + candidate.content_hash, + strftime('%s','now') * 1000, + 1 + FROM tab AS candidate + WHERE candidate.url IS NOT NULL + AND url_indexable(CAST(candidate.url AS TEXT)) = 1 + AND url_canonical(CAST(candidate.url AS TEXT)) = url_canonical(CAST(NEW.url AS TEXT)) + AND (candidate.tab_mode != 1 + OR (SELECT value FROM local_index_setting WHERE "key" = 'index_private') = 1) + AND NOT EXISTS ( + SELECT 1 FROM container + WHERE container.id = candidate.container_id + AND json_extract(container.metadata, '$.excludeFromIndex') = 1 + ) + ORDER BY candidate.timestamp DESC, candidate.rowid DESC + LIMIT 1 + ON CONFLICT(url_canonical) DO UPDATE SET + title = COALESCE(excluded.title, history.title), + is_probably_readerable = excluded.is_probably_readerable, + extracted_content_markdown = excluded.extracted_content_markdown, + extracted_content_plain = excluded.extracted_content_plain, + full_content_markdown = excluded.full_content_markdown, + full_content_plain = excluded.full_content_plain, + content_hash = excluded.content_hash, + observed_at = excluded.observed_at, + observed_count = history.observed_count + 1; +END; + +CREATE TRIGGER container_to_history_on_metadata_update AFTER UPDATE OF metadata +ON container +WHEN COALESCE(json_extract(OLD.metadata, '$.excludeFromIndex') = 1, 0) + != COALESCE(json_extract(NEW.metadata, '$.excludeFromIndex') = 1, 0) + AND (SELECT value FROM local_index_setting WHERE "key" = 'enabled') = 1 +BEGIN + DELETE FROM history + WHERE url_canonical IN ( + SELECT DISTINCT url_canonical(CAST(affected.url AS TEXT)) + FROM tab AS affected + WHERE affected.container_id = NEW.id + AND affected.url IS NOT NULL + AND url_indexable(CAST(affected.url AS TEXT)) = 1 + ) + AND NOT EXISTS ( + SELECT 1 + FROM tab AS candidate + WHERE candidate.url IS NOT NULL + AND url_indexable(CAST(candidate.url AS TEXT)) = 1 + AND url_canonical(CAST(candidate.url AS TEXT)) = history.url_canonical + AND (candidate.tab_mode != 1 + OR (SELECT value FROM local_index_setting WHERE "key" = 'index_private') = 1) + AND NOT EXISTS ( + SELECT 1 FROM container + WHERE container.id = candidate.container_id + AND json_extract(container.metadata, '$.excludeFromIndex') = 1 + ) + ); + + INSERT INTO history ( + url_canonical, url_host, url_path, title, is_probably_readerable, + extracted_content_markdown, extracted_content_plain, + full_content_markdown, full_content_plain, + content_hash, observed_at, observed_count + ) + SELECT + url_canonical(CAST(candidate.url AS TEXT)), + url_host(CAST(candidate.url AS TEXT)), + url_path(CAST(candidate.url AS TEXT)), + candidate.title, + candidate.is_probably_readerable, + candidate.extracted_content_markdown, + candidate.extracted_content_plain, + candidate.full_content_markdown, + candidate.full_content_plain, + candidate.content_hash, + strftime('%s','now') * 1000, + 1 + FROM tab AS candidate + WHERE candidate.url IS NOT NULL + AND url_indexable(CAST(candidate.url AS TEXT)) = 1 + AND url_canonical(CAST(candidate.url AS TEXT)) IN ( + SELECT DISTINCT url_canonical(CAST(affected.url AS TEXT)) + FROM tab AS affected + WHERE affected.container_id = NEW.id + AND affected.url IS NOT NULL + AND url_indexable(CAST(affected.url AS TEXT)) = 1 + ) + AND (candidate.tab_mode != 1 + OR (SELECT value FROM local_index_setting WHERE "key" = 'index_private') = 1) + AND NOT EXISTS ( + SELECT 1 FROM container + WHERE container.id = candidate.container_id + AND json_extract(container.metadata, '$.excludeFromIndex') = 1 + ) + AND NOT EXISTS ( + SELECT 1 + FROM tab AS newer + WHERE newer.url IS NOT NULL + AND url_indexable(CAST(newer.url AS TEXT)) = 1 + AND url_canonical(CAST(newer.url AS TEXT)) = url_canonical(CAST(candidate.url AS TEXT)) + AND (newer.tab_mode != 1 + OR (SELECT value FROM local_index_setting WHERE "key" = 'index_private') = 1) + AND NOT EXISTS ( + SELECT 1 FROM container + WHERE container.id = newer.container_id + AND json_extract(container.metadata, '$.excludeFromIndex') = 1 + ) + AND ( + newer.timestamp > candidate.timestamp + OR (newer.timestamp = candidate.timestamp AND newer.rowid > candidate.rowid) + ) + ) + ON CONFLICT(url_canonical) DO UPDATE SET + title = COALESCE(excluded.title, history.title), + is_probably_readerable = excluded.is_probably_readerable, + extracted_content_markdown = excluded.extracted_content_markdown, + extracted_content_plain = excluded.extracted_content_plain, + full_content_markdown = excluded.full_content_markdown, + full_content_plain = excluded.full_content_plain, + content_hash = excluded.content_hash, + observed_at = excluded.observed_at, + observed_count = history.observed_count + 1; +END; + containersWithCount WITH ContainerDataWithCount: SELECT 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 47367792..23c3a43f 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 @@ -5436,13 +5436,21 @@ i0.Trigger get historyAfterUpdate => i0.Trigger( 'history_after_update', ); i0.Trigger get tabToHistoryOnInsert => i0.Trigger( - 'CREATE TRIGGER tab_to_history_on_insert AFTER INSERT ON tab WHEN NEW.url IS NOT NULL AND url_indexable(CAST(NEW.url AS TEXT)) = 1 AND (SELECT value FROM local_index_setting WHERE "key" = \'enabled\') = 1 AND(NEW.tab_mode != 1 OR (SELECT value FROM local_index_setting WHERE "key" = \'index_private\') = 1)BEGIN INSERT INTO history (url_canonical, url_host, url_path, title, is_probably_readerable, extracted_content_markdown, extracted_content_plain, full_content_markdown, full_content_plain, content_hash, observed_at, observed_count) VALUES (url_canonical(CAST(NEW.url AS TEXT)), url_host(CAST(NEW.url AS TEXT)), url_path(CAST(NEW.url AS TEXT)), NEW.title, NEW.is_probably_readerable, NEW.extracted_content_markdown, NEW.extracted_content_plain, NEW.full_content_markdown, NEW.full_content_plain, NEW.content_hash, strftime(\'%s\', \'now\') * 1000, 1) ON CONFLICT (url_canonical) DO UPDATE SET title = COALESCE(excluded.title, history.title), is_probably_readerable = excluded.is_probably_readerable, extracted_content_markdown = excluded.extracted_content_markdown, extracted_content_plain = excluded.extracted_content_plain, full_content_markdown = excluded.full_content_markdown, full_content_plain = excluded.full_content_plain, content_hash = excluded.content_hash, observed_at = excluded.observed_at, observed_count = history.observed_count + 1 WHERE history.content_hash IS NOT excluded.content_hash;END', + 'CREATE TRIGGER tab_to_history_on_insert AFTER INSERT ON tab WHEN NEW.url IS NOT NULL AND url_indexable(CAST(NEW.url AS TEXT)) = 1 AND (SELECT value FROM local_index_setting WHERE "key" = \'enabled\') = 1 AND(NEW.tab_mode != 1 OR (SELECT value FROM local_index_setting WHERE "key" = \'index_private\') = 1)AND NOT EXISTS (SELECT 1 FROM container WHERE container.id = NEW.container_id AND json_extract(container.metadata, \'\$.excludeFromIndex\') = 1) BEGIN INSERT INTO history (url_canonical, url_host, url_path, title, is_probably_readerable, extracted_content_markdown, extracted_content_plain, full_content_markdown, full_content_plain, content_hash, observed_at, observed_count) VALUES (url_canonical(CAST(NEW.url AS TEXT)), url_host(CAST(NEW.url AS TEXT)), url_path(CAST(NEW.url AS TEXT)), NEW.title, NEW.is_probably_readerable, NEW.extracted_content_markdown, NEW.extracted_content_plain, NEW.full_content_markdown, NEW.full_content_plain, NEW.content_hash, strftime(\'%s\', \'now\') * 1000, 1) ON CONFLICT (url_canonical) DO UPDATE SET title = COALESCE(excluded.title, history.title), is_probably_readerable = excluded.is_probably_readerable, extracted_content_markdown = excluded.extracted_content_markdown, extracted_content_plain = excluded.extracted_content_plain, full_content_markdown = excluded.full_content_markdown, full_content_plain = excluded.full_content_plain, content_hash = excluded.content_hash, observed_at = excluded.observed_at, observed_count = history.observed_count + 1 WHERE history.content_hash IS NOT excluded.content_hash;END', 'tab_to_history_on_insert', ); i0.Trigger get tabToHistoryOnUpdate => i0.Trigger( - 'CREATE TRIGGER tab_to_history_on_update AFTER UPDATE OF title, url, extracted_content_plain, extracted_content_markdown, full_content_plain, full_content_markdown, is_probably_readerable ON tab WHEN NEW.url IS NOT NULL AND url_indexable(CAST(NEW.url AS TEXT)) = 1 AND(OLD.content_hash IS NOT NEW.content_hash OR OLD.url IS NOT NEW.url)AND (SELECT value FROM local_index_setting WHERE "key" = \'enabled\') = 1 AND(NEW.tab_mode != 1 OR (SELECT value FROM local_index_setting WHERE "key" = \'index_private\') = 1)BEGIN INSERT INTO history (url_canonical, url_host, url_path, title, is_probably_readerable, extracted_content_markdown, extracted_content_plain, full_content_markdown, full_content_plain, content_hash, observed_at, observed_count) VALUES (url_canonical(CAST(NEW.url AS TEXT)), url_host(CAST(NEW.url AS TEXT)), url_path(CAST(NEW.url AS TEXT)), NEW.title, NEW.is_probably_readerable, NEW.extracted_content_markdown, NEW.extracted_content_plain, NEW.full_content_markdown, NEW.full_content_plain, NEW.content_hash, strftime(\'%s\', \'now\') * 1000, 1) ON CONFLICT (url_canonical) DO UPDATE SET title = COALESCE(excluded.title, history.title), is_probably_readerable = excluded.is_probably_readerable, extracted_content_markdown = excluded.extracted_content_markdown, extracted_content_plain = excluded.extracted_content_plain, full_content_markdown = excluded.full_content_markdown, full_content_plain = excluded.full_content_plain, content_hash = excluded.content_hash, observed_at = excluded.observed_at, observed_count = history.observed_count + 1 WHERE history.content_hash IS NOT excluded.content_hash;END', + 'CREATE TRIGGER tab_to_history_on_update AFTER UPDATE OF title, url, extracted_content_plain, extracted_content_markdown, full_content_plain, full_content_markdown, is_probably_readerable ON tab WHEN NEW.url IS NOT NULL AND url_indexable(CAST(NEW.url AS TEXT)) = 1 AND(OLD.content_hash IS NOT NEW.content_hash OR OLD.url IS NOT NEW.url)AND (SELECT value FROM local_index_setting WHERE "key" = \'enabled\') = 1 AND(NEW.tab_mode != 1 OR (SELECT value FROM local_index_setting WHERE "key" = \'index_private\') = 1)AND NOT EXISTS (SELECT 1 FROM container WHERE container.id = NEW.container_id AND json_extract(container.metadata, \'\$.excludeFromIndex\') = 1) BEGIN INSERT INTO history (url_canonical, url_host, url_path, title, is_probably_readerable, extracted_content_markdown, extracted_content_plain, full_content_markdown, full_content_plain, content_hash, observed_at, observed_count) VALUES (url_canonical(CAST(NEW.url AS TEXT)), url_host(CAST(NEW.url AS TEXT)), url_path(CAST(NEW.url AS TEXT)), NEW.title, NEW.is_probably_readerable, NEW.extracted_content_markdown, NEW.extracted_content_plain, NEW.full_content_markdown, NEW.full_content_plain, NEW.content_hash, strftime(\'%s\', \'now\') * 1000, 1) ON CONFLICT (url_canonical) DO UPDATE SET title = COALESCE(excluded.title, history.title), is_probably_readerable = excluded.is_probably_readerable, extracted_content_markdown = excluded.extracted_content_markdown, extracted_content_plain = excluded.extracted_content_plain, full_content_markdown = excluded.full_content_markdown, full_content_plain = excluded.full_content_plain, content_hash = excluded.content_hash, observed_at = excluded.observed_at, observed_count = history.observed_count + 1 WHERE history.content_hash IS NOT excluded.content_hash;END', 'tab_to_history_on_update', ); +i0.Trigger get tabToHistoryOnContainerUpdate => i0.Trigger( + 'CREATE TRIGGER tab_to_history_on_container_update AFTER UPDATE OF container_id ON tab WHEN NEW.url IS NOT NULL AND url_indexable(CAST(NEW.url AS TEXT)) = 1 AND (SELECT value FROM local_index_setting WHERE "key" = \'enabled\') = 1 BEGIN DELETE FROM history WHERE url_canonical = url_canonical(CAST(NEW.url AS TEXT)) AND NOT EXISTS (SELECT 1 FROM tab AS candidate WHERE candidate.url IS NOT NULL AND url_indexable(CAST(candidate.url AS TEXT)) = 1 AND url_canonical(CAST(candidate.url AS TEXT)) = history.url_canonical AND(candidate.tab_mode != 1 OR (SELECT value FROM local_index_setting WHERE "key" = \'index_private\') = 1)AND NOT EXISTS (SELECT 1 FROM container WHERE container.id = candidate.container_id AND json_extract(container.metadata, \'\$.excludeFromIndex\') = 1));INSERT INTO history (url_canonical, url_host, url_path, title, is_probably_readerable, extracted_content_markdown, extracted_content_plain, full_content_markdown, full_content_plain, content_hash, observed_at, observed_count) SELECT url_canonical(CAST(candidate.url AS TEXT)), url_host(CAST(candidate.url AS TEXT)), url_path(CAST(candidate.url AS TEXT)), candidate.title, candidate.is_probably_readerable, candidate.extracted_content_markdown, candidate.extracted_content_plain, candidate.full_content_markdown, candidate.full_content_plain, candidate.content_hash, strftime(\'%s\', \'now\') * 1000, 1 FROM tab AS candidate WHERE candidate.url IS NOT NULL AND url_indexable(CAST(candidate.url AS TEXT)) = 1 AND url_canonical(CAST(candidate.url AS TEXT)) = url_canonical(CAST(NEW.url AS TEXT)) AND(candidate.tab_mode != 1 OR (SELECT value FROM local_index_setting WHERE "key" = \'index_private\') = 1)AND NOT EXISTS (SELECT 1 FROM container WHERE container.id = candidate.container_id AND json_extract(container.metadata, \'\$.excludeFromIndex\') = 1) ORDER BY candidate.timestamp DESC, candidate."rowid" DESC LIMIT 1 ON CONFLICT (url_canonical) DO UPDATE SET title = COALESCE(excluded.title, history.title), is_probably_readerable = excluded.is_probably_readerable, extracted_content_markdown = excluded.extracted_content_markdown, extracted_content_plain = excluded.extracted_content_plain, full_content_markdown = excluded.full_content_markdown, full_content_plain = excluded.full_content_plain, content_hash = excluded.content_hash, observed_at = excluded.observed_at, observed_count = history.observed_count + 1;END', + 'tab_to_history_on_container_update', +); +i0.Trigger get containerToHistoryOnMetadataUpdate => i0.Trigger( + 'CREATE TRIGGER container_to_history_on_metadata_update AFTER UPDATE OF metadata ON container WHEN COALESCE(json_extract(OLD.metadata, \'\$.excludeFromIndex\') = 1, 0) != COALESCE(json_extract(NEW.metadata, \'\$.excludeFromIndex\') = 1, 0) AND (SELECT value FROM local_index_setting WHERE "key" = \'enabled\') = 1 BEGIN DELETE FROM history WHERE url_canonical IN (SELECT DISTINCT url_canonical(CAST(affected.url AS TEXT)) FROM tab AS affected WHERE affected.container_id = NEW.id AND affected.url IS NOT NULL AND url_indexable(CAST(affected.url AS TEXT)) = 1) AND NOT EXISTS (SELECT 1 FROM tab AS candidate WHERE candidate.url IS NOT NULL AND url_indexable(CAST(candidate.url AS TEXT)) = 1 AND url_canonical(CAST(candidate.url AS TEXT)) = history.url_canonical AND(candidate.tab_mode != 1 OR (SELECT value FROM local_index_setting WHERE "key" = \'index_private\') = 1)AND NOT EXISTS (SELECT 1 FROM container WHERE container.id = candidate.container_id AND json_extract(container.metadata, \'\$.excludeFromIndex\') = 1));INSERT INTO history (url_canonical, url_host, url_path, title, is_probably_readerable, extracted_content_markdown, extracted_content_plain, full_content_markdown, full_content_plain, content_hash, observed_at, observed_count) SELECT url_canonical(CAST(candidate.url AS TEXT)), url_host(CAST(candidate.url AS TEXT)), url_path(CAST(candidate.url AS TEXT)), candidate.title, candidate.is_probably_readerable, candidate.extracted_content_markdown, candidate.extracted_content_plain, candidate.full_content_markdown, candidate.full_content_plain, candidate.content_hash, strftime(\'%s\', \'now\') * 1000, 1 FROM tab AS candidate WHERE candidate.url IS NOT NULL AND url_indexable(CAST(candidate.url AS TEXT)) = 1 AND url_canonical(CAST(candidate.url AS TEXT)) IN (SELECT DISTINCT url_canonical(CAST(affected.url AS TEXT)) FROM tab AS affected WHERE affected.container_id = NEW.id AND affected.url IS NOT NULL AND url_indexable(CAST(affected.url AS TEXT)) = 1) AND(candidate.tab_mode != 1 OR (SELECT value FROM local_index_setting WHERE "key" = \'index_private\') = 1)AND NOT EXISTS (SELECT 1 FROM container WHERE container.id = candidate.container_id AND json_extract(container.metadata, \'\$.excludeFromIndex\') = 1) AND NOT EXISTS (SELECT 1 FROM tab AS newer WHERE newer.url IS NOT NULL AND url_indexable(CAST(newer.url AS TEXT)) = 1 AND url_canonical(CAST(newer.url AS TEXT)) = url_canonical(CAST(candidate.url AS TEXT)) AND(newer.tab_mode != 1 OR (SELECT value FROM local_index_setting WHERE "key" = \'index_private\') = 1)AND NOT EXISTS (SELECT 1 FROM container WHERE container.id = newer.container_id AND json_extract(container.metadata, \'\$.excludeFromIndex\') = 1) AND(newer.timestamp > candidate.timestamp OR(newer.timestamp = candidate.timestamp AND newer."rowid" > candidate."rowid"))) ON CONFLICT (url_canonical) DO UPDATE SET title = COALESCE(excluded.title, history.title), is_probably_readerable = excluded.is_probably_readerable, extracted_content_markdown = excluded.extracted_content_markdown, extracted_content_plain = excluded.extracted_content_plain, full_content_markdown = excluded.full_content_markdown, full_content_plain = excluded.full_content_plain, content_hash = excluded.content_hash, observed_at = excluded.observed_at, observed_count = history.observed_count + 1;END', + 'container_to_history_on_metadata_update', +); class DefinitionsDrift extends i9.ModularAccessor { DefinitionsDrift(i0.GeneratedDatabase db) : super(db); diff --git a/apps/weblibre/lib/features/geckoview/features/tabs/data/models/container_data.dart b/apps/weblibre/lib/features/geckoview/features/tabs/data/models/container_data.dart index 219f616c..51455b8d 100644 --- a/apps/weblibre/lib/features/geckoview/features/tabs/data/models/container_data.dart +++ b/apps/weblibre/lib/features/geckoview/features/tabs/data/models/container_data.dart @@ -44,6 +44,12 @@ class ContainerMetadata with FastEquatable { @JsonKey(defaultValue: false) final bool clearDataOnExit; + // Read by the `tab_to_history_on_*` SQL triggers via + // `json_extract(metadata, '$.excludeFromIndex')`. Keep this key name in + // sync with the trigger gate in definitions.drift. + @JsonKey(defaultValue: false) + final bool excludeFromIndex; + final List? assignedSites; ContainerMetadata({ @@ -51,6 +57,7 @@ class ContainerMetadata with FastEquatable { required this.contextualIdentity, required this.proxyConnectionId, required this.clearDataOnExit, + required this.excludeFromIndex, required this.assignedSites, }); @@ -59,12 +66,14 @@ class ContainerMetadata with FastEquatable { String? contextualIdentity, ProxyConnectionId? proxyConnectionId, bool? clearDataOnExit, + bool? excludeFromIndex, List? assignedSites, }) : this( iconData: iconData, contextualIdentity: contextualIdentity, proxyConnectionId: proxyConnectionId, clearDataOnExit: clearDataOnExit ?? false, + excludeFromIndex: excludeFromIndex ?? false, assignedSites: assignedSites, ); @@ -81,6 +90,7 @@ class ContainerMetadata with FastEquatable { contextualIdentity, proxyConnectionId, clearDataOnExit, + excludeFromIndex, assignedSites, ]; } diff --git a/apps/weblibre/lib/features/geckoview/features/tabs/data/models/container_data.g.dart b/apps/weblibre/lib/features/geckoview/features/tabs/data/models/container_data.g.dart index 808964f6..541c1983 100644 --- a/apps/weblibre/lib/features/geckoview/features/tabs/data/models/container_data.g.dart +++ b/apps/weblibre/lib/features/geckoview/features/tabs/data/models/container_data.g.dart @@ -15,6 +15,8 @@ abstract class _$ContainerMetadataCWProxy { ContainerMetadata clearDataOnExit(bool clearDataOnExit); + ContainerMetadata excludeFromIndex(bool excludeFromIndex); + ContainerMetadata assignedSites(List? assignedSites); /// Creates a new instance with the provided field values. @@ -29,6 +31,7 @@ abstract class _$ContainerMetadataCWProxy { String? contextualIdentity, ProxyConnectionId? proxyConnectionId, bool clearDataOnExit, + bool excludeFromIndex, List? assignedSites, }); } @@ -55,6 +58,10 @@ class _$ContainerMetadataCWProxyImpl implements _$ContainerMetadataCWProxy { ContainerMetadata clearDataOnExit(bool clearDataOnExit) => call(clearDataOnExit: clearDataOnExit); + @override + ContainerMetadata excludeFromIndex(bool excludeFromIndex) => + call(excludeFromIndex: excludeFromIndex); + @override ContainerMetadata assignedSites(List? assignedSites) => call(assignedSites: assignedSites); @@ -72,6 +79,7 @@ class _$ContainerMetadataCWProxyImpl implements _$ContainerMetadataCWProxy { Object? contextualIdentity = const $CopyWithPlaceholder(), Object? proxyConnectionId = const $CopyWithPlaceholder(), Object? clearDataOnExit = const $CopyWithPlaceholder(), + Object? excludeFromIndex = const $CopyWithPlaceholder(), Object? assignedSites = const $CopyWithPlaceholder(), }) { return ContainerMetadata( @@ -93,6 +101,12 @@ class _$ContainerMetadataCWProxyImpl implements _$ContainerMetadataCWProxy { ? _value.clearDataOnExit // ignore: cast_nullable_to_non_nullable : clearDataOnExit as bool, + excludeFromIndex: + excludeFromIndex == const $CopyWithPlaceholder() || + excludeFromIndex == null + ? _value.excludeFromIndex + // ignore: cast_nullable_to_non_nullable + : excludeFromIndex as bool, assignedSites: assignedSites == const $CopyWithPlaceholder() ? _value.assignedSites // ignore: cast_nullable_to_non_nullable @@ -232,6 +246,7 @@ ContainerMetadata _$ContainerMetadataFromJson(Map json) => json['proxyConnectionId'] as String?, ), clearDataOnExit: json['clearDataOnExit'] as bool? ?? false, + excludeFromIndex: json['excludeFromIndex'] as bool? ?? false, assignedSites: (json['assignedSites'] as List?) ?.map((e) => Uri.parse(e as String)) .toList(), @@ -247,6 +262,7 @@ Map _$ContainerMetadataToJson( 'contextualIdentity': instance.contextualIdentity, 'proxyConnectionId': _proxyConnectionIdToJson(instance.proxyConnectionId), 'clearDataOnExit': instance.clearDataOnExit, + 'excludeFromIndex': instance.excludeFromIndex, 'assignedSites': instance.assignedSites?.map((e) => e.toString()).toList(), }; diff --git a/apps/weblibre/lib/features/geckoview/features/tabs/domain/providers.g.dart b/apps/weblibre/lib/features/geckoview/features/tabs/domain/providers.g.dart index 6c89fea2..1cca3310 100644 --- a/apps/weblibre/lib/features/geckoview/features/tabs/domain/providers.g.dart +++ b/apps/weblibre/lib/features/geckoview/features/tabs/domain/providers.g.dart @@ -503,7 +503,7 @@ final class WatchTabDbDataProvider } } -String _$watchTabDbDataHash() => r'c505be1cdeaffced2615fdb075ed8a9a2132754a'; +String _$watchTabDbDataHash() => r'90d9f479d9d6c5475185b0490089ea67edff7894'; final class WatchTabDbDataFamily extends $Family with $FunctionalFamilyOverride, String> { diff --git a/apps/weblibre/lib/features/geckoview/features/tabs/domain/repositories/tab.g.dart b/apps/weblibre/lib/features/geckoview/features/tabs/domain/repositories/tab.g.dart index 9604f234..6a2cb36f 100644 --- a/apps/weblibre/lib/features/geckoview/features/tabs/domain/repositories/tab.g.dart +++ b/apps/weblibre/lib/features/geckoview/features/tabs/domain/repositories/tab.g.dart @@ -41,7 +41,7 @@ final class TabDataRepositoryProvider } } -String _$tabDataRepositoryHash() => r'2208b806a97f239a5b3e6ed7e6c395cbd0276bdd'; +String _$tabDataRepositoryHash() => r'38750c2e0d27b407b1038d324ad22eeb202da6eb'; abstract class _$TabDataRepository extends $Notifier { void build(); diff --git a/apps/weblibre/lib/features/geckoview/features/tabs/presentation/screens/container_edit.dart b/apps/weblibre/lib/features/geckoview/features/tabs/presentation/screens/container_edit.dart index c359ee77..85924e07 100644 --- a/apps/weblibre/lib/features/geckoview/features/tabs/presentation/screens/container_edit.dart +++ b/apps/weblibre/lib/features/geckoview/features/tabs/presentation/screens/container_edit.dart @@ -96,6 +96,9 @@ class ContainerEditScreen extends HookConsumerWidget { initialContainer.metadata.proxyConnectionId, ); final clearDataOnExit = useState(initialContainer.metadata.clearDataOnExit); + final excludeFromIndex = useState( + initialContainer.metadata.excludeFromIndex, + ); final assignedSites = useState(initialContainer.metadata.assignedSites); final isPinned = useState(initialContainer.isPinned); @@ -118,6 +121,7 @@ class ContainerEditScreen extends HookConsumerWidget { : null, clearDataOnExit: clearDataOnExit.value && contextualIdentity.value != null, + excludeFromIndex: excludeFromIndex.value, assignedSites: assignedSites.value, ), ); @@ -490,6 +494,18 @@ class ContainerEditScreen extends HookConsumerWidget { } : null, ), + const Divider(height: 1, indent: 56), + SwitchListTile.adaptive( + value: excludeFromIndex.value, + title: const Text('Exclude from Search Index'), + subtitle: const Text( + 'Skip pages in this container from the local search index', + ), + secondary: const Icon(MdiIcons.magnifyRemoveOutline), + onChanged: (value) { + excludeFromIndex.value = value; + }, + ), ], ), ), diff --git a/apps/weblibre/test/drift/tabs/generated/schema.dart b/apps/weblibre/test/drift/tabs/generated/schema.dart index 208cc0da..c1c94a54 100644 --- a/apps/weblibre/test/drift/tabs/generated/schema.dart +++ b/apps/weblibre/test/drift/tabs/generated/schema.dart @@ -16,6 +16,7 @@ import 'schema_v10.dart' as v10; import 'schema_v11.dart' as v11; import 'schema_v12.dart' as v12; import 'schema_v13.dart' as v13; +import 'schema_v14.dart' as v14; class GeneratedHelper implements SchemaInstantiationHelper { @override @@ -45,10 +46,12 @@ class GeneratedHelper implements SchemaInstantiationHelper { return v12.DatabaseAtV12(db); case 13: return v13.DatabaseAtV13(db); + case 14: + return v14.DatabaseAtV14(db); default: throw MissingSchemaException(version, versions); } } - static const versions = const [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]; + static const versions = const [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]; } diff --git a/apps/weblibre/test/drift/tabs/generated/schema_v14.dart b/apps/weblibre/test/drift/tabs/generated/schema_v14.dart new file mode 100644 index 00000000..406d410b --- /dev/null +++ b/apps/weblibre/test/drift/tabs/generated/schema_v14.dart @@ -0,0 +1,3198 @@ +// 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 orderKey = GeneratedColumn( + 'order_key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + 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 metadata = GeneratedColumn( + 'metadata', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: '', + ); + @override + List get $columns => [ + id, + name, + color, + orderKey, + isPinned, + 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'], + )!, + orderKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}order_key'], + )!, + isPinned: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_pinned'], + )!, + 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 orderKey; + final int isPinned; + final String? metadata; + const ContainerData({ + required this.id, + this.name, + required this.color, + required this.orderKey, + required this.isPinned, + 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); + map['order_key'] = Variable(orderKey); + map['is_pinned'] = Variable(isPinned); + 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']), + orderKey: serializer.fromJson(json['orderKey']), + isPinned: serializer.fromJson(json['isPinned']), + 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), + 'orderKey': serializer.toJson(orderKey), + 'isPinned': serializer.toJson(isPinned), + 'metadata': serializer.toJson(metadata), + }; + } + + ContainerData copyWith({ + String? id, + Value name = const Value.absent(), + int? color, + String? orderKey, + int? isPinned, + Value metadata = const Value.absent(), + }) => ContainerData( + id: id ?? this.id, + name: name.present ? name.value : this.name, + color: color ?? this.color, + orderKey: orderKey ?? this.orderKey, + isPinned: isPinned ?? this.isPinned, + 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, + orderKey: data.orderKey.present ? data.orderKey.value : this.orderKey, + isPinned: data.isPinned.present ? data.isPinned.value : this.isPinned, + 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('orderKey: $orderKey, ') + ..write('isPinned: $isPinned, ') + ..write('metadata: $metadata') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, name, color, orderKey, isPinned, 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.orderKey == this.orderKey && + other.isPinned == this.isPinned && + other.metadata == this.metadata); +} + +class ContainerCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value color; + final Value orderKey; + final Value isPinned; + final Value metadata; + final Value rowid; + const ContainerCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.color = const Value.absent(), + this.orderKey = const Value.absent(), + this.isPinned = 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, + required String orderKey, + this.isPinned = const Value.absent(), + this.metadata = const Value.absent(), + this.rowid = const Value.absent(), + }) : id = Value(id), + color = Value(color), + orderKey = Value(orderKey); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? color, + Expression? orderKey, + Expression? isPinned, + Expression? metadata, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (color != null) 'color': color, + if (orderKey != null) 'order_key': orderKey, + if (isPinned != null) 'is_pinned': isPinned, + if (metadata != null) 'metadata': metadata, + if (rowid != null) 'rowid': rowid, + }); + } + + ContainerCompanion copyWith({ + Value? id, + Value? name, + Value? color, + Value? orderKey, + Value? isPinned, + Value? metadata, + Value? rowid, + }) { + return ContainerCompanion( + id: id ?? this.id, + name: name ?? this.name, + color: color ?? this.color, + orderKey: orderKey ?? this.orderKey, + isPinned: isPinned ?? this.isPinned, + 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 (orderKey.present) { + map['order_key'] = Variable(orderKey.value); + } + if (isPinned.present) { + map['is_pinned'] = Variable(isPinned.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('orderKey: $orderKey, ') + ..write('isPinned: $isPinned, ') + ..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', + ); + late final GeneratedColumn contentHash = GeneratedColumn( + 'content_hash', + aliasedName, + true, + generatedAs: GeneratedAs( + const CustomExpression( + 'generate_content_hash(title, extracted_content_plain, full_content_plain)', + ), + false, + ), + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'GENERATED ALWAYS AS (generate_content_hash(title, extracted_content_plain, full_content_plain)) VIRTUAL', + ); + @override + List get $columns => [ + id, + source, + parentId, + containerId, + orderKey, + url, + title, + tabMode, + isolationContextId, + isPinned, + isProbablyReaderable, + extractedContentMarkdown, + extractedContentPlain, + fullContentMarkdown, + fullContentPlain, + timestamp, + contentHash, + ]; + @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'], + )!, + contentHash: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}content_hash'], + ), + ); + } + + @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; + final int? contentHash; + 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, + this.contentHash, + }); + @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']), + contentHash: serializer.fromJson(json['contentHash']), + ); + } + @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), + 'contentHash': serializer.toJson(contentHash), + }; + } + + 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, + Value contentHash = const Value.absent(), + }) => 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, + contentHash: contentHash.present ? contentHash.value : this.contentHash, + ); + @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('contentHash: $contentHash') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + source, + parentId, + containerId, + orderKey, + url, + title, + tabMode, + isolationContextId, + isPinned, + isProbablyReaderable, + extractedContentMarkdown, + extractedContentPlain, + fullContentMarkdown, + fullContentPlain, + timestamp, + contentHash, + ); + @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 && + other.contentHash == this.contentHash); +} + +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 CaptureTab extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + CaptureTab(this.attachedDatabase, [this._alias]); + late final GeneratedColumn tabId = GeneratedColumn( + 'tab_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL PRIMARY KEY REFERENCES tab(id)ON DELETE CASCADE', + ); + late final GeneratedColumn captureId = GeneratedColumn( + 'capture_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn sourceUrl = GeneratedColumn( + 'source_url', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn status = GeneratedColumn( + 'status', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT \'pending\'', + defaultValue: const CustomExpression('\'pending\''), + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + @override + List get $columns => [ + tabId, + captureId, + sourceUrl, + status, + createdAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'capture_tab'; + @override + Set get $primaryKey => {tabId}; + @override + CaptureTabData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return CaptureTabData( + tabId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}tab_id'], + )!, + captureId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}capture_id'], + )!, + sourceUrl: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}source_url'], + )!, + status: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}status'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at'], + )!, + ); + } + + @override + CaptureTab createAlias(String alias) { + return CaptureTab(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class CaptureTabData extends DataClass implements Insertable { + final String tabId; + final String captureId; + final String sourceUrl; + final String status; + final int createdAt; + const CaptureTabData({ + required this.tabId, + required this.captureId, + required this.sourceUrl, + required this.status, + required this.createdAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['tab_id'] = Variable(tabId); + map['capture_id'] = Variable(captureId); + map['source_url'] = Variable(sourceUrl); + map['status'] = Variable(status); + map['created_at'] = Variable(createdAt); + return map; + } + + factory CaptureTabData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return CaptureTabData( + tabId: serializer.fromJson(json['tabId']), + captureId: serializer.fromJson(json['captureId']), + sourceUrl: serializer.fromJson(json['sourceUrl']), + status: serializer.fromJson(json['status']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'tabId': serializer.toJson(tabId), + 'captureId': serializer.toJson(captureId), + 'sourceUrl': serializer.toJson(sourceUrl), + 'status': serializer.toJson(status), + 'createdAt': serializer.toJson(createdAt), + }; + } + + CaptureTabData copyWith({ + String? tabId, + String? captureId, + String? sourceUrl, + String? status, + int? createdAt, + }) => CaptureTabData( + tabId: tabId ?? this.tabId, + captureId: captureId ?? this.captureId, + sourceUrl: sourceUrl ?? this.sourceUrl, + status: status ?? this.status, + createdAt: createdAt ?? this.createdAt, + ); + CaptureTabData copyWithCompanion(CaptureTabCompanion data) { + return CaptureTabData( + tabId: data.tabId.present ? data.tabId.value : this.tabId, + captureId: data.captureId.present ? data.captureId.value : this.captureId, + sourceUrl: data.sourceUrl.present ? data.sourceUrl.value : this.sourceUrl, + status: data.status.present ? data.status.value : this.status, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('CaptureTabData(') + ..write('tabId: $tabId, ') + ..write('captureId: $captureId, ') + ..write('sourceUrl: $sourceUrl, ') + ..write('status: $status, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(tabId, captureId, sourceUrl, status, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is CaptureTabData && + other.tabId == this.tabId && + other.captureId == this.captureId && + other.sourceUrl == this.sourceUrl && + other.status == this.status && + other.createdAt == this.createdAt); +} + +class CaptureTabCompanion extends UpdateCompanion { + final Value tabId; + final Value captureId; + final Value sourceUrl; + final Value status; + final Value createdAt; + final Value rowid; + const CaptureTabCompanion({ + this.tabId = const Value.absent(), + this.captureId = const Value.absent(), + this.sourceUrl = const Value.absent(), + this.status = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + CaptureTabCompanion.insert({ + required String tabId, + required String captureId, + required String sourceUrl, + this.status = const Value.absent(), + required int createdAt, + this.rowid = const Value.absent(), + }) : tabId = Value(tabId), + captureId = Value(captureId), + sourceUrl = Value(sourceUrl), + createdAt = Value(createdAt); + static Insertable custom({ + Expression? tabId, + Expression? captureId, + Expression? sourceUrl, + Expression? status, + Expression? createdAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (tabId != null) 'tab_id': tabId, + if (captureId != null) 'capture_id': captureId, + if (sourceUrl != null) 'source_url': sourceUrl, + if (status != null) 'status': status, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + CaptureTabCompanion copyWith({ + Value? tabId, + Value? captureId, + Value? sourceUrl, + Value? status, + Value? createdAt, + Value? rowid, + }) { + return CaptureTabCompanion( + tabId: tabId ?? this.tabId, + captureId: captureId ?? this.captureId, + sourceUrl: sourceUrl ?? this.sourceUrl, + status: status ?? this.status, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (tabId.present) { + map['tab_id'] = Variable(tabId.value); + } + if (captureId.present) { + map['capture_id'] = Variable(captureId.value); + } + if (sourceUrl.present) { + map['source_url'] = Variable(sourceUrl.value); + } + if (status.present) { + map['status'] = Variable(status.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('CaptureTabCompanion(') + ..write('tabId: $tabId, ') + ..write('captureId: $captureId, ') + ..write('sourceUrl: $sourceUrl, ') + ..write('status: $status, ') + ..write('createdAt: $createdAt, ') + ..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 LocalIndexSetting extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalIndexSetting(this.attachedDatabase, [this._alias]); + late final GeneratedColumn key = GeneratedColumn( + 'key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'PRIMARY KEY NOT NULL', + ); + late final GeneratedColumn value = GeneratedColumn( + 'value', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + @override + List get $columns => [key, value]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_index_setting'; + @override + Set get $primaryKey => {key}; + @override + LocalIndexSettingData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalIndexSettingData( + key: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}key'], + )!, + value: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}value'], + )!, + ); + } + + @override + LocalIndexSetting createAlias(String alias) { + return LocalIndexSetting(attachedDatabase, alias); + } + + @override + bool get isStrict => true; + @override + bool get dontWriteConstraints => true; +} + +class LocalIndexSettingData extends DataClass + implements Insertable { + final String key; + final int value; + const LocalIndexSettingData({required this.key, required this.value}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['key'] = Variable(key); + map['value'] = Variable(value); + return map; + } + + factory LocalIndexSettingData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalIndexSettingData( + key: serializer.fromJson(json['key']), + value: serializer.fromJson(json['value']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'key': serializer.toJson(key), + 'value': serializer.toJson(value), + }; + } + + LocalIndexSettingData copyWith({String? key, int? value}) => + LocalIndexSettingData(key: key ?? this.key, value: value ?? this.value); + LocalIndexSettingData copyWithCompanion(LocalIndexSettingCompanion data) { + return LocalIndexSettingData( + key: data.key.present ? data.key.value : this.key, + value: data.value.present ? data.value.value : this.value, + ); + } + + @override + String toString() { + return (StringBuffer('LocalIndexSettingData(') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(key, value); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalIndexSettingData && + other.key == this.key && + other.value == this.value); +} + +class LocalIndexSettingCompanion + extends UpdateCompanion { + final Value key; + final Value value; + final Value rowid; + const LocalIndexSettingCompanion({ + this.key = const Value.absent(), + this.value = const Value.absent(), + this.rowid = const Value.absent(), + }); + LocalIndexSettingCompanion.insert({ + required String key, + required int value, + this.rowid = const Value.absent(), + }) : key = Value(key), + value = Value(value); + static Insertable custom({ + Expression? key, + Expression? value, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (key != null) 'key': key, + if (value != null) 'value': value, + if (rowid != null) 'rowid': rowid, + }); + } + + LocalIndexSettingCompanion copyWith({ + Value? key, + Value? value, + Value? rowid, + }) { + return LocalIndexSettingCompanion( + key: key ?? this.key, + value: value ?? this.value, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (key.present) { + map['key'] = Variable(key.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalIndexSettingCompanion(') + ..write('key: $key, ') + ..write('value: $value, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class History extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + History(this.attachedDatabase, [this._alias]); + late final GeneratedColumn urlCanonical = GeneratedColumn( + 'url_canonical', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'PRIMARY KEY NOT NULL', + ); + late final GeneratedColumn urlHost = GeneratedColumn( + 'url_host', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn urlPath = GeneratedColumn( + 'url_path', + 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 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 contentHash = GeneratedColumn( + 'content_hash', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: '', + ); + late final GeneratedColumn observedAt = GeneratedColumn( + 'observed_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn observedCount = GeneratedColumn( + 'observed_count', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + @override + List get $columns => [ + urlCanonical, + urlHost, + urlPath, + title, + isProbablyReaderable, + extractedContentMarkdown, + extractedContentPlain, + fullContentMarkdown, + fullContentPlain, + contentHash, + observedAt, + observedCount, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'history'; + @override + Set get $primaryKey => {urlCanonical}; + @override + HistoryData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return HistoryData( + urlCanonical: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}url_canonical'], + )!, + urlHost: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}url_host'], + )!, + urlPath: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}url_path'], + ), + title: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}title'], + ), + 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'], + ), + contentHash: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}content_hash'], + ), + observedAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}observed_at'], + )!, + observedCount: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}observed_count'], + )!, + ); + } + + @override + History createAlias(String alias) { + return History(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class HistoryData extends DataClass implements Insertable { + final String urlCanonical; + final String urlHost; + final String? urlPath; + final String? title; + final int? isProbablyReaderable; + final String? extractedContentMarkdown; + final String? extractedContentPlain; + final String? fullContentMarkdown; + final String? fullContentPlain; + final int? contentHash; + final int observedAt; + final int observedCount; + const HistoryData({ + required this.urlCanonical, + required this.urlHost, + this.urlPath, + this.title, + this.isProbablyReaderable, + this.extractedContentMarkdown, + this.extractedContentPlain, + this.fullContentMarkdown, + this.fullContentPlain, + this.contentHash, + required this.observedAt, + required this.observedCount, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['url_canonical'] = Variable(urlCanonical); + map['url_host'] = Variable(urlHost); + if (!nullToAbsent || urlPath != null) { + map['url_path'] = Variable(urlPath); + } + if (!nullToAbsent || title != null) { + map['title'] = Variable(title); + } + 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); + } + if (!nullToAbsent || contentHash != null) { + map['content_hash'] = Variable(contentHash); + } + map['observed_at'] = Variable(observedAt); + map['observed_count'] = Variable(observedCount); + return map; + } + + factory HistoryData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return HistoryData( + urlCanonical: serializer.fromJson(json['urlCanonical']), + urlHost: serializer.fromJson(json['urlHost']), + urlPath: serializer.fromJson(json['urlPath']), + title: serializer.fromJson(json['title']), + 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']), + contentHash: serializer.fromJson(json['contentHash']), + observedAt: serializer.fromJson(json['observedAt']), + observedCount: serializer.fromJson(json['observedCount']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'urlCanonical': serializer.toJson(urlCanonical), + 'urlHost': serializer.toJson(urlHost), + 'urlPath': serializer.toJson(urlPath), + 'title': serializer.toJson(title), + 'isProbablyReaderable': serializer.toJson(isProbablyReaderable), + 'extractedContentMarkdown': serializer.toJson( + extractedContentMarkdown, + ), + 'extractedContentPlain': serializer.toJson( + extractedContentPlain, + ), + 'fullContentMarkdown': serializer.toJson(fullContentMarkdown), + 'fullContentPlain': serializer.toJson(fullContentPlain), + 'contentHash': serializer.toJson(contentHash), + 'observedAt': serializer.toJson(observedAt), + 'observedCount': serializer.toJson(observedCount), + }; + } + + HistoryData copyWith({ + String? urlCanonical, + String? urlHost, + Value urlPath = const Value.absent(), + Value title = const Value.absent(), + 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(), + Value contentHash = const Value.absent(), + int? observedAt, + int? observedCount, + }) => HistoryData( + urlCanonical: urlCanonical ?? this.urlCanonical, + urlHost: urlHost ?? this.urlHost, + urlPath: urlPath.present ? urlPath.value : this.urlPath, + title: title.present ? title.value : this.title, + 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, + contentHash: contentHash.present ? contentHash.value : this.contentHash, + observedAt: observedAt ?? this.observedAt, + observedCount: observedCount ?? this.observedCount, + ); + HistoryData copyWithCompanion(HistoryCompanion data) { + return HistoryData( + urlCanonical: data.urlCanonical.present + ? data.urlCanonical.value + : this.urlCanonical, + urlHost: data.urlHost.present ? data.urlHost.value : this.urlHost, + urlPath: data.urlPath.present ? data.urlPath.value : this.urlPath, + title: data.title.present ? data.title.value : this.title, + 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, + contentHash: data.contentHash.present + ? data.contentHash.value + : this.contentHash, + observedAt: data.observedAt.present + ? data.observedAt.value + : this.observedAt, + observedCount: data.observedCount.present + ? data.observedCount.value + : this.observedCount, + ); + } + + @override + String toString() { + return (StringBuffer('HistoryData(') + ..write('urlCanonical: $urlCanonical, ') + ..write('urlHost: $urlHost, ') + ..write('urlPath: $urlPath, ') + ..write('title: $title, ') + ..write('isProbablyReaderable: $isProbablyReaderable, ') + ..write('extractedContentMarkdown: $extractedContentMarkdown, ') + ..write('extractedContentPlain: $extractedContentPlain, ') + ..write('fullContentMarkdown: $fullContentMarkdown, ') + ..write('fullContentPlain: $fullContentPlain, ') + ..write('contentHash: $contentHash, ') + ..write('observedAt: $observedAt, ') + ..write('observedCount: $observedCount') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + urlCanonical, + urlHost, + urlPath, + title, + isProbablyReaderable, + extractedContentMarkdown, + extractedContentPlain, + fullContentMarkdown, + fullContentPlain, + contentHash, + observedAt, + observedCount, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is HistoryData && + other.urlCanonical == this.urlCanonical && + other.urlHost == this.urlHost && + other.urlPath == this.urlPath && + other.title == this.title && + other.isProbablyReaderable == this.isProbablyReaderable && + other.extractedContentMarkdown == this.extractedContentMarkdown && + other.extractedContentPlain == this.extractedContentPlain && + other.fullContentMarkdown == this.fullContentMarkdown && + other.fullContentPlain == this.fullContentPlain && + other.contentHash == this.contentHash && + other.observedAt == this.observedAt && + other.observedCount == this.observedCount); +} + +class HistoryCompanion extends UpdateCompanion { + final Value urlCanonical; + final Value urlHost; + final Value urlPath; + final Value title; + final Value isProbablyReaderable; + final Value extractedContentMarkdown; + final Value extractedContentPlain; + final Value fullContentMarkdown; + final Value fullContentPlain; + final Value contentHash; + final Value observedAt; + final Value observedCount; + final Value rowid; + const HistoryCompanion({ + this.urlCanonical = const Value.absent(), + this.urlHost = const Value.absent(), + this.urlPath = const Value.absent(), + this.title = 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.contentHash = const Value.absent(), + this.observedAt = const Value.absent(), + this.observedCount = const Value.absent(), + this.rowid = const Value.absent(), + }); + HistoryCompanion.insert({ + required String urlCanonical, + required String urlHost, + this.urlPath = const Value.absent(), + this.title = 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.contentHash = const Value.absent(), + required int observedAt, + this.observedCount = const Value.absent(), + this.rowid = const Value.absent(), + }) : urlCanonical = Value(urlCanonical), + urlHost = Value(urlHost), + observedAt = Value(observedAt); + static Insertable custom({ + Expression? urlCanonical, + Expression? urlHost, + Expression? urlPath, + Expression? title, + Expression? isProbablyReaderable, + Expression? extractedContentMarkdown, + Expression? extractedContentPlain, + Expression? fullContentMarkdown, + Expression? fullContentPlain, + Expression? contentHash, + Expression? observedAt, + Expression? observedCount, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (urlCanonical != null) 'url_canonical': urlCanonical, + if (urlHost != null) 'url_host': urlHost, + if (urlPath != null) 'url_path': urlPath, + if (title != null) 'title': title, + 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 (contentHash != null) 'content_hash': contentHash, + if (observedAt != null) 'observed_at': observedAt, + if (observedCount != null) 'observed_count': observedCount, + if (rowid != null) 'rowid': rowid, + }); + } + + HistoryCompanion copyWith({ + Value? urlCanonical, + Value? urlHost, + Value? urlPath, + Value? title, + Value? isProbablyReaderable, + Value? extractedContentMarkdown, + Value? extractedContentPlain, + Value? fullContentMarkdown, + Value? fullContentPlain, + Value? contentHash, + Value? observedAt, + Value? observedCount, + Value? rowid, + }) { + return HistoryCompanion( + urlCanonical: urlCanonical ?? this.urlCanonical, + urlHost: urlHost ?? this.urlHost, + urlPath: urlPath ?? this.urlPath, + title: title ?? this.title, + isProbablyReaderable: isProbablyReaderable ?? this.isProbablyReaderable, + extractedContentMarkdown: + extractedContentMarkdown ?? this.extractedContentMarkdown, + extractedContentPlain: + extractedContentPlain ?? this.extractedContentPlain, + fullContentMarkdown: fullContentMarkdown ?? this.fullContentMarkdown, + fullContentPlain: fullContentPlain ?? this.fullContentPlain, + contentHash: contentHash ?? this.contentHash, + observedAt: observedAt ?? this.observedAt, + observedCount: observedCount ?? this.observedCount, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (urlCanonical.present) { + map['url_canonical'] = Variable(urlCanonical.value); + } + if (urlHost.present) { + map['url_host'] = Variable(urlHost.value); + } + if (urlPath.present) { + map['url_path'] = Variable(urlPath.value); + } + if (title.present) { + map['title'] = Variable(title.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 (contentHash.present) { + map['content_hash'] = Variable(contentHash.value); + } + if (observedAt.present) { + map['observed_at'] = Variable(observedAt.value); + } + if (observedCount.present) { + map['observed_count'] = Variable(observedCount.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('HistoryCompanion(') + ..write('urlCanonical: $urlCanonical, ') + ..write('urlHost: $urlHost, ') + ..write('urlPath: $urlPath, ') + ..write('title: $title, ') + ..write('isProbablyReaderable: $isProbablyReaderable, ') + ..write('extractedContentMarkdown: $extractedContentMarkdown, ') + ..write('extractedContentPlain: $extractedContentPlain, ') + ..write('fullContentMarkdown: $fullContentMarkdown, ') + ..write('fullContentPlain: $fullContentPlain, ') + ..write('contentHash: $contentHash, ') + ..write('observedAt: $observedAt, ') + ..write('observedCount: $observedCount, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class HistoryFts extends Table + with + TableInfo, + VirtualTableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + HistoryFts(this.attachedDatabase, [this._alias]); + late final GeneratedColumn title = GeneratedColumn( + 'title', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: '', + ); + late final GeneratedColumn urlHost = GeneratedColumn( + 'url_host', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: '', + ); + late final GeneratedColumn urlPath = GeneratedColumn( + 'url_path', + 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, + urlHost, + urlPath, + extractedContentPlain, + fullContentPlain, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'history_fts'; + @override + Set get $primaryKey => const {}; + @override + HistoryFtsData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return HistoryFtsData( + title: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}title'], + ), + urlHost: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}url_host'], + ), + urlPath: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}url_path'], + ), + extractedContentPlain: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}extracted_content_plain'], + ), + fullContentPlain: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}full_content_plain'], + ), + ); + } + + @override + HistoryFts createAlias(String alias) { + return HistoryFts(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; + @override + String get moduleAndArgs => + 'fts5(title, url_host, url_path, extracted_content_plain, full_content_plain, content=history, tokenize="trigram")'; +} + +class HistoryFtsData extends DataClass implements Insertable { + final String? title; + final String? urlHost; + final String? urlPath; + final String? extractedContentPlain; + final String? fullContentPlain; + const HistoryFtsData({ + this.title, + this.urlHost, + this.urlPath, + this.extractedContentPlain, + this.fullContentPlain, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (!nullToAbsent || title != null) { + map['title'] = Variable(title); + } + if (!nullToAbsent || urlHost != null) { + map['url_host'] = Variable(urlHost); + } + if (!nullToAbsent || urlPath != null) { + map['url_path'] = Variable(urlPath); + } + if (!nullToAbsent || extractedContentPlain != null) { + map['extracted_content_plain'] = Variable(extractedContentPlain); + } + if (!nullToAbsent || fullContentPlain != null) { + map['full_content_plain'] = Variable(fullContentPlain); + } + return map; + } + + factory HistoryFtsData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return HistoryFtsData( + title: serializer.fromJson(json['title']), + urlHost: serializer.fromJson(json['urlHost']), + urlPath: serializer.fromJson(json['urlPath']), + extractedContentPlain: serializer.fromJson( + json['extractedContentPlain'], + ), + fullContentPlain: serializer.fromJson(json['fullContentPlain']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'title': serializer.toJson(title), + 'urlHost': serializer.toJson(urlHost), + 'urlPath': serializer.toJson(urlPath), + 'extractedContentPlain': serializer.toJson( + extractedContentPlain, + ), + 'fullContentPlain': serializer.toJson(fullContentPlain), + }; + } + + HistoryFtsData copyWith({ + Value title = const Value.absent(), + Value urlHost = const Value.absent(), + Value urlPath = const Value.absent(), + Value extractedContentPlain = const Value.absent(), + Value fullContentPlain = const Value.absent(), + }) => HistoryFtsData( + title: title.present ? title.value : this.title, + urlHost: urlHost.present ? urlHost.value : this.urlHost, + urlPath: urlPath.present ? urlPath.value : this.urlPath, + extractedContentPlain: extractedContentPlain.present + ? extractedContentPlain.value + : this.extractedContentPlain, + fullContentPlain: fullContentPlain.present + ? fullContentPlain.value + : this.fullContentPlain, + ); + HistoryFtsData copyWithCompanion(HistoryFtsCompanion data) { + return HistoryFtsData( + title: data.title.present ? data.title.value : this.title, + urlHost: data.urlHost.present ? data.urlHost.value : this.urlHost, + urlPath: data.urlPath.present ? data.urlPath.value : this.urlPath, + extractedContentPlain: data.extractedContentPlain.present + ? data.extractedContentPlain.value + : this.extractedContentPlain, + fullContentPlain: data.fullContentPlain.present + ? data.fullContentPlain.value + : this.fullContentPlain, + ); + } + + @override + String toString() { + return (StringBuffer('HistoryFtsData(') + ..write('title: $title, ') + ..write('urlHost: $urlHost, ') + ..write('urlPath: $urlPath, ') + ..write('extractedContentPlain: $extractedContentPlain, ') + ..write('fullContentPlain: $fullContentPlain') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + title, + urlHost, + urlPath, + extractedContentPlain, + fullContentPlain, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is HistoryFtsData && + other.title == this.title && + other.urlHost == this.urlHost && + other.urlPath == this.urlPath && + other.extractedContentPlain == this.extractedContentPlain && + other.fullContentPlain == this.fullContentPlain); +} + +class HistoryFtsCompanion extends UpdateCompanion { + final Value title; + final Value urlHost; + final Value urlPath; + final Value extractedContentPlain; + final Value fullContentPlain; + final Value rowid; + const HistoryFtsCompanion({ + this.title = const Value.absent(), + this.urlHost = const Value.absent(), + this.urlPath = const Value.absent(), + this.extractedContentPlain = const Value.absent(), + this.fullContentPlain = const Value.absent(), + this.rowid = const Value.absent(), + }); + HistoryFtsCompanion.insert({ + this.title = const Value.absent(), + this.urlHost = const Value.absent(), + this.urlPath = const Value.absent(), + this.extractedContentPlain = const Value.absent(), + this.fullContentPlain = const Value.absent(), + this.rowid = const Value.absent(), + }); + static Insertable custom({ + Expression? title, + Expression? urlHost, + Expression? urlPath, + Expression? extractedContentPlain, + Expression? fullContentPlain, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (title != null) 'title': title, + if (urlHost != null) 'url_host': urlHost, + if (urlPath != null) 'url_path': urlPath, + if (extractedContentPlain != null) + 'extracted_content_plain': extractedContentPlain, + if (fullContentPlain != null) 'full_content_plain': fullContentPlain, + if (rowid != null) 'rowid': rowid, + }); + } + + HistoryFtsCompanion copyWith({ + Value? title, + Value? urlHost, + Value? urlPath, + Value? extractedContentPlain, + Value? fullContentPlain, + Value? rowid, + }) { + return HistoryFtsCompanion( + title: title ?? this.title, + urlHost: urlHost ?? this.urlHost, + urlPath: urlPath ?? this.urlPath, + 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 (urlHost.present) { + map['url_host'] = Variable(urlHost.value); + } + if (urlPath.present) { + map['url_path'] = Variable(urlPath.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('HistoryFtsCompanion(') + ..write('title: $title, ') + ..write('urlHost: $urlHost, ') + ..write('urlPath: $urlPath, ') + ..write('extractedContentPlain: $extractedContentPlain, ') + ..write('fullContentPlain: $fullContentPlain, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV14 extends GeneratedDatabase { + DatabaseAtV14(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 CaptureTab captureTab = CaptureTab(this); + late final Index idxCaptureTabCaptureId = Index( + 'idx_capture_tab_capture_id', + 'CREATE INDEX idx_capture_tab_capture_id ON capture_tab (capture_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 OF title, url, extracted_content_plain, full_content_plain ON tab WHEN OLD.content_hash IS NOT NEW.content_hash OR OLD.url IS NOT NEW.url 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', + ); + late final LocalIndexSetting localIndexSetting = LocalIndexSetting(this); + late final History history = History(this); + late final Index idxHistoryHost = Index( + 'idx_history_host', + 'CREATE INDEX idx_history_host ON history (url_host)', + ); + late final Index idxHistoryObserved = Index( + 'idx_history_observed', + 'CREATE INDEX idx_history_observed ON history (observed_at DESC)', + ); + late final HistoryFts historyFts = HistoryFts(this); + late final Trigger historyAfterInsert = Trigger( + 'CREATE TRIGGER history_after_insert AFTER INSERT ON history BEGIN INSERT INTO history_fts ("rowid", title, url_host, url_path, extracted_content_plain, full_content_plain) VALUES (new."rowid", new.title, new.url_host, new.url_path, new.extracted_content_plain, new.full_content_plain);END', + 'history_after_insert', + ); + late final Trigger historyAfterDelete = Trigger( + 'CREATE TRIGGER history_after_delete AFTER DELETE ON history BEGIN INSERT INTO history_fts (history_fts, "rowid", title, url_host, url_path, extracted_content_plain, full_content_plain) VALUES (\'delete\', old."rowid", old.title, old.url_host, old.url_path, old.extracted_content_plain, old.full_content_plain);END', + 'history_after_delete', + ); + late final Trigger historyAfterUpdate = Trigger( + 'CREATE TRIGGER history_after_update AFTER UPDATE OF title, url_host, url_path, extracted_content_plain, full_content_plain ON history BEGIN INSERT INTO history_fts (history_fts, "rowid", title, url_host, url_path, extracted_content_plain, full_content_plain) VALUES (\'delete\', old."rowid", old.title, old.url_host, old.url_path, old.extracted_content_plain, old.full_content_plain);INSERT INTO history_fts ("rowid", title, url_host, url_path, extracted_content_plain, full_content_plain) VALUES (new."rowid", new.title, new.url_host, new.url_path, new.extracted_content_plain, new.full_content_plain);END', + 'history_after_update', + ); + late final Trigger tabToHistoryOnInsert = Trigger( + 'CREATE TRIGGER tab_to_history_on_insert AFTER INSERT ON tab WHEN NEW.url IS NOT NULL AND url_indexable(CAST(NEW.url AS TEXT)) = 1 AND (SELECT value FROM local_index_setting WHERE "key" = \'enabled\') = 1 AND(NEW.tab_mode != 1 OR (SELECT value FROM local_index_setting WHERE "key" = \'index_private\') = 1)AND NOT EXISTS (SELECT 1 FROM container WHERE container.id = NEW.container_id AND json_extract(container.metadata, \'\$.excludeFromIndex\') = 1) BEGIN INSERT INTO history (url_canonical, url_host, url_path, title, is_probably_readerable, extracted_content_markdown, extracted_content_plain, full_content_markdown, full_content_plain, content_hash, observed_at, observed_count) VALUES (url_canonical(CAST(NEW.url AS TEXT)), url_host(CAST(NEW.url AS TEXT)), url_path(CAST(NEW.url AS TEXT)), NEW.title, NEW.is_probably_readerable, NEW.extracted_content_markdown, NEW.extracted_content_plain, NEW.full_content_markdown, NEW.full_content_plain, NEW.content_hash, strftime(\'%s\', \'now\') * 1000, 1) ON CONFLICT (url_canonical) DO UPDATE SET title = COALESCE(excluded.title, history.title), is_probably_readerable = excluded.is_probably_readerable, extracted_content_markdown = excluded.extracted_content_markdown, extracted_content_plain = excluded.extracted_content_plain, full_content_markdown = excluded.full_content_markdown, full_content_plain = excluded.full_content_plain, content_hash = excluded.content_hash, observed_at = excluded.observed_at, observed_count = history.observed_count + 1 WHERE history.content_hash IS NOT excluded.content_hash;END', + 'tab_to_history_on_insert', + ); + late final Trigger tabToHistoryOnUpdate = Trigger( + 'CREATE TRIGGER tab_to_history_on_update AFTER UPDATE OF title, url, extracted_content_plain, extracted_content_markdown, full_content_plain, full_content_markdown, is_probably_readerable ON tab WHEN NEW.url IS NOT NULL AND url_indexable(CAST(NEW.url AS TEXT)) = 1 AND(OLD.content_hash IS NOT NEW.content_hash OR OLD.url IS NOT NEW.url)AND (SELECT value FROM local_index_setting WHERE "key" = \'enabled\') = 1 AND(NEW.tab_mode != 1 OR (SELECT value FROM local_index_setting WHERE "key" = \'index_private\') = 1)AND NOT EXISTS (SELECT 1 FROM container WHERE container.id = NEW.container_id AND json_extract(container.metadata, \'\$.excludeFromIndex\') = 1) BEGIN INSERT INTO history (url_canonical, url_host, url_path, title, is_probably_readerable, extracted_content_markdown, extracted_content_plain, full_content_markdown, full_content_plain, content_hash, observed_at, observed_count) VALUES (url_canonical(CAST(NEW.url AS TEXT)), url_host(CAST(NEW.url AS TEXT)), url_path(CAST(NEW.url AS TEXT)), NEW.title, NEW.is_probably_readerable, NEW.extracted_content_markdown, NEW.extracted_content_plain, NEW.full_content_markdown, NEW.full_content_plain, NEW.content_hash, strftime(\'%s\', \'now\') * 1000, 1) ON CONFLICT (url_canonical) DO UPDATE SET title = COALESCE(excluded.title, history.title), is_probably_readerable = excluded.is_probably_readerable, extracted_content_markdown = excluded.extracted_content_markdown, extracted_content_plain = excluded.extracted_content_plain, full_content_markdown = excluded.full_content_markdown, full_content_plain = excluded.full_content_plain, content_hash = excluded.content_hash, observed_at = excluded.observed_at, observed_count = history.observed_count + 1 WHERE history.content_hash IS NOT excluded.content_hash;END', + 'tab_to_history_on_update', + ); + late final Trigger tabToHistoryOnContainerUpdate = Trigger( + 'CREATE TRIGGER tab_to_history_on_container_update AFTER UPDATE OF container_id ON tab WHEN NEW.url IS NOT NULL AND url_indexable(CAST(NEW.url AS TEXT)) = 1 AND (SELECT value FROM local_index_setting WHERE "key" = \'enabled\') = 1 BEGIN DELETE FROM history WHERE url_canonical = url_canonical(CAST(NEW.url AS TEXT)) AND NOT EXISTS (SELECT 1 FROM tab AS candidate WHERE candidate.url IS NOT NULL AND url_indexable(CAST(candidate.url AS TEXT)) = 1 AND url_canonical(CAST(candidate.url AS TEXT)) = history.url_canonical AND(candidate.tab_mode != 1 OR (SELECT value FROM local_index_setting WHERE "key" = \'index_private\') = 1)AND NOT EXISTS (SELECT 1 FROM container WHERE container.id = candidate.container_id AND json_extract(container.metadata, \'\$.excludeFromIndex\') = 1));INSERT INTO history (url_canonical, url_host, url_path, title, is_probably_readerable, extracted_content_markdown, extracted_content_plain, full_content_markdown, full_content_plain, content_hash, observed_at, observed_count) SELECT url_canonical(CAST(candidate.url AS TEXT)), url_host(CAST(candidate.url AS TEXT)), url_path(CAST(candidate.url AS TEXT)), candidate.title, candidate.is_probably_readerable, candidate.extracted_content_markdown, candidate.extracted_content_plain, candidate.full_content_markdown, candidate.full_content_plain, candidate.content_hash, strftime(\'%s\', \'now\') * 1000, 1 FROM tab AS candidate WHERE candidate.url IS NOT NULL AND url_indexable(CAST(candidate.url AS TEXT)) = 1 AND url_canonical(CAST(candidate.url AS TEXT)) = url_canonical(CAST(NEW.url AS TEXT)) AND(candidate.tab_mode != 1 OR (SELECT value FROM local_index_setting WHERE "key" = \'index_private\') = 1)AND NOT EXISTS (SELECT 1 FROM container WHERE container.id = candidate.container_id AND json_extract(container.metadata, \'\$.excludeFromIndex\') = 1) ORDER BY candidate.timestamp DESC, candidate."rowid" DESC LIMIT 1 ON CONFLICT (url_canonical) DO UPDATE SET title = COALESCE(excluded.title, history.title), is_probably_readerable = excluded.is_probably_readerable, extracted_content_markdown = excluded.extracted_content_markdown, extracted_content_plain = excluded.extracted_content_plain, full_content_markdown = excluded.full_content_markdown, full_content_plain = excluded.full_content_plain, content_hash = excluded.content_hash, observed_at = excluded.observed_at, observed_count = history.observed_count + 1;END', + 'tab_to_history_on_container_update', + ); + late final Trigger containerToHistoryOnMetadataUpdate = Trigger( + 'CREATE TRIGGER container_to_history_on_metadata_update AFTER UPDATE OF metadata ON container WHEN COALESCE(json_extract(OLD.metadata, \'\$.excludeFromIndex\') = 1, 0) != COALESCE(json_extract(NEW.metadata, \'\$.excludeFromIndex\') = 1, 0) AND (SELECT value FROM local_index_setting WHERE "key" = \'enabled\') = 1 BEGIN DELETE FROM history WHERE url_canonical IN (SELECT DISTINCT url_canonical(CAST(affected.url AS TEXT)) FROM tab AS affected WHERE affected.container_id = NEW.id AND affected.url IS NOT NULL AND url_indexable(CAST(affected.url AS TEXT)) = 1) AND NOT EXISTS (SELECT 1 FROM tab AS candidate WHERE candidate.url IS NOT NULL AND url_indexable(CAST(candidate.url AS TEXT)) = 1 AND url_canonical(CAST(candidate.url AS TEXT)) = history.url_canonical AND(candidate.tab_mode != 1 OR (SELECT value FROM local_index_setting WHERE "key" = \'index_private\') = 1)AND NOT EXISTS (SELECT 1 FROM container WHERE container.id = candidate.container_id AND json_extract(container.metadata, \'\$.excludeFromIndex\') = 1));INSERT INTO history (url_canonical, url_host, url_path, title, is_probably_readerable, extracted_content_markdown, extracted_content_plain, full_content_markdown, full_content_plain, content_hash, observed_at, observed_count) SELECT url_canonical(CAST(candidate.url AS TEXT)), url_host(CAST(candidate.url AS TEXT)), url_path(CAST(candidate.url AS TEXT)), candidate.title, candidate.is_probably_readerable, candidate.extracted_content_markdown, candidate.extracted_content_plain, candidate.full_content_markdown, candidate.full_content_plain, candidate.content_hash, strftime(\'%s\', \'now\') * 1000, 1 FROM tab AS candidate WHERE candidate.url IS NOT NULL AND url_indexable(CAST(candidate.url AS TEXT)) = 1 AND url_canonical(CAST(candidate.url AS TEXT)) IN (SELECT DISTINCT url_canonical(CAST(affected.url AS TEXT)) FROM tab AS affected WHERE affected.container_id = NEW.id AND affected.url IS NOT NULL AND url_indexable(CAST(affected.url AS TEXT)) = 1) AND(candidate.tab_mode != 1 OR (SELECT value FROM local_index_setting WHERE "key" = \'index_private\') = 1)AND NOT EXISTS (SELECT 1 FROM container WHERE container.id = candidate.container_id AND json_extract(container.metadata, \'\$.excludeFromIndex\') = 1) AND NOT EXISTS (SELECT 1 FROM tab AS newer WHERE newer.url IS NOT NULL AND url_indexable(CAST(newer.url AS TEXT)) = 1 AND url_canonical(CAST(newer.url AS TEXT)) = url_canonical(CAST(candidate.url AS TEXT)) AND(newer.tab_mode != 1 OR (SELECT value FROM local_index_setting WHERE "key" = \'index_private\') = 1)AND NOT EXISTS (SELECT 1 FROM container WHERE container.id = newer.container_id AND json_extract(container.metadata, \'\$.excludeFromIndex\') = 1) AND(newer.timestamp > candidate.timestamp OR(newer.timestamp = candidate.timestamp AND newer."rowid" > candidate."rowid"))) ON CONFLICT (url_canonical) DO UPDATE SET title = COALESCE(excluded.title, history.title), is_probably_readerable = excluded.is_probably_readerable, extracted_content_markdown = excluded.extracted_content_markdown, extracted_content_plain = excluded.extracted_content_plain, full_content_markdown = excluded.full_content_markdown, full_content_plain = excluded.full_content_plain, content_hash = excluded.content_hash, observed_at = excluded.observed_at, observed_count = history.observed_count + 1;END', + 'container_to_history_on_metadata_update', + ); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + container, + tab, + closedTabTombstone, + idxTabParentContainer, + captureTab, + idxCaptureTabCaptureId, + tabFts, + tabMaintainParentChainOnDelete, + tabAfterInsert, + tabAfterDelete, + tabAfterUpdate, + localIndexSetting, + history, + idxHistoryHost, + idxHistoryObserved, + historyFts, + historyAfterInsert, + historyAfterDelete, + historyAfterUpdate, + tabToHistoryOnInsert, + tabToHistoryOnUpdate, + tabToHistoryOnContainerUpdate, + containerToHistoryOnMetadataUpdate, + ]; + @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('capture_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)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'history', + limitUpdateKind: UpdateKind.insert, + ), + result: [TableUpdate('history_fts', kind: UpdateKind.insert)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'history', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('history_fts', kind: UpdateKind.insert)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'history', + limitUpdateKind: UpdateKind.update, + ), + result: [TableUpdate('history_fts', kind: UpdateKind.insert)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'tab', + limitUpdateKind: UpdateKind.insert, + ), + result: [TableUpdate('history', kind: UpdateKind.insert)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'tab', + limitUpdateKind: UpdateKind.update, + ), + result: [TableUpdate('history', kind: UpdateKind.insert)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'tab', + limitUpdateKind: UpdateKind.update, + ), + result: [ + TableUpdate('history', kind: UpdateKind.delete), + TableUpdate('history', kind: UpdateKind.insert), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'container', + limitUpdateKind: UpdateKind.update, + ), + result: [ + TableUpdate('history', kind: UpdateKind.delete), + TableUpdate('history', kind: UpdateKind.insert), + ], + ), + ]); + @override + int get schemaVersion => 14; +} diff --git a/apps/weblibre/test/drift/tabs/local_history_index_test.dart b/apps/weblibre/test/drift/tabs/local_history_index_test.dart new file mode 100644 index 00000000..ec8ab271 --- /dev/null +++ b/apps/weblibre/test/drift/tabs/local_history_index_test.dart @@ -0,0 +1,257 @@ +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:weblibre/data/database/functions/lexo_rank_functions.dart'; +import 'package:weblibre/data/database/functions/url_functions.dart'; +import 'package:weblibre/features/geckoview/features/tabs/data/database/database.dart'; + +void main() { + late TabDatabase db; + + setUp(() async { + db = TabDatabase( + NativeDatabase.memory( + setup: (database) { + registerLexorankFunctions(database); + registerUrlFunctions(database); + }, + ), + ); + + await db.customStatement( + 'INSERT INTO local_index_setting ("key", value) ' + "VALUES ('enabled', 1), ('index_private', 0)", + ); + }); + + tearDown(() async { + await db.close(); + }); + + test( + 'container exclude flag re-evaluates existing tab history rows', + () async { + await _insertContainer(db, 'container', excludeFromIndex: false); + await _insertTab( + db, + id: 'tab', + containerId: 'container', + url: 'https://example.com/page', + title: 'Visible title', + ); + + expect(await _historyTitles(db), ['Visible title']); + + await _updateContainerExclude(db, 'container', excludeFromIndex: true); + + expect(await _historyTitles(db), isEmpty); + + await _updateContainerExclude(db, 'container', excludeFromIndex: false); + + expect(await _historyTitles(db), ['Visible title']); + }, + ); + + test('tab container assignment re-evaluates existing history rows', () async { + await _insertContainer(db, 'allowed', excludeFromIndex: false); + await _insertContainer(db, 'excluded', excludeFromIndex: true); + await _insertTab( + db, + id: 'tab', + containerId: 'excluded', + url: 'https://example.com/moved', + title: 'Moved title', + ); + + expect(await _historyTitles(db), isEmpty); + + await db.customStatement( + "UPDATE tab SET container_id = 'allowed' WHERE id = 'tab'", + ); + + expect(await _historyTitles(db), ['Moved title']); + + await db.customStatement( + "UPDATE tab SET container_id = 'excluded' WHERE id = 'tab'", + ); + + expect(await _historyTitles(db), isEmpty); + }); + + test( + 'excluding one container preserves duplicate URLs from allowed tabs', + () async { + await _insertContainer(db, 'allowed', excludeFromIndex: false); + await _insertContainer(db, 'excluded', excludeFromIndex: false); + await _insertTab( + db, + id: 'allowed-tab', + containerId: 'allowed', + url: 'https://example.com/shared', + title: 'Allowed title', + timestamp: 1, + ); + await _insertTab( + db, + id: 'excluded-tab', + containerId: 'excluded', + url: 'https://example.com/shared', + title: 'Excluded title', + timestamp: 2, + ); + + expect(await _historyTitles(db), ['Excluded title']); + + await _updateContainerExclude(db, 'excluded', excludeFromIndex: true); + + expect(await _historyTitles(db), ['Allowed title']); + }, + ); + + test( + 'container exclude re-evaluation refreshes recency for same-hash duplicate URLs', + () async { + await _insertContainer(db, 'allowed', excludeFromIndex: false); + await _insertContainer(db, 'later-excluded', excludeFromIndex: false); + await _insertTab( + db, + id: 'later-excluded-tab', + containerId: 'later-excluded', + url: 'https://example.com/same-hash', + title: 'Shared title', + timestamp: 1, + ); + await _insertTab( + db, + id: 'allowed-tab', + containerId: 'allowed', + url: 'https://example.com/same-hash', + title: 'Shared title', + timestamp: 2, + ); + + await _setAllHistoryObservedAt(db, 1); + expect(await _historyObservedAt(db), 1); + + await _updateContainerExclude( + db, + 'later-excluded', + excludeFromIndex: true, + ); + + expect(await _historyTitles(db), ['Shared title']); + expect(await _historyObservedAt(db), greaterThan(1)); + }, + ); + + test( + 'container move re-evaluation refreshes recency for same-hash duplicate URLs', + () async { + await _insertContainer(db, 'allowed-a', excludeFromIndex: false); + await _insertContainer(db, 'allowed-b', excludeFromIndex: false); + await _insertContainer(db, 'excluded', excludeFromIndex: true); + await _insertTab( + db, + id: 'moving-tab', + containerId: 'allowed-a', + url: 'https://example.com/move-same-hash', + title: 'Shared move title', + timestamp: 1, + ); + await _insertTab( + db, + id: 'fallback-tab', + containerId: 'allowed-b', + url: 'https://example.com/move-same-hash', + title: 'Shared move title', + timestamp: 2, + ); + + await _setAllHistoryObservedAt(db, 1); + expect(await _historyObservedAt(db), 1); + + await db.customStatement( + "UPDATE tab SET container_id = 'excluded' WHERE id = 'moving-tab'", + ); + + expect(await _historyTitles(db), ['Shared move title']); + expect(await _historyObservedAt(db), greaterThan(1)); + }, + ); +} + +Future _insertContainer( + TabDatabase db, + String id, { + required bool excludeFromIndex, +}) { + return db.customStatement( + 'INSERT INTO container (id, color, order_key, is_pinned, metadata) ' + "VALUES (?, 0, ?, 0, ?)", + [id, id, '{"excludeFromIndex":$excludeFromIndex}'], + ); +} + +Future _updateContainerExclude( + TabDatabase db, + String id, { + required bool excludeFromIndex, +}) { + return db.customStatement('UPDATE container SET metadata = ? WHERE id = ?', [ + '{"excludeFromIndex":$excludeFromIndex}', + id, + ]); +} + +Future _insertTab( + TabDatabase db, { + required String id, + required String containerId, + required String url, + required String title, + int timestamp = 1, +}) { + return db.customStatement( + 'INSERT INTO tab (id, source, container_id, order_key, url, title, ' + 'extracted_content_plain, full_content_plain, timestamp) ' + 'VALUES (?, 2, ?, ?, ?, ?, ?, ?, ?)', + [ + id, + containerId, + id, + url, + title, + '$title extracted content', + '$title full content', + timestamp, + ], + ); +} + +Future> _historyTitles(TabDatabase db) async { + final rows = await db + .customSelect( + 'SELECT title FROM history ORDER BY url_canonical', + readsFrom: {db.history}, + ) + .get(); + + return [for (final row in rows) row.read('title')]; +} + +Future _setAllHistoryObservedAt(TabDatabase db, int observedAt) { + return db.customStatement('UPDATE history SET observed_at = ?', [observedAt]); +} + +Future _historyObservedAt(TabDatabase db) async { + final row = await db + .customSelect( + 'SELECT CAST(observed_at AS INTEGER) AS observed_at ' + 'FROM history ' + 'ORDER BY url_canonical ' + 'LIMIT 1', + readsFrom: {db.history}, + ) + .getSingleOrNull(); + + return row?.read('observed_at'); +} diff --git a/apps/weblibre/test/drift/tabs/migration_test.dart b/apps/weblibre/test/drift/tabs/migration_test.dart index 76625046..3c11e5f7 100644 --- a/apps/weblibre/test/drift/tabs/migration_test.dart +++ b/apps/weblibre/test/drift/tabs/migration_test.dart @@ -2,8 +2,9 @@ // ignore_for_file: unused_local_variable, unused_import import 'package:drift/drift.dart'; import 'package:drift_dev/api/migrations_native.dart'; -import 'package:weblibre/features/geckoview/features/tabs/data/database/database.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:weblibre/data/database/functions/url_functions.dart'; +import 'package:weblibre/features/geckoview/features/tabs/data/database/database.dart'; import 'generated/schema.dart'; import 'generated/schema_v2.dart' as v2; @@ -14,7 +15,7 @@ void main() { late SchemaVerifier verifier; setUpAll(() { - verifier = SchemaVerifier(GeneratedHelper()); + verifier = SchemaVerifier(GeneratedHelper(), setup: registerUrlFunctions); }); group('simple database migrations', () {