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