1use std::path::Path;
33use std::sync::LazyLock;
34use std::sync::{
35 Arc, Mutex,
36 atomic::{AtomicU64, Ordering},
37};
38
39use rusqlite::{Connection, OpenFlags};
40use tokio::sync::Semaphore;
41use tokio::task;
42
43static MEM_POOL_ID: LazyLock<AtomicU64> = LazyLock::new(|| AtomicU64::new(0));
45
46use crate::error::{KernelError, Result};
47use crate::graph::types::{GraphEdge, GraphNode, GraphStats, ScoredNode};
48
49struct PoolInner {
52 idle: Mutex<Vec<Connection>>,
53 path: String,
54 shared_mem: bool,
57}
58
59impl PoolInner {
60 fn take(&self) -> Result<Connection> {
61 if let Ok(mut guard) = self.idle.lock()
62 && let Some(conn) = guard.pop()
63 {
64 return Ok(conn);
65 }
66 let mut conn = if self.shared_mem {
67 Connection::open_with_flags(
68 &self.path,
69 OpenFlags::SQLITE_OPEN_READ_WRITE
70 | OpenFlags::SQLITE_OPEN_CREATE
71 | OpenFlags::SQLITE_OPEN_URI
72 | OpenFlags::SQLITE_OPEN_NO_MUTEX,
73 )
74 .map_err(|e| KernelError::Store(e.to_string()))?
75 } else {
76 Connection::open(&self.path).map_err(|e| KernelError::Store(e.to_string()))?
77 };
78 apply_concurrency_pragmas(&mut conn)?;
82 Ok(conn)
83 }
84
85 fn return_conn(&self, conn: Connection) {
86 if let Ok(mut guard) = self.idle.lock() {
87 guard.push(conn);
88 }
89 }
91}
92
93fn apply_concurrency_pragmas(conn: &mut Connection) -> Result<()> {
101 conn.execute_batch(
102 "PRAGMA busy_timeout = 5000;\n\
103 PRAGMA synchronous = NORMAL;",
104 )
105 .map_err(|e| KernelError::Store(format!("PRAGMA failed: {e}")))?;
106 Ok(())
107}
108
109#[derive(Clone)]
118pub struct AsyncPoolGraph {
119 inner: Arc<PoolInner>,
120 sem: Arc<Semaphore>,
121}
122
123impl AsyncPoolGraph {
124 pub async fn open(path: impl AsRef<Path>, max_conns: usize) -> Result<Self> {
128 let path_str = path
129 .as_ref()
130 .to_str()
131 .ok_or_else(|| KernelError::Store("invalid path".into()))?
132 .to_string();
133
134 let path_for_open = path_str.clone();
136 let first_conn = task::spawn_blocking(move || -> Result<Connection> {
137 if let Some(parent) = Path::new(&path_for_open).parent() {
138 std::fs::create_dir_all(parent)?;
139 }
140 let mut conn =
141 Connection::open(&path_for_open).map_err(|e| KernelError::Store(e.to_string()))?;
142 crate::graph::schema::init_graph_schema(&conn)?;
143 conn.execute_batch("PRAGMA journal_mode = WAL;")
148 .map_err(|e| KernelError::Store(format!("PRAGMA failed: {e}")))?;
149 apply_concurrency_pragmas(&mut conn)?;
150 Ok(conn)
151 })
152 .await
153 .map_err(|e| KernelError::Store(e.to_string()))??;
154
155 let inner = Arc::new(PoolInner {
156 idle: Mutex::new(vec![first_conn]),
157 path: path_str,
158 shared_mem: false,
159 });
160
161 Ok(Self {
162 inner,
163 sem: Arc::new(Semaphore::new(max_conns.max(1))),
164 })
165 }
166
167 pub async fn open_in_memory(max_conns: usize) -> Result<Self> {
172 let id = MEM_POOL_ID.fetch_add(1, Ordering::Relaxed);
173 let uri = format!("file:llm_kernel_pool_{id}?mode=memory&cache=shared");
174 let uri_clone = uri.clone();
175 let conn = task::spawn_blocking(move || -> Result<Connection> {
176 let mut conn = Connection::open_with_flags(
177 &uri_clone,
178 OpenFlags::SQLITE_OPEN_READ_WRITE
179 | OpenFlags::SQLITE_OPEN_CREATE
180 | OpenFlags::SQLITE_OPEN_URI
181 | OpenFlags::SQLITE_OPEN_NO_MUTEX,
182 )
183 .map_err(|e| KernelError::Store(e.to_string()))?;
184 crate::graph::schema::init_graph_schema(&conn)?;
185 apply_concurrency_pragmas(&mut conn)?;
188 Ok(conn)
189 })
190 .await
191 .map_err(|e| KernelError::Store(e.to_string()))??;
192
193 let inner = Arc::new(PoolInner {
194 idle: Mutex::new(vec![conn]),
195 path: uri,
196 shared_mem: true,
197 });
198
199 Ok(Self {
200 inner,
201 sem: Arc::new(Semaphore::new(max_conns.max(1))),
202 })
203 }
204
205 async fn with_conn<F, T>(&self, f: F) -> Result<T>
207 where
208 F: FnOnce(&Connection) -> Result<T> + Send + 'static,
209 T: Send + 'static,
210 {
211 let _permit = self
212 .sem
213 .acquire()
214 .await
215 .map_err(|_| KernelError::Store("semaphore closed".into()))?;
216
217 let inner = Arc::clone(&self.inner);
218 task::spawn_blocking(move || {
219 let conn = inner.take()?;
220 let result = f(&conn);
221 inner.return_conn(conn);
222 result
223 })
224 .await
225 .map_err(|e| KernelError::Store(e.to_string()))?
226 }
227
228 pub async fn upsert_node(&self, node: GraphNode) -> Result<()> {
232 self.with_conn(move |c| crate::graph::store::upsert_node(c, &node))
233 .await
234 }
235
236 pub async fn read_node(&self, id: impl Into<String>) -> Result<Option<GraphNode>> {
238 let id = id.into();
239 self.with_conn(move |c| crate::graph::store::read_node(c, &id))
240 .await
241 }
242
243 pub async fn read_nodes(&self) -> Result<Vec<GraphNode>> {
245 self.with_conn(|c| crate::graph::store::read_nodes_limited(c, 10_000))
246 .await
247 }
248
249 pub async fn delete_node(&self, id: impl Into<String>) -> Result<bool> {
251 let id = id.into();
252 self.with_conn(move |c| crate::graph::store::delete_node(c, &id))
253 .await
254 }
255
256 pub async fn append_edge(&self, edge: GraphEdge) -> Result<()> {
260 self.with_conn(move |c| crate::graph::store::append_edge(c, &edge))
261 .await
262 }
263
264 pub async fn read_edges(&self) -> Result<Vec<GraphEdge>> {
266 self.with_conn(|c| crate::graph::store::read_edges(c, 10_000))
267 .await
268 }
269
270 pub async fn delete_edge(&self, id: impl Into<String>) -> Result<bool> {
272 let id = id.into();
273 self.with_conn(move |c| crate::graph::store::delete_edge(c, &id))
274 .await
275 }
276
277 pub async fn search_nodes(
281 &self,
282 query: impl Into<String>,
283 limit: usize,
284 ) -> Result<Vec<GraphNode>> {
285 let query = query.into();
286 self.with_conn(move |c| crate::graph::search::search_nodes(c, &query, limit))
287 .await
288 }
289
290 pub async fn smart_recall(
292 &self,
293 project: Option<String>,
294 hint: Option<String>,
295 limit: usize,
296 ) -> Result<Vec<ScoredNode>> {
297 self.with_conn(move |c| {
298 crate::graph::recall::smart_recall(c, project.as_deref(), hint.as_deref(), limit)
299 })
300 .await
301 }
302
303 pub async fn stats(&self) -> Result<GraphStats> {
307 self.with_conn(crate::graph::lifecycle::compute_stats).await
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314
315 fn node(id: &str) -> GraphNode {
316 GraphNode {
317 id: id.into(),
318 node_type: "concept".into(),
319 title: format!("Node {id}"),
320 body: "body".into(),
321 tags: vec![],
322 projects: vec![],
323 agents: vec![],
324 created: "2026-01-01T00:00:00Z".into(),
325 updated: "2026-01-01T00:00:00Z".into(),
326 importance: 0.5,
327 access_count: 0,
328 accessed_at: String::new(),
329 }
330 }
331
332 async fn mem() -> AsyncPoolGraph {
333 AsyncPoolGraph::open_in_memory(2).await.unwrap()
334 }
335
336 #[tokio::test]
337 async fn upsert_and_read_node() {
338 let pool = mem().await;
339 pool.upsert_node(node("n1")).await.unwrap();
340 let n = pool.read_node("n1").await.unwrap().unwrap();
341 assert_eq!(n.id, "n1");
342 }
343
344 #[tokio::test]
345 async fn read_missing_returns_none() {
346 let pool = mem().await;
347 assert!(pool.read_node("ghost").await.unwrap().is_none());
348 }
349
350 #[tokio::test]
351 async fn delete_node() {
352 let pool = mem().await;
353 pool.upsert_node(node("n1")).await.unwrap();
354 assert!(pool.delete_node("n1").await.unwrap());
355 assert!(pool.read_node("n1").await.unwrap().is_none());
356 }
357
358 #[tokio::test]
359 async fn append_and_read_edges() {
360 let pool = mem().await;
361 pool.upsert_node(node("a")).await.unwrap();
362 pool.upsert_node(node("b")).await.unwrap();
363 pool.append_edge(GraphEdge {
364 id: "e1".into(),
365 source: "a".into(),
366 target: "b".into(),
367 relation: "related".into(),
368 weight: 1.0,
369 ts: "2026-01-01T00:00:00Z".into(),
370 })
371 .await
372 .unwrap();
373 let edges = pool.read_edges().await.unwrap();
374 assert_eq!(edges.len(), 1);
375 }
376
377 #[tokio::test]
378 async fn delete_edge() {
379 let pool = mem().await;
380 pool.append_edge(GraphEdge {
381 id: "e1".into(),
382 source: "a".into(),
383 target: "b".into(),
384 relation: "related".into(),
385 weight: 1.0,
386 ts: "2026-01-01T00:00:00Z".into(),
387 })
388 .await
389 .unwrap();
390 assert!(pool.delete_edge("e1").await.unwrap());
391 assert!(pool.read_edges().await.unwrap().is_empty());
392 }
393
394 #[tokio::test]
395 async fn search_finds_nodes() {
396 let pool = mem().await;
397 let mut n = node("n1");
398 n.title = "Rust ownership".to_string();
399 pool.upsert_node(n).await.unwrap();
400 let results = pool.search_nodes("Rust", 10).await.unwrap();
401 assert_eq!(results.len(), 1);
402 }
403
404 #[tokio::test]
405 async fn stats_returns_counts() {
406 let pool = mem().await;
407 pool.upsert_node(node("a")).await.unwrap();
408 pool.upsert_node(node("b")).await.unwrap();
409 let s = pool.stats().await.unwrap();
410 assert_eq!(s.total_nodes, 2);
411 assert_eq!(s.total_edges, 0);
412 }
413
414 #[tokio::test]
415 async fn clone_shares_pool() {
416 let pool = mem().await;
417 let pool2 = pool.clone();
418 pool.upsert_node(node("n1")).await.unwrap();
419 assert!(pool2.read_node("n1").await.unwrap().is_some());
420 }
421
422 #[tokio::test]
423 async fn concurrent_reads() {
424 let pool = mem().await;
425 pool.upsert_node(node("n1")).await.unwrap();
426
427 let mut handles = vec![];
428 for _ in 0..4 {
429 let p = pool.clone();
430 handles.push(tokio::spawn(async move {
431 p.read_node("n1").await.unwrap().is_some()
432 }));
433 }
434 for h in handles {
435 assert!(h.await.unwrap());
436 }
437 }
438
439 #[tokio::test]
440 async fn open_creates_file() {
441 let dir = tempfile::tempdir().unwrap();
442 let path = dir.path().join("sub").join("test.db");
443 let pool = AsyncPoolGraph::open(&path, 2).await.unwrap();
444 pool.upsert_node(node("n1")).await.unwrap();
445 assert!(path.exists());
446 drop(pool);
447 }
448}