diff --git a/docs/architecture/data-model.md b/docs/architecture/data-model.md index 805ac1f5..ae9cc9da 100644 --- a/docs/architecture/data-model.md +++ b/docs/architecture/data-model.md @@ -8,12 +8,12 @@ 不同瀏覽器使用完全不同的時間格式來儲存歷史紀錄: -| 瀏覽器 | 原生時間格式 | -|--------|-------------| -| Chrome / Chromium | WebKit epoch — 自 1601-01-01 00:00:00 UTC 起的微秒數 | -| Firefox | Unix epoch 毫秒數 | -| Safari | Mac absolute time — 自 2001-01-01 00:00:00 UTC 起的秒數(浮點數) | -| Google Takeout | ISO 8601 字串(如 `2024-03-15T10:30:00.000Z`) | +| 瀏覽器 | 原生時間格式 | +| ----------------- | ----------------------------------------------------------------- | +| Chrome / Chromium | WebKit epoch — 自 1601-01-01 00:00:00 UTC 起的微秒數 | +| Firefox | Unix epoch 毫秒數 | +| Safari | Mac absolute time — 自 2001-01-01 00:00:00 UTC 起的秒數(浮點數) | +| Google Takeout | ISO 8601 字串(如 `2024-03-15T10:30:00.000Z`) | **我們的 archive 必須統一所有時間為單一格式。** diff --git a/docs/architecture/tech-stack.md b/docs/architecture/tech-stack.md index 915d31fc..40778962 100644 --- a/docs/architecture/tech-stack.md +++ b/docs/architecture/tech-stack.md @@ -6,19 +6,19 @@ ## 技術棧 -| 層面 | 選型 | 理由 | -|------|------|------| -| 桌面框架 | Tauri 2 | 跨平台、Rust 核心、輕量級 | -| 核心邏輯 | Rust workspace(vault-core, vault-worker, vault-platform) | 高性能、安全、跨平台 | -| 瀏覽器解析 | `browser-history-parser` — 計劃獨立發布的 Rust crate | 通用的瀏覽器歷史紀錄解析,可供社區使用 | -| 前端 | React 19 + TypeScript + Vite | 現代前端、型別安全 | -| 工具鏈 | Bun | JS 側的包管理與腳本 | -| 數據存儲 | SQLite(可選 SQLCipher 加密) | 本地優先、20 年持久性 | -| 全文搜尋 | SQLite FTS5 | 核心召回能力,不依賴外部服務 | -| 向量 / 語義檢索 | LanceDB sidecar | 嵌入式、Rust 原生、disk-based indexing | -| AI 框架 | rig.rs | Rust 原生的 LLM + Embedding 框架 | -| AI 推理 | 本地推理(Ollama / LM Studio)或雲端 API | 可選、可配置 | -| 審計 | Git(只管理 manifests 和審計工件) | 可追溯性 | +| 層面 | 選型 | 理由 | +| --------------- | ---------------------------------------------------------- | -------------------------------------- | +| 桌面框架 | Tauri 2 | 跨平台、Rust 核心、輕量級 | +| 核心邏輯 | Rust workspace(vault-core, vault-worker, vault-platform) | 高性能、安全、跨平台 | +| 瀏覽器解析 | `browser-history-parser` — 計劃獨立發布的 Rust crate | 通用的瀏覽器歷史紀錄解析,可供社區使用 | +| 前端 | React 19 + TypeScript + Vite | 現代前端、型別安全 | +| 工具鏈 | Bun | JS 側的包管理與腳本 | +| 數據存儲 | SQLite(可選 SQLCipher 加密) | 本地優先、20 年持久性 | +| 全文搜尋 | SQLite FTS5 | 核心召回能力,不依賴外部服務 | +| 向量 / 語義檢索 | LanceDB sidecar | 嵌入式、Rust 原生、disk-based indexing | +| AI 框架 | rig.rs | Rust 原生的 LLM + Embedding 框架 | +| AI 推理 | 本地推理(Ollama / LM Studio)或雲端 API | 可選、可配置 | +| 審計 | Git(只管理 manifests 和審計工件) | 可追溯性 | ## 數據庫分層架構 @@ -33,12 +33,12 @@ ## AI 框架決策:rig.rs -| 維度 | 說明 | -|------|------| -| 為什麼 rig.rs | Rust 原生 LLM/Embedding 框架,與我們的 Rust workspace 自然整合 | -| Embedding | 通過 rig.rs 統一的 provider abstraction 調用,支援 Ollama / OpenAI-compatible / 雲端 API | -| LLM | 同上,用於摘要生成、topic 命名、問答等 Intelligence 功能 | -| 向量存儲 | rig.rs 產生的 embedding 存入 LanceDB sidecar | +| 維度 | 說明 | +| ------------- | ---------------------------------------------------------------------------------------- | +| 為什麼 rig.rs | Rust 原生 LLM/Embedding 框架,與我們的 Rust workspace 自然整合 | +| Embedding | 通過 rig.rs 統一的 provider abstraction 調用,支援 Ollama / OpenAI-compatible / 雲端 API | +| LLM | 同上,用於摘要生成、topic 命名、問答等 Intelligence 功能 | +| 向量存儲 | rig.rs 產生的 embedding 存入 LanceDB sidecar | ## 目標平台 diff --git a/docs/design/screens-and-nav.md b/docs/design/screens-and-nav.md index 94561f50..762bec5e 100644 --- a/docs/design/screens-and-nav.md +++ b/docs/design/screens-and-nav.md @@ -7,18 +7,18 @@ ## 畫面清單 -| 畫面 | 核心職責 | -|------|----------| -| **Onboarding / Setup** | 首次啟動引導:發現瀏覽器、選擇 profile、設定存儲、加密選擇 | -| **Dashboard** | 備份狀態總覽、最近 run 摘要、歷史上的今天、定期總結卡片、Job Queue 狀態、快速操作入口 | -| **History Explorer** | 時間軸 + 全文搜尋 + 篩選 + 詳情 + 匯出 | -| **Insights** | 洞察卡片、topic timeline、threads、query ladders、profile facets | -| **AI Assistant** | 自然語言問答介面 | -| **Import** | Takeout 導入 wizard + 瀏覽器直接導入(含 step-by-step UI) | -| **Audit Ledger** | Manifest chain、run 歷史、diff 視圖、schema 變化紀錄 | -| **Security** | 加密設定、keyring、rekey、密碼警告 | -| **Schedule Setup** | 排程預覽 → 手動安裝/自動安裝 → 狀態監控 | -| **Settings** | 通用設定、語言、AI provider 管理、MCP 開關、數據目錄、版本信息 | +| 畫面 | 核心職責 | +| ---------------------- | ------------------------------------------------------------------------------------- | +| **Onboarding / Setup** | 首次啟動引導:發現瀏覽器、選擇 profile、設定存儲、加密選擇 | +| **Dashboard** | 備份狀態總覽、最近 run 摘要、歷史上的今天、定期總結卡片、Job Queue 狀態、快速操作入口 | +| **History Explorer** | 時間軸 + 全文搜尋 + 篩選 + 詳情 + 匯出 | +| **Insights** | 洞察卡片、topic timeline、threads、query ladders、profile facets | +| **AI Assistant** | 自然語言問答介面 | +| **Import** | Takeout 導入 wizard + 瀏覽器直接導入(含 step-by-step UI) | +| **Audit Ledger** | Manifest chain、run 歷史、diff 視圖、schema 變化紀錄 | +| **Security** | 加密設定、keyring、rekey、密碼警告 | +| **Schedule Setup** | 排程預覽 → 手動安裝/自動安裝 → 狀態監控 | +| **Settings** | 通用設定、語言、AI provider 管理、MCP 開關、數據目錄、版本信息 | --- diff --git a/docs/features/archive.md b/docs/features/archive.md index 09d24b3b..9b860c53 100644 --- a/docs/features/archive.md +++ b/docs/features/archive.md @@ -184,6 +184,7 @@ ### 增強層級 **第一層:立即可用(不需要外部請求)** + - URL 結構解析:domain, subdomain, path tokens, query parameters - Domain 分類:docs / forum / video / news / social / shopping / code 等 - 搜尋引擎 query 提取(從 URL 參數中解析 `q=`, `search_query=` 等) @@ -191,11 +192,13 @@ - Favicon **第二層:背景 refetch** + - 訪問 URL 抓取頁面內容,提取 readable text、meta description、OG tags - 提取頁面語言 - Best-effort,失敗不阻塞 **第三層:基於 URL 的專屬 enrichment 插件** + - 設計為**可擴展的插件架構**:每個插件匹配一組 URL pattern,負責提取特定的結構化信息。 - 插件範例: - **arXiv 插件**:匹配 `arxiv.org/abs/*`,調用 arXiv API 獲取論文標題、作者、abstract、分類、發表日期 @@ -209,6 +212,7 @@ - 插件的增強結果存入統一的 enrichment 表,以 JSON 格式保存,欄位隨插件不同而不同。 **第四層:未來擴展 — 瀏覽時即時捕獲** + - 未來可能透過瀏覽器擴充套件在瀏覽時即時抓取頁面內容。 - Schema 預留 `content_source` 欄位標記來源(`plugin`, `refetch`, `realtime_capture`),讓即時捕獲的數據可以取代 refetch 結果。 diff --git a/docs/features/intelligence.md b/docs/features/intelligence.md index 7556b193..46c48493 100644 --- a/docs/features/intelligence.md +++ b/docs/features/intelligence.md @@ -68,6 +68,7 @@ - 第三方未來可以貢獻新的洞察插件。 每個洞察模塊定義: + - **名稱和描述** - **數據依賴**:需要哪些表、哪些 enrichment、是否需要 embedding - **計算邏輯**:怎麼從原始數據算出洞察結果 diff --git a/docs/features/recall.md b/docs/features/recall.md index 2d3be9da..2cb97da6 100644 --- a/docs/features/recall.md +++ b/docs/features/recall.md @@ -50,12 +50,14 @@ 用戶瀏覽歷史紀錄時,每條記錄默認顯示的信息以及可選顯示的信息,**用戶可以在設定中自定義**。 **預設顯示:** + - Favicon + 頁面標題 - URL(可展開/摺疊長 URL) - 訪問時間 - 來源瀏覽器 / Profile 標識 **可選顯示(用戶在設定中開關):** + - 訪問次數 - 來源途徑(typed, link, redirect 等) - Domain 分類標籤 @@ -69,6 +71,7 @@ ### 記錄詳情面板 點擊任一條記錄,展開詳情面板,顯示該條記錄的完整信息: + - 完整 URL - 頁面標題(所有歷史版本) - 所有訪問時間(如果同一 URL 被多次訪問) @@ -109,13 +112,13 @@ ### 典型誤操作場景 -| 場景 | 恢復方式 | -|------|----------| -| 導入了格式錯誤的 Takeout 檔案 | 回滾該次 import run | -| 錯誤的 profile 被選中並備份了 | 回滾該次 backup run | -| 同一份數據被重複導入 | 去重機制自動處理;如有異常可回滾 | -| 導入了別人的歷史紀錄 | 回滾該次 import run | -| Schema migration 後發現問題 | Archive DB 自動備份可恢復 | +| 場景 | 恢復方式 | +| ----------------------------- | -------------------------------- | +| 導入了格式錯誤的 Takeout 檔案 | 回滾該次 import run | +| 錯誤的 profile 被選中並備份了 | 回滾該次 backup run | +| 同一份數據被重複導入 | 去重機制自動處理;如有異常可回滾 | +| 導入了別人的歷史紀錄 | 回滾該次 import run | +| Schema migration 後發現問題 | Archive DB 自動備份可恢復 | ### Archive 快照(Safety Net) diff --git a/src-tauri/crates/vault-core/src/archive.rs b/src-tauri/crates/vault-core/src/archive.rs index 673099d9..4658baba 100644 --- a/src-tauri/crates/vault-core/src/archive.rs +++ b/src-tauri/crates/vault-core/src/archive.rs @@ -46,7 +46,8 @@ struct BackupManifest { warnings: Vec, } -const ARCHIVE_SCHEMA_SQL: &str = include_str!("archive-schema.sql"); +const MIGRATION_0001_SQL: &str = include_str!("migrations/0001_initial_archive.sql"); +const MIGRATION_0002_SQL: &str = include_str!("migrations/0002_import_recoverability.sql"); const RECENT_RUNS_SQL: &str = "SELECT id, started_at, finished_at, status, manifest_hash, summary_json FROM backup_runs ORDER BY id DESC LIMIT 12"; const LIST_HISTORY_SQL: &str = "SELECT id, profile_id, url, title, visit_time, visit_duration, transition, source_visit_id, app_id FROM visit_events WHERE (?1 IS NULL OR profile_id = ?1) AND (?2 IS NULL OR url LIKE '%' || ?2 || '%' OR IFNULL(title, '') LIKE '%' || ?2 || '%') AND (?3 IS NULL OR url LIKE ?3) ORDER BY visit_time DESC LIMIT ?4"; const INGEST_URLS_SQL: &str = "SELECT id, url, title, visit_count, typed_count, last_visit_time, hidden FROM urls WHERE last_visit_time >= ?1 ORDER BY last_visit_time ASC"; @@ -490,21 +491,54 @@ pub(crate) fn export_archive_database( } pub(crate) fn create_schema(connection: &Connection) -> Result<()> { - connection.execute_batch(ARCHIVE_SCHEMA_SQL)?; - ensure_column(connection, "visit_events", "import_batch_id", "INTEGER")?; - ensure_column(connection, "visit_events", "event_fingerprint", "TEXT")?; - ensure_column(connection, "raw_row_versions", "import_batch_id", "INTEGER")?; - #[rustfmt::skip] - connection.execute("CREATE INDEX IF NOT EXISTS idx_visit_events_import_batch_id ON visit_events(import_batch_id)", [])?; - #[rustfmt::skip] - connection.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_visit_events_profile_event_fingerprint ON visit_events(profile_id, event_fingerprint) WHERE event_fingerprint IS NOT NULL AND event_fingerprint != ''", [])?; - #[rustfmt::skip] - connection.execute("CREATE INDEX IF NOT EXISTS idx_raw_row_versions_import_batch_id ON raw_row_versions(import_batch_id)", [])?; + connection.execute( + "CREATE TABLE IF NOT EXISTS schema_migrations ( + version INTEGER PRIMARY KEY, + name TEXT NOT NULL, + applied_at TEXT NOT NULL + )", + [], + )?; + apply_migration(connection, 1, "initial_archive_schema", || { + connection.execute_batch(MIGRATION_0001_SQL)?; + Ok(()) + })?; + apply_migration(connection, 2, "import_recoverability_columns", || { + ensure_column(connection, "visit_events", "import_batch_id", "INTEGER")?; + ensure_column(connection, "visit_events", "event_fingerprint", "TEXT")?; + ensure_column(connection, "raw_row_versions", "import_batch_id", "INTEGER")?; + connection.execute_batch(MIGRATION_0002_SQL)?; + Ok(()) + })?; ensure_ai_schema(connection)?; ensure_insight_schema(connection)?; Ok(()) } +fn apply_migration(connection: &Connection, version: i64, name: &str, migration: F) -> Result<()> +where + F: FnOnce() -> Result<()>, +{ + let already_applied = connection + .query_row( + "SELECT EXISTS(SELECT 1 FROM schema_migrations WHERE version = ?1)", + params![version], + |row| row.get::<_, i64>(0), + ) + .optional()? + .unwrap_or(0) + == 1; + if already_applied { + return Ok(()); + } + migration()?; + connection.execute( + "INSERT INTO schema_migrations (version, name, applied_at) VALUES (?1, ?2, ?3)", + params![version, name, now_rfc3339()], + )?; + Ok(()) +} + fn ensure_column( connection: &Connection, table_name: &str, @@ -1562,13 +1596,12 @@ mod tests { #[test] fn schema_migration_helpers_and_text_renderers_are_stable() { let connection = Connection::open_in_memory().expect("db"); - connection - .execute("CREATE TABLE visit_events (id INTEGER PRIMARY KEY)", []) - .expect("create visit_events"); - ensure_column(&connection, "visit_events", "import_batch_id", "INTEGER") - .expect("add column"); - ensure_column(&connection, "visit_events", "import_batch_id", "INTEGER") - .expect("idempotent"); + create_schema(&connection).expect("create schema"); + create_schema(&connection).expect("idempotent"); + let applied_versions = connection + .query_row("SELECT COUNT(*) FROM schema_migrations", [], |row| row.get::<_, i64>(0)) + .expect("load migration count"); + assert_eq!(applied_versions, 2); let results = HistoryQueryResponse { total: 1, @@ -1598,6 +1631,88 @@ mod tests { })); } + #[test] + fn create_schema_upgrades_legacy_archives_without_baseline_stamping() { + let connection = Connection::open_in_memory().expect("db"); + connection + .execute_batch( + " + CREATE TABLE profiles ( + profile_id TEXT PRIMARY KEY, + profile_name TEXT NOT NULL, + user_name TEXT, + profile_path TEXT NOT NULL, + chrome_version TEXT, + updated_at TEXT NOT NULL + ); + CREATE TABLE visit_events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + profile_id TEXT NOT NULL, + source_visit_id INTEGER NOT NULL, + source_url_id INTEGER NOT NULL, + url TEXT NOT NULL, + title TEXT, + visit_time INTEGER NOT NULL, + payload_hash TEXT NOT NULL, + recorded_at TEXT NOT NULL, + UNIQUE(profile_id, source_visit_id) + ); + CREATE TABLE raw_row_versions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + run_id INTEGER NOT NULL, + profile_id TEXT NOT NULL, + source_kind TEXT NOT NULL, + table_name TEXT NOT NULL, + source_pk TEXT NOT NULL, + payload_hash TEXT NOT NULL, + payload_json TEXT NOT NULL, + schema_hash TEXT NOT NULL, + recorded_at TEXT NOT NULL + ); + ", + ) + .expect("legacy schema"); + + create_schema(&connection).expect("upgrade legacy schema"); + + let migration_names = { + let mut statement = connection + .prepare("SELECT name FROM schema_migrations ORDER BY version ASC") + .expect("prepare migration list"); + statement + .query_map([], |row| row.get::<_, String>(0)) + .expect("query migrations") + .collect::>>() + .expect("collect migrations") + }; + assert_eq!( + migration_names, + vec!["initial_archive_schema".to_string(), "import_recoverability_columns".to_string()] + ); + + let visit_has_import_batch = connection + .prepare("PRAGMA table_info(visit_events)") + .expect("visit info") + .query_map([], |row| row.get::<_, String>(1)) + .expect("visit columns") + .collect::>>() + .expect("collect visit columns") + .into_iter() + .any(|column| column == "import_batch_id"); + assert!(visit_has_import_batch); + + let raw_row_has_import_batch = connection + .prepare("PRAGMA table_info(raw_row_versions)") + .expect("raw_row info") + .query_map([], |row| row.get::<_, String>(1)) + .expect("raw_row columns") + .collect::>>() + .expect("collect raw_row columns") + .into_iter() + .any(|column| column == "import_batch_id"); + assert!(raw_row_has_import_batch); + } + #[test] fn watermark_checkpoint_and_snapshot_helpers_cover_edge_cases() { let dir = tempdir().expect("tempdir"); diff --git a/src-tauri/crates/vault-core/src/migrations/0001_initial_archive.sql b/src-tauri/crates/vault-core/src/migrations/0001_initial_archive.sql new file mode 100644 index 00000000..d3abfa1a --- /dev/null +++ b/src-tauri/crates/vault-core/src/migrations/0001_initial_archive.sql @@ -0,0 +1,148 @@ +PRAGMA journal_mode = WAL; +CREATE TABLE IF NOT EXISTS profiles ( + profile_id TEXT PRIMARY KEY, + profile_name TEXT NOT NULL, + user_name TEXT, + profile_path TEXT NOT NULL, + chrome_version TEXT, + updated_at TEXT NOT NULL +); +CREATE TABLE IF NOT EXISTS source_schemas ( + schema_hash TEXT PRIMARY KEY, + source_kind TEXT NOT NULL, + chrome_version TEXT, + payload_json TEXT NOT NULL, + seen_at TEXT NOT NULL +); +CREATE TABLE IF NOT EXISTS backup_runs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + started_at TEXT NOT NULL, + finished_at TEXT, + status TEXT NOT NULL, + due_only INTEGER NOT NULL, + profiles_json TEXT NOT NULL, + manifest_path TEXT, + manifest_hash TEXT, + previous_manifest_hash TEXT, + summary_json TEXT +); +CREATE TABLE IF NOT EXISTS manifests ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + run_id INTEGER NOT NULL UNIQUE, + manifest_hash TEXT NOT NULL UNIQUE, + previous_manifest_hash TEXT, + path TEXT NOT NULL, + created_at TEXT NOT NULL +); +CREATE TABLE IF NOT EXISTS import_batches ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + source_kind TEXT NOT NULL, + source_path TEXT NOT NULL, + profile_id TEXT NOT NULL, + created_at TEXT NOT NULL, + imported_at TEXT, + reverted_at TEXT, + status TEXT NOT NULL, + summary_json TEXT NOT NULL, + audit_path TEXT, + git_commit TEXT +); +CREATE TABLE IF NOT EXISTS raw_row_versions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + run_id INTEGER NOT NULL, + profile_id TEXT NOT NULL, + source_kind TEXT NOT NULL, + table_name TEXT NOT NULL, + source_pk TEXT NOT NULL, + payload_hash TEXT NOT NULL, + payload_json TEXT NOT NULL, + schema_hash TEXT NOT NULL, + chrome_version TEXT, + recorded_at TEXT NOT NULL, + UNIQUE(profile_id, source_kind, table_name, source_pk, payload_hash) +); +CREATE TABLE IF NOT EXISTS url_versions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + profile_id TEXT NOT NULL, + source_url_id INTEGER NOT NULL, + url TEXT NOT NULL, + title TEXT, + visit_count INTEGER, + typed_count INTEGER, + last_visit_time INTEGER NOT NULL, + hidden INTEGER NOT NULL, + payload_hash TEXT NOT NULL, + recorded_at TEXT NOT NULL, + UNIQUE(profile_id, source_url_id, payload_hash) +); +CREATE TABLE IF NOT EXISTS visit_events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + profile_id TEXT NOT NULL, + source_visit_id INTEGER NOT NULL, + source_url_id INTEGER NOT NULL, + url TEXT NOT NULL, + title TEXT, + visit_time INTEGER NOT NULL, + from_visit INTEGER, + transition INTEGER, + visit_duration INTEGER, + is_known_to_sync INTEGER, + visited_link_id INTEGER, + external_referrer_url TEXT, + app_id TEXT, + event_fingerprint TEXT, + payload_hash TEXT NOT NULL, + recorded_at TEXT NOT NULL, + UNIQUE(profile_id, source_visit_id) +); +CREATE TABLE IF NOT EXISTS download_versions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + profile_id TEXT NOT NULL, + source_download_id INTEGER NOT NULL, + guid TEXT, + current_path TEXT, + target_path TEXT, + start_time INTEGER, + total_bytes INTEGER, + received_bytes INTEGER, + state INTEGER, + mime_type TEXT, + original_mime_type TEXT, + payload_hash TEXT NOT NULL, + recorded_at TEXT NOT NULL, + UNIQUE(profile_id, source_download_id, payload_hash) +); +CREATE TABLE IF NOT EXISTS search_terms ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + profile_id TEXT NOT NULL, + keyword_id INTEGER NOT NULL, + url_id INTEGER NOT NULL, + term TEXT NOT NULL, + normalized_term TEXT NOT NULL, + recorded_at TEXT NOT NULL, + UNIQUE(profile_id, keyword_id, url_id, term, normalized_term) +); +CREATE TABLE IF NOT EXISTS favicons ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + profile_id TEXT NOT NULL, + page_url TEXT NOT NULL, + icon_url TEXT NOT NULL, + icon_type INTEGER, + width INTEGER, + height INTEGER, + last_updated INTEGER, + image_data BLOB, + payload_hash TEXT NOT NULL, + recorded_at TEXT NOT NULL, + UNIQUE(profile_id, page_url, icon_url, payload_hash) +); +CREATE TABLE IF NOT EXISTS profile_watermarks ( + profile_id TEXT PRIMARY KEY, + last_visit_id INTEGER NOT NULL DEFAULT 0, + last_url_last_visit_time INTEGER NOT NULL DEFAULT 0, + last_download_id INTEGER NOT NULL DEFAULT 0, + last_favicon_last_updated INTEGER NOT NULL DEFAULT 0, + last_checkpoint_at TEXT, + last_schema_hash TEXT, + updated_at TEXT NOT NULL +); diff --git a/src-tauri/crates/vault-core/src/migrations/0002_import_recoverability.sql b/src-tauri/crates/vault-core/src/migrations/0002_import_recoverability.sql new file mode 100644 index 00000000..b5894ac1 --- /dev/null +++ b/src-tauri/crates/vault-core/src/migrations/0002_import_recoverability.sql @@ -0,0 +1,4 @@ +CREATE INDEX IF NOT EXISTS idx_visit_events_import_batch_id ON visit_events(import_batch_id); +CREATE UNIQUE INDEX IF NOT EXISTS idx_visit_events_profile_event_fingerprint ON visit_events(profile_id, event_fingerprint) +WHERE event_fingerprint IS NOT NULL AND event_fingerprint != ''; +CREATE INDEX IF NOT EXISTS idx_raw_row_versions_import_batch_id ON raw_row_versions(import_batch_id);