|
| 1 | +//! SQLite schema for the hippocampal memory store. |
| 2 | +//! |
| 3 | +//! Three core tables: |
| 4 | +//! |
| 5 | +//! - **`entities`**: A "thing" the model might need to remember — a file path, |
| 6 | +//! an issue number, a PR, a person, a concept, a decision. |
| 7 | +//! - **`relations`**: A directed edge connecting two entities. The `kind` field |
| 8 | +//! says what the relationship means (e.g. `"fixes"`, `"part_of"`, `"depends_on"`). |
| 9 | +//! - **`facts`**: A standalone statement about something the model learned. May |
| 10 | +//! reference an entity via `entity_id`. |
| 11 | +
|
| 12 | +use rusqlite::Connection; |
| 13 | + |
| 14 | +/// Create all tables if they don't exist. |
| 15 | +pub(crate) fn migrate(conn: &Connection) -> rusqlite::Result<()> { |
| 16 | + conn.execute_batch( |
| 17 | + " |
| 18 | + CREATE TABLE IF NOT EXISTS entities ( |
| 19 | + id TEXT PRIMARY KEY, |
| 20 | + kind TEXT NOT NULL, -- 'file', 'issue', 'pr', 'concept', 'decision', 'person', 'config' |
| 21 | + name TEXT NOT NULL, -- human-readable label |
| 22 | + description TEXT NOT NULL DEFAULT '', |
| 23 | + created_at TEXT NOT NULL DEFAULT (datetime('now')), |
| 24 | + updated_at TEXT NOT NULL DEFAULT (datetime('now')) |
| 25 | + ); |
| 26 | +
|
| 27 | + CREATE INDEX IF NOT EXISTS idx_entities_kind ON entities(kind); |
| 28 | + CREATE INDEX IF NOT EXISTS idx_entities_name ON entities(name); |
| 29 | +
|
| 30 | + CREATE TABLE IF NOT EXISTS relations ( |
| 31 | + id TEXT PRIMARY KEY, |
| 32 | + source_id TEXT NOT NULL REFERENCES entities(id) ON DELETE CASCADE, |
| 33 | + target_id TEXT NOT NULL REFERENCES entities(id) ON DELETE CASCADE, |
| 34 | + kind TEXT NOT NULL, -- 'fixes', 'part_of', 'depends_on', 'contains', 'references', 'implements' |
| 35 | + strength REAL NOT NULL DEFAULT 1.0, -- 0.0–1.0 confidence/importance |
| 36 | + created_at TEXT NOT NULL DEFAULT (datetime('now')), |
| 37 | + session_id TEXT, -- which session created this relation |
| 38 | + UNIQUE(source_id, target_id, kind) |
| 39 | + ); |
| 40 | +
|
| 41 | + CREATE INDEX IF NOT EXISTS idx_relations_source ON relations(source_id); |
| 42 | + CREATE INDEX IF NOT EXISTS idx_relations_target ON relations(target_id); |
| 43 | + CREATE INDEX IF NOT EXISTS idx_relations_kind ON relations(kind); |
| 44 | +
|
| 45 | + CREATE TABLE IF NOT EXISTS facts ( |
| 46 | + id TEXT PRIMARY KEY, |
| 47 | + entity_id TEXT REFERENCES entities(id) ON DELETE SET NULL, |
| 48 | + content TEXT NOT NULL, -- the factual statement |
| 49 | + source TEXT NOT NULL DEFAULT '', -- where this fact came from (tool call, session, user) |
| 50 | + importance REAL NOT NULL DEFAULT 0.5, -- 0.0–1.0 |
| 51 | + created_at TEXT NOT NULL DEFAULT (datetime('now')), |
| 52 | + session_id TEXT |
| 53 | + ); |
| 54 | +
|
| 55 | + CREATE INDEX IF NOT EXISTS idx_facts_entity ON facts(entity_id); |
| 56 | + CREATE INDEX IF NOT EXISTS idx_facts_importance ON facts(importance DESC); |
| 57 | +
|
| 58 | + -- Full-text search over facts (enables pattern-completion-like queries) |
| 59 | + CREATE VIRTUAL TABLE IF NOT EXISTS facts_fts USING fts5( |
| 60 | + content, |
| 61 | + content=facts, |
| 62 | + content_rowid=rowid |
| 63 | + ); |
| 64 | +
|
| 65 | + -- Triggers to keep FTS index in sync |
| 66 | + CREATE TRIGGER IF NOT EXISTS facts_ai AFTER INSERT ON facts BEGIN |
| 67 | + INSERT INTO facts_fts(rowid, content) VALUES (new.rowid, new.content); |
| 68 | + END; |
| 69 | +
|
| 70 | + CREATE TRIGGER IF NOT EXISTS facts_ad AFTER DELETE ON facts BEGIN |
| 71 | + INSERT INTO facts_fts(facts_fts, rowid, content) VALUES('delete', old.rowid, old.content); |
| 72 | + END; |
| 73 | +
|
| 74 | + CREATE TRIGGER IF NOT EXISTS facts_au AFTER UPDATE ON facts BEGIN |
| 75 | + INSERT INTO facts_fts(facts_fts, rowid, content) VALUES('delete', old.rowid, old.content); |
| 76 | + INSERT INTO facts_fts(rowid, content) VALUES (new.rowid, new.content); |
| 77 | + END; |
| 78 | + ", |
| 79 | + ) |
| 80 | +} |
0 commit comments