import sqlite3 SCHEMA_VERSION = 1 SCHEMA = """ CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT); CREATE TABLE IF NOT EXISTS assets ( immich_id TEXT PRIMARY KEY, taken_at TEXT, gps_lat REAL, gps_lon REAL, place_city TEXT, place_country TEXT, type TEXT, has_gps INTEGER NOT NULL DEFAULT 0, thumb_path TEXT, processed INTEGER NOT NULL DEFAULT 0, ingested_at TEXT, updated_at TEXT ); CREATE TABLE IF NOT EXISTS tags ( name TEXT PRIMARY KEY, immich_tag_id TEXT, count INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS asset_tags ( immich_id TEXT NOT NULL, tag_name TEXT NOT NULL, PRIMARY KEY (immich_id, tag_name) ); CREATE TABLE IF NOT EXISTS clusters ( id INTEGER PRIMARY KEY AUTOINCREMENT, start_at TEXT, end_at TEXT, count INTEGER NOT NULL DEFAULT 0, suggested_name TEXT, confidence REAL NOT NULL DEFAULT 0, kind_guess TEXT NOT NULL DEFAULT 'trip', status TEXT NOT NULL DEFAULT 'pending', decided_name TEXT, reviewed_at TEXT, notes TEXT ); CREATE TABLE IF NOT EXISTS cluster_members ( cluster_id INTEGER NOT NULL, immich_id TEXT NOT NULL, member_confidence REAL NOT NULL DEFAULT 1.0, is_outlier INTEGER NOT NULL DEFAULT 0, included INTEGER NOT NULL DEFAULT 1, flagged_coverage INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (cluster_id, immich_id) ); CREATE TABLE IF NOT EXISTS writeback_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, immich_id TEXT NOT NULL, action TEXT NOT NULL, tag TEXT, result TEXT NOT NULL, applied_at TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_assets_taken_at ON assets(taken_at); CREATE INDEX IF NOT EXISTS idx_members_cluster ON cluster_members(cluster_id); CREATE INDEX IF NOT EXISTS idx_members_asset ON cluster_members(immich_id); """ class Store: def __init__(self, db_path: str): self.db_path = db_path self._conn = None def connect(self) -> "Store": self._conn = sqlite3.connect(self.db_path) self._conn.row_factory = sqlite3.Row self._conn.execute("PRAGMA foreign_keys=ON") self.migrate() return self @property def conn(self) -> sqlite3.Connection: if self._conn is None: raise RuntimeError("Store not connected; call connect()") return self._conn def migrate(self) -> None: self.conn.executescript(SCHEMA) if self.get_meta("schema_version") is None: self.set_meta("schema_version", str(SCHEMA_VERSION)) self.conn.commit() def set_meta(self, key: str, value: str) -> None: self.conn.execute( "INSERT INTO meta(key, value) VALUES(?, ?) " "ON CONFLICT(key) DO UPDATE SET value=excluded.value", (key, value)) self.conn.commit() def get_meta(self, key: str, default=None): row = self.conn.execute("SELECT value FROM meta WHERE key=?", (key,)).fetchone() return row["value"] if row else default def close(self) -> None: if self._conn is not None: self._conn.close() self._conn = None