datacules-agentdb 0.7.0

Single-file embedded database for AI agents. SQL + Vector Search + Full-Text Search + Hybrid Queries + Memory Graphs.
Documentation
use crate::error::{AgentDbError, Result};
use crate::schema::now_ms;
use rusqlite::params;
use rusqlite::Connection;
use serde_json::Value;
use std::sync::{Arc, Mutex};
use uuid::Uuid;

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct AuditEntry {
    pub id: String,
    pub timestamp: i64,
    pub actor: Option<String>,
    pub action: String,
    pub table_name: String,
    pub record_id: String,
    pub old_value: Option<Value>,
    pub new_value: Option<Value>,
    pub reason: Option<String>,
}

pub struct AuditStore {
    conn: Arc<Mutex<Connection>>,
}

impl AuditStore {
    pub(crate) fn new(conn: Arc<Mutex<Connection>>) -> Self {
        Self { conn }
    }

    #[allow(clippy::too_many_arguments)]
    pub fn log(
        &self,
        actor: Option<&str>,
        action: &str,
        table_name: &str,
        record_id: &str,
        old_value: Option<Value>,
        new_value: Option<Value>,
        reason: Option<&str>,
    ) -> Result<String> {
        let id = Uuid::new_v4().to_string();
        let conn = self.conn.lock().unwrap();
        let old_str = old_value.as_ref().map(|v| v.to_string());
        let new_str = new_value.as_ref().map(|v| v.to_string());
        let now = now_ms();
        conn.execute(
            "INSERT INTO _adb_audit_log (id, timestamp, actor, action, table_name, record_id, old_value, new_value, reason)
             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
            params![id, now, actor, action, table_name, record_id, old_str, new_str, reason],
        )?;
        Ok(id)
    }

    pub fn query_by_record(
        &self,
        table_name: &str,
        record_id: &str,
        limit: Option<usize>,
    ) -> Result<Vec<AuditEntry>> {
        let conn = self.conn.lock().unwrap();
        let lim = limit.unwrap_or(100) as i64;
        let mut stmt = conn.prepare(
            "SELECT id, timestamp, actor, action, table_name, record_id, old_value, new_value, reason
             FROM _adb_audit_log
             WHERE table_name = ?1 AND record_id = ?2
             ORDER BY timestamp DESC LIMIT ?3",
        )?;
        let rows = stmt.query_map(params![table_name, record_id, lim], parse_audit_row)?;
        rows.map(|r| r.map_err(AgentDbError::Sqlite)).collect()
    }

    pub fn query_by_actor(&self, actor: &str, limit: Option<usize>) -> Result<Vec<AuditEntry>> {
        let conn = self.conn.lock().unwrap();
        let lim = limit.unwrap_or(100) as i64;
        let mut stmt = conn.prepare(
            "SELECT id, timestamp, actor, action, table_name, record_id, old_value, new_value, reason
             FROM _adb_audit_log
             WHERE actor = ?1
             ORDER BY timestamp DESC LIMIT ?2",
        )?;
        let rows = stmt.query_map(params![actor, lim], parse_audit_row)?;
        rows.map(|r| r.map_err(AgentDbError::Sqlite)).collect()
    }

    pub fn query_recent(&self, limit: Option<usize>) -> Result<Vec<AuditEntry>> {
        let conn = self.conn.lock().unwrap();
        let lim = limit.unwrap_or(100) as i64;
        let mut stmt = conn.prepare(
            "SELECT id, timestamp, actor, action, table_name, record_id, old_value, new_value, reason
             FROM _adb_audit_log
             ORDER BY timestamp DESC LIMIT ?1",
        )?;
        let rows = stmt.query_map(params![lim], parse_audit_row)?;
        rows.map(|r| r.map_err(AgentDbError::Sqlite)).collect()
    }
}

fn parse_audit_row(row: &rusqlite::Row) -> rusqlite::Result<AuditEntry> {
    let old_str: Option<String> = row.get(6)?;
    let new_str: Option<String> = row.get(7)?;
    Ok(AuditEntry {
        id: row.get(0)?,
        timestamp: row.get(1)?,
        actor: row.get(2)?,
        action: row.get(3)?,
        table_name: row.get(4)?,
        record_id: row.get(5)?,
        old_value: old_str.and_then(|s| serde_json::from_str(&s).ok()),
        new_value: new_str.and_then(|s| serde_json::from_str(&s).ok()),
        reason: row.get(8)?,
    })
}