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 rusqlite::{params, Connection};
10use serde::{Deserialize, Serialize};
11
12/// Input payload for inserting or updating a memory.
13///
14/// `body_hash` must be the BLAKE3 digest of `body`. The `metadata` field is
15/// stored as a TEXT column containing JSON.
16#[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/// Fully materialized row from the `memories` table.
30///
31/// Returned by [`read_by_name`], [`read_full`], [`list`] and [`fts_search`].
32/// The `metadata` field is kept as a JSON string to avoid double parsing.
33#[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
49/// Finds a live memory by `(namespace, name)` and returns key metadata.
50///
51/// # Arguments
52///
53/// - `conn` — open SQLite connection configured with the project pragmas.
54/// - `namespace` — resolved namespace for the lookup.
55/// - `name` — kebab-case memory name.
56///
57/// # Returns
58///
59/// `Ok(Some((id, updated_at, max_version)))` when the memory exists and is
60/// not soft-deleted, `Ok(None)` otherwise.
61///
62/// # Errors
63///
64/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
65pub 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
91/// Looks up a live memory by exact `body_hash` within a namespace.
92///
93/// Used during `remember` to short-circuit semantic duplicates before
94/// spending an embedding call.
95///
96/// # Returns
97///
98/// `Ok(Some(id))` when a live memory with the same hash exists,
99/// `Ok(None)` otherwise.
100///
101/// # Errors
102///
103/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
104pub 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
119/// Inserts a new row into the `memories` table.
120///
121/// # Arguments
122///
123/// - `conn` — active SQLite connection, typically inside a transaction.
124/// - `m` — validated payload including `body_hash` and serialized metadata.
125///
126/// # Returns
127///
128/// The `rowid` assigned to the newly inserted memory.
129///
130/// # Errors
131///
132/// Returns `Err(AppError::Database)` on insertion failure and
133/// `Err(AppError::Json)` if metadata serialization fails.
134pub 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
147/// Updates an existing memory optionally guarded by optimistic concurrency.
148///
149/// When `expected_updated_at` is `Some(ts)` the row is only updated if its
150/// current `updated_at` equals `ts`. This protects concurrent `edit` calls
151/// from silently clobbering each other.
152///
153/// # Returns
154///
155/// `Ok(true)` when exactly one row was updated, `Ok(false)` when the
156/// optimistic check failed or the memory does not exist.
157///
158/// # Errors
159///
160/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
161pub 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
204/// Replaces the vector row for a memory in `vec_memories`.
205///
206/// `sqlite-vec` virtual tables do not implement `INSERT OR REPLACE`, so the
207/// existing row is deleted first and a fresh vector is inserted. Callers
208/// must pass an `embedding` with length [`crate::constants::EMBEDDING_DIM`].
209///
210/// # Errors
211///
212/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
213pub 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    // sqlite-vec virtual tables do not support INSERT OR REPLACE semantics.
223    // Must delete the existing row first, then insert.
224    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
243/// Deletes the vector row for `memory_id` from `vec_memories`.
244///
245/// Called during `forget` and `purge` to keep the vector table consistent
246/// with the logical state of `memories`.
247///
248/// # Errors
249///
250/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
251pub 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
259/// Fetches a live memory by `(namespace, name)` and returns all columns.
260///
261/// # Returns
262///
263/// `Ok(Some(row))` when found, `Ok(None)` when missing or soft-deleted.
264///
265/// # Errors
266///
267/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
268pub 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
300/// Soft-deletes a memory by setting `deleted_at = unixepoch()`.
301///
302/// Versions and chunks are preserved so `restore` can undo the operation
303/// until a subsequent `purge` reclaims the storage permanently.
304///
305/// # Returns
306///
307/// `Ok(true)` when a live memory was soft-deleted, `Ok(false)` when no
308/// matching live row existed.
309///
310/// # Errors
311///
312/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
313pub 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
321/// Lists live memories in a namespace ordered by `updated_at` descending.
322///
323/// # Arguments
324///
325/// - `memory_type` — optional filter on the `type` column.
326/// - `limit` / `offset` — standard pagination controls in rows.
327///
328/// # Errors
329///
330/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
331pub 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
393/// Runs a KNN search over `vec_memories` restricted to a namespace.
394///
395/// # Arguments
396///
397/// - `embedding` — query vector of length [`crate::constants::EMBEDDING_DIM`].
398/// - `memory_type` — optional filter on the `type` column.
399/// - `k` — maximum number of hits to return.
400///
401/// # Returns
402///
403/// A vector of `(memory_id, distance)` pairs sorted by ascending distance.
404///
405/// # Errors
406///
407/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
408pub 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
443/// Fetches a live memory by primary key and returns all columns.
444///
445/// Mirrors [`read_by_name`] but keyed on `rowid` for use after a KNN search.
446///
447/// # Errors
448///
449/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
450pub 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
478/// Fetches all memory_ids in a namespace that are soft-deleted and whose
479/// `deleted_at` is older than `before_ts` (unix epoch seconds).
480///
481/// Used by `purge` to collect stale rows for permanent deletion.
482///
483/// # Errors
484///
485/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
486pub 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
500/// Executes a prefix-matching FTS5 search against `fts_memories`.
501///
502/// The supplied `query` is suffixed with `*` to enable prefix matching, then
503/// joined back to `memories` to materialize full rows filtered by namespace.
504///
505/// # Errors
506///
507/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
508pub 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        // emb_a: predominantemente positivo — cosseno alto com query [1.0; 384]
918        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        // emb_b: predominantemente negativo — cosseno baixo com query [1.0; 384]
924        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}