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 if embedding.is_empty() {
284 tracing::debug!(
285 memory_id,
286 "empty memory embedding: skipping memory_embeddings row (backfill via enrich re-embed)"
287 );
288 return Ok(());
289 }
290 let embedding_bytes = f32_to_bytes(embedding);
291 with_busy_retry(|| {
292 conn.execute(
293 "DELETE FROM memory_embeddings WHERE memory_id = ?1",
294 params![memory_id],
295 )?;
296 conn.execute(
297 "INSERT INTO memory_embeddings(memory_id, namespace, embedding, source, model, dim)
298 VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
299 params![
300 memory_id,
301 namespace,
302 &embedding_bytes,
303 "llm-headless",
304 crate::constants::SQLITE_GRAPHRAG_VERSION,
305 crate::constants::embedding_dim() as i64,
306 ],
307 )?;
308 Ok(())
309 })
310}
311
312pub fn delete_vec(conn: &Connection, memory_id: i64) -> Result<(), AppError> {
324 conn.execute(
325 "DELETE FROM memory_embeddings WHERE memory_id = ?1",
326 params![memory_id],
327 )?;
328 Ok(())
329}
330
331pub fn read_by_name(
341 conn: &Connection,
342 namespace: &str,
343 name: &str,
344) -> Result<Option<MemoryRow>, AppError> {
345 let mut stmt = conn.prepare_cached(
346 "SELECT id, namespace, name, type, description, body, body_hash,
347 session_id, source, metadata, created_at, updated_at, deleted_at
348 FROM memories WHERE namespace=?1 AND name=?2 AND deleted_at IS NULL",
349 )?;
350 match stmt.query_row(params![namespace, name], |r| {
351 Ok(MemoryRow {
352 id: r.get(0)?,
353 namespace: r.get(1)?,
354 name: r.get(2)?,
355 memory_type: r.get(3)?,
356 description: r.get(4)?,
357 body: r.get(5)?,
358 body_hash: r.get(6)?,
359 session_id: r.get(7)?,
360 source: r.get(8)?,
361 metadata: r.get(9)?,
362 created_at: r.get(10)?,
363 updated_at: r.get(11)?,
364 deleted_at: r.get(12)?,
365 })
366 }) {
367 Ok(m) => Ok(Some(m)),
368 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
369 Err(e) => Err(AppError::Database(e)),
370 }
371}
372
373pub fn soft_delete(conn: &Connection, namespace: &str, name: &str) -> Result<bool, AppError> {
387 let affected = conn.execute(
388 "UPDATE memories SET deleted_at = unixepoch() WHERE namespace=?1 AND name=?2 AND deleted_at IS NULL",
389 params![namespace, name],
390 )?;
391 Ok(affected == 1)
392}
393
394pub fn list(
405 conn: &Connection,
406 namespace: &str,
407 memory_type: Option<&str>,
408 limit: usize,
409 offset: usize,
410 include_deleted: bool,
411) -> Result<Vec<MemoryRow>, AppError> {
412 if let Some(mt) = memory_type {
413 let sql = if include_deleted {
414 "SELECT id, namespace, name, type, description, body, body_hash,
415 session_id, source, metadata, created_at, updated_at, deleted_at
416 FROM memories WHERE namespace=?1 AND type=?2
417 ORDER BY updated_at DESC LIMIT ?3 OFFSET ?4"
418 } else {
419 "SELECT id, namespace, name, type, description, body, body_hash,
420 session_id, source, metadata, created_at, updated_at, deleted_at
421 FROM memories WHERE namespace=?1 AND type=?2 AND deleted_at IS NULL
422 ORDER BY updated_at DESC LIMIT ?3 OFFSET ?4"
423 };
424 let mut stmt = conn.prepare_cached(sql)?;
425 let rows = stmt
426 .query_map(params![namespace, mt, limit as i64, offset as i64], |r| {
427 Ok(MemoryRow {
428 id: r.get(0)?,
429 namespace: r.get(1)?,
430 name: r.get(2)?,
431 memory_type: r.get(3)?,
432 description: r.get(4)?,
433 body: r.get(5)?,
434 body_hash: r.get(6)?,
435 session_id: r.get(7)?,
436 source: r.get(8)?,
437 metadata: r.get(9)?,
438 created_at: r.get(10)?,
439 updated_at: r.get(11)?,
440 deleted_at: r.get(12)?,
441 })
442 })?
443 .collect::<Result<Vec<_>, _>>()?;
444 Ok(rows)
445 } else {
446 let sql = if include_deleted {
447 "SELECT id, namespace, name, type, description, body, body_hash,
448 session_id, source, metadata, created_at, updated_at, deleted_at
449 FROM memories WHERE namespace=?1
450 ORDER BY updated_at DESC LIMIT ?2 OFFSET ?3"
451 } else {
452 "SELECT id, namespace, name, type, description, body, body_hash,
453 session_id, source, metadata, created_at, updated_at, deleted_at
454 FROM memories WHERE namespace=?1 AND deleted_at IS NULL
455 ORDER BY updated_at DESC LIMIT ?2 OFFSET ?3"
456 };
457 let mut stmt = conn.prepare_cached(sql)?;
458 let rows = stmt
459 .query_map(params![namespace, limit as i64, offset as i64], |r| {
460 Ok(MemoryRow {
461 id: r.get(0)?,
462 namespace: r.get(1)?,
463 name: r.get(2)?,
464 memory_type: r.get(3)?,
465 description: r.get(4)?,
466 body: r.get(5)?,
467 body_hash: r.get(6)?,
468 session_id: r.get(7)?,
469 source: r.get(8)?,
470 metadata: r.get(9)?,
471 created_at: r.get(10)?,
472 updated_at: r.get(11)?,
473 deleted_at: r.get(12)?,
474 })
475 })?
476 .collect::<Result<Vec<_>, _>>()?;
477 Ok(rows)
478 }
479}
480
481pub fn count(
482 conn: &Connection,
483 namespace: &str,
484 memory_type: Option<&str>,
485 include_deleted: bool,
486) -> Result<usize, AppError> {
487 let (sql, params_vec): (&str, Vec<Box<dyn rusqlite::types::ToSql>>) = match (
488 memory_type,
489 include_deleted,
490 ) {
491 (Some(mt), true) => (
492 "SELECT COUNT(*) FROM memories WHERE namespace=?1 AND type=?2",
493 vec![
494 Box::new(namespace.to_string()) as Box<dyn rusqlite::types::ToSql>,
495 Box::new(mt.to_string()),
496 ],
497 ),
498 (Some(mt), false) => (
499 "SELECT COUNT(*) FROM memories WHERE namespace=?1 AND type=?2 AND deleted_at IS NULL",
500 vec![
501 Box::new(namespace.to_string()) as Box<dyn rusqlite::types::ToSql>,
502 Box::new(mt.to_string()),
503 ],
504 ),
505 (None, true) => (
506 "SELECT COUNT(*) FROM memories WHERE namespace=?1",
507 vec![Box::new(namespace.to_string()) as Box<dyn rusqlite::types::ToSql>],
508 ),
509 (None, false) => (
510 "SELECT COUNT(*) FROM memories WHERE namespace=?1 AND deleted_at IS NULL",
511 vec![Box::new(namespace.to_string()) as Box<dyn rusqlite::types::ToSql>],
512 ),
513 };
514 let params_refs: Vec<&dyn rusqlite::types::ToSql> =
515 params_vec.iter().map(|b| b.as_ref()).collect();
516 let n: i64 = conn.query_row(sql, params_refs.as_slice(), |r| r.get(0))?;
517 Ok(n as usize)
518}
519
520pub fn knn_search(
537 conn: &Connection,
538 embedding: &[f32],
539 namespaces: &[String],
540 memory_type: Option<&str>,
541 k: usize,
542) -> Result<Vec<(i64, f32)>, AppError> {
543 if embedding.len() != crate::constants::embedding_dim() {
544 return Err(AppError::Embedding(format!(
545 "knn_search embedding has {} dims, expected {}",
546 embedding.len(),
547 crate::constants::embedding_dim()
548 )));
549 }
550 let placeholders = (0..namespaces.len())
559 .map(|_| "?")
560 .collect::<Vec<_>>()
561 .join(",");
562 let sql = if namespaces.is_empty() {
563 "SELECT memory_id, embedding, namespace FROM memory_embeddings".to_string()
564 } else {
565 format!(
566 "SELECT memory_id, embedding, namespace FROM memory_embeddings \
567 WHERE namespace IN ({placeholders})"
568 )
569 };
570 let mut stmt = conn.prepare(&sql)?;
571 let mut raw_params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
572 for ns in namespaces {
573 raw_params.push(Box::new(ns.clone()));
574 }
575 let param_refs: Vec<&dyn rusqlite::ToSql> = raw_params.iter().map(|b| b.as_ref()).collect();
576 let rows = stmt.query_map(param_refs.as_slice(), |r| {
577 let id: i64 = r.get(0)?;
578 let bytes: Vec<u8> = r.get(1)?;
579 let ns: String = r.get(2)?;
580 Ok((id, bytes, ns))
581 })?;
582
583 let type_filter = memory_type.map(|t| t.to_string());
586 let mut candidates: Vec<(i64, f32)> = Vec::new();
587 for row in rows {
588 let (id, bytes, ns) = row?;
589 let stored = crate::embedder::bytes_to_f32(&bytes);
590 if stored.len() != embedding.len() {
591 continue;
592 }
593 let sim = crate::similarity::cosine_similarity(embedding, &stored);
594 let dist = crate::similarity::similarity_to_distance(sim);
595 if let Some(mt) = &type_filter {
596 let actual: Option<String> = conn
601 .query_row(
602 "SELECT type FROM memories WHERE id = ?1",
603 params![id],
604 |r| r.get(0),
605 )
606 .ok();
607 if actual.as_deref() != Some(mt.as_str()) {
608 continue;
609 }
610 }
611 let _ = ns; candidates.push((id, dist));
613 }
614 candidates.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
616 candidates.truncate(k);
617 Ok(candidates)
618}
619
620pub fn read_full(conn: &Connection, memory_id: i64) -> Result<Option<MemoryRow>, AppError> {
629 let mut stmt = conn.prepare_cached(
630 "SELECT id, namespace, name, type, description, body, body_hash,
631 session_id, source, metadata, created_at, updated_at, deleted_at
632 FROM memories WHERE id=?1 AND deleted_at IS NULL",
633 )?;
634 match stmt.query_row(params![memory_id], |r| {
635 Ok(MemoryRow {
636 id: r.get(0)?,
637 namespace: r.get(1)?,
638 name: r.get(2)?,
639 memory_type: r.get(3)?,
640 description: r.get(4)?,
641 body: r.get(5)?,
642 body_hash: r.get(6)?,
643 session_id: r.get(7)?,
644 source: r.get(8)?,
645 metadata: r.get(9)?,
646 created_at: r.get(10)?,
647 updated_at: r.get(11)?,
648 deleted_at: r.get(12)?,
649 })
650 }) {
651 Ok(m) => Ok(Some(m)),
652 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
653 Err(e) => Err(AppError::Database(e)),
654 }
655}
656
657pub fn list_deleted_before(
666 conn: &Connection,
667 namespace: &str,
668 before_ts: i64,
669) -> Result<Vec<i64>, AppError> {
670 let mut stmt = conn.prepare_cached(
671 "SELECT id FROM memories WHERE namespace = ?1 AND deleted_at IS NOT NULL AND deleted_at < ?2",
672 )?;
673 let ids = stmt
674 .query_map(params![namespace, before_ts], |r| r.get::<_, i64>(0))?
675 .collect::<Result<Vec<_>, _>>()?;
676 Ok(ids)
677}
678
679fn preprocess_fts_query(raw: &str) -> String {
689 const SEPARATORS: &[char] = &['-', '.', '_', '/'];
690 const FTS5_SYNTAX: &[char] = &['"', '*', '(', ')', '^', ':'];
691 const FTS5_KEYWORDS: &[&str] = &["OR", "AND", "NOT", "NEAR"];
692
693 let sanitized: String = raw.chars().filter(|c| !FTS5_SYNTAX.contains(c)).collect();
694 let trimmed = sanitized.trim();
695 if trimmed.is_empty() {
696 return String::new();
697 }
698
699 let is_fts_keyword = |t: &str| FTS5_KEYWORDS.iter().any(|kw| kw.eq_ignore_ascii_case(t));
700
701 if !trimmed.chars().any(|c| SEPARATORS.contains(&c)) {
702 return trimmed
703 .split_whitespace()
704 .filter(|t| !is_fts_keyword(t))
705 .map(|t| format!("{t}*"))
706 .collect::<Vec<_>>()
707 .join(" ");
708 }
709 let tokens: Vec<&str> = trimmed
710 .split(|c: char| SEPARATORS.contains(&c) || c.is_whitespace())
711 .filter(|t| !t.is_empty() && !is_fts_keyword(t))
712 .collect();
713 if tokens.is_empty() {
714 return String::new();
715 }
716 let phrase = format!("\"{}\"", tokens.join(" "));
717 let prefix_terms: Vec<String> = tokens.iter().map(|t| format!("{t}*")).collect();
718 format!("{phrase} OR {}", prefix_terms.join(" OR "))
719}
720
721pub fn fts_search(
730 conn: &Connection,
731 query: &str,
732 namespace: &str,
733 memory_type: Option<&str>,
734 limit: usize,
735) -> Result<Vec<MemoryRow>, AppError> {
736 let fts_query = preprocess_fts_query(query);
737 if let Some(mt) = memory_type {
738 let mut stmt = conn.prepare_cached(
739 "SELECT m.id, m.namespace, m.name, m.type, m.description, m.body, m.body_hash,
740 m.session_id, m.source, m.metadata, m.created_at, m.updated_at, m.deleted_at
741 FROM fts_memories fts
742 JOIN memories m ON m.id = fts.rowid
743 WHERE fts_memories MATCH ?1 AND m.namespace = ?2 AND m.type = ?3 AND m.deleted_at IS NULL
744 ORDER BY rank LIMIT ?4",
745 )?;
746 let rows = stmt
747 .query_map(params![fts_query, namespace, mt, limit as i64], |r| {
748 Ok(MemoryRow {
749 id: r.get(0)?,
750 namespace: r.get(1)?,
751 name: r.get(2)?,
752 memory_type: r.get(3)?,
753 description: r.get(4)?,
754 body: r.get(5)?,
755 body_hash: r.get(6)?,
756 session_id: r.get(7)?,
757 source: r.get(8)?,
758 metadata: r.get(9)?,
759 created_at: r.get(10)?,
760 updated_at: r.get(11)?,
761 deleted_at: r.get(12)?,
762 })
763 })?
764 .collect::<Result<Vec<_>, _>>()?;
765 Ok(rows)
766 } else {
767 let mut stmt = conn.prepare_cached(
768 "SELECT m.id, m.namespace, m.name, m.type, m.description, m.body, m.body_hash,
769 m.session_id, m.source, m.metadata, m.created_at, m.updated_at, m.deleted_at
770 FROM fts_memories fts
771 JOIN memories m ON m.id = fts.rowid
772 WHERE fts_memories MATCH ?1 AND m.namespace = ?2 AND m.deleted_at IS NULL
773 ORDER BY rank LIMIT ?3",
774 )?;
775 let rows = stmt
776 .query_map(params![fts_query, namespace, limit as i64], |r| {
777 Ok(MemoryRow {
778 id: r.get(0)?,
779 namespace: r.get(1)?,
780 name: r.get(2)?,
781 memory_type: r.get(3)?,
782 description: r.get(4)?,
783 body: r.get(5)?,
784 body_hash: r.get(6)?,
785 session_id: r.get(7)?,
786 source: r.get(8)?,
787 metadata: r.get(9)?,
788 created_at: r.get(10)?,
789 updated_at: r.get(11)?,
790 deleted_at: r.get(12)?,
791 })
792 })?
793 .collect::<Result<Vec<_>, _>>()?;
794 Ok(rows)
795 }
796}
797
798#[allow(clippy::too_many_arguments)]
806pub fn sync_fts_after_update(
807 conn: &Connection,
808 memory_id: i64,
809 old_name: &str,
810 old_desc: &str,
811 old_body: &str,
812 new_name: &str,
813 new_desc: &str,
814 new_body: &str,
815) -> Result<(), AppError> {
816 conn.execute(
817 "INSERT INTO fts_memories(fts_memories, rowid, name, description, body)
818 VALUES('delete', ?1, ?2, ?3, ?4)",
819 params![memory_id, old_name, old_desc, old_body],
820 )?;
821 conn.execute(
822 "INSERT INTO fts_memories(rowid, name, description, body)
823 VALUES(?1, ?2, ?3, ?4)",
824 params![memory_id, new_name, new_desc, new_body],
825 )?;
826 Ok(())
827}
828
829#[cfg(test)]
830mod tests {
831 use super::*;
832 use rusqlite::Connection;
833
834 type TestResult = Result<(), Box<dyn std::error::Error>>;
835
836 fn setup_conn() -> Result<Connection, Box<dyn std::error::Error>> {
837 crate::storage::connection::register_vec_extension();
838 let mut conn = Connection::open_in_memory()?;
839 conn.execute_batch(
840 "PRAGMA foreign_keys = ON;
841 PRAGMA temp_store = MEMORY;",
842 )?;
843 crate::migrations::runner().run(&mut conn)?;
844 Ok(conn)
845 }
846
847 fn new_memory(name: &str) -> NewMemory {
848 NewMemory {
849 namespace: "global".to_string(),
850 name: name.to_string(),
851 memory_type: "user".to_string(),
852 description: "descricao de teste".to_string(),
853 body: "test memory body".to_string(),
854 body_hash: format!("hash-{name}"),
855 session_id: None,
856 source: "agent".to_string(),
857 metadata: serde_json::json!({}),
858 }
859 }
860
861 #[test]
862 fn insert_and_find_by_name_return_id() -> TestResult {
863 let conn = setup_conn()?;
864 let m = new_memory("mem-alpha");
865 let id = insert(&conn, &m)?;
866 assert!(id > 0);
867
868 let found = find_by_name(&conn, "global", "mem-alpha")?;
869 assert!(found.is_some());
870 let (found_id, _, _) = found.ok_or("mem-alpha should exist")?;
871 assert_eq!(found_id, id);
872 Ok(())
873 }
874
875 #[test]
876 fn find_by_name_returns_none_when_not_found() -> TestResult {
877 let conn = setup_conn()?;
878 let result = find_by_name(&conn, "global", "inexistente")?;
879 assert!(result.is_none());
880 Ok(())
881 }
882
883 #[test]
884 fn find_by_hash_returns_correct_id() -> TestResult {
885 let conn = setup_conn()?;
886 let m = new_memory("mem-hash");
887 let id = insert(&conn, &m)?;
888
889 let found = find_by_hash(&conn, "global", "hash-mem-hash")?;
890 assert_eq!(found, Some(id));
891 Ok(())
892 }
893
894 #[test]
895 fn find_by_hash_returns_none_when_hash_not_found() -> TestResult {
896 let conn = setup_conn()?;
897 let result = find_by_hash(&conn, "global", "hash-inexistente")?;
898 assert!(result.is_none());
899 Ok(())
900 }
901
902 #[test]
903 fn find_by_hash_ignores_different_namespace() -> TestResult {
904 let conn = setup_conn()?;
905 let m = new_memory("mem-ns");
906 insert(&conn, &m)?;
907
908 let result = find_by_hash(&conn, "outro-namespace", "hash-mem-ns")?;
909 assert!(result.is_none());
910 Ok(())
911 }
912
913 #[test]
914 fn read_by_name_returns_full_memory() -> TestResult {
915 let conn = setup_conn()?;
916 let m = new_memory("mem-read");
917 let id = insert(&conn, &m)?;
918
919 let row = read_by_name(&conn, "global", "mem-read")?.ok_or("mem-read should exist")?;
920 assert_eq!(row.id, id);
921 assert_eq!(row.name, "mem-read");
922 assert_eq!(row.memory_type, "user");
923 assert_eq!(row.body, "test memory body");
924 assert_eq!(row.namespace, "global");
925 Ok(())
926 }
927
928 #[test]
929 fn read_by_name_returns_none_for_missing() -> TestResult {
930 let conn = setup_conn()?;
931 let result = read_by_name(&conn, "global", "nao-existe")?;
932 assert!(result.is_none());
933 Ok(())
934 }
935
936 #[test]
937 fn read_full_by_id_returns_memory() -> TestResult {
938 let conn = setup_conn()?;
939 let m = new_memory("mem-full");
940 let id = insert(&conn, &m)?;
941
942 let row = read_full(&conn, id)?.ok_or("mem-full should exist")?;
943 assert_eq!(row.id, id);
944 assert_eq!(row.name, "mem-full");
945 Ok(())
946 }
947
948 #[test]
949 fn read_full_returns_none_for_missing_id() -> TestResult {
950 let conn = setup_conn()?;
951 let result = read_full(&conn, 9999)?;
952 assert!(result.is_none());
953 Ok(())
954 }
955
956 #[test]
957 fn update_without_optimism_modifies_fields() -> TestResult {
958 let conn = setup_conn()?;
959 let m = new_memory("mem-upd");
960 let id = insert(&conn, &m)?;
961
962 let mut m2 = new_memory("mem-upd");
963 m2.body = "updated body".to_string();
964 m2.body_hash = "hash-novo".to_string();
965 let ok = update(&conn, id, &m2, None)?;
966 assert!(ok);
967
968 let row = read_full(&conn, id)?.ok_or("mem-upd should exist")?;
969 assert_eq!(row.body, "updated body");
970 assert_eq!(row.body_hash, "hash-novo");
971 Ok(())
972 }
973
974 #[test]
975 fn update_with_correct_expected_updated_at_succeeds() -> TestResult {
976 let conn = setup_conn()?;
977 let m = new_memory("mem-opt");
978 let id = insert(&conn, &m)?;
979
980 let (_, updated_at, _) =
981 find_by_name(&conn, "global", "mem-opt")?.ok_or("mem-opt should exist")?;
982
983 let mut m2 = new_memory("mem-opt");
984 m2.body = "optimistic body".to_string();
985 m2.body_hash = "hash-optimistic".to_string();
986 let ok = update(&conn, id, &m2, Some(updated_at))?;
987 assert!(ok);
988
989 let row = read_full(&conn, id)?.ok_or("mem-opt should exist after update")?;
990 assert_eq!(row.body, "optimistic body");
991 Ok(())
992 }
993
994 #[test]
995 fn update_with_wrong_expected_updated_at_returns_false() -> TestResult {
996 let conn = setup_conn()?;
997 let m = new_memory("mem-conflict");
998 let id = insert(&conn, &m)?;
999
1000 let mut m2 = new_memory("mem-conflict");
1001 m2.body = "must not appear".to_string();
1002 m2.body_hash = "hash-x".to_string();
1003 let ok = update(&conn, id, &m2, Some(0))?;
1004 assert!(!ok);
1005
1006 let row = read_full(&conn, id)?.ok_or("mem-conflict should exist")?;
1007 assert_eq!(row.body, "test memory body");
1008 Ok(())
1009 }
1010
1011 #[test]
1012 fn update_missing_id_returns_false() -> TestResult {
1013 let conn = setup_conn()?;
1014 let m = new_memory("fantasma");
1015 let ok = update(&conn, 9999, &m, None)?;
1016 assert!(!ok);
1017 Ok(())
1018 }
1019
1020 #[test]
1021 fn soft_delete_marks_deleted_at() -> TestResult {
1022 let conn = setup_conn()?;
1023 let m = new_memory("mem-del");
1024 insert(&conn, &m)?;
1025
1026 let ok = soft_delete(&conn, "global", "mem-del")?;
1027 assert!(ok);
1028
1029 let result = find_by_name(&conn, "global", "mem-del")?;
1030 assert!(result.is_none());
1031
1032 let result_read = read_by_name(&conn, "global", "mem-del")?;
1033 assert!(result_read.is_none());
1034 Ok(())
1035 }
1036
1037 #[test]
1038 fn soft_delete_returns_false_when_not_found() -> TestResult {
1039 let conn = setup_conn()?;
1040 let ok = soft_delete(&conn, "global", "nao-existe")?;
1041 assert!(!ok);
1042 Ok(())
1043 }
1044
1045 #[test]
1046 fn double_soft_delete_returns_false_on_second_call() -> TestResult {
1047 let conn = setup_conn()?;
1048 let m = new_memory("mem-del2");
1049 insert(&conn, &m)?;
1050
1051 soft_delete(&conn, "global", "mem-del2")?;
1052 let ok = soft_delete(&conn, "global", "mem-del2")?;
1053 assert!(!ok);
1054 Ok(())
1055 }
1056
1057 #[test]
1058 fn list_returns_memories_from_namespace() -> TestResult {
1059 let conn = setup_conn()?;
1060 insert(&conn, &new_memory("mem-list-a"))?;
1061 insert(&conn, &new_memory("mem-list-b"))?;
1062
1063 let rows = list(&conn, "global", None, 10, 0, false)?;
1064 assert!(rows.len() >= 2);
1065 let nomes: Vec<_> = rows.iter().map(|r| r.name.as_str()).collect();
1066 assert!(nomes.contains(&"mem-list-a"));
1067 assert!(nomes.contains(&"mem-list-b"));
1068 Ok(())
1069 }
1070
1071 #[test]
1072 fn list_with_type_filter_returns_only_correct_type() -> TestResult {
1073 let conn = setup_conn()?;
1074 insert(&conn, &new_memory("mem-user"))?;
1075
1076 let mut m2 = new_memory("mem-feedback");
1077 m2.memory_type = "feedback".to_string();
1078 insert(&conn, &m2)?;
1079
1080 let rows_user = list(&conn, "global", Some("user"), 10, 0, false)?;
1081 assert!(rows_user.iter().all(|r| r.memory_type == "user"));
1082
1083 let rows_fb = list(&conn, "global", Some("feedback"), 10, 0, false)?;
1084 assert!(rows_fb.iter().all(|r| r.memory_type == "feedback"));
1085 Ok(())
1086 }
1087
1088 #[test]
1089 fn list_exclui_soft_deleted() -> TestResult {
1090 let conn = setup_conn()?;
1091 let m = new_memory("mem-excluida");
1092 insert(&conn, &m)?;
1093 soft_delete(&conn, "global", "mem-excluida")?;
1094
1095 let rows = list(&conn, "global", None, 10, 0, false)?;
1096 assert!(rows.iter().all(|r| r.name != "mem-excluida"));
1097 Ok(())
1098 }
1099
1100 #[test]
1101 fn list_pagination_works() -> TestResult {
1102 let conn = setup_conn()?;
1103 for i in 0..5 {
1104 insert(&conn, &new_memory(&format!("mem-pag-{i}")))?;
1105 }
1106
1107 let pagina1 = list(&conn, "global", None, 2, 0, false)?;
1108 let pagina2 = list(&conn, "global", None, 2, 2, false)?;
1109 assert!(pagina1.len() <= 2);
1110 assert!(pagina2.len() <= 2);
1111 if !pagina1.is_empty() && !pagina2.is_empty() {
1112 assert_ne!(pagina1[0].id, pagina2[0].id);
1113 }
1114 Ok(())
1115 }
1116
1117 #[test]
1118 #[serial_test::serial(env)]
1119 fn upsert_vec_and_delete_vec_work() -> TestResult {
1120 let conn = setup_conn()?;
1121 let m = new_memory("mem-vec");
1122 let id = insert(&conn, &m)?;
1123
1124 let embedding: Vec<f32> = vec![0.1; crate::constants::embedding_dim()];
1125 upsert_vec(
1126 &conn, id, "global", "user", &embedding, "mem-vec", "snippet",
1127 )?;
1128
1129 let count: 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, 1);
1135
1136 delete_vec(&conn, id)?;
1137
1138 let count_after: i64 = conn.query_row(
1139 "SELECT COUNT(*) FROM memory_embeddings WHERE memory_id = ?1",
1140 params![id],
1141 |r| r.get(0),
1142 )?;
1143 assert_eq!(count_after, 0);
1144 Ok(())
1145 }
1146
1147 #[test]
1148 #[serial_test::serial(env)]
1149 fn upsert_vec_replaces_existing_vector() -> TestResult {
1150 let conn = setup_conn()?;
1151 let m = new_memory("mem-vec-upsert");
1152 let id = insert(&conn, &m)?;
1153
1154 let emb1: Vec<f32> = vec![0.1; crate::constants::embedding_dim()];
1155 upsert_vec(&conn, id, "global", "user", &emb1, "mem-vec-upsert", "s1")?;
1156
1157 let emb2: Vec<f32> = vec![0.9; crate::constants::embedding_dim()];
1158 upsert_vec(&conn, id, "global", "user", &emb2, "mem-vec-upsert", "s2")?;
1159
1160 let count: i64 = conn.query_row(
1161 "SELECT COUNT(*) FROM memory_embeddings WHERE memory_id = ?1",
1162 params![id],
1163 |r| r.get(0),
1164 )?;
1165 assert_eq!(count, 1);
1166 Ok(())
1167 }
1168
1169 #[test]
1172 fn upsert_vec_empty_embedding_skips_row() -> TestResult {
1173 let conn = setup_conn()?;
1174 let m = new_memory("mem-vec-vazia");
1175 let id = insert(&conn, &m)?;
1176
1177 upsert_vec(&conn, id, "global", "user", &[], "mem-vec-vazia", "s")?;
1178
1179 let count: i64 = conn.query_row(
1180 "SELECT COUNT(*) FROM memory_embeddings WHERE memory_id = ?1",
1181 params![id],
1182 |r| r.get(0),
1183 )?;
1184 assert_eq!(count, 0, "empty embedding must not persist a row");
1185 Ok(())
1186 }
1187
1188 #[test]
1189 #[serial_test::serial(env)]
1190 fn knn_search_returns_results_by_distance() -> TestResult {
1191 let conn = setup_conn()?;
1192
1193 let ma = new_memory("mem-knn-a");
1195 let id_a = insert(&conn, &ma)?;
1196 let emb_a: Vec<f32> = vec![1.0; crate::constants::embedding_dim()];
1197 upsert_vec(&conn, id_a, "global", "user", &emb_a, "mem-knn-a", "s")?;
1198
1199 let mb = new_memory("mem-knn-b");
1201 let id_b = insert(&conn, &mb)?;
1202 let emb_b: Vec<f32> = vec![-1.0; crate::constants::embedding_dim()];
1203 upsert_vec(&conn, id_b, "global", "user", &emb_b, "mem-knn-b", "s")?;
1204
1205 let query: Vec<f32> = vec![1.0; crate::constants::embedding_dim()];
1206 let results = knn_search(&conn, &query, &["global".to_string()], None, 2)?;
1207 assert!(!results.is_empty());
1208 assert_eq!(results[0].0, id_a);
1209 Ok(())
1210 }
1211
1212 #[test]
1213 #[serial_test::serial(env)]
1214 fn knn_search_with_type_filter_restricts_result() -> TestResult {
1215 let conn = setup_conn()?;
1216
1217 let ma = new_memory("mem-knn-tipo-user");
1218 let id_a = insert(&conn, &ma)?;
1219 let emb: Vec<f32> = vec![1.0; crate::constants::embedding_dim()];
1220 upsert_vec(
1221 &conn,
1222 id_a,
1223 "global",
1224 "user",
1225 &emb,
1226 "mem-knn-tipo-user",
1227 "s",
1228 )?;
1229
1230 let mut mb = new_memory("mem-knn-tipo-fb");
1231 mb.memory_type = "feedback".to_string();
1232 let id_b = insert(&conn, &mb)?;
1233 upsert_vec(
1234 &conn,
1235 id_b,
1236 "global",
1237 "feedback",
1238 &emb,
1239 "mem-knn-tipo-fb",
1240 "s",
1241 )?;
1242
1243 let query: Vec<f32> = vec![1.0; crate::constants::embedding_dim()];
1244 let results_user = knn_search(&conn, &query, &["global".to_string()], Some("user"), 5)?;
1245 assert!(results_user.iter().all(|(id, _)| *id == id_a));
1246
1247 let results_fb = knn_search(&conn, &query, &["global".to_string()], Some("feedback"), 5)?;
1248 assert!(results_fb.iter().all(|(id, _)| *id == id_b));
1249 Ok(())
1250 }
1251
1252 #[test]
1253 fn fts_search_finds_by_prefix_in_body() -> TestResult {
1254 let conn = setup_conn()?;
1255 let mut m = new_memory("mem-fts");
1256 m.body = "linguagem de programacao rust".to_string();
1257 insert(&conn, &m)?;
1258
1259 conn.execute_batch(
1260 "INSERT INTO fts_memories(rowid, name, description, body)
1261 SELECT id, name, description, body FROM memories WHERE deleted_at IS NULL",
1262 )?;
1263
1264 let rows = fts_search(&conn, "programacao", "global", None, 10)?;
1265 assert!(!rows.is_empty());
1266 assert!(rows.iter().any(|r| r.name == "mem-fts"));
1267 Ok(())
1268 }
1269
1270 #[test]
1271 fn fts_search_with_type_filter() -> TestResult {
1272 let conn = setup_conn()?;
1273 let mut m = new_memory("mem-fts-tipo");
1274 m.body = "linguagem especial para filtro".to_string();
1275 insert(&conn, &m)?;
1276
1277 let mut m2 = new_memory("mem-fts-feedback");
1278 m2.memory_type = "feedback".to_string();
1279 m2.body = "linguagem especial para filtro".to_string();
1280 insert(&conn, &m2)?;
1281
1282 conn.execute_batch(
1283 "INSERT INTO fts_memories(rowid, name, description, body)
1284 SELECT id, name, description, body FROM memories WHERE deleted_at IS NULL",
1285 )?;
1286
1287 let rows_user = fts_search(&conn, "especial", "global", Some("user"), 10)?;
1288 assert!(rows_user.iter().all(|r| r.memory_type == "user"));
1289
1290 let rows_fb = fts_search(&conn, "especial", "global", Some("feedback"), 10)?;
1291 assert!(rows_fb.iter().all(|r| r.memory_type == "feedback"));
1292 Ok(())
1293 }
1294
1295 #[test]
1296 fn fts_search_excludes_deleted() -> TestResult {
1297 let conn = setup_conn()?;
1298 let mut m = new_memory("mem-fts-del");
1299 m.body = "deleted fts content".to_string();
1300 insert(&conn, &m)?;
1301
1302 conn.execute_batch(
1303 "INSERT INTO fts_memories(rowid, name, description, body)
1304 SELECT id, name, description, body FROM memories WHERE deleted_at IS NULL",
1305 )?;
1306
1307 soft_delete(&conn, "global", "mem-fts-del")?;
1308
1309 let rows = fts_search(&conn, "deleted", "global", None, 10)?;
1310 assert!(rows.iter().all(|r| r.name != "mem-fts-del"));
1311 Ok(())
1312 }
1313
1314 #[test]
1315 fn list_deleted_before_returns_correct_ids() -> TestResult {
1316 let conn = setup_conn()?;
1317 let m = new_memory("mem-purge");
1318 insert(&conn, &m)?;
1319 soft_delete(&conn, "global", "mem-purge")?;
1320
1321 let ids = list_deleted_before(&conn, "global", i64::MAX)?;
1322 assert!(!ids.is_empty());
1323
1324 let ids_antes = list_deleted_before(&conn, "global", 0)?;
1325 assert!(ids_antes.is_empty());
1326 Ok(())
1327 }
1328
1329 #[test]
1330 fn find_by_name_returns_correct_max_version() -> TestResult {
1331 let conn = setup_conn()?;
1332 let m = new_memory("mem-ver");
1333 let id = insert(&conn, &m)?;
1334
1335 let (_, _, v0) = find_by_name(&conn, "global", "mem-ver")?.ok_or("mem-ver should exist")?;
1336 assert_eq!(v0, 0);
1337
1338 conn.execute(
1339 "INSERT INTO memory_versions (memory_id, version, name, type, description, body, metadata, change_reason)
1340 VALUES (?1, 1, 'mem-ver', 'user', 'desc', 'body', '{}', 'create')",
1341 params![id],
1342 )?;
1343
1344 let (_, _, v1) =
1345 find_by_name(&conn, "global", "mem-ver")?.ok_or("mem-ver should exist after insert")?;
1346 assert_eq!(v1, 1);
1347 Ok(())
1348 }
1349
1350 #[test]
1351 fn insert_com_metadata_json() -> TestResult {
1352 let conn = setup_conn()?;
1353 let mut m = new_memory("mem-meta");
1354 m.metadata = serde_json::json!({"chave": "valor", "numero": 42});
1355 let id = insert(&conn, &m)?;
1356
1357 let row = read_full(&conn, id)?.ok_or("mem-meta should exist")?;
1358 let meta: serde_json::Value = serde_json::from_str(&row.metadata)?;
1359 assert_eq!(meta["chave"], "valor");
1360 assert_eq!(meta["numero"], 42);
1361 Ok(())
1362 }
1363
1364 #[test]
1365 fn insert_com_session_id() -> TestResult {
1366 let conn = setup_conn()?;
1367 let mut m = new_memory("mem-session");
1368 m.session_id = Some("sessao-xyz".to_string());
1369 let id = insert(&conn, &m)?;
1370
1371 let row = read_full(&conn, id)?.ok_or("mem-session should exist")?;
1372 assert_eq!(row.session_id, Some("sessao-xyz".to_string()));
1373 Ok(())
1374 }
1375
1376 #[test]
1377 fn delete_vec_for_nonexistent_id_does_not_fail() -> TestResult {
1378 let conn = setup_conn()?;
1379 let result = delete_vec(&conn, 99999);
1380 assert!(result.is_ok());
1381 Ok(())
1382 }
1383
1384 #[test]
1385 fn preprocess_fts_query_no_separators() {
1386 assert_eq!(preprocess_fts_query("hello"), "hello*");
1387 assert_eq!(preprocess_fts_query("hello world"), "hello* world*");
1388 }
1389
1390 #[test]
1391 fn preprocess_fts_query_with_hyphens() {
1392 let result = preprocess_fts_query("graphrag-precompact");
1393 assert!(result.contains("\"graphrag precompact\""));
1394 assert!(result.contains("graphrag*"));
1395 assert!(result.contains("precompact*"));
1396 }
1397
1398 #[test]
1399 fn preprocess_fts_query_with_dots() {
1400 let result = preprocess_fts_query("v1.0.44");
1401 assert!(result.contains("\"v1 0 44\""));
1402 assert!(result.contains("v1*"));
1403 assert!(result.contains("44*"));
1404 }
1405
1406 #[test]
1407 fn preprocess_fts_query_with_mixed_separators() {
1408 let result = preprocess_fts_query("graphrag-precompact.sh");
1409 assert!(result.contains("\"graphrag precompact sh\""));
1410 assert!(result.contains("graphrag*"));
1411 }
1412
1413 #[test]
1414 fn preprocess_fts_query_empty_and_whitespace() {
1415 assert_eq!(preprocess_fts_query(""), "");
1416 assert_eq!(preprocess_fts_query(" "), "");
1417 }
1418
1419 #[test]
1420 fn preprocess_fts_query_strips_quotes() {
1421 let result = preprocess_fts_query(r#"hello "world"#);
1422 assert!(result.contains("hello*"));
1423 assert!(result.contains("world*"));
1424 }
1425
1426 #[test]
1427 fn preprocess_fts_query_strips_asterisks() {
1428 assert_eq!(preprocess_fts_query("test*"), "test*");
1429 }
1430
1431 #[test]
1432 fn preprocess_fts_query_strips_parens() {
1433 let result = preprocess_fts_query("(hello)");
1434 assert!(result.contains("hello*"));
1435 assert!(!result.contains('('));
1436 }
1437
1438 #[test]
1439 fn preprocess_fts_query_filters_fts_keywords() {
1440 let result = preprocess_fts_query("foo OR bar");
1441 assert!(result.contains("foo*"));
1442 assert!(result.contains("bar*"));
1443 assert!(!result.contains("OR*"));
1444 }
1445
1446 #[test]
1447 fn preprocess_fts_query_only_fts_keywords() {
1448 assert_eq!(preprocess_fts_query("OR AND NOT"), "");
1449 }
1450
1451 #[test]
1452 fn preprocess_fts_query_keywords_with_separators() {
1453 let result = preprocess_fts_query("hello-OR-world");
1454 assert!(result.contains("hello*"));
1455 assert!(result.contains("world*"));
1456 assert!(!result.contains("OR*"));
1457 }
1458
1459 #[test]
1460 fn fts_search_finds_compound_term_with_hyphen() -> TestResult {
1461 let conn = setup_conn()?;
1462 let mut m = new_memory("mem-compound");
1463 m.body = "the graphrag-precompact script runs daily".to_string();
1464 insert(&conn, &m)?;
1465 conn.execute_batch(
1466 "INSERT INTO fts_memories(rowid, name, description, body)
1467 SELECT id, name, description, body FROM memories WHERE deleted_at IS NULL",
1468 )?;
1469 let rows = fts_search(&conn, "graphrag-precompact", "global", None, 10)?;
1470 assert!(!rows.is_empty(), "should find compound hyphenated term");
1471 Ok(())
1472 }
1473
1474 #[test]
1475 fn find_by_name_any_state_returns_deleted_flag() -> TestResult {
1476 let conn = setup_conn()?;
1477 let m = new_memory("mem-soft-del");
1478 let id = insert(&conn, &m)?;
1479 conn.execute(
1480 "UPDATE memories SET deleted_at = unixepoch() WHERE id = ?1",
1481 rusqlite::params![id],
1482 )?;
1483 let result = find_by_name_any_state(&conn, "global", "mem-soft-del")?;
1484 assert_eq!(result, Some((id, true)));
1485 Ok(())
1486 }
1487
1488 #[test]
1489 fn find_by_name_any_state_returns_not_deleted() -> TestResult {
1490 let conn = setup_conn()?;
1491 let m = new_memory("mem-active");
1492 let id = insert(&conn, &m)?;
1493 let result = find_by_name_any_state(&conn, "global", "mem-active")?;
1494 assert_eq!(result, Some((id, false)));
1495 Ok(())
1496 }
1497
1498 #[test]
1499 fn find_by_name_any_state_returns_none_when_absent() -> TestResult {
1500 let conn = setup_conn()?;
1501 let result = find_by_name_any_state(&conn, "global", "does-not-exist")?;
1502 assert!(result.is_none());
1503 Ok(())
1504 }
1505
1506 #[test]
1507 fn clear_deleted_at_restores_memory() -> TestResult {
1508 let conn = setup_conn()?;
1509 let m = new_memory("mem-restore");
1510 let id = insert(&conn, &m)?;
1511 conn.execute(
1512 "UPDATE memories SET deleted_at = unixepoch() WHERE id = ?1",
1513 rusqlite::params![id],
1514 )?;
1515 assert!(find_by_name(&conn, "global", "mem-restore")?.is_none());
1517 clear_deleted_at(&conn, id)?;
1518 let found = find_by_name(&conn, "global", "mem-restore")?;
1520 assert!(found.is_some());
1521 assert_eq!(found.unwrap().0, id);
1522 Ok(())
1523 }
1524}