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 knn_search(
489 conn: &Connection,
490 embedding: &[f32],
491 namespaces: &[String],
492 memory_type: Option<&str>,
493 k: usize,
494) -> Result<Vec<(i64, f32)>, AppError> {
495 if embedding.len() != crate::constants::EMBEDDING_DIM {
496 return Err(AppError::Embedding(format!(
497 "knn_search embedding has {} dims, expected {}",
498 embedding.len(),
499 crate::constants::EMBEDDING_DIM
500 )));
501 }
502 let placeholders = (0..namespaces.len())
511 .map(|_| "?")
512 .collect::<Vec<_>>()
513 .join(",");
514 let sql = if namespaces.is_empty() {
515 "SELECT memory_id, embedding, namespace FROM memory_embeddings".to_string()
516 } else {
517 format!(
518 "SELECT memory_id, embedding, namespace FROM memory_embeddings \
519 WHERE namespace IN ({placeholders})"
520 )
521 };
522 let mut stmt = conn.prepare(&sql)?;
523 let mut raw_params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
524 for ns in namespaces {
525 raw_params.push(Box::new(ns.clone()));
526 }
527 let param_refs: Vec<&dyn rusqlite::ToSql> = raw_params.iter().map(|b| b.as_ref()).collect();
528 let rows = stmt.query_map(param_refs.as_slice(), |r| {
529 let id: i64 = r.get(0)?;
530 let bytes: Vec<u8> = r.get(1)?;
531 let ns: String = r.get(2)?;
532 Ok((id, bytes, ns))
533 })?;
534
535 let type_filter = memory_type.map(|t| t.to_string());
538 let mut candidates: Vec<(i64, f32)> = Vec::new();
539 for row in rows {
540 let (id, bytes, ns) = row?;
541 let stored = crate::embedder::bytes_to_f32(&bytes);
542 if stored.len() != embedding.len() {
543 continue;
544 }
545 let sim = crate::similarity::cosine_similarity(embedding, &stored);
546 let dist = crate::similarity::similarity_to_distance(sim);
547 if let Some(mt) = &type_filter {
548 let actual: Option<String> = conn
553 .query_row(
554 "SELECT type FROM memories WHERE id = ?1",
555 params![id],
556 |r| r.get(0),
557 )
558 .ok();
559 if actual.as_deref() != Some(mt.as_str()) {
560 continue;
561 }
562 }
563 let _ = ns; candidates.push((id, dist));
565 }
566 candidates.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
568 candidates.truncate(k);
569 Ok(candidates)
570}
571
572pub fn read_full(conn: &Connection, memory_id: i64) -> Result<Option<MemoryRow>, AppError> {
581 let mut stmt = conn.prepare_cached(
582 "SELECT id, namespace, name, type, description, body, body_hash,
583 session_id, source, metadata, created_at, updated_at, deleted_at
584 FROM memories WHERE id=?1 AND deleted_at IS NULL",
585 )?;
586 match stmt.query_row(params![memory_id], |r| {
587 Ok(MemoryRow {
588 id: r.get(0)?,
589 namespace: r.get(1)?,
590 name: r.get(2)?,
591 memory_type: r.get(3)?,
592 description: r.get(4)?,
593 body: r.get(5)?,
594 body_hash: r.get(6)?,
595 session_id: r.get(7)?,
596 source: r.get(8)?,
597 metadata: r.get(9)?,
598 created_at: r.get(10)?,
599 updated_at: r.get(11)?,
600 deleted_at: r.get(12)?,
601 })
602 }) {
603 Ok(m) => Ok(Some(m)),
604 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
605 Err(e) => Err(AppError::Database(e)),
606 }
607}
608
609pub fn list_deleted_before(
618 conn: &Connection,
619 namespace: &str,
620 before_ts: i64,
621) -> Result<Vec<i64>, AppError> {
622 let mut stmt = conn.prepare_cached(
623 "SELECT id FROM memories WHERE namespace = ?1 AND deleted_at IS NOT NULL AND deleted_at < ?2",
624 )?;
625 let ids = stmt
626 .query_map(params![namespace, before_ts], |r| r.get::<_, i64>(0))?
627 .collect::<Result<Vec<_>, _>>()?;
628 Ok(ids)
629}
630
631fn preprocess_fts_query(raw: &str) -> String {
641 const SEPARATORS: &[char] = &['-', '.', '_', '/'];
642 const FTS5_SYNTAX: &[char] = &['"', '*', '(', ')', '^', ':'];
643 const FTS5_KEYWORDS: &[&str] = &["OR", "AND", "NOT", "NEAR"];
644
645 let sanitized: String = raw.chars().filter(|c| !FTS5_SYNTAX.contains(c)).collect();
646 let trimmed = sanitized.trim();
647 if trimmed.is_empty() {
648 return String::new();
649 }
650
651 let is_fts_keyword = |t: &str| FTS5_KEYWORDS.iter().any(|kw| kw.eq_ignore_ascii_case(t));
652
653 if !trimmed.chars().any(|c| SEPARATORS.contains(&c)) {
654 return trimmed
655 .split_whitespace()
656 .filter(|t| !is_fts_keyword(t))
657 .map(|t| format!("{t}*"))
658 .collect::<Vec<_>>()
659 .join(" ");
660 }
661 let tokens: Vec<&str> = trimmed
662 .split(|c: char| SEPARATORS.contains(&c) || c.is_whitespace())
663 .filter(|t| !t.is_empty() && !is_fts_keyword(t))
664 .collect();
665 if tokens.is_empty() {
666 return String::new();
667 }
668 let phrase = format!("\"{}\"", tokens.join(" "));
669 let prefix_terms: Vec<String> = tokens.iter().map(|t| format!("{t}*")).collect();
670 format!("{phrase} OR {}", prefix_terms.join(" OR "))
671}
672
673pub fn fts_search(
682 conn: &Connection,
683 query: &str,
684 namespace: &str,
685 memory_type: Option<&str>,
686 limit: usize,
687) -> Result<Vec<MemoryRow>, AppError> {
688 let fts_query = preprocess_fts_query(query);
689 if let Some(mt) = memory_type {
690 let mut stmt = conn.prepare_cached(
691 "SELECT m.id, m.namespace, m.name, m.type, m.description, m.body, m.body_hash,
692 m.session_id, m.source, m.metadata, m.created_at, m.updated_at, m.deleted_at
693 FROM fts_memories fts
694 JOIN memories m ON m.id = fts.rowid
695 WHERE fts_memories MATCH ?1 AND m.namespace = ?2 AND m.type = ?3 AND m.deleted_at IS NULL
696 ORDER BY rank LIMIT ?4",
697 )?;
698 let rows = stmt
699 .query_map(params![fts_query, namespace, mt, limit as i64], |r| {
700 Ok(MemoryRow {
701 id: r.get(0)?,
702 namespace: r.get(1)?,
703 name: r.get(2)?,
704 memory_type: r.get(3)?,
705 description: r.get(4)?,
706 body: r.get(5)?,
707 body_hash: r.get(6)?,
708 session_id: r.get(7)?,
709 source: r.get(8)?,
710 metadata: r.get(9)?,
711 created_at: r.get(10)?,
712 updated_at: r.get(11)?,
713 deleted_at: r.get(12)?,
714 })
715 })?
716 .collect::<Result<Vec<_>, _>>()?;
717 Ok(rows)
718 } else {
719 let mut stmt = conn.prepare_cached(
720 "SELECT m.id, m.namespace, m.name, m.type, m.description, m.body, m.body_hash,
721 m.session_id, m.source, m.metadata, m.created_at, m.updated_at, m.deleted_at
722 FROM fts_memories fts
723 JOIN memories m ON m.id = fts.rowid
724 WHERE fts_memories MATCH ?1 AND m.namespace = ?2 AND m.deleted_at IS NULL
725 ORDER BY rank LIMIT ?3",
726 )?;
727 let rows = stmt
728 .query_map(params![fts_query, namespace, limit as i64], |r| {
729 Ok(MemoryRow {
730 id: r.get(0)?,
731 namespace: r.get(1)?,
732 name: r.get(2)?,
733 memory_type: r.get(3)?,
734 description: r.get(4)?,
735 body: r.get(5)?,
736 body_hash: r.get(6)?,
737 session_id: r.get(7)?,
738 source: r.get(8)?,
739 metadata: r.get(9)?,
740 created_at: r.get(10)?,
741 updated_at: r.get(11)?,
742 deleted_at: r.get(12)?,
743 })
744 })?
745 .collect::<Result<Vec<_>, _>>()?;
746 Ok(rows)
747 }
748}
749
750#[allow(clippy::too_many_arguments)]
758pub fn sync_fts_after_update(
759 conn: &Connection,
760 memory_id: i64,
761 old_name: &str,
762 old_desc: &str,
763 old_body: &str,
764 new_name: &str,
765 new_desc: &str,
766 new_body: &str,
767) -> Result<(), AppError> {
768 conn.execute(
769 "INSERT INTO fts_memories(fts_memories, rowid, name, description, body)
770 VALUES('delete', ?1, ?2, ?3, ?4)",
771 params![memory_id, old_name, old_desc, old_body],
772 )?;
773 conn.execute(
774 "INSERT INTO fts_memories(rowid, name, description, body)
775 VALUES(?1, ?2, ?3, ?4)",
776 params![memory_id, new_name, new_desc, new_body],
777 )?;
778 Ok(())
779}
780
781#[cfg(test)]
782mod tests {
783 use super::*;
784 use rusqlite::Connection;
785
786 type TestResult = Result<(), Box<dyn std::error::Error>>;
787
788 fn setup_conn() -> Result<Connection, Box<dyn std::error::Error>> {
789 crate::storage::connection::register_vec_extension();
790 let mut conn = Connection::open_in_memory()?;
791 conn.execute_batch(
792 "PRAGMA foreign_keys = ON;
793 PRAGMA temp_store = MEMORY;",
794 )?;
795 crate::migrations::runner().run(&mut conn)?;
796 Ok(conn)
797 }
798
799 fn new_memory(name: &str) -> NewMemory {
800 NewMemory {
801 namespace: "global".to_string(),
802 name: name.to_string(),
803 memory_type: "user".to_string(),
804 description: "descricao de teste".to_string(),
805 body: "test memory body".to_string(),
806 body_hash: format!("hash-{name}"),
807 session_id: None,
808 source: "agent".to_string(),
809 metadata: serde_json::json!({}),
810 }
811 }
812
813 #[test]
814 fn insert_and_find_by_name_return_id() -> TestResult {
815 let conn = setup_conn()?;
816 let m = new_memory("mem-alpha");
817 let id = insert(&conn, &m)?;
818 assert!(id > 0);
819
820 let found = find_by_name(&conn, "global", "mem-alpha")?;
821 assert!(found.is_some());
822 let (found_id, _, _) = found.ok_or("mem-alpha should exist")?;
823 assert_eq!(found_id, id);
824 Ok(())
825 }
826
827 #[test]
828 fn find_by_name_returns_none_when_not_found() -> TestResult {
829 let conn = setup_conn()?;
830 let result = find_by_name(&conn, "global", "inexistente")?;
831 assert!(result.is_none());
832 Ok(())
833 }
834
835 #[test]
836 fn find_by_hash_returns_correct_id() -> TestResult {
837 let conn = setup_conn()?;
838 let m = new_memory("mem-hash");
839 let id = insert(&conn, &m)?;
840
841 let found = find_by_hash(&conn, "global", "hash-mem-hash")?;
842 assert_eq!(found, Some(id));
843 Ok(())
844 }
845
846 #[test]
847 fn find_by_hash_returns_none_when_hash_not_found() -> TestResult {
848 let conn = setup_conn()?;
849 let result = find_by_hash(&conn, "global", "hash-inexistente")?;
850 assert!(result.is_none());
851 Ok(())
852 }
853
854 #[test]
855 fn find_by_hash_ignores_different_namespace() -> TestResult {
856 let conn = setup_conn()?;
857 let m = new_memory("mem-ns");
858 insert(&conn, &m)?;
859
860 let result = find_by_hash(&conn, "outro-namespace", "hash-mem-ns")?;
861 assert!(result.is_none());
862 Ok(())
863 }
864
865 #[test]
866 fn read_by_name_returns_full_memory() -> TestResult {
867 let conn = setup_conn()?;
868 let m = new_memory("mem-read");
869 let id = insert(&conn, &m)?;
870
871 let row = read_by_name(&conn, "global", "mem-read")?.ok_or("mem-read should exist")?;
872 assert_eq!(row.id, id);
873 assert_eq!(row.name, "mem-read");
874 assert_eq!(row.memory_type, "user");
875 assert_eq!(row.body, "test memory body");
876 assert_eq!(row.namespace, "global");
877 Ok(())
878 }
879
880 #[test]
881 fn read_by_name_returns_none_for_missing() -> TestResult {
882 let conn = setup_conn()?;
883 let result = read_by_name(&conn, "global", "nao-existe")?;
884 assert!(result.is_none());
885 Ok(())
886 }
887
888 #[test]
889 fn read_full_by_id_returns_memory() -> TestResult {
890 let conn = setup_conn()?;
891 let m = new_memory("mem-full");
892 let id = insert(&conn, &m)?;
893
894 let row = read_full(&conn, id)?.ok_or("mem-full should exist")?;
895 assert_eq!(row.id, id);
896 assert_eq!(row.name, "mem-full");
897 Ok(())
898 }
899
900 #[test]
901 fn read_full_returns_none_for_missing_id() -> TestResult {
902 let conn = setup_conn()?;
903 let result = read_full(&conn, 9999)?;
904 assert!(result.is_none());
905 Ok(())
906 }
907
908 #[test]
909 fn update_without_optimism_modifies_fields() -> TestResult {
910 let conn = setup_conn()?;
911 let m = new_memory("mem-upd");
912 let id = insert(&conn, &m)?;
913
914 let mut m2 = new_memory("mem-upd");
915 m2.body = "updated body".to_string();
916 m2.body_hash = "hash-novo".to_string();
917 let ok = update(&conn, id, &m2, None)?;
918 assert!(ok);
919
920 let row = read_full(&conn, id)?.ok_or("mem-upd should exist")?;
921 assert_eq!(row.body, "updated body");
922 assert_eq!(row.body_hash, "hash-novo");
923 Ok(())
924 }
925
926 #[test]
927 fn update_with_correct_expected_updated_at_succeeds() -> TestResult {
928 let conn = setup_conn()?;
929 let m = new_memory("mem-opt");
930 let id = insert(&conn, &m)?;
931
932 let (_, updated_at, _) =
933 find_by_name(&conn, "global", "mem-opt")?.ok_or("mem-opt should exist")?;
934
935 let mut m2 = new_memory("mem-opt");
936 m2.body = "optimistic body".to_string();
937 m2.body_hash = "hash-optimistic".to_string();
938 let ok = update(&conn, id, &m2, Some(updated_at))?;
939 assert!(ok);
940
941 let row = read_full(&conn, id)?.ok_or("mem-opt should exist after update")?;
942 assert_eq!(row.body, "optimistic body");
943 Ok(())
944 }
945
946 #[test]
947 fn update_with_wrong_expected_updated_at_returns_false() -> TestResult {
948 let conn = setup_conn()?;
949 let m = new_memory("mem-conflict");
950 let id = insert(&conn, &m)?;
951
952 let mut m2 = new_memory("mem-conflict");
953 m2.body = "must not appear".to_string();
954 m2.body_hash = "hash-x".to_string();
955 let ok = update(&conn, id, &m2, Some(0))?;
956 assert!(!ok);
957
958 let row = read_full(&conn, id)?.ok_or("mem-conflict should exist")?;
959 assert_eq!(row.body, "test memory body");
960 Ok(())
961 }
962
963 #[test]
964 fn update_missing_id_returns_false() -> TestResult {
965 let conn = setup_conn()?;
966 let m = new_memory("fantasma");
967 let ok = update(&conn, 9999, &m, None)?;
968 assert!(!ok);
969 Ok(())
970 }
971
972 #[test]
973 fn soft_delete_marks_deleted_at() -> TestResult {
974 let conn = setup_conn()?;
975 let m = new_memory("mem-del");
976 insert(&conn, &m)?;
977
978 let ok = soft_delete(&conn, "global", "mem-del")?;
979 assert!(ok);
980
981 let result = find_by_name(&conn, "global", "mem-del")?;
982 assert!(result.is_none());
983
984 let result_read = read_by_name(&conn, "global", "mem-del")?;
985 assert!(result_read.is_none());
986 Ok(())
987 }
988
989 #[test]
990 fn soft_delete_returns_false_when_not_found() -> TestResult {
991 let conn = setup_conn()?;
992 let ok = soft_delete(&conn, "global", "nao-existe")?;
993 assert!(!ok);
994 Ok(())
995 }
996
997 #[test]
998 fn double_soft_delete_returns_false_on_second_call() -> TestResult {
999 let conn = setup_conn()?;
1000 let m = new_memory("mem-del2");
1001 insert(&conn, &m)?;
1002
1003 soft_delete(&conn, "global", "mem-del2")?;
1004 let ok = soft_delete(&conn, "global", "mem-del2")?;
1005 assert!(!ok);
1006 Ok(())
1007 }
1008
1009 #[test]
1010 fn list_returns_memories_from_namespace() -> TestResult {
1011 let conn = setup_conn()?;
1012 insert(&conn, &new_memory("mem-list-a"))?;
1013 insert(&conn, &new_memory("mem-list-b"))?;
1014
1015 let rows = list(&conn, "global", None, 10, 0, false)?;
1016 assert!(rows.len() >= 2);
1017 let nomes: Vec<_> = rows.iter().map(|r| r.name.as_str()).collect();
1018 assert!(nomes.contains(&"mem-list-a"));
1019 assert!(nomes.contains(&"mem-list-b"));
1020 Ok(())
1021 }
1022
1023 #[test]
1024 fn list_with_type_filter_returns_only_correct_type() -> TestResult {
1025 let conn = setup_conn()?;
1026 insert(&conn, &new_memory("mem-user"))?;
1027
1028 let mut m2 = new_memory("mem-feedback");
1029 m2.memory_type = "feedback".to_string();
1030 insert(&conn, &m2)?;
1031
1032 let rows_user = list(&conn, "global", Some("user"), 10, 0, false)?;
1033 assert!(rows_user.iter().all(|r| r.memory_type == "user"));
1034
1035 let rows_fb = list(&conn, "global", Some("feedback"), 10, 0, false)?;
1036 assert!(rows_fb.iter().all(|r| r.memory_type == "feedback"));
1037 Ok(())
1038 }
1039
1040 #[test]
1041 fn list_exclui_soft_deleted() -> TestResult {
1042 let conn = setup_conn()?;
1043 let m = new_memory("mem-excluida");
1044 insert(&conn, &m)?;
1045 soft_delete(&conn, "global", "mem-excluida")?;
1046
1047 let rows = list(&conn, "global", None, 10, 0, false)?;
1048 assert!(rows.iter().all(|r| r.name != "mem-excluida"));
1049 Ok(())
1050 }
1051
1052 #[test]
1053 fn list_pagination_works() -> TestResult {
1054 let conn = setup_conn()?;
1055 for i in 0..5 {
1056 insert(&conn, &new_memory(&format!("mem-pag-{i}")))?;
1057 }
1058
1059 let pagina1 = list(&conn, "global", None, 2, 0, false)?;
1060 let pagina2 = list(&conn, "global", None, 2, 2, false)?;
1061 assert!(pagina1.len() <= 2);
1062 assert!(pagina2.len() <= 2);
1063 if !pagina1.is_empty() && !pagina2.is_empty() {
1064 assert_ne!(pagina1[0].id, pagina2[0].id);
1065 }
1066 Ok(())
1067 }
1068
1069 #[test]
1070 fn upsert_vec_and_delete_vec_work() -> TestResult {
1071 let conn = setup_conn()?;
1072 let m = new_memory("mem-vec");
1073 let id = insert(&conn, &m)?;
1074
1075 let embedding: Vec<f32> = vec![0.1; 384];
1076 upsert_vec(
1077 &conn, id, "global", "user", &embedding, "mem-vec", "snippet",
1078 )?;
1079
1080 let count: i64 = conn.query_row(
1081 "SELECT COUNT(*) FROM memory_embeddings WHERE memory_id = ?1",
1082 params![id],
1083 |r| r.get(0),
1084 )?;
1085 assert_eq!(count, 1);
1086
1087 delete_vec(&conn, id)?;
1088
1089 let count_after: i64 = conn.query_row(
1090 "SELECT COUNT(*) FROM memory_embeddings WHERE memory_id = ?1",
1091 params![id],
1092 |r| r.get(0),
1093 )?;
1094 assert_eq!(count_after, 0);
1095 Ok(())
1096 }
1097
1098 #[test]
1099 fn upsert_vec_replaces_existing_vector() -> TestResult {
1100 let conn = setup_conn()?;
1101 let m = new_memory("mem-vec-upsert");
1102 let id = insert(&conn, &m)?;
1103
1104 let emb1: Vec<f32> = vec![0.1; 384];
1105 upsert_vec(&conn, id, "global", "user", &emb1, "mem-vec-upsert", "s1")?;
1106
1107 let emb2: Vec<f32> = vec![0.9; 384];
1108 upsert_vec(&conn, id, "global", "user", &emb2, "mem-vec-upsert", "s2")?;
1109
1110 let count: i64 = conn.query_row(
1111 "SELECT COUNT(*) FROM memory_embeddings WHERE memory_id = ?1",
1112 params![id],
1113 |r| r.get(0),
1114 )?;
1115 assert_eq!(count, 1);
1116 Ok(())
1117 }
1118
1119 #[test]
1120 fn knn_search_returns_results_by_distance() -> TestResult {
1121 let conn = setup_conn()?;
1122
1123 let ma = new_memory("mem-knn-a");
1125 let id_a = insert(&conn, &ma)?;
1126 let emb_a: Vec<f32> = vec![1.0; 384];
1127 upsert_vec(&conn, id_a, "global", "user", &emb_a, "mem-knn-a", "s")?;
1128
1129 let mb = new_memory("mem-knn-b");
1131 let id_b = insert(&conn, &mb)?;
1132 let emb_b: Vec<f32> = vec![-1.0; 384];
1133 upsert_vec(&conn, id_b, "global", "user", &emb_b, "mem-knn-b", "s")?;
1134
1135 let query: Vec<f32> = vec![1.0; 384];
1136 let results = knn_search(&conn, &query, &["global".to_string()], None, 2)?;
1137 assert!(!results.is_empty());
1138 assert_eq!(results[0].0, id_a);
1139 Ok(())
1140 }
1141
1142 #[test]
1143 fn knn_search_with_type_filter_restricts_result() -> TestResult {
1144 let conn = setup_conn()?;
1145
1146 let ma = new_memory("mem-knn-tipo-user");
1147 let id_a = insert(&conn, &ma)?;
1148 let emb: Vec<f32> = vec![1.0; 384];
1149 upsert_vec(
1150 &conn,
1151 id_a,
1152 "global",
1153 "user",
1154 &emb,
1155 "mem-knn-tipo-user",
1156 "s",
1157 )?;
1158
1159 let mut mb = new_memory("mem-knn-tipo-fb");
1160 mb.memory_type = "feedback".to_string();
1161 let id_b = insert(&conn, &mb)?;
1162 upsert_vec(
1163 &conn,
1164 id_b,
1165 "global",
1166 "feedback",
1167 &emb,
1168 "mem-knn-tipo-fb",
1169 "s",
1170 )?;
1171
1172 let query: Vec<f32> = vec![1.0; 384];
1173 let results_user = knn_search(&conn, &query, &["global".to_string()], Some("user"), 5)?;
1174 assert!(results_user.iter().all(|(id, _)| *id == id_a));
1175
1176 let results_fb = knn_search(&conn, &query, &["global".to_string()], Some("feedback"), 5)?;
1177 assert!(results_fb.iter().all(|(id, _)| *id == id_b));
1178 Ok(())
1179 }
1180
1181 #[test]
1182 fn fts_search_finds_by_prefix_in_body() -> TestResult {
1183 let conn = setup_conn()?;
1184 let mut m = new_memory("mem-fts");
1185 m.body = "linguagem de programacao rust".to_string();
1186 insert(&conn, &m)?;
1187
1188 conn.execute_batch(
1189 "INSERT INTO fts_memories(rowid, name, description, body)
1190 SELECT id, name, description, body FROM memories WHERE deleted_at IS NULL",
1191 )?;
1192
1193 let rows = fts_search(&conn, "programacao", "global", None, 10)?;
1194 assert!(!rows.is_empty());
1195 assert!(rows.iter().any(|r| r.name == "mem-fts"));
1196 Ok(())
1197 }
1198
1199 #[test]
1200 fn fts_search_with_type_filter() -> TestResult {
1201 let conn = setup_conn()?;
1202 let mut m = new_memory("mem-fts-tipo");
1203 m.body = "linguagem especial para filtro".to_string();
1204 insert(&conn, &m)?;
1205
1206 let mut m2 = new_memory("mem-fts-feedback");
1207 m2.memory_type = "feedback".to_string();
1208 m2.body = "linguagem especial para filtro".to_string();
1209 insert(&conn, &m2)?;
1210
1211 conn.execute_batch(
1212 "INSERT INTO fts_memories(rowid, name, description, body)
1213 SELECT id, name, description, body FROM memories WHERE deleted_at IS NULL",
1214 )?;
1215
1216 let rows_user = fts_search(&conn, "especial", "global", Some("user"), 10)?;
1217 assert!(rows_user.iter().all(|r| r.memory_type == "user"));
1218
1219 let rows_fb = fts_search(&conn, "especial", "global", Some("feedback"), 10)?;
1220 assert!(rows_fb.iter().all(|r| r.memory_type == "feedback"));
1221 Ok(())
1222 }
1223
1224 #[test]
1225 fn fts_search_excludes_deleted() -> TestResult {
1226 let conn = setup_conn()?;
1227 let mut m = new_memory("mem-fts-del");
1228 m.body = "deleted fts content".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 soft_delete(&conn, "global", "mem-fts-del")?;
1237
1238 let rows = fts_search(&conn, "deleted", "global", None, 10)?;
1239 assert!(rows.iter().all(|r| r.name != "mem-fts-del"));
1240 Ok(())
1241 }
1242
1243 #[test]
1244 fn list_deleted_before_returns_correct_ids() -> TestResult {
1245 let conn = setup_conn()?;
1246 let m = new_memory("mem-purge");
1247 insert(&conn, &m)?;
1248 soft_delete(&conn, "global", "mem-purge")?;
1249
1250 let ids = list_deleted_before(&conn, "global", i64::MAX)?;
1251 assert!(!ids.is_empty());
1252
1253 let ids_antes = list_deleted_before(&conn, "global", 0)?;
1254 assert!(ids_antes.is_empty());
1255 Ok(())
1256 }
1257
1258 #[test]
1259 fn find_by_name_returns_correct_max_version() -> TestResult {
1260 let conn = setup_conn()?;
1261 let m = new_memory("mem-ver");
1262 let id = insert(&conn, &m)?;
1263
1264 let (_, _, v0) = find_by_name(&conn, "global", "mem-ver")?.ok_or("mem-ver should exist")?;
1265 assert_eq!(v0, 0);
1266
1267 conn.execute(
1268 "INSERT INTO memory_versions (memory_id, version, name, type, description, body, metadata, change_reason)
1269 VALUES (?1, 1, 'mem-ver', 'user', 'desc', 'body', '{}', 'create')",
1270 params![id],
1271 )?;
1272
1273 let (_, _, v1) =
1274 find_by_name(&conn, "global", "mem-ver")?.ok_or("mem-ver should exist after insert")?;
1275 assert_eq!(v1, 1);
1276 Ok(())
1277 }
1278
1279 #[test]
1280 fn insert_com_metadata_json() -> TestResult {
1281 let conn = setup_conn()?;
1282 let mut m = new_memory("mem-meta");
1283 m.metadata = serde_json::json!({"chave": "valor", "numero": 42});
1284 let id = insert(&conn, &m)?;
1285
1286 let row = read_full(&conn, id)?.ok_or("mem-meta should exist")?;
1287 let meta: serde_json::Value = serde_json::from_str(&row.metadata)?;
1288 assert_eq!(meta["chave"], "valor");
1289 assert_eq!(meta["numero"], 42);
1290 Ok(())
1291 }
1292
1293 #[test]
1294 fn insert_com_session_id() -> TestResult {
1295 let conn = setup_conn()?;
1296 let mut m = new_memory("mem-session");
1297 m.session_id = Some("sessao-xyz".to_string());
1298 let id = insert(&conn, &m)?;
1299
1300 let row = read_full(&conn, id)?.ok_or("mem-session should exist")?;
1301 assert_eq!(row.session_id, Some("sessao-xyz".to_string()));
1302 Ok(())
1303 }
1304
1305 #[test]
1306 fn delete_vec_for_nonexistent_id_does_not_fail() -> TestResult {
1307 let conn = setup_conn()?;
1308 let result = delete_vec(&conn, 99999);
1309 assert!(result.is_ok());
1310 Ok(())
1311 }
1312
1313 #[test]
1314 fn preprocess_fts_query_no_separators() {
1315 assert_eq!(preprocess_fts_query("hello"), "hello*");
1316 assert_eq!(preprocess_fts_query("hello world"), "hello* world*");
1317 }
1318
1319 #[test]
1320 fn preprocess_fts_query_with_hyphens() {
1321 let result = preprocess_fts_query("graphrag-precompact");
1322 assert!(result.contains("\"graphrag precompact\""));
1323 assert!(result.contains("graphrag*"));
1324 assert!(result.contains("precompact*"));
1325 }
1326
1327 #[test]
1328 fn preprocess_fts_query_with_dots() {
1329 let result = preprocess_fts_query("v1.0.44");
1330 assert!(result.contains("\"v1 0 44\""));
1331 assert!(result.contains("v1*"));
1332 assert!(result.contains("44*"));
1333 }
1334
1335 #[test]
1336 fn preprocess_fts_query_with_mixed_separators() {
1337 let result = preprocess_fts_query("graphrag-precompact.sh");
1338 assert!(result.contains("\"graphrag precompact sh\""));
1339 assert!(result.contains("graphrag*"));
1340 }
1341
1342 #[test]
1343 fn preprocess_fts_query_empty_and_whitespace() {
1344 assert_eq!(preprocess_fts_query(""), "");
1345 assert_eq!(preprocess_fts_query(" "), "");
1346 }
1347
1348 #[test]
1349 fn preprocess_fts_query_strips_quotes() {
1350 let result = preprocess_fts_query(r#"hello "world"#);
1351 assert!(result.contains("hello*"));
1352 assert!(result.contains("world*"));
1353 }
1354
1355 #[test]
1356 fn preprocess_fts_query_strips_asterisks() {
1357 assert_eq!(preprocess_fts_query("test*"), "test*");
1358 }
1359
1360 #[test]
1361 fn preprocess_fts_query_strips_parens() {
1362 let result = preprocess_fts_query("(hello)");
1363 assert!(result.contains("hello*"));
1364 assert!(!result.contains('('));
1365 }
1366
1367 #[test]
1368 fn preprocess_fts_query_filters_fts_keywords() {
1369 let result = preprocess_fts_query("foo OR bar");
1370 assert!(result.contains("foo*"));
1371 assert!(result.contains("bar*"));
1372 assert!(!result.contains("OR*"));
1373 }
1374
1375 #[test]
1376 fn preprocess_fts_query_only_fts_keywords() {
1377 assert_eq!(preprocess_fts_query("OR AND NOT"), "");
1378 }
1379
1380 #[test]
1381 fn preprocess_fts_query_keywords_with_separators() {
1382 let result = preprocess_fts_query("hello-OR-world");
1383 assert!(result.contains("hello*"));
1384 assert!(result.contains("world*"));
1385 assert!(!result.contains("OR*"));
1386 }
1387
1388 #[test]
1389 fn fts_search_finds_compound_term_with_hyphen() -> TestResult {
1390 let conn = setup_conn()?;
1391 let mut m = new_memory("mem-compound");
1392 m.body = "the graphrag-precompact script runs daily".to_string();
1393 insert(&conn, &m)?;
1394 conn.execute_batch(
1395 "INSERT INTO fts_memories(rowid, name, description, body)
1396 SELECT id, name, description, body FROM memories WHERE deleted_at IS NULL",
1397 )?;
1398 let rows = fts_search(&conn, "graphrag-precompact", "global", None, 10)?;
1399 assert!(!rows.is_empty(), "should find compound hyphenated term");
1400 Ok(())
1401 }
1402
1403 #[test]
1404 fn find_by_name_any_state_returns_deleted_flag() -> TestResult {
1405 let conn = setup_conn()?;
1406 let m = new_memory("mem-soft-del");
1407 let id = insert(&conn, &m)?;
1408 conn.execute(
1409 "UPDATE memories SET deleted_at = unixepoch() WHERE id = ?1",
1410 rusqlite::params![id],
1411 )?;
1412 let result = find_by_name_any_state(&conn, "global", "mem-soft-del")?;
1413 assert_eq!(result, Some((id, true)));
1414 Ok(())
1415 }
1416
1417 #[test]
1418 fn find_by_name_any_state_returns_not_deleted() -> TestResult {
1419 let conn = setup_conn()?;
1420 let m = new_memory("mem-active");
1421 let id = insert(&conn, &m)?;
1422 let result = find_by_name_any_state(&conn, "global", "mem-active")?;
1423 assert_eq!(result, Some((id, false)));
1424 Ok(())
1425 }
1426
1427 #[test]
1428 fn find_by_name_any_state_returns_none_when_absent() -> TestResult {
1429 let conn = setup_conn()?;
1430 let result = find_by_name_any_state(&conn, "global", "does-not-exist")?;
1431 assert!(result.is_none());
1432 Ok(())
1433 }
1434
1435 #[test]
1436 fn clear_deleted_at_restores_memory() -> TestResult {
1437 let conn = setup_conn()?;
1438 let m = new_memory("mem-restore");
1439 let id = insert(&conn, &m)?;
1440 conn.execute(
1441 "UPDATE memories SET deleted_at = unixepoch() WHERE id = ?1",
1442 rusqlite::params![id],
1443 )?;
1444 assert!(find_by_name(&conn, "global", "mem-restore")?.is_none());
1446 clear_deleted_at(&conn, id)?;
1447 let found = find_by_name(&conn, "global", "mem-restore")?;
1449 assert!(found.is_some());
1450 assert_eq!(found.unwrap().0, id);
1451 Ok(())
1452 }
1453}