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#[allow(clippy::too_many_arguments)]
778pub fn sync_fts_after_update(
779 conn: &Connection,
780 memory_id: i64,
781 old_name: &str,
782 old_desc: &str,
783 old_body: &str,
784 new_name: &str,
785 new_desc: &str,
786 new_body: &str,
787) -> Result<(), AppError> {
788 conn.execute(
789 "INSERT INTO fts_memories(fts_memories, rowid, name, description, body)
790 VALUES('delete', ?1, ?2, ?3, ?4)",
791 params![memory_id, old_name, old_desc, old_body],
792 )?;
793 conn.execute(
794 "INSERT INTO fts_memories(rowid, name, description, body)
795 VALUES(?1, ?2, ?3, ?4)",
796 params![memory_id, new_name, new_desc, new_body],
797 )?;
798 Ok(())
799}
800
801#[cfg(test)]
802mod tests {
803 use super::*;
804 use rusqlite::Connection;
805
806 type TestResult = Result<(), Box<dyn std::error::Error>>;
807
808 fn setup_conn() -> Result<Connection, Box<dyn std::error::Error>> {
809 crate::storage::connection::register_vec_extension();
810 let mut conn = Connection::open_in_memory()?;
811 conn.execute_batch(
812 "PRAGMA foreign_keys = ON;
813 PRAGMA temp_store = MEMORY;",
814 )?;
815 crate::migrations::runner().run(&mut conn)?;
816 Ok(conn)
817 }
818
819 fn new_memory(name: &str) -> NewMemory {
820 NewMemory {
821 namespace: "global".to_string(),
822 name: name.to_string(),
823 memory_type: "user".to_string(),
824 description: "descricao de teste".to_string(),
825 body: "test memory body".to_string(),
826 body_hash: format!("hash-{name}"),
827 session_id: None,
828 source: "agent".to_string(),
829 metadata: serde_json::json!({}),
830 }
831 }
832
833 #[test]
834 fn insert_and_find_by_name_return_id() -> TestResult {
835 let conn = setup_conn()?;
836 let m = new_memory("mem-alpha");
837 let id = insert(&conn, &m)?;
838 assert!(id > 0);
839
840 let found = find_by_name(&conn, "global", "mem-alpha")?;
841 assert!(found.is_some());
842 let (found_id, _, _) = found.ok_or("mem-alpha should exist")?;
843 assert_eq!(found_id, id);
844 Ok(())
845 }
846
847 #[test]
848 fn find_by_name_returns_none_when_not_found() -> TestResult {
849 let conn = setup_conn()?;
850 let result = find_by_name(&conn, "global", "inexistente")?;
851 assert!(result.is_none());
852 Ok(())
853 }
854
855 #[test]
856 fn find_by_hash_returns_correct_id() -> TestResult {
857 let conn = setup_conn()?;
858 let m = new_memory("mem-hash");
859 let id = insert(&conn, &m)?;
860
861 let found = find_by_hash(&conn, "global", "hash-mem-hash")?;
862 assert_eq!(found, Some(id));
863 Ok(())
864 }
865
866 #[test]
867 fn find_by_hash_returns_none_when_hash_not_found() -> TestResult {
868 let conn = setup_conn()?;
869 let result = find_by_hash(&conn, "global", "hash-inexistente")?;
870 assert!(result.is_none());
871 Ok(())
872 }
873
874 #[test]
875 fn find_by_hash_ignores_different_namespace() -> TestResult {
876 let conn = setup_conn()?;
877 let m = new_memory("mem-ns");
878 insert(&conn, &m)?;
879
880 let result = find_by_hash(&conn, "outro-namespace", "hash-mem-ns")?;
881 assert!(result.is_none());
882 Ok(())
883 }
884
885 #[test]
886 fn read_by_name_returns_full_memory() -> TestResult {
887 let conn = setup_conn()?;
888 let m = new_memory("mem-read");
889 let id = insert(&conn, &m)?;
890
891 let row = read_by_name(&conn, "global", "mem-read")?.ok_or("mem-read should exist")?;
892 assert_eq!(row.id, id);
893 assert_eq!(row.name, "mem-read");
894 assert_eq!(row.memory_type, "user");
895 assert_eq!(row.body, "test memory body");
896 assert_eq!(row.namespace, "global");
897 Ok(())
898 }
899
900 #[test]
901 fn read_by_name_returns_none_for_missing() -> TestResult {
902 let conn = setup_conn()?;
903 let result = read_by_name(&conn, "global", "nao-existe")?;
904 assert!(result.is_none());
905 Ok(())
906 }
907
908 #[test]
909 fn read_full_by_id_returns_memory() -> TestResult {
910 let conn = setup_conn()?;
911 let m = new_memory("mem-full");
912 let id = insert(&conn, &m)?;
913
914 let row = read_full(&conn, id)?.ok_or("mem-full should exist")?;
915 assert_eq!(row.id, id);
916 assert_eq!(row.name, "mem-full");
917 Ok(())
918 }
919
920 #[test]
921 fn read_full_returns_none_for_missing_id() -> TestResult {
922 let conn = setup_conn()?;
923 let result = read_full(&conn, 9999)?;
924 assert!(result.is_none());
925 Ok(())
926 }
927
928 #[test]
929 fn update_without_optimism_modifies_fields() -> TestResult {
930 let conn = setup_conn()?;
931 let m = new_memory("mem-upd");
932 let id = insert(&conn, &m)?;
933
934 let mut m2 = new_memory("mem-upd");
935 m2.body = "updated body".to_string();
936 m2.body_hash = "hash-novo".to_string();
937 let ok = update(&conn, id, &m2, None)?;
938 assert!(ok);
939
940 let row = read_full(&conn, id)?.ok_or("mem-upd should exist")?;
941 assert_eq!(row.body, "updated body");
942 assert_eq!(row.body_hash, "hash-novo");
943 Ok(())
944 }
945
946 #[test]
947 fn update_with_correct_expected_updated_at_succeeds() -> TestResult {
948 let conn = setup_conn()?;
949 let m = new_memory("mem-opt");
950 let id = insert(&conn, &m)?;
951
952 let (_, updated_at, _) =
953 find_by_name(&conn, "global", "mem-opt")?.ok_or("mem-opt should exist")?;
954
955 let mut m2 = new_memory("mem-opt");
956 m2.body = "optimistic body".to_string();
957 m2.body_hash = "hash-optimistic".to_string();
958 let ok = update(&conn, id, &m2, Some(updated_at))?;
959 assert!(ok);
960
961 let row = read_full(&conn, id)?.ok_or("mem-opt should exist after update")?;
962 assert_eq!(row.body, "optimistic body");
963 Ok(())
964 }
965
966 #[test]
967 fn update_with_wrong_expected_updated_at_returns_false() -> TestResult {
968 let conn = setup_conn()?;
969 let m = new_memory("mem-conflict");
970 let id = insert(&conn, &m)?;
971
972 let mut m2 = new_memory("mem-conflict");
973 m2.body = "must not appear".to_string();
974 m2.body_hash = "hash-x".to_string();
975 let ok = update(&conn, id, &m2, Some(0))?;
976 assert!(!ok);
977
978 let row = read_full(&conn, id)?.ok_or("mem-conflict should exist")?;
979 assert_eq!(row.body, "test memory body");
980 Ok(())
981 }
982
983 #[test]
984 fn update_missing_id_returns_false() -> TestResult {
985 let conn = setup_conn()?;
986 let m = new_memory("fantasma");
987 let ok = update(&conn, 9999, &m, None)?;
988 assert!(!ok);
989 Ok(())
990 }
991
992 #[test]
993 fn soft_delete_marks_deleted_at() -> TestResult {
994 let conn = setup_conn()?;
995 let m = new_memory("mem-del");
996 insert(&conn, &m)?;
997
998 let ok = soft_delete(&conn, "global", "mem-del")?;
999 assert!(ok);
1000
1001 let result = find_by_name(&conn, "global", "mem-del")?;
1002 assert!(result.is_none());
1003
1004 let result_read = read_by_name(&conn, "global", "mem-del")?;
1005 assert!(result_read.is_none());
1006 Ok(())
1007 }
1008
1009 #[test]
1010 fn soft_delete_returns_false_when_not_found() -> TestResult {
1011 let conn = setup_conn()?;
1012 let ok = soft_delete(&conn, "global", "nao-existe")?;
1013 assert!(!ok);
1014 Ok(())
1015 }
1016
1017 #[test]
1018 fn double_soft_delete_returns_false_on_second_call() -> TestResult {
1019 let conn = setup_conn()?;
1020 let m = new_memory("mem-del2");
1021 insert(&conn, &m)?;
1022
1023 soft_delete(&conn, "global", "mem-del2")?;
1024 let ok = soft_delete(&conn, "global", "mem-del2")?;
1025 assert!(!ok);
1026 Ok(())
1027 }
1028
1029 #[test]
1030 fn list_returns_memories_from_namespace() -> TestResult {
1031 let conn = setup_conn()?;
1032 insert(&conn, &new_memory("mem-list-a"))?;
1033 insert(&conn, &new_memory("mem-list-b"))?;
1034
1035 let rows = list(&conn, "global", None, 10, 0, false)?;
1036 assert!(rows.len() >= 2);
1037 let nomes: Vec<_> = rows.iter().map(|r| r.name.as_str()).collect();
1038 assert!(nomes.contains(&"mem-list-a"));
1039 assert!(nomes.contains(&"mem-list-b"));
1040 Ok(())
1041 }
1042
1043 #[test]
1044 fn list_with_type_filter_returns_only_correct_type() -> TestResult {
1045 let conn = setup_conn()?;
1046 insert(&conn, &new_memory("mem-user"))?;
1047
1048 let mut m2 = new_memory("mem-feedback");
1049 m2.memory_type = "feedback".to_string();
1050 insert(&conn, &m2)?;
1051
1052 let rows_user = list(&conn, "global", Some("user"), 10, 0, false)?;
1053 assert!(rows_user.iter().all(|r| r.memory_type == "user"));
1054
1055 let rows_fb = list(&conn, "global", Some("feedback"), 10, 0, false)?;
1056 assert!(rows_fb.iter().all(|r| r.memory_type == "feedback"));
1057 Ok(())
1058 }
1059
1060 #[test]
1061 fn list_exclui_soft_deleted() -> TestResult {
1062 let conn = setup_conn()?;
1063 let m = new_memory("mem-excluida");
1064 insert(&conn, &m)?;
1065 soft_delete(&conn, "global", "mem-excluida")?;
1066
1067 let rows = list(&conn, "global", None, 10, 0, false)?;
1068 assert!(rows.iter().all(|r| r.name != "mem-excluida"));
1069 Ok(())
1070 }
1071
1072 #[test]
1073 fn list_pagination_works() -> TestResult {
1074 let conn = setup_conn()?;
1075 for i in 0..5 {
1076 insert(&conn, &new_memory(&format!("mem-pag-{i}")))?;
1077 }
1078
1079 let pagina1 = list(&conn, "global", None, 2, 0, false)?;
1080 let pagina2 = list(&conn, "global", None, 2, 2, false)?;
1081 assert!(pagina1.len() <= 2);
1082 assert!(pagina2.len() <= 2);
1083 if !pagina1.is_empty() && !pagina2.is_empty() {
1084 assert_ne!(pagina1[0].id, pagina2[0].id);
1085 }
1086 Ok(())
1087 }
1088
1089 #[test]
1090 fn upsert_vec_and_delete_vec_work() -> TestResult {
1091 let conn = setup_conn()?;
1092 let m = new_memory("mem-vec");
1093 let id = insert(&conn, &m)?;
1094
1095 let embedding: Vec<f32> = vec![0.1; 384];
1096 upsert_vec(
1097 &conn, id, "global", "user", &embedding, "mem-vec", "snippet",
1098 )?;
1099
1100 let count: i64 = conn.query_row(
1101 "SELECT COUNT(*) FROM vec_memories WHERE memory_id = ?1",
1102 params![id],
1103 |r| r.get(0),
1104 )?;
1105 assert_eq!(count, 1);
1106
1107 delete_vec(&conn, id)?;
1108
1109 let count_after: i64 = conn.query_row(
1110 "SELECT COUNT(*) FROM vec_memories WHERE memory_id = ?1",
1111 params![id],
1112 |r| r.get(0),
1113 )?;
1114 assert_eq!(count_after, 0);
1115 Ok(())
1116 }
1117
1118 #[test]
1119 fn upsert_vec_replaces_existing_vector() -> TestResult {
1120 let conn = setup_conn()?;
1121 let m = new_memory("mem-vec-upsert");
1122 let id = insert(&conn, &m)?;
1123
1124 let emb1: Vec<f32> = vec![0.1; 384];
1125 upsert_vec(&conn, id, "global", "user", &emb1, "mem-vec-upsert", "s1")?;
1126
1127 let emb2: Vec<f32> = vec![0.9; 384];
1128 upsert_vec(&conn, id, "global", "user", &emb2, "mem-vec-upsert", "s2")?;
1129
1130 let count: i64 = conn.query_row(
1131 "SELECT COUNT(*) FROM vec_memories WHERE memory_id = ?1",
1132 params![id],
1133 |r| r.get(0),
1134 )?;
1135 assert_eq!(count, 1);
1136 Ok(())
1137 }
1138
1139 #[test]
1140 fn knn_search_returns_results_by_distance() -> TestResult {
1141 let conn = setup_conn()?;
1142
1143 let ma = new_memory("mem-knn-a");
1145 let id_a = insert(&conn, &ma)?;
1146 let emb_a: Vec<f32> = vec![1.0; 384];
1147 upsert_vec(&conn, id_a, "global", "user", &emb_a, "mem-knn-a", "s")?;
1148
1149 let mb = new_memory("mem-knn-b");
1151 let id_b = insert(&conn, &mb)?;
1152 let emb_b: Vec<f32> = vec![-1.0; 384];
1153 upsert_vec(&conn, id_b, "global", "user", &emb_b, "mem-knn-b", "s")?;
1154
1155 let query: Vec<f32> = vec![1.0; 384];
1156 let results = knn_search(&conn, &query, &["global".to_string()], None, 2)?;
1157 assert!(!results.is_empty());
1158 assert_eq!(results[0].0, id_a);
1159 Ok(())
1160 }
1161
1162 #[test]
1163 fn knn_search_with_type_filter_restricts_result() -> TestResult {
1164 let conn = setup_conn()?;
1165
1166 let ma = new_memory("mem-knn-tipo-user");
1167 let id_a = insert(&conn, &ma)?;
1168 let emb: Vec<f32> = vec![1.0; 384];
1169 upsert_vec(
1170 &conn,
1171 id_a,
1172 "global",
1173 "user",
1174 &emb,
1175 "mem-knn-tipo-user",
1176 "s",
1177 )?;
1178
1179 let mut mb = new_memory("mem-knn-tipo-fb");
1180 mb.memory_type = "feedback".to_string();
1181 let id_b = insert(&conn, &mb)?;
1182 upsert_vec(
1183 &conn,
1184 id_b,
1185 "global",
1186 "feedback",
1187 &emb,
1188 "mem-knn-tipo-fb",
1189 "s",
1190 )?;
1191
1192 let query: Vec<f32> = vec![1.0; 384];
1193 let results_user = knn_search(&conn, &query, &["global".to_string()], Some("user"), 5)?;
1194 assert!(results_user.iter().all(|(id, _)| *id == id_a));
1195
1196 let results_fb = knn_search(&conn, &query, &["global".to_string()], Some("feedback"), 5)?;
1197 assert!(results_fb.iter().all(|(id, _)| *id == id_b));
1198 Ok(())
1199 }
1200
1201 #[test]
1202 fn fts_search_finds_by_prefix_in_body() -> TestResult {
1203 let conn = setup_conn()?;
1204 let mut m = new_memory("mem-fts");
1205 m.body = "linguagem de programacao rust".to_string();
1206 insert(&conn, &m)?;
1207
1208 conn.execute_batch(
1209 "INSERT INTO fts_memories(rowid, name, description, body)
1210 SELECT id, name, description, body FROM memories WHERE deleted_at IS NULL",
1211 )?;
1212
1213 let rows = fts_search(&conn, "programacao", "global", None, 10)?;
1214 assert!(!rows.is_empty());
1215 assert!(rows.iter().any(|r| r.name == "mem-fts"));
1216 Ok(())
1217 }
1218
1219 #[test]
1220 fn fts_search_with_type_filter() -> TestResult {
1221 let conn = setup_conn()?;
1222 let mut m = new_memory("mem-fts-tipo");
1223 m.body = "linguagem especial para filtro".to_string();
1224 insert(&conn, &m)?;
1225
1226 let mut m2 = new_memory("mem-fts-feedback");
1227 m2.memory_type = "feedback".to_string();
1228 m2.body = "linguagem especial para filtro".to_string();
1229 insert(&conn, &m2)?;
1230
1231 conn.execute_batch(
1232 "INSERT INTO fts_memories(rowid, name, description, body)
1233 SELECT id, name, description, body FROM memories WHERE deleted_at IS NULL",
1234 )?;
1235
1236 let rows_user = fts_search(&conn, "especial", "global", Some("user"), 10)?;
1237 assert!(rows_user.iter().all(|r| r.memory_type == "user"));
1238
1239 let rows_fb = fts_search(&conn, "especial", "global", Some("feedback"), 10)?;
1240 assert!(rows_fb.iter().all(|r| r.memory_type == "feedback"));
1241 Ok(())
1242 }
1243
1244 #[test]
1245 fn fts_search_excludes_deleted() -> TestResult {
1246 let conn = setup_conn()?;
1247 let mut m = new_memory("mem-fts-del");
1248 m.body = "deleted fts content".to_string();
1249 insert(&conn, &m)?;
1250
1251 conn.execute_batch(
1252 "INSERT INTO fts_memories(rowid, name, description, body)
1253 SELECT id, name, description, body FROM memories WHERE deleted_at IS NULL",
1254 )?;
1255
1256 soft_delete(&conn, "global", "mem-fts-del")?;
1257
1258 let rows = fts_search(&conn, "deleted", "global", None, 10)?;
1259 assert!(rows.iter().all(|r| r.name != "mem-fts-del"));
1260 Ok(())
1261 }
1262
1263 #[test]
1264 fn list_deleted_before_returns_correct_ids() -> TestResult {
1265 let conn = setup_conn()?;
1266 let m = new_memory("mem-purge");
1267 insert(&conn, &m)?;
1268 soft_delete(&conn, "global", "mem-purge")?;
1269
1270 let ids = list_deleted_before(&conn, "global", i64::MAX)?;
1271 assert!(!ids.is_empty());
1272
1273 let ids_antes = list_deleted_before(&conn, "global", 0)?;
1274 assert!(ids_antes.is_empty());
1275 Ok(())
1276 }
1277
1278 #[test]
1279 fn find_by_name_returns_correct_max_version() -> TestResult {
1280 let conn = setup_conn()?;
1281 let m = new_memory("mem-ver");
1282 let id = insert(&conn, &m)?;
1283
1284 let (_, _, v0) = find_by_name(&conn, "global", "mem-ver")?.ok_or("mem-ver should exist")?;
1285 assert_eq!(v0, 0);
1286
1287 conn.execute(
1288 "INSERT INTO memory_versions (memory_id, version, name, type, description, body, metadata, change_reason)
1289 VALUES (?1, 1, 'mem-ver', 'user', 'desc', 'body', '{}', 'create')",
1290 params![id],
1291 )?;
1292
1293 let (_, _, v1) =
1294 find_by_name(&conn, "global", "mem-ver")?.ok_or("mem-ver should exist after insert")?;
1295 assert_eq!(v1, 1);
1296 Ok(())
1297 }
1298
1299 #[test]
1300 fn insert_com_metadata_json() -> TestResult {
1301 let conn = setup_conn()?;
1302 let mut m = new_memory("mem-meta");
1303 m.metadata = serde_json::json!({"chave": "valor", "numero": 42});
1304 let id = insert(&conn, &m)?;
1305
1306 let row = read_full(&conn, id)?.ok_or("mem-meta should exist")?;
1307 let meta: serde_json::Value = serde_json::from_str(&row.metadata)?;
1308 assert_eq!(meta["chave"], "valor");
1309 assert_eq!(meta["numero"], 42);
1310 Ok(())
1311 }
1312
1313 #[test]
1314 fn insert_com_session_id() -> TestResult {
1315 let conn = setup_conn()?;
1316 let mut m = new_memory("mem-session");
1317 m.session_id = Some("sessao-xyz".to_string());
1318 let id = insert(&conn, &m)?;
1319
1320 let row = read_full(&conn, id)?.ok_or("mem-session should exist")?;
1321 assert_eq!(row.session_id, Some("sessao-xyz".to_string()));
1322 Ok(())
1323 }
1324
1325 #[test]
1326 fn delete_vec_for_nonexistent_id_does_not_fail() -> TestResult {
1327 let conn = setup_conn()?;
1328 let result = delete_vec(&conn, 99999);
1329 assert!(result.is_ok());
1330 Ok(())
1331 }
1332
1333 #[test]
1334 fn preprocess_fts_query_no_separators() {
1335 assert_eq!(preprocess_fts_query("hello"), "hello*");
1336 assert_eq!(preprocess_fts_query("hello world"), "hello* world*");
1337 }
1338
1339 #[test]
1340 fn preprocess_fts_query_with_hyphens() {
1341 let result = preprocess_fts_query("graphrag-precompact");
1342 assert!(result.contains("\"graphrag precompact\""));
1343 assert!(result.contains("graphrag*"));
1344 assert!(result.contains("precompact*"));
1345 }
1346
1347 #[test]
1348 fn preprocess_fts_query_with_dots() {
1349 let result = preprocess_fts_query("v1.0.44");
1350 assert!(result.contains("\"v1 0 44\""));
1351 assert!(result.contains("v1*"));
1352 assert!(result.contains("44*"));
1353 }
1354
1355 #[test]
1356 fn preprocess_fts_query_with_mixed_separators() {
1357 let result = preprocess_fts_query("graphrag-precompact.sh");
1358 assert!(result.contains("\"graphrag precompact sh\""));
1359 assert!(result.contains("graphrag*"));
1360 }
1361
1362 #[test]
1363 fn preprocess_fts_query_empty_and_whitespace() {
1364 assert_eq!(preprocess_fts_query(""), "");
1365 assert_eq!(preprocess_fts_query(" "), "");
1366 }
1367
1368 #[test]
1369 fn preprocess_fts_query_strips_quotes() {
1370 let result = preprocess_fts_query(r#"hello "world"#);
1371 assert!(result.contains("hello*"));
1372 assert!(result.contains("world*"));
1373 }
1374
1375 #[test]
1376 fn preprocess_fts_query_strips_asterisks() {
1377 assert_eq!(preprocess_fts_query("test*"), "test*");
1378 }
1379
1380 #[test]
1381 fn preprocess_fts_query_strips_parens() {
1382 let result = preprocess_fts_query("(hello)");
1383 assert!(result.contains("hello*"));
1384 assert!(!result.contains('('));
1385 }
1386
1387 #[test]
1388 fn preprocess_fts_query_filters_fts_keywords() {
1389 let result = preprocess_fts_query("foo OR bar");
1390 assert!(result.contains("foo*"));
1391 assert!(result.contains("bar*"));
1392 assert!(!result.contains("OR*"));
1393 }
1394
1395 #[test]
1396 fn preprocess_fts_query_only_fts_keywords() {
1397 assert_eq!(preprocess_fts_query("OR AND NOT"), "");
1398 }
1399
1400 #[test]
1401 fn preprocess_fts_query_keywords_with_separators() {
1402 let result = preprocess_fts_query("hello-OR-world");
1403 assert!(result.contains("hello*"));
1404 assert!(result.contains("world*"));
1405 assert!(!result.contains("OR*"));
1406 }
1407
1408 #[test]
1409 fn fts_search_finds_compound_term_with_hyphen() -> TestResult {
1410 let conn = setup_conn()?;
1411 let mut m = new_memory("mem-compound");
1412 m.body = "the graphrag-precompact script runs daily".to_string();
1413 insert(&conn, &m)?;
1414 conn.execute_batch(
1415 "INSERT INTO fts_memories(rowid, name, description, body)
1416 SELECT id, name, description, body FROM memories WHERE deleted_at IS NULL",
1417 )?;
1418 let rows = fts_search(&conn, "graphrag-precompact", "global", None, 10)?;
1419 assert!(!rows.is_empty(), "should find compound hyphenated term");
1420 Ok(())
1421 }
1422
1423 #[test]
1424 fn find_by_name_any_state_returns_deleted_flag() -> TestResult {
1425 let conn = setup_conn()?;
1426 let m = new_memory("mem-soft-del");
1427 let id = insert(&conn, &m)?;
1428 conn.execute(
1429 "UPDATE memories SET deleted_at = unixepoch() WHERE id = ?1",
1430 rusqlite::params![id],
1431 )?;
1432 let result = find_by_name_any_state(&conn, "global", "mem-soft-del")?;
1433 assert_eq!(result, Some((id, true)));
1434 Ok(())
1435 }
1436
1437 #[test]
1438 fn find_by_name_any_state_returns_not_deleted() -> TestResult {
1439 let conn = setup_conn()?;
1440 let m = new_memory("mem-active");
1441 let id = insert(&conn, &m)?;
1442 let result = find_by_name_any_state(&conn, "global", "mem-active")?;
1443 assert_eq!(result, Some((id, false)));
1444 Ok(())
1445 }
1446
1447 #[test]
1448 fn find_by_name_any_state_returns_none_when_absent() -> TestResult {
1449 let conn = setup_conn()?;
1450 let result = find_by_name_any_state(&conn, "global", "does-not-exist")?;
1451 assert!(result.is_none());
1452 Ok(())
1453 }
1454
1455 #[test]
1456 fn clear_deleted_at_restores_memory() -> TestResult {
1457 let conn = setup_conn()?;
1458 let m = new_memory("mem-restore");
1459 let id = insert(&conn, &m)?;
1460 conn.execute(
1461 "UPDATE memories SET deleted_at = unixepoch() WHERE id = ?1",
1462 rusqlite::params![id],
1463 )?;
1464 assert!(find_by_name(&conn, "global", "mem-restore")?.is_none());
1466 clear_deleted_at(&conn, id)?;
1467 let found = find_by_name(&conn, "global", "mem-restore")?;
1469 assert!(found.is_some());
1470 assert_eq!(found.unwrap().0, id);
1471 Ok(())
1472 }
1473}