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