1use crate::embedder::f32_to_bytes;
8use crate::errors::AppError;
9use rusqlite::{params, Connection};
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Serialize, Deserialize)]
17pub struct NewMemory {
18 pub namespace: String,
19 pub name: String,
20 pub memory_type: String,
21 pub description: String,
22 pub body: String,
23 pub body_hash: String,
24 pub session_id: Option<String>,
25 pub source: String,
26 pub metadata: serde_json::Value,
27}
28
29#[derive(Debug, Serialize)]
34pub struct MemoryRow {
35 pub id: i64,
36 pub namespace: String,
37 pub name: String,
38 pub memory_type: String,
39 pub description: String,
40 pub body: String,
41 pub body_hash: String,
42 pub session_id: Option<String>,
43 pub source: String,
44 pub metadata: String,
45 pub created_at: i64,
46 pub updated_at: i64,
47}
48
49pub fn find_by_name(
66 conn: &Connection,
67 namespace: &str,
68 name: &str,
69) -> Result<Option<(i64, i64, i64)>, AppError> {
70 let mut stmt = conn.prepare_cached(
71 "SELECT m.id, m.updated_at, COALESCE(MAX(v.version), 0)
72 FROM memories m
73 LEFT JOIN memory_versions v ON v.memory_id = m.id
74 WHERE m.namespace = ?1 AND m.name = ?2 AND m.deleted_at IS NULL
75 GROUP BY m.id",
76 )?;
77 let result = stmt.query_row(params![namespace, name], |r| {
78 Ok((
79 r.get::<_, i64>(0)?,
80 r.get::<_, i64>(1)?,
81 r.get::<_, i64>(2)?,
82 ))
83 });
84 match result {
85 Ok(row) => Ok(Some(row)),
86 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
87 Err(e) => Err(AppError::Database(e)),
88 }
89}
90
91pub fn find_by_hash(
105 conn: &Connection,
106 namespace: &str,
107 body_hash: &str,
108) -> Result<Option<i64>, AppError> {
109 let mut stmt = conn.prepare_cached(
110 "SELECT id FROM memories WHERE namespace = ?1 AND body_hash = ?2 AND deleted_at IS NULL",
111 )?;
112 match stmt.query_row(params![namespace, body_hash], |r| r.get(0)) {
113 Ok(id) => Ok(Some(id)),
114 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
115 Err(e) => Err(AppError::Database(e)),
116 }
117}
118
119pub fn insert(conn: &Connection, m: &NewMemory) -> Result<i64, AppError> {
135 conn.execute(
136 "INSERT INTO memories (namespace, name, type, description, body, body_hash, session_id, source, metadata)
137 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
138 params![
139 m.namespace, m.name, m.memory_type, m.description, m.body,
140 m.body_hash, m.session_id, m.source,
141 serde_json::to_string(&m.metadata)?
142 ],
143 )?;
144 Ok(conn.last_insert_rowid())
145}
146
147pub fn update(
162 conn: &Connection,
163 id: i64,
164 m: &NewMemory,
165 expected_updated_at: Option<i64>,
166) -> Result<bool, AppError> {
167 let affected = if let Some(ts) = expected_updated_at {
168 conn.execute(
169 "UPDATE memories SET type=?2, description=?3, body=?4, body_hash=?5,
170 session_id=?6, source=?7, metadata=?8
171 WHERE id=?1 AND updated_at=?9 AND deleted_at IS NULL",
172 params![
173 id,
174 m.memory_type,
175 m.description,
176 m.body,
177 m.body_hash,
178 m.session_id,
179 m.source,
180 serde_json::to_string(&m.metadata)?,
181 ts
182 ],
183 )?
184 } else {
185 conn.execute(
186 "UPDATE memories SET type=?2, description=?3, body=?4, body_hash=?5,
187 session_id=?6, source=?7, metadata=?8
188 WHERE id=?1 AND deleted_at IS NULL",
189 params![
190 id,
191 m.memory_type,
192 m.description,
193 m.body,
194 m.body_hash,
195 m.session_id,
196 m.source,
197 serde_json::to_string(&m.metadata)?
198 ],
199 )?
200 };
201 Ok(affected == 1)
202}
203
204pub fn upsert_vec(
214 conn: &Connection,
215 memory_id: i64,
216 namespace: &str,
217 memory_type: &str,
218 embedding: &[f32],
219 name: &str,
220 snippet: &str,
221) -> Result<(), AppError> {
222 conn.execute(
225 "DELETE FROM vec_memories WHERE memory_id = ?1",
226 params![memory_id],
227 )?;
228 conn.execute(
229 "INSERT INTO vec_memories(memory_id, namespace, type, embedding, name, snippet)
230 VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
231 params![
232 memory_id,
233 namespace,
234 memory_type,
235 f32_to_bytes(embedding),
236 name,
237 snippet
238 ],
239 )?;
240 Ok(())
241}
242
243pub fn delete_vec(conn: &Connection, memory_id: i64) -> Result<(), AppError> {
252 conn.execute(
253 "DELETE FROM vec_memories WHERE memory_id = ?1",
254 params![memory_id],
255 )?;
256 Ok(())
257}
258
259pub fn read_by_name(
269 conn: &Connection,
270 namespace: &str,
271 name: &str,
272) -> Result<Option<MemoryRow>, AppError> {
273 let mut stmt = conn.prepare_cached(
274 "SELECT id, namespace, name, type, description, body, body_hash,
275 session_id, source, metadata, created_at, updated_at
276 FROM memories WHERE namespace=?1 AND name=?2 AND deleted_at IS NULL",
277 )?;
278 match stmt.query_row(params![namespace, name], |r| {
279 Ok(MemoryRow {
280 id: r.get(0)?,
281 namespace: r.get(1)?,
282 name: r.get(2)?,
283 memory_type: r.get(3)?,
284 description: r.get(4)?,
285 body: r.get(5)?,
286 body_hash: r.get(6)?,
287 session_id: r.get(7)?,
288 source: r.get(8)?,
289 metadata: r.get(9)?,
290 created_at: r.get(10)?,
291 updated_at: r.get(11)?,
292 })
293 }) {
294 Ok(m) => Ok(Some(m)),
295 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
296 Err(e) => Err(AppError::Database(e)),
297 }
298}
299
300pub fn soft_delete(conn: &Connection, namespace: &str, name: &str) -> Result<bool, AppError> {
314 let affected = conn.execute(
315 "UPDATE memories SET deleted_at = unixepoch() WHERE namespace=?1 AND name=?2 AND deleted_at IS NULL",
316 params![namespace, name],
317 )?;
318 Ok(affected == 1)
319}
320
321pub fn list(
332 conn: &Connection,
333 namespace: &str,
334 memory_type: Option<&str>,
335 limit: usize,
336 offset: usize,
337) -> Result<Vec<MemoryRow>, AppError> {
338 if let Some(mt) = memory_type {
339 let mut stmt = conn.prepare(
340 "SELECT id, namespace, name, type, description, body, body_hash,
341 session_id, source, metadata, created_at, updated_at
342 FROM memories WHERE namespace=?1 AND type=?2 AND deleted_at IS NULL
343 ORDER BY updated_at DESC LIMIT ?3 OFFSET ?4",
344 )?;
345 let rows = stmt
346 .query_map(params![namespace, mt, limit as i64, offset as i64], |r| {
347 Ok(MemoryRow {
348 id: r.get(0)?,
349 namespace: r.get(1)?,
350 name: r.get(2)?,
351 memory_type: r.get(3)?,
352 description: r.get(4)?,
353 body: r.get(5)?,
354 body_hash: r.get(6)?,
355 session_id: r.get(7)?,
356 source: r.get(8)?,
357 metadata: r.get(9)?,
358 created_at: r.get(10)?,
359 updated_at: r.get(11)?,
360 })
361 })?
362 .collect::<Result<Vec<_>, _>>()?;
363 Ok(rows)
364 } else {
365 let mut stmt = conn.prepare(
366 "SELECT id, namespace, name, type, description, body, body_hash,
367 session_id, source, metadata, created_at, updated_at
368 FROM memories WHERE namespace=?1 AND deleted_at IS NULL
369 ORDER BY updated_at DESC LIMIT ?2 OFFSET ?3",
370 )?;
371 let rows = stmt
372 .query_map(params![namespace, limit as i64, offset as i64], |r| {
373 Ok(MemoryRow {
374 id: r.get(0)?,
375 namespace: r.get(1)?,
376 name: r.get(2)?,
377 memory_type: r.get(3)?,
378 description: r.get(4)?,
379 body: r.get(5)?,
380 body_hash: r.get(6)?,
381 session_id: r.get(7)?,
382 source: r.get(8)?,
383 metadata: r.get(9)?,
384 created_at: r.get(10)?,
385 updated_at: r.get(11)?,
386 })
387 })?
388 .collect::<Result<Vec<_>, _>>()?;
389 Ok(rows)
390 }
391}
392
393pub fn knn_search(
409 conn: &Connection,
410 embedding: &[f32],
411 namespace: &str,
412 memory_type: Option<&str>,
413 k: usize,
414) -> Result<Vec<(i64, f32)>, AppError> {
415 let bytes = f32_to_bytes(embedding);
416 if let Some(mt) = memory_type {
417 let mut stmt = conn.prepare(
418 "SELECT memory_id, distance FROM vec_memories
419 WHERE embedding MATCH ?1 AND namespace = ?2 AND type = ?3
420 ORDER BY distance LIMIT ?4",
421 )?;
422 let rows = stmt
423 .query_map(params![bytes, namespace, mt, k as i64], |r| {
424 Ok((r.get::<_, i64>(0)?, r.get::<_, f32>(1)?))
425 })?
426 .collect::<Result<Vec<_>, _>>()?;
427 Ok(rows)
428 } else {
429 let mut stmt = conn.prepare(
430 "SELECT memory_id, distance FROM vec_memories
431 WHERE embedding MATCH ?1 AND namespace = ?2
432 ORDER BY distance LIMIT ?3",
433 )?;
434 let rows = stmt
435 .query_map(params![bytes, namespace, k as i64], |r| {
436 Ok((r.get::<_, i64>(0)?, r.get::<_, f32>(1)?))
437 })?
438 .collect::<Result<Vec<_>, _>>()?;
439 Ok(rows)
440 }
441}
442
443pub fn read_full(conn: &Connection, memory_id: i64) -> Result<Option<MemoryRow>, AppError> {
451 let mut stmt = conn.prepare_cached(
452 "SELECT id, namespace, name, type, description, body, body_hash,
453 session_id, source, metadata, created_at, updated_at
454 FROM memories WHERE id=?1 AND deleted_at IS NULL",
455 )?;
456 match stmt.query_row(params![memory_id], |r| {
457 Ok(MemoryRow {
458 id: r.get(0)?,
459 namespace: r.get(1)?,
460 name: r.get(2)?,
461 memory_type: r.get(3)?,
462 description: r.get(4)?,
463 body: r.get(5)?,
464 body_hash: r.get(6)?,
465 session_id: r.get(7)?,
466 source: r.get(8)?,
467 metadata: r.get(9)?,
468 created_at: r.get(10)?,
469 updated_at: r.get(11)?,
470 })
471 }) {
472 Ok(m) => Ok(Some(m)),
473 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
474 Err(e) => Err(AppError::Database(e)),
475 }
476}
477
478pub fn list_deleted_before(
487 conn: &Connection,
488 namespace: &str,
489 before_ts: i64,
490) -> Result<Vec<i64>, AppError> {
491 let mut stmt = conn.prepare_cached(
492 "SELECT id FROM memories WHERE namespace = ?1 AND deleted_at IS NOT NULL AND deleted_at < ?2",
493 )?;
494 let ids = stmt
495 .query_map(params![namespace, before_ts], |r| r.get::<_, i64>(0))?
496 .collect::<Result<Vec<_>, _>>()?;
497 Ok(ids)
498}
499
500pub fn fts_search(
509 conn: &Connection,
510 query: &str,
511 namespace: &str,
512 memory_type: Option<&str>,
513 limit: usize,
514) -> Result<Vec<MemoryRow>, AppError> {
515 let fts_query = format!("{query}*");
516 if let Some(mt) = memory_type {
517 let mut stmt = conn.prepare(
518 "SELECT m.id, m.namespace, m.name, m.type, m.description, m.body, m.body_hash,
519 m.session_id, m.source, m.metadata, m.created_at, m.updated_at
520 FROM fts_memories fts
521 JOIN memories m ON m.id = fts.rowid
522 WHERE fts_memories MATCH ?1 AND m.namespace = ?2 AND m.type = ?3 AND m.deleted_at IS NULL
523 ORDER BY rank LIMIT ?4",
524 )?;
525 let rows = stmt
526 .query_map(params![fts_query, namespace, mt, limit as i64], |r| {
527 Ok(MemoryRow {
528 id: r.get(0)?,
529 namespace: r.get(1)?,
530 name: r.get(2)?,
531 memory_type: r.get(3)?,
532 description: r.get(4)?,
533 body: r.get(5)?,
534 body_hash: r.get(6)?,
535 session_id: r.get(7)?,
536 source: r.get(8)?,
537 metadata: r.get(9)?,
538 created_at: r.get(10)?,
539 updated_at: r.get(11)?,
540 })
541 })?
542 .collect::<Result<Vec<_>, _>>()?;
543 Ok(rows)
544 } else {
545 let mut stmt = conn.prepare(
546 "SELECT m.id, m.namespace, m.name, m.type, m.description, m.body, m.body_hash,
547 m.session_id, m.source, m.metadata, m.created_at, m.updated_at
548 FROM fts_memories fts
549 JOIN memories m ON m.id = fts.rowid
550 WHERE fts_memories MATCH ?1 AND m.namespace = ?2 AND m.deleted_at IS NULL
551 ORDER BY rank LIMIT ?3",
552 )?;
553 let rows = stmt
554 .query_map(params![fts_query, namespace, limit as i64], |r| {
555 Ok(MemoryRow {
556 id: r.get(0)?,
557 namespace: r.get(1)?,
558 name: r.get(2)?,
559 memory_type: r.get(3)?,
560 description: r.get(4)?,
561 body: r.get(5)?,
562 body_hash: r.get(6)?,
563 session_id: r.get(7)?,
564 source: r.get(8)?,
565 metadata: r.get(9)?,
566 created_at: r.get(10)?,
567 updated_at: r.get(11)?,
568 })
569 })?
570 .collect::<Result<Vec<_>, _>>()?;
571 Ok(rows)
572 }
573}
574
575#[cfg(test)]
576mod testes {
577 use super::*;
578 use rusqlite::Connection;
579
580 type Resultado = Result<(), Box<dyn std::error::Error>>;
581
582 fn setup_conn() -> Result<Connection, Box<dyn std::error::Error>> {
583 crate::storage::connection::register_vec_extension();
584 let mut conn = Connection::open_in_memory()?;
585 conn.execute_batch(
586 "PRAGMA foreign_keys = ON;
587 PRAGMA temp_store = MEMORY;",
588 )?;
589 crate::migrations::runner().run(&mut conn)?;
590 Ok(conn)
591 }
592
593 fn nova_memoria(name: &str) -> NewMemory {
594 NewMemory {
595 namespace: "global".to_string(),
596 name: name.to_string(),
597 memory_type: "user".to_string(),
598 description: "descricao de teste".to_string(),
599 body: "corpo da memoria de teste".to_string(),
600 body_hash: format!("hash-{name}"),
601 session_id: None,
602 source: "agent".to_string(),
603 metadata: serde_json::json!({}),
604 }
605 }
606
607 #[test]
608 fn insert_e_find_by_name_retornam_id() -> Resultado {
609 let conn = setup_conn()?;
610 let m = nova_memoria("mem-alpha");
611 let id = insert(&conn, &m)?;
612 assert!(id > 0);
613
614 let found = find_by_name(&conn, "global", "mem-alpha")?;
615 assert!(found.is_some());
616 let (found_id, _, _) = found.ok_or("mem-alpha deveria existir")?;
617 assert_eq!(found_id, id);
618 Ok(())
619 }
620
621 #[test]
622 fn find_by_name_retorna_none_quando_nao_existe() -> Resultado {
623 let conn = setup_conn()?;
624 let result = find_by_name(&conn, "global", "inexistente")?;
625 assert!(result.is_none());
626 Ok(())
627 }
628
629 #[test]
630 fn find_by_hash_retorna_id_correto() -> Resultado {
631 let conn = setup_conn()?;
632 let m = nova_memoria("mem-hash");
633 let id = insert(&conn, &m)?;
634
635 let found = find_by_hash(&conn, "global", "hash-mem-hash")?;
636 assert_eq!(found, Some(id));
637 Ok(())
638 }
639
640 #[test]
641 fn find_by_hash_retorna_none_quando_hash_nao_existe() -> Resultado {
642 let conn = setup_conn()?;
643 let result = find_by_hash(&conn, "global", "hash-inexistente")?;
644 assert!(result.is_none());
645 Ok(())
646 }
647
648 #[test]
649 fn find_by_hash_ignora_namespace_diferente() -> Resultado {
650 let conn = setup_conn()?;
651 let m = nova_memoria("mem-ns");
652 insert(&conn, &m)?;
653
654 let result = find_by_hash(&conn, "outro-namespace", "hash-mem-ns")?;
655 assert!(result.is_none());
656 Ok(())
657 }
658
659 #[test]
660 fn read_by_name_retorna_memoria_completa() -> Resultado {
661 let conn = setup_conn()?;
662 let m = nova_memoria("mem-read");
663 let id = insert(&conn, &m)?;
664
665 let row = read_by_name(&conn, "global", "mem-read")?.ok_or("mem-read deveria existir")?;
666 assert_eq!(row.id, id);
667 assert_eq!(row.name, "mem-read");
668 assert_eq!(row.memory_type, "user");
669 assert_eq!(row.body, "corpo da memoria de teste");
670 assert_eq!(row.namespace, "global");
671 Ok(())
672 }
673
674 #[test]
675 fn read_by_name_retorna_none_para_ausente() -> Resultado {
676 let conn = setup_conn()?;
677 let result = read_by_name(&conn, "global", "nao-existe")?;
678 assert!(result.is_none());
679 Ok(())
680 }
681
682 #[test]
683 fn read_full_por_id_retorna_memoria() -> Resultado {
684 let conn = setup_conn()?;
685 let m = nova_memoria("mem-full");
686 let id = insert(&conn, &m)?;
687
688 let row = read_full(&conn, id)?.ok_or("mem-full deveria existir")?;
689 assert_eq!(row.id, id);
690 assert_eq!(row.name, "mem-full");
691 Ok(())
692 }
693
694 #[test]
695 fn read_full_retorna_none_para_id_inexistente() -> Resultado {
696 let conn = setup_conn()?;
697 let result = read_full(&conn, 9999)?;
698 assert!(result.is_none());
699 Ok(())
700 }
701
702 #[test]
703 fn update_sem_otimismo_modifica_campos() -> Resultado {
704 let conn = setup_conn()?;
705 let m = nova_memoria("mem-upd");
706 let id = insert(&conn, &m)?;
707
708 let mut m2 = nova_memoria("mem-upd");
709 m2.body = "corpo atualizado".to_string();
710 m2.body_hash = "hash-novo".to_string();
711 let ok = update(&conn, id, &m2, None)?;
712 assert!(ok);
713
714 let row = read_full(&conn, id)?.ok_or("mem-upd deveria existir")?;
715 assert_eq!(row.body, "corpo atualizado");
716 assert_eq!(row.body_hash, "hash-novo");
717 Ok(())
718 }
719
720 #[test]
721 fn update_com_expected_updated_at_correto_tem_sucesso() -> Resultado {
722 let conn = setup_conn()?;
723 let m = nova_memoria("mem-opt");
724 let id = insert(&conn, &m)?;
725
726 let (_, updated_at, _) =
727 find_by_name(&conn, "global", "mem-opt")?.ok_or("mem-opt deveria existir")?;
728
729 let mut m2 = nova_memoria("mem-opt");
730 m2.body = "corpo otimista".to_string();
731 m2.body_hash = "hash-otimista".to_string();
732 let ok = update(&conn, id, &m2, Some(updated_at))?;
733 assert!(ok);
734
735 let row = read_full(&conn, id)?.ok_or("mem-opt deveria existir após update")?;
736 assert_eq!(row.body, "corpo otimista");
737 Ok(())
738 }
739
740 #[test]
741 fn update_com_expected_updated_at_errado_retorna_false() -> Resultado {
742 let conn = setup_conn()?;
743 let m = nova_memoria("mem-conflict");
744 let id = insert(&conn, &m)?;
745
746 let mut m2 = nova_memoria("mem-conflict");
747 m2.body = "nao deve aparecer".to_string();
748 m2.body_hash = "hash-x".to_string();
749 let ok = update(&conn, id, &m2, Some(0))?;
750 assert!(!ok);
751
752 let row = read_full(&conn, id)?.ok_or("mem-conflict deveria existir")?;
753 assert_eq!(row.body, "corpo da memoria de teste");
754 Ok(())
755 }
756
757 #[test]
758 fn update_id_inexistente_retorna_false() -> Resultado {
759 let conn = setup_conn()?;
760 let m = nova_memoria("fantasma");
761 let ok = update(&conn, 9999, &m, None)?;
762 assert!(!ok);
763 Ok(())
764 }
765
766 #[test]
767 fn soft_delete_marca_deleted_at() -> Resultado {
768 let conn = setup_conn()?;
769 let m = nova_memoria("mem-del");
770 insert(&conn, &m)?;
771
772 let ok = soft_delete(&conn, "global", "mem-del")?;
773 assert!(ok);
774
775 let result = find_by_name(&conn, "global", "mem-del")?;
776 assert!(result.is_none());
777
778 let result_read = read_by_name(&conn, "global", "mem-del")?;
779 assert!(result_read.is_none());
780 Ok(())
781 }
782
783 #[test]
784 fn soft_delete_retorna_false_quando_nao_existe() -> Resultado {
785 let conn = setup_conn()?;
786 let ok = soft_delete(&conn, "global", "nao-existe")?;
787 assert!(!ok);
788 Ok(())
789 }
790
791 #[test]
792 fn soft_delete_duplo_retorna_false_na_segunda_vez() -> Resultado {
793 let conn = setup_conn()?;
794 let m = nova_memoria("mem-del2");
795 insert(&conn, &m)?;
796
797 soft_delete(&conn, "global", "mem-del2")?;
798 let ok = soft_delete(&conn, "global", "mem-del2")?;
799 assert!(!ok);
800 Ok(())
801 }
802
803 #[test]
804 fn list_retorna_memorias_do_namespace() -> Resultado {
805 let conn = setup_conn()?;
806 insert(&conn, &nova_memoria("mem-list-a"))?;
807 insert(&conn, &nova_memoria("mem-list-b"))?;
808
809 let rows = list(&conn, "global", None, 10, 0)?;
810 assert!(rows.len() >= 2);
811 let nomes: Vec<_> = rows.iter().map(|r| r.name.as_str()).collect();
812 assert!(nomes.contains(&"mem-list-a"));
813 assert!(nomes.contains(&"mem-list-b"));
814 Ok(())
815 }
816
817 #[test]
818 fn list_com_filtro_de_tipo_retorna_apenas_tipo_correto() -> Resultado {
819 let conn = setup_conn()?;
820 insert(&conn, &nova_memoria("mem-user"))?;
821
822 let mut m2 = nova_memoria("mem-feedback");
823 m2.memory_type = "feedback".to_string();
824 insert(&conn, &m2)?;
825
826 let rows_user = list(&conn, "global", Some("user"), 10, 0)?;
827 assert!(rows_user.iter().all(|r| r.memory_type == "user"));
828
829 let rows_fb = list(&conn, "global", Some("feedback"), 10, 0)?;
830 assert!(rows_fb.iter().all(|r| r.memory_type == "feedback"));
831 Ok(())
832 }
833
834 #[test]
835 fn list_exclui_soft_deleted() -> Resultado {
836 let conn = setup_conn()?;
837 let m = nova_memoria("mem-excluida");
838 insert(&conn, &m)?;
839 soft_delete(&conn, "global", "mem-excluida")?;
840
841 let rows = list(&conn, "global", None, 10, 0)?;
842 assert!(rows.iter().all(|r| r.name != "mem-excluida"));
843 Ok(())
844 }
845
846 #[test]
847 fn list_paginacao_funciona() -> Resultado {
848 let conn = setup_conn()?;
849 for i in 0..5 {
850 insert(&conn, &nova_memoria(&format!("mem-pag-{i}")))?;
851 }
852
853 let pagina1 = list(&conn, "global", None, 2, 0)?;
854 let pagina2 = list(&conn, "global", None, 2, 2)?;
855 assert!(pagina1.len() <= 2);
856 assert!(pagina2.len() <= 2);
857 if !pagina1.is_empty() && !pagina2.is_empty() {
858 assert_ne!(pagina1[0].id, pagina2[0].id);
859 }
860 Ok(())
861 }
862
863 #[test]
864 fn upsert_vec_e_delete_vec_funcionam() -> Resultado {
865 let conn = setup_conn()?;
866 let m = nova_memoria("mem-vec");
867 let id = insert(&conn, &m)?;
868
869 let embedding: Vec<f32> = vec![0.1; 384];
870 upsert_vec(
871 &conn, id, "global", "user", &embedding, "mem-vec", "snippet",
872 )?;
873
874 let count: i64 = conn.query_row(
875 "SELECT COUNT(*) FROM vec_memories WHERE memory_id = ?1",
876 params![id],
877 |r| r.get(0),
878 )?;
879 assert_eq!(count, 1);
880
881 delete_vec(&conn, id)?;
882
883 let count_after: i64 = conn.query_row(
884 "SELECT COUNT(*) FROM vec_memories WHERE memory_id = ?1",
885 params![id],
886 |r| r.get(0),
887 )?;
888 assert_eq!(count_after, 0);
889 Ok(())
890 }
891
892 #[test]
893 fn upsert_vec_substitui_vetor_existente() -> Resultado {
894 let conn = setup_conn()?;
895 let m = nova_memoria("mem-vec-upsert");
896 let id = insert(&conn, &m)?;
897
898 let emb1: Vec<f32> = vec![0.1; 384];
899 upsert_vec(&conn, id, "global", "user", &emb1, "mem-vec-upsert", "s1")?;
900
901 let emb2: Vec<f32> = vec![0.9; 384];
902 upsert_vec(&conn, id, "global", "user", &emb2, "mem-vec-upsert", "s2")?;
903
904 let count: i64 = conn.query_row(
905 "SELECT COUNT(*) FROM vec_memories WHERE memory_id = ?1",
906 params![id],
907 |r| r.get(0),
908 )?;
909 assert_eq!(count, 1);
910 Ok(())
911 }
912
913 #[test]
914 fn knn_search_retorna_resultados_por_distancia() -> Resultado {
915 let conn = setup_conn()?;
916
917 let ma = nova_memoria("mem-knn-a");
919 let id_a = insert(&conn, &ma)?;
920 let emb_a: Vec<f32> = vec![1.0; 384];
921 upsert_vec(&conn, id_a, "global", "user", &emb_a, "mem-knn-a", "s")?;
922
923 let mb = nova_memoria("mem-knn-b");
925 let id_b = insert(&conn, &mb)?;
926 let emb_b: Vec<f32> = vec![-1.0; 384];
927 upsert_vec(&conn, id_b, "global", "user", &emb_b, "mem-knn-b", "s")?;
928
929 let query: Vec<f32> = vec![1.0; 384];
930 let results = knn_search(&conn, &query, "global", None, 2)?;
931 assert!(!results.is_empty());
932 assert_eq!(results[0].0, id_a);
933 Ok(())
934 }
935
936 #[test]
937 fn knn_search_com_filtro_de_tipo_restringe_resultado() -> Resultado {
938 let conn = setup_conn()?;
939
940 let ma = nova_memoria("mem-knn-tipo-user");
941 let id_a = insert(&conn, &ma)?;
942 let emb: Vec<f32> = vec![1.0; 384];
943 upsert_vec(
944 &conn,
945 id_a,
946 "global",
947 "user",
948 &emb,
949 "mem-knn-tipo-user",
950 "s",
951 )?;
952
953 let mut mb = nova_memoria("mem-knn-tipo-fb");
954 mb.memory_type = "feedback".to_string();
955 let id_b = insert(&conn, &mb)?;
956 upsert_vec(
957 &conn,
958 id_b,
959 "global",
960 "feedback",
961 &emb,
962 "mem-knn-tipo-fb",
963 "s",
964 )?;
965
966 let query: Vec<f32> = vec![1.0; 384];
967 let results_user = knn_search(&conn, &query, "global", Some("user"), 5)?;
968 assert!(results_user.iter().all(|(id, _)| *id == id_a));
969
970 let results_fb = knn_search(&conn, &query, "global", Some("feedback"), 5)?;
971 assert!(results_fb.iter().all(|(id, _)| *id == id_b));
972 Ok(())
973 }
974
975 #[test]
976 fn fts_search_encontra_por_prefixo_no_body() -> Resultado {
977 let conn = setup_conn()?;
978 let mut m = nova_memoria("mem-fts");
979 m.body = "linguagem de programacao rust".to_string();
980 insert(&conn, &m)?;
981
982 conn.execute_batch(
983 "INSERT INTO fts_memories(rowid, name, description, body)
984 SELECT id, name, description, body FROM memories WHERE deleted_at IS NULL",
985 )?;
986
987 let rows = fts_search(&conn, "programacao", "global", None, 10)?;
988 assert!(!rows.is_empty());
989 assert!(rows.iter().any(|r| r.name == "mem-fts"));
990 Ok(())
991 }
992
993 #[test]
994 fn fts_search_com_filtro_de_tipo() -> Resultado {
995 let conn = setup_conn()?;
996 let mut m = nova_memoria("mem-fts-tipo");
997 m.body = "linguagem especial para filtro".to_string();
998 insert(&conn, &m)?;
999
1000 let mut m2 = nova_memoria("mem-fts-feedback");
1001 m2.memory_type = "feedback".to_string();
1002 m2.body = "linguagem especial para filtro".to_string();
1003 insert(&conn, &m2)?;
1004
1005 conn.execute_batch(
1006 "INSERT INTO fts_memories(rowid, name, description, body)
1007 SELECT id, name, description, body FROM memories WHERE deleted_at IS NULL",
1008 )?;
1009
1010 let rows_user = fts_search(&conn, "especial", "global", Some("user"), 10)?;
1011 assert!(rows_user.iter().all(|r| r.memory_type == "user"));
1012
1013 let rows_fb = fts_search(&conn, "especial", "global", Some("feedback"), 10)?;
1014 assert!(rows_fb.iter().all(|r| r.memory_type == "feedback"));
1015 Ok(())
1016 }
1017
1018 #[test]
1019 fn fts_search_nao_retorna_deletados() -> Resultado {
1020 let conn = setup_conn()?;
1021 let mut m = nova_memoria("mem-fts-del");
1022 m.body = "conteudo deletado fts".to_string();
1023 insert(&conn, &m)?;
1024
1025 conn.execute_batch(
1026 "INSERT INTO fts_memories(rowid, name, description, body)
1027 SELECT id, name, description, body FROM memories WHERE deleted_at IS NULL",
1028 )?;
1029
1030 soft_delete(&conn, "global", "mem-fts-del")?;
1031
1032 let rows = fts_search(&conn, "deletado", "global", None, 10)?;
1033 assert!(rows.iter().all(|r| r.name != "mem-fts-del"));
1034 Ok(())
1035 }
1036
1037 #[test]
1038 fn list_deleted_before_retorna_ids_corretos() -> Resultado {
1039 let conn = setup_conn()?;
1040 let m = nova_memoria("mem-purge");
1041 insert(&conn, &m)?;
1042 soft_delete(&conn, "global", "mem-purge")?;
1043
1044 let ids = list_deleted_before(&conn, "global", i64::MAX)?;
1045 assert!(!ids.is_empty());
1046
1047 let ids_antes = list_deleted_before(&conn, "global", 0)?;
1048 assert!(ids_antes.is_empty());
1049 Ok(())
1050 }
1051
1052 #[test]
1053 fn find_by_name_retorna_max_version_correto() -> Resultado {
1054 let conn = setup_conn()?;
1055 let m = nova_memoria("mem-ver");
1056 let id = insert(&conn, &m)?;
1057
1058 let (_, _, v0) =
1059 find_by_name(&conn, "global", "mem-ver")?.ok_or("mem-ver deveria existir")?;
1060 assert_eq!(v0, 0);
1061
1062 conn.execute(
1063 "INSERT INTO memory_versions (memory_id, version, name, type, description, body, metadata, change_reason)
1064 VALUES (?1, 1, 'mem-ver', 'user', 'desc', 'body', '{}', 'create')",
1065 params![id],
1066 )?;
1067
1068 let (_, _, v1) = find_by_name(&conn, "global", "mem-ver")?
1069 .ok_or("mem-ver deveria existir após insert")?;
1070 assert_eq!(v1, 1);
1071 Ok(())
1072 }
1073
1074 #[test]
1075 fn insert_com_metadata_json() -> Resultado {
1076 let conn = setup_conn()?;
1077 let mut m = nova_memoria("mem-meta");
1078 m.metadata = serde_json::json!({"chave": "valor", "numero": 42});
1079 let id = insert(&conn, &m)?;
1080
1081 let row = read_full(&conn, id)?.ok_or("mem-meta deveria existir")?;
1082 let meta: serde_json::Value = serde_json::from_str(&row.metadata)?;
1083 assert_eq!(meta["chave"], "valor");
1084 assert_eq!(meta["numero"], 42);
1085 Ok(())
1086 }
1087
1088 #[test]
1089 fn insert_com_session_id() -> Resultado {
1090 let conn = setup_conn()?;
1091 let mut m = nova_memoria("mem-session");
1092 m.session_id = Some("sessao-xyz".to_string());
1093 let id = insert(&conn, &m)?;
1094
1095 let row = read_full(&conn, id)?.ok_or("mem-session deveria existir")?;
1096 assert_eq!(row.session_id, Some("sessao-xyz".to_string()));
1097 Ok(())
1098 }
1099
1100 #[test]
1101 fn delete_vec_em_id_inexistente_nao_falha() -> Resultado {
1102 let conn = setup_conn()?;
1103 let result = delete_vec(&conn, 99999);
1104 assert!(result.is_ok());
1105 Ok(())
1106 }
1107}