Files
versioning/src/versiond/db.py
T
retoorandClaude Opus 5.5 ddf09bea88 Implement versiond M1: inotify monitor, versioning, diff, restore, systemd install
FastAPI service on 127.0.0.1:9922 that monitors user-chosen directories
with pruned inotify watches, stores versions in a local content-addressed
spool indexed in SQLite, coalesces bursts (first + last), and offers
history, diff and dry-run-first bulk restore. Includes a CLI with a
systemd user unit installer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 18:46:12 +02:00

187 lines
6.2 KiB
Python

"""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