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 conn.execute(
182 "INSERT INTO memories (namespace, name, type, description, body, body_hash, session_id, source, metadata)
183 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
184 params![
185 m.namespace, m.name, m.memory_type, m.description, m.body,
186 m.body_hash, m.session_id, m.source,
187 serde_json::to_string(&m.metadata)?
188 ],
189 )?;
190 Ok(conn.last_insert_rowid())
191}
192
193pub fn update(
208 conn: &Connection,
209 id: i64,
210 m: &NewMemory,
211 expected_updated_at: Option<i64>,
212) -> Result<bool, AppError> {
213 let affected = if let Some(ts) = expected_updated_at {
214 conn.execute(
215 "UPDATE memories SET type=?2, description=?3, body=?4, body_hash=?5,
216 session_id=?6, source=?7, metadata=?8
217 WHERE id=?1 AND updated_at=?9 AND deleted_at IS NULL",
218 params![
219 id,
220 m.memory_type,
221 m.description,
222 m.body,
223 m.body_hash,
224 m.session_id,
225 m.source,
226 serde_json::to_string(&m.metadata)?,
227 ts
228 ],
229 )?
230 } else {
231 conn.execute(
232 "UPDATE memories SET type=?2, description=?3, body=?4, body_hash=?5,
233 session_id=?6, source=?7, metadata=?8
234 WHERE id=?1 AND deleted_at IS NULL",
235 params![
236 id,
237 m.memory_type,
238 m.description,
239 m.body,
240 m.body_hash,
241 m.session_id,
242 m.source,
243 serde_json::to_string(&m.metadata)?
244 ],
245 )?
246 };
247 Ok(affected == 1)
248}
249
250pub fn upsert_vec(
260 conn: &Connection,
261 memory_id: i64,
262 namespace: &str,
263 memory_type: &str,
264 embedding: &[f32],
265 name: &str,
266 snippet: &str,
267) -> Result<(), AppError> {
268 let embedding_bytes = f32_to_bytes(embedding);
273 with_busy_retry(|| {
274 conn.execute(
275 "DELETE FROM vec_memories WHERE memory_id = ?1",
276 params![memory_id],
277 )?;
278 conn.execute(
279 "INSERT INTO vec_memories(memory_id, namespace, type, embedding, name, snippet)
280 VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
281 params![
282 memory_id,
283 namespace,
284 memory_type,
285 &embedding_bytes,
286 name,
287 snippet
288 ],
289 )?;
290 Ok(())
291 })
292}
293
294pub fn delete_vec(conn: &Connection, memory_id: i64) -> Result<(), AppError> {
303 conn.execute(
304 "DELETE FROM vec_memories WHERE memory_id = ?1",
305 params![memory_id],
306 )?;
307 Ok(())
308}
309
310pub fn read_by_name(
320 conn: &Connection,
321 namespace: &str,
322 name: &str,
323) -> Result<Option<MemoryRow>, AppError> {
324 let mut stmt = conn.prepare_cached(
325 "SELECT id, namespace, name, type, description, body, body_hash,
326 session_id, source, metadata, created_at, updated_at, deleted_at
327 FROM memories WHERE namespace=?1 AND name=?2 AND deleted_at IS NULL",
328 )?;
329 match stmt.query_row(params![namespace, name], |r| {
330 Ok(MemoryRow {
331 id: r.get(0)?,
332 namespace: r.get(1)?,
333 name: r.get(2)?,
334 memory_type: r.get(3)?,
335 description: r.get(4)?,
336 body: r.get(5)?,
337 body_hash: r.get(6)?,
338 session_id: r.get(7)?,
339 source: r.get(8)?,
340 metadata: r.get(9)?,
341 created_at: r.get(10)?,
342 updated_at: r.get(11)?,
343 deleted_at: r.get(12)?,
344 })
345 }) {
346 Ok(m) => Ok(Some(m)),
347 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
348 Err(e) => Err(AppError::Database(e)),
349 }
350}
351
352pub fn soft_delete(conn: &Connection, namespace: &str, name: &str) -> Result<bool, AppError> {
366 let affected = conn.execute(
367 "UPDATE memories SET deleted_at = unixepoch() WHERE namespace=?1 AND name=?2 AND deleted_at IS NULL",
368 params![namespace, name],
369 )?;
370 Ok(affected == 1)
371}
372
373pub fn list(
384 conn: &Connection,
385 namespace: &str,
386 memory_type: Option<&str>,
387 limit: usize,
388 offset: usize,
389 include_deleted: bool,
390) -> Result<Vec<MemoryRow>, AppError> {
391 let deleted_clause = if include_deleted {
392 ""
393 } else {
394 " AND deleted_at IS NULL"
395 };
396 if let Some(mt) = memory_type {
397 let sql = format!(
398 "SELECT id, namespace, name, type, description, body, body_hash,
399 session_id, source, metadata, created_at, updated_at, deleted_at
400 FROM memories WHERE namespace=?1 AND type=?2{deleted_clause}
401 ORDER BY updated_at DESC LIMIT ?3 OFFSET ?4"
402 );
403 let mut stmt = conn.prepare(&sql)?;
404 let rows = stmt
405 .query_map(params![namespace, mt, limit as i64, offset as i64], |r| {
406 Ok(MemoryRow {
407 id: r.get(0)?,
408 namespace: r.get(1)?,
409 name: r.get(2)?,
410 memory_type: r.get(3)?,
411 description: r.get(4)?,
412 body: r.get(5)?,
413 body_hash: r.get(6)?,
414 session_id: r.get(7)?,
415 source: r.get(8)?,
416 metadata: r.get(9)?,
417 created_at: r.get(10)?,
418 updated_at: r.get(11)?,
419 deleted_at: r.get(12)?,
420 })
421 })?
422 .collect::<Result<Vec<_>, _>>()?;
423 Ok(rows)
424 } else {
425 let sql = format!(
426 "SELECT id, namespace, name, type, description, body, body_hash,
427 session_id, source, metadata, created_at, updated_at, deleted_at
428 FROM memories WHERE namespace=?1{deleted_clause}
429 ORDER BY updated_at DESC LIMIT ?2 OFFSET ?3"
430 );
431 let mut stmt = conn.prepare(&sql)?;
432 let rows = stmt
433 .query_map(params![namespace, limit as i64, offset as i64], |r| {
434 Ok(MemoryRow {
435 id: r.get(0)?,
436 namespace: r.get(1)?,
437 name: r.get(2)?,
438 memory_type: r.get(3)?,
439 description: r.get(4)?,
440 body: r.get(5)?,
441 body_hash: r.get(6)?,
442 session_id: r.get(7)?,
443 source: r.get(8)?,
444 metadata: r.get(9)?,
445 created_at: r.get(10)?,
446 updated_at: r.get(11)?,
447 deleted_at: r.get(12)?,
448 })
449 })?
450 .collect::<Result<Vec<_>, _>>()?;
451 Ok(rows)
452 }
453}
454
455pub fn knn_search(
472 conn: &Connection,
473 embedding: &[f32],
474 namespaces: &[String],
475 memory_type: Option<&str>,
476 k: usize,
477) -> Result<Vec<(i64, f32)>, AppError> {
478 let bytes = f32_to_bytes(embedding);
479
480 match namespaces.len() {
481 0 => {
482 if let Some(mt) = memory_type {
484 let mut stmt = conn.prepare(
485 "SELECT memory_id, distance FROM vec_memories \
486 WHERE embedding MATCH ?1 AND type = ?2 \
487 ORDER BY distance LIMIT ?3",
488 )?;
489 let rows = stmt
490 .query_map(params![bytes, mt, k as i64], |r| {
491 Ok((r.get::<_, i64>(0)?, r.get::<_, f32>(1)?))
492 })?
493 .collect::<Result<Vec<_>, _>>()?;
494 Ok(rows)
495 } else {
496 let mut stmt = conn.prepare(
497 "SELECT memory_id, distance FROM vec_memories \
498 WHERE embedding MATCH ?1 \
499 ORDER BY distance LIMIT ?2",
500 )?;
501 let rows = stmt
502 .query_map(params![bytes, k as i64], |r| {
503 Ok((r.get::<_, i64>(0)?, r.get::<_, f32>(1)?))
504 })?
505 .collect::<Result<Vec<_>, _>>()?;
506 Ok(rows)
507 }
508 }
509 1 => {
510 let ns = &namespaces[0];
512 if let Some(mt) = memory_type {
513 let mut stmt = conn.prepare(
514 "SELECT memory_id, distance FROM vec_memories \
515 WHERE embedding MATCH ?1 AND namespace = ?2 AND type = ?3 \
516 ORDER BY distance LIMIT ?4",
517 )?;
518 let rows = stmt
519 .query_map(params![bytes, ns, mt, k as i64], |r| {
520 Ok((r.get::<_, i64>(0)?, r.get::<_, f32>(1)?))
521 })?
522 .collect::<Result<Vec<_>, _>>()?;
523 Ok(rows)
524 } else {
525 let mut stmt = conn.prepare(
526 "SELECT memory_id, distance FROM vec_memories \
527 WHERE embedding MATCH ?1 AND namespace = ?2 \
528 ORDER BY distance LIMIT ?3",
529 )?;
530 let rows = stmt
531 .query_map(params![bytes, ns, k as i64], |r| {
532 Ok((r.get::<_, i64>(0)?, r.get::<_, f32>(1)?))
533 })?
534 .collect::<Result<Vec<_>, _>>()?;
535 Ok(rows)
536 }
537 }
538 _ => {
539 let placeholders = (0..namespaces.len())
542 .map(|_| "?")
543 .collect::<Vec<_>>()
544 .join(",");
545 if let Some(mt) = memory_type {
546 let query = format!(
547 "SELECT memory_id, distance FROM vec_memories \
548 WHERE embedding MATCH ? AND type = ? AND namespace IN ({placeholders}) \
549 ORDER BY distance LIMIT ?"
550 );
551 let mut stmt = conn.prepare(&query)?;
552 let mut raw_params: Vec<Box<dyn rusqlite::ToSql>> =
554 vec![Box::new(bytes), Box::new(mt.to_string())];
555 for ns in namespaces {
556 raw_params.push(Box::new(ns.clone()));
557 }
558 raw_params.push(Box::new(k as i64));
559 let param_refs: Vec<&dyn rusqlite::ToSql> =
560 raw_params.iter().map(|b| b.as_ref()).collect();
561 let rows = stmt
562 .query_map(param_refs.as_slice(), |r| {
563 Ok((r.get::<_, i64>(0)?, r.get::<_, f32>(1)?))
564 })?
565 .collect::<Result<Vec<_>, _>>()?;
566 Ok(rows)
567 } else {
568 let query = format!(
569 "SELECT memory_id, distance FROM vec_memories \
570 WHERE embedding MATCH ? AND namespace IN ({placeholders}) \
571 ORDER BY distance LIMIT ?"
572 );
573 let mut stmt = conn.prepare(&query)?;
574 let mut raw_params: Vec<Box<dyn rusqlite::ToSql>> = vec![Box::new(bytes)];
576 for ns in namespaces {
577 raw_params.push(Box::new(ns.clone()));
578 }
579 raw_params.push(Box::new(k as i64));
580 let param_refs: Vec<&dyn rusqlite::ToSql> =
581 raw_params.iter().map(|b| b.as_ref()).collect();
582 let rows = stmt
583 .query_map(param_refs.as_slice(), |r| {
584 Ok((r.get::<_, i64>(0)?, r.get::<_, f32>(1)?))
585 })?
586 .collect::<Result<Vec<_>, _>>()?;
587 Ok(rows)
588 }
589 }
590 }
591}
592
593pub fn read_full(conn: &Connection, memory_id: i64) -> Result<Option<MemoryRow>, AppError> {
601 let mut stmt = conn.prepare_cached(
602 "SELECT id, namespace, name, type, description, body, body_hash,
603 session_id, source, metadata, created_at, updated_at, deleted_at
604 FROM memories WHERE id=?1 AND deleted_at IS NULL",
605 )?;
606 match stmt.query_row(params![memory_id], |r| {
607 Ok(MemoryRow {
608 id: r.get(0)?,
609 namespace: r.get(1)?,
610 name: r.get(2)?,
611 memory_type: r.get(3)?,
612 description: r.get(4)?,
613 body: r.get(5)?,
614 body_hash: r.get(6)?,
615 session_id: r.get(7)?,
616 source: r.get(8)?,
617 metadata: r.get(9)?,
618 created_at: r.get(10)?,
619 updated_at: r.get(11)?,
620 deleted_at: r.get(12)?,
621 })
622 }) {
623 Ok(m) => Ok(Some(m)),
624 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
625 Err(e) => Err(AppError::Database(e)),
626 }
627}
628
629pub fn list_deleted_before(
638 conn: &Connection,
639 namespace: &str,
640 before_ts: i64,
641) -> Result<Vec<i64>, AppError> {
642 let mut stmt = conn.prepare_cached(
643 "SELECT id FROM memories WHERE namespace = ?1 AND deleted_at IS NOT NULL AND deleted_at < ?2",
644 )?;
645 let ids = stmt
646 .query_map(params![namespace, before_ts], |r| r.get::<_, i64>(0))?
647 .collect::<Result<Vec<_>, _>>()?;
648 Ok(ids)
649}
650
651fn preprocess_fts_query(raw: &str) -> String {
661 const SEPARATORS: &[char] = &['-', '.', '_', '/'];
662 const FTS5_SYNTAX: &[char] = &['"', '*', '(', ')', '^', ':'];
663 const FTS5_KEYWORDS: &[&str] = &["OR", "AND", "NOT", "NEAR"];
664
665 let sanitized: String = raw.chars().filter(|c| !FTS5_SYNTAX.contains(c)).collect();
666 let trimmed = sanitized.trim();
667 if trimmed.is_empty() {
668 return String::new();
669 }
670
671 let is_fts_keyword = |t: &str| FTS5_KEYWORDS.iter().any(|kw| kw.eq_ignore_ascii_case(t));
672
673 if !trimmed.chars().any(|c| SEPARATORS.contains(&c)) {
674 return trimmed
675 .split_whitespace()
676 .filter(|t| !is_fts_keyword(t))
677 .map(|t| format!("{t}*"))
678 .collect::<Vec<_>>()
679 .join(" ");
680 }
681 let tokens: Vec<&str> = trimmed
682 .split(|c: char| SEPARATORS.contains(&c) || c.is_whitespace())
683 .filter(|t| !t.is_empty() && !is_fts_keyword(t))
684 .collect();
685 if tokens.is_empty() {
686 return String::new();
687 }
688 let phrase = format!("\"{}\"", tokens.join(" "));
689 let prefix_terms: Vec<String> = tokens.iter().map(|t| format!("{t}*")).collect();
690 format!("{phrase} OR {}", prefix_terms.join(" OR "))
691}
692
693pub fn fts_search(
702 conn: &Connection,
703 query: &str,
704 namespace: &str,
705 memory_type: Option<&str>,
706 limit: usize,
707) -> Result<Vec<MemoryRow>, AppError> {
708 let fts_query = preprocess_fts_query(query);
709 if let Some(mt) = memory_type {
710 let mut stmt = conn.prepare(
711 "SELECT m.id, m.namespace, m.name, m.type, m.description, m.body, m.body_hash,
712 m.session_id, m.source, m.metadata, m.created_at, m.updated_at, m.deleted_at
713 FROM fts_memories fts
714 JOIN memories m ON m.id = fts.rowid
715 WHERE fts_memories MATCH ?1 AND m.namespace = ?2 AND m.type = ?3 AND m.deleted_at IS NULL
716 ORDER BY rank LIMIT ?4",
717 )?;
718 let rows = stmt
719 .query_map(params![fts_query, namespace, mt, limit as i64], |r| {
720 Ok(MemoryRow {
721 id: r.get(0)?,
722 namespace: r.get(1)?,
723 name: r.get(2)?,
724 memory_type: r.get(3)?,
725 description: r.get(4)?,
726 body: r.get(5)?,
727 body_hash: r.get(6)?,
728 session_id: r.get(7)?,
729 source: r.get(8)?,
730 metadata: r.get(9)?,
731 created_at: r.get(10)?,
732 updated_at: r.get(11)?,
733 deleted_at: r.get(12)?,
734 })
735 })?
736 .collect::<Result<Vec<_>, _>>()?;
737 Ok(rows)
738 } else {
739 let mut stmt = conn.prepare(
740 "SELECT m.id, m.namespace, m.name, m.type, m.description, m.body, m.body_hash,
741 m.session_id, m.source, m.metadata, m.created_at, m.updated_at, m.deleted_at
742 FROM fts_memories fts
743 JOIN memories m ON m.id = fts.rowid
744 WHERE fts_memories MATCH ?1 AND m.namespace = ?2 AND m.deleted_at IS NULL
745 ORDER BY rank LIMIT ?3",
746 )?;
747 let rows = stmt
748 .query_map(params![fts_query, namespace, limit as i64], |r| {
749 Ok(MemoryRow {
750 id: r.get(0)?,
751 namespace: r.get(1)?,
752 name: r.get(2)?,
753 memory_type: r.get(3)?,
754 description: r.get(4)?,
755 body: r.get(5)?,
756 body_hash: r.get(6)?,
757 session_id: r.get(7)?,
758 source: r.get(8)?,
759 metadata: r.get(9)?,
760 created_at: r.get(10)?,
761 updated_at: r.get(11)?,
762 deleted_at: r.get(12)?,
763 })
764 })?
765 .collect::<Result<Vec<_>, _>>()?;
766 Ok(rows)
767 }
768}
769
770#[cfg(test)]
771mod tests {
772 use super::*;
773 use rusqlite::Connection;
774
775 type TestResult = Result<(), Box<dyn std::error::Error>>;
776
777 fn setup_conn() -> Result<Connection, Box<dyn std::error::Error>> {
778 crate::storage::connection::register_vec_extension();
779 let mut conn = Connection::open_in_memory()?;
780 conn.execute_batch(
781 "PRAGMA foreign_keys = ON;
782 PRAGMA temp_store = MEMORY;",
783 )?;
784 crate::migrations::runner().run(&mut conn)?;
785 Ok(conn)
786 }
787
788 fn new_memory(name: &str) -> NewMemory {
789 NewMemory {
790 namespace: "global".to_string(),
791 name: name.to_string(),
792 memory_type: "user".to_string(),
793 description: "descricao de teste".to_string(),
794 body: "test memory body".to_string(),
795 body_hash: format!("hash-{name}"),
796 session_id: None,
797 source: "agent".to_string(),
798 metadata: serde_json::json!({}),
799 }
800 }
801
802 #[test]
803 fn insert_and_find_by_name_return_id() -> TestResult {
804 let conn = setup_conn()?;
805 let m = new_memory("mem-alpha");
806 let id = insert(&conn, &m)?;
807 assert!(id > 0);
808
809 let found = find_by_name(&conn, "global", "mem-alpha")?;
810 assert!(found.is_some());
811 let (found_id, _, _) = found.ok_or("mem-alpha should exist")?;
812 assert_eq!(found_id, id);
813 Ok(())
814 }
815
816 #[test]
817 fn find_by_name_returns_none_when_not_found() -> TestResult {
818 let conn = setup_conn()?;
819 let result = find_by_name(&conn, "global", "inexistente")?;
820 assert!(result.is_none());
821 Ok(())
822 }
823
824 #[test]
825 fn find_by_hash_returns_correct_id() -> TestResult {
826 let conn = setup_conn()?;
827 let m = new_memory("mem-hash");
828 let id = insert(&conn, &m)?;
829
830 let found = find_by_hash(&conn, "global", "hash-mem-hash")?;
831 assert_eq!(found, Some(id));
832 Ok(())
833 }
834
835 #[test]
836 fn find_by_hash_returns_none_when_hash_not_found() -> TestResult {
837 let conn = setup_conn()?;
838 let result = find_by_hash(&conn, "global", "hash-inexistente")?;
839 assert!(result.is_none());
840 Ok(())
841 }
842
843 #[test]
844 fn find_by_hash_ignores_different_namespace() -> TestResult {
845 let conn = setup_conn()?;
846 let m = new_memory("mem-ns");
847 insert(&conn, &m)?;
848
849 let result = find_by_hash(&conn, "outro-namespace", "hash-mem-ns")?;
850 assert!(result.is_none());
851 Ok(())
852 }
853
854 #[test]
855 fn read_by_name_returns_full_memory() -> TestResult {
856 let conn = setup_conn()?;
857 let m = new_memory("mem-read");
858 let id = insert(&conn, &m)?;
859
860 let row = read_by_name(&conn, "global", "mem-read")?.ok_or("mem-read should exist")?;
861 assert_eq!(row.id, id);
862 assert_eq!(row.name, "mem-read");
863 assert_eq!(row.memory_type, "user");
864 assert_eq!(row.body, "test memory body");
865 assert_eq!(row.namespace, "global");
866 Ok(())
867 }
868
869 #[test]
870 fn read_by_name_returns_none_for_missing() -> TestResult {
871 let conn = setup_conn()?;
872 let result = read_by_name(&conn, "global", "nao-existe")?;
873 assert!(result.is_none());
874 Ok(())
875 }
876
877 #[test]
878 fn read_full_by_id_returns_memory() -> TestResult {
879 let conn = setup_conn()?;
880 let m = new_memory("mem-full");
881 let id = insert(&conn, &m)?;
882
883 let row = read_full(&conn, id)?.ok_or("mem-full should exist")?;
884 assert_eq!(row.id, id);
885 assert_eq!(row.name, "mem-full");
886 Ok(())
887 }
888
889 #[test]
890 fn read_full_returns_none_for_missing_id() -> TestResult {
891 let conn = setup_conn()?;
892 let result = read_full(&conn, 9999)?;
893 assert!(result.is_none());
894 Ok(())
895 }
896
897 #[test]
898 fn update_without_optimism_modifies_fields() -> TestResult {
899 let conn = setup_conn()?;
900 let m = new_memory("mem-upd");
901 let id = insert(&conn, &m)?;
902
903 let mut m2 = new_memory("mem-upd");
904 m2.body = "updated body".to_string();
905 m2.body_hash = "hash-novo".to_string();
906 let ok = update(&conn, id, &m2, None)?;
907 assert!(ok);
908
909 let row = read_full(&conn, id)?.ok_or("mem-upd should exist")?;
910 assert_eq!(row.body, "updated body");
911 assert_eq!(row.body_hash, "hash-novo");
912 Ok(())
913 }
914
915 #[test]
916 fn update_with_correct_expected_updated_at_succeeds() -> TestResult {
917 let conn = setup_conn()?;
918 let m = new_memory("mem-opt");
919 let id = insert(&conn, &m)?;
920
921 let (_, updated_at, _) =
922 find_by_name(&conn, "global", "mem-opt")?.ok_or("mem-opt should exist")?;
923
924 let mut m2 = new_memory("mem-opt");
925 m2.body = "optimistic body".to_string();
926 m2.body_hash = "hash-optimistic".to_string();
927 let ok = update(&conn, id, &m2, Some(updated_at))?;
928 assert!(ok);
929
930 let row = read_full(&conn, id)?.ok_or("mem-opt should exist after update")?;
931 assert_eq!(row.body, "optimistic body");
932 Ok(())
933 }
934
935 #[test]
936 fn update_with_wrong_expected_updated_at_returns_false() -> TestResult {
937 let conn = setup_conn()?;
938 let m = new_memory("mem-conflict");
939 let id = insert(&conn, &m)?;
940
941 let mut m2 = new_memory("mem-conflict");
942 m2.body = "must not appear".to_string();
943 m2.body_hash = "hash-x".to_string();
944 let ok = update(&conn, id, &m2, Some(0))?;
945 assert!(!ok);
946
947 let row = read_full(&conn, id)?.ok_or("mem-conflict should exist")?;
948 assert_eq!(row.body, "test memory body");
949 Ok(())
950 }
951
952 #[test]
953 fn update_missing_id_returns_false() -> TestResult {
954 let conn = setup_conn()?;
955 let m = new_memory("fantasma");
956 let ok = update(&conn, 9999, &m, None)?;
957 assert!(!ok);
958 Ok(())
959 }
960
961 #[test]
962 fn soft_delete_marks_deleted_at() -> TestResult {
963 let conn = setup_conn()?;
964 let m = new_memory("mem-del");
965 insert(&conn, &m)?;
966
967 let ok = soft_delete(&conn, "global", "mem-del")?;
968 assert!(ok);
969
970 let result = find_by_name(&conn, "global", "mem-del")?;
971 assert!(result.is_none());
972
973 let result_read = read_by_name(&conn, "global", "mem-del")?;
974 assert!(result_read.is_none());
975 Ok(())
976 }
977
978 #[test]
979 fn soft_delete_returns_false_when_not_found() -> TestResult {
980 let conn = setup_conn()?;
981 let ok = soft_delete(&conn, "global", "nao-existe")?;
982 assert!(!ok);
983 Ok(())
984 }
985
986 #[test]
987 fn double_soft_delete_returns_false_on_second_call() -> TestResult {
988 let conn = setup_conn()?;
989 let m = new_memory("mem-del2");
990 insert(&conn, &m)?;
991
992 soft_delete(&conn, "global", "mem-del2")?;
993 let ok = soft_delete(&conn, "global", "mem-del2")?;
994 assert!(!ok);
995 Ok(())
996 }
997
998 #[test]
999 fn list_returns_memories_from_namespace() -> TestResult {
1000 let conn = setup_conn()?;
1001 insert(&conn, &new_memory("mem-list-a"))?;
1002 insert(&conn, &new_memory("mem-list-b"))?;
1003
1004 let rows = list(&conn, "global", None, 10, 0, false)?;
1005 assert!(rows.len() >= 2);
1006 let nomes: Vec<_> = rows.iter().map(|r| r.name.as_str()).collect();
1007 assert!(nomes.contains(&"mem-list-a"));
1008 assert!(nomes.contains(&"mem-list-b"));
1009 Ok(())
1010 }
1011
1012 #[test]
1013 fn list_with_type_filter_returns_only_correct_type() -> TestResult {
1014 let conn = setup_conn()?;
1015 insert(&conn, &new_memory("mem-user"))?;
1016
1017 let mut m2 = new_memory("mem-feedback");
1018 m2.memory_type = "feedback".to_string();
1019 insert(&conn, &m2)?;
1020
1021 let rows_user = list(&conn, "global", Some("user"), 10, 0, false)?;
1022 assert!(rows_user.iter().all(|r| r.memory_type == "user"));
1023
1024 let rows_fb = list(&conn, "global", Some("feedback"), 10, 0, false)?;
1025 assert!(rows_fb.iter().all(|r| r.memory_type == "feedback"));
1026 Ok(())
1027 }
1028
1029 #[test]
1030 fn list_exclui_soft_deleted() -> TestResult {
1031 let conn = setup_conn()?;
1032 let m = new_memory("mem-excluida");
1033 insert(&conn, &m)?;
1034 soft_delete(&conn, "global", "mem-excluida")?;
1035
1036 let rows = list(&conn, "global", None, 10, 0, false)?;
1037 assert!(rows.iter().all(|r| r.name != "mem-excluida"));
1038 Ok(())
1039 }
1040
1041 #[test]
1042 fn list_pagination_works() -> TestResult {
1043 let conn = setup_conn()?;
1044 for i in 0..5 {
1045 insert(&conn, &new_memory(&format!("mem-pag-{i}")))?;
1046 }
1047
1048 let pagina1 = list(&conn, "global", None, 2, 0, false)?;
1049 let pagina2 = list(&conn, "global", None, 2, 2, false)?;
1050 assert!(pagina1.len() <= 2);
1051 assert!(pagina2.len() <= 2);
1052 if !pagina1.is_empty() && !pagina2.is_empty() {
1053 assert_ne!(pagina1[0].id, pagina2[0].id);
1054 }
1055 Ok(())
1056 }
1057
1058 #[test]
1059 fn upsert_vec_and_delete_vec_work() -> TestResult {
1060 let conn = setup_conn()?;
1061 let m = new_memory("mem-vec");
1062 let id = insert(&conn, &m)?;
1063
1064 let embedding: Vec<f32> = vec![0.1; 384];
1065 upsert_vec(
1066 &conn, id, "global", "user", &embedding, "mem-vec", "snippet",
1067 )?;
1068
1069 let count: i64 = conn.query_row(
1070 "SELECT COUNT(*) FROM vec_memories WHERE memory_id = ?1",
1071 params![id],
1072 |r| r.get(0),
1073 )?;
1074 assert_eq!(count, 1);
1075
1076 delete_vec(&conn, id)?;
1077
1078 let count_after: i64 = conn.query_row(
1079 "SELECT COUNT(*) FROM vec_memories WHERE memory_id = ?1",
1080 params![id],
1081 |r| r.get(0),
1082 )?;
1083 assert_eq!(count_after, 0);
1084 Ok(())
1085 }
1086
1087 #[test]
1088 fn upsert_vec_replaces_existing_vector() -> TestResult {
1089 let conn = setup_conn()?;
1090 let m = new_memory("mem-vec-upsert");
1091 let id = insert(&conn, &m)?;
1092
1093 let emb1: Vec<f32> = vec![0.1; 384];
1094 upsert_vec(&conn, id, "global", "user", &emb1, "mem-vec-upsert", "s1")?;
1095
1096 let emb2: Vec<f32> = vec![0.9; 384];
1097 upsert_vec(&conn, id, "global", "user", &emb2, "mem-vec-upsert", "s2")?;
1098
1099 let count: i64 = conn.query_row(
1100 "SELECT COUNT(*) FROM vec_memories WHERE memory_id = ?1",
1101 params![id],
1102 |r| r.get(0),
1103 )?;
1104 assert_eq!(count, 1);
1105 Ok(())
1106 }
1107
1108 #[test]
1109 fn knn_search_returns_results_by_distance() -> TestResult {
1110 let conn = setup_conn()?;
1111
1112 let ma = new_memory("mem-knn-a");
1114 let id_a = insert(&conn, &ma)?;
1115 let emb_a: Vec<f32> = vec![1.0; 384];
1116 upsert_vec(&conn, id_a, "global", "user", &emb_a, "mem-knn-a", "s")?;
1117
1118 let mb = new_memory("mem-knn-b");
1120 let id_b = insert(&conn, &mb)?;
1121 let emb_b: Vec<f32> = vec![-1.0; 384];
1122 upsert_vec(&conn, id_b, "global", "user", &emb_b, "mem-knn-b", "s")?;
1123
1124 let query: Vec<f32> = vec![1.0; 384];
1125 let results = knn_search(&conn, &query, &["global".to_string()], None, 2)?;
1126 assert!(!results.is_empty());
1127 assert_eq!(results[0].0, id_a);
1128 Ok(())
1129 }
1130
1131 #[test]
1132 fn knn_search_with_type_filter_restricts_result() -> TestResult {
1133 let conn = setup_conn()?;
1134
1135 let ma = new_memory("mem-knn-tipo-user");
1136 let id_a = insert(&conn, &ma)?;
1137 let emb: Vec<f32> = vec![1.0; 384];
1138 upsert_vec(
1139 &conn,
1140 id_a,
1141 "global",
1142 "user",
1143 &emb,
1144 "mem-knn-tipo-user",
1145 "s",
1146 )?;
1147
1148 let mut mb = new_memory("mem-knn-tipo-fb");
1149 mb.memory_type = "feedback".to_string();
1150 let id_b = insert(&conn, &mb)?;
1151 upsert_vec(
1152 &conn,
1153 id_b,
1154 "global",
1155 "feedback",
1156 &emb,
1157 "mem-knn-tipo-fb",
1158 "s",
1159 )?;
1160
1161 let query: Vec<f32> = vec![1.0; 384];
1162 let results_user = knn_search(&conn, &query, &["global".to_string()], Some("user"), 5)?;
1163 assert!(results_user.iter().all(|(id, _)| *id == id_a));
1164
1165 let results_fb = knn_search(&conn, &query, &["global".to_string()], Some("feedback"), 5)?;
1166 assert!(results_fb.iter().all(|(id, _)| *id == id_b));
1167 Ok(())
1168 }
1169
1170 #[test]
1171 fn fts_search_finds_by_prefix_in_body() -> TestResult {
1172 let conn = setup_conn()?;
1173 let mut m = new_memory("mem-fts");
1174 m.body = "linguagem de programacao rust".to_string();
1175 insert(&conn, &m)?;
1176
1177 conn.execute_batch(
1178 "INSERT INTO fts_memories(rowid, name, description, body)
1179 SELECT id, name, description, body FROM memories WHERE deleted_at IS NULL",
1180 )?;
1181
1182 let rows = fts_search(&conn, "programacao", "global", None, 10)?;
1183 assert!(!rows.is_empty());
1184 assert!(rows.iter().any(|r| r.name == "mem-fts"));
1185 Ok(())
1186 }
1187
1188 #[test]
1189 fn fts_search_with_type_filter() -> TestResult {
1190 let conn = setup_conn()?;
1191 let mut m = new_memory("mem-fts-tipo");
1192 m.body = "linguagem especial para filtro".to_string();
1193 insert(&conn, &m)?;
1194
1195 let mut m2 = new_memory("mem-fts-feedback");
1196 m2.memory_type = "feedback".to_string();
1197 m2.body = "linguagem especial para filtro".to_string();
1198 insert(&conn, &m2)?;
1199
1200 conn.execute_batch(
1201 "INSERT INTO fts_memories(rowid, name, description, body)
1202 SELECT id, name, description, body FROM memories WHERE deleted_at IS NULL",
1203 )?;
1204
1205 let rows_user = fts_search(&conn, "especial", "global", Some("user"), 10)?;
1206 assert!(rows_user.iter().all(|r| r.memory_type == "user"));
1207
1208 let rows_fb = fts_search(&conn, "especial", "global", Some("feedback"), 10)?;
1209 assert!(rows_fb.iter().all(|r| r.memory_type == "feedback"));
1210 Ok(())
1211 }
1212
1213 #[test]
1214 fn fts_search_excludes_deleted() -> TestResult {
1215 let conn = setup_conn()?;
1216 let mut m = new_memory("mem-fts-del");
1217 m.body = "deleted fts content".to_string();
1218 insert(&conn, &m)?;
1219
1220 conn.execute_batch(
1221 "INSERT INTO fts_memories(rowid, name, description, body)
1222 SELECT id, name, description, body FROM memories WHERE deleted_at IS NULL",
1223 )?;
1224
1225 soft_delete(&conn, "global", "mem-fts-del")?;
1226
1227 let rows = fts_search(&conn, "deleted", "global", None, 10)?;
1228 assert!(rows.iter().all(|r| r.name != "mem-fts-del"));
1229 Ok(())
1230 }
1231
1232 #[test]
1233 fn list_deleted_before_returns_correct_ids() -> TestResult {
1234 let conn = setup_conn()?;
1235 let m = new_memory("mem-purge");
1236 insert(&conn, &m)?;
1237 soft_delete(&conn, "global", "mem-purge")?;
1238
1239 let ids = list_deleted_before(&conn, "global", i64::MAX)?;
1240 assert!(!ids.is_empty());
1241
1242 let ids_antes = list_deleted_before(&conn, "global", 0)?;
1243 assert!(ids_antes.is_empty());
1244 Ok(())
1245 }
1246
1247 #[test]
1248 fn find_by_name_returns_correct_max_version() -> TestResult {
1249 let conn = setup_conn()?;
1250 let m = new_memory("mem-ver");
1251 let id = insert(&conn, &m)?;
1252
1253 let (_, _, v0) = find_by_name(&conn, "global", "mem-ver")?.ok_or("mem-ver should exist")?;
1254 assert_eq!(v0, 0);
1255
1256 conn.execute(
1257 "INSERT INTO memory_versions (memory_id, version, name, type, description, body, metadata, change_reason)
1258 VALUES (?1, 1, 'mem-ver', 'user', 'desc', 'body', '{}', 'create')",
1259 params![id],
1260 )?;
1261
1262 let (_, _, v1) =
1263 find_by_name(&conn, "global", "mem-ver")?.ok_or("mem-ver should exist after insert")?;
1264 assert_eq!(v1, 1);
1265 Ok(())
1266 }
1267
1268 #[test]
1269 fn insert_com_metadata_json() -> TestResult {
1270 let conn = setup_conn()?;
1271 let mut m = new_memory("mem-meta");
1272 m.metadata = serde_json::json!({"chave": "valor", "numero": 42});
1273 let id = insert(&conn, &m)?;
1274
1275 let row = read_full(&conn, id)?.ok_or("mem-meta should exist")?;
1276 let meta: serde_json::Value = serde_json::from_str(&row.metadata)?;
1277 assert_eq!(meta["chave"], "valor");
1278 assert_eq!(meta["numero"], 42);
1279 Ok(())
1280 }
1281
1282 #[test]
1283 fn insert_com_session_id() -> TestResult {
1284 let conn = setup_conn()?;
1285 let mut m = new_memory("mem-session");
1286 m.session_id = Some("sessao-xyz".to_string());
1287 let id = insert(&conn, &m)?;
1288
1289 let row = read_full(&conn, id)?.ok_or("mem-session should exist")?;
1290 assert_eq!(row.session_id, Some("sessao-xyz".to_string()));
1291 Ok(())
1292 }
1293
1294 #[test]
1295 fn delete_vec_for_nonexistent_id_does_not_fail() -> TestResult {
1296 let conn = setup_conn()?;
1297 let result = delete_vec(&conn, 99999);
1298 assert!(result.is_ok());
1299 Ok(())
1300 }
1301
1302 #[test]
1303 fn preprocess_fts_query_no_separators() {
1304 assert_eq!(preprocess_fts_query("hello"), "hello*");
1305 assert_eq!(preprocess_fts_query("hello world"), "hello* world*");
1306 }
1307
1308 #[test]
1309 fn preprocess_fts_query_with_hyphens() {
1310 let result = preprocess_fts_query("graphrag-precompact");
1311 assert!(result.contains("\"graphrag precompact\""));
1312 assert!(result.contains("graphrag*"));
1313 assert!(result.contains("precompact*"));
1314 }
1315
1316 #[test]
1317 fn preprocess_fts_query_with_dots() {
1318 let result = preprocess_fts_query("v1.0.44");
1319 assert!(result.contains("\"v1 0 44\""));
1320 assert!(result.contains("v1*"));
1321 assert!(result.contains("44*"));
1322 }
1323
1324 #[test]
1325 fn preprocess_fts_query_with_mixed_separators() {
1326 let result = preprocess_fts_query("graphrag-precompact.sh");
1327 assert!(result.contains("\"graphrag precompact sh\""));
1328 assert!(result.contains("graphrag*"));
1329 }
1330
1331 #[test]
1332 fn preprocess_fts_query_empty_and_whitespace() {
1333 assert_eq!(preprocess_fts_query(""), "");
1334 assert_eq!(preprocess_fts_query(" "), "");
1335 }
1336
1337 #[test]
1338 fn preprocess_fts_query_strips_quotes() {
1339 let result = preprocess_fts_query(r#"hello "world"#);
1340 assert!(result.contains("hello*"));
1341 assert!(result.contains("world*"));
1342 }
1343
1344 #[test]
1345 fn preprocess_fts_query_strips_asterisks() {
1346 assert_eq!(preprocess_fts_query("test*"), "test*");
1347 }
1348
1349 #[test]
1350 fn preprocess_fts_query_strips_parens() {
1351 let result = preprocess_fts_query("(hello)");
1352 assert!(result.contains("hello*"));
1353 assert!(!result.contains('('));
1354 }
1355
1356 #[test]
1357 fn preprocess_fts_query_filters_fts_keywords() {
1358 let result = preprocess_fts_query("foo OR bar");
1359 assert!(result.contains("foo*"));
1360 assert!(result.contains("bar*"));
1361 assert!(!result.contains("OR*"));
1362 }
1363
1364 #[test]
1365 fn preprocess_fts_query_only_fts_keywords() {
1366 assert_eq!(preprocess_fts_query("OR AND NOT"), "");
1367 }
1368
1369 #[test]
1370 fn preprocess_fts_query_keywords_with_separators() {
1371 let result = preprocess_fts_query("hello-OR-world");
1372 assert!(result.contains("hello*"));
1373 assert!(result.contains("world*"));
1374 assert!(!result.contains("OR*"));
1375 }
1376
1377 #[test]
1378 fn fts_search_finds_compound_term_with_hyphen() -> TestResult {
1379 let conn = setup_conn()?;
1380 let mut m = new_memory("mem-compound");
1381 m.body = "the graphrag-precompact script runs daily".to_string();
1382 insert(&conn, &m)?;
1383 conn.execute_batch(
1384 "INSERT INTO fts_memories(rowid, name, description, body)
1385 SELECT id, name, description, body FROM memories WHERE deleted_at IS NULL",
1386 )?;
1387 let rows = fts_search(&conn, "graphrag-precompact", "global", None, 10)?;
1388 assert!(!rows.is_empty(), "should find compound hyphenated term");
1389 Ok(())
1390 }
1391
1392 #[test]
1393 fn find_by_name_any_state_returns_deleted_flag() -> TestResult {
1394 let conn = setup_conn()?;
1395 let m = new_memory("mem-soft-del");
1396 let id = insert(&conn, &m)?;
1397 conn.execute(
1398 "UPDATE memories SET deleted_at = unixepoch() WHERE id = ?1",
1399 rusqlite::params![id],
1400 )?;
1401 let result = find_by_name_any_state(&conn, "global", "mem-soft-del")?;
1402 assert_eq!(result, Some((id, true)));
1403 Ok(())
1404 }
1405
1406 #[test]
1407 fn find_by_name_any_state_returns_not_deleted() -> TestResult {
1408 let conn = setup_conn()?;
1409 let m = new_memory("mem-active");
1410 let id = insert(&conn, &m)?;
1411 let result = find_by_name_any_state(&conn, "global", "mem-active")?;
1412 assert_eq!(result, Some((id, false)));
1413 Ok(())
1414 }
1415
1416 #[test]
1417 fn find_by_name_any_state_returns_none_when_absent() -> TestResult {
1418 let conn = setup_conn()?;
1419 let result = find_by_name_any_state(&conn, "global", "does-not-exist")?;
1420 assert!(result.is_none());
1421 Ok(())
1422 }
1423
1424 #[test]
1425 fn clear_deleted_at_restores_memory() -> TestResult {
1426 let conn = setup_conn()?;
1427 let m = new_memory("mem-restore");
1428 let id = insert(&conn, &m)?;
1429 conn.execute(
1430 "UPDATE memories SET deleted_at = unixepoch() WHERE id = ?1",
1431 rusqlite::params![id],
1432 )?;
1433 assert!(find_by_name(&conn, "global", "mem-restore")?.is_none());
1435 clear_deleted_at(&conn, id)?;
1436 let found = find_by_name(&conn, "global", "mem-restore")?;
1438 assert!(found.is_some());
1439 assert_eq!(found.unwrap().0, id);
1440 Ok(())
1441 }
1442}