Skip to main content

codemem_storage/
backend.rs

1//! `StorageBackend` trait implementation for Storage.
2
3use crate::{MemoryRow, Storage};
4use codemem_core::{
5    CodememError, ConsolidationLogEntry, Edge, GraphNode, MemoryNode, NodeKind, Session,
6    StorageBackend, StorageStats,
7};
8use rusqlite::params;
9use std::collections::HashMap;
10
11impl StorageBackend for Storage {
12    fn insert_memory(&self, memory: &MemoryNode) -> Result<(), CodememError> {
13        Storage::insert_memory(self, memory)
14    }
15
16    fn get_memory(&self, id: &str) -> Result<Option<MemoryNode>, CodememError> {
17        Storage::get_memory(self, id)
18    }
19
20    fn get_memories_batch(&self, ids: &[&str]) -> Result<Vec<MemoryNode>, CodememError> {
21        if ids.is_empty() {
22            return Ok(Vec::new());
23        }
24        let conn = self.conn();
25
26        let placeholders: Vec<String> = (1..=ids.len()).map(|i| format!("?{i}")).collect();
27        let sql = format!(
28            "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 IN ({})",
29            placeholders.join(",")
30        );
31
32        let mut stmt = conn
33            .prepare(&sql)
34            .map_err(|e| CodememError::Storage(e.to_string()))?;
35
36        let params: Vec<&dyn rusqlite::types::ToSql> = ids
37            .iter()
38            .map(|id| id as &dyn rusqlite::types::ToSql)
39            .collect();
40
41        let rows = stmt
42            .query_map(params.as_slice(), |row| {
43                Ok(MemoryRow {
44                    id: row.get(0)?,
45                    content: row.get(1)?,
46                    memory_type: row.get(2)?,
47                    importance: row.get(3)?,
48                    confidence: row.get(4)?,
49                    access_count: row.get(5)?,
50                    content_hash: row.get(6)?,
51                    tags: row.get(7)?,
52                    metadata: row.get(8)?,
53                    namespace: row.get(9)?,
54                    created_at: row.get(10)?,
55                    updated_at: row.get(11)?,
56                    last_accessed_at: row.get(12)?,
57                })
58            })
59            .map_err(|e| CodememError::Storage(e.to_string()))?;
60
61        let mut memories = Vec::new();
62        for row in rows {
63            let row = row.map_err(|e| CodememError::Storage(e.to_string()))?;
64            memories.push(row.into_memory_node()?);
65        }
66        Ok(memories)
67    }
68
69    fn update_memory(
70        &self,
71        id: &str,
72        content: &str,
73        importance: Option<f64>,
74    ) -> Result<(), CodememError> {
75        Storage::update_memory(self, id, content, importance)
76    }
77
78    fn delete_memory(&self, id: &str) -> Result<bool, CodememError> {
79        Storage::delete_memory(self, id)
80    }
81
82    fn list_memory_ids(&self) -> Result<Vec<String>, CodememError> {
83        Storage::list_memory_ids(self)
84    }
85
86    fn list_memory_ids_for_namespace(&self, namespace: &str) -> Result<Vec<String>, CodememError> {
87        Storage::list_memory_ids_for_namespace(self, namespace)
88    }
89
90    fn list_namespaces(&self) -> Result<Vec<String>, CodememError> {
91        Storage::list_namespaces(self)
92    }
93
94    fn memory_count(&self) -> Result<usize, CodememError> {
95        Storage::memory_count(self)
96    }
97
98    fn store_embedding(&self, memory_id: &str, embedding: &[f32]) -> Result<(), CodememError> {
99        Storage::store_embedding(self, memory_id, embedding)
100    }
101
102    fn get_embedding(&self, memory_id: &str) -> Result<Option<Vec<f32>>, CodememError> {
103        Storage::get_embedding(self, memory_id)
104    }
105
106    fn delete_embedding(&self, memory_id: &str) -> Result<bool, CodememError> {
107        let conn = self.conn();
108        let deleted = conn
109            .execute(
110                "DELETE FROM memory_embeddings WHERE memory_id = ?1",
111                [memory_id],
112            )
113            .map_err(|e| CodememError::Storage(e.to_string()))?;
114        Ok(deleted > 0)
115    }
116
117    fn list_all_embeddings(&self) -> Result<Vec<(String, Vec<f32>)>, CodememError> {
118        let conn = self.conn();
119        let mut stmt = conn
120            .prepare("SELECT memory_id, embedding FROM memory_embeddings")
121            .map_err(|e| CodememError::Storage(e.to_string()))?;
122        let rows = stmt
123            .query_map([], |row| {
124                let id: String = row.get(0)?;
125                let blob: Vec<u8> = row.get(1)?;
126                Ok((id, blob))
127            })
128            .map_err(|e| CodememError::Storage(e.to_string()))?;
129        let mut result = Vec::new();
130        for row in rows {
131            let (id, blob) = row.map_err(|e| CodememError::Storage(e.to_string()))?;
132            let floats: Vec<f32> = blob
133                .chunks_exact(4)
134                .map(|chunk| f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
135                .collect();
136            result.push((id, floats));
137        }
138        Ok(result)
139    }
140
141    fn insert_graph_node(&self, node: &GraphNode) -> Result<(), CodememError> {
142        Storage::insert_graph_node(self, node)
143    }
144
145    fn get_graph_node(&self, id: &str) -> Result<Option<GraphNode>, CodememError> {
146        Storage::get_graph_node(self, id)
147    }
148
149    fn delete_graph_node(&self, id: &str) -> Result<bool, CodememError> {
150        Storage::delete_graph_node(self, id)
151    }
152
153    fn all_graph_nodes(&self) -> Result<Vec<GraphNode>, CodememError> {
154        Storage::all_graph_nodes(self)
155    }
156
157    fn insert_graph_edge(&self, edge: &Edge) -> Result<(), CodememError> {
158        Storage::insert_graph_edge(self, edge)
159    }
160
161    fn get_edges_for_node(&self, node_id: &str) -> Result<Vec<Edge>, CodememError> {
162        Storage::get_edges_for_node(self, node_id)
163    }
164
165    fn all_graph_edges(&self) -> Result<Vec<Edge>, CodememError> {
166        Storage::all_graph_edges(self)
167    }
168
169    fn delete_graph_edges_for_node(&self, node_id: &str) -> Result<usize, CodememError> {
170        Storage::delete_graph_edges_for_node(self, node_id)
171    }
172
173    fn start_session(&self, id: &str, namespace: Option<&str>) -> Result<(), CodememError> {
174        Storage::start_session(self, id, namespace)
175    }
176
177    fn end_session(&self, id: &str, summary: Option<&str>) -> Result<(), CodememError> {
178        Storage::end_session(self, id, summary)
179    }
180
181    fn list_sessions(
182        &self,
183        namespace: Option<&str>,
184        limit: usize,
185    ) -> Result<Vec<Session>, CodememError> {
186        self.list_sessions_with_limit(namespace, limit)
187    }
188
189    fn insert_consolidation_log(
190        &self,
191        cycle_type: &str,
192        affected_count: usize,
193    ) -> Result<(), CodememError> {
194        Storage::insert_consolidation_log(self, cycle_type, affected_count)
195    }
196
197    fn last_consolidation_runs(&self) -> Result<Vec<ConsolidationLogEntry>, CodememError> {
198        Storage::last_consolidation_runs(self)
199    }
200
201    fn get_repeated_searches(
202        &self,
203        min_count: usize,
204        namespace: Option<&str>,
205    ) -> Result<Vec<(String, usize, Vec<String>)>, CodememError> {
206        Storage::get_repeated_searches(self, min_count, namespace)
207    }
208
209    fn get_file_hotspots(
210        &self,
211        min_count: usize,
212        namespace: Option<&str>,
213    ) -> Result<Vec<(String, usize, Vec<String>)>, CodememError> {
214        Storage::get_file_hotspots(self, min_count, namespace)
215    }
216
217    fn get_tool_usage_stats(
218        &self,
219        namespace: Option<&str>,
220    ) -> Result<Vec<(String, usize)>, CodememError> {
221        let map = Storage::get_tool_usage_stats(self, namespace)?;
222        let mut vec: Vec<(String, usize)> = map.into_iter().collect();
223        vec.sort_by(|a, b| b.1.cmp(&a.1));
224        Ok(vec)
225    }
226
227    fn get_decision_chains(
228        &self,
229        min_count: usize,
230        namespace: Option<&str>,
231    ) -> Result<Vec<(String, usize, Vec<String>)>, CodememError> {
232        Storage::get_decision_chains(self, min_count, namespace)
233    }
234
235    fn decay_stale_memories(
236        &self,
237        threshold_ts: i64,
238        decay_factor: f64,
239    ) -> Result<usize, CodememError> {
240        let conn = self.conn();
241        let rows = conn
242            .execute(
243                "UPDATE memories SET importance = importance * ?1 WHERE last_accessed_at < ?2",
244                params![decay_factor, threshold_ts],
245            )
246            .map_err(|e| CodememError::Storage(e.to_string()))?;
247        Ok(rows)
248    }
249
250    fn list_memories_for_creative(
251        &self,
252    ) -> Result<Vec<(String, String, Vec<String>)>, CodememError> {
253        let conn = self.conn();
254        let mut stmt = conn
255            .prepare("SELECT id, memory_type, tags FROM memories ORDER BY created_at DESC")
256            .map_err(|e| CodememError::Storage(e.to_string()))?;
257
258        let rows = stmt
259            .query_map([], |row| {
260                Ok((
261                    row.get::<_, String>(0)?,
262                    row.get::<_, String>(1)?,
263                    row.get::<_, String>(2)?,
264                ))
265            })
266            .map_err(|e| CodememError::Storage(e.to_string()))?
267            .collect::<Result<Vec<_>, _>>()
268            .map_err(|e| CodememError::Storage(e.to_string()))?;
269
270        Ok(rows
271            .into_iter()
272            .map(|(id, mtype, tags_json)| {
273                let tags: Vec<String> = serde_json::from_str(&tags_json).unwrap_or_default();
274                (id, mtype, tags)
275            })
276            .collect())
277    }
278
279    fn find_cluster_duplicates(&self) -> Result<Vec<(String, String, f64)>, CodememError> {
280        let conn = self.conn();
281        let mut stmt = conn
282            .prepare(
283                "SELECT a.id, b.id, 1.0 as similarity
284                 FROM memories a
285                 INNER JOIN memories b ON substr(a.content_hash, 1, 16) = substr(b.content_hash, 1, 16)
286                 WHERE a.id < b.id",
287            )
288            .map_err(|e| CodememError::Storage(e.to_string()))?;
289
290        let rows = stmt
291            .query_map([], |row| {
292                Ok((
293                    row.get::<_, String>(0)?,
294                    row.get::<_, String>(1)?,
295                    row.get::<_, f64>(2)?,
296                ))
297            })
298            .map_err(|e| CodememError::Storage(e.to_string()))?
299            .collect::<Result<Vec<_>, _>>()
300            .map_err(|e| CodememError::Storage(e.to_string()))?;
301
302        Ok(rows)
303    }
304
305    fn find_forgettable(&self, importance_threshold: f64) -> Result<Vec<String>, CodememError> {
306        let conn = self.conn();
307        let mut stmt = conn
308            .prepare(
309                "SELECT id FROM memories WHERE importance < ?1 AND access_count = 0 ORDER BY importance ASC, last_accessed_at ASC",
310            )
311            .map_err(|e| CodememError::Storage(e.to_string()))?;
312
313        let ids = stmt
314            .query_map(params![importance_threshold], |row| row.get(0))
315            .map_err(|e| CodememError::Storage(e.to_string()))?
316            .collect::<Result<Vec<String>, _>>()
317            .map_err(|e| CodememError::Storage(e.to_string()))?;
318
319        Ok(ids)
320    }
321
322    fn insert_memories_batch(&self, memories: &[MemoryNode]) -> Result<(), CodememError> {
323        let conn = self.conn();
324        let tx = conn
325            .unchecked_transaction()
326            .map_err(|e| CodememError::Storage(e.to_string()))?;
327
328        for memory in memories {
329            let tags_json = serde_json::to_string(&memory.tags)?;
330            let metadata_json = serde_json::to_string(&memory.metadata)?;
331
332            tx.execute(
333                "INSERT OR IGNORE INTO memories (id, content, memory_type, importance, confidence, access_count, content_hash, tags, metadata, namespace, created_at, updated_at, last_accessed_at)
334                 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)",
335                params![
336                    memory.id,
337                    memory.content,
338                    memory.memory_type.to_string(),
339                    memory.importance,
340                    memory.confidence,
341                    memory.access_count,
342                    memory.content_hash,
343                    tags_json,
344                    metadata_json,
345                    memory.namespace,
346                    memory.created_at.timestamp(),
347                    memory.updated_at.timestamp(),
348                    memory.last_accessed_at.timestamp(),
349                ],
350            )
351            .map_err(|e| CodememError::Storage(e.to_string()))?;
352        }
353
354        tx.commit()
355            .map_err(|e| CodememError::Storage(e.to_string()))?;
356        Ok(())
357    }
358
359    fn store_embeddings_batch(&self, items: &[(&str, &[f32])]) -> Result<(), CodememError> {
360        let conn = self.conn();
361        let tx = conn
362            .unchecked_transaction()
363            .map_err(|e| CodememError::Storage(e.to_string()))?;
364
365        for (id, embedding) in items {
366            let blob: Vec<u8> = embedding.iter().flat_map(|f| f.to_le_bytes()).collect();
367            tx.execute(
368                "INSERT OR REPLACE INTO memory_embeddings (memory_id, embedding) VALUES (?1, ?2)",
369                params![id, blob],
370            )
371            .map_err(|e| CodememError::Storage(e.to_string()))?;
372        }
373
374        tx.commit()
375            .map_err(|e| CodememError::Storage(e.to_string()))?;
376        Ok(())
377    }
378
379    fn load_file_hashes(&self) -> Result<HashMap<String, String>, CodememError> {
380        let conn = self.conn();
381        let mut stmt = conn
382            .prepare("SELECT file_path, content_hash FROM file_hashes")
383            .map_err(|e| CodememError::Storage(e.to_string()))?;
384
385        let rows = stmt
386            .query_map([], |row| {
387                Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
388            })
389            .map_err(|e| CodememError::Storage(e.to_string()))?
390            .collect::<Result<Vec<_>, _>>()
391            .map_err(|e| CodememError::Storage(e.to_string()))?;
392
393        Ok(rows.into_iter().collect())
394    }
395
396    fn save_file_hashes(&self, hashes: &HashMap<String, String>) -> Result<(), CodememError> {
397        let conn = self.conn();
398        let tx = conn
399            .unchecked_transaction()
400            .map_err(|e| CodememError::Storage(e.to_string()))?;
401
402        tx.execute("DELETE FROM file_hashes", [])
403            .map_err(|e| CodememError::Storage(e.to_string()))?;
404
405        for (path, hash) in hashes {
406            tx.execute(
407                "INSERT INTO file_hashes (file_path, content_hash) VALUES (?1, ?2)",
408                params![path, hash],
409            )
410            .map_err(|e| CodememError::Storage(e.to_string()))?;
411        }
412
413        tx.commit()
414            .map_err(|e| CodememError::Storage(e.to_string()))?;
415        Ok(())
416    }
417
418    fn insert_graph_nodes_batch(&self, nodes: &[GraphNode]) -> Result<(), CodememError> {
419        let conn = self.conn();
420        let tx = conn
421            .unchecked_transaction()
422            .map_err(|e| CodememError::Storage(e.to_string()))?;
423
424        for node in nodes {
425            let payload_json =
426                serde_json::to_string(&node.payload).unwrap_or_else(|_| "{}".to_string());
427            tx.execute(
428                "INSERT OR REPLACE INTO graph_nodes (id, kind, label, payload, centrality, memory_id, namespace)
429                 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
430                params![
431                    node.id,
432                    node.kind.to_string(),
433                    node.label,
434                    payload_json,
435                    node.centrality,
436                    node.memory_id,
437                    node.namespace,
438                ],
439            )
440            .map_err(|e| CodememError::Storage(e.to_string()))?;
441        }
442
443        tx.commit()
444            .map_err(|e| CodememError::Storage(e.to_string()))?;
445        Ok(())
446    }
447
448    fn insert_graph_edges_batch(&self, edges: &[Edge]) -> Result<(), CodememError> {
449        let conn = self.conn();
450        let tx = conn
451            .unchecked_transaction()
452            .map_err(|e| CodememError::Storage(e.to_string()))?;
453
454        for edge in edges {
455            let props_json =
456                serde_json::to_string(&edge.properties).unwrap_or_else(|_| "{}".to_string());
457            tx.execute(
458                "INSERT OR REPLACE INTO graph_edges (id, src, dst, relationship, weight, properties, created_at)
459                 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
460                params![
461                    edge.id,
462                    edge.src,
463                    edge.dst,
464                    edge.relationship.to_string(),
465                    edge.weight,
466                    props_json,
467                    edge.created_at.timestamp(),
468                ],
469            )
470            .map_err(|e| CodememError::Storage(e.to_string()))?;
471        }
472
473        tx.commit()
474            .map_err(|e| CodememError::Storage(e.to_string()))?;
475        Ok(())
476    }
477
478    fn find_unembedded_memories(&self) -> Result<Vec<(String, String)>, CodememError> {
479        let conn = self.conn();
480        let mut stmt = conn
481            .prepare(
482                "SELECT m.id, m.content FROM memories m
483                 LEFT JOIN memory_embeddings me ON m.id = me.memory_id
484                 WHERE me.memory_id IS NULL",
485            )
486            .map_err(|e| CodememError::Storage(e.to_string()))?;
487
488        let rows = stmt
489            .query_map([], |row| {
490                Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
491            })
492            .map_err(|e| CodememError::Storage(e.to_string()))?
493            .collect::<Result<Vec<_>, _>>()
494            .map_err(|e| CodememError::Storage(e.to_string()))?;
495
496        Ok(rows)
497    }
498
499    fn search_graph_nodes(
500        &self,
501        query: &str,
502        namespace: Option<&str>,
503        limit: usize,
504    ) -> Result<Vec<GraphNode>, CodememError> {
505        let conn = self.conn();
506        let pattern = format!("%{}%", query.to_lowercase());
507
508        let (sql, params_vec): (String, Vec<Box<dyn rusqlite::types::ToSql>>) =
509            if let Some(ns) = namespace {
510                (
511                    "SELECT id, kind, label, payload, centrality, memory_id, namespace \
512                 FROM graph_nodes WHERE LOWER(label) LIKE ?1 AND namespace = ?2 \
513                 ORDER BY centrality DESC LIMIT ?3"
514                        .to_string(),
515                    vec![
516                        Box::new(pattern) as Box<dyn rusqlite::types::ToSql>,
517                        Box::new(ns.to_string()),
518                        Box::new(limit as i64),
519                    ],
520                )
521            } else {
522                (
523                    "SELECT id, kind, label, payload, centrality, memory_id, namespace \
524                 FROM graph_nodes WHERE LOWER(label) LIKE ?1 \
525                 ORDER BY centrality DESC LIMIT ?2"
526                        .to_string(),
527                    vec![
528                        Box::new(pattern) as Box<dyn rusqlite::types::ToSql>,
529                        Box::new(limit as i64),
530                    ],
531                )
532            };
533
534        let refs: Vec<&dyn rusqlite::types::ToSql> =
535            params_vec.iter().map(|p| p.as_ref()).collect();
536        let mut stmt = conn
537            .prepare(&sql)
538            .map_err(|e| CodememError::Storage(e.to_string()))?;
539
540        let rows = stmt
541            .query_map(refs.as_slice(), |row| {
542                let kind_str: String = row.get(1)?;
543                let payload_str: String = row.get(3)?;
544                Ok(GraphNode {
545                    id: row.get(0)?,
546                    kind: kind_str.parse().unwrap_or(NodeKind::Memory),
547                    label: row.get(2)?,
548                    payload: serde_json::from_str(&payload_str).unwrap_or_default(),
549                    centrality: row.get(4)?,
550                    memory_id: row.get(5)?,
551                    namespace: row.get(6)?,
552                })
553            })
554            .map_err(|e| CodememError::Storage(e.to_string()))?
555            .collect::<Result<Vec<_>, _>>()
556            .map_err(|e| CodememError::Storage(e.to_string()))?;
557
558        Ok(rows)
559    }
560
561    fn list_memories_filtered(
562        &self,
563        namespace: Option<&str>,
564        memory_type: Option<&str>,
565    ) -> Result<Vec<MemoryNode>, CodememError> {
566        let conn = self.conn();
567        let mut sql = "SELECT id, content, memory_type, importance, confidence, access_count, \
568                        content_hash, tags, metadata, namespace, created_at, updated_at, \
569                        last_accessed_at FROM memories WHERE 1=1"
570            .to_string();
571        let mut param_values: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
572
573        if let Some(ns) = namespace {
574            param_values.push(Box::new(ns.to_string()));
575            sql.push_str(&format!(" AND namespace = ?{}", param_values.len()));
576        }
577        if let Some(mt) = memory_type {
578            param_values.push(Box::new(mt.to_string()));
579            sql.push_str(&format!(" AND memory_type = ?{}", param_values.len()));
580        }
581        sql.push_str(" ORDER BY created_at DESC");
582
583        let refs: Vec<&dyn rusqlite::types::ToSql> =
584            param_values.iter().map(|p| p.as_ref()).collect();
585        let mut stmt = conn
586            .prepare(&sql)
587            .map_err(|e| CodememError::Storage(e.to_string()))?;
588
589        let rows = stmt
590            .query_map(refs.as_slice(), |row| {
591                Ok(MemoryRow {
592                    id: row.get(0)?,
593                    content: row.get(1)?,
594                    memory_type: row.get(2)?,
595                    importance: row.get(3)?,
596                    confidence: row.get(4)?,
597                    access_count: row.get(5)?,
598                    content_hash: row.get(6)?,
599                    tags: row.get(7)?,
600                    metadata: row.get(8)?,
601                    namespace: row.get(9)?,
602                    created_at: row.get(10)?,
603                    updated_at: row.get(11)?,
604                    last_accessed_at: row.get(12)?,
605                })
606            })
607            .map_err(|e| CodememError::Storage(e.to_string()))?;
608
609        let mut result = Vec::new();
610        for row in rows {
611            let mr = row.map_err(|e| CodememError::Storage(e.to_string()))?;
612            result.push(mr.into_memory_node()?);
613        }
614
615        Ok(result)
616    }
617
618    fn graph_edges_for_namespace(&self, namespace: &str) -> Result<Vec<Edge>, CodememError> {
619        Storage::graph_edges_for_namespace(self, namespace)
620    }
621
622    fn stats(&self) -> Result<StorageStats, CodememError> {
623        Storage::stats(self)
624    }
625}
626
627#[cfg(test)]
628mod tests {
629    use crate::Storage;
630    use codemem_core::{MemoryNode, MemoryType, StorageBackend};
631    use std::collections::HashMap;
632
633    fn test_memory() -> MemoryNode {
634        let now = chrono::Utc::now();
635        let content = "Test memory content";
636        MemoryNode {
637            id: uuid::Uuid::new_v4().to_string(),
638            content: content.to_string(),
639            memory_type: MemoryType::Context,
640            importance: 0.7,
641            confidence: 1.0,
642            access_count: 0,
643            content_hash: Storage::content_hash(content),
644            tags: vec!["test".to_string()],
645            metadata: HashMap::new(),
646            namespace: None,
647            created_at: now,
648            updated_at: now,
649            last_accessed_at: now,
650        }
651    }
652
653    #[test]
654    fn get_memories_batch_returns_multiple() {
655        let storage = Storage::open_in_memory().unwrap();
656        let m1 = test_memory();
657        let mut m2 = test_memory();
658        m2.id = uuid::Uuid::new_v4().to_string();
659        m2.content = "Different content".to_string();
660        m2.content_hash = Storage::content_hash(&m2.content);
661
662        storage.insert_memory(&m1).unwrap();
663        storage.insert_memory(&m2).unwrap();
664
665        let backend: &dyn StorageBackend = &storage;
666        let batch = backend.get_memories_batch(&[&m1.id, &m2.id]).unwrap();
667        assert_eq!(batch.len(), 2);
668    }
669
670    #[test]
671    fn get_memories_batch_empty() {
672        let storage = Storage::open_in_memory().unwrap();
673        let backend: &dyn StorageBackend = &storage;
674        let batch = backend.get_memories_batch(&[]).unwrap();
675        assert!(batch.is_empty());
676    }
677
678    #[test]
679    fn storage_backend_trait_object() {
680        let storage = Storage::open_in_memory().unwrap();
681        let backend: Box<dyn StorageBackend> = Box::new(storage);
682
683        let m = test_memory();
684        backend.insert_memory(&m).unwrap();
685        let retrieved = backend.get_memory(&m.id).unwrap().unwrap();
686        assert_eq!(retrieved.id, m.id);
687    }
688
689    #[test]
690    fn file_hashes_roundtrip() {
691        let storage = Storage::open_in_memory().unwrap();
692        let backend: &dyn StorageBackend = &storage;
693
694        let mut hashes = HashMap::new();
695        hashes.insert("src/main.rs".to_string(), "abc123".to_string());
696        hashes.insert("src/lib.rs".to_string(), "def456".to_string());
697
698        backend.save_file_hashes(&hashes).unwrap();
699        let loaded = backend.load_file_hashes().unwrap();
700        assert_eq!(loaded.len(), 2);
701        assert_eq!(loaded.get("src/main.rs"), Some(&"abc123".to_string()));
702    }
703
704    #[test]
705    fn decay_stale_memories_updates() {
706        let storage = Storage::open_in_memory().unwrap();
707        let backend: &dyn StorageBackend = &storage;
708
709        let m = test_memory();
710        backend.insert_memory(&m).unwrap();
711
712        // Decay memories older than far future = none affected
713        let count = backend.decay_stale_memories(0, 0.5).unwrap();
714        assert_eq!(count, 0);
715
716        // Decay all memories (threshold in the future)
717        let count = backend.decay_stale_memories(i64::MAX, 0.5).unwrap();
718        assert_eq!(count, 1);
719    }
720
721    #[test]
722    fn find_forgettable_returns_low_importance() {
723        let storage = Storage::open_in_memory().unwrap();
724        let backend: &dyn StorageBackend = &storage;
725
726        let mut m = test_memory();
727        m.importance = 0.1;
728        backend.insert_memory(&m).unwrap();
729
730        let forgettable = backend.find_forgettable(0.5).unwrap();
731        assert_eq!(forgettable.len(), 1);
732        assert_eq!(forgettable[0], m.id);
733
734        let forgettable = backend.find_forgettable(0.05).unwrap();
735        assert!(forgettable.is_empty());
736    }
737}