Skip to main content

codemem_storage/
memory.rs

1//! Memory CRUD operations on Storage.
2
3use crate::{MemoryRow, Storage};
4use codemem_core::{CodememError, MemoryNode};
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
204#[cfg(test)]
205mod tests {
206    use crate::Storage;
207    use codemem_core::{CodememError, MemoryNode, MemoryType};
208    use std::collections::HashMap;
209
210    fn test_memory() -> MemoryNode {
211        let now = chrono::Utc::now();
212        let content = "Test memory content";
213        MemoryNode {
214            id: uuid::Uuid::new_v4().to_string(),
215            content: content.to_string(),
216            memory_type: MemoryType::Context,
217            importance: 0.7,
218            confidence: 1.0,
219            access_count: 0,
220            content_hash: Storage::content_hash(content),
221            tags: vec!["test".to_string()],
222            metadata: HashMap::new(),
223            namespace: None,
224            created_at: now,
225            updated_at: now,
226            last_accessed_at: now,
227        }
228    }
229
230    #[test]
231    fn insert_and_get_memory() {
232        let storage = Storage::open_in_memory().unwrap();
233        let memory = test_memory();
234        storage.insert_memory(&memory).unwrap();
235
236        let retrieved = storage.get_memory(&memory.id).unwrap().unwrap();
237        assert_eq!(retrieved.id, memory.id);
238        assert_eq!(retrieved.content, memory.content);
239        assert_eq!(retrieved.access_count, 1); // bumped on get
240    }
241
242    #[test]
243    fn dedup_by_content_hash() {
244        let storage = Storage::open_in_memory().unwrap();
245        let m1 = test_memory();
246        storage.insert_memory(&m1).unwrap();
247
248        let mut m2 = test_memory();
249        m2.id = uuid::Uuid::new_v4().to_string();
250        m2.content_hash = m1.content_hash.clone(); // same hash
251
252        assert!(matches!(
253            storage.insert_memory(&m2),
254            Err(CodememError::Duplicate(_))
255        ));
256    }
257
258    #[test]
259    fn delete_memory() {
260        let storage = Storage::open_in_memory().unwrap();
261        let memory = test_memory();
262        storage.insert_memory(&memory).unwrap();
263        assert!(storage.delete_memory(&memory.id).unwrap());
264        assert!(storage.get_memory(&memory.id).unwrap().is_none());
265    }
266}