1use crate::error::{AgentDbError, Result};
2use crate::schema::now_ms;
3use rusqlite::params;
4use rusqlite::Connection;
5use serde_json::Value;
6use std::sync::{Arc, Mutex};
7
8#[derive(Debug, Clone)]
10pub struct Node {
11 pub id: String,
13 pub kind: String,
15 pub data: Option<Value>,
17 pub created_at: i64,
19 pub updated_at: i64,
21}
22
23#[derive(Debug, Clone)]
25pub struct Edge {
26 pub src: String,
28 pub dst: String,
30 pub relation: String,
32 pub weight: f64,
34 pub created_at: i64,
36}
37
38#[derive(Debug, Clone, Default)]
40pub struct TraversalOptions {
41 pub relation: Option<String>,
43 pub max_depth: usize,
45 pub min_weight: Option<f64>,
47}
48
49#[derive(Debug, Clone)]
51pub struct TraversalResult {
52 pub node: Node,
54 pub depth: usize,
56 pub weight: f64,
58}
59
60pub struct MemoryGraph {
62 conn: Arc<Mutex<Connection>>,
63}
64
65impl MemoryGraph {
66 pub(crate) fn new(conn: Arc<Mutex<Connection>>) -> Self {
67 Self { conn }
68 }
69
70 pub fn add_node(&self, id: &str, kind: &str, data: Option<Value>) -> Result<()> {
73 let conn = self.conn.lock().unwrap();
74 let data_str = data.as_ref().map(|d| d.to_string());
75 let now = now_ms();
76 conn.execute(
77 "INSERT INTO _adb_nodes (id, kind, data, created_at, updated_at)
78 VALUES (?1, ?2, ?3, ?4, ?5)
79 ON CONFLICT(id) DO UPDATE SET
80 kind = excluded.kind,
81 data = excluded.data,
82 updated_at = excluded.updated_at",
83 params![id, kind, data_str, now, now],
84 )?;
85 Ok(())
86 }
87
88 pub fn get_node(&self, id: &str) -> Result<Node> {
92 let conn = self.conn.lock().unwrap();
93 conn.query_row(
94 "SELECT id, kind, data, created_at, updated_at FROM _adb_nodes WHERE id = ?1",
95 params![id],
96 |row| {
97 let data_str: Option<String> = row.get(2)?;
98 Ok(Node {
99 id: row.get(0)?,
100 kind: row.get(1)?,
101 data: data_str
102 .as_deref()
103 .and_then(|s| serde_json::from_str(s).ok()),
104 created_at: row.get(3)?,
105 updated_at: row.get(4)?,
106 })
107 },
108 )
109 .map_err(|_| AgentDbError::NodeNotFound(id.to_string()))
110 }
111
112 pub fn delete_node(&self, id: &str) -> Result<()> {
114 let conn = self.conn.lock().unwrap();
115 conn.execute("DELETE FROM _adb_nodes WHERE id = ?1", params![id])?;
116 Ok(())
117 }
118
119 pub fn add_edge(&self, src: &str, dst: &str, relation: &str, weight: f64) -> Result<()> {
124 self.get_node(src)?;
125 self.get_node(dst)?;
126 let conn = self.conn.lock().unwrap();
127 conn.execute(
128 "INSERT INTO _adb_edges (src, dst, relation, weight, created_at)
129 VALUES (?1, ?2, ?3, ?4, ?5)
130 ON CONFLICT(src, dst, relation) DO UPDATE SET
131 weight = excluded.weight,
132 created_at = excluded.created_at",
133 params![src, dst, relation, weight, now_ms()],
134 )?;
135 Ok(())
136 }
137
138 pub fn delete_edge(&self, src: &str, dst: &str, relation: &str) -> Result<()> {
142 let conn = self.conn.lock().unwrap();
143 let changed = conn.execute(
144 "DELETE FROM _adb_edges WHERE src = ?1 AND dst = ?2 AND relation = ?3",
145 params![src, dst, relation],
146 )?;
147 if changed == 0 {
148 return Err(AgentDbError::EdgeNotFound {
149 src: src.to_string(),
150 dst: dst.to_string(),
151 });
152 }
153 Ok(())
154 }
155
156 pub fn neighbors(&self, node_id: &str, opts: TraversalOptions) -> Result<Vec<TraversalResult>> {
162 let conn = self.conn.lock().unwrap();
163 let max_depth = opts.max_depth.max(1) as i64;
164 let min_weight = opts.min_weight.unwrap_or(0.0);
165
166 let results = if let Some(ref relation) = opts.relation {
167 let sql = "
168 WITH RECURSIVE traverse(node_id, depth, weight) AS (
169 SELECT dst, 1, weight
170 FROM _adb_edges
171 WHERE src = ?1 AND relation = ?2 AND weight >= ?4
172 UNION ALL
173 SELECT e.dst, t.depth + 1, e.weight
174 FROM _adb_edges e
175 JOIN traverse t ON e.src = t.node_id
176 WHERE t.depth < ?3
177 AND e.relation = ?2
178 AND e.weight >= ?4
179 )
180 SELECT DISTINCT n.id, n.kind, n.data, n.created_at, n.updated_at,
181 t.depth, t.weight
182 FROM traverse t
183 JOIN _adb_nodes n ON n.id = t.node_id
184 ORDER BY t.depth ASC, t.weight DESC
185 ";
186 let mut stmt = conn.prepare(sql)?;
187 let rows =
188 stmt.query_map(params![node_id, relation, max_depth, min_weight], parse_row)?;
189 rows.map(|r| r.map_err(AgentDbError::Sqlite))
190 .collect::<Result<Vec<_>>>()?
191 } else {
192 let sql = "
193 WITH RECURSIVE traverse(node_id, depth, weight) AS (
194 SELECT dst, 1, weight
195 FROM _adb_edges
196 WHERE src = ?1 AND weight >= ?3
197 UNION ALL
198 SELECT e.dst, t.depth + 1, e.weight
199 FROM _adb_edges e
200 JOIN traverse t ON e.src = t.node_id
201 WHERE t.depth < ?2
202 AND e.weight >= ?3
203 )
204 SELECT DISTINCT n.id, n.kind, n.data, n.created_at, n.updated_at,
205 t.depth, t.weight
206 FROM traverse t
207 JOIN _adb_nodes n ON n.id = t.node_id
208 ORDER BY t.depth ASC, t.weight DESC
209 ";
210 let mut stmt = conn.prepare(sql)?;
211 let rows = stmt.query_map(params![node_id, max_depth, min_weight], parse_row)?;
212 rows.map(|r| r.map_err(AgentDbError::Sqlite))
213 .collect::<Result<Vec<_>>>()?
214 };
215
216 Ok(results)
217 }
218
219 pub fn nodes_by_kind(&self, kind: &str) -> Result<Vec<Node>> {
221 let conn = self.conn.lock().unwrap();
222 let mut stmt = conn.prepare(
223 "SELECT id, kind, data, created_at, updated_at FROM _adb_nodes WHERE kind = ?1",
224 )?;
225 let rows = stmt.query_map(params![kind], |row| {
226 let data_str: Option<String> = row.get(2)?;
227 Ok(Node {
228 id: row.get(0)?,
229 kind: row.get(1)?,
230 data: data_str
231 .as_deref()
232 .and_then(|s| serde_json::from_str(s).ok()),
233 created_at: row.get(3)?,
234 updated_at: row.get(4)?,
235 })
236 })?;
237 rows.map(|r| r.map_err(AgentDbError::Sqlite)).collect()
238 }
239
240 pub fn stats(&self) -> Result<(i64, i64)> {
242 let conn = self.conn.lock().unwrap();
243 let nodes: i64 = conn.query_row("SELECT COUNT(*) FROM _adb_nodes", [], |r| r.get(0))?;
244 let edges: i64 = conn.query_row("SELECT COUNT(*) FROM _adb_edges", [], |r| r.get(0))?;
245 Ok((nodes, edges))
246 }
247}
248
249fn parse_row(row: &rusqlite::Row) -> rusqlite::Result<TraversalResult> {
250 let data_str: Option<String> = row.get(2)?;
251 Ok(TraversalResult {
252 node: Node {
253 id: row.get(0)?,
254 kind: row.get(1)?,
255 data: data_str
256 .as_deref()
257 .and_then(|s| serde_json::from_str(s).ok()),
258 created_at: row.get(3)?,
259 updated_at: row.get(4)?,
260 },
261 depth: row.get::<_, i64>(5)? as usize,
262 weight: row.get(6)?,
263 })
264}