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}
49
50pub fn find_by_name(
67 conn: &Connection,
68 namespace: &str,
69 name: &str,
70) -> Result<Option<(i64, i64, i64)>, AppError> {
71 let mut stmt = conn.prepare_cached(
72 "SELECT m.id, m.updated_at, COALESCE(MAX(v.version), 0)
73 FROM memories m
74 LEFT JOIN memory_versions v ON v.memory_id = m.id
75 WHERE m.namespace = ?1 AND m.name = ?2 AND m.deleted_at IS NULL
76 GROUP BY m.id",
77 )?;
78 let result = stmt.query_row(params![namespace, name], |r| {
79 Ok((
80 r.get::<_, i64>(0)?,
81 r.get::<_, i64>(1)?,
82 r.get::<_, i64>(2)?,
83 ))
84 });
85 match result {
86 Ok(row) => Ok(Some(row)),
87 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
88 Err(e) => Err(AppError::Database(e)),
89 }
90}
91
92pub fn find_by_hash(
106 conn: &Connection,
107 namespace: &str,
108 body_hash: &str,
109) -> Result<Option<i64>, AppError> {
110 let mut stmt = conn.prepare_cached(
111 "SELECT id FROM memories WHERE namespace = ?1 AND body_hash = ?2 AND deleted_at IS NULL",
112 )?;
113 match stmt.query_row(params![namespace, body_hash], |r| r.get(0)) {
114 Ok(id) => Ok(Some(id)),
115 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
116 Err(e) => Err(AppError::Database(e)),
117 }
118}
119
120pub fn insert(conn: &Connection, m: &NewMemory) -> Result<i64, AppError> {
136 conn.execute(
137 "INSERT INTO memories (namespace, name, type, description, body, body_hash, session_id, source, metadata)
138 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
139 params![
140 m.namespace, m.name, m.memory_type, m.description, m.body,
141 m.body_hash, m.session_id, m.source,
142 serde_json::to_string(&m.metadata)?
143 ],
144 )?;
145 Ok(conn.last_insert_rowid())
146}
147
148pub fn update(
163 conn: &Connection,
164 id: i64,
165 m: &NewMemory,
166 expected_updated_at: Option<i64>,
167) -> Result<bool, AppError> {
168 let affected = if let Some(ts) = expected_updated_at {
169 conn.execute(
170 "UPDATE memories SET type=?2, description=?3, body=?4, body_hash=?5,
171 session_id=?6, source=?7, metadata=?8
172 WHERE id=?1 AND updated_at=?9 AND deleted_at IS NULL",
173 params![
174 id,
175 m.memory_type,
176 m.description,
177 m.body,
178 m.body_hash,
179 m.session_id,
180 m.source,
181 serde_json::to_string(&m.metadata)?,
182 ts
183 ],
184 )?
185 } else {
186 conn.execute(
187 "UPDATE memories SET type=?2, description=?3, body=?4, body_hash=?5,
188 session_id=?6, source=?7, metadata=?8
189 WHERE id=?1 AND deleted_at IS NULL",
190 params![
191 id,
192 m.memory_type,
193 m.description,
194 m.body,
195 m.body_hash,
196 m.session_id,
197 m.source,
198 serde_json::to_string(&m.metadata)?
199 ],
200 )?
201 };
202 Ok(affected == 1)
203}
204
205pub fn upsert_vec(
215 conn: &Connection,
216 memory_id: i64,
217 namespace: &str,
218 memory_type: &str,
219 embedding: &[f32],
220 name: &str,
221 snippet: &str,
222) -> Result<(), AppError> {
223 let embedding_bytes = f32_to_bytes(embedding);
228 with_busy_retry(|| {
229 conn.execute(
230 "DELETE FROM vec_memories WHERE memory_id = ?1",
231 params![memory_id],
232 )?;
233 conn.execute(
234 "INSERT INTO vec_memories(memory_id, namespace, type, embedding, name, snippet)
235 VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
236 params![
237 memory_id,
238 namespace,
239 memory_type,
240 &embedding_bytes,
241 name,
242 snippet
243 ],
244 )?;
245 Ok(())
246 })
247}
248
249pub fn delete_vec(conn: &Connection, memory_id: i64) -> Result<(), AppError> {
258 conn.execute(
259 "DELETE FROM vec_memories WHERE memory_id = ?1",
260 params![memory_id],
261 )?;
262 Ok(())
263}
264
265pub fn read_by_name(
275 conn: &Connection,
276 namespace: &str,
277 name: &str,
278) -> Result<Option<MemoryRow>, AppError> {
279 let mut stmt = conn.prepare_cached(
280 "SELECT id, namespace, name, type, description, body, body_hash,
281 session_id, source, metadata, created_at, updated_at
282 FROM memories WHERE namespace=?1 AND name=?2 AND deleted_at IS NULL",
283 )?;
284 match stmt.query_row(params![namespace, name], |r| {
285 Ok(MemoryRow {
286 id: r.get(0)?,
287 namespace: r.get(1)?,
288 name: r.get(2)?,
289 memory_type: r.get(3)?,
290 description: r.get(4)?,
291 body: r.get(5)?,
292 body_hash: r.get(6)?,
293 session_id: r.get(7)?,
294 source: r.get(8)?,
295 metadata: r.get(9)?,
296 created_at: r.get(10)?,
297 updated_at: r.get(11)?,
298 })
299 }) {
300 Ok(m) => Ok(Some(m)),
301 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
302 Err(e) => Err(AppError::Database(e)),
303 }
304}
305
306pub fn soft_delete(conn: &Connection, namespace: &str, name: &str) -> Result<bool, AppError> {
320 let affected = conn.execute(
321 "UPDATE memories SET deleted_at = unixepoch() WHERE namespace=?1 AND name=?2 AND deleted_at IS NULL",
322 params![namespace, name],
323 )?;
324 Ok(affected == 1)
325}
326
327pub fn list(
338 conn: &Connection,
339 namespace: &str,
340 memory_type: Option<&str>,
341 limit: usize,
342 offset: usize,
343 include_deleted: bool,
344) -> Result<Vec<MemoryRow>, AppError> {
345 let deleted_clause = if include_deleted {
346 ""
347 } else {
348 " AND deleted_at IS NULL"
349 };
350 if let Some(mt) = memory_type {
351 let sql = format!(
352 "SELECT id, namespace, name, type, description, body, body_hash,
353 session_id, source, metadata, created_at, updated_at
354 FROM memories WHERE namespace=?1 AND type=?2{deleted_clause}
355 ORDER BY updated_at DESC LIMIT ?3 OFFSET ?4"
356 );
357 let mut stmt = conn.prepare(&sql)?;
358 let rows = stmt
359 .query_map(params![namespace, mt, limit as i64, offset as i64], |r| {
360 Ok(MemoryRow {
361 id: r.get(0)?,
362 namespace: r.get(1)?,
363 name: r.get(2)?,
364 memory_type: r.get(3)?,
365 description: r.get(4)?,
366 body: r.get(5)?,
367 body_hash: r.get(6)?,
368 session_id: r.get(7)?,
369 source: r.get(8)?,
370 metadata: r.get(9)?,
371 created_at: r.get(10)?,
372 updated_at: r.get(11)?,
373 })
374 })?
375 .collect::<Result<Vec<_>, _>>()?;
376 Ok(rows)
377 } else {
378 let sql = format!(
379 "SELECT id, namespace, name, type, description, body, body_hash,
380 session_id, source, metadata, created_at, updated_at
381 FROM memories WHERE namespace=?1{deleted_clause}
382 ORDER BY updated_at DESC LIMIT ?2 OFFSET ?3"
383 );
384 let mut stmt = conn.prepare(&sql)?;
385 let rows = stmt
386 .query_map(params![namespace, limit as i64, offset as i64], |r| {
387 Ok(MemoryRow {
388 id: r.get(0)?,
389 namespace: r.get(1)?,
390 name: r.get(2)?,
391 memory_type: r.get(3)?,
392 description: r.get(4)?,
393 body: r.get(5)?,
394 body_hash: r.get(6)?,
395 session_id: r.get(7)?,
396 source: r.get(8)?,
397 metadata: r.get(9)?,
398 created_at: r.get(10)?,
399 updated_at: r.get(11)?,
400 })
401 })?
402 .collect::<Result<Vec<_>, _>>()?;
403 Ok(rows)
404 }
405}
406
407pub fn knn_search(
424 conn: &Connection,
425 embedding: &[f32],
426 namespaces: &[String],
427 memory_type: Option<&str>,
428 k: usize,
429) -> Result<Vec<(i64, f32)>, AppError> {
430 let bytes = f32_to_bytes(embedding);
431
432 match namespaces.len() {
433 0 => {
434 if let Some(mt) = memory_type {
436 let mut stmt = conn.prepare(
437 "SELECT memory_id, distance FROM vec_memories \
438 WHERE embedding MATCH ?1 AND type = ?2 \
439 ORDER BY distance LIMIT ?3",
440 )?;
441 let rows = stmt
442 .query_map(params![bytes, mt, k as i64], |r| {
443 Ok((r.get::<_, i64>(0)?, r.get::<_, f32>(1)?))
444 })?
445 .collect::<Result<Vec<_>, _>>()?;
446 Ok(rows)
447 } else {
448 let mut stmt = conn.prepare(
449 "SELECT memory_id, distance FROM vec_memories \
450 WHERE embedding MATCH ?1 \
451 ORDER BY distance LIMIT ?2",
452 )?;
453 let rows = stmt
454 .query_map(params![bytes, k as i64], |r| {
455 Ok((r.get::<_, i64>(0)?, r.get::<_, f32>(1)?))
456 })?
457 .collect::<Result<Vec<_>, _>>()?;
458 Ok(rows)
459 }
460 }
461 1 => {
462 let ns = &namespaces[0];
464 if let Some(mt) = memory_type {
465 let mut stmt = conn.prepare(
466 "SELECT memory_id, distance FROM vec_memories \
467 WHERE embedding MATCH ?1 AND namespace = ?2 AND type = ?3 \
468 ORDER BY distance LIMIT ?4",
469 )?;
470 let rows = stmt
471 .query_map(params![bytes, ns, mt, k as i64], |r| {
472 Ok((r.get::<_, i64>(0)?, r.get::<_, f32>(1)?))
473 })?
474 .collect::<Result<Vec<_>, _>>()?;
475 Ok(rows)
476 } else {
477 let mut stmt = conn.prepare(
478 "SELECT memory_id, distance FROM vec_memories \
479 WHERE embedding MATCH ?1 AND namespace = ?2 \
480 ORDER BY distance LIMIT ?3",
481 )?;
482 let rows = stmt
483 .query_map(params![bytes, ns, k as i64], |r| {
484 Ok((r.get::<_, i64>(0)?, r.get::<_, f32>(1)?))
485 })?
486 .collect::<Result<Vec<_>, _>>()?;
487 Ok(rows)
488 }
489 }
490 _ => {
491 let placeholders = (0..namespaces.len())
494 .map(|_| "?")
495 .collect::<Vec<_>>()
496 .join(",");
497 if let Some(mt) = memory_type {
498 let query = format!(
499 "SELECT memory_id, distance FROM vec_memories \
500 WHERE embedding MATCH ? AND type = ? AND namespace IN ({placeholders}) \
501 ORDER BY distance LIMIT ?"
502 );
503 let mut stmt = conn.prepare(&query)?;
504 let mut raw_params: Vec<Box<dyn rusqlite::ToSql>> =
506 vec![Box::new(bytes), Box::new(mt.to_string())];
507 for ns in namespaces {
508 raw_params.push(Box::new(ns.clone()));
509 }
510 raw_params.push(Box::new(k as i64));
511 let param_refs: Vec<&dyn rusqlite::ToSql> =
512 raw_params.iter().map(|b| b.as_ref()).collect();
513 let rows = stmt
514 .query_map(param_refs.as_slice(), |r| {
515 Ok((r.get::<_, i64>(0)?, r.get::<_, f32>(1)?))
516 })?
517 .collect::<Result<Vec<_>, _>>()?;
518 Ok(rows)
519 } else {
520 let query = format!(
521 "SELECT memory_id, distance FROM vec_memories \
522 WHERE embedding MATCH ? AND namespace IN ({placeholders}) \
523 ORDER BY distance LIMIT ?"
524 );
525 let mut stmt = conn.prepare(&query)?;
526 let mut raw_params: Vec<Box<dyn rusqlite::ToSql>> = vec![Box::new(bytes)];
528 for ns in namespaces {
529 raw_params.push(Box::new(ns.clone()));
530 }
531 raw_params.push(Box::new(k as i64));
532 let param_refs: Vec<&dyn rusqlite::ToSql> =
533 raw_params.iter().map(|b| b.as_ref()).collect();
534 let rows = stmt
535 .query_map(param_refs.as_slice(), |r| {
536 Ok((r.get::<_, i64>(0)?, r.get::<_, f32>(1)?))
537 })?
538 .collect::<Result<Vec<_>, _>>()?;
539 Ok(rows)
540 }
541 }
542 }
543}
544
545pub fn read_full(conn: &Connection, memory_id: i64) -> Result<Option<MemoryRow>, AppError> {
553 let mut stmt = conn.prepare_cached(
554 "SELECT id, namespace, name, type, description, body, body_hash,
555 session_id, source, metadata, created_at, updated_at
556 FROM memories WHERE id=?1 AND deleted_at IS NULL",
557 )?;
558 match stmt.query_row(params![memory_id], |r| {
559 Ok(MemoryRow {
560 id: r.get(0)?,
561 namespace: r.get(1)?,
562 name: r.get(2)?,
563 memory_type: r.get(3)?,
564 description: r.get(4)?,
565 body: r.get(5)?,
566 body_hash: r.get(6)?,
567 session_id: r.get(7)?,
568 source: r.get(8)?,
569 metadata: r.get(9)?,
570 created_at: r.get(10)?,
571 updated_at: r.get(11)?,
572 })
573 }) {
574 Ok(m) => Ok(Some(m)),
575 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
576 Err(e) => Err(AppError::Database(e)),
577 }
578}
579
580pub fn list_deleted_before(
589 conn: &Connection,
590 namespace: &str,
591 before_ts: i64,
592) -> Result<Vec<i64>, AppError> {
593 let mut stmt = conn.prepare_cached(
594 "SELECT id FROM memories WHERE namespace = ?1 AND deleted_at IS NOT NULL AND deleted_at < ?2",
595 )?;
596 let ids = stmt
597 .query_map(params![namespace, before_ts], |r| r.get::<_, i64>(0))?
598 .collect::<Result<Vec<_>, _>>()?;
599 Ok(ids)
600}
601
602pub fn fts_search(
611 conn: &Connection,
612 query: &str,
613 namespace: &str,
614 memory_type: Option<&str>,
615 limit: usize,
616) -> Result<Vec<MemoryRow>, AppError> {
617 let fts_query = format!("{query}*");
618 if let Some(mt) = memory_type {
619 let mut stmt = conn.prepare(
620 "SELECT m.id, m.namespace, m.name, m.type, m.description, m.body, m.body_hash,
621 m.session_id, m.source, m.metadata, m.created_at, m.updated_at
622 FROM fts_memories fts
623 JOIN memories m ON m.id = fts.rowid
624 WHERE fts_memories MATCH ?1 AND m.namespace = ?2 AND m.type = ?3 AND m.deleted_at IS NULL
625 ORDER BY rank LIMIT ?4",
626 )?;
627 let rows = stmt
628 .query_map(params![fts_query, namespace, mt, limit as i64], |r| {
629 Ok(MemoryRow {
630 id: r.get(0)?,
631 namespace: r.get(1)?,
632 name: r.get(2)?,
633 memory_type: r.get(3)?,
634 description: r.get(4)?,
635 body: r.get(5)?,
636 body_hash: r.get(6)?,
637 session_id: r.get(7)?,
638 source: r.get(8)?,
639 metadata: r.get(9)?,
640 created_at: r.get(10)?,
641 updated_at: r.get(11)?,
642 })
643 })?
644 .collect::<Result<Vec<_>, _>>()?;
645 Ok(rows)
646 } else {
647 let mut stmt = conn.prepare(
648 "SELECT m.id, m.namespace, m.name, m.type, m.description, m.body, m.body_hash,
649 m.session_id, m.source, m.metadata, m.created_at, m.updated_at
650 FROM fts_memories fts
651 JOIN memories m ON m.id = fts.rowid
652 WHERE fts_memories MATCH ?1 AND m.namespace = ?2 AND m.deleted_at IS NULL
653 ORDER BY rank LIMIT ?3",
654 )?;
655 let rows = stmt
656 .query_map(params![fts_query, namespace, limit as i64], |r| {
657 Ok(MemoryRow {
658 id: r.get(0)?,
659 namespace: r.get(1)?,
660 name: r.get(2)?,
661 memory_type: r.get(3)?,
662 description: r.get(4)?,
663 body: r.get(5)?,
664 body_hash: r.get(6)?,
665 session_id: r.get(7)?,
666 source: r.get(8)?,
667 metadata: r.get(9)?,
668 created_at: r.get(10)?,
669 updated_at: r.get(11)?,
670 })
671 })?
672 .collect::<Result<Vec<_>, _>>()?;
673 Ok(rows)
674 }
675}
676
677#[cfg(test)]
678mod tests {
679 use super::*;
680 use rusqlite::Connection;
681
682 type TestResult = Result<(), Box<dyn std::error::Error>>;
683
684 fn setup_conn() -> Result<Connection, Box<dyn std::error::Error>> {
685 crate::storage::connection::register_vec_extension();
686 let mut conn = Connection::open_in_memory()?;
687 conn.execute_batch(
688 "PRAGMA foreign_keys = ON;
689 PRAGMA temp_store = MEMORY;",
690 )?;
691 crate::migrations::runner().run(&mut conn)?;
692 Ok(conn)
693 }
694
695 fn new_memory(name: &str) -> NewMemory {
696 NewMemory {
697 namespace: "global".to_string(),
698 name: name.to_string(),
699 memory_type: "user".to_string(),
700 description: "descricao de teste".to_string(),
701 body: "corpo da memoria de teste".to_string(),
702 body_hash: format!("hash-{name}"),
703 session_id: None,
704 source: "agent".to_string(),
705 metadata: serde_json::json!({}),
706 }
707 }
708
709 #[test]
710 fn insert_and_find_by_name_return_id() -> TestResult {
711 let conn = setup_conn()?;
712 let m = new_memory("mem-alpha");
713 let id = insert(&conn, &m)?;
714 assert!(id > 0);
715
716 let found = find_by_name(&conn, "global", "mem-alpha")?;
717 assert!(found.is_some());
718 let (found_id, _, _) = found.ok_or("mem-alpha should exist")?;
719 assert_eq!(found_id, id);
720 Ok(())
721 }
722
723 #[test]
724 fn find_by_name_returns_none_when_not_found() -> TestResult {
725 let conn = setup_conn()?;
726 let result = find_by_name(&conn, "global", "inexistente")?;
727 assert!(result.is_none());
728 Ok(())
729 }
730
731 #[test]
732 fn find_by_hash_returns_correct_id() -> TestResult {
733 let conn = setup_conn()?;
734 let m = new_memory("mem-hash");
735 let id = insert(&conn, &m)?;
736
737 let found = find_by_hash(&conn, "global", "hash-mem-hash")?;
738 assert_eq!(found, Some(id));
739 Ok(())
740 }
741
742 #[test]
743 fn find_by_hash_returns_none_when_hash_not_found() -> TestResult {
744 let conn = setup_conn()?;
745 let result = find_by_hash(&conn, "global", "hash-inexistente")?;
746 assert!(result.is_none());
747 Ok(())
748 }
749
750 #[test]
751 fn find_by_hash_ignores_different_namespace() -> TestResult {
752 let conn = setup_conn()?;
753 let m = new_memory("mem-ns");
754 insert(&conn, &m)?;
755
756 let result = find_by_hash(&conn, "outro-namespace", "hash-mem-ns")?;
757 assert!(result.is_none());
758 Ok(())
759 }
760
761 #[test]
762 fn read_by_name_returns_full_memory() -> TestResult {
763 let conn = setup_conn()?;
764 let m = new_memory("mem-read");
765 let id = insert(&conn, &m)?;
766
767 let row = read_by_name(&conn, "global", "mem-read")?.ok_or("mem-read should exist")?;
768 assert_eq!(row.id, id);
769 assert_eq!(row.name, "mem-read");
770 assert_eq!(row.memory_type, "user");
771 assert_eq!(row.body, "corpo da memoria de teste");
772 assert_eq!(row.namespace, "global");
773 Ok(())
774 }
775
776 #[test]
777 fn read_by_name_returns_none_for_missing() -> TestResult {
778 let conn = setup_conn()?;
779 let result = read_by_name(&conn, "global", "nao-existe")?;
780 assert!(result.is_none());
781 Ok(())
782 }
783
784 #[test]
785 fn read_full_by_id_returns_memory() -> TestResult {
786 let conn = setup_conn()?;
787 let m = new_memory("mem-full");
788 let id = insert(&conn, &m)?;
789
790 let row = read_full(&conn, id)?.ok_or("mem-full should exist")?;
791 assert_eq!(row.id, id);
792 assert_eq!(row.name, "mem-full");
793 Ok(())
794 }
795
796 #[test]
797 fn read_full_returns_none_for_missing_id() -> TestResult {
798 let conn = setup_conn()?;
799 let result = read_full(&conn, 9999)?;
800 assert!(result.is_none());
801 Ok(())
802 }
803
804 #[test]
805 fn update_without_optimism_modifies_fields() -> TestResult {
806 let conn = setup_conn()?;
807 let m = new_memory("mem-upd");
808 let id = insert(&conn, &m)?;
809
810 let mut m2 = new_memory("mem-upd");
811 m2.body = "corpo atualizado".to_string();
812 m2.body_hash = "hash-novo".to_string();
813 let ok = update(&conn, id, &m2, None)?;
814 assert!(ok);
815
816 let row = read_full(&conn, id)?.ok_or("mem-upd should exist")?;
817 assert_eq!(row.body, "corpo atualizado");
818 assert_eq!(row.body_hash, "hash-novo");
819 Ok(())
820 }
821
822 #[test]
823 fn update_with_correct_expected_updated_at_succeeds() -> TestResult {
824 let conn = setup_conn()?;
825 let m = new_memory("mem-opt");
826 let id = insert(&conn, &m)?;
827
828 let (_, updated_at, _) =
829 find_by_name(&conn, "global", "mem-opt")?.ok_or("mem-opt should exist")?;
830
831 let mut m2 = new_memory("mem-opt");
832 m2.body = "optimistic body".to_string();
833 m2.body_hash = "hash-optimistic".to_string();
834 let ok = update(&conn, id, &m2, Some(updated_at))?;
835 assert!(ok);
836
837 let row = read_full(&conn, id)?.ok_or("mem-opt should exist after update")?;
838 assert_eq!(row.body, "optimistic body");
839 Ok(())
840 }
841
842 #[test]
843 fn update_with_wrong_expected_updated_at_returns_false() -> TestResult {
844 let conn = setup_conn()?;
845 let m = new_memory("mem-conflict");
846 let id = insert(&conn, &m)?;
847
848 let mut m2 = new_memory("mem-conflict");
849 m2.body = "nao deve aparecer".to_string();
850 m2.body_hash = "hash-x".to_string();
851 let ok = update(&conn, id, &m2, Some(0))?;
852 assert!(!ok);
853
854 let row = read_full(&conn, id)?.ok_or("mem-conflict should exist")?;
855 assert_eq!(row.body, "corpo da memoria de teste");
856 Ok(())
857 }
858
859 #[test]
860 fn update_missing_id_returns_false() -> TestResult {
861 let conn = setup_conn()?;
862 let m = new_memory("fantasma");
863 let ok = update(&conn, 9999, &m, None)?;
864 assert!(!ok);
865 Ok(())
866 }
867
868 #[test]
869 fn soft_delete_marks_deleted_at() -> TestResult {
870 let conn = setup_conn()?;
871 let m = new_memory("mem-del");
872 insert(&conn, &m)?;
873
874 let ok = soft_delete(&conn, "global", "mem-del")?;
875 assert!(ok);
876
877 let result = find_by_name(&conn, "global", "mem-del")?;
878 assert!(result.is_none());
879
880 let result_read = read_by_name(&conn, "global", "mem-del")?;
881 assert!(result_read.is_none());
882 Ok(())
883 }
884
885 #[test]
886 fn soft_delete_returns_false_when_not_found() -> TestResult {
887 let conn = setup_conn()?;
888 let ok = soft_delete(&conn, "global", "nao-existe")?;
889 assert!(!ok);
890 Ok(())
891 }
892
893 #[test]
894 fn double_soft_delete_returns_false_on_second_call() -> TestResult {
895 let conn = setup_conn()?;
896 let m = new_memory("mem-del2");
897 insert(&conn, &m)?;
898
899 soft_delete(&conn, "global", "mem-del2")?;
900 let ok = soft_delete(&conn, "global", "mem-del2")?;
901 assert!(!ok);
902 Ok(())
903 }
904
905 #[test]
906 fn list_returns_memories_from_namespace() -> TestResult {
907 let conn = setup_conn()?;
908 insert(&conn, &new_memory("mem-list-a"))?;
909 insert(&conn, &new_memory("mem-list-b"))?;
910
911 let rows = list(&conn, "global", None, 10, 0, false)?;
912 assert!(rows.len() >= 2);
913 let nomes: Vec<_> = rows.iter().map(|r| r.name.as_str()).collect();
914 assert!(nomes.contains(&"mem-list-a"));
915 assert!(nomes.contains(&"mem-list-b"));
916 Ok(())
917 }
918
919 #[test]
920 fn list_with_type_filter_returns_only_correct_type() -> TestResult {
921 let conn = setup_conn()?;
922 insert(&conn, &new_memory("mem-user"))?;
923
924 let mut m2 = new_memory("mem-feedback");
925 m2.memory_type = "feedback".to_string();
926 insert(&conn, &m2)?;
927
928 let rows_user = list(&conn, "global", Some("user"), 10, 0, false)?;
929 assert!(rows_user.iter().all(|r| r.memory_type == "user"));
930
931 let rows_fb = list(&conn, "global", Some("feedback"), 10, 0, false)?;
932 assert!(rows_fb.iter().all(|r| r.memory_type == "feedback"));
933 Ok(())
934 }
935
936 #[test]
937 fn list_exclui_soft_deleted() -> TestResult {
938 let conn = setup_conn()?;
939 let m = new_memory("mem-excluida");
940 insert(&conn, &m)?;
941 soft_delete(&conn, "global", "mem-excluida")?;
942
943 let rows = list(&conn, "global", None, 10, 0, false)?;
944 assert!(rows.iter().all(|r| r.name != "mem-excluida"));
945 Ok(())
946 }
947
948 #[test]
949 fn list_pagination_works() -> TestResult {
950 let conn = setup_conn()?;
951 for i in 0..5 {
952 insert(&conn, &new_memory(&format!("mem-pag-{i}")))?;
953 }
954
955 let pagina1 = list(&conn, "global", None, 2, 0, false)?;
956 let pagina2 = list(&conn, "global", None, 2, 2, false)?;
957 assert!(pagina1.len() <= 2);
958 assert!(pagina2.len() <= 2);
959 if !pagina1.is_empty() && !pagina2.is_empty() {
960 assert_ne!(pagina1[0].id, pagina2[0].id);
961 }
962 Ok(())
963 }
964
965 #[test]
966 fn upsert_vec_and_delete_vec_work() -> TestResult {
967 let conn = setup_conn()?;
968 let m = new_memory("mem-vec");
969 let id = insert(&conn, &m)?;
970
971 let embedding: Vec<f32> = vec![0.1; 384];
972 upsert_vec(
973 &conn, id, "global", "user", &embedding, "mem-vec", "snippet",
974 )?;
975
976 let count: i64 = conn.query_row(
977 "SELECT COUNT(*) FROM vec_memories WHERE memory_id = ?1",
978 params![id],
979 |r| r.get(0),
980 )?;
981 assert_eq!(count, 1);
982
983 delete_vec(&conn, id)?;
984
985 let count_after: i64 = conn.query_row(
986 "SELECT COUNT(*) FROM vec_memories WHERE memory_id = ?1",
987 params![id],
988 |r| r.get(0),
989 )?;
990 assert_eq!(count_after, 0);
991 Ok(())
992 }
993
994 #[test]
995 fn upsert_vec_replaces_existing_vector() -> TestResult {
996 let conn = setup_conn()?;
997 let m = new_memory("mem-vec-upsert");
998 let id = insert(&conn, &m)?;
999
1000 let emb1: Vec<f32> = vec![0.1; 384];
1001 upsert_vec(&conn, id, "global", "user", &emb1, "mem-vec-upsert", "s1")?;
1002
1003 let emb2: Vec<f32> = vec![0.9; 384];
1004 upsert_vec(&conn, id, "global", "user", &emb2, "mem-vec-upsert", "s2")?;
1005
1006 let count: i64 = conn.query_row(
1007 "SELECT COUNT(*) FROM vec_memories WHERE memory_id = ?1",
1008 params![id],
1009 |r| r.get(0),
1010 )?;
1011 assert_eq!(count, 1);
1012 Ok(())
1013 }
1014
1015 #[test]
1016 fn knn_search_returns_results_by_distance() -> TestResult {
1017 let conn = setup_conn()?;
1018
1019 let ma = new_memory("mem-knn-a");
1021 let id_a = insert(&conn, &ma)?;
1022 let emb_a: Vec<f32> = vec![1.0; 384];
1023 upsert_vec(&conn, id_a, "global", "user", &emb_a, "mem-knn-a", "s")?;
1024
1025 let mb = new_memory("mem-knn-b");
1027 let id_b = insert(&conn, &mb)?;
1028 let emb_b: Vec<f32> = vec![-1.0; 384];
1029 upsert_vec(&conn, id_b, "global", "user", &emb_b, "mem-knn-b", "s")?;
1030
1031 let query: Vec<f32> = vec![1.0; 384];
1032 let results = knn_search(&conn, &query, &["global".to_string()], None, 2)?;
1033 assert!(!results.is_empty());
1034 assert_eq!(results[0].0, id_a);
1035 Ok(())
1036 }
1037
1038 #[test]
1039 fn knn_search_with_type_filter_restricts_result() -> TestResult {
1040 let conn = setup_conn()?;
1041
1042 let ma = new_memory("mem-knn-tipo-user");
1043 let id_a = insert(&conn, &ma)?;
1044 let emb: Vec<f32> = vec![1.0; 384];
1045 upsert_vec(
1046 &conn,
1047 id_a,
1048 "global",
1049 "user",
1050 &emb,
1051 "mem-knn-tipo-user",
1052 "s",
1053 )?;
1054
1055 let mut mb = new_memory("mem-knn-tipo-fb");
1056 mb.memory_type = "feedback".to_string();
1057 let id_b = insert(&conn, &mb)?;
1058 upsert_vec(
1059 &conn,
1060 id_b,
1061 "global",
1062 "feedback",
1063 &emb,
1064 "mem-knn-tipo-fb",
1065 "s",
1066 )?;
1067
1068 let query: Vec<f32> = vec![1.0; 384];
1069 let results_user = knn_search(&conn, &query, &["global".to_string()], Some("user"), 5)?;
1070 assert!(results_user.iter().all(|(id, _)| *id == id_a));
1071
1072 let results_fb = knn_search(&conn, &query, &["global".to_string()], Some("feedback"), 5)?;
1073 assert!(results_fb.iter().all(|(id, _)| *id == id_b));
1074 Ok(())
1075 }
1076
1077 #[test]
1078 fn fts_search_finds_by_prefix_in_body() -> TestResult {
1079 let conn = setup_conn()?;
1080 let mut m = new_memory("mem-fts");
1081 m.body = "linguagem de programacao rust".to_string();
1082 insert(&conn, &m)?;
1083
1084 conn.execute_batch(
1085 "INSERT INTO fts_memories(rowid, name, description, body)
1086 SELECT id, name, description, body FROM memories WHERE deleted_at IS NULL",
1087 )?;
1088
1089 let rows = fts_search(&conn, "programacao", "global", None, 10)?;
1090 assert!(!rows.is_empty());
1091 assert!(rows.iter().any(|r| r.name == "mem-fts"));
1092 Ok(())
1093 }
1094
1095 #[test]
1096 fn fts_search_with_type_filter() -> TestResult {
1097 let conn = setup_conn()?;
1098 let mut m = new_memory("mem-fts-tipo");
1099 m.body = "linguagem especial para filtro".to_string();
1100 insert(&conn, &m)?;
1101
1102 let mut m2 = new_memory("mem-fts-feedback");
1103 m2.memory_type = "feedback".to_string();
1104 m2.body = "linguagem especial para filtro".to_string();
1105 insert(&conn, &m2)?;
1106
1107 conn.execute_batch(
1108 "INSERT INTO fts_memories(rowid, name, description, body)
1109 SELECT id, name, description, body FROM memories WHERE deleted_at IS NULL",
1110 )?;
1111
1112 let rows_user = fts_search(&conn, "especial", "global", Some("user"), 10)?;
1113 assert!(rows_user.iter().all(|r| r.memory_type == "user"));
1114
1115 let rows_fb = fts_search(&conn, "especial", "global", Some("feedback"), 10)?;
1116 assert!(rows_fb.iter().all(|r| r.memory_type == "feedback"));
1117 Ok(())
1118 }
1119
1120 #[test]
1121 fn fts_search_excludes_deleted() -> TestResult {
1122 let conn = setup_conn()?;
1123 let mut m = new_memory("mem-fts-del");
1124 m.body = "conteudo deletado fts".to_string();
1125 insert(&conn, &m)?;
1126
1127 conn.execute_batch(
1128 "INSERT INTO fts_memories(rowid, name, description, body)
1129 SELECT id, name, description, body FROM memories WHERE deleted_at IS NULL",
1130 )?;
1131
1132 soft_delete(&conn, "global", "mem-fts-del")?;
1133
1134 let rows = fts_search(&conn, "deletado", "global", None, 10)?;
1135 assert!(rows.iter().all(|r| r.name != "mem-fts-del"));
1136 Ok(())
1137 }
1138
1139 #[test]
1140 fn list_deleted_before_returns_correct_ids() -> TestResult {
1141 let conn = setup_conn()?;
1142 let m = new_memory("mem-purge");
1143 insert(&conn, &m)?;
1144 soft_delete(&conn, "global", "mem-purge")?;
1145
1146 let ids = list_deleted_before(&conn, "global", i64::MAX)?;
1147 assert!(!ids.is_empty());
1148
1149 let ids_antes = list_deleted_before(&conn, "global", 0)?;
1150 assert!(ids_antes.is_empty());
1151 Ok(())
1152 }
1153
1154 #[test]
1155 fn find_by_name_returns_correct_max_version() -> TestResult {
1156 let conn = setup_conn()?;
1157 let m = new_memory("mem-ver");
1158 let id = insert(&conn, &m)?;
1159
1160 let (_, _, v0) = find_by_name(&conn, "global", "mem-ver")?.ok_or("mem-ver should exist")?;
1161 assert_eq!(v0, 0);
1162
1163 conn.execute(
1164 "INSERT INTO memory_versions (memory_id, version, name, type, description, body, metadata, change_reason)
1165 VALUES (?1, 1, 'mem-ver', 'user', 'desc', 'body', '{}', 'create')",
1166 params![id],
1167 )?;
1168
1169 let (_, _, v1) =
1170 find_by_name(&conn, "global", "mem-ver")?.ok_or("mem-ver should exist after insert")?;
1171 assert_eq!(v1, 1);
1172 Ok(())
1173 }
1174
1175 #[test]
1176 fn insert_com_metadata_json() -> TestResult {
1177 let conn = setup_conn()?;
1178 let mut m = new_memory("mem-meta");
1179 m.metadata = serde_json::json!({"chave": "valor", "numero": 42});
1180 let id = insert(&conn, &m)?;
1181
1182 let row = read_full(&conn, id)?.ok_or("mem-meta should exist")?;
1183 let meta: serde_json::Value = serde_json::from_str(&row.metadata)?;
1184 assert_eq!(meta["chave"], "valor");
1185 assert_eq!(meta["numero"], 42);
1186 Ok(())
1187 }
1188
1189 #[test]
1190 fn insert_com_session_id() -> TestResult {
1191 let conn = setup_conn()?;
1192 let mut m = new_memory("mem-session");
1193 m.session_id = Some("sessao-xyz".to_string());
1194 let id = insert(&conn, &m)?;
1195
1196 let row = read_full(&conn, id)?.ok_or("mem-session should exist")?;
1197 assert_eq!(row.session_id, Some("sessao-xyz".to_string()));
1198 Ok(())
1199 }
1200
1201 #[test]
1202 fn delete_vec_for_nonexistent_id_does_not_fail() -> TestResult {
1203 let conn = setup_conn()?;
1204 let result = delete_vec(&conn, 99999);
1205 assert!(result.is_ok());
1206 Ok(())
1207 }
1208}