1use crate::embedder::f32_to_bytes;
8use crate::errors::AppError;
9use crate::storage::utils::with_busy_retry;
10use rusqlite::{params, Connection};
11use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Serialize, Deserialize)]
18pub struct NewMemory {
19 pub namespace: String,
20 pub name: String,
21 pub memory_type: String,
22 pub description: String,
23 pub body: String,
24 pub body_hash: String,
25 pub session_id: Option<String>,
26 pub source: String,
27 pub metadata: serde_json::Value,
28}
29
30#[derive(Debug, Serialize)]
35pub struct MemoryRow {
36 pub id: i64,
37 pub namespace: String,
38 pub name: String,
39 pub memory_type: String,
40 pub description: String,
41 pub body: String,
42 pub body_hash: String,
43 pub session_id: Option<String>,
44 pub source: String,
45 pub metadata: String,
46 pub created_at: i64,
47 pub updated_at: i64,
48 #[serde(skip_serializing_if = "Option::is_none")]
52 pub deleted_at: Option<i64>,
53}
54
55pub fn find_by_name(
72 conn: &Connection,
73 namespace: &str,
74 name: &str,
75) -> Result<Option<(i64, i64, i64)>, AppError> {
76 let mut stmt = conn.prepare_cached(
77 "SELECT m.id, m.updated_at, COALESCE(MAX(v.version), 0)
78 FROM memories m
79 LEFT JOIN memory_versions v ON v.memory_id = m.id
80 WHERE m.namespace = ?1 AND m.name = ?2 AND m.deleted_at IS NULL
81 GROUP BY m.id",
82 )?;
83 let result = stmt.query_row(params![namespace, name], |r| {
84 Ok((
85 r.get::<_, i64>(0)?,
86 r.get::<_, i64>(1)?,
87 r.get::<_, i64>(2)?,
88 ))
89 });
90 match result {
91 Ok(row) => Ok(Some(row)),
92 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
93 Err(e) => Err(AppError::Database(e)),
94 }
95}
96
97pub fn find_by_name_any_state(
106 conn: &Connection,
107 namespace: &str,
108 name: &str,
109) -> Result<Option<(i64, bool)>, AppError> {
110 let mut stmt = conn.prepare_cached(
111 "SELECT id, (deleted_at IS NOT NULL) AS is_deleted
112 FROM memories WHERE namespace = ?1 AND name = ?2",
113 )?;
114 let result = stmt.query_row(params![namespace, name], |r| {
115 Ok((r.get::<_, i64>(0)?, r.get::<_, bool>(1)?))
116 });
117 match result {
118 Ok(row) => Ok(Some(row)),
119 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
120 Err(e) => Err(AppError::Database(e)),
121 }
122}
123
124pub fn clear_deleted_at(conn: &Connection, memory_id: i64) -> Result<(), AppError> {
130 conn.execute(
131 "UPDATE memories SET deleted_at = NULL WHERE id = ?1",
132 params![memory_id],
133 )?;
134 Ok(())
135}
136
137pub fn find_by_hash(
151 conn: &Connection,
152 namespace: &str,
153 body_hash: &str,
154) -> Result<Option<i64>, AppError> {
155 let mut stmt = conn.prepare_cached(
156 "SELECT id FROM memories WHERE namespace = ?1 AND body_hash = ?2 AND deleted_at IS NULL",
157 )?;
158 match stmt.query_row(params![namespace, body_hash], |r| r.get(0)) {
159 Ok(id) => Ok(Some(id)),
160 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
161 Err(e) => Err(AppError::Database(e)),
162 }
163}
164
165pub fn insert(conn: &Connection, m: &NewMemory) -> Result<i64, AppError> {
181 let validated_source = crate::memory_source::validate_source(&m.source)?;
187 conn.execute(
188 "INSERT INTO memories (namespace, name, type, description, body, body_hash, session_id, source, metadata)
189 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
190 params![
191 m.namespace, m.name, m.memory_type, m.description, m.body,
192 m.body_hash, m.session_id, validated_source,
193 serde_json::to_string(&m.metadata)?
194 ],
195 )?;
196 Ok(conn.last_insert_rowid())
197}
198
199pub fn update(
214 conn: &Connection,
215 id: i64,
216 m: &NewMemory,
217 expected_updated_at: Option<i64>,
218) -> Result<bool, AppError> {
219 let validated_source = crate::memory_source::validate_source(&m.source)?;
224 let affected = if let Some(ts) = expected_updated_at {
225 conn.execute(
226 "UPDATE memories SET type=?2, description=?3, body=?4, body_hash=?5,
227 session_id=?6, source=?7, metadata=?8
228 WHERE id=?1 AND updated_at=?9 AND deleted_at IS NULL",
229 params![
230 id,
231 m.memory_type,
232 m.description,
233 m.body,
234 m.body_hash,
235 m.session_id,
236 validated_source,
237 serde_json::to_string(&m.metadata)?,
238 ts
239 ],
240 )?
241 } else {
242 conn.execute(
243 "UPDATE memories SET type=?2, description=?3, body=?4, body_hash=?5,
244 session_id=?6, source=?7, metadata=?8
245 WHERE id=?1 AND deleted_at IS NULL",
246 params![
247 id,
248 m.memory_type,
249 m.description,
250 m.body,
251 m.body_hash,
252 m.session_id,
253 validated_source,
254 serde_json::to_string(&m.metadata)?
255 ],
256 )?
257 };
258 Ok(affected == 1)
259}
260
261pub fn upsert_vec(
273 conn: &Connection,
274 memory_id: i64,
275 namespace: &str,
276 _memory_type: &str,
277 embedding: &[f32],
278 _name: &str,
279 _snippet: &str,
280) -> Result<(), AppError> {
281 let embedding_bytes = f32_to_bytes(embedding);
282 with_busy_retry(|| {
283 conn.execute(
284 "DELETE FROM memory_embeddings WHERE memory_id = ?1",
285 params![memory_id],
286 )?;
287 conn.execute(
288 "INSERT INTO memory_embeddings(memory_id, namespace, embedding, source, model, dim)
289 VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
290 params![
291 memory_id,
292 namespace,
293 &embedding_bytes,
294 "llm-headless",
295 crate::constants::SQLITE_GRAPHRAG_VERSION,
296 crate::constants::embedding_dim() as i64,
297 ],
298 )?;
299 Ok(())
300 })
301}
302
303pub fn delete_vec(conn: &Connection, memory_id: i64) -> Result<(), AppError> {
315 conn.execute(
316 "DELETE FROM memory_embeddings WHERE memory_id = ?1",
317 params![memory_id],
318 )?;
319 Ok(())
320}
321
322pub fn read_by_name(
332 conn: &Connection,
333 namespace: &str,
334 name: &str,
335) -> Result<Option<MemoryRow>, AppError> {
336 let mut stmt = conn.prepare_cached(
337 "SELECT id, namespace, name, type, description, body, body_hash,
338 session_id, source, metadata, created_at, updated_at, deleted_at
339 FROM memories WHERE namespace=?1 AND name=?2 AND deleted_at IS NULL",
340 )?;
341 match stmt.query_row(params![namespace, name], |r| {
342 Ok(MemoryRow {
343 id: r.get(0)?,
344 namespace: r.get(1)?,
345 name: r.get(2)?,
346 memory_type: r.get(3)?,
347 description: r.get(4)?,
348 body: r.get(5)?,
349 body_hash: r.get(6)?,
350 session_id: r.get(7)?,
351 source: r.get(8)?,
352 metadata: r.get(9)?,
353 created_at: r.get(10)?,
354 updated_at: r.get(11)?,
355 deleted_at: r.get(12)?,
356 })
357 }) {
358 Ok(m) => Ok(Some(m)),
359 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
360 Err(e) => Err(AppError::Database(e)),
361 }
362}
363
364pub fn soft_delete(conn: &Connection, namespace: &str, name: &str) -> Result<bool, AppError> {
378 let affected = conn.execute(
379 "UPDATE memories SET deleted_at = unixepoch() WHERE namespace=?1 AND name=?2 AND deleted_at IS NULL",
380 params![namespace, name],
381 )?;
382 Ok(affected == 1)
383}
384
385pub fn list(
396 conn: &Connection,
397 namespace: &str,
398 memory_type: Option<&str>,
399 limit: usize,
400 offset: usize,
401 include_deleted: bool,
402) -> Result<Vec<MemoryRow>, AppError> {
403 if let Some(mt) = memory_type {
404 let sql = if include_deleted {
405 "SELECT id, namespace, name, type, description, body, body_hash,
406 session_id, source, metadata, created_at, updated_at, deleted_at
407 FROM memories WHERE namespace=?1 AND type=?2
408 ORDER BY updated_at DESC LIMIT ?3 OFFSET ?4"
409 } else {
410 "SELECT id, namespace, name, type, description, body, body_hash,
411 session_id, source, metadata, created_at, updated_at, deleted_at
412 FROM memories WHERE namespace=?1 AND type=?2 AND deleted_at IS NULL
413 ORDER BY updated_at DESC LIMIT ?3 OFFSET ?4"
414 };
415 let mut stmt = conn.prepare_cached(sql)?;
416 let rows = stmt
417 .query_map(params![namespace, mt, limit as i64, offset as i64], |r| {
418 Ok(MemoryRow {
419 id: r.get(0)?,
420 namespace: r.get(1)?,
421 name: r.get(2)?,
422 memory_type: r.get(3)?,
423 description: r.get(4)?,
424 body: r.get(5)?,
425 body_hash: r.get(6)?,
426 session_id: r.get(7)?,
427 source: r.get(8)?,
428 metadata: r.get(9)?,
429 created_at: r.get(10)?,
430 updated_at: r.get(11)?,
431 deleted_at: r.get(12)?,
432 })
433 })?
434 .collect::<Result<Vec<_>, _>>()?;
435 Ok(rows)
436 } else {
437 let sql = if include_deleted {
438 "SELECT id, namespace, name, type, description, body, body_hash,
439 session_id, source, metadata, created_at, updated_at, deleted_at
440 FROM memories WHERE namespace=?1
441 ORDER BY updated_at DESC LIMIT ?2 OFFSET ?3"
442 } else {
443 "SELECT id, namespace, name, type, description, body, body_hash,
444 session_id, source, metadata, created_at, updated_at, deleted_at
445 FROM memories WHERE namespace=?1 AND deleted_at IS NULL
446 ORDER BY updated_at DESC LIMIT ?2 OFFSET ?3"
447 };
448 let mut stmt = conn.prepare_cached(sql)?;
449 let rows = stmt
450 .query_map(params![namespace, limit as i64, offset as i64], |r| {
451 Ok(MemoryRow {
452 id: r.get(0)?,
453 namespace: r.get(1)?,
454 name: r.get(2)?,
455 memory_type: r.get(3)?,
456 description: r.get(4)?,
457 body: r.get(5)?,
458 body_hash: r.get(6)?,
459 session_id: r.get(7)?,
460 source: r.get(8)?,
461 metadata: r.get(9)?,
462 created_at: r.get(10)?,
463 updated_at: r.get(11)?,
464 deleted_at: r.get(12)?,
465 })
466 })?
467 .collect::<Result<Vec<_>, _>>()?;
468 Ok(rows)
469 }
470}
471
472pub fn count(
473 conn: &Connection,
474 namespace: &str,
475 memory_type: Option<&str>,
476 include_deleted: bool,
477) -> Result<usize, AppError> {
478 let (sql, params_vec): (&str, Vec<Box<dyn rusqlite::types::ToSql>>) = match (
479 memory_type,
480 include_deleted,
481 ) {
482 (Some(mt), true) => (
483 "SELECT COUNT(*) FROM memories WHERE namespace=?1 AND type=?2",
484 vec![
485 Box::new(namespace.to_string()) as Box<dyn rusqlite::types::ToSql>,
486 Box::new(mt.to_string()),
487 ],
488 ),
489 (Some(mt), false) => (
490 "SELECT COUNT(*) FROM memories WHERE namespace=?1 AND type=?2 AND deleted_at IS NULL",
491 vec![
492 Box::new(namespace.to_string()) as Box<dyn rusqlite::types::ToSql>,
493 Box::new(mt.to_string()),
494 ],
495 ),
496 (None, true) => (
497 "SELECT COUNT(*) FROM memories WHERE namespace=?1",
498 vec![Box::new(namespace.to_string()) as Box<dyn rusqlite::types::ToSql>],
499 ),
500 (None, false) => (
501 "SELECT COUNT(*) FROM memories WHERE namespace=?1 AND deleted_at IS NULL",
502 vec![Box::new(namespace.to_string()) as Box<dyn rusqlite::types::ToSql>],
503 ),
504 };
505 let params_refs: Vec<&dyn rusqlite::types::ToSql> =
506 params_vec.iter().map(|b| b.as_ref()).collect();
507 let n: i64 = conn.query_row(sql, params_refs.as_slice(), |r| r.get(0))?;
508 Ok(n as usize)
509}
510
511pub fn knn_search(
528 conn: &Connection,
529 embedding: &[f32],
530 namespaces: &[String],
531 memory_type: Option<&str>,
532 k: usize,
533) -> Result<Vec<(i64, f32)>, AppError> {
534 if embedding.len() != crate::constants::embedding_dim() {
535 return Err(AppError::Embedding(format!(
536 "knn_search embedding has {} dims, expected {}",
537 embedding.len(),
538 crate::constants::embedding_dim()
539 )));
540 }
541 let placeholders = (0..namespaces.len())
550 .map(|_| "?")
551 .collect::<Vec<_>>()
552 .join(",");
553 let sql = if namespaces.is_empty() {
554 "SELECT memory_id, embedding, namespace FROM memory_embeddings".to_string()
555 } else {
556 format!(
557 "SELECT memory_id, embedding, namespace FROM memory_embeddings \
558 WHERE namespace IN ({placeholders})"
559 )
560 };
561 let mut stmt = conn.prepare(&sql)?;
562 let mut raw_params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
563 for ns in namespaces {
564 raw_params.push(Box::new(ns.clone()));
565 }
566 let param_refs: Vec<&dyn rusqlite::ToSql> = raw_params.iter().map(|b| b.as_ref()).collect();
567 let rows = stmt.query_map(param_refs.as_slice(), |r| {
568 let id: i64 = r.get(0)?;
569 let bytes: Vec<u8> = r.get(1)?;
570 let ns: String = r.get(2)?;
571 Ok((id, bytes, ns))
572 })?;
573
574 let type_filter = memory_type.map(|t| t.to_string());
577 let mut candidates: Vec<(i64, f32)> = Vec::new();
578 for row in rows {
579 let (id, bytes, ns) = row?;
580 let stored = crate::embedder::bytes_to_f32(&bytes);
581 if stored.len() != embedding.len() {
582 continue;
583 }
584 let sim = crate::similarity::cosine_similarity(embedding, &stored);
585 let dist = crate::similarity::similarity_to_distance(sim);
586 if let Some(mt) = &type_filter {
587 let actual: Option<String> = conn
592 .query_row(
593 "SELECT type FROM memories WHERE id = ?1",
594 params![id],
595 |r| r.get(0),
596 )
597 .ok();
598 if actual.as_deref() != Some(mt.as_str()) {
599 continue;
600 }
601 }
602 let _ = ns; candidates.push((id, dist));
604 }
605 candidates.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
607 candidates.truncate(k);
608 Ok(candidates)
609}
610
611pub fn read_full(conn: &Connection, memory_id: i64) -> Result<Option<MemoryRow>, AppError> {
620 let mut stmt = conn.prepare_cached(
621 "SELECT id, namespace, name, type, description, body, body_hash,
622 session_id, source, metadata, created_at, updated_at, deleted_at
623 FROM memories WHERE id=?1 AND deleted_at IS NULL",
624 )?;
625 match stmt.query_row(params![memory_id], |r| {
626 Ok(MemoryRow {
627 id: r.get(0)?,
628 namespace: r.get(1)?,
629 name: r.get(2)?,
630 memory_type: r.get(3)?,
631 description: r.get(4)?,
632 body: r.get(5)?,
633 body_hash: r.get(6)?,
634 session_id: r.get(7)?,
635 source: r.get(8)?,
636 metadata: r.get(9)?,
637 created_at: r.get(10)?,
638 updated_at: r.get(11)?,
639 deleted_at: r.get(12)?,
640 })
641 }) {
642 Ok(m) => Ok(Some(m)),
643 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
644 Err(e) => Err(AppError::Database(e)),
645 }
646}
647
648pub fn list_deleted_before(
657 conn: &Connection,
658 namespace: &str,
659 before_ts: i64,
660) -> Result<Vec<i64>, AppError> {
661 let mut stmt = conn.prepare_cached(
662 "SELECT id FROM memories WHERE namespace = ?1 AND deleted_at IS NOT NULL AND deleted_at < ?2",
663 )?;
664 let ids = stmt
665 .query_map(params![namespace, before_ts], |r| r.get::<_, i64>(0))?
666 .collect::<Result<Vec<_>, _>>()?;
667 Ok(ids)
668}
669
670fn preprocess_fts_query(raw: &str) -> String {
680 const SEPARATORS: &[char] = &['-', '.', '_', '/'];
681 const FTS5_SYNTAX: &[char] = &['"', '*', '(', ')', '^', ':'];
682 const FTS5_KEYWORDS: &[&str] = &["OR", "AND", "NOT", "NEAR"];
683
684 let sanitized: String = raw.chars().filter(|c| !FTS5_SYNTAX.contains(c)).collect();
685 let trimmed = sanitized.trim();
686 if trimmed.is_empty() {
687 return String::new();
688 }
689
690 let is_fts_keyword = |t: &str| FTS5_KEYWORDS.iter().any(|kw| kw.eq_ignore_ascii_case(t));
691
692 if !trimmed.chars().any(|c| SEPARATORS.contains(&c)) {
693 return trimmed
694 .split_whitespace()
695 .filter(|t| !is_fts_keyword(t))
696 .map(|t| format!("{t}*"))
697 .collect::<Vec<_>>()
698 .join(" ");
699 }
700 let tokens: Vec<&str> = trimmed
701 .split(|c: char| SEPARATORS.contains(&c) || c.is_whitespace())
702 .filter(|t| !t.is_empty() && !is_fts_keyword(t))
703 .collect();
704 if tokens.is_empty() {
705 return String::new();
706 }
707 let phrase = format!("\"{}\"", tokens.join(" "));
708 let prefix_terms: Vec<String> = tokens.iter().map(|t| format!("{t}*")).collect();
709 format!("{phrase} OR {}", prefix_terms.join(" OR "))
710}
711
712pub fn fts_search(
721 conn: &Connection,
722 query: &str,
723 namespace: &str,
724 memory_type: Option<&str>,
725 limit: usize,
726) -> Result<Vec<MemoryRow>, AppError> {
727 let fts_query = preprocess_fts_query(query);
728 if let Some(mt) = memory_type {
729 let mut stmt = conn.prepare_cached(
730 "SELECT m.id, m.namespace, m.name, m.type, m.description, m.body, m.body_hash,
731 m.session_id, m.source, m.metadata, m.created_at, m.updated_at, m.deleted_at
732 FROM fts_memories fts
733 JOIN memories m ON m.id = fts.rowid
734 WHERE fts_memories MATCH ?1 AND m.namespace = ?2 AND m.type = ?3 AND m.deleted_at IS NULL
735 ORDER BY rank LIMIT ?4",
736 )?;
737 let rows = stmt
738 .query_map(params![fts_query, namespace, mt, limit as i64], |r| {
739 Ok(MemoryRow {
740 id: r.get(0)?,
741 namespace: r.get(1)?,
742 name: r.get(2)?,
743 memory_type: r.get(3)?,
744 description: r.get(4)?,
745 body: r.get(5)?,
746 body_hash: r.get(6)?,
747 session_id: r.get(7)?,
748 source: r.get(8)?,
749 metadata: r.get(9)?,
750 created_at: r.get(10)?,
751 updated_at: r.get(11)?,
752 deleted_at: r.get(12)?,
753 })
754 })?
755 .collect::<Result<Vec<_>, _>>()?;
756 Ok(rows)
757 } else {
758 let mut stmt = conn.prepare_cached(
759 "SELECT m.id, m.namespace, m.name, m.type, m.description, m.body, m.body_hash,
760 m.session_id, m.source, m.metadata, m.created_at, m.updated_at, m.deleted_at
761 FROM fts_memories fts
762 JOIN memories m ON m.id = fts.rowid
763 WHERE fts_memories MATCH ?1 AND m.namespace = ?2 AND m.deleted_at IS NULL
764 ORDER BY rank LIMIT ?3",
765 )?;
766 let rows = stmt
767 .query_map(params![fts_query, namespace, limit as i64], |r| {
768 Ok(MemoryRow {
769 id: r.get(0)?,
770 namespace: r.get(1)?,
771 name: r.get(2)?,
772 memory_type: r.get(3)?,
773 description: r.get(4)?,
774 body: r.get(5)?,
775 body_hash: r.get(6)?,
776 session_id: r.get(7)?,
777 source: r.get(8)?,
778 metadata: r.get(9)?,
779 created_at: r.get(10)?,
780 updated_at: r.get(11)?,
781 deleted_at: r.get(12)?,
782 })
783 })?
784 .collect::<Result<Vec<_>, _>>()?;
785 Ok(rows)
786 }
787}
788
789#[allow(clippy::too_many_arguments)]
797pub fn sync_fts_after_update(
798 conn: &Connection,
799 memory_id: i64,
800 old_name: &str,
801 old_desc: &str,
802 old_body: &str,
803 new_name: &str,
804 new_desc: &str,
805 new_body: &str,
806) -> Result<(), AppError> {
807 conn.execute(
808 "INSERT INTO fts_memories(fts_memories, rowid, name, description, body)
809 VALUES('delete', ?1, ?2, ?3, ?4)",
810 params![memory_id, old_name, old_desc, old_body],
811 )?;
812 conn.execute(
813 "INSERT INTO fts_memories(rowid, name, description, body)
814 VALUES(?1, ?2, ?3, ?4)",
815 params![memory_id, new_name, new_desc, new_body],
816 )?;
817 Ok(())
818}
819
820#[cfg(test)]
821mod tests {
822 use super::*;
823 use rusqlite::Connection;
824
825 type TestResult = Result<(), Box<dyn std::error::Error>>;
826
827 fn setup_conn() -> Result<Connection, Box<dyn std::error::Error>> {
828 crate::storage::connection::register_vec_extension();
829 let mut conn = Connection::open_in_memory()?;
830 conn.execute_batch(
831 "PRAGMA foreign_keys = ON;
832 PRAGMA temp_store = MEMORY;",
833 )?;
834 crate::migrations::runner().run(&mut conn)?;
835 Ok(conn)
836 }
837
838 fn new_memory(name: &str) -> NewMemory {
839 NewMemory {
840 namespace: "global".to_string(),
841 name: name.to_string(),
842 memory_type: "user".to_string(),
843 description: "descricao de teste".to_string(),
844 body: "test memory body".to_string(),
845 body_hash: format!("hash-{name}"),
846 session_id: None,
847 source: "agent".to_string(),
848 metadata: serde_json::json!({}),
849 }
850 }
851
852 #[test]
853 fn insert_and_find_by_name_return_id() -> TestResult {
854 let conn = setup_conn()?;
855 let m = new_memory("mem-alpha");
856 let id = insert(&conn, &m)?;
857 assert!(id > 0);
858
859 let found = find_by_name(&conn, "global", "mem-alpha")?;
860 assert!(found.is_some());
861 let (found_id, _, _) = found.ok_or("mem-alpha should exist")?;
862 assert_eq!(found_id, id);
863 Ok(())
864 }
865
866 #[test]
867 fn find_by_name_returns_none_when_not_found() -> TestResult {
868 let conn = setup_conn()?;
869 let result = find_by_name(&conn, "global", "inexistente")?;
870 assert!(result.is_none());
871 Ok(())
872 }
873
874 #[test]
875 fn find_by_hash_returns_correct_id() -> TestResult {
876 let conn = setup_conn()?;
877 let m = new_memory("mem-hash");
878 let id = insert(&conn, &m)?;
879
880 let found = find_by_hash(&conn, "global", "hash-mem-hash")?;
881 assert_eq!(found, Some(id));
882 Ok(())
883 }
884
885 #[test]
886 fn find_by_hash_returns_none_when_hash_not_found() -> TestResult {
887 let conn = setup_conn()?;
888 let result = find_by_hash(&conn, "global", "hash-inexistente")?;
889 assert!(result.is_none());
890 Ok(())
891 }
892
893 #[test]
894 fn find_by_hash_ignores_different_namespace() -> TestResult {
895 let conn = setup_conn()?;
896 let m = new_memory("mem-ns");
897 insert(&conn, &m)?;
898
899 let result = find_by_hash(&conn, "outro-namespace", "hash-mem-ns")?;
900 assert!(result.is_none());
901 Ok(())
902 }
903
904 #[test]
905 fn read_by_name_returns_full_memory() -> TestResult {
906 let conn = setup_conn()?;
907 let m = new_memory("mem-read");
908 let id = insert(&conn, &m)?;
909
910 let row = read_by_name(&conn, "global", "mem-read")?.ok_or("mem-read should exist")?;
911 assert_eq!(row.id, id);
912 assert_eq!(row.name, "mem-read");
913 assert_eq!(row.memory_type, "user");
914 assert_eq!(row.body, "test memory body");
915 assert_eq!(row.namespace, "global");
916 Ok(())
917 }
918
919 #[test]
920 fn read_by_name_returns_none_for_missing() -> TestResult {
921 let conn = setup_conn()?;
922 let result = read_by_name(&conn, "global", "nao-existe")?;
923 assert!(result.is_none());
924 Ok(())
925 }
926
927 #[test]
928 fn read_full_by_id_returns_memory() -> TestResult {
929 let conn = setup_conn()?;
930 let m = new_memory("mem-full");
931 let id = insert(&conn, &m)?;
932
933 let row = read_full(&conn, id)?.ok_or("mem-full should exist")?;
934 assert_eq!(row.id, id);
935 assert_eq!(row.name, "mem-full");
936 Ok(())
937 }
938
939 #[test]
940 fn read_full_returns_none_for_missing_id() -> TestResult {
941 let conn = setup_conn()?;
942 let result = read_full(&conn, 9999)?;
943 assert!(result.is_none());
944 Ok(())
945 }
946
947 #[test]
948 fn update_without_optimism_modifies_fields() -> TestResult {
949 let conn = setup_conn()?;
950 let m = new_memory("mem-upd");
951 let id = insert(&conn, &m)?;
952
953 let mut m2 = new_memory("mem-upd");
954 m2.body = "updated body".to_string();
955 m2.body_hash = "hash-novo".to_string();
956 let ok = update(&conn, id, &m2, None)?;
957 assert!(ok);
958
959 let row = read_full(&conn, id)?.ok_or("mem-upd should exist")?;
960 assert_eq!(row.body, "updated body");
961 assert_eq!(row.body_hash, "hash-novo");
962 Ok(())
963 }
964
965 #[test]
966 fn update_with_correct_expected_updated_at_succeeds() -> TestResult {
967 let conn = setup_conn()?;
968 let m = new_memory("mem-opt");
969 let id = insert(&conn, &m)?;
970
971 let (_, updated_at, _) =
972 find_by_name(&conn, "global", "mem-opt")?.ok_or("mem-opt should exist")?;
973
974 let mut m2 = new_memory("mem-opt");
975 m2.body = "optimistic body".to_string();
976 m2.body_hash = "hash-optimistic".to_string();
977 let ok = update(&conn, id, &m2, Some(updated_at))?;
978 assert!(ok);
979
980 let row = read_full(&conn, id)?.ok_or("mem-opt should exist after update")?;
981 assert_eq!(row.body, "optimistic body");
982 Ok(())
983 }
984
985 #[test]
986 fn update_with_wrong_expected_updated_at_returns_false() -> TestResult {
987 let conn = setup_conn()?;
988 let m = new_memory("mem-conflict");
989 let id = insert(&conn, &m)?;
990
991 let mut m2 = new_memory("mem-conflict");
992 m2.body = "must not appear".to_string();
993 m2.body_hash = "hash-x".to_string();
994 let ok = update(&conn, id, &m2, Some(0))?;
995 assert!(!ok);
996
997 let row = read_full(&conn, id)?.ok_or("mem-conflict should exist")?;
998 assert_eq!(row.body, "test memory body");
999 Ok(())
1000 }
1001
1002 #[test]
1003 fn update_missing_id_returns_false() -> TestResult {
1004 let conn = setup_conn()?;
1005 let m = new_memory("fantasma");
1006 let ok = update(&conn, 9999, &m, None)?;
1007 assert!(!ok);
1008 Ok(())
1009 }
1010
1011 #[test]
1012 fn soft_delete_marks_deleted_at() -> TestResult {
1013 let conn = setup_conn()?;
1014 let m = new_memory("mem-del");
1015 insert(&conn, &m)?;
1016
1017 let ok = soft_delete(&conn, "global", "mem-del")?;
1018 assert!(ok);
1019
1020 let result = find_by_name(&conn, "global", "mem-del")?;
1021 assert!(result.is_none());
1022
1023 let result_read = read_by_name(&conn, "global", "mem-del")?;
1024 assert!(result_read.is_none());
1025 Ok(())
1026 }
1027
1028 #[test]
1029 fn soft_delete_returns_false_when_not_found() -> TestResult {
1030 let conn = setup_conn()?;
1031 let ok = soft_delete(&conn, "global", "nao-existe")?;
1032 assert!(!ok);
1033 Ok(())
1034 }
1035
1036 #[test]
1037 fn double_soft_delete_returns_false_on_second_call() -> TestResult {
1038 let conn = setup_conn()?;
1039 let m = new_memory("mem-del2");
1040 insert(&conn, &m)?;
1041
1042 soft_delete(&conn, "global", "mem-del2")?;
1043 let ok = soft_delete(&conn, "global", "mem-del2")?;
1044 assert!(!ok);
1045 Ok(())
1046 }
1047
1048 #[test]
1049 fn list_returns_memories_from_namespace() -> TestResult {
1050 let conn = setup_conn()?;
1051 insert(&conn, &new_memory("mem-list-a"))?;
1052 insert(&conn, &new_memory("mem-list-b"))?;
1053
1054 let rows = list(&conn, "global", None, 10, 0, false)?;
1055 assert!(rows.len() >= 2);
1056 let nomes: Vec<_> = rows.iter().map(|r| r.name.as_str()).collect();
1057 assert!(nomes.contains(&"mem-list-a"));
1058 assert!(nomes.contains(&"mem-list-b"));
1059 Ok(())
1060 }
1061
1062 #[test]
1063 fn list_with_type_filter_returns_only_correct_type() -> TestResult {
1064 let conn = setup_conn()?;
1065 insert(&conn, &new_memory("mem-user"))?;
1066
1067 let mut m2 = new_memory("mem-feedback");
1068 m2.memory_type = "feedback".to_string();
1069 insert(&conn, &m2)?;
1070
1071 let rows_user = list(&conn, "global", Some("user"), 10, 0, false)?;
1072 assert!(rows_user.iter().all(|r| r.memory_type == "user"));
1073
1074 let rows_fb = list(&conn, "global", Some("feedback"), 10, 0, false)?;
1075 assert!(rows_fb.iter().all(|r| r.memory_type == "feedback"));
1076 Ok(())
1077 }
1078
1079 #[test]
1080 fn list_exclui_soft_deleted() -> TestResult {
1081 let conn = setup_conn()?;
1082 let m = new_memory("mem-excluida");
1083 insert(&conn, &m)?;
1084 soft_delete(&conn, "global", "mem-excluida")?;
1085
1086 let rows = list(&conn, "global", None, 10, 0, false)?;
1087 assert!(rows.iter().all(|r| r.name != "mem-excluida"));
1088 Ok(())
1089 }
1090
1091 #[test]
1092 fn list_pagination_works() -> TestResult {
1093 let conn = setup_conn()?;
1094 for i in 0..5 {
1095 insert(&conn, &new_memory(&format!("mem-pag-{i}")))?;
1096 }
1097
1098 let pagina1 = list(&conn, "global", None, 2, 0, false)?;
1099 let pagina2 = list(&conn, "global", None, 2, 2, false)?;
1100 assert!(pagina1.len() <= 2);
1101 assert!(pagina2.len() <= 2);
1102 if !pagina1.is_empty() && !pagina2.is_empty() {
1103 assert_ne!(pagina1[0].id, pagina2[0].id);
1104 }
1105 Ok(())
1106 }
1107
1108 #[test]
1109 #[serial_test::serial(env)]
1110 fn upsert_vec_and_delete_vec_work() -> TestResult {
1111 let conn = setup_conn()?;
1112 let m = new_memory("mem-vec");
1113 let id = insert(&conn, &m)?;
1114
1115 let embedding: Vec<f32> = vec![0.1; crate::constants::embedding_dim()];
1116 upsert_vec(
1117 &conn, id, "global", "user", &embedding, "mem-vec", "snippet",
1118 )?;
1119
1120 let count: i64 = conn.query_row(
1121 "SELECT COUNT(*) FROM memory_embeddings WHERE memory_id = ?1",
1122 params![id],
1123 |r| r.get(0),
1124 )?;
1125 assert_eq!(count, 1);
1126
1127 delete_vec(&conn, id)?;
1128
1129 let count_after: i64 = conn.query_row(
1130 "SELECT COUNT(*) FROM memory_embeddings WHERE memory_id = ?1",
1131 params![id],
1132 |r| r.get(0),
1133 )?;
1134 assert_eq!(count_after, 0);
1135 Ok(())
1136 }
1137
1138 #[test]
1139 #[serial_test::serial(env)]
1140 fn upsert_vec_replaces_existing_vector() -> TestResult {
1141 let conn = setup_conn()?;
1142 let m = new_memory("mem-vec-upsert");
1143 let id = insert(&conn, &m)?;
1144
1145 let emb1: Vec<f32> = vec![0.1; crate::constants::embedding_dim()];
1146 upsert_vec(&conn, id, "global", "user", &emb1, "mem-vec-upsert", "s1")?;
1147
1148 let emb2: Vec<f32> = vec![0.9; crate::constants::embedding_dim()];
1149 upsert_vec(&conn, id, "global", "user", &emb2, "mem-vec-upsert", "s2")?;
1150
1151 let count: i64 = conn.query_row(
1152 "SELECT COUNT(*) FROM memory_embeddings WHERE memory_id = ?1",
1153 params![id],
1154 |r| r.get(0),
1155 )?;
1156 assert_eq!(count, 1);
1157 Ok(())
1158 }
1159
1160 #[test]
1161 #[serial_test::serial(env)]
1162 fn knn_search_returns_results_by_distance() -> TestResult {
1163 let conn = setup_conn()?;
1164
1165 let ma = new_memory("mem-knn-a");
1167 let id_a = insert(&conn, &ma)?;
1168 let emb_a: Vec<f32> = vec![1.0; crate::constants::embedding_dim()];
1169 upsert_vec(&conn, id_a, "global", "user", &emb_a, "mem-knn-a", "s")?;
1170
1171 let mb = new_memory("mem-knn-b");
1173 let id_b = insert(&conn, &mb)?;
1174 let emb_b: Vec<f32> = vec![-1.0; crate::constants::embedding_dim()];
1175 upsert_vec(&conn, id_b, "global", "user", &emb_b, "mem-knn-b", "s")?;
1176
1177 let query: Vec<f32> = vec![1.0; crate::constants::embedding_dim()];
1178 let results = knn_search(&conn, &query, &["global".to_string()], None, 2)?;
1179 assert!(!results.is_empty());
1180 assert_eq!(results[0].0, id_a);
1181 Ok(())
1182 }
1183
1184 #[test]
1185 #[serial_test::serial(env)]
1186 fn knn_search_with_type_filter_restricts_result() -> TestResult {
1187 let conn = setup_conn()?;
1188
1189 let ma = new_memory("mem-knn-tipo-user");
1190 let id_a = insert(&conn, &ma)?;
1191 let emb: Vec<f32> = vec![1.0; crate::constants::embedding_dim()];
1192 upsert_vec(
1193 &conn,
1194 id_a,
1195 "global",
1196 "user",
1197 &emb,
1198 "mem-knn-tipo-user",
1199 "s",
1200 )?;
1201
1202 let mut mb = new_memory("mem-knn-tipo-fb");
1203 mb.memory_type = "feedback".to_string();
1204 let id_b = insert(&conn, &mb)?;
1205 upsert_vec(
1206 &conn,
1207 id_b,
1208 "global",
1209 "feedback",
1210 &emb,
1211 "mem-knn-tipo-fb",
1212 "s",
1213 )?;
1214
1215 let query: Vec<f32> = vec![1.0; crate::constants::embedding_dim()];
1216 let results_user = knn_search(&conn, &query, &["global".to_string()], Some("user"), 5)?;
1217 assert!(results_user.iter().all(|(id, _)| *id == id_a));
1218
1219 let results_fb = knn_search(&conn, &query, &["global".to_string()], Some("feedback"), 5)?;
1220 assert!(results_fb.iter().all(|(id, _)| *id == id_b));
1221 Ok(())
1222 }
1223
1224 #[test]
1225 fn fts_search_finds_by_prefix_in_body() -> TestResult {
1226 let conn = setup_conn()?;
1227 let mut m = new_memory("mem-fts");
1228 m.body = "linguagem de programacao rust".to_string();
1229 insert(&conn, &m)?;
1230
1231 conn.execute_batch(
1232 "INSERT INTO fts_memories(rowid, name, description, body)
1233 SELECT id, name, description, body FROM memories WHERE deleted_at IS NULL",
1234 )?;
1235
1236 let rows = fts_search(&conn, "programacao", "global", None, 10)?;
1237 assert!(!rows.is_empty());
1238 assert!(rows.iter().any(|r| r.name == "mem-fts"));
1239 Ok(())
1240 }
1241
1242 #[test]
1243 fn fts_search_with_type_filter() -> TestResult {
1244 let conn = setup_conn()?;
1245 let mut m = new_memory("mem-fts-tipo");
1246 m.body = "linguagem especial para filtro".to_string();
1247 insert(&conn, &m)?;
1248
1249 let mut m2 = new_memory("mem-fts-feedback");
1250 m2.memory_type = "feedback".to_string();
1251 m2.body = "linguagem especial para filtro".to_string();
1252 insert(&conn, &m2)?;
1253
1254 conn.execute_batch(
1255 "INSERT INTO fts_memories(rowid, name, description, body)
1256 SELECT id, name, description, body FROM memories WHERE deleted_at IS NULL",
1257 )?;
1258
1259 let rows_user = fts_search(&conn, "especial", "global", Some("user"), 10)?;
1260 assert!(rows_user.iter().all(|r| r.memory_type == "user"));
1261
1262 let rows_fb = fts_search(&conn, "especial", "global", Some("feedback"), 10)?;
1263 assert!(rows_fb.iter().all(|r| r.memory_type == "feedback"));
1264 Ok(())
1265 }
1266
1267 #[test]
1268 fn fts_search_excludes_deleted() -> TestResult {
1269 let conn = setup_conn()?;
1270 let mut m = new_memory("mem-fts-del");
1271 m.body = "deleted fts content".to_string();
1272 insert(&conn, &m)?;
1273
1274 conn.execute_batch(
1275 "INSERT INTO fts_memories(rowid, name, description, body)
1276 SELECT id, name, description, body FROM memories WHERE deleted_at IS NULL",
1277 )?;
1278
1279 soft_delete(&conn, "global", "mem-fts-del")?;
1280
1281 let rows = fts_search(&conn, "deleted", "global", None, 10)?;
1282 assert!(rows.iter().all(|r| r.name != "mem-fts-del"));
1283 Ok(())
1284 }
1285
1286 #[test]
1287 fn list_deleted_before_returns_correct_ids() -> TestResult {
1288 let conn = setup_conn()?;
1289 let m = new_memory("mem-purge");
1290 insert(&conn, &m)?;
1291 soft_delete(&conn, "global", "mem-purge")?;
1292
1293 let ids = list_deleted_before(&conn, "global", i64::MAX)?;
1294 assert!(!ids.is_empty());
1295
1296 let ids_antes = list_deleted_before(&conn, "global", 0)?;
1297 assert!(ids_antes.is_empty());
1298 Ok(())
1299 }
1300
1301 #[test]
1302 fn find_by_name_returns_correct_max_version() -> TestResult {
1303 let conn = setup_conn()?;
1304 let m = new_memory("mem-ver");
1305 let id = insert(&conn, &m)?;
1306
1307 let (_, _, v0) = find_by_name(&conn, "global", "mem-ver")?.ok_or("mem-ver should exist")?;
1308 assert_eq!(v0, 0);
1309
1310 conn.execute(
1311 "INSERT INTO memory_versions (memory_id, version, name, type, description, body, metadata, change_reason)
1312 VALUES (?1, 1, 'mem-ver', 'user', 'desc', 'body', '{}', 'create')",
1313 params![id],
1314 )?;
1315
1316 let (_, _, v1) =
1317 find_by_name(&conn, "global", "mem-ver")?.ok_or("mem-ver should exist after insert")?;
1318 assert_eq!(v1, 1);
1319 Ok(())
1320 }
1321
1322 #[test]
1323 fn insert_com_metadata_json() -> TestResult {
1324 let conn = setup_conn()?;
1325 let mut m = new_memory("mem-meta");
1326 m.metadata = serde_json::json!({"chave": "valor", "numero": 42});
1327 let id = insert(&conn, &m)?;
1328
1329 let row = read_full(&conn, id)?.ok_or("mem-meta should exist")?;
1330 let meta: serde_json::Value = serde_json::from_str(&row.metadata)?;
1331 assert_eq!(meta["chave"], "valor");
1332 assert_eq!(meta["numero"], 42);
1333 Ok(())
1334 }
1335
1336 #[test]
1337 fn insert_com_session_id() -> TestResult {
1338 let conn = setup_conn()?;
1339 let mut m = new_memory("mem-session");
1340 m.session_id = Some("sessao-xyz".to_string());
1341 let id = insert(&conn, &m)?;
1342
1343 let row = read_full(&conn, id)?.ok_or("mem-session should exist")?;
1344 assert_eq!(row.session_id, Some("sessao-xyz".to_string()));
1345 Ok(())
1346 }
1347
1348 #[test]
1349 fn delete_vec_for_nonexistent_id_does_not_fail() -> TestResult {
1350 let conn = setup_conn()?;
1351 let result = delete_vec(&conn, 99999);
1352 assert!(result.is_ok());
1353 Ok(())
1354 }
1355
1356 #[test]
1357 fn preprocess_fts_query_no_separators() {
1358 assert_eq!(preprocess_fts_query("hello"), "hello*");
1359 assert_eq!(preprocess_fts_query("hello world"), "hello* world*");
1360 }
1361
1362 #[test]
1363 fn preprocess_fts_query_with_hyphens() {
1364 let result = preprocess_fts_query("graphrag-precompact");
1365 assert!(result.contains("\"graphrag precompact\""));
1366 assert!(result.contains("graphrag*"));
1367 assert!(result.contains("precompact*"));
1368 }
1369
1370 #[test]
1371 fn preprocess_fts_query_with_dots() {
1372 let result = preprocess_fts_query("v1.0.44");
1373 assert!(result.contains("\"v1 0 44\""));
1374 assert!(result.contains("v1*"));
1375 assert!(result.contains("44*"));
1376 }
1377
1378 #[test]
1379 fn preprocess_fts_query_with_mixed_separators() {
1380 let result = preprocess_fts_query("graphrag-precompact.sh");
1381 assert!(result.contains("\"graphrag precompact sh\""));
1382 assert!(result.contains("graphrag*"));
1383 }
1384
1385 #[test]
1386 fn preprocess_fts_query_empty_and_whitespace() {
1387 assert_eq!(preprocess_fts_query(""), "");
1388 assert_eq!(preprocess_fts_query(" "), "");
1389 }
1390
1391 #[test]
1392 fn preprocess_fts_query_strips_quotes() {
1393 let result = preprocess_fts_query(r#"hello "world"#);
1394 assert!(result.contains("hello*"));
1395 assert!(result.contains("world*"));
1396 }
1397
1398 #[test]
1399 fn preprocess_fts_query_strips_asterisks() {
1400 assert_eq!(preprocess_fts_query("test*"), "test*");
1401 }
1402
1403 #[test]
1404 fn preprocess_fts_query_strips_parens() {
1405 let result = preprocess_fts_query("(hello)");
1406 assert!(result.contains("hello*"));
1407 assert!(!result.contains('('));
1408 }
1409
1410 #[test]
1411 fn preprocess_fts_query_filters_fts_keywords() {
1412 let result = preprocess_fts_query("foo OR bar");
1413 assert!(result.contains("foo*"));
1414 assert!(result.contains("bar*"));
1415 assert!(!result.contains("OR*"));
1416 }
1417
1418 #[test]
1419 fn preprocess_fts_query_only_fts_keywords() {
1420 assert_eq!(preprocess_fts_query("OR AND NOT"), "");
1421 }
1422
1423 #[test]
1424 fn preprocess_fts_query_keywords_with_separators() {
1425 let result = preprocess_fts_query("hello-OR-world");
1426 assert!(result.contains("hello*"));
1427 assert!(result.contains("world*"));
1428 assert!(!result.contains("OR*"));
1429 }
1430
1431 #[test]
1432 fn fts_search_finds_compound_term_with_hyphen() -> TestResult {
1433 let conn = setup_conn()?;
1434 let mut m = new_memory("mem-compound");
1435 m.body = "the graphrag-precompact script runs daily".to_string();
1436 insert(&conn, &m)?;
1437 conn.execute_batch(
1438 "INSERT INTO fts_memories(rowid, name, description, body)
1439 SELECT id, name, description, body FROM memories WHERE deleted_at IS NULL",
1440 )?;
1441 let rows = fts_search(&conn, "graphrag-precompact", "global", None, 10)?;
1442 assert!(!rows.is_empty(), "should find compound hyphenated term");
1443 Ok(())
1444 }
1445
1446 #[test]
1447 fn find_by_name_any_state_returns_deleted_flag() -> TestResult {
1448 let conn = setup_conn()?;
1449 let m = new_memory("mem-soft-del");
1450 let id = insert(&conn, &m)?;
1451 conn.execute(
1452 "UPDATE memories SET deleted_at = unixepoch() WHERE id = ?1",
1453 rusqlite::params![id],
1454 )?;
1455 let result = find_by_name_any_state(&conn, "global", "mem-soft-del")?;
1456 assert_eq!(result, Some((id, true)));
1457 Ok(())
1458 }
1459
1460 #[test]
1461 fn find_by_name_any_state_returns_not_deleted() -> TestResult {
1462 let conn = setup_conn()?;
1463 let m = new_memory("mem-active");
1464 let id = insert(&conn, &m)?;
1465 let result = find_by_name_any_state(&conn, "global", "mem-active")?;
1466 assert_eq!(result, Some((id, false)));
1467 Ok(())
1468 }
1469
1470 #[test]
1471 fn find_by_name_any_state_returns_none_when_absent() -> TestResult {
1472 let conn = setup_conn()?;
1473 let result = find_by_name_any_state(&conn, "global", "does-not-exist")?;
1474 assert!(result.is_none());
1475 Ok(())
1476 }
1477
1478 #[test]
1479 fn clear_deleted_at_restores_memory() -> TestResult {
1480 let conn = setup_conn()?;
1481 let m = new_memory("mem-restore");
1482 let id = insert(&conn, &m)?;
1483 conn.execute(
1484 "UPDATE memories SET deleted_at = unixepoch() WHERE id = ?1",
1485 rusqlite::params![id],
1486 )?;
1487 assert!(find_by_name(&conn, "global", "mem-restore")?.is_none());
1489 clear_deleted_at(&conn, id)?;
1490 let found = find_by_name(&conn, "global", "mem-restore")?;
1492 assert!(found.is_some());
1493 assert_eq!(found.unwrap().0, id);
1494 Ok(())
1495 }
1496}