Skip to main content

agentdb/
db.rs

1use crate::conversations::ConversationStore;
2use crate::error::Result;
3use crate::fts::FullTextStore;
4use crate::hybrid::{HybridQuery, HybridResult, HybridStore};
5use crate::memory::MemoryGraph;
6use crate::schema;
7use crate::traces::TraceStore;
8use crate::vectors::VectorStore;
9use crate::workflows::WorkflowStore;
10use rusqlite::Connection;
11use std::sync::{Arc, Mutex};
12
13/// The main AgentDB connection — your single-file AI database.
14pub struct AgentDB {
15    conn: Arc<Mutex<Connection>>,
16}
17
18impl AgentDB {
19    /// Open or create an AgentDB database. Use `":memory:"` for tests.
20    pub fn open(path: &str) -> Result<Self> {
21        let conn = Connection::open(path)?;
22        schema::bootstrap(&conn)?;
23        schema::check_version(&conn)?;
24        Ok(Self {
25            conn: Arc::new(Mutex::new(conn)),
26        })
27    }
28
29    /// Access the vector store layer
30    pub fn vectors(&self) -> VectorStore {
31        VectorStore::new(Arc::clone(&self.conn))
32    }
33
34    /// Access the memory graph layer
35    pub fn memory(&self) -> MemoryGraph {
36        MemoryGraph::new(Arc::clone(&self.conn))
37    }
38
39    /// Access the full-text search layer
40    pub fn fts(&self) -> FullTextStore {
41        FullTextStore::new(Arc::clone(&self.conn))
42    }
43
44    /// Access the conversation / message-threading layer
45    pub fn conversations(&self) -> ConversationStore {
46        ConversationStore::new(Arc::clone(&self.conn))
47    }
48
49    /// Access the workflow persistence layer
50    pub fn workflows(&self) -> WorkflowStore {
51        WorkflowStore::new(Arc::clone(&self.conn))
52    }
53
54    /// Access the reasoning-trace layer
55    pub fn traces(&self) -> TraceStore {
56        TraceStore::new(Arc::clone(&self.conn))
57    }
58
59    /// Run a hybrid graph + vector query
60    pub fn hybrid_query(&self, q: HybridQuery) -> Result<Vec<HybridResult>> {
61        let dim: usize = {
62            let conn = self.conn.lock().unwrap();
63            conn.query_row(
64                "SELECT dim FROM _adb_collections WHERE name = ?1",
65                rusqlite::params![q.collection],
66                |r| r.get(0),
67            )
68            .unwrap_or(q.embedding.len())
69        };
70        let col = self.vectors().collection(q.collection, dim)?;
71        let store = HybridStore::new(Arc::clone(&self.conn));
72        store.query(q, &col)
73    }
74
75    /// Execute a raw SQL statement
76    pub fn execute(&self, sql: &str) -> Result<usize> {
77        let conn = self.conn.lock().unwrap();
78        Ok(conn.execute(sql, [])?)
79    }
80
81    /// Execute a parameterized SQL statement
82    pub fn execute_params(&self, sql: &str, params: &[&dyn rusqlite::ToSql]) -> Result<usize> {
83        let conn = self.conn.lock().unwrap();
84        Ok(conn.execute(sql, params)?)
85    }
86
87    /// Run multiple operations atomically inside a single SQLite transaction.
88    ///
89    /// The closure receives a [`rusqlite::Transaction`] and may perform any
90    /// number of reads or writes.  If the closure returns `Ok`, the transaction
91    /// is committed; if it returns `Err` (or panics), the transaction is rolled
92    /// back automatically.
93    ///
94    /// # Example
95    /// ```rust,no_run
96    /// # use agentdb::AgentDB;
97    /// let db = AgentDB::open(":memory:").unwrap();
98    /// db.transaction(|tx| {
99    ///     tx.execute("INSERT INTO _adb_nodes (id, label, data) VALUES ('x','tag','{}')", [])?;
100    ///     tx.execute("INSERT INTO _adb_nodes (id, label, data) VALUES ('y','tag','{}')", [])?;
101    ///     Ok(())
102    /// }).unwrap();
103    /// ```
104    pub fn transaction<F, T>(&self, f: F) -> Result<T>
105    where
106        F: FnOnce(&rusqlite::Transaction) -> Result<T>,
107    {
108        let mut conn = self.conn.lock().unwrap();
109        let tx = conn.transaction()?;
110        let result = f(&tx)?;
111        tx.commit()?;
112        Ok(result)
113    }
114
115    /// Execute one or more semicolon-separated SQL statements as a single
116    /// atomic batch.  This is a convenience wrapper around
117    /// [`execute_batch`](rusqlite::Connection::execute_batch) that wraps the
118    /// statements in an explicit transaction so partial execution is never
119    /// visible to other threads.
120    ///
121    /// # Example
122    /// ```rust,no_run
123    /// # use agentdb::AgentDB;
124    /// let db = AgentDB::open(":memory:").unwrap();
125    /// db.execute_batch(
126    ///     "INSERT INTO _adb_nodes (id,label,data) VALUES ('a','t','{}');
127    ///      INSERT INTO _adb_nodes (id,label,data) VALUES ('b','t','{}');"
128    /// ).unwrap();
129    /// ```
130    pub fn execute_batch(&self, sql: &str) -> Result<()> {
131        let mut conn = self.conn.lock().unwrap();
132        let tx = conn.transaction()?;
133        tx.execute_batch(sql)?;
134        tx.commit()?;
135        Ok(())
136    }
137
138    /// Query and return rows as JSON values
139    pub fn query_json(&self, sql: &str) -> Result<Vec<serde_json::Value>> {
140        let conn = self.conn.lock().unwrap();
141        let mut stmt = conn.prepare(sql)?;
142        let col_names: Vec<String> = stmt.column_names().iter().map(|s| s.to_string()).collect();
143        let rows = stmt.query_map([], |row| {
144            let mut map = serde_json::Map::new();
145            for (i, name) in col_names.iter().enumerate() {
146                let val: rusqlite::types::Value = row.get(i)?;
147                map.insert(name.clone(), rusqlite_value_to_json(val));
148            }
149            Ok(serde_json::Value::Object(map))
150        })?;
151        rows.map(|r| r.map_err(crate::error::AgentDbError::Sqlite))
152            .collect()
153    }
154
155    /// Flush dirty HNSW indexes and close gracefully
156    pub fn close(self) -> Result<()> {
157        let collections = self.vectors().list_collections()?;
158        for (name, dim, _) in collections {
159            let col = self.vectors().collection(&name, dim)?;
160            let is_dirty: i64 = {
161                let conn = self.conn.lock().unwrap();
162                conn.query_row(
163                    "SELECT COALESCE(
164                        (SELECT is_dirty FROM _adb_hnsw_index
165                         WHERE collection_id =
166                           (SELECT id FROM _adb_collections WHERE name = ?1)
167                        ), 0)",
168                    rusqlite::params![name],
169                    |r| r.get(0),
170                )
171                .unwrap_or(0)
172            };
173            if is_dirty == 1 {
174                col.reindex()?;
175            }
176        }
177        Ok(())
178    }
179
180    /// Return database-wide statistics
181    pub fn stats(&self) -> Result<DbStats> {
182        let conn = self.conn.lock().unwrap();
183        let collections: i64 =
184            conn.query_row("SELECT COUNT(*) FROM _adb_collections", [], |r| r.get(0))?;
185        let vectors: i64 = conn.query_row(
186            "SELECT COALESCE(SUM(count), 0) FROM _adb_collections",
187            [],
188            |r| r.get(0),
189        )?;
190        let nodes: i64 = conn.query_row("SELECT COUNT(*) FROM _adb_nodes", [], |r| r.get(0))?;
191        let edges: i64 = conn.query_row("SELECT COUNT(*) FROM _adb_edges", [], |r| r.get(0))?;
192        Ok(DbStats {
193            collections,
194            vectors,
195            nodes,
196            edges,
197        })
198    }
199}
200
201/// Database-wide statistics returned by [`AgentDB::stats`].
202#[derive(Debug)]
203pub struct DbStats {
204    /// Number of named vector collections.
205    pub collections: i64,
206    /// Total number of vectors across all collections.
207    pub vectors: i64,
208    /// Number of nodes in the memory graph.
209    pub nodes: i64,
210    /// Number of directed edges in the memory graph.
211    pub edges: i64,
212}
213
214fn rusqlite_value_to_json(val: rusqlite::types::Value) -> serde_json::Value {
215    match val {
216        rusqlite::types::Value::Null => serde_json::Value::Null,
217        rusqlite::types::Value::Integer(i) => serde_json::Value::Number(i.into()),
218        rusqlite::types::Value::Real(f) => serde_json::Number::from_f64(f)
219            .map(serde_json::Value::Number)
220            .unwrap_or(serde_json::Value::Null),
221        rusqlite::types::Value::Text(s) => serde_json::Value::String(s),
222        rusqlite::types::Value::Blob(b) => {
223            serde_json::Value::String(format!("<blob {} bytes>", b.len()))
224        }
225    }
226}