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::atomic::{AtomicBool, Ordering};
12use std::sync::{Arc, Mutex};
13
14#[derive(Clone)]
16pub struct AgentDB {
17 conn: Arc<Mutex<Connection>>,
18 closed: Arc<AtomicBool>,
19}
20
21impl AgentDB {
22 pub fn open(path: &str) -> Result<Self> {
24 let conn = Connection::open(path)?;
25 schema::bootstrap(&conn)?;
26 schema::check_version(&conn)?;
27 Ok(Self {
28 conn: Arc::new(Mutex::new(conn)),
29 closed: Arc::new(AtomicBool::new(false)),
30 })
31 }
32
33 pub fn vectors(&self) -> VectorStore {
35 VectorStore::new(Arc::clone(&self.conn))
36 }
37
38 pub fn memory(&self) -> MemoryGraph {
40 MemoryGraph::new(Arc::clone(&self.conn))
41 }
42
43 pub fn fts(&self) -> FullTextStore {
45 FullTextStore::new(Arc::clone(&self.conn))
46 }
47
48 pub fn conversations(&self) -> ConversationStore {
50 ConversationStore::new(Arc::clone(&self.conn))
51 }
52
53 pub fn workflows(&self) -> WorkflowStore {
55 WorkflowStore::new(Arc::clone(&self.conn))
56 }
57
58 pub fn traces(&self) -> TraceStore {
60 TraceStore::new(Arc::clone(&self.conn))
61 }
62
63 pub fn hybrid_query(&self, q: HybridQuery) -> Result<Vec<HybridResult>> {
65 let dim: usize = {
66 let conn = self.conn.lock().unwrap();
67 conn.query_row(
68 "SELECT dim FROM _adb_collections WHERE name = ?1",
69 rusqlite::params![q.collection],
70 |r| r.get::<_, i64>(0).map(|v| v as usize),
71 )
72 .unwrap_or(q.embedding.len())
73 };
74 let col = self.vectors().collection(q.collection, dim)?;
75 let store = HybridStore::new(Arc::clone(&self.conn));
76 store.query(q, &col)
77 }
78
79 pub fn execute(&self, sql: &str) -> Result<usize> {
81 let conn = self.conn.lock().unwrap();
82 Ok(conn.execute(sql, [])?)
83 }
84
85 pub fn execute_params(&self, sql: &str, params: &[&dyn rusqlite::ToSql]) -> Result<usize> {
87 let conn = self.conn.lock().unwrap();
88 Ok(conn.execute(sql, params)?)
89 }
90
91 pub fn transaction<F, T>(&self, f: F) -> Result<T>
109 where
110 F: FnOnce(&rusqlite::Transaction) -> Result<T>,
111 {
112 let mut conn = self.conn.lock().unwrap();
113 let tx = conn.transaction()?;
114 let result = f(&tx)?;
115 tx.commit()?;
116 Ok(result)
117 }
118
119 pub fn execute_batch(&self, sql: &str) -> Result<()> {
135 let mut conn = self.conn.lock().unwrap();
136 let tx = conn.transaction()?;
137 tx.execute_batch(sql)?;
138 tx.commit()?;
139 Ok(())
140 }
141
142 pub fn query_json(&self, sql: &str) -> Result<Vec<serde_json::Value>> {
144 let conn = self.conn.lock().unwrap();
145 let mut stmt = conn.prepare(sql)?;
146 let col_names: Vec<String> = stmt.column_names().iter().map(|s| s.to_string()).collect();
147 let rows = stmt.query_map([], |row| {
148 let mut map = serde_json::Map::new();
149 for (i, name) in col_names.iter().enumerate() {
150 let val: rusqlite::types::Value = row.get(i)?;
151 map.insert(name.clone(), rusqlite_value_to_json(val));
152 }
153 Ok(serde_json::Value::Object(map))
154 })?;
155 rows.map(|r| r.map_err(crate::error::AgentDbError::Sqlite))
156 .collect()
157 }
158
159 pub fn query_json_params(
173 &self,
174 sql: &str,
175 params: &[&dyn rusqlite::ToSql],
176 ) -> Result<Vec<serde_json::Value>> {
177 let conn = self.conn.lock().unwrap();
178 let mut stmt = conn.prepare(sql)?;
179 let col_names: Vec<String> = stmt.column_names().iter().map(|s| s.to_string()).collect();
180 let rows = stmt.query_map(params, |row| {
181 let mut map = serde_json::Map::new();
182 for (i, name) in col_names.iter().enumerate() {
183 let val: rusqlite::types::Value = row.get(i)?;
184 map.insert(name.clone(), rusqlite_value_to_json(val));
185 }
186 Ok(serde_json::Value::Object(map))
187 })?;
188 rows.map(|r| r.map_err(crate::error::AgentDbError::Sqlite))
189 .collect()
190 }
191
192 pub fn close(self) -> Result<()> {
196 let collections = self.vectors().list_collections()?;
197 for (name, dim, _) in collections {
198 let col = self.vectors().collection(&name, dim)?;
199 let is_dirty: i64 = {
200 let conn = self.conn.lock().unwrap();
201 conn.query_row(
202 "SELECT COALESCE(
203 (SELECT is_dirty FROM _adb_hnsw_index
204 WHERE collection_id =
205 (SELECT id FROM _adb_collections WHERE name = ?1)
206 ), 0)",
207 rusqlite::params![name],
208 |r| r.get(0),
209 )
210 .unwrap_or(0)
211 };
212 if is_dirty == 1 {
213 col.reindex()?;
214 }
215 }
216 self.closed.store(true, Ordering::Release);
217 Ok(())
218 }
219
220 pub fn stats(&self) -> Result<DbStats> {
222 let conn = self.conn.lock().unwrap();
223 conn.query_row(
224 "SELECT
225 (SELECT COUNT(*) FROM _adb_collections) AS collections,
226 (SELECT COALESCE(SUM(count),0) FROM _adb_collections) AS vectors,
227 (SELECT COUNT(*) FROM _adb_nodes) AS nodes,
228 (SELECT COUNT(*) FROM _adb_edges) AS edges,
229 (SELECT COUNT(*) FROM _adb_conversations) AS conversations,
230 (SELECT COUNT(*) FROM _adb_messages) AS messages,
231 (SELECT COUNT(*) FROM _adb_workflows) AS workflows,
232 (SELECT COUNT(*) FROM _adb_workflow_steps) AS workflow_steps,
233 (SELECT COUNT(*) FROM _adb_traces) AS traces",
234 [],
235 |r| {
236 Ok(DbStats {
237 collections: r.get(0)?,
238 vectors: r.get(1)?,
239 nodes: r.get(2)?,
240 edges: r.get(3)?,
241 conversations: r.get(4)?,
242 messages: r.get(5)?,
243 workflows: r.get(6)?,
244 workflow_steps: r.get(7)?,
245 traces: r.get(8)?,
246 })
247 },
248 )
249 .map_err(crate::error::AgentDbError::Sqlite)
250 }
251}
252
253#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
255pub struct DbStats {
256 pub collections: i64,
258 pub vectors: i64,
260 pub nodes: i64,
262 pub edges: i64,
264 pub conversations: i64,
266 pub messages: i64,
268 pub workflows: i64,
270 pub workflow_steps: i64,
272 pub traces: i64,
274}
275
276impl Drop for AgentDB {
277 fn drop(&mut self) {
278 if self.closed.load(Ordering::Acquire) {
279 return;
280 }
281 if let Ok(collections) = self.vectors().list_collections() {
282 for (name, dim, _) in collections {
283 if let Ok(col) = self.vectors().collection(&name, dim) {
284 let is_dirty: bool = {
285 let conn = self.conn.lock().unwrap();
286 conn.query_row(
287 "SELECT COALESCE(
288 (SELECT is_dirty FROM _adb_hnsw_index
289 WHERE collection_id =
290 (SELECT id FROM _adb_collections WHERE name = ?1)
291 ), 0)",
292 rusqlite::params![name],
293 |r| r.get::<_, i64>(0),
294 )
295 .unwrap_or(0) == 1
296 };
297 if is_dirty {
298 let _ = col.reindex();
299 }
300 }
301 }
302 }
303 }
304}
305
306fn rusqlite_value_to_json(val: rusqlite::types::Value) -> serde_json::Value {
307 match val {
308 rusqlite::types::Value::Null => serde_json::Value::Null,
309 rusqlite::types::Value::Integer(i) => serde_json::Value::Number(i.into()),
310 rusqlite::types::Value::Real(f) => serde_json::Number::from_f64(f)
311 .map(serde_json::Value::Number)
312 .unwrap_or(serde_json::Value::Null),
313 rusqlite::types::Value::Text(s) => serde_json::Value::String(s),
314 rusqlite::types::Value::Blob(b) => {
315 serde_json::Value::String(format!("<blob {} bytes>", b.len()))
316 }
317 }
318}