1use crate::Storage;
4use codemem_core::{CodememError, ConsolidationLogEntry, Session, StorageStats};
5use rusqlite::params;
6use std::collections::HashMap;
7
8impl Storage {
9 pub fn integrity_check(&self) -> Result<bool, CodememError> {
13 let conn = self.conn();
14 let result: String = conn
15 .query_row("PRAGMA integrity_check", [], |row| row.get(0))
16 .map_err(|e| CodememError::Storage(e.to_string()))?;
17 Ok(result == "ok")
18 }
19
20 pub fn schema_version(&self) -> Result<u32, CodememError> {
22 let conn = self.conn();
23 let version: u32 = conn
24 .query_row(
25 "SELECT COALESCE(MAX(version), 0) FROM schema_version",
26 [],
27 |row| row.get(0),
28 )
29 .map_err(|e| CodememError::Storage(e.to_string()))?;
30 Ok(version)
31 }
32
33 pub fn stats(&self) -> Result<StorageStats, CodememError> {
37 let memory_count = self.memory_count()?;
38 let conn = self.conn();
39
40 let embedding_count: i64 = conn
41 .query_row("SELECT COUNT(*) FROM memory_embeddings", [], |row| {
42 row.get(0)
43 })
44 .map_err(|e| CodememError::Storage(e.to_string()))?;
45
46 let node_count: i64 = conn
47 .query_row("SELECT COUNT(*) FROM graph_nodes", [], |row| row.get(0))
48 .map_err(|e| CodememError::Storage(e.to_string()))?;
49
50 let edge_count: i64 = conn
51 .query_row("SELECT COUNT(*) FROM graph_edges", [], |row| row.get(0))
52 .map_err(|e| CodememError::Storage(e.to_string()))?;
53
54 Ok(StorageStats {
55 memory_count,
56 embedding_count: embedding_count as usize,
57 node_count: node_count as usize,
58 edge_count: edge_count as usize,
59 })
60 }
61
62 pub fn insert_consolidation_log(
66 &self,
67 cycle_type: &str,
68 affected_count: usize,
69 ) -> Result<(), CodememError> {
70 let conn = self.conn();
71 let now = chrono::Utc::now().timestamp();
72 conn.execute(
73 "INSERT INTO consolidation_log (cycle_type, run_at, affected_count) VALUES (?1, ?2, ?3)",
74 params![cycle_type, now, affected_count as i64],
75 )
76 .map_err(|e| CodememError::Storage(e.to_string()))?;
77 Ok(())
78 }
79
80 pub fn last_consolidation_runs(&self) -> Result<Vec<ConsolidationLogEntry>, CodememError> {
82 let conn = self.conn();
83 let mut stmt = conn
84 .prepare(
85 "SELECT cycle_type, run_at, affected_count FROM consolidation_log
86 WHERE id IN (
87 SELECT id FROM consolidation_log c2
88 WHERE c2.cycle_type = consolidation_log.cycle_type
89 ORDER BY run_at DESC LIMIT 1
90 )
91 GROUP BY cycle_type
92 ORDER BY cycle_type",
93 )
94 .map_err(|e| CodememError::Storage(e.to_string()))?;
95
96 let entries = stmt
97 .query_map([], |row| {
98 Ok(ConsolidationLogEntry {
99 cycle_type: row.get(0)?,
100 run_at: row.get(1)?,
101 affected_count: row.get::<_, i64>(2)? as usize,
102 })
103 })
104 .map_err(|e| CodememError::Storage(e.to_string()))?
105 .collect::<Result<Vec<_>, _>>()
106 .map_err(|e| CodememError::Storage(e.to_string()))?;
107
108 Ok(entries)
109 }
110
111 pub fn get_repeated_searches(
117 &self,
118 min_count: usize,
119 namespace: Option<&str>,
120 ) -> Result<Vec<(String, usize, Vec<String>)>, CodememError> {
121 let conn = self.conn();
122 let sql = if namespace.is_some() {
123 "SELECT json_extract(metadata, '$.pattern') AS pat,
124 COUNT(*) AS cnt,
125 GROUP_CONCAT(id, ',') AS ids
126 FROM memories
127 WHERE json_extract(metadata, '$.tool') IN ('Grep', 'Glob')
128 AND pat IS NOT NULL
129 AND namespace = ?1
130 GROUP BY pat
131 HAVING cnt >= ?2
132 ORDER BY cnt DESC"
133 } else {
134 "SELECT json_extract(metadata, '$.pattern') AS pat,
135 COUNT(*) AS cnt,
136 GROUP_CONCAT(id, ',') AS ids
137 FROM memories
138 WHERE json_extract(metadata, '$.tool') IN ('Grep', 'Glob')
139 AND pat IS NOT NULL
140 GROUP BY pat
141 HAVING cnt >= ?1
142 ORDER BY cnt DESC"
143 };
144
145 let mut stmt = conn
146 .prepare(sql)
147 .map_err(|e| CodememError::Storage(e.to_string()))?;
148
149 let rows = if let Some(ns) = namespace {
150 stmt.query_map(params![ns, min_count as i64], |row| {
151 Ok((
152 row.get::<_, String>(0)?,
153 row.get::<_, i64>(1)?,
154 row.get::<_, String>(2)?,
155 ))
156 })
157 .map_err(|e| CodememError::Storage(e.to_string()))?
158 .collect::<Result<Vec<_>, _>>()
159 .map_err(|e| CodememError::Storage(e.to_string()))?
160 } else {
161 stmt.query_map(params![min_count as i64], |row| {
162 Ok((
163 row.get::<_, String>(0)?,
164 row.get::<_, i64>(1)?,
165 row.get::<_, String>(2)?,
166 ))
167 })
168 .map_err(|e| CodememError::Storage(e.to_string()))?
169 .collect::<Result<Vec<_>, _>>()
170 .map_err(|e| CodememError::Storage(e.to_string()))?
171 };
172
173 Ok(rows
174 .into_iter()
175 .map(|(pat, cnt, ids_str)| {
176 let ids: Vec<String> = ids_str.split(',').map(String::from).collect();
177 (pat, cnt as usize, ids)
178 })
179 .collect())
180 }
181
182 pub fn get_file_hotspots(
184 &self,
185 min_count: usize,
186 namespace: Option<&str>,
187 ) -> Result<Vec<(String, usize, Vec<String>)>, CodememError> {
188 let conn = self.conn();
189 let sql = if namespace.is_some() {
190 "SELECT json_extract(metadata, '$.file_path') AS fp,
191 COUNT(*) AS cnt,
192 GROUP_CONCAT(id, ',') AS ids
193 FROM memories
194 WHERE fp IS NOT NULL
195 AND namespace = ?1
196 GROUP BY fp
197 HAVING cnt >= ?2
198 ORDER BY cnt DESC"
199 } else {
200 "SELECT json_extract(metadata, '$.file_path') AS fp,
201 COUNT(*) AS cnt,
202 GROUP_CONCAT(id, ',') AS ids
203 FROM memories
204 WHERE fp IS NOT NULL
205 GROUP BY fp
206 HAVING cnt >= ?1
207 ORDER BY cnt DESC"
208 };
209
210 let mut stmt = conn
211 .prepare(sql)
212 .map_err(|e| CodememError::Storage(e.to_string()))?;
213
214 let rows = if let Some(ns) = namespace {
215 stmt.query_map(params![ns, min_count as i64], |row| {
216 Ok((
217 row.get::<_, String>(0)?,
218 row.get::<_, i64>(1)?,
219 row.get::<_, String>(2)?,
220 ))
221 })
222 .map_err(|e| CodememError::Storage(e.to_string()))?
223 .collect::<Result<Vec<_>, _>>()
224 .map_err(|e| CodememError::Storage(e.to_string()))?
225 } else {
226 stmt.query_map(params![min_count as i64], |row| {
227 Ok((
228 row.get::<_, String>(0)?,
229 row.get::<_, i64>(1)?,
230 row.get::<_, String>(2)?,
231 ))
232 })
233 .map_err(|e| CodememError::Storage(e.to_string()))?
234 .collect::<Result<Vec<_>, _>>()
235 .map_err(|e| CodememError::Storage(e.to_string()))?
236 };
237
238 Ok(rows
239 .into_iter()
240 .map(|(fp, cnt, ids_str)| {
241 let ids: Vec<String> = ids_str.split(',').map(String::from).collect();
242 (fp, cnt as usize, ids)
243 })
244 .collect())
245 }
246
247 pub fn get_tool_usage_stats(
249 &self,
250 namespace: Option<&str>,
251 ) -> Result<HashMap<String, usize>, CodememError> {
252 let conn = self.conn();
253 let sql = if namespace.is_some() {
254 "SELECT json_extract(metadata, '$.tool') AS tool,
255 COUNT(*) AS cnt
256 FROM memories
257 WHERE tool IS NOT NULL
258 AND namespace = ?1
259 GROUP BY tool
260 ORDER BY cnt DESC"
261 } else {
262 "SELECT json_extract(metadata, '$.tool') AS tool,
263 COUNT(*) AS cnt
264 FROM memories
265 WHERE tool IS NOT NULL
266 GROUP BY tool
267 ORDER BY cnt DESC"
268 };
269
270 let mut stmt = conn
271 .prepare(sql)
272 .map_err(|e| CodememError::Storage(e.to_string()))?;
273
274 let rows = if let Some(ns) = namespace {
275 stmt.query_map(params![ns], |row| {
276 Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?))
277 })
278 .map_err(|e| CodememError::Storage(e.to_string()))?
279 .collect::<Result<Vec<_>, _>>()
280 .map_err(|e| CodememError::Storage(e.to_string()))?
281 } else {
282 stmt.query_map([], |row| {
283 Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?))
284 })
285 .map_err(|e| CodememError::Storage(e.to_string()))?
286 .collect::<Result<Vec<_>, _>>()
287 .map_err(|e| CodememError::Storage(e.to_string()))?
288 };
289
290 Ok(rows
291 .into_iter()
292 .map(|(tool, cnt)| (tool, cnt as usize))
293 .collect())
294 }
295
296 pub fn get_decision_chains(
298 &self,
299 min_count: usize,
300 namespace: Option<&str>,
301 ) -> Result<Vec<(String, usize, Vec<String>)>, CodememError> {
302 let conn = self.conn();
303 let sql = if namespace.is_some() {
304 "SELECT json_extract(metadata, '$.file_path') AS fp,
305 COUNT(*) AS cnt,
306 GROUP_CONCAT(id, ',') AS ids
307 FROM memories
308 WHERE json_extract(metadata, '$.tool') IN ('Edit', 'Write')
309 AND fp IS NOT NULL
310 AND namespace = ?1
311 GROUP BY fp
312 HAVING cnt >= ?2
313 ORDER BY cnt DESC"
314 } else {
315 "SELECT json_extract(metadata, '$.file_path') AS fp,
316 COUNT(*) AS cnt,
317 GROUP_CONCAT(id, ',') AS ids
318 FROM memories
319 WHERE json_extract(metadata, '$.tool') IN ('Edit', 'Write')
320 AND fp IS NOT NULL
321 GROUP BY fp
322 HAVING cnt >= ?1
323 ORDER BY cnt DESC"
324 };
325
326 let mut stmt = conn
327 .prepare(sql)
328 .map_err(|e| CodememError::Storage(e.to_string()))?;
329
330 let rows = if let Some(ns) = namespace {
331 stmt.query_map(params![ns, min_count as i64], |row| {
332 Ok((
333 row.get::<_, String>(0)?,
334 row.get::<_, i64>(1)?,
335 row.get::<_, String>(2)?,
336 ))
337 })
338 .map_err(|e| CodememError::Storage(e.to_string()))?
339 .collect::<Result<Vec<_>, _>>()
340 .map_err(|e| CodememError::Storage(e.to_string()))?
341 } else {
342 stmt.query_map(params![min_count as i64], |row| {
343 Ok((
344 row.get::<_, String>(0)?,
345 row.get::<_, i64>(1)?,
346 row.get::<_, String>(2)?,
347 ))
348 })
349 .map_err(|e| CodememError::Storage(e.to_string()))?
350 .collect::<Result<Vec<_>, _>>()
351 .map_err(|e| CodememError::Storage(e.to_string()))?
352 };
353
354 Ok(rows
355 .into_iter()
356 .map(|(fp, cnt, ids_str)| {
357 let ids: Vec<String> = ids_str.split(',').map(String::from).collect();
358 (fp, cnt as usize, ids)
359 })
360 .collect())
361 }
362
363 pub fn count_memories_matching_keywords(
367 &self,
368 keywords: &[&str],
369 namespace: Option<&str>,
370 ) -> Result<usize, CodememError> {
371 if keywords.is_empty() {
372 return Ok(0);
373 }
374 let conn = self.conn();
375 let like_clauses: Vec<String> = keywords
376 .iter()
377 .enumerate()
378 .map(|(i, _)| format!("content LIKE ?{}", i + 1))
379 .collect();
380 let where_likes = like_clauses.join(" OR ");
381
382 let (sql, params_vec): (String, Vec<Box<dyn rusqlite::types::ToSql>>) =
383 if let Some(ns) = namespace {
384 let sql = format!(
385 "SELECT COUNT(*) FROM memories WHERE ({}) AND namespace = ?{}",
386 where_likes,
387 keywords.len() + 1,
388 );
389 let mut p: Vec<Box<dyn rusqlite::types::ToSql>> = keywords
390 .iter()
391 .map(|k| Box::new(format!("%{k}%")) as Box<dyn rusqlite::types::ToSql>)
392 .collect();
393 p.push(Box::new(ns.to_string()));
394 (sql, p)
395 } else {
396 let sql = format!("SELECT COUNT(*) FROM memories WHERE ({})", where_likes);
397 let p: Vec<Box<dyn rusqlite::types::ToSql>> = keywords
398 .iter()
399 .map(|k| Box::new(format!("%{k}%")) as Box<dyn rusqlite::types::ToSql>)
400 .collect();
401 (sql, p)
402 };
403
404 let params_refs: Vec<&dyn rusqlite::types::ToSql> =
405 params_vec.iter().map(|b| &**b).collect();
406
407 let count: i64 = conn
408 .query_row(&sql, params_refs.as_slice(), |row| row.get(0))
409 .map_err(|e| CodememError::Storage(e.to_string()))?;
410 Ok(count as usize)
411 }
412
413 pub fn list_memories_by_tag(
415 &self,
416 tag: &str,
417 namespace: Option<&str>,
418 limit: usize,
419 ) -> Result<Vec<codemem_core::MemoryNode>, CodememError> {
420 let conn = self.conn();
421 let like_pattern = format!("%\"{tag}\"%");
422
423 let (sql, params_vec): (String, Vec<Box<dyn rusqlite::types::ToSql>>) = if let Some(ns) =
424 namespace
425 {
426 (
427 "SELECT id, content, memory_type, importance, confidence, access_count, \
428 content_hash, tags, metadata, namespace, created_at, updated_at, last_accessed_at \
429 FROM memories WHERE tags LIKE ?1 AND namespace = ?2 \
430 ORDER BY created_at DESC LIMIT ?3"
431 .to_string(),
432 vec![
433 Box::new(like_pattern) as Box<dyn rusqlite::types::ToSql>,
434 Box::new(ns.to_string()),
435 Box::new(limit as i64),
436 ],
437 )
438 } else {
439 (
440 "SELECT id, content, memory_type, importance, confidence, access_count, \
441 content_hash, tags, metadata, namespace, created_at, updated_at, last_accessed_at \
442 FROM memories WHERE tags LIKE ?1 \
443 ORDER BY created_at DESC LIMIT ?2"
444 .to_string(),
445 vec![
446 Box::new(like_pattern) as Box<dyn rusqlite::types::ToSql>,
447 Box::new(limit as i64),
448 ],
449 )
450 };
451
452 let params_refs: Vec<&dyn rusqlite::types::ToSql> =
453 params_vec.iter().map(|b| &**b).collect();
454
455 let mut stmt = conn
456 .prepare(&sql)
457 .map_err(|e| CodememError::Storage(e.to_string()))?;
458
459 let rows = stmt
460 .query_map(params_refs.as_slice(), |row| {
461 let created_ts: i64 = row.get(10)?;
462 let updated_ts: i64 = row.get(11)?;
463 let accessed_ts: i64 = row.get(12)?;
464 let tags_json: String = row.get(7)?;
465 let metadata_json: String = row.get(8)?;
466 let memory_type_str: String = row.get(2)?;
467
468 Ok(codemem_core::MemoryNode {
469 id: row.get(0)?,
470 content: row.get(1)?,
471 memory_type: memory_type_str
472 .parse()
473 .unwrap_or(codemem_core::MemoryType::Context),
474 importance: row.get(3)?,
475 confidence: row.get(4)?,
476 access_count: row.get::<_, i64>(5).unwrap_or(0) as u32,
477 content_hash: row.get(6)?,
478 tags: serde_json::from_str(&tags_json).unwrap_or_default(),
479 metadata: serde_json::from_str(&metadata_json).unwrap_or_default(),
480 namespace: row.get(9)?,
481 created_at: chrono::DateTime::from_timestamp(created_ts, 0)
482 .unwrap_or_default()
483 .with_timezone(&chrono::Utc),
484 updated_at: chrono::DateTime::from_timestamp(updated_ts, 0)
485 .unwrap_or_default()
486 .with_timezone(&chrono::Utc),
487 last_accessed_at: chrono::DateTime::from_timestamp(accessed_ts, 0)
488 .unwrap_or_default()
489 .with_timezone(&chrono::Utc),
490 })
491 })
492 .map_err(|e| CodememError::Storage(e.to_string()))?
493 .collect::<Result<Vec<_>, _>>()
494 .map_err(|e| CodememError::Storage(e.to_string()))?;
495
496 Ok(rows)
497 }
498
499 pub fn start_session(&self, id: &str, namespace: Option<&str>) -> Result<(), CodememError> {
503 let conn = self.conn();
504 let now = chrono::Utc::now().timestamp();
505 conn.execute(
506 "INSERT OR IGNORE INTO sessions (id, namespace, started_at) VALUES (?1, ?2, ?3)",
507 params![id, namespace, now],
508 )
509 .map_err(|e| CodememError::Storage(e.to_string()))?;
510 Ok(())
511 }
512
513 pub fn end_session(&self, id: &str, summary: Option<&str>) -> Result<(), CodememError> {
515 let conn = self.conn();
516 let now = chrono::Utc::now().timestamp();
517 conn.execute(
518 "UPDATE sessions SET ended_at = ?1, summary = ?2 WHERE id = ?3",
519 params![now, summary, id],
520 )
521 .map_err(|e| CodememError::Storage(e.to_string()))?;
522 Ok(())
523 }
524
525 pub fn list_sessions(&self, namespace: Option<&str>) -> Result<Vec<Session>, CodememError> {
527 self.list_sessions_with_limit(namespace, usize::MAX)
528 }
529
530 pub fn record_session_activity(
534 &self,
535 session_id: &str,
536 tool_name: &str,
537 file_path: Option<&str>,
538 directory: Option<&str>,
539 pattern: Option<&str>,
540 ) -> Result<(), CodememError> {
541 let conn = self.conn();
542 let now = chrono::Utc::now().timestamp();
543 conn.execute(
544 "INSERT INTO session_activity (session_id, tool_name, file_path, directory, pattern, created_at)
545 VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
546 params![session_id, tool_name, file_path, directory, pattern, now],
547 )
548 .map_err(|e| CodememError::Storage(e.to_string()))?;
549 Ok(())
550 }
551
552 pub fn get_session_activity_summary(
554 &self,
555 session_id: &str,
556 ) -> Result<codemem_core::SessionActivitySummary, CodememError> {
557 let conn = self.conn();
558
559 let files_read: i64 = conn
560 .query_row(
561 "SELECT COUNT(DISTINCT file_path) FROM session_activity
562 WHERE session_id = ?1 AND tool_name = 'Read' AND file_path IS NOT NULL",
563 params![session_id],
564 |row| row.get(0),
565 )
566 .map_err(|e| CodememError::Storage(e.to_string()))?;
567
568 let files_edited: i64 = conn
569 .query_row(
570 "SELECT COUNT(DISTINCT file_path) FROM session_activity
571 WHERE session_id = ?1 AND tool_name IN ('Edit', 'Write') AND file_path IS NOT NULL",
572 params![session_id],
573 |row| row.get(0),
574 )
575 .map_err(|e| CodememError::Storage(e.to_string()))?;
576
577 let searches: i64 = conn
578 .query_row(
579 "SELECT COUNT(*) FROM session_activity
580 WHERE session_id = ?1 AND tool_name IN ('Grep', 'Glob')",
581 params![session_id],
582 |row| row.get(0),
583 )
584 .map_err(|e| CodememError::Storage(e.to_string()))?;
585
586 let total_actions: i64 = conn
587 .query_row(
588 "SELECT COUNT(*) FROM session_activity WHERE session_id = ?1",
589 params![session_id],
590 |row| row.get(0),
591 )
592 .map_err(|e| CodememError::Storage(e.to_string()))?;
593
594 Ok(codemem_core::SessionActivitySummary {
595 files_read: files_read as usize,
596 files_edited: files_edited as usize,
597 searches: searches as usize,
598 total_actions: total_actions as usize,
599 })
600 }
601
602 pub fn get_session_hot_directories(
604 &self,
605 session_id: &str,
606 limit: usize,
607 ) -> Result<Vec<(String, usize)>, CodememError> {
608 let conn = self.conn();
609 let mut stmt = conn
610 .prepare(
611 "SELECT directory, COUNT(*) AS cnt FROM session_activity
612 WHERE session_id = ?1 AND directory IS NOT NULL
613 GROUP BY directory ORDER BY cnt DESC LIMIT ?2",
614 )
615 .map_err(|e| CodememError::Storage(e.to_string()))?;
616
617 let rows = stmt
618 .query_map(params![session_id, limit as i64], |row| {
619 Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?))
620 })
621 .map_err(|e| CodememError::Storage(e.to_string()))?
622 .collect::<Result<Vec<_>, _>>()
623 .map_err(|e| CodememError::Storage(e.to_string()))?;
624
625 Ok(rows
626 .into_iter()
627 .map(|(dir, cnt)| (dir, cnt as usize))
628 .collect())
629 }
630
631 pub fn has_auto_insight(
633 &self,
634 session_id: &str,
635 dedup_tag: &str,
636 ) -> Result<bool, CodememError> {
637 let conn = self.conn();
638 let like_session = format!("%\"session_id\":\"{session_id}\"%");
639 let like_dedup = format!("%\"auto_insight_tag\":\"{dedup_tag}\"%");
640 let count: i64 = conn
641 .query_row(
642 "SELECT COUNT(*) FROM memories
643 WHERE metadata LIKE ?1 AND metadata LIKE ?2",
644 params![like_session, like_dedup],
645 |row| row.get(0),
646 )
647 .map_err(|e| CodememError::Storage(e.to_string()))?;
648 Ok(count > 0)
649 }
650
651 pub fn count_directory_reads(
653 &self,
654 session_id: &str,
655 directory: &str,
656 ) -> Result<usize, CodememError> {
657 let conn = self.conn();
658 let count: i64 = conn
659 .query_row(
660 "SELECT COUNT(*) FROM session_activity
661 WHERE session_id = ?1 AND tool_name = 'Read' AND directory = ?2",
662 params![session_id, directory],
663 |row| row.get(0),
664 )
665 .map_err(|e| CodememError::Storage(e.to_string()))?;
666 Ok(count as usize)
667 }
668
669 pub fn was_file_read_in_session(
671 &self,
672 session_id: &str,
673 file_path: &str,
674 ) -> Result<bool, CodememError> {
675 let conn = self.conn();
676 let count: i64 = conn
677 .query_row(
678 "SELECT COUNT(*) FROM session_activity
679 WHERE session_id = ?1 AND tool_name = 'Read' AND file_path = ?2",
680 params![session_id, file_path],
681 |row| row.get(0),
682 )
683 .map_err(|e| CodememError::Storage(e.to_string()))?;
684 Ok(count > 0)
685 }
686
687 pub fn count_search_pattern_in_session(
689 &self,
690 session_id: &str,
691 pattern: &str,
692 ) -> Result<usize, CodememError> {
693 let conn = self.conn();
694 let count: i64 = conn
695 .query_row(
696 "SELECT COUNT(*) FROM session_activity
697 WHERE session_id = ?1 AND tool_name IN ('Grep', 'Glob') AND pattern = ?2",
698 params![session_id, pattern],
699 |row| row.get(0),
700 )
701 .map_err(|e| CodememError::Storage(e.to_string()))?;
702 Ok(count as usize)
703 }
704
705 pub(crate) fn list_sessions_with_limit(
707 &self,
708 namespace: Option<&str>,
709 limit: usize,
710 ) -> Result<Vec<Session>, CodememError> {
711 let conn = self.conn();
712 let sql_with_ns = "SELECT id, namespace, started_at, ended_at, memory_count, summary FROM sessions WHERE namespace = ?1 ORDER BY started_at DESC LIMIT ?2";
713 let sql_all = "SELECT id, namespace, started_at, ended_at, memory_count, summary FROM sessions ORDER BY started_at DESC LIMIT ?1";
714
715 let map_row = |row: &rusqlite::Row<'_>| -> rusqlite::Result<Session> {
716 let started_ts: i64 = row.get(2)?;
717 let ended_ts: Option<i64> = row.get(3)?;
718 Ok(Session {
719 id: row.get(0)?,
720 namespace: row.get(1)?,
721 started_at: chrono::DateTime::from_timestamp(started_ts, 0)
722 .unwrap_or_default()
723 .with_timezone(&chrono::Utc),
724 ended_at: ended_ts.and_then(|ts| {
725 chrono::DateTime::from_timestamp(ts, 0).map(|dt| dt.with_timezone(&chrono::Utc))
726 }),
727 memory_count: row.get::<_, i64>(4).unwrap_or(0) as u32,
728 summary: row.get(5)?,
729 })
730 };
731
732 if let Some(ns) = namespace {
733 let mut stmt = conn
734 .prepare(sql_with_ns)
735 .map_err(|e| CodememError::Storage(e.to_string()))?;
736 let rows = stmt
737 .query_map(params![ns, limit as i64], map_row)
738 .map_err(|e| CodememError::Storage(e.to_string()))?;
739 rows.collect::<Result<Vec<_>, _>>()
740 .map_err(|e| CodememError::Storage(e.to_string()))
741 } else {
742 let mut stmt = conn
743 .prepare(sql_all)
744 .map_err(|e| CodememError::Storage(e.to_string()))?;
745 let rows = stmt
746 .query_map(params![limit as i64], map_row)
747 .map_err(|e| CodememError::Storage(e.to_string()))?;
748 rows.collect::<Result<Vec<_>, _>>()
749 .map_err(|e| CodememError::Storage(e.to_string()))
750 }
751 }
752 pub fn delete_graph_nodes_by_prefix(&self, prefix: &str) -> Result<usize, CodememError> {
757 let conn = self.conn();
758 let like_pattern = format!("{prefix}%");
759
760 conn.execute(
762 "DELETE FROM graph_edges WHERE src LIKE ?1 OR dst LIKE ?1",
763 params![like_pattern],
764 )
765 .map_err(|e| CodememError::Storage(e.to_string()))?;
766
767 conn.execute(
769 "DELETE FROM memory_embeddings WHERE memory_id LIKE ?1",
770 params![like_pattern],
771 )
772 .map_err(|e| CodememError::Storage(e.to_string()))?;
773
774 let rows = conn
776 .execute(
777 "DELETE FROM graph_nodes WHERE id LIKE ?1",
778 params![like_pattern],
779 )
780 .map_err(|e| CodememError::Storage(e.to_string()))?;
781
782 Ok(rows)
783 }
784}
785
786#[cfg(test)]
787#[path = "tests/queries_tests.rs"]
788mod tests;