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::{EdgeDirection, 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 query_nodes_ex(&self, q: crate::graph::NodeQuery) -> Result<Vec<GraphNode>> {
252 self.with_conn(move |c| crate::graph::search::query_nodes_ex(c, &q))
253 .await
254 }
255
256 pub async fn delete_node(&self, id: impl Into<String>) -> Result<bool> {
258 let id = id.into();
259 self.with_conn(move |c| crate::graph::store::delete_node(c, &id))
260 .await
261 }
262
263 pub async fn append_edge(&self, edge: GraphEdge) -> Result<()> {
267 self.with_conn(move |c| crate::graph::store::append_edge(c, &edge))
268 .await
269 }
270
271 pub async fn append_edges(&self, edges: Vec<GraphEdge>) -> Result<()> {
274 self.with_conn(move |c| crate::graph::store::append_edges(c, &edges))
275 .await
276 }
277
278 pub async fn edges_for_node_dir(
280 &self,
281 node_id: impl Into<String>,
282 dir: EdgeDirection,
283 relation: Option<String>,
284 ) -> Result<Vec<GraphEdge>> {
285 let node_id = node_id.into();
286 self.with_conn(move |c| {
287 crate::graph::store::edges_for_node_dir(c, &node_id, dir, relation.as_deref())
288 })
289 .await
290 }
291
292 pub async fn neighbors_weighted(
295 &self,
296 seed_ids: Vec<String>,
297 dir: EdgeDirection,
298 relation: Option<String>,
299 ) -> Result<Vec<(String, f64)>> {
300 self.with_conn(move |c| {
301 Ok(crate::graph::traversal::neighbors_weighted(
302 c,
303 &seed_ids,
304 dir,
305 relation.as_deref(),
306 ))
307 })
308 .await
309 }
310
311 pub async fn read_edges(&self) -> Result<Vec<GraphEdge>> {
313 self.with_conn(|c| crate::graph::store::read_edges(c, 10_000))
314 .await
315 }
316
317 pub async fn delete_edge(&self, id: impl Into<String>) -> Result<bool> {
319 let id = id.into();
320 self.with_conn(move |c| crate::graph::store::delete_edge(c, &id))
321 .await
322 }
323
324 pub async fn search_nodes(
328 &self,
329 query: impl Into<String>,
330 limit: usize,
331 ) -> Result<Vec<GraphNode>> {
332 let query = query.into();
333 self.with_conn(move |c| crate::graph::search::search_nodes(c, &query, limit))
334 .await
335 }
336
337 pub async fn smart_recall(
339 &self,
340 project: Option<String>,
341 hint: Option<String>,
342 limit: usize,
343 ) -> Result<Vec<ScoredNode>> {
344 self.with_conn(move |c| {
345 crate::graph::recall::smart_recall(c, project.as_deref(), hint.as_deref(), limit)
346 })
347 .await
348 }
349
350 pub async fn smart_recall_with(
352 &self,
353 opts: crate::graph::RecallOptions,
354 ) -> Result<Vec<ScoredNode>> {
355 self.with_conn(move |c| crate::graph::recall::smart_recall_with(c, &opts))
356 .await
357 }
358
359 pub async fn stats(&self) -> Result<GraphStats> {
363 self.with_conn(crate::graph::lifecycle::compute_stats).await
364 }
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370
371 fn node(id: &str) -> GraphNode {
372 GraphNode {
373 id: id.into(),
374 node_type: "concept".into(),
375 title: format!("Node {id}"),
376 body: "body".into(),
377 tags: vec![],
378 projects: vec![],
379 agents: vec![],
380 created: "2026-01-01T00:00:00Z".into(),
381 updated: "2026-01-01T00:00:00Z".into(),
382 importance: 0.5,
383 access_count: 0,
384 accessed_at: String::new(),
385 }
386 }
387
388 async fn mem() -> AsyncPoolGraph {
389 AsyncPoolGraph::open_in_memory(2).await.unwrap()
390 }
391
392 #[tokio::test]
393 async fn upsert_and_read_node() {
394 let pool = mem().await;
395 pool.upsert_node(node("n1")).await.unwrap();
396 let n = pool.read_node("n1").await.unwrap().unwrap();
397 assert_eq!(n.id, "n1");
398 }
399
400 #[tokio::test]
401 async fn read_missing_returns_none() {
402 let pool = mem().await;
403 assert!(pool.read_node("ghost").await.unwrap().is_none());
404 }
405
406 #[tokio::test]
407 async fn delete_node() {
408 let pool = mem().await;
409 pool.upsert_node(node("n1")).await.unwrap();
410 assert!(pool.delete_node("n1").await.unwrap());
411 assert!(pool.read_node("n1").await.unwrap().is_none());
412 }
413
414 #[tokio::test]
415 async fn append_and_read_edges() {
416 let pool = mem().await;
417 pool.upsert_node(node("a")).await.unwrap();
418 pool.upsert_node(node("b")).await.unwrap();
419 pool.append_edge(GraphEdge {
420 id: "e1".into(),
421 source: "a".into(),
422 target: "b".into(),
423 relation: "related".into(),
424 weight: 1.0,
425 ts: "2026-01-01T00:00:00Z".into(),
426 })
427 .await
428 .unwrap();
429 let edges = pool.read_edges().await.unwrap();
430 assert_eq!(edges.len(), 1);
431 }
432
433 #[tokio::test]
434 async fn delete_edge() {
435 let pool = mem().await;
436 pool.append_edge(GraphEdge {
437 id: "e1".into(),
438 source: "a".into(),
439 target: "b".into(),
440 relation: "related".into(),
441 weight: 1.0,
442 ts: "2026-01-01T00:00:00Z".into(),
443 })
444 .await
445 .unwrap();
446 assert!(pool.delete_edge("e1").await.unwrap());
447 assert!(pool.read_edges().await.unwrap().is_empty());
448 }
449
450 #[tokio::test]
451 async fn search_finds_nodes() {
452 let pool = mem().await;
453 let mut n = node("n1");
454 n.title = "Rust ownership".to_string();
455 pool.upsert_node(n).await.unwrap();
456 let results = pool.search_nodes("Rust", 10).await.unwrap();
457 assert_eq!(results.len(), 1);
458 }
459
460 #[tokio::test]
461 async fn stats_returns_counts() {
462 let pool = mem().await;
463 pool.upsert_node(node("a")).await.unwrap();
464 pool.upsert_node(node("b")).await.unwrap();
465 let s = pool.stats().await.unwrap();
466 assert_eq!(s.total_nodes, 2);
467 assert_eq!(s.total_edges, 0);
468 }
469
470 #[tokio::test]
471 async fn clone_shares_pool() {
472 let pool = mem().await;
473 let pool2 = pool.clone();
474 pool.upsert_node(node("n1")).await.unwrap();
475 assert!(pool2.read_node("n1").await.unwrap().is_some());
476 }
477
478 #[tokio::test]
479 async fn concurrent_reads() {
480 let pool = mem().await;
481 pool.upsert_node(node("n1")).await.unwrap();
482
483 let mut handles = vec![];
484 for _ in 0..4 {
485 let p = pool.clone();
486 handles.push(tokio::spawn(async move {
487 p.read_node("n1").await.unwrap().is_some()
488 }));
489 }
490 for h in handles {
491 assert!(h.await.unwrap());
492 }
493 }
494
495 #[tokio::test]
496 async fn open_creates_file() {
497 let dir = tempfile::tempdir().unwrap();
498 let path = dir.path().join("sub").join("test.db");
499 let pool = AsyncPoolGraph::open(&path, 2).await.unwrap();
500 pool.upsert_node(node("n1")).await.unwrap();
501 assert!(path.exists());
502 drop(pool);
503 }
504}