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