engram-storage 0.2.0

SQLite storage with FTS5
Documentation
use crate::error::StorageError;

pub const CREATE_MEMORIES: &str = r#"
CREATE TABLE IF NOT EXISTS memories (
    id TEXT PRIMARY KEY,
    memory_type TEXT NOT NULL CHECK(memory_type IN ('decision','pattern','bugfix','context','antipattern','insight')),
    context TEXT NOT NULL,
    action TEXT NOT NULL,
    result TEXT NOT NULL,
    score REAL DEFAULT 0.0,
    embedding_context BLOB,
    embedding_action BLOB,
    embedding_result BLOB,
    indexed BOOLEAN DEFAULT FALSE,
    tags TEXT,
    project TEXT,
    parent_id TEXT,
    source_ids TEXT,
    insight_type TEXT CHECK(insight_type IS NULL OR insight_type IN ('cluster','temporal','causal')),
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL,
    used_count INTEGER DEFAULT 0,
    last_used_at TEXT,
    superseded_by TEXT,
    FOREIGN KEY (superseded_by) REFERENCES memories(id),
    FOREIGN KEY (parent_id) REFERENCES memories(id)
)
"#;

pub const CREATE_MEMORIES_FTS: &str = r#"
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
    context, action, result,
    content='memories',
    content_rowid='rowid'
)
"#;

pub const CREATE_FTS_INSERT_TRIGGER: &str = r#"
CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
    INSERT INTO memories_fts(rowid, context, action, result)
    VALUES (new.rowid, new.context, new.action, new.result);
END
"#;

pub const CREATE_FTS_DELETE_TRIGGER: &str = r#"
CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
    INSERT INTO memories_fts(memories_fts, rowid, context, action, result)
    VALUES ('delete', old.rowid, old.context, old.action, old.result);
END
"#;

pub const CREATE_FTS_UPDATE_TRIGGER: &str = r#"
CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
    INSERT INTO memories_fts(memories_fts, rowid, context, action, result)
    VALUES ('delete', old.rowid, old.context, old.action, old.result);
    INSERT INTO memories_fts(rowid, context, action, result)
    VALUES (new.rowid, new.context, new.action, new.result);
END
"#;

pub const CREATE_Q_TABLE: &str = r#"
CREATE TABLE IF NOT EXISTS q_table (
    router_level TEXT NOT NULL,
    state TEXT NOT NULL,
    action TEXT NOT NULL,
    value REAL DEFAULT 0.0,
    update_count INTEGER DEFAULT 0,
    updated_at TEXT NOT NULL,
    PRIMARY KEY (router_level, state, action)
)
"#;

pub const CREATE_CONSOLIDATION_LOG: &str = r#"
CREATE TABLE IF NOT EXISTS consolidation_log (
    id TEXT PRIMARY KEY,
    action TEXT NOT NULL,
    memory_ids TEXT NOT NULL,
    reason TEXT,
    performed_at TEXT NOT NULL,
    performed_by TEXT NOT NULL
)
"#;

pub const CREATE_FEEDBACK_TRACKING: &str = r#"
CREATE TABLE IF NOT EXISTS feedback_tracking (
    memory_id TEXT NOT NULL,
    searched_at TEXT NOT NULL,
    judged BOOLEAN DEFAULT FALSE,
    judged_at TEXT,
    FOREIGN KEY (memory_id) REFERENCES memories(id)
)
"#;

pub const CREATE_RECOMMENDATIONS: &str = r#"
CREATE TABLE IF NOT EXISTS recommendations (
    id TEXT PRIMARY KEY,
    key TEXT NOT NULL,
    current_value TEXT,
    suggested_value TEXT NOT NULL,
    reason TEXT NOT NULL,
    created_at TEXT NOT NULL,
    status TEXT DEFAULT 'pending' CHECK(status IN ('pending','accepted','rejected'))
)
"#;

pub const CREATE_METRICS: &str = r#"
CREATE TABLE IF NOT EXISTS metrics (
    id TEXT PRIMARY KEY,
    name TEXT NOT NULL,
    value REAL NOT NULL,
    period_start TEXT NOT NULL,
    period_end TEXT NOT NULL,
    created_at TEXT NOT NULL
)
"#;

pub const CREATE_SCHEMA_META: &str = r#"
CREATE TABLE IF NOT EXISTS schema_meta (
    key TEXT PRIMARY KEY,
    value TEXT NOT NULL
)
"#;

const SCHEMA_STATEMENTS: &[&str] = &[
    CREATE_MEMORIES,
    CREATE_MEMORIES_FTS,
    CREATE_FTS_INSERT_TRIGGER,
    CREATE_FTS_DELETE_TRIGGER,
    CREATE_FTS_UPDATE_TRIGGER,
    CREATE_Q_TABLE,
    CREATE_CONSOLIDATION_LOG,
    CREATE_FEEDBACK_TRACKING,
    CREATE_RECOMMENDATIONS,
    CREATE_METRICS,
    CREATE_SCHEMA_META,
];

pub fn apply_schema(connection: &rusqlite::Connection) -> Result<(), StorageError> {
    connection.execute_batch("PRAGMA journal_mode = WAL;")?;
    connection.execute_batch("PRAGMA foreign_keys = ON;")?;
    for statement in SCHEMA_STATEMENTS {
        connection.execute_batch(statement)?;
    }
    Ok(())
}