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    /// Unix epoch when the memory was soft-deleted, or `None` for active memories.
49    /// Surfaced in `list --include-deleted --json` so LLM consumers can distinguish
50    /// active from soft-deleted rows without a second SQL query (v1.0.37 H7+M9 fix).
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub deleted_at: Option<i64>,
53}
54
55/// Finds a live memory by `(namespace, name)` and returns key metadata.
56///
57/// # Arguments
58///
59/// - `conn` — open SQLite connection configured with the project pragmas.
60/// - `namespace` — resolved namespace for the lookup.
61/// - `name` — kebab-case memory name.
62///
63/// # Returns
64///
65/// `Ok(Some((id, updated_at, max_version)))` when the memory exists and is
66/// not soft-deleted, `Ok(None)` otherwise.
67///
68/// # Errors
69///
70/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
71pub fn find_by_name(
72    conn: &Connection,
73    namespace: &str,
74    name: &str,
75) -> Result<Option<(i64, i64, i64)>, AppError> {
76    let mut stmt = conn.prepare_cached(
77        "SELECT m.id, m.updated_at, COALESCE(MAX(v.version), 0)
78         FROM memories m
79         LEFT JOIN memory_versions v ON v.memory_id = m.id
80         WHERE m.namespace = ?1 AND m.name = ?2 AND m.deleted_at IS NULL
81         GROUP BY m.id",
82    )?;
83    let result = stmt.query_row(params![namespace, name], |r| {
84        Ok((
85            r.get::<_, i64>(0)?,
86            r.get::<_, i64>(1)?,
87            r.get::<_, i64>(2)?,
88        ))
89    });
90    match result {
91        Ok(row) => Ok(Some(row)),
92        Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
93        Err(e) => Err(AppError::Database(e)),
94    }
95}
96
97/// Looks up a live memory by exact `body_hash` within a namespace.
98///
99/// Used during `remember` to short-circuit semantic duplicates before
100/// spending an embedding call.
101///
102/// # Returns
103///
104/// `Ok(Some(id))` when a live memory with the same hash exists,
105/// `Ok(None)` otherwise.
106///
107/// # Errors
108///
109/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
110pub fn find_by_hash(
111    conn: &Connection,
112    namespace: &str,
113    body_hash: &str,
114) -> Result<Option<i64>, AppError> {
115    let mut stmt = conn.prepare_cached(
116        "SELECT id FROM memories WHERE namespace = ?1 AND body_hash = ?2 AND deleted_at IS NULL",
117    )?;
118    match stmt.query_row(params![namespace, body_hash], |r| r.get(0)) {
119        Ok(id) => Ok(Some(id)),
120        Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
121        Err(e) => Err(AppError::Database(e)),
122    }
123}
124
125/// Inserts a new row into the `memories` table.
126///
127/// # Arguments
128///
129/// - `conn` — active SQLite connection, typically inside a transaction.
130/// - `m` — validated payload including `body_hash` and serialized metadata.
131///
132/// # Returns
133///
134/// The `rowid` assigned to the newly inserted memory.
135///
136/// # Errors
137///
138/// Returns `Err(AppError::Database)` on insertion failure and
139/// `Err(AppError::Json)` if metadata serialization fails.
140pub fn insert(conn: &Connection, m: &NewMemory) -> Result<i64, AppError> {
141    conn.execute(
142        "INSERT INTO memories (namespace, name, type, description, body, body_hash, session_id, source, metadata)
143         VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
144        params![
145            m.namespace, m.name, m.memory_type, m.description, m.body,
146            m.body_hash, m.session_id, m.source,
147            serde_json::to_string(&m.metadata)?
148        ],
149    )?;
150    Ok(conn.last_insert_rowid())
151}
152
153/// Updates an existing memory optionally guarded by optimistic concurrency.
154///
155/// When `expected_updated_at` is `Some(ts)` the row is only updated if its
156/// current `updated_at` equals `ts`. This protects concurrent `edit` calls
157/// from silently clobbering each other.
158///
159/// # Returns
160///
161/// `Ok(true)` when exactly one row was updated, `Ok(false)` when the
162/// optimistic check failed or the memory does not exist.
163///
164/// # Errors
165///
166/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
167pub fn update(
168    conn: &Connection,
169    id: i64,
170    m: &NewMemory,
171    expected_updated_at: Option<i64>,
172) -> Result<bool, AppError> {
173    let affected = if let Some(ts) = expected_updated_at {
174        conn.execute(
175            "UPDATE memories SET type=?2, description=?3, body=?4, body_hash=?5,
176             session_id=?6, source=?7, metadata=?8
177             WHERE id=?1 AND updated_at=?9 AND deleted_at IS NULL",
178            params![
179                id,
180                m.memory_type,
181                m.description,
182                m.body,
183                m.body_hash,
184                m.session_id,
185                m.source,
186                serde_json::to_string(&m.metadata)?,
187                ts
188            ],
189        )?
190    } else {
191        conn.execute(
192            "UPDATE memories SET type=?2, description=?3, body=?4, body_hash=?5,
193             session_id=?6, source=?7, metadata=?8
194             WHERE id=?1 AND deleted_at IS NULL",
195            params![
196                id,
197                m.memory_type,
198                m.description,
199                m.body,
200                m.body_hash,
201                m.session_id,
202                m.source,
203                serde_json::to_string(&m.metadata)?
204            ],
205        )?
206    };
207    Ok(affected == 1)
208}
209
210/// Replaces the vector row for a memory in `vec_memories`.
211///
212/// `sqlite-vec` virtual tables do not implement `INSERT OR REPLACE`, so the
213/// existing row is deleted first and a fresh vector is inserted. Callers
214/// must pass an `embedding` with length [`crate::constants::EMBEDDING_DIM`].
215///
216/// # Errors
217///
218/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
219pub fn upsert_vec(
220    conn: &Connection,
221    memory_id: i64,
222    namespace: &str,
223    memory_type: &str,
224    embedding: &[f32],
225    name: &str,
226    snippet: &str,
227) -> Result<(), AppError> {
228    // sqlite-vec virtual tables do not support INSERT OR REPLACE semantics.
229    // Must delete the existing row first, then insert.  Both statements are
230    // wrapped in `with_busy_retry` because WAL-mode concurrent writers can
231    // cause SQLITE_BUSY on vec0 virtual table writes.
232    let embedding_bytes = f32_to_bytes(embedding);
233    with_busy_retry(|| {
234        conn.execute(
235            "DELETE FROM vec_memories WHERE memory_id = ?1",
236            params![memory_id],
237        )?;
238        conn.execute(
239            "INSERT INTO vec_memories(memory_id, namespace, type, embedding, name, snippet)
240             VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
241            params![
242                memory_id,
243                namespace,
244                memory_type,
245                &embedding_bytes,
246                name,
247                snippet
248            ],
249        )?;
250        Ok(())
251    })
252}
253
254/// Deletes the vector row for `memory_id` from `vec_memories`.
255///
256/// Called during `forget` and `purge` to keep the vector table consistent
257/// with the logical state of `memories`.
258///
259/// # Errors
260///
261/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
262pub fn delete_vec(conn: &Connection, memory_id: i64) -> Result<(), AppError> {
263    conn.execute(
264        "DELETE FROM vec_memories WHERE memory_id = ?1",
265        params![memory_id],
266    )?;
267    Ok(())
268}
269
270/// Fetches a live memory by `(namespace, name)` and returns all columns.
271///
272/// # Returns
273///
274/// `Ok(Some(row))` when found, `Ok(None)` when missing or soft-deleted.
275///
276/// # Errors
277///
278/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
279pub fn read_by_name(
280    conn: &Connection,
281    namespace: &str,
282    name: &str,
283) -> Result<Option<MemoryRow>, AppError> {
284    let mut stmt = conn.prepare_cached(
285        "SELECT id, namespace, name, type, description, body, body_hash,
286                session_id, source, metadata, created_at, updated_at, deleted_at
287         FROM memories WHERE namespace=?1 AND name=?2 AND deleted_at IS NULL",
288    )?;
289    match stmt.query_row(params![namespace, name], |r| {
290        Ok(MemoryRow {
291            id: r.get(0)?,
292            namespace: r.get(1)?,
293            name: r.get(2)?,
294            memory_type: r.get(3)?,
295            description: r.get(4)?,
296            body: r.get(5)?,
297            body_hash: r.get(6)?,
298            session_id: r.get(7)?,
299            source: r.get(8)?,
300            metadata: r.get(9)?,
301            created_at: r.get(10)?,
302            updated_at: r.get(11)?,
303            deleted_at: r.get(12)?,
304        })
305    }) {
306        Ok(m) => Ok(Some(m)),
307        Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
308        Err(e) => Err(AppError::Database(e)),
309    }
310}
311
312/// Soft-deletes a memory by setting `deleted_at = unixepoch()`.
313///
314/// Versions and chunks are preserved so `restore` can undo the operation
315/// until a subsequent `purge` reclaims the storage permanently.
316///
317/// # Returns
318///
319/// `Ok(true)` when a live memory was soft-deleted, `Ok(false)` when no
320/// matching live row existed.
321///
322/// # Errors
323///
324/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
325pub fn soft_delete(conn: &Connection, namespace: &str, name: &str) -> Result<bool, AppError> {
326    let affected = conn.execute(
327        "UPDATE memories SET deleted_at = unixepoch() WHERE namespace=?1 AND name=?2 AND deleted_at IS NULL",
328        params![namespace, name],
329    )?;
330    Ok(affected == 1)
331}
332
333/// Lists live memories in a namespace ordered by `updated_at` descending.
334///
335/// # Arguments
336///
337/// - `memory_type` — optional filter on the `type` column.
338/// - `limit` / `offset` — standard pagination controls in rows.
339///
340/// # Errors
341///
342/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
343pub fn list(
344    conn: &Connection,
345    namespace: &str,
346    memory_type: Option<&str>,
347    limit: usize,
348    offset: usize,
349    include_deleted: bool,
350) -> Result<Vec<MemoryRow>, AppError> {
351    let deleted_clause = if include_deleted {
352        ""
353    } else {
354        " AND deleted_at IS NULL"
355    };
356    if let Some(mt) = memory_type {
357        let sql = format!(
358            "SELECT id, namespace, name, type, description, body, body_hash,
359                    session_id, source, metadata, created_at, updated_at, deleted_at
360             FROM memories WHERE namespace=?1 AND type=?2{deleted_clause}
361             ORDER BY updated_at DESC LIMIT ?3 OFFSET ?4"
362        );
363        let mut stmt = conn.prepare(&sql)?;
364        let rows = stmt
365            .query_map(params![namespace, mt, limit as i64, offset as i64], |r| {
366                Ok(MemoryRow {
367                    id: r.get(0)?,
368                    namespace: r.get(1)?,
369                    name: r.get(2)?,
370                    memory_type: r.get(3)?,
371                    description: r.get(4)?,
372                    body: r.get(5)?,
373                    body_hash: r.get(6)?,
374                    session_id: r.get(7)?,
375                    source: r.get(8)?,
376                    metadata: r.get(9)?,
377                    created_at: r.get(10)?,
378                    updated_at: r.get(11)?,
379                    deleted_at: r.get(12)?,
380                })
381            })?
382            .collect::<Result<Vec<_>, _>>()?;
383        Ok(rows)
384    } else {
385        let sql = format!(
386            "SELECT id, namespace, name, type, description, body, body_hash,
387                    session_id, source, metadata, created_at, updated_at, deleted_at
388             FROM memories WHERE namespace=?1{deleted_clause}
389             ORDER BY updated_at DESC LIMIT ?2 OFFSET ?3"
390        );
391        let mut stmt = conn.prepare(&sql)?;
392        let rows = stmt
393            .query_map(params![namespace, limit as i64, offset as i64], |r| {
394                Ok(MemoryRow {
395                    id: r.get(0)?,
396                    namespace: r.get(1)?,
397                    name: r.get(2)?,
398                    memory_type: r.get(3)?,
399                    description: r.get(4)?,
400                    body: r.get(5)?,
401                    body_hash: r.get(6)?,
402                    session_id: r.get(7)?,
403                    source: r.get(8)?,
404                    metadata: r.get(9)?,
405                    created_at: r.get(10)?,
406                    updated_at: r.get(11)?,
407                    deleted_at: r.get(12)?,
408                })
409            })?
410            .collect::<Result<Vec<_>, _>>()?;
411        Ok(rows)
412    }
413}
414
415/// Runs a KNN search over `vec_memories`, optionally restricted to namespaces.
416///
417/// # Arguments
418///
419/// - `embedding` — query vector of length [`crate::constants::EMBEDDING_DIM`].
420/// - `namespaces` — namespaces to search. Empty slice means "all namespaces".
421/// - `memory_type` — optional filter on the `type` column.
422/// - `k` — maximum number of hits to return.
423///
424/// # Returns
425///
426/// A vector of `(memory_id, distance)` pairs sorted by ascending distance.
427///
428/// # Errors
429///
430/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
431pub fn knn_search(
432    conn: &Connection,
433    embedding: &[f32],
434    namespaces: &[String],
435    memory_type: Option<&str>,
436    k: usize,
437) -> Result<Vec<(i64, f32)>, AppError> {
438    let bytes = f32_to_bytes(embedding);
439
440    match namespaces.len() {
441        0 => {
442            // No namespace filter — search all namespaces.
443            if let Some(mt) = memory_type {
444                let mut stmt = conn.prepare(
445                    "SELECT memory_id, distance FROM vec_memories \
446                     WHERE embedding MATCH ?1 AND type = ?2 \
447                     ORDER BY distance LIMIT ?3",
448                )?;
449                let rows = stmt
450                    .query_map(params![bytes, mt, k as i64], |r| {
451                        Ok((r.get::<_, i64>(0)?, r.get::<_, f32>(1)?))
452                    })?
453                    .collect::<Result<Vec<_>, _>>()?;
454                Ok(rows)
455            } else {
456                let mut stmt = conn.prepare(
457                    "SELECT memory_id, distance FROM vec_memories \
458                     WHERE embedding MATCH ?1 \
459                     ORDER BY distance LIMIT ?2",
460                )?;
461                let rows = stmt
462                    .query_map(params![bytes, k as i64], |r| {
463                        Ok((r.get::<_, i64>(0)?, r.get::<_, f32>(1)?))
464                    })?
465                    .collect::<Result<Vec<_>, _>>()?;
466                Ok(rows)
467            }
468        }
469        1 => {
470            // Fast single-namespace path (preserved from previous implementation).
471            let ns = &namespaces[0];
472            if let Some(mt) = memory_type {
473                let mut stmt = conn.prepare(
474                    "SELECT memory_id, distance FROM vec_memories \
475                     WHERE embedding MATCH ?1 AND namespace = ?2 AND type = ?3 \
476                     ORDER BY distance LIMIT ?4",
477                )?;
478                let rows = stmt
479                    .query_map(params![bytes, ns, mt, k as i64], |r| {
480                        Ok((r.get::<_, i64>(0)?, r.get::<_, f32>(1)?))
481                    })?
482                    .collect::<Result<Vec<_>, _>>()?;
483                Ok(rows)
484            } else {
485                let mut stmt = conn.prepare(
486                    "SELECT memory_id, distance FROM vec_memories \
487                     WHERE embedding MATCH ?1 AND namespace = ?2 \
488                     ORDER BY distance LIMIT ?3",
489                )?;
490                let rows = stmt
491                    .query_map(params![bytes, ns, k as i64], |r| {
492                        Ok((r.get::<_, i64>(0)?, r.get::<_, f32>(1)?))
493                    })?
494                    .collect::<Result<Vec<_>, _>>()?;
495                Ok(rows)
496            }
497        }
498        _ => {
499            // Multiple explicit namespaces: build IN clause with positional placeholders.
500            // rusqlite does not support array binding, so we generate "?,?,..." manually.
501            let placeholders = (0..namespaces.len())
502                .map(|_| "?")
503                .collect::<Vec<_>>()
504                .join(",");
505            if let Some(mt) = memory_type {
506                let query = format!(
507                    "SELECT memory_id, distance FROM vec_memories \
508                     WHERE embedding MATCH ? AND type = ? AND namespace IN ({placeholders}) \
509                     ORDER BY distance LIMIT ?"
510                );
511                let mut stmt = conn.prepare(&query)?;
512                // Params: [bytes, mt, ns0, ns1, ..., k]
513                let mut raw_params: Vec<Box<dyn rusqlite::ToSql>> =
514                    vec![Box::new(bytes), Box::new(mt.to_string())];
515                for ns in namespaces {
516                    raw_params.push(Box::new(ns.clone()));
517                }
518                raw_params.push(Box::new(k as i64));
519                let param_refs: Vec<&dyn rusqlite::ToSql> =
520                    raw_params.iter().map(|b| b.as_ref()).collect();
521                let rows = stmt
522                    .query_map(param_refs.as_slice(), |r| {
523                        Ok((r.get::<_, i64>(0)?, r.get::<_, f32>(1)?))
524                    })?
525                    .collect::<Result<Vec<_>, _>>()?;
526                Ok(rows)
527            } else {
528                let query = format!(
529                    "SELECT memory_id, distance FROM vec_memories \
530                     WHERE embedding MATCH ? AND namespace IN ({placeholders}) \
531                     ORDER BY distance LIMIT ?"
532                );
533                let mut stmt = conn.prepare(&query)?;
534                // Params: [bytes, ns0, ns1, ..., k]
535                let mut raw_params: Vec<Box<dyn rusqlite::ToSql>> = vec![Box::new(bytes)];
536                for ns in namespaces {
537                    raw_params.push(Box::new(ns.clone()));
538                }
539                raw_params.push(Box::new(k as i64));
540                let param_refs: Vec<&dyn rusqlite::ToSql> =
541                    raw_params.iter().map(|b| b.as_ref()).collect();
542                let rows = stmt
543                    .query_map(param_refs.as_slice(), |r| {
544                        Ok((r.get::<_, i64>(0)?, r.get::<_, f32>(1)?))
545                    })?
546                    .collect::<Result<Vec<_>, _>>()?;
547                Ok(rows)
548            }
549        }
550    }
551}
552
553/// Fetches a live memory by primary key and returns all columns.
554///
555/// Mirrors [`read_by_name`] but keyed on `rowid` for use after a KNN search.
556///
557/// # Errors
558///
559/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
560pub fn read_full(conn: &Connection, memory_id: i64) -> Result<Option<MemoryRow>, AppError> {
561    let mut stmt = conn.prepare_cached(
562        "SELECT id, namespace, name, type, description, body, body_hash,
563                session_id, source, metadata, created_at, updated_at, deleted_at
564         FROM memories WHERE id=?1 AND deleted_at IS NULL",
565    )?;
566    match stmt.query_row(params![memory_id], |r| {
567        Ok(MemoryRow {
568            id: r.get(0)?,
569            namespace: r.get(1)?,
570            name: r.get(2)?,
571            memory_type: r.get(3)?,
572            description: r.get(4)?,
573            body: r.get(5)?,
574            body_hash: r.get(6)?,
575            session_id: r.get(7)?,
576            source: r.get(8)?,
577            metadata: r.get(9)?,
578            created_at: r.get(10)?,
579            updated_at: r.get(11)?,
580            deleted_at: r.get(12)?,
581        })
582    }) {
583        Ok(m) => Ok(Some(m)),
584        Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
585        Err(e) => Err(AppError::Database(e)),
586    }
587}
588
589/// Fetches all memory_ids in a namespace that are soft-deleted and whose
590/// `deleted_at` is older than `before_ts` (unix epoch seconds).
591///
592/// Used by `purge` to collect stale rows for permanent deletion.
593///
594/// # Errors
595///
596/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
597pub fn list_deleted_before(
598    conn: &Connection,
599    namespace: &str,
600    before_ts: i64,
601) -> Result<Vec<i64>, AppError> {
602    let mut stmt = conn.prepare_cached(
603        "SELECT id FROM memories WHERE namespace = ?1 AND deleted_at IS NOT NULL AND deleted_at < ?2",
604    )?;
605    let ids = stmt
606        .query_map(params![namespace, before_ts], |r| r.get::<_, i64>(0))?
607        .collect::<Result<Vec<_>, _>>()?;
608    Ok(ids)
609}
610
611/// Executes a prefix-matching FTS5 search against `fts_memories`.
612///
613/// The supplied `query` is suffixed with `*` to enable prefix matching, then
614/// joined back to `memories` to materialize full rows filtered by namespace.
615///
616/// # Errors
617///
618/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
619pub fn fts_search(
620    conn: &Connection,
621    query: &str,
622    namespace: &str,
623    memory_type: Option<&str>,
624    limit: usize,
625) -> Result<Vec<MemoryRow>, AppError> {
626    let fts_query = format!("{query}*");
627    if let Some(mt) = memory_type {
628        let mut stmt = conn.prepare(
629            "SELECT m.id, m.namespace, m.name, m.type, m.description, m.body, m.body_hash,
630                    m.session_id, m.source, m.metadata, m.created_at, m.updated_at, m.deleted_at
631             FROM fts_memories fts
632             JOIN memories m ON m.id = fts.rowid
633             WHERE fts_memories MATCH ?1 AND m.namespace = ?2 AND m.type = ?3 AND m.deleted_at IS NULL
634             ORDER BY rank LIMIT ?4",
635        )?;
636        let rows = stmt
637            .query_map(params![fts_query, namespace, mt, limit as i64], |r| {
638                Ok(MemoryRow {
639                    id: r.get(0)?,
640                    namespace: r.get(1)?,
641                    name: r.get(2)?,
642                    memory_type: r.get(3)?,
643                    description: r.get(4)?,
644                    body: r.get(5)?,
645                    body_hash: r.get(6)?,
646                    session_id: r.get(7)?,
647                    source: r.get(8)?,
648                    metadata: r.get(9)?,
649                    created_at: r.get(10)?,
650                    updated_at: r.get(11)?,
651                    deleted_at: r.get(12)?,
652                })
653            })?
654            .collect::<Result<Vec<_>, _>>()?;
655        Ok(rows)
656    } else {
657        let mut stmt = conn.prepare(
658            "SELECT m.id, m.namespace, m.name, m.type, m.description, m.body, m.body_hash,
659                    m.session_id, m.source, m.metadata, m.created_at, m.updated_at, m.deleted_at
660             FROM fts_memories fts
661             JOIN memories m ON m.id = fts.rowid
662             WHERE fts_memories MATCH ?1 AND m.namespace = ?2 AND m.deleted_at IS NULL
663             ORDER BY rank LIMIT ?3",
664        )?;
665        let rows = stmt
666            .query_map(params![fts_query, namespace, limit as i64], |r| {
667                Ok(MemoryRow {
668                    id: r.get(0)?,
669                    namespace: r.get(1)?,
670                    name: r.get(2)?,
671                    memory_type: r.get(3)?,
672                    description: r.get(4)?,
673                    body: r.get(5)?,
674                    body_hash: r.get(6)?,
675                    session_id: r.get(7)?,
676                    source: r.get(8)?,
677                    metadata: r.get(9)?,
678                    created_at: r.get(10)?,
679                    updated_at: r.get(11)?,
680                    deleted_at: r.get(12)?,
681                })
682            })?
683            .collect::<Result<Vec<_>, _>>()?;
684        Ok(rows)
685    }
686}
687
688#[cfg(test)]
689mod tests {
690    use super::*;
691    use rusqlite::Connection;
692
693    type TestResult = Result<(), Box<dyn std::error::Error>>;
694
695    fn setup_conn() -> Result<Connection, Box<dyn std::error::Error>> {
696        crate::storage::connection::register_vec_extension();
697        let mut conn = Connection::open_in_memory()?;
698        conn.execute_batch(
699            "PRAGMA foreign_keys = ON;
700             PRAGMA temp_store = MEMORY;",
701        )?;
702        crate::migrations::runner().run(&mut conn)?;
703        Ok(conn)
704    }
705
706    fn new_memory(name: &str) -> NewMemory {
707        NewMemory {
708            namespace: "global".to_string(),
709            name: name.to_string(),
710            memory_type: "user".to_string(),
711            description: "descricao de teste".to_string(),
712            body: "test memory body".to_string(),
713            body_hash: format!("hash-{name}"),
714            session_id: None,
715            source: "agent".to_string(),
716            metadata: serde_json::json!({}),
717        }
718    }
719
720    #[test]
721    fn insert_and_find_by_name_return_id() -> TestResult {
722        let conn = setup_conn()?;
723        let m = new_memory("mem-alpha");
724        let id = insert(&conn, &m)?;
725        assert!(id > 0);
726
727        let found = find_by_name(&conn, "global", "mem-alpha")?;
728        assert!(found.is_some());
729        let (found_id, _, _) = found.ok_or("mem-alpha should exist")?;
730        assert_eq!(found_id, id);
731        Ok(())
732    }
733
734    #[test]
735    fn find_by_name_returns_none_when_not_found() -> TestResult {
736        let conn = setup_conn()?;
737        let result = find_by_name(&conn, "global", "inexistente")?;
738        assert!(result.is_none());
739        Ok(())
740    }
741
742    #[test]
743    fn find_by_hash_returns_correct_id() -> TestResult {
744        let conn = setup_conn()?;
745        let m = new_memory("mem-hash");
746        let id = insert(&conn, &m)?;
747
748        let found = find_by_hash(&conn, "global", "hash-mem-hash")?;
749        assert_eq!(found, Some(id));
750        Ok(())
751    }
752
753    #[test]
754    fn find_by_hash_returns_none_when_hash_not_found() -> TestResult {
755        let conn = setup_conn()?;
756        let result = find_by_hash(&conn, "global", "hash-inexistente")?;
757        assert!(result.is_none());
758        Ok(())
759    }
760
761    #[test]
762    fn find_by_hash_ignores_different_namespace() -> TestResult {
763        let conn = setup_conn()?;
764        let m = new_memory("mem-ns");
765        insert(&conn, &m)?;
766
767        let result = find_by_hash(&conn, "outro-namespace", "hash-mem-ns")?;
768        assert!(result.is_none());
769        Ok(())
770    }
771
772    #[test]
773    fn read_by_name_returns_full_memory() -> TestResult {
774        let conn = setup_conn()?;
775        let m = new_memory("mem-read");
776        let id = insert(&conn, &m)?;
777
778        let row = read_by_name(&conn, "global", "mem-read")?.ok_or("mem-read should exist")?;
779        assert_eq!(row.id, id);
780        assert_eq!(row.name, "mem-read");
781        assert_eq!(row.memory_type, "user");
782        assert_eq!(row.body, "test memory body");
783        assert_eq!(row.namespace, "global");
784        Ok(())
785    }
786
787    #[test]
788    fn read_by_name_returns_none_for_missing() -> TestResult {
789        let conn = setup_conn()?;
790        let result = read_by_name(&conn, "global", "nao-existe")?;
791        assert!(result.is_none());
792        Ok(())
793    }
794
795    #[test]
796    fn read_full_by_id_returns_memory() -> TestResult {
797        let conn = setup_conn()?;
798        let m = new_memory("mem-full");
799        let id = insert(&conn, &m)?;
800
801        let row = read_full(&conn, id)?.ok_or("mem-full should exist")?;
802        assert_eq!(row.id, id);
803        assert_eq!(row.name, "mem-full");
804        Ok(())
805    }
806
807    #[test]
808    fn read_full_returns_none_for_missing_id() -> TestResult {
809        let conn = setup_conn()?;
810        let result = read_full(&conn, 9999)?;
811        assert!(result.is_none());
812        Ok(())
813    }
814
815    #[test]
816    fn update_without_optimism_modifies_fields() -> TestResult {
817        let conn = setup_conn()?;
818        let m = new_memory("mem-upd");
819        let id = insert(&conn, &m)?;
820
821        let mut m2 = new_memory("mem-upd");
822        m2.body = "updated body".to_string();
823        m2.body_hash = "hash-novo".to_string();
824        let ok = update(&conn, id, &m2, None)?;
825        assert!(ok);
826
827        let row = read_full(&conn, id)?.ok_or("mem-upd should exist")?;
828        assert_eq!(row.body, "updated body");
829        assert_eq!(row.body_hash, "hash-novo");
830        Ok(())
831    }
832
833    #[test]
834    fn update_with_correct_expected_updated_at_succeeds() -> TestResult {
835        let conn = setup_conn()?;
836        let m = new_memory("mem-opt");
837        let id = insert(&conn, &m)?;
838
839        let (_, updated_at, _) =
840            find_by_name(&conn, "global", "mem-opt")?.ok_or("mem-opt should exist")?;
841
842        let mut m2 = new_memory("mem-opt");
843        m2.body = "optimistic body".to_string();
844        m2.body_hash = "hash-optimistic".to_string();
845        let ok = update(&conn, id, &m2, Some(updated_at))?;
846        assert!(ok);
847
848        let row = read_full(&conn, id)?.ok_or("mem-opt should exist after update")?;
849        assert_eq!(row.body, "optimistic body");
850        Ok(())
851    }
852
853    #[test]
854    fn update_with_wrong_expected_updated_at_returns_false() -> TestResult {
855        let conn = setup_conn()?;
856        let m = new_memory("mem-conflict");
857        let id = insert(&conn, &m)?;
858
859        let mut m2 = new_memory("mem-conflict");
860        m2.body = "must not appear".to_string();
861        m2.body_hash = "hash-x".to_string();
862        let ok = update(&conn, id, &m2, Some(0))?;
863        assert!(!ok);
864
865        let row = read_full(&conn, id)?.ok_or("mem-conflict should exist")?;
866        assert_eq!(row.body, "test memory body");
867        Ok(())
868    }
869
870    #[test]
871    fn update_missing_id_returns_false() -> TestResult {
872        let conn = setup_conn()?;
873        let m = new_memory("fantasma");
874        let ok = update(&conn, 9999, &m, None)?;
875        assert!(!ok);
876        Ok(())
877    }
878
879    #[test]
880    fn soft_delete_marks_deleted_at() -> TestResult {
881        let conn = setup_conn()?;
882        let m = new_memory("mem-del");
883        insert(&conn, &m)?;
884
885        let ok = soft_delete(&conn, "global", "mem-del")?;
886        assert!(ok);
887
888        let result = find_by_name(&conn, "global", "mem-del")?;
889        assert!(result.is_none());
890
891        let result_read = read_by_name(&conn, "global", "mem-del")?;
892        assert!(result_read.is_none());
893        Ok(())
894    }
895
896    #[test]
897    fn soft_delete_returns_false_when_not_found() -> TestResult {
898        let conn = setup_conn()?;
899        let ok = soft_delete(&conn, "global", "nao-existe")?;
900        assert!(!ok);
901        Ok(())
902    }
903
904    #[test]
905    fn double_soft_delete_returns_false_on_second_call() -> TestResult {
906        let conn = setup_conn()?;
907        let m = new_memory("mem-del2");
908        insert(&conn, &m)?;
909
910        soft_delete(&conn, "global", "mem-del2")?;
911        let ok = soft_delete(&conn, "global", "mem-del2")?;
912        assert!(!ok);
913        Ok(())
914    }
915
916    #[test]
917    fn list_returns_memories_from_namespace() -> TestResult {
918        let conn = setup_conn()?;
919        insert(&conn, &new_memory("mem-list-a"))?;
920        insert(&conn, &new_memory("mem-list-b"))?;
921
922        let rows = list(&conn, "global", None, 10, 0, false)?;
923        assert!(rows.len() >= 2);
924        let nomes: Vec<_> = rows.iter().map(|r| r.name.as_str()).collect();
925        assert!(nomes.contains(&"mem-list-a"));
926        assert!(nomes.contains(&"mem-list-b"));
927        Ok(())
928    }
929
930    #[test]
931    fn list_with_type_filter_returns_only_correct_type() -> TestResult {
932        let conn = setup_conn()?;
933        insert(&conn, &new_memory("mem-user"))?;
934
935        let mut m2 = new_memory("mem-feedback");
936        m2.memory_type = "feedback".to_string();
937        insert(&conn, &m2)?;
938
939        let rows_user = list(&conn, "global", Some("user"), 10, 0, false)?;
940        assert!(rows_user.iter().all(|r| r.memory_type == "user"));
941
942        let rows_fb = list(&conn, "global", Some("feedback"), 10, 0, false)?;
943        assert!(rows_fb.iter().all(|r| r.memory_type == "feedback"));
944        Ok(())
945    }
946
947    #[test]
948    fn list_exclui_soft_deleted() -> TestResult {
949        let conn = setup_conn()?;
950        let m = new_memory("mem-excluida");
951        insert(&conn, &m)?;
952        soft_delete(&conn, "global", "mem-excluida")?;
953
954        let rows = list(&conn, "global", None, 10, 0, false)?;
955        assert!(rows.iter().all(|r| r.name != "mem-excluida"));
956        Ok(())
957    }
958
959    #[test]
960    fn list_pagination_works() -> TestResult {
961        let conn = setup_conn()?;
962        for i in 0..5 {
963            insert(&conn, &new_memory(&format!("mem-pag-{i}")))?;
964        }
965
966        let pagina1 = list(&conn, "global", None, 2, 0, false)?;
967        let pagina2 = list(&conn, "global", None, 2, 2, false)?;
968        assert!(pagina1.len() <= 2);
969        assert!(pagina2.len() <= 2);
970        if !pagina1.is_empty() && !pagina2.is_empty() {
971            assert_ne!(pagina1[0].id, pagina2[0].id);
972        }
973        Ok(())
974    }
975
976    #[test]
977    fn upsert_vec_and_delete_vec_work() -> TestResult {
978        let conn = setup_conn()?;
979        let m = new_memory("mem-vec");
980        let id = insert(&conn, &m)?;
981
982        let embedding: Vec<f32> = vec![0.1; 384];
983        upsert_vec(
984            &conn, id, "global", "user", &embedding, "mem-vec", "snippet",
985        )?;
986
987        let count: i64 = conn.query_row(
988            "SELECT COUNT(*) FROM vec_memories WHERE memory_id = ?1",
989            params![id],
990            |r| r.get(0),
991        )?;
992        assert_eq!(count, 1);
993
994        delete_vec(&conn, id)?;
995
996        let count_after: i64 = conn.query_row(
997            "SELECT COUNT(*) FROM vec_memories WHERE memory_id = ?1",
998            params![id],
999            |r| r.get(0),
1000        )?;
1001        assert_eq!(count_after, 0);
1002        Ok(())
1003    }
1004
1005    #[test]
1006    fn upsert_vec_replaces_existing_vector() -> TestResult {
1007        let conn = setup_conn()?;
1008        let m = new_memory("mem-vec-upsert");
1009        let id = insert(&conn, &m)?;
1010
1011        let emb1: Vec<f32> = vec![0.1; 384];
1012        upsert_vec(&conn, id, "global", "user", &emb1, "mem-vec-upsert", "s1")?;
1013
1014        let emb2: Vec<f32> = vec![0.9; 384];
1015        upsert_vec(&conn, id, "global", "user", &emb2, "mem-vec-upsert", "s2")?;
1016
1017        let count: i64 = conn.query_row(
1018            "SELECT COUNT(*) FROM vec_memories WHERE memory_id = ?1",
1019            params![id],
1020            |r| r.get(0),
1021        )?;
1022        assert_eq!(count, 1);
1023        Ok(())
1024    }
1025
1026    #[test]
1027    fn knn_search_returns_results_by_distance() -> TestResult {
1028        let conn = setup_conn()?;
1029
1030        // emb_a: predominantemente positivo — cosseno alto com query [1.0; 384]
1031        let ma = new_memory("mem-knn-a");
1032        let id_a = insert(&conn, &ma)?;
1033        let emb_a: Vec<f32> = vec![1.0; 384];
1034        upsert_vec(&conn, id_a, "global", "user", &emb_a, "mem-knn-a", "s")?;
1035
1036        // emb_b: predominantemente negativo — cosseno baixo com query [1.0; 384]
1037        let mb = new_memory("mem-knn-b");
1038        let id_b = insert(&conn, &mb)?;
1039        let emb_b: Vec<f32> = vec![-1.0; 384];
1040        upsert_vec(&conn, id_b, "global", "user", &emb_b, "mem-knn-b", "s")?;
1041
1042        let query: Vec<f32> = vec![1.0; 384];
1043        let results = knn_search(&conn, &query, &["global".to_string()], None, 2)?;
1044        assert!(!results.is_empty());
1045        assert_eq!(results[0].0, id_a);
1046        Ok(())
1047    }
1048
1049    #[test]
1050    fn knn_search_with_type_filter_restricts_result() -> TestResult {
1051        let conn = setup_conn()?;
1052
1053        let ma = new_memory("mem-knn-tipo-user");
1054        let id_a = insert(&conn, &ma)?;
1055        let emb: Vec<f32> = vec![1.0; 384];
1056        upsert_vec(
1057            &conn,
1058            id_a,
1059            "global",
1060            "user",
1061            &emb,
1062            "mem-knn-tipo-user",
1063            "s",
1064        )?;
1065
1066        let mut mb = new_memory("mem-knn-tipo-fb");
1067        mb.memory_type = "feedback".to_string();
1068        let id_b = insert(&conn, &mb)?;
1069        upsert_vec(
1070            &conn,
1071            id_b,
1072            "global",
1073            "feedback",
1074            &emb,
1075            "mem-knn-tipo-fb",
1076            "s",
1077        )?;
1078
1079        let query: Vec<f32> = vec![1.0; 384];
1080        let results_user = knn_search(&conn, &query, &["global".to_string()], Some("user"), 5)?;
1081        assert!(results_user.iter().all(|(id, _)| *id == id_a));
1082
1083        let results_fb = knn_search(&conn, &query, &["global".to_string()], Some("feedback"), 5)?;
1084        assert!(results_fb.iter().all(|(id, _)| *id == id_b));
1085        Ok(())
1086    }
1087
1088    #[test]
1089    fn fts_search_finds_by_prefix_in_body() -> TestResult {
1090        let conn = setup_conn()?;
1091        let mut m = new_memory("mem-fts");
1092        m.body = "linguagem de programacao rust".to_string();
1093        insert(&conn, &m)?;
1094
1095        conn.execute_batch(
1096            "INSERT INTO fts_memories(rowid, name, description, body)
1097             SELECT id, name, description, body FROM memories WHERE deleted_at IS NULL",
1098        )?;
1099
1100        let rows = fts_search(&conn, "programacao", "global", None, 10)?;
1101        assert!(!rows.is_empty());
1102        assert!(rows.iter().any(|r| r.name == "mem-fts"));
1103        Ok(())
1104    }
1105
1106    #[test]
1107    fn fts_search_with_type_filter() -> TestResult {
1108        let conn = setup_conn()?;
1109        let mut m = new_memory("mem-fts-tipo");
1110        m.body = "linguagem especial para filtro".to_string();
1111        insert(&conn, &m)?;
1112
1113        let mut m2 = new_memory("mem-fts-feedback");
1114        m2.memory_type = "feedback".to_string();
1115        m2.body = "linguagem especial para filtro".to_string();
1116        insert(&conn, &m2)?;
1117
1118        conn.execute_batch(
1119            "INSERT INTO fts_memories(rowid, name, description, body)
1120             SELECT id, name, description, body FROM memories WHERE deleted_at IS NULL",
1121        )?;
1122
1123        let rows_user = fts_search(&conn, "especial", "global", Some("user"), 10)?;
1124        assert!(rows_user.iter().all(|r| r.memory_type == "user"));
1125
1126        let rows_fb = fts_search(&conn, "especial", "global", Some("feedback"), 10)?;
1127        assert!(rows_fb.iter().all(|r| r.memory_type == "feedback"));
1128        Ok(())
1129    }
1130
1131    #[test]
1132    fn fts_search_excludes_deleted() -> TestResult {
1133        let conn = setup_conn()?;
1134        let mut m = new_memory("mem-fts-del");
1135        m.body = "deleted fts content".to_string();
1136        insert(&conn, &m)?;
1137
1138        conn.execute_batch(
1139            "INSERT INTO fts_memories(rowid, name, description, body)
1140             SELECT id, name, description, body FROM memories WHERE deleted_at IS NULL",
1141        )?;
1142
1143        soft_delete(&conn, "global", "mem-fts-del")?;
1144
1145        let rows = fts_search(&conn, "deleted", "global", None, 10)?;
1146        assert!(rows.iter().all(|r| r.name != "mem-fts-del"));
1147        Ok(())
1148    }
1149
1150    #[test]
1151    fn list_deleted_before_returns_correct_ids() -> TestResult {
1152        let conn = setup_conn()?;
1153        let m = new_memory("mem-purge");
1154        insert(&conn, &m)?;
1155        soft_delete(&conn, "global", "mem-purge")?;
1156
1157        let ids = list_deleted_before(&conn, "global", i64::MAX)?;
1158        assert!(!ids.is_empty());
1159
1160        let ids_antes = list_deleted_before(&conn, "global", 0)?;
1161        assert!(ids_antes.is_empty());
1162        Ok(())
1163    }
1164
1165    #[test]
1166    fn find_by_name_returns_correct_max_version() -> TestResult {
1167        let conn = setup_conn()?;
1168        let m = new_memory("mem-ver");
1169        let id = insert(&conn, &m)?;
1170
1171        let (_, _, v0) = find_by_name(&conn, "global", "mem-ver")?.ok_or("mem-ver should exist")?;
1172        assert_eq!(v0, 0);
1173
1174        conn.execute(
1175            "INSERT INTO memory_versions (memory_id, version, name, type, description, body, metadata, change_reason)
1176             VALUES (?1, 1, 'mem-ver', 'user', 'desc', 'body', '{}', 'create')",
1177            params![id],
1178        )?;
1179
1180        let (_, _, v1) =
1181            find_by_name(&conn, "global", "mem-ver")?.ok_or("mem-ver should exist after insert")?;
1182        assert_eq!(v1, 1);
1183        Ok(())
1184    }
1185
1186    #[test]
1187    fn insert_com_metadata_json() -> TestResult {
1188        let conn = setup_conn()?;
1189        let mut m = new_memory("mem-meta");
1190        m.metadata = serde_json::json!({"chave": "valor", "numero": 42});
1191        let id = insert(&conn, &m)?;
1192
1193        let row = read_full(&conn, id)?.ok_or("mem-meta should exist")?;
1194        let meta: serde_json::Value = serde_json::from_str(&row.metadata)?;
1195        assert_eq!(meta["chave"], "valor");
1196        assert_eq!(meta["numero"], 42);
1197        Ok(())
1198    }
1199
1200    #[test]
1201    fn insert_com_session_id() -> TestResult {
1202        let conn = setup_conn()?;
1203        let mut m = new_memory("mem-session");
1204        m.session_id = Some("sessao-xyz".to_string());
1205        let id = insert(&conn, &m)?;
1206
1207        let row = read_full(&conn, id)?.ok_or("mem-session should exist")?;
1208        assert_eq!(row.session_id, Some("sessao-xyz".to_string()));
1209        Ok(())
1210    }
1211
1212    #[test]
1213    fn delete_vec_for_nonexistent_id_does_not_fail() -> TestResult {
1214        let conn = setup_conn()?;
1215        let result = delete_vec(&conn, 99999);
1216        assert!(result.is_ok());
1217        Ok(())
1218    }
1219}