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