per-containder indexing opt out

This commit is contained in:
Fabian Freund
2026-05-26 16:15:14 +02:00
parent 33fd8e65f1
commit 1b8eaf2c76
16 changed files with 5164 additions and 15 deletions
File diff suppressed because it is too large Load Diff
@@ -41,7 +41,7 @@ final class TabRepositoryProvider
}
}
String _$tabRepositoryHash() => r'2cd5b6bf5bb9a62c05b038b28acef59cecbeae4e';
String _$tabRepositoryHash() => r'7d41038ac5a20ecf6de0645b99231fb8b2f6963a';
abstract class _$TabRepository extends $Notifier<void> {
void build();
@@ -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);
},
);
}
@@ -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),
],
),
]);
}
@@ -1823,6 +1823,222 @@ class Shape12 extends i0.VersionedTable {
columnsByName['metadata']! as i1.GeneratedColumn<String>;
}
final class Schema14 extends i0.VersionedSchema {
Schema14({required super.database}) : super(version: 14);
@override
late final List<i1.DatabaseSchemaEntity> 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<void> Function(i1.Migrator m, Schema3 schema) from2To3,
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
@@ -1835,6 +2051,7 @@ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema11 schema) from10To11,
required Future<void> Function(i1.Migrator m, Schema12 schema) from11To12,
required Future<void> Function(i1.Migrator m, Schema13 schema) from12To13,
required Future<void> 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<void> Function(i1.Migrator m, Schema11 schema) from10To11,
required Future<void> Function(i1.Migrator m, Schema12 schema) from11To12,
required Future<void> Function(i1.Migrator m, Schema13 schema) from12To13,
required Future<void> 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,
),
);
@@ -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.*,
@@ -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);
@@ -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<Uri>? 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<Uri>? 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,
];
}
@@ -15,6 +15,8 @@ abstract class _$ContainerMetadataCWProxy {
ContainerMetadata clearDataOnExit(bool clearDataOnExit);
ContainerMetadata excludeFromIndex(bool excludeFromIndex);
ContainerMetadata assignedSites(List<Uri>? 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<Uri>? 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<Uri>? 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<String, dynamic> json) =>
json['proxyConnectionId'] as String?,
),
clearDataOnExit: json['clearDataOnExit'] as bool? ?? false,
excludeFromIndex: json['excludeFromIndex'] as bool? ?? false,
assignedSites: (json['assignedSites'] as List<dynamic>?)
?.map((e) => Uri.parse(e as String))
.toList(),
@@ -247,6 +262,7 @@ Map<String, dynamic> _$ContainerMetadataToJson(
'contextualIdentity': instance.contextualIdentity,
'proxyConnectionId': _proxyConnectionIdToJson(instance.proxyConnectionId),
'clearDataOnExit': instance.clearDataOnExit,
'excludeFromIndex': instance.excludeFromIndex,
'assignedSites': instance.assignedSites?.map((e) => e.toString()).toList(),
};
@@ -503,7 +503,7 @@ final class WatchTabDbDataProvider
}
}
String _$watchTabDbDataHash() => r'c505be1cdeaffced2615fdb075ed8a9a2132754a';
String _$watchTabDbDataHash() => r'90d9f479d9d6c5475185b0490089ea67edff7894';
final class WatchTabDbDataFamily extends $Family
with $FunctionalFamilyOverride<Stream<TabData?>, String> {
@@ -41,7 +41,7 @@ final class TabDataRepositoryProvider
}
}
String _$tabDataRepositoryHash() => r'2208b806a97f239a5b3e6ed7e6c395cbd0276bdd';
String _$tabDataRepositoryHash() => r'38750c2e0d27b407b1038d324ad22eeb202da6eb';
abstract class _$TabDataRepository extends $Notifier<void> {
void build();
@@ -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;
},
),
],
),
),
@@ -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];
}
File diff suppressed because it is too large Load Diff
@@ -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<void> _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<void> _updateContainerExclude(
TabDatabase db,
String id, {
required bool excludeFromIndex,
}) {
return db.customStatement('UPDATE container SET metadata = ? WHERE id = ?', [
'{"excludeFromIndex":$excludeFromIndex}',
id,
]);
}
Future<void> _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<List<String?>> _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<String?>('title')];
}
Future<void> _setAllHistoryObservedAt(TabDatabase db, int observedAt) {
return db.customStatement('UPDATE history SET observed_at = ?', [observedAt]);
}
Future<int?> _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<int>('observed_at');
}
@@ -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', () {