Skip to main content

agentdb/
db.rs

1use crate::audit::AuditStore;
2use crate::context::ContextStore;
3use crate::conversations::ConversationStore;
4use crate::error::Result;
5use crate::fts::FullTextStore;
6use crate::hybrid::{HybridQuery, HybridResult, HybridStore, TriModalQuery, TriModalResult};
7use crate::labels::LabelStore;
8use crate::memory::MemoryGraph;
9use crate::prompts::PromptStore;
10use crate::schema;
11use crate::tools::ToolStore;
12use crate::traces::TraceStore;
13use crate::vectors::VectorStore;
14use crate::workflows::WorkflowStore;
15use rusqlite::Connection;
16use std::sync::atomic::{AtomicBool, Ordering};
17use std::sync::{Arc, Mutex};
18
19/// The main AgentDB connection — your single-file AI database.
20#[derive(Clone)]
21pub struct AgentDB {
22    conn: Arc<Mutex<Connection>>,
23    closed: Arc<AtomicBool>,
24}
25
26impl AgentDB {
27    /// Open or create an AgentDB database. Use `":memory:"` for tests.
28    pub fn open(path: &str) -> Result<Self> {
29        let conn = Connection::open(path)?;
30        schema::bootstrap(&conn)?;
31        schema::check_version(&conn)?;
32        Ok(Self {
33            conn: Arc::new(Mutex::new(conn)),
34            closed: Arc::new(AtomicBool::new(false)),
35        })
36    }
37
38    /// Access the vector store layer
39    pub fn vectors(&self) -> VectorStore {
40        VectorStore::new(Arc::clone(&self.conn))
41    }
42
43    /// Access the memory graph layer
44    pub fn memory(&self) -> MemoryGraph {
45        MemoryGraph::new(Arc::clone(&self.conn))
46    }
47
48    /// Access the full-text search layer
49    pub fn fts(&self) -> FullTextStore {
50        FullTextStore::new(Arc::clone(&self.conn))
51    }
52
53    /// Access the conversation / message-threading layer
54    pub fn conversations(&self) -> ConversationStore {
55        ConversationStore::new(Arc::clone(&self.conn))
56    }
57
58    /// Access the workflow persistence layer
59    pub fn workflows(&self) -> WorkflowStore {
60        WorkflowStore::new(Arc::clone(&self.conn))
61    }
62
63    /// Access the reasoning-trace layer
64    pub fn traces(&self) -> TraceStore {
65        TraceStore::new(Arc::clone(&self.conn))
66    }
67
68    /// Access the tool registry and call log
69    pub fn tools(&self) -> ToolStore {
70        ToolStore::new(Arc::clone(&self.conn))
71    }
72
73    /// Access the immutable audit log
74    pub fn audit(&self) -> AuditStore {
75        AuditStore::new(Arc::clone(&self.conn))
76    }
77
78    /// Access the token-budgeted context window manager
79    pub fn context(&self) -> ContextStore {
80        ContextStore::new(Arc::clone(&self.conn))
81    }
82
83    /// Access the versioned prompt template store
84    pub fn prompts(&self) -> PromptStore {
85        PromptStore::new(Arc::clone(&self.conn))
86    }
87
88    /// Access the data classification / privacy label store
89    pub fn labels(&self) -> LabelStore {
90        LabelStore::new(Arc::clone(&self.conn))
91    }
92
93    /// Run a tri-modal graph + vector + FTS query
94    pub fn tri_modal_query(&self, q: &TriModalQuery) -> Result<Vec<TriModalResult>> {
95        let dim: usize = {
96            let conn = self.conn.lock().unwrap();
97            conn.query_row(
98                "SELECT dim FROM _adb_collections WHERE name = ?1",
99                rusqlite::params![q.collection],
100                |r| r.get::<_, i64>(0).map(|v| v as usize),
101            )
102            .unwrap_or(q.embedding.len())
103        };
104        let col = self.vectors().collection(&q.collection, dim)?;
105        let store = HybridStore::new(Arc::clone(&self.conn));
106        store.tri_modal_query(q, &col)
107    }
108
109    /// Run a hybrid graph + vector query
110    pub fn hybrid_query(&self, q: HybridQuery) -> Result<Vec<HybridResult>> {
111        let dim: usize = {
112            let conn = self.conn.lock().unwrap();
113            conn.query_row(
114                "SELECT dim FROM _adb_collections WHERE name = ?1",
115                rusqlite::params![q.collection],
116                |r| r.get::<_, i64>(0).map(|v| v as usize),
117            )
118            .unwrap_or(q.embedding.len())
119        };
120        let col = self.vectors().collection(q.collection, dim)?;
121        let store = HybridStore::new(Arc::clone(&self.conn));
122        store.query(q, &col)
123    }
124
125    /// Execute a raw SQL statement
126    pub fn execute(&self, sql: &str) -> Result<usize> {
127        let conn = self.conn.lock().unwrap();
128        Ok(conn.execute(sql, [])?)
129    }
130
131    /// Execute a parameterized SQL statement
132    pub fn execute_params(&self, sql: &str, params: &[&dyn rusqlite::ToSql]) -> Result<usize> {
133        let conn = self.conn.lock().unwrap();
134        Ok(conn.execute(sql, params)?)
135    }
136
137    /// Run multiple operations atomically inside a single SQLite transaction.
138    ///
139    /// The closure receives a [`rusqlite::Transaction`] and may perform any
140    /// number of reads or writes.  If the closure returns `Ok`, the transaction
141    /// is committed; if it returns `Err` (or panics), the transaction is rolled
142    /// back automatically.
143    ///
144    /// # Example
145    /// ```rust,no_run
146    /// # use agentdb::AgentDB;
147    /// let db = AgentDB::open(":memory:").unwrap();
148    /// db.transaction(|tx| {
149    ///     tx.execute("INSERT INTO _adb_nodes (id, kind, data, created_at, updated_at) VALUES ('x','tag','{}',0,0)", [])?;
150    ///     tx.execute("INSERT INTO _adb_nodes (id, kind, data, created_at, updated_at) VALUES ('y','tag','{}',0,0)", [])?;
151    ///     Ok(())
152    /// }).unwrap();
153    /// ```
154    pub fn transaction<F, T>(&self, f: F) -> Result<T>
155    where
156        F: FnOnce(&rusqlite::Transaction) -> Result<T>,
157    {
158        let mut conn = self.conn.lock().unwrap();
159        let tx = conn.transaction()?;
160        let result = f(&tx)?;
161        tx.commit()?;
162        Ok(result)
163    }
164
165    /// Execute one or more semicolon-separated SQL statements as a single
166    /// atomic batch.  This is a convenience wrapper around
167    /// [`execute_batch`](rusqlite::Connection::execute_batch) that wraps the
168    /// statements in an explicit transaction so partial execution is never
169    /// visible to other threads.
170    ///
171    /// # Example
172    /// ```rust,no_run
173    /// # use agentdb::AgentDB;
174    /// let db = AgentDB::open(":memory:").unwrap();
175    /// db.execute_batch(
176    ///     "INSERT INTO _adb_nodes (id,kind,data,created_at,updated_at) VALUES ('a','t','{}',0,0);
177    ///      INSERT INTO _adb_nodes (id,kind,data,created_at,updated_at) VALUES ('b','t','{}',0,0);"
178    /// ).unwrap();
179    /// ```
180    pub fn execute_batch(&self, sql: &str) -> Result<()> {
181        let mut conn = self.conn.lock().unwrap();
182        let tx = conn.transaction()?;
183        tx.execute_batch(sql)?;
184        tx.commit()?;
185        Ok(())
186    }
187
188    /// Query and return rows as JSON values
189    pub fn query_json(&self, sql: &str) -> Result<Vec<serde_json::Value>> {
190        let conn = self.conn.lock().unwrap();
191        let mut stmt = conn.prepare(sql)?;
192        let col_names: Vec<String> = stmt.column_names().iter().map(|s| s.to_string()).collect();
193        let rows = stmt.query_map([], |row| {
194            let mut map = serde_json::Map::new();
195            for (i, name) in col_names.iter().enumerate() {
196                let val: rusqlite::types::Value = row.get(i)?;
197                map.insert(name.clone(), rusqlite_value_to_json(val));
198            }
199            Ok(serde_json::Value::Object(map))
200        })?;
201        rows.map(|r| r.map_err(crate::error::AgentDbError::Sqlite))
202            .collect()
203    }
204
205    /// Query with parameters and return rows as JSON values.
206    ///
207    /// Use this for any query involving user-supplied values to prevent SQL injection.
208    ///
209    /// # Example
210    /// ```rust,no_run
211    /// # use agentdb::AgentDB;
212    /// let db = AgentDB::open(":memory:").unwrap();
213    /// let rows = db.query_json_params(
214    ///     "SELECT * FROM _adb_nodes WHERE kind = ?1",
215    ///     &[&"session" as &dyn rusqlite::ToSql],
216    /// ).unwrap();
217    /// ```
218    pub fn query_json_params(
219        &self,
220        sql: &str,
221        params: &[&dyn rusqlite::ToSql],
222    ) -> Result<Vec<serde_json::Value>> {
223        let conn = self.conn.lock().unwrap();
224        let mut stmt = conn.prepare(sql)?;
225        let col_names: Vec<String> = stmt.column_names().iter().map(|s| s.to_string()).collect();
226        let rows = stmt.query_map(params, |row| {
227            let mut map = serde_json::Map::new();
228            for (i, name) in col_names.iter().enumerate() {
229                let val: rusqlite::types::Value = row.get(i)?;
230                map.insert(name.clone(), rusqlite_value_to_json(val));
231            }
232            Ok(serde_json::Value::Object(map))
233        })?;
234        rows.map(|r| r.map_err(crate::error::AgentDbError::Sqlite))
235            .collect()
236    }
237
238    /// Flush dirty HNSW indexes and close gracefully.
239    ///
240    /// After this returns, the subsequent `Drop` is a no-op (no double flush).
241    pub fn close(self) -> Result<()> {
242        let collections = self.vectors().list_collections()?;
243        for (name, dim, _) in collections {
244            let col = self.vectors().collection(&name, dim)?;
245            let is_dirty: i64 = {
246                let conn = self.conn.lock().unwrap();
247                conn.query_row(
248                    "SELECT COALESCE(
249                        (SELECT is_dirty FROM _adb_hnsw_index
250                         WHERE collection_id =
251                           (SELECT id FROM _adb_collections WHERE name = ?1)
252                        ), 0)",
253                    rusqlite::params![name],
254                    |r| r.get(0),
255                )
256                .unwrap_or(0)
257            };
258            if is_dirty == 1 {
259                col.reindex()?;
260            }
261        }
262        // Checkpoint WAL to consolidate into a single file
263        {
264            let conn = self.conn.lock().unwrap();
265            let _ = conn.pragma_update(None, "wal_checkpoint", "TRUNCATE");
266        }
267        self.closed.store(true, Ordering::Release);
268        Ok(())
269    }
270
271    /// Return database-wide statistics (single-query implementation).
272    pub fn stats(&self) -> Result<DbStats> {
273        let conn = self.conn.lock().unwrap();
274        conn.query_row(
275            "SELECT
276                 (SELECT COUNT(*)                FROM _adb_collections)     AS collections,
277                 (SELECT COALESCE(SUM(count),0)  FROM _adb_collections)     AS vectors,
278                 (SELECT COUNT(*)                FROM _adb_nodes)            AS nodes,
279                 (SELECT COUNT(*)                FROM _adb_edges)            AS edges,
280                 (SELECT COUNT(*)                FROM _adb_conversations)    AS conversations,
281                 (SELECT COUNT(*)                FROM _adb_messages)         AS messages,
282                 (SELECT COUNT(*)                FROM _adb_workflows)        AS workflows,
283                 (SELECT COUNT(*)                FROM _adb_workflow_steps)   AS workflow_steps,
284                 (SELECT COUNT(*)                FROM _adb_traces)           AS traces,
285                 (SELECT COUNT(*)                FROM _adb_tools)            AS tools,
286                 (SELECT COUNT(*)                FROM _adb_tool_calls)       AS tool_calls,
287                 (SELECT COUNT(*)                FROM _adb_audit_log)        AS audit_entries,
288                 (SELECT COUNT(*)                FROM _adb_prompt_templates) AS prompt_templates",
289            [],
290            |r| {
291                Ok(DbStats {
292                    collections: r.get(0)?,
293                    vectors: r.get(1)?,
294                    nodes: r.get(2)?,
295                    edges: r.get(3)?,
296                    conversations: r.get(4)?,
297                    messages: r.get(5)?,
298                    workflows: r.get(6)?,
299                    workflow_steps: r.get(7)?,
300                    traces: r.get(8)?,
301                    tools: r.get(9)?,
302                    tool_calls: r.get(10)?,
303                    audit_entries: r.get(11)?,
304                    prompt_templates: r.get(12)?,
305                })
306            },
307        )
308        .map_err(crate::error::AgentDbError::Sqlite)
309    }
310}
311
312/// Database-wide statistics returned by [`AgentDB::stats`].
313#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
314pub struct DbStats {
315    /// Number of named vector collections.
316    pub collections: i64,
317    /// Total number of vectors across all collections.
318    pub vectors: i64,
319    /// Number of nodes in the memory graph.
320    pub nodes: i64,
321    /// Number of directed edges in the memory graph.
322    pub edges: i64,
323    /// Number of conversation threads.
324    pub conversations: i64,
325    /// Total number of messages across all conversations.
326    pub messages: i64,
327    /// Number of workflow records.
328    pub workflows: i64,
329    /// Total number of workflow steps across all workflows.
330    pub workflow_steps: i64,
331    /// Total number of reasoning trace entries.
332    pub traces: i64,
333    /// Number of registered tool definitions.
334    pub tools: i64,
335    /// Total number of logged tool calls.
336    pub tool_calls: i64,
337    /// Total number of audit log entries.
338    pub audit_entries: i64,
339    /// Total number of prompt template versions.
340    pub prompt_templates: i64,
341}
342
343impl Drop for AgentDB {
344    fn drop(&mut self) {
345        if self.closed.load(Ordering::Acquire) {
346            return;
347        }
348        if let Ok(collections) = self.vectors().list_collections() {
349            for (name, dim, _) in collections {
350                if let Ok(col) = self.vectors().collection(&name, dim) {
351                    let is_dirty: bool = {
352                        let conn = self.conn.lock().unwrap();
353                        conn.query_row(
354                            "SELECT COALESCE(
355                                (SELECT is_dirty FROM _adb_hnsw_index
356                                 WHERE collection_id =
357                                   (SELECT id FROM _adb_collections WHERE name = ?1)
358                                ), 0)",
359                            rusqlite::params![name],
360                            |r| r.get::<_, i64>(0),
361                        )
362                        .unwrap_or(0)
363                            == 1
364                    };
365                    if is_dirty {
366                        let _ = col.reindex();
367                    }
368                }
369            }
370        }
371    }
372}
373
374// ── Internal connection accessor (used by sync module) ───────────────────────
375
376impl AgentDB {
377    /// Returns a clone of the internal `Arc<Mutex<Connection>>`.
378    ///
379    /// This is intentionally `pub(crate)` — only sub-modules of this crate
380    /// that need to share the same SQLite connection (e.g. `sync`) should use
381    /// it.  External callers should use the higher-level store accessors.
382    pub(crate) fn conn_arc(&self) -> Arc<Mutex<Connection>> {
383        Arc::clone(&self.conn)
384    }
385}
386
387// ── WASM-only connection accessor ────────────────────────────────────────────
388
389/// Methods only compiled when the `wasm` feature is active.
390///
391/// These provide low-level access to the underlying `rusqlite::Connection`
392/// required by the OPFS persistence layer (`crate::wasm_opfs`). They are
393/// intentionally not exposed in non-WASM builds to avoid leaking internals.
394#[cfg(feature = "wasm")]
395impl AgentDB {
396    /// Run a closure with exclusive access to the underlying SQLite connection.
397    ///
398    /// Used by `wasm_opfs::sqlite_serialize` and `sqlite_deserialize` which
399    /// need the raw `*mut sqlite3` handle.
400    ///
401    /// The closure **must not** panic while holding the lock.
402    pub(crate) fn with_conn<F, T>(&self, f: F) -> crate::error::Result<T>
403    where
404        F: FnOnce(&rusqlite::Connection) -> crate::error::Result<T>,
405    {
406        let conn = self.conn.lock().unwrap();
407        f(&conn)
408    }
409}
410
411fn rusqlite_value_to_json(val: rusqlite::types::Value) -> serde_json::Value {
412    match val {
413        rusqlite::types::Value::Null => serde_json::Value::Null,
414        rusqlite::types::Value::Integer(i) => serde_json::Value::Number(i.into()),
415        rusqlite::types::Value::Real(f) => serde_json::Number::from_f64(f)
416            .map(serde_json::Value::Number)
417            .unwrap_or(serde_json::Value::Null),
418        rusqlite::types::Value::Text(s) => serde_json::Value::String(s),
419        rusqlite::types::Value::Blob(b) => {
420            serde_json::Value::String(format!("<blob {} bytes>", b.len()))
421        }
422    }
423}