Skip to main content

sqlite_graphrag/storage/
memories.rs

1//! Persistence layer for the `memories` table and its vector companion.
2//!
3//! Functions here encapsulate every SQL statement touching `memories`,
4//! `vec_memories` and the FTS5 `fts_memories` shadow table. Callers receive
5//! typed [`MemoryRow`] or [`NewMemory`] values and never build SQL strings.
6
7use 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/// Input payload for inserting or updating a memory.
14///
15/// `body_hash` must be the BLAKE3 digest of `body`. The `metadata` field is
16/// stored as a TEXT column containing JSON.
17#[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/// Fully materialized row from the `memories` table.
31///
32/// Returned by [`read_by_name`], [`read_full`], [`list`] and [`fts_search`].
33/// The `metadata` field is kept as a JSON string to avoid double parsing.
34#[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
50/// Finds a live memory by `(namespace, name)` and returns key metadata.
51///
52/// # Arguments
53///
54/// - `conn` — open SQLite connection configured with the project pragmas.
55/// - `namespace` — resolved namespace for the lookup.
56/// - `name` — kebab-case memory name.
57///
58/// # Returns
59///
60/// `Ok(Some((id, updated_at, max_version)))` when the memory exists and is
61/// not soft-deleted, `Ok(None)` otherwise.
62///
63/// # Errors
64///
65/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
66pub 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
92/// Looks up a live memory by exact `body_hash` within a namespace.
93///
94/// Used during `remember` to short-circuit semantic duplicates before
95/// spending an embedding call.
96///
97/// # Returns
98///
99/// `Ok(Some(id))` when a live memory with the same hash exists,
100/// `Ok(None)` otherwise.
101///
102/// # Errors
103///
104/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
105pub 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
120/// Inserts a new row into the `memories` table.
121///
122/// # Arguments
123///
124/// - `conn` — active SQLite connection, typically inside a transaction.
125/// - `m` — validated payload including `body_hash` and serialized metadata.
126///
127/// # Returns
128///
129/// The `rowid` assigned to the newly inserted memory.
130///
131/// # Errors
132///
133/// Returns `Err(AppError::Database)` on insertion failure and
134/// `Err(AppError::Json)` if metadata serialization fails.
135pub 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
148/// Updates an existing memory optionally guarded by optimistic concurrency.
149///
150/// When `expected_updated_at` is `Some(ts)` the row is only updated if its
151/// current `updated_at` equals `ts`. This protects concurrent `edit` calls
152/// from silently clobbering each other.
153///
154/// # Returns
155///
156/// `Ok(true)` when exactly one row was updated, `Ok(false)` when the
157/// optimistic check failed or the memory does not exist.
158///
159/// # Errors
160///
161/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
162pub 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
205/// Replaces the vector row for a memory in `vec_memories`.
206///
207/// `sqlite-vec` virtual tables do not implement `INSERT OR REPLACE`, so the
208/// existing row is deleted first and a fresh vector is inserted. Callers
209/// must pass an `embedding` with length [`crate::constants::EMBEDDING_DIM`].
210///
211/// # Errors
212///
213/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
214pub 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    // sqlite-vec virtual tables do not support INSERT OR REPLACE semantics.
224    // Must delete the existing row first, then insert.  Both statements are
225    // wrapped in `with_busy_retry` because WAL-mode concurrent writers can
226    // cause SQLITE_BUSY on vec0 virtual table writes.
227    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
249/// Deletes the vector row for `memory_id` from `vec_memories`.
250///
251/// Called during `forget` and `purge` to keep the vector table consistent
252/// with the logical state of `memories`.
253///
254/// # Errors
255///
256/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
257pub 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
265/// Fetches a live memory by `(namespace, name)` and returns all columns.
266///
267/// # Returns
268///
269/// `Ok(Some(row))` when found, `Ok(None)` when missing or soft-deleted.
270///
271/// # Errors
272///
273/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
274pub 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
306/// Soft-deletes a memory by setting `deleted_at = unixepoch()`.
307///
308/// Versions and chunks are preserved so `restore` can undo the operation
309/// until a subsequent `purge` reclaims the storage permanently.
310///
311/// # Returns
312///
313/// `Ok(true)` when a live memory was soft-deleted, `Ok(false)` when no
314/// matching live row existed.
315///
316/// # Errors
317///
318/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
319pub 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
327/// Lists live memories in a namespace ordered by `updated_at` descending.
328///
329/// # Arguments
330///
331/// - `memory_type` — optional filter on the `type` column.
332/// - `limit` / `offset` — standard pagination controls in rows.
333///
334/// # Errors
335///
336/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
337pub 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
399/// Runs a KNN search over `vec_memories` restricted to a namespace.
400///
401/// # Arguments
402///
403/// - `embedding` — query vector of length [`crate::constants::EMBEDDING_DIM`].
404/// - `memory_type` — optional filter on the `type` column.
405/// - `k` — maximum number of hits to return.
406///
407/// # Returns
408///
409/// A vector of `(memory_id, distance)` pairs sorted by ascending distance.
410///
411/// # Errors
412///
413/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
414pub 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
449/// Fetches a live memory by primary key and returns all columns.
450///
451/// Mirrors [`read_by_name`] but keyed on `rowid` for use after a KNN search.
452///
453/// # Errors
454///
455/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
456pub 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
484/// Fetches all memory_ids in a namespace that are soft-deleted and whose
485/// `deleted_at` is older than `before_ts` (unix epoch seconds).
486///
487/// Used by `purge` to collect stale rows for permanent deletion.
488///
489/// # Errors
490///
491/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
492pub 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
506/// Executes a prefix-matching FTS5 search against `fts_memories`.
507///
508/// The supplied `query` is suffixed with `*` to enable prefix matching, then
509/// joined back to `memories` to materialize full rows filtered by namespace.
510///
511/// # Errors
512///
513/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
514pub 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        // emb_a: predominantemente positivo — cosseno alto com query [1.0; 384]
924        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        // emb_b: predominantemente negativo — cosseno baixo com query [1.0; 384]
930        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}