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`, optionally restricted to namespaces.
400///
401/// # Arguments
402///
403/// - `embedding` — query vector of length [`crate::constants::EMBEDDING_DIM`].
404/// - `namespaces` — namespaces to search. Empty slice means "all namespaces".
405/// - `memory_type` — optional filter on the `type` column.
406/// - `k` — maximum number of hits to return.
407///
408/// # Returns
409///
410/// A vector of `(memory_id, distance)` pairs sorted by ascending distance.
411///
412/// # Errors
413///
414/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
415pub 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            // No namespace filter — search all namespaces.
427            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            // Fast single-namespace path (preserved from previous implementation).
455            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            // Multiple explicit namespaces: build IN clause with positional placeholders.
484            // rusqlite does not support array binding, so we generate "?,?,..." manually.
485            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                // Params: [bytes, mt, ns0, ns1, ..., k]
497                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                // Params: [bytes, ns0, ns1, ..., k]
519                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
537/// Fetches a live memory by primary key and returns all columns.
538///
539/// Mirrors [`read_by_name`] but keyed on `rowid` for use after a KNN search.
540///
541/// # Errors
542///
543/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
544pub 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
572/// Fetches all memory_ids in a namespace that are soft-deleted and whose
573/// `deleted_at` is older than `before_ts` (unix epoch seconds).
574///
575/// Used by `purge` to collect stale rows for permanent deletion.
576///
577/// # Errors
578///
579/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
580pub 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
594/// Executes a prefix-matching FTS5 search against `fts_memories`.
595///
596/// The supplied `query` is suffixed with `*` to enable prefix matching, then
597/// joined back to `memories` to materialize full rows filtered by namespace.
598///
599/// # Errors
600///
601/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
602pub 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        // emb_a: predominantemente positivo — cosseno alto com query [1.0; 384]
1012        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        // emb_b: predominantemente negativo — cosseno baixo com query [1.0; 384]
1018        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}