sqlite_graphrag/storage/memories/soft_delete.rs
1//! Soft-delete lifecycle helpers for the `memories` table.
2//!
3//! Soft-delete sets `deleted_at = unixepoch()` without removing versions or
4//! chunks, so `restore` can undo until `purge` reclaims storage permanently.
5
6use crate::errors::AppError;
7use rusqlite::{params, Connection};
8
9/// Looks up a memory by `(namespace, name)` regardless of deletion state.
10///
11/// Returns `Some((id, is_deleted))` when the row exists.
12/// `is_deleted` is `true` when `deleted_at IS NOT NULL`.
13///
14/// # Errors
15///
16/// Propagates [`AppError::Database`] on SQLite failures.
17pub fn find_by_name_any_state(
18 conn: &Connection,
19 namespace: &str,
20 name: &str,
21) -> Result<Option<(i64, bool)>, AppError> {
22 let mut stmt = conn.prepare_cached(
23 "SELECT id, (deleted_at IS NOT NULL) AS is_deleted
24 FROM memories WHERE namespace = ?1 AND name = ?2",
25 )?;
26 let result = stmt.query_row(params![namespace, name], |r| {
27 Ok((r.get::<_, i64>(0)?, r.get::<_, bool>(1)?))
28 });
29 match result {
30 Ok(row) => Ok(Some(row)),
31 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
32 Err(e) => Err(AppError::Database(e)),
33 }
34}
35
36/// Clears `deleted_at` to restore a soft-deleted memory.
37///
38/// # Errors
39///
40/// Propagates [`AppError::Database`] on SQLite failures.
41pub fn clear_deleted_at(conn: &Connection, memory_id: i64) -> Result<(), AppError> {
42 conn.execute(
43 "UPDATE memories SET deleted_at = NULL WHERE id = ?1",
44 params![memory_id],
45 )?;
46 Ok(())
47}
48
49/// Soft-deletes a memory by setting `deleted_at = unixepoch()`.
50///
51/// Versions and chunks are preserved so `restore` can undo the operation
52/// until a subsequent `purge` reclaims the storage permanently.
53///
54/// # Returns
55///
56/// `Ok(true)` when a live memory was soft-deleted, `Ok(false)` when no
57/// matching live row existed.
58///
59/// # Errors
60///
61/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
62pub fn soft_delete(conn: &Connection, namespace: &str, name: &str) -> Result<bool, AppError> {
63 let affected = conn.execute(
64 "UPDATE memories SET deleted_at = unixepoch() WHERE namespace=?1 AND name=?2 AND deleted_at IS NULL",
65 params![namespace, name],
66 )?;
67 Ok(affected == 1)
68}
69
70/// Fetches all memory_ids in a namespace that are soft-deleted and whose
71/// `deleted_at` is older than `before_ts` (unix epoch seconds).
72///
73/// Used by `purge` to collect stale rows for permanent deletion.
74///
75/// # Errors
76///
77/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
78pub fn list_deleted_before(
79 conn: &Connection,
80 namespace: &str,
81 before_ts: i64,
82) -> Result<Vec<i64>, AppError> {
83 let mut stmt = conn.prepare_cached(
84 "SELECT id FROM memories WHERE namespace = ?1 AND deleted_at IS NOT NULL AND deleted_at < ?2",
85 )?;
86 let ids = stmt
87 .query_map(params![namespace, before_ts], |r| r.get::<_, i64>(0))?
88 .collect::<Result<Vec<_>, _>>()?;
89 Ok(ids)
90}