1use rusqlite::params;
2
3use crate::types::MemoryMetric;
4use crate::Result;
5
6use super::Store;
7
8impl Store {
9 pub fn record_injection(&self, memory_ids: &[i64], tokens_per_memory: i32) -> Result<()> {
10 let tx = self.conn().unchecked_transaction()?;
11 for id in memory_ids {
12 tx.execute(
13 "INSERT INTO metrics (memory_id, injections, tokens_injected, last_injected_at)
14 VALUES (?1, 1, ?2, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
15 ON CONFLICT(memory_id) DO UPDATE SET
16 injections = injections + 1,
17 tokens_injected = tokens_injected + ?2,
18 last_injected_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')",
19 params![id, tokens_per_memory],
20 )?;
21 }
22 tx.commit()?;
23 Ok(())
24 }
25
26 pub fn record_hit(&self, memory_id: i64) -> Result<()> {
27 self.conn().execute(
28 "INSERT INTO metrics (memory_id, hits, last_hit_at)
29 VALUES (?1, 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
30 ON CONFLICT(memory_id) DO UPDATE SET
31 hits = hits + 1,
32 last_hit_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')",
33 params![memory_id],
34 )?;
35 Ok(())
36 }
37
38 pub fn record_hit_batch(&self, memory_ids: &[i64]) -> Result<()> {
39 let tx = self.conn().unchecked_transaction()?;
40 for id in memory_ids {
41 tx.execute(
42 "INSERT INTO metrics (memory_id, hits, last_hit_at)
43 VALUES (?1, 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
44 ON CONFLICT(memory_id) DO UPDATE SET
45 hits = hits + 1,
46 last_hit_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')",
47 params![id],
48 )?;
49 }
50 tx.commit()?;
51 Ok(())
52 }
53
54 pub fn cumulative_stats(&self) -> Result<crate::types::TokenStats> {
56 let (injections, hits, tokens_injected) = self.conn().query_row(
57 "SELECT COALESCE(SUM(injections), 0), COALESCE(SUM(hits), 0), COALESCE(SUM(tokens_injected), 0) FROM metrics",
58 [],
59 |row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?, row.get::<_, i64>(2)?)),
60 )?;
61 let unique = self.conn().query_row(
62 "SELECT COUNT(*) FROM metrics WHERE injections > 0",
63 [],
64 |row| row.get::<_, i64>(0),
65 )?;
66 Ok(crate::types::TokenStats {
67 injections,
68 hits,
69 unique_memories_injected: unique,
70 tokens_injected,
71 })
72 }
73
74 pub fn dedup_total(&self) -> Result<i64> {
75 Ok(self.conn().query_row(
76 "SELECT COALESCE(SUM(duplicate_count), 0) FROM memories WHERE deleted_at IS NULL",
77 [],
78 |row| row.get(0),
79 )?)
80 }
81
82 pub fn revision_total(&self) -> Result<i64> {
83 Ok(self.conn().query_row(
84 "SELECT COALESCE(SUM(revision_count), 0) FROM memories WHERE deleted_at IS NULL",
85 [],
86 |row| row.get(0),
87 )?)
88 }
89
90 pub fn low_roi_count(&self) -> Result<i64> {
91 Ok(self.conn().query_row(
92 "SELECT COUNT(*) FROM metrics WHERE injections > 10 AND CAST(hits AS REAL) / injections < 0.1",
93 [],
94 |row| row.get(0),
95 )?)
96 }
97
98 pub fn get_metrics(&self) -> Result<Vec<MemoryMetric>> {
99 let mut stmt = self.conn().prepare(
100 "SELECT m.id, m.key, m.scope,
101 COALESCE(mt.injections, 0),
102 COALESCE(mt.hits, 0),
103 COALESCE(mt.tokens_injected, 0),
104 CASE WHEN COALESCE(mt.injections, 0) > 0
105 THEN CAST(COALESCE(mt.hits, 0) AS REAL) / mt.injections
106 ELSE 0.0 END
107 FROM memories m
108 LEFT JOIN metrics mt ON mt.memory_id = m.id
109 WHERE m.deleted_at IS NULL
110 ORDER BY COALESCE(mt.injections, 0) DESC
111 LIMIT 100",
112 )?;
113 let results = stmt
114 .query_map([], |row| {
115 Ok(MemoryMetric {
116 id: row.get(0)?,
117 key: row.get(1)?,
118 scope: row.get(2)?,
119 injections: row.get(3)?,
120 hits: row.get(4)?,
121 tokens_injected: row.get(5)?,
122 hit_rate: row.get(6)?,
123 })
124 })?
125 .collect::<std::result::Result<Vec<_>, _>>()?;
126 Ok(results)
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use crate::store::Store;
133 use crate::types::SaveParams;
134
135 fn make_memory(store: &Store, key: &str) -> i64 {
136 store
137 .save(SaveParams {
138 key: key.to_string(),
139 value: "test value".to_string(),
140 ..Default::default()
141 })
142 .unwrap()
143 .id()
144 }
145
146 #[test]
147 fn test_record_injection_increments() {
148 let store = Store::open_in_memory().unwrap();
149 let id = make_memory(&store, "test/key");
150 store.record_injection(&[id], 0).unwrap();
151 store.record_injection(&[id], 0).unwrap();
152 let metrics = store.get_metrics().unwrap();
153 let m = metrics.iter().find(|m| m.id == id).unwrap();
154 assert_eq!(m.injections, 2);
155 }
156
157 #[test]
158 fn test_record_hit_increments() {
159 let store = Store::open_in_memory().unwrap();
160 let id = make_memory(&store, "test/key");
161 store.record_hit(id).unwrap();
162 store.record_hit(id).unwrap();
163 store.record_hit(id).unwrap();
164 let metrics = store.get_metrics().unwrap();
165 let m = metrics.iter().find(|m| m.id == id).unwrap();
166 assert_eq!(m.hits, 3);
167 }
168
169 #[test]
170 fn test_hit_rate_calculation() {
171 let store = Store::open_in_memory().unwrap();
172 let id = make_memory(&store, "test/key");
173 store.record_injection(&[id], 0).unwrap();
174 store.record_injection(&[id], 0).unwrap();
175 store.record_hit(id).unwrap();
176 let metrics = store.get_metrics().unwrap();
177 let m = metrics.iter().find(|m| m.id == id).unwrap();
178 assert!((m.hit_rate - 0.5).abs() < f64::EPSILON);
179 }
180
181 #[test]
182 fn test_record_injection_accumulates_tokens() {
183 let store = Store::open_in_memory().unwrap();
184 let id = make_memory(&store, "key/one");
185 store.record_injection(&[id], 50).unwrap();
186 store.record_injection(&[id], 50).unwrap();
187 let metrics = store.get_metrics().unwrap();
188 let m = metrics.iter().find(|m| m.id == id).unwrap();
189 assert_eq!(m.tokens_injected, 100);
190 }
191}