"""SQLite metadata index.""" from __future__ import annotations import json import sqlite3 import time from pathlib import Path from typing import Any, Iterable SCHEMA_VERSION = 1 SCHEMA = """ CREATE TABLE IF NOT EXISTS roots ( id INTEGER PRIMARY KEY, path TEXT NOT NULL UNIQUE, mode TEXT NOT NULL DEFAULT 'pending', watch_count INTEGER NOT NULL DEFAULT 0, polled_dirs INTEGER NOT NULL DEFAULT 0, baseline_state TEXT NOT NULL DEFAULT 'pending', baseline_files INTEGER NOT NULL DEFAULT 0, added_at REAL NOT NULL, last_scan_at REAL ); CREATE TABLE IF NOT EXISTS projects ( id INTEGER PRIMARY KEY, root_id INTEGER REFERENCES roots(id) ON DELETE SET NULL, path TEXT NOT NULL UNIQUE, detected_by TEXT NOT NULL, created_at REAL NOT NULL, last_activity_at REAL ); CREATE TABLE IF NOT EXISTS files ( id INTEGER PRIMARY KEY, root_id INTEGER REFERENCES roots(id) ON DELETE SET NULL, project_id INTEGER REFERENCES projects(id) ON DELETE SET NULL, path TEXT NOT NULL UNIQUE, exists_on_disk INTEGER NOT NULL DEFAULT 1, inode INTEGER, mtime_ns INTEGER, size INTEGER, first_seen_at REAL NOT NULL, last_version_at REAL ); CREATE TABLE IF NOT EXISTS blobs ( sha256 TEXT PRIMARY KEY, size INTEGER NOT NULL, stored_size INTEGER NOT NULL, created_at REAL NOT NULL ); CREATE TABLE IF NOT EXISTS versions ( id INTEGER PRIMARY KEY, file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, blob_sha256 TEXT NOT NULL REFERENCES blobs(sha256), captured_at REAL NOT NULL, source TEXT NOT NULL, reason TEXT NOT NULL, pinned INTEGER NOT NULL DEFAULT 0, durability TEXT NOT NULL DEFAULT 'local' ); CREATE TABLE IF NOT EXISTS renames ( id INTEGER PRIMARY KEY, file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, old_path TEXT NOT NULL, new_path TEXT NOT NULL, at REAL NOT NULL ); CREATE TABLE IF NOT EXISTS restore_plans ( id TEXT PRIMARY KEY, created_at REAL NOT NULL, expires_at REAL NOT NULL, criteria_json TEXT NOT NULL, plan_json TEXT NOT NULL, status TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS audit_log ( id INTEGER PRIMARY KEY, at REAL NOT NULL, action TEXT NOT NULL, details_json TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS versions_file_time ON versions(file_id, captured_at); CREATE INDEX IF NOT EXISTS versions_blob ON versions(blob_sha256); CREATE INDEX IF NOT EXISTS files_root ON files(root_id); CREATE INDEX IF NOT EXISTS files_project ON files(project_id); """ def row_dict(row: sqlite3.Row | None) -> dict[str, Any] | None: return dict(row) if row is not None else None class Database: def __init__(self, path: Path): self.conn = sqlite3.connect(path, check_same_thread=False, isolation_level=None) self.conn.row_factory = sqlite3.Row self.conn.execute("PRAGMA journal_mode=WAL") self.conn.execute("PRAGMA synchronous=NORMAL") self.conn.execute("PRAGMA foreign_keys=ON") self.conn.executescript(SCHEMA) current = self.conn.execute("PRAGMA user_version").fetchone()[0] if current < SCHEMA_VERSION: self.conn.execute(f"PRAGMA user_version={SCHEMA_VERSION}") def close(self) -> None: self.conn.close() def execute(self, sql: str, params: Iterable[Any] = ()) -> sqlite3.Cursor: return self.conn.execute(sql, tuple(params)) def one(self, sql: str, params: Iterable[Any] = ()) -> dict[str, Any] | None: return row_dict(self.conn.execute(sql, tuple(params)).fetchone()) def all(self, sql: str, params: Iterable[Any] = ()) -> list[dict[str, Any]]: return [dict(r) for r in self.conn.execute(sql, tuple(params)).fetchall()] def transaction(self): return _Transaction(self.conn) def audit(self, action: str, **details: Any) -> None: self.execute( "INSERT INTO audit_log(at, action, details_json) VALUES (?, ?, ?)", (time.time(), action, json.dumps(details, default=str)), ) # roots def roots(self) -> list[dict[str, Any]]: return self.all("SELECT * FROM roots ORDER BY id") def root(self, root_id: int) -> dict[str, Any] | None: return self.one("SELECT * FROM roots WHERE id = ?", (root_id,)) def update_root(self, root_id: int, **fields: Any) -> None: cols = ", ".join(f"{k} = ?" for k in fields) self.execute(f"UPDATE roots SET {cols} WHERE id = ?", (*fields.values(), root_id)) # files and versions def file_by_path(self, path: str) -> dict[str, Any] | None: return self.one("SELECT * FROM files WHERE path = ?", (path,)) def file(self, file_id: int) -> dict[str, Any] | None: return self.one("SELECT * FROM files WHERE id = ?", (file_id,)) def latest_version(self, file_id: int) -> dict[str, Any] | None: return self.one( "SELECT * FROM versions WHERE file_id = ? ORDER BY captured_at DESC, id DESC LIMIT 1", (file_id,), ) def version(self, version_id: int) -> dict[str, Any] | None: return self.one( "SELECT v.*, f.path, b.size FROM versions v " "JOIN files f ON f.id = v.file_id JOIN blobs b ON b.sha256 = v.blob_sha256 " "WHERE v.id = ?", (version_id,), ) def version_at(self, file_id: int, as_of: float) -> dict[str, Any] | None: return self.one( "SELECT * FROM versions WHERE file_id = ? AND captured_at <= ? " "ORDER BY captured_at DESC, id DESC LIMIT 1", (file_id, as_of), ) def history(self, file_id: int) -> list[dict[str, Any]]: return self.all( "SELECT v.id, v.captured_at, v.source, v.reason, v.pinned, v.durability, " "v.blob_sha256, b.size FROM versions v JOIN blobs b ON b.sha256 = v.blob_sha256 " "WHERE v.file_id = ? ORDER BY v.captured_at DESC, v.id DESC", (file_id,), ) class _Transaction: def __init__(self, conn: sqlite3.Connection): self.conn = conn def __enter__(self): self.conn.execute("BEGIN IMMEDIATE") return self.conn def __exit__(self, exc_type, exc, tb): self.conn.execute("ROLLBACK" if exc_type else "COMMIT") return False