"""SQLite Schema & Migrationen für Tier 2. Enthält alle CREATE TABLE / INDEX / TRIGGER Statements mit Schema-Versionierung. """ import sqlite3 import time from pathlib import Path SCHEMA_VERSION = 1 SCHEMA_SQL = """ -- Schema-Version CREATE TABLE IF NOT EXISTS memory_schema_version ( version INTEGER PRIMARY KEY, applied_at REAL NOT NULL ); -- Fakten CREATE TABLE IF NOT EXISTS facts ( id INTEGER PRIMARY KEY AUTOINCREMENT, uuid TEXT NOT NULL UNIQUE, content TEXT NOT NULL, content_hash TEXT NOT NULL, category TEXT, confidence REAL DEFAULT 1.0, source_type TEXT NOT NULL, source_id TEXT, created_at REAL NOT NULL, updated_at REAL NOT NULL, expires_at REAL, access_count INTEGER DEFAULT 0, last_accessed REAL, is_archived INTEGER DEFAULT 0 ); CREATE INDEX IF NOT EXISTS idx_facts_category ON facts(category); CREATE INDEX IF NOT EXISTS idx_facts_source ON facts(source_type, source_id); CREATE INDEX IF NOT EXISTS idx_facts_created ON facts(created_at DESC); CREATE INDEX IF NOT EXISTS idx_facts_hash ON facts(content_hash); CREATE INDEX IF NOT EXISTS idx_facts_confidence ON facts(confidence DESC); -- Entitäten CREATE TABLE IF NOT EXISTS entities ( id INTEGER PRIMARY KEY AUTOINCREMENT, uuid TEXT NOT NULL UNIQUE, name TEXT NOT NULL, aliases TEXT, entity_type TEXT NOT NULL, description TEXT, first_seen REAL NOT NULL, last_seen REAL NOT NULL, occurrence_count INTEGER DEFAULT 1, metadata TEXT ); CREATE INDEX IF NOT EXISTS idx_entities_name ON entities(name); CREATE INDEX IF NOT EXISTS idx_entities_type ON entities(entity_type); -- Relationen CREATE TABLE IF NOT EXISTS relations ( id INTEGER PRIMARY KEY AUTOINCREMENT, uuid TEXT NOT NULL UNIQUE, from_entity_id TEXT NOT NULL, to_entity_id TEXT NOT NULL, relation_type TEXT NOT NULL, strength REAL DEFAULT 1.0, evidence_fact_id TEXT, created_at REAL NOT NULL, updated_at REAL NOT NULL ); CREATE INDEX IF NOT EXISTS idx_relations_from ON relations(from_entity_id); CREATE INDEX IF NOT EXISTS idx_relations_to ON relations(to_entity_id); CREATE INDEX IF NOT EXISTS idx_relations_type ON relations(relation_type); -- Timeline CREATE TABLE IF NOT EXISTS timeline ( id INTEGER PRIMARY KEY AUTOINCREMENT, uuid TEXT NOT NULL UNIQUE, event_type TEXT NOT NULL, title TEXT NOT NULL, description TEXT, related_entities TEXT, related_facts TEXT, session_id TEXT, timestamp REAL NOT NULL, importance REAL DEFAULT 0.5 ); CREATE INDEX IF NOT EXISTS idx_timeline_time ON timeline(timestamp DESC); CREATE INDEX IF NOT EXISTS idx_timeline_type ON timeline(event_type); -- Audit-Log CREATE TABLE IF NOT EXISTS memory_audit_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp REAL NOT NULL, action TEXT NOT NULL, tier TEXT NOT NULL, actor TEXT NOT NULL, target_uuid TEXT, diff TEXT, success INTEGER DEFAULT 1 ); CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON memory_audit_log(timestamp DESC); CREATE INDEX IF NOT EXISTS idx_audit_actor ON memory_audit_log(actor); -- Embedding-Queue CREATE TABLE IF NOT EXISTS embedding_queue ( id INTEGER PRIMARY KEY AUTOINCREMENT, fact_id TEXT, content TEXT NOT NULL, source_type TEXT NOT NULL, session_id TEXT, message_id INTEGER, queued_at REAL NOT NULL, processed INTEGER DEFAULT 0 ); CREATE INDEX IF NOT EXISTS idx_queue_processed ON embedding_queue(processed, queued_at); """ FTS_SQL = """ CREATE VIRTUAL TABLE IF NOT EXISTS facts_fts USING fts5( content, content_rowid='id', tokenize='unicode61' ); CREATE TRIGGER IF NOT EXISTS facts_fts_insert AFTER INSERT ON facts BEGIN INSERT INTO facts_fts(rowid, content) VALUES (new.id, new.content); END; CREATE TRIGGER IF NOT EXISTS facts_fts_delete AFTER DELETE ON facts BEGIN DELETE FROM facts_fts WHERE rowid = old.id; END; CREATE TRIGGER IF NOT EXISTS facts_fts_update AFTER UPDATE ON facts BEGIN DELETE FROM facts_fts WHERE rowid = old.id; INSERT INTO facts_fts(rowid, content) VALUES (new.id, new.content); END; CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5( name, content_rowid='id', tokenize='unicode61' ); CREATE TRIGGER IF NOT EXISTS entities_fts_insert AFTER INSERT ON entities BEGIN INSERT INTO entities_fts(rowid, name) VALUES (new.id, new.name); END; CREATE TRIGGER IF NOT EXISTS entities_fts_delete AFTER DELETE ON entities BEGIN DELETE FROM entities_fts WHERE rowid = old.id; END; CREATE TRIGGER IF NOT EXISTS entities_fts_update AFTER UPDATE ON entities BEGIN DELETE FROM entities_fts WHERE rowid = old.id; INSERT INTO entities_fts(rowid, name) VALUES (new.id, new.name); END; """ def init_schema(conn: sqlite3.Connection) -> None: """Initialisiert alle Tabellen, Indizes und FTS5.""" conn.executescript(SCHEMA_SQL) conn.executescript(FTS_SQL) conn.execute( "INSERT OR REPLACE INTO memory_schema_version (version, applied_at) VALUES (?, ?)", (SCHEMA_VERSION, time.time()), ) conn.commit() def migrate(conn: sqlite3.Connection) -> None: """Führt Migrationen durch (aktuell: nur Schema-Version prüfen).""" row = conn.execute("SELECT version FROM memory_schema_version ORDER BY version DESC LIMIT 1").fetchone() current = row[0] if row else 0 if current < SCHEMA_VERSION: init_schema(conn) def connect(db_path: Path, wal_mode: bool = True) -> sqlite3.Connection: """Erstellt Verbindung mit WAL-Mode und Foreign Keys.""" db_path.parent.mkdir(parents=True, exist_ok=True) conn = sqlite3.connect(str(db_path), check_same_thread=False, timeout=10.0) conn.row_factory = sqlite3.Row conn.execute("PRAGMA foreign_keys=ON") if wal_mode: try: conn.execute("PRAGMA journal_mode=WAL") except sqlite3.OperationalError: conn.execute("PRAGMA journal_mode=DELETE") return conn