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    include_deleted: bool,
344) -> Result<Vec<MemoryRow>, AppError> {
345    let deleted_clause = if include_deleted {
346        ""
347    } else {
348        " AND deleted_at IS NULL"
349    };
350    if let Some(mt) = memory_type {
351        let sql = format!(
352            "SELECT id, namespace, name, type, description, body, body_hash,
353                    session_id, source, metadata, created_at, updated_at
354             FROM memories WHERE namespace=?1 AND type=?2{deleted_clause}
355             ORDER BY updated_at DESC LIMIT ?3 OFFSET ?4"
356        );
357        let mut stmt = conn.prepare(&sql)?;
358        let rows = stmt
359            .query_map(params![namespace, mt, limit as i64, offset as i64], |r| {
360                Ok(MemoryRow {
361                    id: r.get(0)?,
362                    namespace: r.get(1)?,
363                    name: r.get(2)?,
364                    memory_type: r.get(3)?,
365                    description: r.get(4)?,
366                    body: r.get(5)?,
367                    body_hash: r.get(6)?,
368                    session_id: r.get(7)?,
369                    source: r.get(8)?,
370                    metadata: r.get(9)?,
371                    created_at: r.get(10)?,
372                    updated_at: r.get(11)?,
373                })
374            })?
375            .collect::<Result<Vec<_>, _>>()?;
376        Ok(rows)
377    } else {
378        let sql = format!(
379            "SELECT id, namespace, name, type, description, body, body_hash,
380                    session_id, source, metadata, created_at, updated_at
381             FROM memories WHERE namespace=?1{deleted_clause}
382             ORDER BY updated_at DESC LIMIT ?2 OFFSET ?3"
383        );
384        let mut stmt = conn.prepare(&sql)?;
385        let rows = stmt
386            .query_map(params![namespace, limit as i64, offset as i64], |r| {
387                Ok(MemoryRow {
388                    id: r.get(0)?,
389                    namespace: r.get(1)?,
390                    name: r.get(2)?,
391                    memory_type: r.get(3)?,
392                    description: r.get(4)?,
393                    body: r.get(5)?,
394                    body_hash: r.get(6)?,
395                    session_id: r.get(7)?,
396                    source: r.get(8)?,
397                    metadata: r.get(9)?,
398                    created_at: r.get(10)?,
399                    updated_at: r.get(11)?,
400                })
401            })?
402            .collect::<Result<Vec<_>, _>>()?;
403        Ok(rows)
404    }
405}
406
407/// Runs a KNN search over `vec_memories`, optionally restricted to namespaces.
408///
409/// # Arguments
410///
411/// - `embedding` — query vector of length [`crate::constants::EMBEDDING_DIM`].
412/// - `namespaces` — namespaces to search. Empty slice means "all namespaces".
413/// - `memory_type` — optional filter on the `type` column.
414/// - `k` — maximum number of hits to return.
415///
416/// # Returns
417///
418/// A vector of `(memory_id, distance)` pairs sorted by ascending distance.
419///
420/// # Errors
421///
422/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
423pub fn knn_search(
424    conn: &Connection,
425    embedding: &[f32],
426    namespaces: &[String],
427    memory_type: Option<&str>,
428    k: usize,
429) -> Result<Vec<(i64, f32)>, AppError> {
430    let bytes = f32_to_bytes(embedding);
431
432    match namespaces.len() {
433        0 => {
434            // No namespace filter — search all namespaces.
435            if let Some(mt) = memory_type {
436                let mut stmt = conn.prepare(
437                    "SELECT memory_id, distance FROM vec_memories \
438                     WHERE embedding MATCH ?1 AND type = ?2 \
439                     ORDER BY distance LIMIT ?3",
440                )?;
441                let rows = stmt
442                    .query_map(params![bytes, mt, k as i64], |r| {
443                        Ok((r.get::<_, i64>(0)?, r.get::<_, f32>(1)?))
444                    })?
445                    .collect::<Result<Vec<_>, _>>()?;
446                Ok(rows)
447            } else {
448                let mut stmt = conn.prepare(
449                    "SELECT memory_id, distance FROM vec_memories \
450                     WHERE embedding MATCH ?1 \
451                     ORDER BY distance LIMIT ?2",
452                )?;
453                let rows = stmt
454                    .query_map(params![bytes, k as i64], |r| {
455                        Ok((r.get::<_, i64>(0)?, r.get::<_, f32>(1)?))
456                    })?
457                    .collect::<Result<Vec<_>, _>>()?;
458                Ok(rows)
459            }
460        }
461        1 => {
462            // Fast single-namespace path (preserved from previous implementation).
463            let ns = &namespaces[0];
464            if let Some(mt) = memory_type {
465                let mut stmt = conn.prepare(
466                    "SELECT memory_id, distance FROM vec_memories \
467                     WHERE embedding MATCH ?1 AND namespace = ?2 AND type = ?3 \
468                     ORDER BY distance LIMIT ?4",
469                )?;
470                let rows = stmt
471                    .query_map(params![bytes, ns, mt, k as i64], |r| {
472                        Ok((r.get::<_, i64>(0)?, r.get::<_, f32>(1)?))
473                    })?
474                    .collect::<Result<Vec<_>, _>>()?;
475                Ok(rows)
476            } else {
477                let mut stmt = conn.prepare(
478                    "SELECT memory_id, distance FROM vec_memories \
479                     WHERE embedding MATCH ?1 AND namespace = ?2 \
480                     ORDER BY distance LIMIT ?3",
481                )?;
482                let rows = stmt
483                    .query_map(params![bytes, ns, k as i64], |r| {
484                        Ok((r.get::<_, i64>(0)?, r.get::<_, f32>(1)?))
485                    })?
486                    .collect::<Result<Vec<_>, _>>()?;
487                Ok(rows)
488            }
489        }
490        _ => {
491            // Multiple explicit namespaces: build IN clause with positional placeholders.
492            // rusqlite does not support array binding, so we generate "?,?,..." manually.
493            let placeholders = (0..namespaces.len())
494                .map(|_| "?")
495                .collect::<Vec<_>>()
496                .join(",");
497            if let Some(mt) = memory_type {
498                let query = format!(
499                    "SELECT memory_id, distance FROM vec_memories \
500                     WHERE embedding MATCH ? AND type = ? AND namespace IN ({placeholders}) \
501                     ORDER BY distance LIMIT ?"
502                );
503                let mut stmt = conn.prepare(&query)?;
504                // Params: [bytes, mt, ns0, ns1, ..., k]
505                let mut raw_params: Vec<Box<dyn rusqlite::ToSql>> =
506                    vec![Box::new(bytes), Box::new(mt.to_string())];
507                for ns in namespaces {
508                    raw_params.push(Box::new(ns.clone()));
509                }
510                raw_params.push(Box::new(k as i64));
511                let param_refs: Vec<&dyn rusqlite::ToSql> =
512                    raw_params.iter().map(|b| b.as_ref()).collect();
513                let rows = stmt
514                    .query_map(param_refs.as_slice(), |r| {
515                        Ok((r.get::<_, i64>(0)?, r.get::<_, f32>(1)?))
516                    })?
517                    .collect::<Result<Vec<_>, _>>()?;
518                Ok(rows)
519            } else {
520                let query = format!(
521                    "SELECT memory_id, distance FROM vec_memories \
522                     WHERE embedding MATCH ? AND namespace IN ({placeholders}) \
523                     ORDER BY distance LIMIT ?"
524                );
525                let mut stmt = conn.prepare(&query)?;
526                // Params: [bytes, ns0, ns1, ..., k]
527                let mut raw_params: Vec<Box<dyn rusqlite::ToSql>> = vec![Box::new(bytes)];
528                for ns in namespaces {
529                    raw_params.push(Box::new(ns.clone()));
530                }
531                raw_params.push(Box::new(k as i64));
532                let param_refs: Vec<&dyn rusqlite::ToSql> =
533                    raw_params.iter().map(|b| b.as_ref()).collect();
534                let rows = stmt
535                    .query_map(param_refs.as_slice(), |r| {
536                        Ok((r.get::<_, i64>(0)?, r.get::<_, f32>(1)?))
537                    })?
538                    .collect::<Result<Vec<_>, _>>()?;
539                Ok(rows)
540            }
541        }
542    }
543}
544
545/// Fetches a live memory by primary key and returns all columns.
546///
547/// Mirrors [`read_by_name`] but keyed on `rowid` for use after a KNN search.
548///
549/// # Errors
550///
551/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
552pub fn read_full(conn: &Connection, memory_id: i64) -> Result<Option<MemoryRow>, AppError> {
553    let mut stmt = conn.prepare_cached(
554        "SELECT id, namespace, name, type, description, body, body_hash,
555                session_id, source, metadata, created_at, updated_at
556         FROM memories WHERE id=?1 AND deleted_at IS NULL",
557    )?;
558    match stmt.query_row(params![memory_id], |r| {
559        Ok(MemoryRow {
560            id: r.get(0)?,
561            namespace: r.get(1)?,
562            name: r.get(2)?,
563            memory_type: r.get(3)?,
564            description: r.get(4)?,
565            body: r.get(5)?,
566            body_hash: r.get(6)?,
567            session_id: r.get(7)?,
568            source: r.get(8)?,
569            metadata: r.get(9)?,
570            created_at: r.get(10)?,
571            updated_at: r.get(11)?,
572        })
573    }) {
574        Ok(m) => Ok(Some(m)),
575        Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
576        Err(e) => Err(AppError::Database(e)),
577    }
578}
579
580/// Fetches all memory_ids in a namespace that are soft-deleted and whose
581/// `deleted_at` is older than `before_ts` (unix epoch seconds).
582///
583/// Used by `purge` to collect stale rows for permanent deletion.
584///
585/// # Errors
586///
587/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
588pub fn list_deleted_before(
589    conn: &Connection,
590    namespace: &str,
591    before_ts: i64,
592) -> Result<Vec<i64>, AppError> {
593    let mut stmt = conn.prepare_cached(
594        "SELECT id FROM memories WHERE namespace = ?1 AND deleted_at IS NOT NULL AND deleted_at < ?2",
595    )?;
596    let ids = stmt
597        .query_map(params![namespace, before_ts], |r| r.get::<_, i64>(0))?
598        .collect::<Result<Vec<_>, _>>()?;
599    Ok(ids)
600}
601
602/// Executes a prefix-matching FTS5 search against `fts_memories`.
603///
604/// The supplied `query` is suffixed with `*` to enable prefix matching, then
605/// joined back to `memories` to materialize full rows filtered by namespace.
606///
607/// # Errors
608///
609/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
610pub fn fts_search(
611    conn: &Connection,
612    query: &str,
613    namespace: &str,
614    memory_type: Option<&str>,
615    limit: usize,
616) -> Result<Vec<MemoryRow>, AppError> {
617    let fts_query = format!("{query}*");
618    if let Some(mt) = memory_type {
619        let mut stmt = conn.prepare(
620            "SELECT m.id, m.namespace, m.name, m.type, m.description, m.body, m.body_hash,
621                    m.session_id, m.source, m.metadata, m.created_at, m.updated_at
622             FROM fts_memories fts
623             JOIN memories m ON m.id = fts.rowid
624             WHERE fts_memories MATCH ?1 AND m.namespace = ?2 AND m.type = ?3 AND m.deleted_at IS NULL
625             ORDER BY rank LIMIT ?4",
626        )?;
627        let rows = stmt
628            .query_map(params![fts_query, namespace, mt, limit as i64], |r| {
629                Ok(MemoryRow {
630                    id: r.get(0)?,
631                    namespace: r.get(1)?,
632                    name: r.get(2)?,
633                    memory_type: r.get(3)?,
634                    description: r.get(4)?,
635                    body: r.get(5)?,
636                    body_hash: r.get(6)?,
637                    session_id: r.get(7)?,
638                    source: r.get(8)?,
639                    metadata: r.get(9)?,
640                    created_at: r.get(10)?,
641                    updated_at: r.get(11)?,
642                })
643            })?
644            .collect::<Result<Vec<_>, _>>()?;
645        Ok(rows)
646    } else {
647        let mut stmt = conn.prepare(
648            "SELECT m.id, m.namespace, m.name, m.type, m.description, m.body, m.body_hash,
649                    m.session_id, m.source, m.metadata, m.created_at, m.updated_at
650             FROM fts_memories fts
651             JOIN memories m ON m.id = fts.rowid
652             WHERE fts_memories MATCH ?1 AND m.namespace = ?2 AND m.deleted_at IS NULL
653             ORDER BY rank LIMIT ?3",
654        )?;
655        let rows = stmt
656            .query_map(params![fts_query, namespace, limit as i64], |r| {
657                Ok(MemoryRow {
658                    id: r.get(0)?,
659                    namespace: r.get(1)?,
660                    name: r.get(2)?,
661                    memory_type: r.get(3)?,
662                    description: r.get(4)?,
663                    body: r.get(5)?,
664                    body_hash: r.get(6)?,
665                    session_id: r.get(7)?,
666                    source: r.get(8)?,
667                    metadata: r.get(9)?,
668                    created_at: r.get(10)?,
669                    updated_at: r.get(11)?,
670                })
671            })?
672            .collect::<Result<Vec<_>, _>>()?;
673        Ok(rows)
674    }
675}
676
677#[cfg(test)]
678mod tests {
679    use super::*;
680    use rusqlite::Connection;
681
682    type TestResult = Result<(), Box<dyn std::error::Error>>;
683
684    fn setup_conn() -> Result<Connection, Box<dyn std::error::Error>> {
685        crate::storage::connection::register_vec_extension();
686        let mut conn = Connection::open_in_memory()?;
687        conn.execute_batch(
688            "PRAGMA foreign_keys = ON;
689             PRAGMA temp_store = MEMORY;",
690        )?;
691        crate::migrations::runner().run(&mut conn)?;
692        Ok(conn)
693    }
694
695    fn new_memory(name: &str) -> NewMemory {
696        NewMemory {
697            namespace: "global".to_string(),
698            name: name.to_string(),
699            memory_type: "user".to_string(),
700            description: "descricao de teste".to_string(),
701            body: "corpo da memoria de teste".to_string(),
702            body_hash: format!("hash-{name}"),
703            session_id: None,
704            source: "agent".to_string(),
705            metadata: serde_json::json!({}),
706        }
707    }
708
709    #[test]
710    fn insert_and_find_by_name_return_id() -> TestResult {
711        let conn = setup_conn()?;
712        let m = new_memory("mem-alpha");
713        let id = insert(&conn, &m)?;
714        assert!(id > 0);
715
716        let found = find_by_name(&conn, "global", "mem-alpha")?;
717        assert!(found.is_some());
718        let (found_id, _, _) = found.ok_or("mem-alpha should exist")?;
719        assert_eq!(found_id, id);
720        Ok(())
721    }
722
723    #[test]
724    fn find_by_name_returns_none_when_not_found() -> TestResult {
725        let conn = setup_conn()?;
726        let result = find_by_name(&conn, "global", "inexistente")?;
727        assert!(result.is_none());
728        Ok(())
729    }
730
731    #[test]
732    fn find_by_hash_returns_correct_id() -> TestResult {
733        let conn = setup_conn()?;
734        let m = new_memory("mem-hash");
735        let id = insert(&conn, &m)?;
736
737        let found = find_by_hash(&conn, "global", "hash-mem-hash")?;
738        assert_eq!(found, Some(id));
739        Ok(())
740    }
741
742    #[test]
743    fn find_by_hash_returns_none_when_hash_not_found() -> TestResult {
744        let conn = setup_conn()?;
745        let result = find_by_hash(&conn, "global", "hash-inexistente")?;
746        assert!(result.is_none());
747        Ok(())
748    }
749
750    #[test]
751    fn find_by_hash_ignores_different_namespace() -> TestResult {
752        let conn = setup_conn()?;
753        let m = new_memory("mem-ns");
754        insert(&conn, &m)?;
755
756        let result = find_by_hash(&conn, "outro-namespace", "hash-mem-ns")?;
757        assert!(result.is_none());
758        Ok(())
759    }
760
761    #[test]
762    fn read_by_name_returns_full_memory() -> TestResult {
763        let conn = setup_conn()?;
764        let m = new_memory("mem-read");
765        let id = insert(&conn, &m)?;
766
767        let row = read_by_name(&conn, "global", "mem-read")?.ok_or("mem-read should exist")?;
768        assert_eq!(row.id, id);
769        assert_eq!(row.name, "mem-read");
770        assert_eq!(row.memory_type, "user");
771        assert_eq!(row.body, "corpo da memoria de teste");
772        assert_eq!(row.namespace, "global");
773        Ok(())
774    }
775
776    #[test]
777    fn read_by_name_returns_none_for_missing() -> TestResult {
778        let conn = setup_conn()?;
779        let result = read_by_name(&conn, "global", "nao-existe")?;
780        assert!(result.is_none());
781        Ok(())
782    }
783
784    #[test]
785    fn read_full_by_id_returns_memory() -> TestResult {
786        let conn = setup_conn()?;
787        let m = new_memory("mem-full");
788        let id = insert(&conn, &m)?;
789
790        let row = read_full(&conn, id)?.ok_or("mem-full should exist")?;
791        assert_eq!(row.id, id);
792        assert_eq!(row.name, "mem-full");
793        Ok(())
794    }
795
796    #[test]
797    fn read_full_returns_none_for_missing_id() -> TestResult {
798        let conn = setup_conn()?;
799        let result = read_full(&conn, 9999)?;
800        assert!(result.is_none());
801        Ok(())
802    }
803
804    #[test]
805    fn update_without_optimism_modifies_fields() -> TestResult {
806        let conn = setup_conn()?;
807        let m = new_memory("mem-upd");
808        let id = insert(&conn, &m)?;
809
810        let mut m2 = new_memory("mem-upd");
811        m2.body = "corpo atualizado".to_string();
812        m2.body_hash = "hash-novo".to_string();
813        let ok = update(&conn, id, &m2, None)?;
814        assert!(ok);
815
816        let row = read_full(&conn, id)?.ok_or("mem-upd should exist")?;
817        assert_eq!(row.body, "corpo atualizado");
818        assert_eq!(row.body_hash, "hash-novo");
819        Ok(())
820    }
821
822    #[test]
823    fn update_with_correct_expected_updated_at_succeeds() -> TestResult {
824        let conn = setup_conn()?;
825        let m = new_memory("mem-opt");
826        let id = insert(&conn, &m)?;
827
828        let (_, updated_at, _) =
829            find_by_name(&conn, "global", "mem-opt")?.ok_or("mem-opt should exist")?;
830
831        let mut m2 = new_memory("mem-opt");
832        m2.body = "optimistic body".to_string();
833        m2.body_hash = "hash-optimistic".to_string();
834        let ok = update(&conn, id, &m2, Some(updated_at))?;
835        assert!(ok);
836
837        let row = read_full(&conn, id)?.ok_or("mem-opt should exist after update")?;
838        assert_eq!(row.body, "optimistic body");
839        Ok(())
840    }
841
842    #[test]
843    fn update_with_wrong_expected_updated_at_returns_false() -> TestResult {
844        let conn = setup_conn()?;
845        let m = new_memory("mem-conflict");
846        let id = insert(&conn, &m)?;
847
848        let mut m2 = new_memory("mem-conflict");
849        m2.body = "nao deve aparecer".to_string();
850        m2.body_hash = "hash-x".to_string();
851        let ok = update(&conn, id, &m2, Some(0))?;
852        assert!(!ok);
853
854        let row = read_full(&conn, id)?.ok_or("mem-conflict should exist")?;
855        assert_eq!(row.body, "corpo da memoria de teste");
856        Ok(())
857    }
858
859    #[test]
860    fn update_missing_id_returns_false() -> TestResult {
861        let conn = setup_conn()?;
862        let m = new_memory("fantasma");
863        let ok = update(&conn, 9999, &m, None)?;
864        assert!(!ok);
865        Ok(())
866    }
867
868    #[test]
869    fn soft_delete_marks_deleted_at() -> TestResult {
870        let conn = setup_conn()?;
871        let m = new_memory("mem-del");
872        insert(&conn, &m)?;
873
874        let ok = soft_delete(&conn, "global", "mem-del")?;
875        assert!(ok);
876
877        let result = find_by_name(&conn, "global", "mem-del")?;
878        assert!(result.is_none());
879
880        let result_read = read_by_name(&conn, "global", "mem-del")?;
881        assert!(result_read.is_none());
882        Ok(())
883    }
884
885    #[test]
886    fn soft_delete_returns_false_when_not_found() -> TestResult {
887        let conn = setup_conn()?;
888        let ok = soft_delete(&conn, "global", "nao-existe")?;
889        assert!(!ok);
890        Ok(())
891    }
892
893    #[test]
894    fn double_soft_delete_returns_false_on_second_call() -> TestResult {
895        let conn = setup_conn()?;
896        let m = new_memory("mem-del2");
897        insert(&conn, &m)?;
898
899        soft_delete(&conn, "global", "mem-del2")?;
900        let ok = soft_delete(&conn, "global", "mem-del2")?;
901        assert!(!ok);
902        Ok(())
903    }
904
905    #[test]
906    fn list_returns_memories_from_namespace() -> TestResult {
907        let conn = setup_conn()?;
908        insert(&conn, &new_memory("mem-list-a"))?;
909        insert(&conn, &new_memory("mem-list-b"))?;
910
911        let rows = list(&conn, "global", None, 10, 0, false)?;
912        assert!(rows.len() >= 2);
913        let nomes: Vec<_> = rows.iter().map(|r| r.name.as_str()).collect();
914        assert!(nomes.contains(&"mem-list-a"));
915        assert!(nomes.contains(&"mem-list-b"));
916        Ok(())
917    }
918
919    #[test]
920    fn list_with_type_filter_returns_only_correct_type() -> TestResult {
921        let conn = setup_conn()?;
922        insert(&conn, &new_memory("mem-user"))?;
923
924        let mut m2 = new_memory("mem-feedback");
925        m2.memory_type = "feedback".to_string();
926        insert(&conn, &m2)?;
927
928        let rows_user = list(&conn, "global", Some("user"), 10, 0, false)?;
929        assert!(rows_user.iter().all(|r| r.memory_type == "user"));
930
931        let rows_fb = list(&conn, "global", Some("feedback"), 10, 0, false)?;
932        assert!(rows_fb.iter().all(|r| r.memory_type == "feedback"));
933        Ok(())
934    }
935
936    #[test]
937    fn list_exclui_soft_deleted() -> TestResult {
938        let conn = setup_conn()?;
939        let m = new_memory("mem-excluida");
940        insert(&conn, &m)?;
941        soft_delete(&conn, "global", "mem-excluida")?;
942
943        let rows = list(&conn, "global", None, 10, 0, false)?;
944        assert!(rows.iter().all(|r| r.name != "mem-excluida"));
945        Ok(())
946    }
947
948    #[test]
949    fn list_pagination_works() -> TestResult {
950        let conn = setup_conn()?;
951        for i in 0..5 {
952            insert(&conn, &new_memory(&format!("mem-pag-{i}")))?;
953        }
954
955        let pagina1 = list(&conn, "global", None, 2, 0, false)?;
956        let pagina2 = list(&conn, "global", None, 2, 2, false)?;
957        assert!(pagina1.len() <= 2);
958        assert!(pagina2.len() <= 2);
959        if !pagina1.is_empty() && !pagina2.is_empty() {
960            assert_ne!(pagina1[0].id, pagina2[0].id);
961        }
962        Ok(())
963    }
964
965    #[test]
966    fn upsert_vec_and_delete_vec_work() -> TestResult {
967        let conn = setup_conn()?;
968        let m = new_memory("mem-vec");
969        let id = insert(&conn, &m)?;
970
971        let embedding: Vec<f32> = vec![0.1; 384];
972        upsert_vec(
973            &conn, id, "global", "user", &embedding, "mem-vec", "snippet",
974        )?;
975
976        let count: i64 = conn.query_row(
977            "SELECT COUNT(*) FROM vec_memories WHERE memory_id = ?1",
978            params![id],
979            |r| r.get(0),
980        )?;
981        assert_eq!(count, 1);
982
983        delete_vec(&conn, id)?;
984
985        let count_after: i64 = conn.query_row(
986            "SELECT COUNT(*) FROM vec_memories WHERE memory_id = ?1",
987            params![id],
988            |r| r.get(0),
989        )?;
990        assert_eq!(count_after, 0);
991        Ok(())
992    }
993
994    #[test]
995    fn upsert_vec_replaces_existing_vector() -> TestResult {
996        let conn = setup_conn()?;
997        let m = new_memory("mem-vec-upsert");
998        let id = insert(&conn, &m)?;
999
1000        let emb1: Vec<f32> = vec![0.1; 384];
1001        upsert_vec(&conn, id, "global", "user", &emb1, "mem-vec-upsert", "s1")?;
1002
1003        let emb2: Vec<f32> = vec![0.9; 384];
1004        upsert_vec(&conn, id, "global", "user", &emb2, "mem-vec-upsert", "s2")?;
1005
1006        let count: i64 = conn.query_row(
1007            "SELECT COUNT(*) FROM vec_memories WHERE memory_id = ?1",
1008            params![id],
1009            |r| r.get(0),
1010        )?;
1011        assert_eq!(count, 1);
1012        Ok(())
1013    }
1014
1015    #[test]
1016    fn knn_search_returns_results_by_distance() -> TestResult {
1017        let conn = setup_conn()?;
1018
1019        // emb_a: predominantemente positivo — cosseno alto com query [1.0; 384]
1020        let ma = new_memory("mem-knn-a");
1021        let id_a = insert(&conn, &ma)?;
1022        let emb_a: Vec<f32> = vec![1.0; 384];
1023        upsert_vec(&conn, id_a, "global", "user", &emb_a, "mem-knn-a", "s")?;
1024
1025        // emb_b: predominantemente negativo — cosseno baixo com query [1.0; 384]
1026        let mb = new_memory("mem-knn-b");
1027        let id_b = insert(&conn, &mb)?;
1028        let emb_b: Vec<f32> = vec![-1.0; 384];
1029        upsert_vec(&conn, id_b, "global", "user", &emb_b, "mem-knn-b", "s")?;
1030
1031        let query: Vec<f32> = vec![1.0; 384];
1032        let results = knn_search(&conn, &query, &["global".to_string()], None, 2)?;
1033        assert!(!results.is_empty());
1034        assert_eq!(results[0].0, id_a);
1035        Ok(())
1036    }
1037
1038    #[test]
1039    fn knn_search_with_type_filter_restricts_result() -> TestResult {
1040        let conn = setup_conn()?;
1041
1042        let ma = new_memory("mem-knn-tipo-user");
1043        let id_a = insert(&conn, &ma)?;
1044        let emb: Vec<f32> = vec![1.0; 384];
1045        upsert_vec(
1046            &conn,
1047            id_a,
1048            "global",
1049            "user",
1050            &emb,
1051            "mem-knn-tipo-user",
1052            "s",
1053        )?;
1054
1055        let mut mb = new_memory("mem-knn-tipo-fb");
1056        mb.memory_type = "feedback".to_string();
1057        let id_b = insert(&conn, &mb)?;
1058        upsert_vec(
1059            &conn,
1060            id_b,
1061            "global",
1062            "feedback",
1063            &emb,
1064            "mem-knn-tipo-fb",
1065            "s",
1066        )?;
1067
1068        let query: Vec<f32> = vec![1.0; 384];
1069        let results_user = knn_search(&conn, &query, &["global".to_string()], Some("user"), 5)?;
1070        assert!(results_user.iter().all(|(id, _)| *id == id_a));
1071
1072        let results_fb = knn_search(&conn, &query, &["global".to_string()], Some("feedback"), 5)?;
1073        assert!(results_fb.iter().all(|(id, _)| *id == id_b));
1074        Ok(())
1075    }
1076
1077    #[test]
1078    fn fts_search_finds_by_prefix_in_body() -> TestResult {
1079        let conn = setup_conn()?;
1080        let mut m = new_memory("mem-fts");
1081        m.body = "linguagem de programacao rust".to_string();
1082        insert(&conn, &m)?;
1083
1084        conn.execute_batch(
1085            "INSERT INTO fts_memories(rowid, name, description, body)
1086             SELECT id, name, description, body FROM memories WHERE deleted_at IS NULL",
1087        )?;
1088
1089        let rows = fts_search(&conn, "programacao", "global", None, 10)?;
1090        assert!(!rows.is_empty());
1091        assert!(rows.iter().any(|r| r.name == "mem-fts"));
1092        Ok(())
1093    }
1094
1095    #[test]
1096    fn fts_search_with_type_filter() -> TestResult {
1097        let conn = setup_conn()?;
1098        let mut m = new_memory("mem-fts-tipo");
1099        m.body = "linguagem especial para filtro".to_string();
1100        insert(&conn, &m)?;
1101
1102        let mut m2 = new_memory("mem-fts-feedback");
1103        m2.memory_type = "feedback".to_string();
1104        m2.body = "linguagem especial para filtro".to_string();
1105        insert(&conn, &m2)?;
1106
1107        conn.execute_batch(
1108            "INSERT INTO fts_memories(rowid, name, description, body)
1109             SELECT id, name, description, body FROM memories WHERE deleted_at IS NULL",
1110        )?;
1111
1112        let rows_user = fts_search(&conn, "especial", "global", Some("user"), 10)?;
1113        assert!(rows_user.iter().all(|r| r.memory_type == "user"));
1114
1115        let rows_fb = fts_search(&conn, "especial", "global", Some("feedback"), 10)?;
1116        assert!(rows_fb.iter().all(|r| r.memory_type == "feedback"));
1117        Ok(())
1118    }
1119
1120    #[test]
1121    fn fts_search_excludes_deleted() -> TestResult {
1122        let conn = setup_conn()?;
1123        let mut m = new_memory("mem-fts-del");
1124        m.body = "conteudo deletado fts".to_string();
1125        insert(&conn, &m)?;
1126
1127        conn.execute_batch(
1128            "INSERT INTO fts_memories(rowid, name, description, body)
1129             SELECT id, name, description, body FROM memories WHERE deleted_at IS NULL",
1130        )?;
1131
1132        soft_delete(&conn, "global", "mem-fts-del")?;
1133
1134        let rows = fts_search(&conn, "deletado", "global", None, 10)?;
1135        assert!(rows.iter().all(|r| r.name != "mem-fts-del"));
1136        Ok(())
1137    }
1138
1139    #[test]
1140    fn list_deleted_before_returns_correct_ids() -> TestResult {
1141        let conn = setup_conn()?;
1142        let m = new_memory("mem-purge");
1143        insert(&conn, &m)?;
1144        soft_delete(&conn, "global", "mem-purge")?;
1145
1146        let ids = list_deleted_before(&conn, "global", i64::MAX)?;
1147        assert!(!ids.is_empty());
1148
1149        let ids_antes = list_deleted_before(&conn, "global", 0)?;
1150        assert!(ids_antes.is_empty());
1151        Ok(())
1152    }
1153
1154    #[test]
1155    fn find_by_name_returns_correct_max_version() -> TestResult {
1156        let conn = setup_conn()?;
1157        let m = new_memory("mem-ver");
1158        let id = insert(&conn, &m)?;
1159
1160        let (_, _, v0) = find_by_name(&conn, "global", "mem-ver")?.ok_or("mem-ver should exist")?;
1161        assert_eq!(v0, 0);
1162
1163        conn.execute(
1164            "INSERT INTO memory_versions (memory_id, version, name, type, description, body, metadata, change_reason)
1165             VALUES (?1, 1, 'mem-ver', 'user', 'desc', 'body', '{}', 'create')",
1166            params![id],
1167        )?;
1168
1169        let (_, _, v1) =
1170            find_by_name(&conn, "global", "mem-ver")?.ok_or("mem-ver should exist after insert")?;
1171        assert_eq!(v1, 1);
1172        Ok(())
1173    }
1174
1175    #[test]
1176    fn insert_com_metadata_json() -> TestResult {
1177        let conn = setup_conn()?;
1178        let mut m = new_memory("mem-meta");
1179        m.metadata = serde_json::json!({"chave": "valor", "numero": 42});
1180        let id = insert(&conn, &m)?;
1181
1182        let row = read_full(&conn, id)?.ok_or("mem-meta should exist")?;
1183        let meta: serde_json::Value = serde_json::from_str(&row.metadata)?;
1184        assert_eq!(meta["chave"], "valor");
1185        assert_eq!(meta["numero"], 42);
1186        Ok(())
1187    }
1188
1189    #[test]
1190    fn insert_com_session_id() -> TestResult {
1191        let conn = setup_conn()?;
1192        let mut m = new_memory("mem-session");
1193        m.session_id = Some("sessao-xyz".to_string());
1194        let id = insert(&conn, &m)?;
1195
1196        let row = read_full(&conn, id)?.ok_or("mem-session should exist")?;
1197        assert_eq!(row.session_id, Some("sessao-xyz".to_string()));
1198        Ok(())
1199    }
1200
1201    #[test]
1202    fn delete_vec_for_nonexistent_id_does_not_fail() -> TestResult {
1203        let conn = setup_conn()?;
1204        let result = delete_vec(&conn, 99999);
1205        assert!(result.is_ok());
1206        Ok(())
1207    }
1208}