Skip to main content

codemem_storage/
memory.rs

1//! Memory CRUD operations on Storage.
2
3use crate::{MemoryRow, Storage};
4use codemem_core::{CodememError, MemoryNode, Repository};
5use rusqlite::{params, OptionalExtension};
6
7impl Storage {
8    /// Insert a new memory. Returns Err(Duplicate) if content hash already exists.
9    pub fn insert_memory(&self, memory: &MemoryNode) -> Result<(), CodememError> {
10        let conn = self.conn();
11
12        // Check dedup
13        let existing: Option<String> = conn
14            .query_row(
15                "SELECT id FROM memories WHERE content_hash = ?1",
16                params![memory.content_hash],
17                |row| row.get(0),
18            )
19            .optional()
20            .map_err(|e| CodememError::Storage(e.to_string()))?;
21
22        if let Some(_existing_id) = existing {
23            return Err(CodememError::Duplicate(memory.content_hash.clone()));
24        }
25
26        let tags_json = serde_json::to_string(&memory.tags)?;
27        let metadata_json = serde_json::to_string(&memory.metadata)?;
28
29        conn.execute(
30            "INSERT INTO memories (id, content, memory_type, importance, confidence, access_count, content_hash, tags, metadata, namespace, created_at, updated_at, last_accessed_at)
31             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)",
32            params![
33                memory.id,
34                memory.content,
35                memory.memory_type.to_string(),
36                memory.importance,
37                memory.confidence,
38                memory.access_count,
39                memory.content_hash,
40                tags_json,
41                metadata_json,
42                memory.namespace,
43                memory.created_at.timestamp(),
44                memory.updated_at.timestamp(),
45                memory.last_accessed_at.timestamp(),
46            ],
47        )
48        .map_err(|e| CodememError::Storage(e.to_string()))?;
49
50        Ok(())
51    }
52
53    /// Get a memory by ID. Updates access_count and last_accessed_at.
54    pub fn get_memory(&self, id: &str) -> Result<Option<MemoryNode>, CodememError> {
55        let conn = self.conn();
56
57        // Bump access count first
58        let updated = conn
59            .execute(
60                "UPDATE memories SET access_count = access_count + 1, last_accessed_at = ?1 WHERE id = ?2",
61                params![chrono::Utc::now().timestamp(), id],
62            )
63            .map_err(|e| CodememError::Storage(e.to_string()))?;
64
65        if updated == 0 {
66            return Ok(None);
67        }
68
69        let result = conn
70            .query_row(
71                "SELECT id, content, memory_type, importance, confidence, access_count, content_hash, tags, metadata, namespace, created_at, updated_at, last_accessed_at FROM memories WHERE id = ?1",
72                params![id],
73                |row| {
74                    Ok(MemoryRow {
75                        id: row.get(0)?,
76                        content: row.get(1)?,
77                        memory_type: row.get(2)?,
78                        importance: row.get(3)?,
79                        confidence: row.get(4)?,
80                        access_count: row.get(5)?,
81                        content_hash: row.get(6)?,
82                        tags: row.get(7)?,
83                        metadata: row.get(8)?,
84                        namespace: row.get(9)?,
85                        created_at: row.get(10)?,
86                        updated_at: row.get(11)?,
87                        last_accessed_at: row.get(12)?,
88                    })
89                },
90            )
91            .optional()
92            .map_err(|e| CodememError::Storage(e.to_string()))?;
93
94        match result {
95            Some(row) => Ok(Some(row.into_memory_node()?)),
96            None => Ok(None),
97        }
98    }
99
100    /// Update a memory's content and re-hash.
101    pub fn update_memory(
102        &self,
103        id: &str,
104        content: &str,
105        importance: Option<f64>,
106    ) -> Result<(), CodememError> {
107        let conn = self.conn();
108        let hash = Self::content_hash(content);
109        let now = chrono::Utc::now().timestamp();
110
111        if let Some(imp) = importance {
112            conn.execute(
113                "UPDATE memories SET content = ?1, content_hash = ?2, updated_at = ?3, importance = ?4 WHERE id = ?5",
114                params![content, hash, now, imp, id],
115            )
116            .map_err(|e| CodememError::Storage(e.to_string()))?;
117        } else {
118            conn.execute(
119                "UPDATE memories SET content = ?1, content_hash = ?2, updated_at = ?3 WHERE id = ?4",
120                params![content, hash, now, id],
121            )
122            .map_err(|e| CodememError::Storage(e.to_string()))?;
123        }
124
125        Ok(())
126    }
127
128    /// Delete a memory by ID.
129    pub fn delete_memory(&self, id: &str) -> Result<bool, CodememError> {
130        let conn = self.conn();
131        let rows = conn
132            .execute("DELETE FROM memories WHERE id = ?1", params![id])
133            .map_err(|e| CodememError::Storage(e.to_string()))?;
134        Ok(rows > 0)
135    }
136
137    /// List all memory IDs.
138    pub fn list_memory_ids(&self) -> Result<Vec<String>, CodememError> {
139        let conn = self.conn();
140        let mut stmt = conn
141            .prepare("SELECT id FROM memories ORDER BY created_at DESC")
142            .map_err(|e| CodememError::Storage(e.to_string()))?;
143
144        let ids = stmt
145            .query_map([], |row| row.get(0))
146            .map_err(|e| CodememError::Storage(e.to_string()))?
147            .collect::<Result<Vec<String>, _>>()
148            .map_err(|e| CodememError::Storage(e.to_string()))?;
149
150        Ok(ids)
151    }
152
153    /// List memory IDs scoped to a specific namespace.
154    pub fn list_memory_ids_for_namespace(
155        &self,
156        namespace: &str,
157    ) -> Result<Vec<String>, CodememError> {
158        let conn = self.conn();
159        let mut stmt = conn
160            .prepare("SELECT id FROM memories WHERE namespace = ?1 ORDER BY created_at DESC")
161            .map_err(|e| CodememError::Storage(e.to_string()))?;
162
163        let ids = stmt
164            .query_map(params![namespace], |row| row.get(0))
165            .map_err(|e| CodememError::Storage(e.to_string()))?
166            .collect::<Result<Vec<String>, _>>()
167            .map_err(|e| CodememError::Storage(e.to_string()))?;
168
169        Ok(ids)
170    }
171
172    /// List all distinct namespaces.
173    pub fn list_namespaces(&self) -> Result<Vec<String>, CodememError> {
174        let conn = self.conn();
175        let mut stmt = conn
176            .prepare(
177                "SELECT DISTINCT namespace FROM (
178                    SELECT namespace FROM memories WHERE namespace IS NOT NULL
179                    UNION
180                    SELECT namespace FROM graph_nodes WHERE namespace IS NOT NULL
181                ) ORDER BY namespace",
182            )
183            .map_err(|e| CodememError::Storage(e.to_string()))?;
184
185        let namespaces = stmt
186            .query_map([], |row| row.get(0))
187            .map_err(|e| CodememError::Storage(e.to_string()))?
188            .collect::<Result<Vec<String>, _>>()
189            .map_err(|e| CodememError::Storage(e.to_string()))?;
190
191        Ok(namespaces)
192    }
193
194    /// Get memory count.
195    pub fn memory_count(&self) -> Result<usize, CodememError> {
196        let conn = self.conn();
197        let count: i64 = conn
198            .query_row("SELECT COUNT(*) FROM memories", [], |row| row.get(0))
199            .map_err(|e| CodememError::Storage(e.to_string()))?;
200        Ok(count as usize)
201    }
202
203    // ── Repository CRUD ─────────────────────────────────────────────────────
204
205    /// List all registered repositories.
206    pub fn list_repos(&self) -> Result<Vec<Repository>, CodememError> {
207        let conn = self.conn();
208        let mut stmt = conn
209            .prepare(
210                "SELECT id, path, name, namespace, created_at, last_indexed_at, status FROM repositories ORDER BY created_at DESC",
211            )
212            .map_err(|e| CodememError::Storage(e.to_string()))?;
213
214        let repos = stmt
215            .query_map([], |row| {
216                Ok(Repository {
217                    id: row.get(0)?,
218                    path: row.get(1)?,
219                    name: row.get(2)?,
220                    namespace: row.get(3)?,
221                    created_at: row.get(4)?,
222                    last_indexed_at: row.get(5)?,
223                    status: row
224                        .get::<_, Option<String>>(6)?
225                        .unwrap_or_else(|| "idle".to_string()),
226                })
227            })
228            .map_err(|e| CodememError::Storage(e.to_string()))?
229            .collect::<Result<Vec<Repository>, _>>()
230            .map_err(|e| CodememError::Storage(e.to_string()))?;
231
232        Ok(repos)
233    }
234
235    /// Add a new repository.
236    pub fn add_repo(&self, repo: &Repository) -> Result<(), CodememError> {
237        let conn = self.conn();
238        conn.execute(
239            "INSERT INTO repositories (id, path, name, namespace, created_at, last_indexed_at, status) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
240            params![
241                repo.id,
242                repo.path,
243                repo.name,
244                repo.namespace,
245                repo.created_at,
246                repo.last_indexed_at,
247                repo.status,
248            ],
249        )
250        .map_err(|e| CodememError::Storage(e.to_string()))?;
251        Ok(())
252    }
253
254    /// Remove a repository by ID.
255    pub fn remove_repo(&self, id: &str) -> Result<bool, CodememError> {
256        let conn = self.conn();
257        let rows = conn
258            .execute("DELETE FROM repositories WHERE id = ?1", params![id])
259            .map_err(|e| CodememError::Storage(e.to_string()))?;
260        Ok(rows > 0)
261    }
262
263    /// Get a repository by ID.
264    pub fn get_repo(&self, id: &str) -> Result<Option<Repository>, CodememError> {
265        let conn = self.conn();
266        let result = conn
267            .query_row(
268                "SELECT id, path, name, namespace, created_at, last_indexed_at, status FROM repositories WHERE id = ?1",
269                params![id],
270                |row| {
271                    Ok(Repository {
272                        id: row.get(0)?,
273                        path: row.get(1)?,
274                        name: row.get(2)?,
275                        namespace: row.get(3)?,
276                        created_at: row.get(4)?,
277                        last_indexed_at: row.get(5)?,
278                        status: row.get::<_, Option<String>>(6)?.unwrap_or_else(|| "idle".to_string()),
279                    })
280                },
281            )
282            .optional()
283            .map_err(|e| CodememError::Storage(e.to_string()))?;
284        Ok(result)
285    }
286
287    /// Update a repository's status and optionally last_indexed_at.
288    pub fn update_repo_status(
289        &self,
290        id: &str,
291        status: &str,
292        indexed_at: Option<&str>,
293    ) -> Result<(), CodememError> {
294        let conn = self.conn();
295        if let Some(ts) = indexed_at {
296            conn.execute(
297                "UPDATE repositories SET status = ?1, last_indexed_at = ?2 WHERE id = ?3",
298                params![status, ts, id],
299            )
300            .map_err(|e| CodememError::Storage(e.to_string()))?;
301        } else {
302            conn.execute(
303                "UPDATE repositories SET status = ?1 WHERE id = ?2",
304                params![status, id],
305            )
306            .map_err(|e| CodememError::Storage(e.to_string()))?;
307        }
308        Ok(())
309    }
310}
311
312#[cfg(test)]
313#[path = "tests/memory_tests.rs"]
314mod tests;