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_hash(
111 conn: &Connection,
112 namespace: &str,
113 body_hash: &str,
114) -> Result<Option<i64>, AppError> {
115 let mut stmt = conn.prepare_cached(
116 "SELECT id FROM memories WHERE namespace = ?1 AND body_hash = ?2 AND deleted_at IS NULL",
117 )?;
118 match stmt.query_row(params![namespace, body_hash], |r| r.get(0)) {
119 Ok(id) => Ok(Some(id)),
120 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
121 Err(e) => Err(AppError::Database(e)),
122 }
123}
124
125pub fn insert(conn: &Connection, m: &NewMemory) -> Result<i64, AppError> {
141 conn.execute(
142 "INSERT INTO memories (namespace, name, type, description, body, body_hash, session_id, source, metadata)
143 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
144 params![
145 m.namespace, m.name, m.memory_type, m.description, m.body,
146 m.body_hash, m.session_id, m.source,
147 serde_json::to_string(&m.metadata)?
148 ],
149 )?;
150 Ok(conn.last_insert_rowid())
151}
152
153pub fn update(
168 conn: &Connection,
169 id: i64,
170 m: &NewMemory,
171 expected_updated_at: Option<i64>,
172) -> Result<bool, AppError> {
173 let affected = if let Some(ts) = expected_updated_at {
174 conn.execute(
175 "UPDATE memories SET type=?2, description=?3, body=?4, body_hash=?5,
176 session_id=?6, source=?7, metadata=?8
177 WHERE id=?1 AND updated_at=?9 AND deleted_at IS NULL",
178 params![
179 id,
180 m.memory_type,
181 m.description,
182 m.body,
183 m.body_hash,
184 m.session_id,
185 m.source,
186 serde_json::to_string(&m.metadata)?,
187 ts
188 ],
189 )?
190 } else {
191 conn.execute(
192 "UPDATE memories SET type=?2, description=?3, body=?4, body_hash=?5,
193 session_id=?6, source=?7, metadata=?8
194 WHERE id=?1 AND deleted_at IS NULL",
195 params![
196 id,
197 m.memory_type,
198 m.description,
199 m.body,
200 m.body_hash,
201 m.session_id,
202 m.source,
203 serde_json::to_string(&m.metadata)?
204 ],
205 )?
206 };
207 Ok(affected == 1)
208}
209
210pub fn upsert_vec(
220 conn: &Connection,
221 memory_id: i64,
222 namespace: &str,
223 memory_type: &str,
224 embedding: &[f32],
225 name: &str,
226 snippet: &str,
227) -> Result<(), AppError> {
228 let embedding_bytes = f32_to_bytes(embedding);
233 with_busy_retry(|| {
234 conn.execute(
235 "DELETE FROM vec_memories WHERE memory_id = ?1",
236 params![memory_id],
237 )?;
238 conn.execute(
239 "INSERT INTO vec_memories(memory_id, namespace, type, embedding, name, snippet)
240 VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
241 params![
242 memory_id,
243 namespace,
244 memory_type,
245 &embedding_bytes,
246 name,
247 snippet
248 ],
249 )?;
250 Ok(())
251 })
252}
253
254pub fn delete_vec(conn: &Connection, memory_id: i64) -> Result<(), AppError> {
263 conn.execute(
264 "DELETE FROM vec_memories WHERE memory_id = ?1",
265 params![memory_id],
266 )?;
267 Ok(())
268}
269
270pub fn read_by_name(
280 conn: &Connection,
281 namespace: &str,
282 name: &str,
283) -> Result<Option<MemoryRow>, AppError> {
284 let mut stmt = conn.prepare_cached(
285 "SELECT id, namespace, name, type, description, body, body_hash,
286 session_id, source, metadata, created_at, updated_at, deleted_at
287 FROM memories WHERE namespace=?1 AND name=?2 AND deleted_at IS NULL",
288 )?;
289 match stmt.query_row(params![namespace, name], |r| {
290 Ok(MemoryRow {
291 id: r.get(0)?,
292 namespace: r.get(1)?,
293 name: r.get(2)?,
294 memory_type: r.get(3)?,
295 description: r.get(4)?,
296 body: r.get(5)?,
297 body_hash: r.get(6)?,
298 session_id: r.get(7)?,
299 source: r.get(8)?,
300 metadata: r.get(9)?,
301 created_at: r.get(10)?,
302 updated_at: r.get(11)?,
303 deleted_at: r.get(12)?,
304 })
305 }) {
306 Ok(m) => Ok(Some(m)),
307 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
308 Err(e) => Err(AppError::Database(e)),
309 }
310}
311
312pub fn soft_delete(conn: &Connection, namespace: &str, name: &str) -> Result<bool, AppError> {
326 let affected = conn.execute(
327 "UPDATE memories SET deleted_at = unixepoch() WHERE namespace=?1 AND name=?2 AND deleted_at IS NULL",
328 params![namespace, name],
329 )?;
330 Ok(affected == 1)
331}
332
333pub fn list(
344 conn: &Connection,
345 namespace: &str,
346 memory_type: Option<&str>,
347 limit: usize,
348 offset: usize,
349 include_deleted: bool,
350) -> Result<Vec<MemoryRow>, AppError> {
351 let deleted_clause = if include_deleted {
352 ""
353 } else {
354 " AND deleted_at IS NULL"
355 };
356 if let Some(mt) = memory_type {
357 let sql = format!(
358 "SELECT id, namespace, name, type, description, body, body_hash,
359 session_id, source, metadata, created_at, updated_at, deleted_at
360 FROM memories WHERE namespace=?1 AND type=?2{deleted_clause}
361 ORDER BY updated_at DESC LIMIT ?3 OFFSET ?4"
362 );
363 let mut stmt = conn.prepare(&sql)?;
364 let rows = stmt
365 .query_map(params![namespace, mt, limit as i64, offset as i64], |r| {
366 Ok(MemoryRow {
367 id: r.get(0)?,
368 namespace: r.get(1)?,
369 name: r.get(2)?,
370 memory_type: r.get(3)?,
371 description: r.get(4)?,
372 body: r.get(5)?,
373 body_hash: r.get(6)?,
374 session_id: r.get(7)?,
375 source: r.get(8)?,
376 metadata: r.get(9)?,
377 created_at: r.get(10)?,
378 updated_at: r.get(11)?,
379 deleted_at: r.get(12)?,
380 })
381 })?
382 .collect::<Result<Vec<_>, _>>()?;
383 Ok(rows)
384 } else {
385 let sql = format!(
386 "SELECT id, namespace, name, type, description, body, body_hash,
387 session_id, source, metadata, created_at, updated_at, deleted_at
388 FROM memories WHERE namespace=?1{deleted_clause}
389 ORDER BY updated_at DESC LIMIT ?2 OFFSET ?3"
390 );
391 let mut stmt = conn.prepare(&sql)?;
392 let rows = stmt
393 .query_map(params![namespace, limit as i64, offset as i64], |r| {
394 Ok(MemoryRow {
395 id: r.get(0)?,
396 namespace: r.get(1)?,
397 name: r.get(2)?,
398 memory_type: r.get(3)?,
399 description: r.get(4)?,
400 body: r.get(5)?,
401 body_hash: r.get(6)?,
402 session_id: r.get(7)?,
403 source: r.get(8)?,
404 metadata: r.get(9)?,
405 created_at: r.get(10)?,
406 updated_at: r.get(11)?,
407 deleted_at: r.get(12)?,
408 })
409 })?
410 .collect::<Result<Vec<_>, _>>()?;
411 Ok(rows)
412 }
413}
414
415pub fn knn_search(
432 conn: &Connection,
433 embedding: &[f32],
434 namespaces: &[String],
435 memory_type: Option<&str>,
436 k: usize,
437) -> Result<Vec<(i64, f32)>, AppError> {
438 let bytes = f32_to_bytes(embedding);
439
440 match namespaces.len() {
441 0 => {
442 if let Some(mt) = memory_type {
444 let mut stmt = conn.prepare(
445 "SELECT memory_id, distance FROM vec_memories \
446 WHERE embedding MATCH ?1 AND type = ?2 \
447 ORDER BY distance LIMIT ?3",
448 )?;
449 let rows = stmt
450 .query_map(params![bytes, mt, k as i64], |r| {
451 Ok((r.get::<_, i64>(0)?, r.get::<_, f32>(1)?))
452 })?
453 .collect::<Result<Vec<_>, _>>()?;
454 Ok(rows)
455 } else {
456 let mut stmt = conn.prepare(
457 "SELECT memory_id, distance FROM vec_memories \
458 WHERE embedding MATCH ?1 \
459 ORDER BY distance LIMIT ?2",
460 )?;
461 let rows = stmt
462 .query_map(params![bytes, k as i64], |r| {
463 Ok((r.get::<_, i64>(0)?, r.get::<_, f32>(1)?))
464 })?
465 .collect::<Result<Vec<_>, _>>()?;
466 Ok(rows)
467 }
468 }
469 1 => {
470 let ns = &namespaces[0];
472 if let Some(mt) = memory_type {
473 let mut stmt = conn.prepare(
474 "SELECT memory_id, distance FROM vec_memories \
475 WHERE embedding MATCH ?1 AND namespace = ?2 AND type = ?3 \
476 ORDER BY distance LIMIT ?4",
477 )?;
478 let rows = stmt
479 .query_map(params![bytes, ns, mt, k as i64], |r| {
480 Ok((r.get::<_, i64>(0)?, r.get::<_, f32>(1)?))
481 })?
482 .collect::<Result<Vec<_>, _>>()?;
483 Ok(rows)
484 } else {
485 let mut stmt = conn.prepare(
486 "SELECT memory_id, distance FROM vec_memories \
487 WHERE embedding MATCH ?1 AND namespace = ?2 \
488 ORDER BY distance LIMIT ?3",
489 )?;
490 let rows = stmt
491 .query_map(params![bytes, ns, k as i64], |r| {
492 Ok((r.get::<_, i64>(0)?, r.get::<_, f32>(1)?))
493 })?
494 .collect::<Result<Vec<_>, _>>()?;
495 Ok(rows)
496 }
497 }
498 _ => {
499 let placeholders = (0..namespaces.len())
502 .map(|_| "?")
503 .collect::<Vec<_>>()
504 .join(",");
505 if let Some(mt) = memory_type {
506 let query = format!(
507 "SELECT memory_id, distance FROM vec_memories \
508 WHERE embedding MATCH ? AND type = ? AND namespace IN ({placeholders}) \
509 ORDER BY distance LIMIT ?"
510 );
511 let mut stmt = conn.prepare(&query)?;
512 let mut raw_params: Vec<Box<dyn rusqlite::ToSql>> =
514 vec![Box::new(bytes), Box::new(mt.to_string())];
515 for ns in namespaces {
516 raw_params.push(Box::new(ns.clone()));
517 }
518 raw_params.push(Box::new(k as i64));
519 let param_refs: Vec<&dyn rusqlite::ToSql> =
520 raw_params.iter().map(|b| b.as_ref()).collect();
521 let rows = stmt
522 .query_map(param_refs.as_slice(), |r| {
523 Ok((r.get::<_, i64>(0)?, r.get::<_, f32>(1)?))
524 })?
525 .collect::<Result<Vec<_>, _>>()?;
526 Ok(rows)
527 } else {
528 let query = format!(
529 "SELECT memory_id, distance FROM vec_memories \
530 WHERE embedding MATCH ? AND namespace IN ({placeholders}) \
531 ORDER BY distance LIMIT ?"
532 );
533 let mut stmt = conn.prepare(&query)?;
534 let mut raw_params: Vec<Box<dyn rusqlite::ToSql>> = vec![Box::new(bytes)];
536 for ns in namespaces {
537 raw_params.push(Box::new(ns.clone()));
538 }
539 raw_params.push(Box::new(k as i64));
540 let param_refs: Vec<&dyn rusqlite::ToSql> =
541 raw_params.iter().map(|b| b.as_ref()).collect();
542 let rows = stmt
543 .query_map(param_refs.as_slice(), |r| {
544 Ok((r.get::<_, i64>(0)?, r.get::<_, f32>(1)?))
545 })?
546 .collect::<Result<Vec<_>, _>>()?;
547 Ok(rows)
548 }
549 }
550 }
551}
552
553pub fn read_full(conn: &Connection, memory_id: i64) -> Result<Option<MemoryRow>, AppError> {
561 let mut stmt = conn.prepare_cached(
562 "SELECT id, namespace, name, type, description, body, body_hash,
563 session_id, source, metadata, created_at, updated_at, deleted_at
564 FROM memories WHERE id=?1 AND deleted_at IS NULL",
565 )?;
566 match stmt.query_row(params![memory_id], |r| {
567 Ok(MemoryRow {
568 id: r.get(0)?,
569 namespace: r.get(1)?,
570 name: r.get(2)?,
571 memory_type: r.get(3)?,
572 description: r.get(4)?,
573 body: r.get(5)?,
574 body_hash: r.get(6)?,
575 session_id: r.get(7)?,
576 source: r.get(8)?,
577 metadata: r.get(9)?,
578 created_at: r.get(10)?,
579 updated_at: r.get(11)?,
580 deleted_at: r.get(12)?,
581 })
582 }) {
583 Ok(m) => Ok(Some(m)),
584 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
585 Err(e) => Err(AppError::Database(e)),
586 }
587}
588
589pub fn list_deleted_before(
598 conn: &Connection,
599 namespace: &str,
600 before_ts: i64,
601) -> Result<Vec<i64>, AppError> {
602 let mut stmt = conn.prepare_cached(
603 "SELECT id FROM memories WHERE namespace = ?1 AND deleted_at IS NOT NULL AND deleted_at < ?2",
604 )?;
605 let ids = stmt
606 .query_map(params![namespace, before_ts], |r| r.get::<_, i64>(0))?
607 .collect::<Result<Vec<_>, _>>()?;
608 Ok(ids)
609}
610
611fn preprocess_fts_query(raw: &str) -> String {
621 const SEPARATORS: &[char] = &['-', '.', '_', '/'];
622 let trimmed = raw.trim();
623 if trimmed.is_empty() {
624 return String::new();
625 }
626 if !trimmed.chars().any(|c| SEPARATORS.contains(&c)) {
627 return trimmed
628 .split_whitespace()
629 .map(|t| format!("{t}*"))
630 .collect::<Vec<_>>()
631 .join(" ");
632 }
633 let tokens: Vec<&str> = trimmed
634 .split(|c: char| SEPARATORS.contains(&c) || c.is_whitespace())
635 .filter(|t| !t.is_empty())
636 .collect();
637 if tokens.is_empty() {
638 return String::new();
639 }
640 let phrase = format!("\"{}\"", tokens.join(" "));
641 let prefix_terms: Vec<String> = tokens.iter().map(|t| format!("{t}*")).collect();
642 format!("{phrase} OR {}", prefix_terms.join(" OR "))
643}
644
645pub fn fts_search(
654 conn: &Connection,
655 query: &str,
656 namespace: &str,
657 memory_type: Option<&str>,
658 limit: usize,
659) -> Result<Vec<MemoryRow>, AppError> {
660 let fts_query = preprocess_fts_query(query);
661 if let Some(mt) = memory_type {
662 let mut stmt = conn.prepare(
663 "SELECT m.id, m.namespace, m.name, m.type, m.description, m.body, m.body_hash,
664 m.session_id, m.source, m.metadata, m.created_at, m.updated_at, m.deleted_at
665 FROM fts_memories fts
666 JOIN memories m ON m.id = fts.rowid
667 WHERE fts_memories MATCH ?1 AND m.namespace = ?2 AND m.type = ?3 AND m.deleted_at IS NULL
668 ORDER BY rank LIMIT ?4",
669 )?;
670 let rows = stmt
671 .query_map(params![fts_query, namespace, mt, limit as i64], |r| {
672 Ok(MemoryRow {
673 id: r.get(0)?,
674 namespace: r.get(1)?,
675 name: r.get(2)?,
676 memory_type: r.get(3)?,
677 description: r.get(4)?,
678 body: r.get(5)?,
679 body_hash: r.get(6)?,
680 session_id: r.get(7)?,
681 source: r.get(8)?,
682 metadata: r.get(9)?,
683 created_at: r.get(10)?,
684 updated_at: r.get(11)?,
685 deleted_at: r.get(12)?,
686 })
687 })?
688 .collect::<Result<Vec<_>, _>>()?;
689 Ok(rows)
690 } else {
691 let mut stmt = conn.prepare(
692 "SELECT m.id, m.namespace, m.name, m.type, m.description, m.body, m.body_hash,
693 m.session_id, m.source, m.metadata, m.created_at, m.updated_at, m.deleted_at
694 FROM fts_memories fts
695 JOIN memories m ON m.id = fts.rowid
696 WHERE fts_memories MATCH ?1 AND m.namespace = ?2 AND m.deleted_at IS NULL
697 ORDER BY rank LIMIT ?3",
698 )?;
699 let rows = stmt
700 .query_map(params![fts_query, namespace, limit as i64], |r| {
701 Ok(MemoryRow {
702 id: r.get(0)?,
703 namespace: r.get(1)?,
704 name: r.get(2)?,
705 memory_type: r.get(3)?,
706 description: r.get(4)?,
707 body: r.get(5)?,
708 body_hash: r.get(6)?,
709 session_id: r.get(7)?,
710 source: r.get(8)?,
711 metadata: r.get(9)?,
712 created_at: r.get(10)?,
713 updated_at: r.get(11)?,
714 deleted_at: r.get(12)?,
715 })
716 })?
717 .collect::<Result<Vec<_>, _>>()?;
718 Ok(rows)
719 }
720}
721
722#[cfg(test)]
723mod tests {
724 use super::*;
725 use rusqlite::Connection;
726
727 type TestResult = Result<(), Box<dyn std::error::Error>>;
728
729 fn setup_conn() -> Result<Connection, Box<dyn std::error::Error>> {
730 crate::storage::connection::register_vec_extension();
731 let mut conn = Connection::open_in_memory()?;
732 conn.execute_batch(
733 "PRAGMA foreign_keys = ON;
734 PRAGMA temp_store = MEMORY;",
735 )?;
736 crate::migrations::runner().run(&mut conn)?;
737 Ok(conn)
738 }
739
740 fn new_memory(name: &str) -> NewMemory {
741 NewMemory {
742 namespace: "global".to_string(),
743 name: name.to_string(),
744 memory_type: "user".to_string(),
745 description: "descricao de teste".to_string(),
746 body: "test memory body".to_string(),
747 body_hash: format!("hash-{name}"),
748 session_id: None,
749 source: "agent".to_string(),
750 metadata: serde_json::json!({}),
751 }
752 }
753
754 #[test]
755 fn insert_and_find_by_name_return_id() -> TestResult {
756 let conn = setup_conn()?;
757 let m = new_memory("mem-alpha");
758 let id = insert(&conn, &m)?;
759 assert!(id > 0);
760
761 let found = find_by_name(&conn, "global", "mem-alpha")?;
762 assert!(found.is_some());
763 let (found_id, _, _) = found.ok_or("mem-alpha should exist")?;
764 assert_eq!(found_id, id);
765 Ok(())
766 }
767
768 #[test]
769 fn find_by_name_returns_none_when_not_found() -> TestResult {
770 let conn = setup_conn()?;
771 let result = find_by_name(&conn, "global", "inexistente")?;
772 assert!(result.is_none());
773 Ok(())
774 }
775
776 #[test]
777 fn find_by_hash_returns_correct_id() -> TestResult {
778 let conn = setup_conn()?;
779 let m = new_memory("mem-hash");
780 let id = insert(&conn, &m)?;
781
782 let found = find_by_hash(&conn, "global", "hash-mem-hash")?;
783 assert_eq!(found, Some(id));
784 Ok(())
785 }
786
787 #[test]
788 fn find_by_hash_returns_none_when_hash_not_found() -> TestResult {
789 let conn = setup_conn()?;
790 let result = find_by_hash(&conn, "global", "hash-inexistente")?;
791 assert!(result.is_none());
792 Ok(())
793 }
794
795 #[test]
796 fn find_by_hash_ignores_different_namespace() -> TestResult {
797 let conn = setup_conn()?;
798 let m = new_memory("mem-ns");
799 insert(&conn, &m)?;
800
801 let result = find_by_hash(&conn, "outro-namespace", "hash-mem-ns")?;
802 assert!(result.is_none());
803 Ok(())
804 }
805
806 #[test]
807 fn read_by_name_returns_full_memory() -> TestResult {
808 let conn = setup_conn()?;
809 let m = new_memory("mem-read");
810 let id = insert(&conn, &m)?;
811
812 let row = read_by_name(&conn, "global", "mem-read")?.ok_or("mem-read should exist")?;
813 assert_eq!(row.id, id);
814 assert_eq!(row.name, "mem-read");
815 assert_eq!(row.memory_type, "user");
816 assert_eq!(row.body, "test memory body");
817 assert_eq!(row.namespace, "global");
818 Ok(())
819 }
820
821 #[test]
822 fn read_by_name_returns_none_for_missing() -> TestResult {
823 let conn = setup_conn()?;
824 let result = read_by_name(&conn, "global", "nao-existe")?;
825 assert!(result.is_none());
826 Ok(())
827 }
828
829 #[test]
830 fn read_full_by_id_returns_memory() -> TestResult {
831 let conn = setup_conn()?;
832 let m = new_memory("mem-full");
833 let id = insert(&conn, &m)?;
834
835 let row = read_full(&conn, id)?.ok_or("mem-full should exist")?;
836 assert_eq!(row.id, id);
837 assert_eq!(row.name, "mem-full");
838 Ok(())
839 }
840
841 #[test]
842 fn read_full_returns_none_for_missing_id() -> TestResult {
843 let conn = setup_conn()?;
844 let result = read_full(&conn, 9999)?;
845 assert!(result.is_none());
846 Ok(())
847 }
848
849 #[test]
850 fn update_without_optimism_modifies_fields() -> TestResult {
851 let conn = setup_conn()?;
852 let m = new_memory("mem-upd");
853 let id = insert(&conn, &m)?;
854
855 let mut m2 = new_memory("mem-upd");
856 m2.body = "updated body".to_string();
857 m2.body_hash = "hash-novo".to_string();
858 let ok = update(&conn, id, &m2, None)?;
859 assert!(ok);
860
861 let row = read_full(&conn, id)?.ok_or("mem-upd should exist")?;
862 assert_eq!(row.body, "updated body");
863 assert_eq!(row.body_hash, "hash-novo");
864 Ok(())
865 }
866
867 #[test]
868 fn update_with_correct_expected_updated_at_succeeds() -> TestResult {
869 let conn = setup_conn()?;
870 let m = new_memory("mem-opt");
871 let id = insert(&conn, &m)?;
872
873 let (_, updated_at, _) =
874 find_by_name(&conn, "global", "mem-opt")?.ok_or("mem-opt should exist")?;
875
876 let mut m2 = new_memory("mem-opt");
877 m2.body = "optimistic body".to_string();
878 m2.body_hash = "hash-optimistic".to_string();
879 let ok = update(&conn, id, &m2, Some(updated_at))?;
880 assert!(ok);
881
882 let row = read_full(&conn, id)?.ok_or("mem-opt should exist after update")?;
883 assert_eq!(row.body, "optimistic body");
884 Ok(())
885 }
886
887 #[test]
888 fn update_with_wrong_expected_updated_at_returns_false() -> TestResult {
889 let conn = setup_conn()?;
890 let m = new_memory("mem-conflict");
891 let id = insert(&conn, &m)?;
892
893 let mut m2 = new_memory("mem-conflict");
894 m2.body = "must not appear".to_string();
895 m2.body_hash = "hash-x".to_string();
896 let ok = update(&conn, id, &m2, Some(0))?;
897 assert!(!ok);
898
899 let row = read_full(&conn, id)?.ok_or("mem-conflict should exist")?;
900 assert_eq!(row.body, "test memory body");
901 Ok(())
902 }
903
904 #[test]
905 fn update_missing_id_returns_false() -> TestResult {
906 let conn = setup_conn()?;
907 let m = new_memory("fantasma");
908 let ok = update(&conn, 9999, &m, None)?;
909 assert!(!ok);
910 Ok(())
911 }
912
913 #[test]
914 fn soft_delete_marks_deleted_at() -> TestResult {
915 let conn = setup_conn()?;
916 let m = new_memory("mem-del");
917 insert(&conn, &m)?;
918
919 let ok = soft_delete(&conn, "global", "mem-del")?;
920 assert!(ok);
921
922 let result = find_by_name(&conn, "global", "mem-del")?;
923 assert!(result.is_none());
924
925 let result_read = read_by_name(&conn, "global", "mem-del")?;
926 assert!(result_read.is_none());
927 Ok(())
928 }
929
930 #[test]
931 fn soft_delete_returns_false_when_not_found() -> TestResult {
932 let conn = setup_conn()?;
933 let ok = soft_delete(&conn, "global", "nao-existe")?;
934 assert!(!ok);
935 Ok(())
936 }
937
938 #[test]
939 fn double_soft_delete_returns_false_on_second_call() -> TestResult {
940 let conn = setup_conn()?;
941 let m = new_memory("mem-del2");
942 insert(&conn, &m)?;
943
944 soft_delete(&conn, "global", "mem-del2")?;
945 let ok = soft_delete(&conn, "global", "mem-del2")?;
946 assert!(!ok);
947 Ok(())
948 }
949
950 #[test]
951 fn list_returns_memories_from_namespace() -> TestResult {
952 let conn = setup_conn()?;
953 insert(&conn, &new_memory("mem-list-a"))?;
954 insert(&conn, &new_memory("mem-list-b"))?;
955
956 let rows = list(&conn, "global", None, 10, 0, false)?;
957 assert!(rows.len() >= 2);
958 let nomes: Vec<_> = rows.iter().map(|r| r.name.as_str()).collect();
959 assert!(nomes.contains(&"mem-list-a"));
960 assert!(nomes.contains(&"mem-list-b"));
961 Ok(())
962 }
963
964 #[test]
965 fn list_with_type_filter_returns_only_correct_type() -> TestResult {
966 let conn = setup_conn()?;
967 insert(&conn, &new_memory("mem-user"))?;
968
969 let mut m2 = new_memory("mem-feedback");
970 m2.memory_type = "feedback".to_string();
971 insert(&conn, &m2)?;
972
973 let rows_user = list(&conn, "global", Some("user"), 10, 0, false)?;
974 assert!(rows_user.iter().all(|r| r.memory_type == "user"));
975
976 let rows_fb = list(&conn, "global", Some("feedback"), 10, 0, false)?;
977 assert!(rows_fb.iter().all(|r| r.memory_type == "feedback"));
978 Ok(())
979 }
980
981 #[test]
982 fn list_exclui_soft_deleted() -> TestResult {
983 let conn = setup_conn()?;
984 let m = new_memory("mem-excluida");
985 insert(&conn, &m)?;
986 soft_delete(&conn, "global", "mem-excluida")?;
987
988 let rows = list(&conn, "global", None, 10, 0, false)?;
989 assert!(rows.iter().all(|r| r.name != "mem-excluida"));
990 Ok(())
991 }
992
993 #[test]
994 fn list_pagination_works() -> TestResult {
995 let conn = setup_conn()?;
996 for i in 0..5 {
997 insert(&conn, &new_memory(&format!("mem-pag-{i}")))?;
998 }
999
1000 let pagina1 = list(&conn, "global", None, 2, 0, false)?;
1001 let pagina2 = list(&conn, "global", None, 2, 2, false)?;
1002 assert!(pagina1.len() <= 2);
1003 assert!(pagina2.len() <= 2);
1004 if !pagina1.is_empty() && !pagina2.is_empty() {
1005 assert_ne!(pagina1[0].id, pagina2[0].id);
1006 }
1007 Ok(())
1008 }
1009
1010 #[test]
1011 fn upsert_vec_and_delete_vec_work() -> TestResult {
1012 let conn = setup_conn()?;
1013 let m = new_memory("mem-vec");
1014 let id = insert(&conn, &m)?;
1015
1016 let embedding: Vec<f32> = vec![0.1; 384];
1017 upsert_vec(
1018 &conn, id, "global", "user", &embedding, "mem-vec", "snippet",
1019 )?;
1020
1021 let count: i64 = conn.query_row(
1022 "SELECT COUNT(*) FROM vec_memories WHERE memory_id = ?1",
1023 params![id],
1024 |r| r.get(0),
1025 )?;
1026 assert_eq!(count, 1);
1027
1028 delete_vec(&conn, id)?;
1029
1030 let count_after: i64 = conn.query_row(
1031 "SELECT COUNT(*) FROM vec_memories WHERE memory_id = ?1",
1032 params![id],
1033 |r| r.get(0),
1034 )?;
1035 assert_eq!(count_after, 0);
1036 Ok(())
1037 }
1038
1039 #[test]
1040 fn upsert_vec_replaces_existing_vector() -> TestResult {
1041 let conn = setup_conn()?;
1042 let m = new_memory("mem-vec-upsert");
1043 let id = insert(&conn, &m)?;
1044
1045 let emb1: Vec<f32> = vec![0.1; 384];
1046 upsert_vec(&conn, id, "global", "user", &emb1, "mem-vec-upsert", "s1")?;
1047
1048 let emb2: Vec<f32> = vec![0.9; 384];
1049 upsert_vec(&conn, id, "global", "user", &emb2, "mem-vec-upsert", "s2")?;
1050
1051 let count: i64 = conn.query_row(
1052 "SELECT COUNT(*) FROM vec_memories WHERE memory_id = ?1",
1053 params![id],
1054 |r| r.get(0),
1055 )?;
1056 assert_eq!(count, 1);
1057 Ok(())
1058 }
1059
1060 #[test]
1061 fn knn_search_returns_results_by_distance() -> TestResult {
1062 let conn = setup_conn()?;
1063
1064 let ma = new_memory("mem-knn-a");
1066 let id_a = insert(&conn, &ma)?;
1067 let emb_a: Vec<f32> = vec![1.0; 384];
1068 upsert_vec(&conn, id_a, "global", "user", &emb_a, "mem-knn-a", "s")?;
1069
1070 let mb = new_memory("mem-knn-b");
1072 let id_b = insert(&conn, &mb)?;
1073 let emb_b: Vec<f32> = vec![-1.0; 384];
1074 upsert_vec(&conn, id_b, "global", "user", &emb_b, "mem-knn-b", "s")?;
1075
1076 let query: Vec<f32> = vec![1.0; 384];
1077 let results = knn_search(&conn, &query, &["global".to_string()], None, 2)?;
1078 assert!(!results.is_empty());
1079 assert_eq!(results[0].0, id_a);
1080 Ok(())
1081 }
1082
1083 #[test]
1084 fn knn_search_with_type_filter_restricts_result() -> TestResult {
1085 let conn = setup_conn()?;
1086
1087 let ma = new_memory("mem-knn-tipo-user");
1088 let id_a = insert(&conn, &ma)?;
1089 let emb: Vec<f32> = vec![1.0; 384];
1090 upsert_vec(
1091 &conn,
1092 id_a,
1093 "global",
1094 "user",
1095 &emb,
1096 "mem-knn-tipo-user",
1097 "s",
1098 )?;
1099
1100 let mut mb = new_memory("mem-knn-tipo-fb");
1101 mb.memory_type = "feedback".to_string();
1102 let id_b = insert(&conn, &mb)?;
1103 upsert_vec(
1104 &conn,
1105 id_b,
1106 "global",
1107 "feedback",
1108 &emb,
1109 "mem-knn-tipo-fb",
1110 "s",
1111 )?;
1112
1113 let query: Vec<f32> = vec![1.0; 384];
1114 let results_user = knn_search(&conn, &query, &["global".to_string()], Some("user"), 5)?;
1115 assert!(results_user.iter().all(|(id, _)| *id == id_a));
1116
1117 let results_fb = knn_search(&conn, &query, &["global".to_string()], Some("feedback"), 5)?;
1118 assert!(results_fb.iter().all(|(id, _)| *id == id_b));
1119 Ok(())
1120 }
1121
1122 #[test]
1123 fn fts_search_finds_by_prefix_in_body() -> TestResult {
1124 let conn = setup_conn()?;
1125 let mut m = new_memory("mem-fts");
1126 m.body = "linguagem de programacao rust".to_string();
1127 insert(&conn, &m)?;
1128
1129 conn.execute_batch(
1130 "INSERT INTO fts_memories(rowid, name, description, body)
1131 SELECT id, name, description, body FROM memories WHERE deleted_at IS NULL",
1132 )?;
1133
1134 let rows = fts_search(&conn, "programacao", "global", None, 10)?;
1135 assert!(!rows.is_empty());
1136 assert!(rows.iter().any(|r| r.name == "mem-fts"));
1137 Ok(())
1138 }
1139
1140 #[test]
1141 fn fts_search_with_type_filter() -> TestResult {
1142 let conn = setup_conn()?;
1143 let mut m = new_memory("mem-fts-tipo");
1144 m.body = "linguagem especial para filtro".to_string();
1145 insert(&conn, &m)?;
1146
1147 let mut m2 = new_memory("mem-fts-feedback");
1148 m2.memory_type = "feedback".to_string();
1149 m2.body = "linguagem especial para filtro".to_string();
1150 insert(&conn, &m2)?;
1151
1152 conn.execute_batch(
1153 "INSERT INTO fts_memories(rowid, name, description, body)
1154 SELECT id, name, description, body FROM memories WHERE deleted_at IS NULL",
1155 )?;
1156
1157 let rows_user = fts_search(&conn, "especial", "global", Some("user"), 10)?;
1158 assert!(rows_user.iter().all(|r| r.memory_type == "user"));
1159
1160 let rows_fb = fts_search(&conn, "especial", "global", Some("feedback"), 10)?;
1161 assert!(rows_fb.iter().all(|r| r.memory_type == "feedback"));
1162 Ok(())
1163 }
1164
1165 #[test]
1166 fn fts_search_excludes_deleted() -> TestResult {
1167 let conn = setup_conn()?;
1168 let mut m = new_memory("mem-fts-del");
1169 m.body = "deleted fts content".to_string();
1170 insert(&conn, &m)?;
1171
1172 conn.execute_batch(
1173 "INSERT INTO fts_memories(rowid, name, description, body)
1174 SELECT id, name, description, body FROM memories WHERE deleted_at IS NULL",
1175 )?;
1176
1177 soft_delete(&conn, "global", "mem-fts-del")?;
1178
1179 let rows = fts_search(&conn, "deleted", "global", None, 10)?;
1180 assert!(rows.iter().all(|r| r.name != "mem-fts-del"));
1181 Ok(())
1182 }
1183
1184 #[test]
1185 fn list_deleted_before_returns_correct_ids() -> TestResult {
1186 let conn = setup_conn()?;
1187 let m = new_memory("mem-purge");
1188 insert(&conn, &m)?;
1189 soft_delete(&conn, "global", "mem-purge")?;
1190
1191 let ids = list_deleted_before(&conn, "global", i64::MAX)?;
1192 assert!(!ids.is_empty());
1193
1194 let ids_antes = list_deleted_before(&conn, "global", 0)?;
1195 assert!(ids_antes.is_empty());
1196 Ok(())
1197 }
1198
1199 #[test]
1200 fn find_by_name_returns_correct_max_version() -> TestResult {
1201 let conn = setup_conn()?;
1202 let m = new_memory("mem-ver");
1203 let id = insert(&conn, &m)?;
1204
1205 let (_, _, v0) = find_by_name(&conn, "global", "mem-ver")?.ok_or("mem-ver should exist")?;
1206 assert_eq!(v0, 0);
1207
1208 conn.execute(
1209 "INSERT INTO memory_versions (memory_id, version, name, type, description, body, metadata, change_reason)
1210 VALUES (?1, 1, 'mem-ver', 'user', 'desc', 'body', '{}', 'create')",
1211 params![id],
1212 )?;
1213
1214 let (_, _, v1) =
1215 find_by_name(&conn, "global", "mem-ver")?.ok_or("mem-ver should exist after insert")?;
1216 assert_eq!(v1, 1);
1217 Ok(())
1218 }
1219
1220 #[test]
1221 fn insert_com_metadata_json() -> TestResult {
1222 let conn = setup_conn()?;
1223 let mut m = new_memory("mem-meta");
1224 m.metadata = serde_json::json!({"chave": "valor", "numero": 42});
1225 let id = insert(&conn, &m)?;
1226
1227 let row = read_full(&conn, id)?.ok_or("mem-meta should exist")?;
1228 let meta: serde_json::Value = serde_json::from_str(&row.metadata)?;
1229 assert_eq!(meta["chave"], "valor");
1230 assert_eq!(meta["numero"], 42);
1231 Ok(())
1232 }
1233
1234 #[test]
1235 fn insert_com_session_id() -> TestResult {
1236 let conn = setup_conn()?;
1237 let mut m = new_memory("mem-session");
1238 m.session_id = Some("sessao-xyz".to_string());
1239 let id = insert(&conn, &m)?;
1240
1241 let row = read_full(&conn, id)?.ok_or("mem-session should exist")?;
1242 assert_eq!(row.session_id, Some("sessao-xyz".to_string()));
1243 Ok(())
1244 }
1245
1246 #[test]
1247 fn delete_vec_for_nonexistent_id_does_not_fail() -> TestResult {
1248 let conn = setup_conn()?;
1249 let result = delete_vec(&conn, 99999);
1250 assert!(result.is_ok());
1251 Ok(())
1252 }
1253
1254 #[test]
1255 fn preprocess_fts_query_no_separators() {
1256 assert_eq!(preprocess_fts_query("hello"), "hello*");
1257 assert_eq!(preprocess_fts_query("hello world"), "hello* world*");
1258 }
1259
1260 #[test]
1261 fn preprocess_fts_query_with_hyphens() {
1262 let result = preprocess_fts_query("graphrag-precompact");
1263 assert!(result.contains("\"graphrag precompact\""));
1264 assert!(result.contains("graphrag*"));
1265 assert!(result.contains("precompact*"));
1266 }
1267
1268 #[test]
1269 fn preprocess_fts_query_with_dots() {
1270 let result = preprocess_fts_query("v1.0.44");
1271 assert!(result.contains("\"v1 0 44\""));
1272 assert!(result.contains("v1*"));
1273 assert!(result.contains("44*"));
1274 }
1275
1276 #[test]
1277 fn preprocess_fts_query_with_mixed_separators() {
1278 let result = preprocess_fts_query("graphrag-precompact.sh");
1279 assert!(result.contains("\"graphrag precompact sh\""));
1280 assert!(result.contains("graphrag*"));
1281 }
1282
1283 #[test]
1284 fn preprocess_fts_query_empty_and_whitespace() {
1285 assert_eq!(preprocess_fts_query(""), "");
1286 assert_eq!(preprocess_fts_query(" "), "");
1287 }
1288
1289 #[test]
1290 fn fts_search_finds_compound_term_with_hyphen() -> TestResult {
1291 let conn = setup_conn()?;
1292 let mut m = new_memory("mem-compound");
1293 m.body = "the graphrag-precompact script runs daily".to_string();
1294 insert(&conn, &m)?;
1295 conn.execute_batch(
1296 "INSERT INTO fts_memories(rowid, name, description, body)
1297 SELECT id, name, description, body FROM memories WHERE deleted_at IS NULL",
1298 )?;
1299 let rows = fts_search(&conn, "graphrag-precompact", "global", None, 10)?;
1300 assert!(!rows.is_empty(), "should find compound hyphenated term");
1301 Ok(())
1302 }
1303}