Skip to main content

sqlite_graphrag/storage/memories/
crud.rs

1//! Single-row CRUD over `memories`: lookup, insert, update, read.
2//!
3//! Every statement that resolves or mutates ONE memory by name, hash or id.
4
5use super::rows::{MemoryRow, NewMemory};
6use crate::errors::AppError;
7use rusqlite::{params, Connection};
8
9/// Finds a live memory by `(namespace, name)` and returns key metadata.
10///
11/// # Arguments
12///
13/// - `conn` — open SQLite connection configured with the project pragmas.
14/// - `namespace` — resolved namespace for the lookup.
15/// - `name` — kebab-case memory name.
16///
17/// # Returns
18///
19/// `Ok(Some((id, updated_at, max_version)))` when the memory exists and is
20/// not soft-deleted, `Ok(None)` otherwise.
21///
22/// # Errors
23///
24/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
25pub fn find_by_name(
26    conn: &Connection,
27    namespace: &str,
28    name: &str,
29) -> Result<Option<(i64, i64, i64)>, AppError> {
30    let mut stmt = conn.prepare_cached(
31        "SELECT m.id, m.updated_at, COALESCE(MAX(v.version), 0)
32         FROM memories m
33         LEFT JOIN memory_versions v ON v.memory_id = m.id
34         WHERE m.namespace = ?1 AND m.name = ?2 AND m.deleted_at IS NULL
35         GROUP BY m.id",
36    )?;
37    let result = stmt.query_row(params![namespace, name], |r| {
38        Ok((
39            r.get::<_, i64>(0)?,
40            r.get::<_, i64>(1)?,
41            r.get::<_, i64>(2)?,
42        ))
43    });
44    match result {
45        Ok(row) => Ok(Some(row)),
46        Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
47        Err(e) => Err(AppError::Database(e)),
48    }
49}
50
51/// Looks up a live memory by exact `body_hash` within a namespace.
52///
53/// Used during `remember` to short-circuit semantic duplicates before
54/// spending an embedding call.
55///
56/// # Returns
57///
58/// `Ok(Some(id))` when a live memory with the same hash exists,
59/// `Ok(None)` otherwise.
60///
61/// # Errors
62///
63/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
64pub fn find_by_hash(
65    conn: &Connection,
66    namespace: &str,
67    body_hash: &str,
68) -> Result<Option<i64>, AppError> {
69    let mut stmt = conn.prepare_cached(
70        "SELECT id FROM memories WHERE namespace = ?1 AND body_hash = ?2 AND deleted_at IS NULL",
71    )?;
72    match stmt.query_row(params![namespace, body_hash], |r| r.get(0)) {
73        Ok(id) => Ok(Some(id)),
74        Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
75        Err(e) => Err(AppError::Database(e)),
76    }
77}
78
79/// Inserts a new row into the `memories` table.
80///
81/// # Arguments
82///
83/// - `conn` — active SQLite connection, typically inside a transaction.
84/// - `m` — validated payload including `body_hash` and serialized metadata.
85///
86/// # Returns
87///
88/// The `rowid` assigned to the newly inserted memory.
89///
90/// # Errors
91///
92/// Returns `Err(AppError::Database)` on insertion failure and
93/// `Err(AppError::Json)` if metadata serialization fails.
94pub fn insert(conn: &Connection, m: &NewMemory) -> Result<i64, AppError> {
95    // G29 Passo 2 (v1.0.69): runtime guard for the CHECK constraint on
96    // `source`. Even though `MemorySource` is the typed future, every
97    // legacy `NewMemory { source: "..." }` literal still flows through
98    // this function; validating here keeps the footgun from regressing
99    // for callers that have not yet migrated to the enum.
100    let validated_source = crate::memory_source::validate_source(&m.source)?;
101    conn.execute(
102        "INSERT INTO memories (namespace, name, type, description, body, body_hash, session_id, source, metadata)
103         VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
104        params![
105            m.namespace, m.name, m.memory_type, m.description, m.body,
106            m.body_hash, m.session_id, validated_source,
107            serde_json::to_string(&m.metadata)?
108        ],
109    )?;
110    Ok(conn.last_insert_rowid())
111}
112
113/// Updates an existing memory optionally guarded by optimistic concurrency.
114///
115/// When `expected_updated_at` is `Some(ts)` the row is only updated if its
116/// current `updated_at` equals `ts`. This protects concurrent `edit` calls
117/// from silently clobbering each other.
118///
119/// # Returns
120///
121/// `Ok(true)` when exactly one row was updated, `Ok(false)` when the
122/// optimistic check failed or the memory does not exist.
123///
124/// # Errors
125///
126/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
127pub fn update(
128    conn: &Connection,
129    id: i64,
130    m: &NewMemory,
131    expected_updated_at: Option<i64>,
132) -> Result<bool, AppError> {
133    // G29 Passo 2 (v1.0.69): runtime guard for the CHECK constraint on
134    // `source`. Mirrors `insert` so `body-enrich` and other mutations
135    // cannot reintroduce the historical "enrich" literal that broke
136    // `body-enrich` in v1.0.55 - v1.0.68.
137    let validated_source = crate::memory_source::validate_source(&m.source)?;
138    let affected = if let Some(ts) = expected_updated_at {
139        conn.execute(
140            "UPDATE memories SET type=?2, description=?3, body=?4, body_hash=?5,
141             session_id=?6, source=?7, metadata=?8
142             WHERE id=?1 AND updated_at=?9 AND deleted_at IS NULL",
143            params![
144                id,
145                m.memory_type,
146                m.description,
147                m.body,
148                m.body_hash,
149                m.session_id,
150                validated_source,
151                serde_json::to_string(&m.metadata)?,
152                ts
153            ],
154        )?
155    } else {
156        conn.execute(
157            "UPDATE memories SET type=?2, description=?3, body=?4, body_hash=?5,
158             session_id=?6, source=?7, metadata=?8
159             WHERE id=?1 AND deleted_at IS NULL",
160            params![
161                id,
162                m.memory_type,
163                m.description,
164                m.body,
165                m.body_hash,
166                m.session_id,
167                validated_source,
168                serde_json::to_string(&m.metadata)?
169            ],
170        )?
171    };
172    Ok(affected == 1)
173}
174
175/// Fetches a live memory by `(namespace, name)` and returns all columns.
176///
177/// # Returns
178///
179/// `Ok(Some(row))` when found, `Ok(None)` when missing or soft-deleted.
180///
181/// # Errors
182///
183/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
184pub fn read_by_name(
185    conn: &Connection,
186    namespace: &str,
187    name: &str,
188) -> Result<Option<MemoryRow>, AppError> {
189    let mut stmt = conn.prepare_cached(
190        "SELECT id, namespace, name, type, description, body, body_hash,
191                session_id, source, metadata, created_at, updated_at, deleted_at
192         FROM memories WHERE namespace=?1 AND name=?2 AND deleted_at IS NULL",
193    )?;
194    match stmt.query_row(params![namespace, name], |r| {
195        Ok(MemoryRow {
196            id: r.get(0)?,
197            namespace: r.get(1)?,
198            name: r.get(2)?,
199            memory_type: r.get(3)?,
200            description: r.get(4)?,
201            body: r.get(5)?,
202            body_hash: r.get(6)?,
203            session_id: r.get(7)?,
204            source: r.get(8)?,
205            metadata: r.get(9)?,
206            created_at: r.get(10)?,
207            updated_at: r.get(11)?,
208            deleted_at: r.get(12)?,
209        })
210    }) {
211        Ok(m) => Ok(Some(m)),
212        Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
213        Err(e) => Err(AppError::Database(e)),
214    }
215}
216
217/// Fetches a live memory by `(namespace, name)` and returns all columns.
218/// Fetches a live memory by primary key and returns all columns.
219///
220/// Mirrors [`read_by_name`] but keyed on `rowid` for use after a KNN search.
221///
222/// # Errors
223///
224/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
225pub fn read_full(conn: &Connection, memory_id: i64) -> Result<Option<MemoryRow>, AppError> {
226    let mut stmt = conn.prepare_cached(
227        "SELECT id, namespace, name, type, description, body, body_hash,
228                session_id, source, metadata, created_at, updated_at, deleted_at
229         FROM memories WHERE id=?1 AND deleted_at IS NULL",
230    )?;
231    match stmt.query_row(params![memory_id], |r| {
232        Ok(MemoryRow {
233            id: r.get(0)?,
234            namespace: r.get(1)?,
235            name: r.get(2)?,
236            memory_type: r.get(3)?,
237            description: r.get(4)?,
238            body: r.get(5)?,
239            body_hash: r.get(6)?,
240            session_id: r.get(7)?,
241            source: r.get(8)?,
242            metadata: r.get(9)?,
243            created_at: r.get(10)?,
244            updated_at: r.get(11)?,
245            deleted_at: r.get(12)?,
246        })
247    }) {
248        Ok(m) => Ok(Some(m)),
249        Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
250        Err(e) => Err(AppError::Database(e)),
251    }
252}