Skip to main content

codemem_storage/
backend.rs

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