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) -> Result<Vec<MemoryRow>, AppError> {
344 if let Some(mt) = memory_type {
345 let mut stmt = conn.prepare(
346 "SELECT id, namespace, name, type, description, body, body_hash,
347 session_id, source, metadata, created_at, updated_at
348 FROM memories WHERE namespace=?1 AND type=?2 AND deleted_at IS NULL
349 ORDER BY updated_at DESC LIMIT ?3 OFFSET ?4",
350 )?;
351 let rows = stmt
352 .query_map(params![namespace, mt, limit as i64, offset as i64], |r| {
353 Ok(MemoryRow {
354 id: r.get(0)?,
355 namespace: r.get(1)?,
356 name: r.get(2)?,
357 memory_type: r.get(3)?,
358 description: r.get(4)?,
359 body: r.get(5)?,
360 body_hash: r.get(6)?,
361 session_id: r.get(7)?,
362 source: r.get(8)?,
363 metadata: r.get(9)?,
364 created_at: r.get(10)?,
365 updated_at: r.get(11)?,
366 })
367 })?
368 .collect::<Result<Vec<_>, _>>()?;
369 Ok(rows)
370 } else {
371 let mut stmt = conn.prepare(
372 "SELECT id, namespace, name, type, description, body, body_hash,
373 session_id, source, metadata, created_at, updated_at
374 FROM memories WHERE namespace=?1 AND deleted_at IS NULL
375 ORDER BY updated_at DESC LIMIT ?2 OFFSET ?3",
376 )?;
377 let rows = stmt
378 .query_map(params![namespace, limit as i64, offset as i64], |r| {
379 Ok(MemoryRow {
380 id: r.get(0)?,
381 namespace: r.get(1)?,
382 name: r.get(2)?,
383 memory_type: r.get(3)?,
384 description: r.get(4)?,
385 body: r.get(5)?,
386 body_hash: r.get(6)?,
387 session_id: r.get(7)?,
388 source: r.get(8)?,
389 metadata: r.get(9)?,
390 created_at: r.get(10)?,
391 updated_at: r.get(11)?,
392 })
393 })?
394 .collect::<Result<Vec<_>, _>>()?;
395 Ok(rows)
396 }
397}
398
399pub fn knn_search(
415 conn: &Connection,
416 embedding: &[f32],
417 namespace: &str,
418 memory_type: Option<&str>,
419 k: usize,
420) -> Result<Vec<(i64, f32)>, AppError> {
421 let bytes = f32_to_bytes(embedding);
422 if let Some(mt) = memory_type {
423 let mut stmt = conn.prepare(
424 "SELECT memory_id, distance FROM vec_memories
425 WHERE embedding MATCH ?1 AND namespace = ?2 AND type = ?3
426 ORDER BY distance LIMIT ?4",
427 )?;
428 let rows = stmt
429 .query_map(params![bytes, namespace, mt, k as i64], |r| {
430 Ok((r.get::<_, i64>(0)?, r.get::<_, f32>(1)?))
431 })?
432 .collect::<Result<Vec<_>, _>>()?;
433 Ok(rows)
434 } else {
435 let mut stmt = conn.prepare(
436 "SELECT memory_id, distance FROM vec_memories
437 WHERE embedding MATCH ?1 AND namespace = ?2
438 ORDER BY distance LIMIT ?3",
439 )?;
440 let rows = stmt
441 .query_map(params![bytes, namespace, k as i64], |r| {
442 Ok((r.get::<_, i64>(0)?, r.get::<_, f32>(1)?))
443 })?
444 .collect::<Result<Vec<_>, _>>()?;
445 Ok(rows)
446 }
447}
448
449pub fn read_full(conn: &Connection, memory_id: i64) -> Result<Option<MemoryRow>, AppError> {
457 let mut stmt = conn.prepare_cached(
458 "SELECT id, namespace, name, type, description, body, body_hash,
459 session_id, source, metadata, created_at, updated_at
460 FROM memories WHERE id=?1 AND deleted_at IS NULL",
461 )?;
462 match stmt.query_row(params![memory_id], |r| {
463 Ok(MemoryRow {
464 id: r.get(0)?,
465 namespace: r.get(1)?,
466 name: r.get(2)?,
467 memory_type: r.get(3)?,
468 description: r.get(4)?,
469 body: r.get(5)?,
470 body_hash: r.get(6)?,
471 session_id: r.get(7)?,
472 source: r.get(8)?,
473 metadata: r.get(9)?,
474 created_at: r.get(10)?,
475 updated_at: r.get(11)?,
476 })
477 }) {
478 Ok(m) => Ok(Some(m)),
479 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
480 Err(e) => Err(AppError::Database(e)),
481 }
482}
483
484pub fn list_deleted_before(
493 conn: &Connection,
494 namespace: &str,
495 before_ts: i64,
496) -> Result<Vec<i64>, AppError> {
497 let mut stmt = conn.prepare_cached(
498 "SELECT id FROM memories WHERE namespace = ?1 AND deleted_at IS NOT NULL AND deleted_at < ?2",
499 )?;
500 let ids = stmt
501 .query_map(params![namespace, before_ts], |r| r.get::<_, i64>(0))?
502 .collect::<Result<Vec<_>, _>>()?;
503 Ok(ids)
504}
505
506pub fn fts_search(
515 conn: &Connection,
516 query: &str,
517 namespace: &str,
518 memory_type: Option<&str>,
519 limit: usize,
520) -> Result<Vec<MemoryRow>, AppError> {
521 let fts_query = format!("{query}*");
522 if let Some(mt) = memory_type {
523 let mut stmt = conn.prepare(
524 "SELECT m.id, m.namespace, m.name, m.type, m.description, m.body, m.body_hash,
525 m.session_id, m.source, m.metadata, m.created_at, m.updated_at
526 FROM fts_memories fts
527 JOIN memories m ON m.id = fts.rowid
528 WHERE fts_memories MATCH ?1 AND m.namespace = ?2 AND m.type = ?3 AND m.deleted_at IS NULL
529 ORDER BY rank LIMIT ?4",
530 )?;
531 let rows = stmt
532 .query_map(params![fts_query, namespace, mt, limit as i64], |r| {
533 Ok(MemoryRow {
534 id: r.get(0)?,
535 namespace: r.get(1)?,
536 name: r.get(2)?,
537 memory_type: r.get(3)?,
538 description: r.get(4)?,
539 body: r.get(5)?,
540 body_hash: r.get(6)?,
541 session_id: r.get(7)?,
542 source: r.get(8)?,
543 metadata: r.get(9)?,
544 created_at: r.get(10)?,
545 updated_at: r.get(11)?,
546 })
547 })?
548 .collect::<Result<Vec<_>, _>>()?;
549 Ok(rows)
550 } else {
551 let mut stmt = conn.prepare(
552 "SELECT m.id, m.namespace, m.name, m.type, m.description, m.body, m.body_hash,
553 m.session_id, m.source, m.metadata, m.created_at, m.updated_at
554 FROM fts_memories fts
555 JOIN memories m ON m.id = fts.rowid
556 WHERE fts_memories MATCH ?1 AND m.namespace = ?2 AND m.deleted_at IS NULL
557 ORDER BY rank LIMIT ?3",
558 )?;
559 let rows = stmt
560 .query_map(params![fts_query, namespace, limit as i64], |r| {
561 Ok(MemoryRow {
562 id: r.get(0)?,
563 namespace: r.get(1)?,
564 name: r.get(2)?,
565 memory_type: r.get(3)?,
566 description: r.get(4)?,
567 body: r.get(5)?,
568 body_hash: r.get(6)?,
569 session_id: r.get(7)?,
570 source: r.get(8)?,
571 metadata: r.get(9)?,
572 created_at: r.get(10)?,
573 updated_at: r.get(11)?,
574 })
575 })?
576 .collect::<Result<Vec<_>, _>>()?;
577 Ok(rows)
578 }
579}
580
581#[cfg(test)]
582mod testes {
583 use super::*;
584 use rusqlite::Connection;
585
586 type Resultado = Result<(), Box<dyn std::error::Error>>;
587
588 fn setup_conn() -> Result<Connection, Box<dyn std::error::Error>> {
589 crate::storage::connection::register_vec_extension();
590 let mut conn = Connection::open_in_memory()?;
591 conn.execute_batch(
592 "PRAGMA foreign_keys = ON;
593 PRAGMA temp_store = MEMORY;",
594 )?;
595 crate::migrations::runner().run(&mut conn)?;
596 Ok(conn)
597 }
598
599 fn nova_memoria(name: &str) -> NewMemory {
600 NewMemory {
601 namespace: "global".to_string(),
602 name: name.to_string(),
603 memory_type: "user".to_string(),
604 description: "descricao de teste".to_string(),
605 body: "corpo da memoria de teste".to_string(),
606 body_hash: format!("hash-{name}"),
607 session_id: None,
608 source: "agent".to_string(),
609 metadata: serde_json::json!({}),
610 }
611 }
612
613 #[test]
614 fn insert_e_find_by_name_retornam_id() -> Resultado {
615 let conn = setup_conn()?;
616 let m = nova_memoria("mem-alpha");
617 let id = insert(&conn, &m)?;
618 assert!(id > 0);
619
620 let found = find_by_name(&conn, "global", "mem-alpha")?;
621 assert!(found.is_some());
622 let (found_id, _, _) = found.ok_or("mem-alpha deveria existir")?;
623 assert_eq!(found_id, id);
624 Ok(())
625 }
626
627 #[test]
628 fn find_by_name_retorna_none_quando_nao_existe() -> Resultado {
629 let conn = setup_conn()?;
630 let result = find_by_name(&conn, "global", "inexistente")?;
631 assert!(result.is_none());
632 Ok(())
633 }
634
635 #[test]
636 fn find_by_hash_retorna_id_correto() -> Resultado {
637 let conn = setup_conn()?;
638 let m = nova_memoria("mem-hash");
639 let id = insert(&conn, &m)?;
640
641 let found = find_by_hash(&conn, "global", "hash-mem-hash")?;
642 assert_eq!(found, Some(id));
643 Ok(())
644 }
645
646 #[test]
647 fn find_by_hash_retorna_none_quando_hash_nao_existe() -> Resultado {
648 let conn = setup_conn()?;
649 let result = find_by_hash(&conn, "global", "hash-inexistente")?;
650 assert!(result.is_none());
651 Ok(())
652 }
653
654 #[test]
655 fn find_by_hash_ignora_namespace_diferente() -> Resultado {
656 let conn = setup_conn()?;
657 let m = nova_memoria("mem-ns");
658 insert(&conn, &m)?;
659
660 let result = find_by_hash(&conn, "outro-namespace", "hash-mem-ns")?;
661 assert!(result.is_none());
662 Ok(())
663 }
664
665 #[test]
666 fn read_by_name_retorna_memoria_completa() -> Resultado {
667 let conn = setup_conn()?;
668 let m = nova_memoria("mem-read");
669 let id = insert(&conn, &m)?;
670
671 let row = read_by_name(&conn, "global", "mem-read")?.ok_or("mem-read deveria existir")?;
672 assert_eq!(row.id, id);
673 assert_eq!(row.name, "mem-read");
674 assert_eq!(row.memory_type, "user");
675 assert_eq!(row.body, "corpo da memoria de teste");
676 assert_eq!(row.namespace, "global");
677 Ok(())
678 }
679
680 #[test]
681 fn read_by_name_retorna_none_para_ausente() -> Resultado {
682 let conn = setup_conn()?;
683 let result = read_by_name(&conn, "global", "nao-existe")?;
684 assert!(result.is_none());
685 Ok(())
686 }
687
688 #[test]
689 fn read_full_por_id_retorna_memoria() -> Resultado {
690 let conn = setup_conn()?;
691 let m = nova_memoria("mem-full");
692 let id = insert(&conn, &m)?;
693
694 let row = read_full(&conn, id)?.ok_or("mem-full deveria existir")?;
695 assert_eq!(row.id, id);
696 assert_eq!(row.name, "mem-full");
697 Ok(())
698 }
699
700 #[test]
701 fn read_full_retorna_none_para_id_inexistente() -> Resultado {
702 let conn = setup_conn()?;
703 let result = read_full(&conn, 9999)?;
704 assert!(result.is_none());
705 Ok(())
706 }
707
708 #[test]
709 fn update_sem_otimismo_modifica_campos() -> Resultado {
710 let conn = setup_conn()?;
711 let m = nova_memoria("mem-upd");
712 let id = insert(&conn, &m)?;
713
714 let mut m2 = nova_memoria("mem-upd");
715 m2.body = "corpo atualizado".to_string();
716 m2.body_hash = "hash-novo".to_string();
717 let ok = update(&conn, id, &m2, None)?;
718 assert!(ok);
719
720 let row = read_full(&conn, id)?.ok_or("mem-upd deveria existir")?;
721 assert_eq!(row.body, "corpo atualizado");
722 assert_eq!(row.body_hash, "hash-novo");
723 Ok(())
724 }
725
726 #[test]
727 fn update_com_expected_updated_at_correto_tem_sucesso() -> Resultado {
728 let conn = setup_conn()?;
729 let m = nova_memoria("mem-opt");
730 let id = insert(&conn, &m)?;
731
732 let (_, updated_at, _) =
733 find_by_name(&conn, "global", "mem-opt")?.ok_or("mem-opt deveria existir")?;
734
735 let mut m2 = nova_memoria("mem-opt");
736 m2.body = "corpo otimista".to_string();
737 m2.body_hash = "hash-otimista".to_string();
738 let ok = update(&conn, id, &m2, Some(updated_at))?;
739 assert!(ok);
740
741 let row = read_full(&conn, id)?.ok_or("mem-opt deveria existir após update")?;
742 assert_eq!(row.body, "corpo otimista");
743 Ok(())
744 }
745
746 #[test]
747 fn update_com_expected_updated_at_errado_retorna_false() -> Resultado {
748 let conn = setup_conn()?;
749 let m = nova_memoria("mem-conflict");
750 let id = insert(&conn, &m)?;
751
752 let mut m2 = nova_memoria("mem-conflict");
753 m2.body = "nao deve aparecer".to_string();
754 m2.body_hash = "hash-x".to_string();
755 let ok = update(&conn, id, &m2, Some(0))?;
756 assert!(!ok);
757
758 let row = read_full(&conn, id)?.ok_or("mem-conflict deveria existir")?;
759 assert_eq!(row.body, "corpo da memoria de teste");
760 Ok(())
761 }
762
763 #[test]
764 fn update_id_inexistente_retorna_false() -> Resultado {
765 let conn = setup_conn()?;
766 let m = nova_memoria("fantasma");
767 let ok = update(&conn, 9999, &m, None)?;
768 assert!(!ok);
769 Ok(())
770 }
771
772 #[test]
773 fn soft_delete_marca_deleted_at() -> Resultado {
774 let conn = setup_conn()?;
775 let m = nova_memoria("mem-del");
776 insert(&conn, &m)?;
777
778 let ok = soft_delete(&conn, "global", "mem-del")?;
779 assert!(ok);
780
781 let result = find_by_name(&conn, "global", "mem-del")?;
782 assert!(result.is_none());
783
784 let result_read = read_by_name(&conn, "global", "mem-del")?;
785 assert!(result_read.is_none());
786 Ok(())
787 }
788
789 #[test]
790 fn soft_delete_retorna_false_quando_nao_existe() -> Resultado {
791 let conn = setup_conn()?;
792 let ok = soft_delete(&conn, "global", "nao-existe")?;
793 assert!(!ok);
794 Ok(())
795 }
796
797 #[test]
798 fn soft_delete_duplo_retorna_false_na_segunda_vez() -> Resultado {
799 let conn = setup_conn()?;
800 let m = nova_memoria("mem-del2");
801 insert(&conn, &m)?;
802
803 soft_delete(&conn, "global", "mem-del2")?;
804 let ok = soft_delete(&conn, "global", "mem-del2")?;
805 assert!(!ok);
806 Ok(())
807 }
808
809 #[test]
810 fn list_retorna_memorias_do_namespace() -> Resultado {
811 let conn = setup_conn()?;
812 insert(&conn, &nova_memoria("mem-list-a"))?;
813 insert(&conn, &nova_memoria("mem-list-b"))?;
814
815 let rows = list(&conn, "global", None, 10, 0)?;
816 assert!(rows.len() >= 2);
817 let nomes: Vec<_> = rows.iter().map(|r| r.name.as_str()).collect();
818 assert!(nomes.contains(&"mem-list-a"));
819 assert!(nomes.contains(&"mem-list-b"));
820 Ok(())
821 }
822
823 #[test]
824 fn list_com_filtro_de_tipo_retorna_apenas_tipo_correto() -> Resultado {
825 let conn = setup_conn()?;
826 insert(&conn, &nova_memoria("mem-user"))?;
827
828 let mut m2 = nova_memoria("mem-feedback");
829 m2.memory_type = "feedback".to_string();
830 insert(&conn, &m2)?;
831
832 let rows_user = list(&conn, "global", Some("user"), 10, 0)?;
833 assert!(rows_user.iter().all(|r| r.memory_type == "user"));
834
835 let rows_fb = list(&conn, "global", Some("feedback"), 10, 0)?;
836 assert!(rows_fb.iter().all(|r| r.memory_type == "feedback"));
837 Ok(())
838 }
839
840 #[test]
841 fn list_exclui_soft_deleted() -> Resultado {
842 let conn = setup_conn()?;
843 let m = nova_memoria("mem-excluida");
844 insert(&conn, &m)?;
845 soft_delete(&conn, "global", "mem-excluida")?;
846
847 let rows = list(&conn, "global", None, 10, 0)?;
848 assert!(rows.iter().all(|r| r.name != "mem-excluida"));
849 Ok(())
850 }
851
852 #[test]
853 fn list_paginacao_funciona() -> Resultado {
854 let conn = setup_conn()?;
855 for i in 0..5 {
856 insert(&conn, &nova_memoria(&format!("mem-pag-{i}")))?;
857 }
858
859 let pagina1 = list(&conn, "global", None, 2, 0)?;
860 let pagina2 = list(&conn, "global", None, 2, 2)?;
861 assert!(pagina1.len() <= 2);
862 assert!(pagina2.len() <= 2);
863 if !pagina1.is_empty() && !pagina2.is_empty() {
864 assert_ne!(pagina1[0].id, pagina2[0].id);
865 }
866 Ok(())
867 }
868
869 #[test]
870 fn upsert_vec_e_delete_vec_funcionam() -> Resultado {
871 let conn = setup_conn()?;
872 let m = nova_memoria("mem-vec");
873 let id = insert(&conn, &m)?;
874
875 let embedding: Vec<f32> = vec![0.1; 384];
876 upsert_vec(
877 &conn, id, "global", "user", &embedding, "mem-vec", "snippet",
878 )?;
879
880 let count: i64 = conn.query_row(
881 "SELECT COUNT(*) FROM vec_memories WHERE memory_id = ?1",
882 params![id],
883 |r| r.get(0),
884 )?;
885 assert_eq!(count, 1);
886
887 delete_vec(&conn, id)?;
888
889 let count_after: i64 = conn.query_row(
890 "SELECT COUNT(*) FROM vec_memories WHERE memory_id = ?1",
891 params![id],
892 |r| r.get(0),
893 )?;
894 assert_eq!(count_after, 0);
895 Ok(())
896 }
897
898 #[test]
899 fn upsert_vec_substitui_vetor_existente() -> Resultado {
900 let conn = setup_conn()?;
901 let m = nova_memoria("mem-vec-upsert");
902 let id = insert(&conn, &m)?;
903
904 let emb1: Vec<f32> = vec![0.1; 384];
905 upsert_vec(&conn, id, "global", "user", &emb1, "mem-vec-upsert", "s1")?;
906
907 let emb2: Vec<f32> = vec![0.9; 384];
908 upsert_vec(&conn, id, "global", "user", &emb2, "mem-vec-upsert", "s2")?;
909
910 let count: i64 = conn.query_row(
911 "SELECT COUNT(*) FROM vec_memories WHERE memory_id = ?1",
912 params![id],
913 |r| r.get(0),
914 )?;
915 assert_eq!(count, 1);
916 Ok(())
917 }
918
919 #[test]
920 fn knn_search_retorna_resultados_por_distancia() -> Resultado {
921 let conn = setup_conn()?;
922
923 let ma = nova_memoria("mem-knn-a");
925 let id_a = insert(&conn, &ma)?;
926 let emb_a: Vec<f32> = vec![1.0; 384];
927 upsert_vec(&conn, id_a, "global", "user", &emb_a, "mem-knn-a", "s")?;
928
929 let mb = nova_memoria("mem-knn-b");
931 let id_b = insert(&conn, &mb)?;
932 let emb_b: Vec<f32> = vec![-1.0; 384];
933 upsert_vec(&conn, id_b, "global", "user", &emb_b, "mem-knn-b", "s")?;
934
935 let query: Vec<f32> = vec![1.0; 384];
936 let results = knn_search(&conn, &query, "global", None, 2)?;
937 assert!(!results.is_empty());
938 assert_eq!(results[0].0, id_a);
939 Ok(())
940 }
941
942 #[test]
943 fn knn_search_com_filtro_de_tipo_restringe_resultado() -> Resultado {
944 let conn = setup_conn()?;
945
946 let ma = nova_memoria("mem-knn-tipo-user");
947 let id_a = insert(&conn, &ma)?;
948 let emb: Vec<f32> = vec![1.0; 384];
949 upsert_vec(
950 &conn,
951 id_a,
952 "global",
953 "user",
954 &emb,
955 "mem-knn-tipo-user",
956 "s",
957 )?;
958
959 let mut mb = nova_memoria("mem-knn-tipo-fb");
960 mb.memory_type = "feedback".to_string();
961 let id_b = insert(&conn, &mb)?;
962 upsert_vec(
963 &conn,
964 id_b,
965 "global",
966 "feedback",
967 &emb,
968 "mem-knn-tipo-fb",
969 "s",
970 )?;
971
972 let query: Vec<f32> = vec![1.0; 384];
973 let results_user = knn_search(&conn, &query, "global", Some("user"), 5)?;
974 assert!(results_user.iter().all(|(id, _)| *id == id_a));
975
976 let results_fb = knn_search(&conn, &query, "global", Some("feedback"), 5)?;
977 assert!(results_fb.iter().all(|(id, _)| *id == id_b));
978 Ok(())
979 }
980
981 #[test]
982 fn fts_search_encontra_por_prefixo_no_body() -> Resultado {
983 let conn = setup_conn()?;
984 let mut m = nova_memoria("mem-fts");
985 m.body = "linguagem de programacao rust".to_string();
986 insert(&conn, &m)?;
987
988 conn.execute_batch(
989 "INSERT INTO fts_memories(rowid, name, description, body)
990 SELECT id, name, description, body FROM memories WHERE deleted_at IS NULL",
991 )?;
992
993 let rows = fts_search(&conn, "programacao", "global", None, 10)?;
994 assert!(!rows.is_empty());
995 assert!(rows.iter().any(|r| r.name == "mem-fts"));
996 Ok(())
997 }
998
999 #[test]
1000 fn fts_search_com_filtro_de_tipo() -> Resultado {
1001 let conn = setup_conn()?;
1002 let mut m = nova_memoria("mem-fts-tipo");
1003 m.body = "linguagem especial para filtro".to_string();
1004 insert(&conn, &m)?;
1005
1006 let mut m2 = nova_memoria("mem-fts-feedback");
1007 m2.memory_type = "feedback".to_string();
1008 m2.body = "linguagem especial para filtro".to_string();
1009 insert(&conn, &m2)?;
1010
1011 conn.execute_batch(
1012 "INSERT INTO fts_memories(rowid, name, description, body)
1013 SELECT id, name, description, body FROM memories WHERE deleted_at IS NULL",
1014 )?;
1015
1016 let rows_user = fts_search(&conn, "especial", "global", Some("user"), 10)?;
1017 assert!(rows_user.iter().all(|r| r.memory_type == "user"));
1018
1019 let rows_fb = fts_search(&conn, "especial", "global", Some("feedback"), 10)?;
1020 assert!(rows_fb.iter().all(|r| r.memory_type == "feedback"));
1021 Ok(())
1022 }
1023
1024 #[test]
1025 fn fts_search_nao_retorna_deletados() -> Resultado {
1026 let conn = setup_conn()?;
1027 let mut m = nova_memoria("mem-fts-del");
1028 m.body = "conteudo deletado fts".to_string();
1029 insert(&conn, &m)?;
1030
1031 conn.execute_batch(
1032 "INSERT INTO fts_memories(rowid, name, description, body)
1033 SELECT id, name, description, body FROM memories WHERE deleted_at IS NULL",
1034 )?;
1035
1036 soft_delete(&conn, "global", "mem-fts-del")?;
1037
1038 let rows = fts_search(&conn, "deletado", "global", None, 10)?;
1039 assert!(rows.iter().all(|r| r.name != "mem-fts-del"));
1040 Ok(())
1041 }
1042
1043 #[test]
1044 fn list_deleted_before_retorna_ids_corretos() -> Resultado {
1045 let conn = setup_conn()?;
1046 let m = nova_memoria("mem-purge");
1047 insert(&conn, &m)?;
1048 soft_delete(&conn, "global", "mem-purge")?;
1049
1050 let ids = list_deleted_before(&conn, "global", i64::MAX)?;
1051 assert!(!ids.is_empty());
1052
1053 let ids_antes = list_deleted_before(&conn, "global", 0)?;
1054 assert!(ids_antes.is_empty());
1055 Ok(())
1056 }
1057
1058 #[test]
1059 fn find_by_name_retorna_max_version_correto() -> Resultado {
1060 let conn = setup_conn()?;
1061 let m = nova_memoria("mem-ver");
1062 let id = insert(&conn, &m)?;
1063
1064 let (_, _, v0) =
1065 find_by_name(&conn, "global", "mem-ver")?.ok_or("mem-ver deveria existir")?;
1066 assert_eq!(v0, 0);
1067
1068 conn.execute(
1069 "INSERT INTO memory_versions (memory_id, version, name, type, description, body, metadata, change_reason)
1070 VALUES (?1, 1, 'mem-ver', 'user', 'desc', 'body', '{}', 'create')",
1071 params![id],
1072 )?;
1073
1074 let (_, _, v1) = find_by_name(&conn, "global", "mem-ver")?
1075 .ok_or("mem-ver deveria existir após insert")?;
1076 assert_eq!(v1, 1);
1077 Ok(())
1078 }
1079
1080 #[test]
1081 fn insert_com_metadata_json() -> Resultado {
1082 let conn = setup_conn()?;
1083 let mut m = nova_memoria("mem-meta");
1084 m.metadata = serde_json::json!({"chave": "valor", "numero": 42});
1085 let id = insert(&conn, &m)?;
1086
1087 let row = read_full(&conn, id)?.ok_or("mem-meta deveria existir")?;
1088 let meta: serde_json::Value = serde_json::from_str(&row.metadata)?;
1089 assert_eq!(meta["chave"], "valor");
1090 assert_eq!(meta["numero"], 42);
1091 Ok(())
1092 }
1093
1094 #[test]
1095 fn insert_com_session_id() -> Resultado {
1096 let conn = setup_conn()?;
1097 let mut m = nova_memoria("mem-session");
1098 m.session_id = Some("sessao-xyz".to_string());
1099 let id = insert(&conn, &m)?;
1100
1101 let row = read_full(&conn, id)?.ok_or("mem-session deveria existir")?;
1102 assert_eq!(row.session_id, Some("sessao-xyz".to_string()));
1103 Ok(())
1104 }
1105
1106 #[test]
1107 fn delete_vec_em_id_inexistente_nao_falha() -> Resultado {
1108 let conn = setup_conn()?;
1109 let result = delete_vec(&conn, 99999);
1110 assert!(result.is_ok());
1111 Ok(())
1112 }
1113}