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