Skip to main content

memory_core/store/
eventlog.rs

1use rusqlite::params;
2
3use crate::types::EventLogEntry;
4use crate::Result;
5
6use super::Store;
7
8impl Store {
9    /// Write an event to the live activity feed.
10    /// `action`: "save" | "search" | "inject" | "hit"
11    /// `key`:    memory key, query string, or count description
12    /// `scope`:  scope path
13    /// `tokens`: tokens injected (non-zero only for "inject" action)
14    pub fn write_event(&self, action: &str, key: &str, scope: &str, tokens: i32) -> Result<()> {
15        self.conn().execute(
16            "INSERT INTO event_log (action, key, scope, tokens) VALUES (?1, ?2, ?3, ?4)",
17            params![action, key, scope, tokens],
18        )?;
19        Ok(())
20    }
21
22    /// Return the N most recent events, newest first.
23    pub fn recent_events(&self, limit: i32) -> Result<Vec<EventLogEntry>> {
24        let mut stmt = self.conn().prepare(
25            "SELECT id, action, key, scope, tokens, created_at
26             FROM event_log
27             ORDER BY created_at DESC, id DESC
28             LIMIT ?1",
29        )?;
30        let results = stmt
31            .query_map(params![limit], |row| {
32                Ok(EventLogEntry {
33                    id: row.get(0)?,
34                    action: row.get(1)?,
35                    key: row.get(2)?,
36                    scope: row.get(3)?,
37                    tokens: row.get(4)?,
38                    created_at: row.get(5)?,
39                })
40            })?
41            .collect::<std::result::Result<Vec<_>, _>>()?;
42        Ok(results)
43    }
44
45    /// Counts for today: (saves, injections, searches, tokens_injected).
46    pub fn events_today_summary(&self) -> Result<(i64, i64, i64, i64)> {
47        Ok(self.conn().query_row(
48            "SELECT
49                COALESCE(SUM(CASE WHEN action = 'save'   THEN 1 ELSE 0 END), 0),
50                COALESCE(SUM(CASE WHEN action = 'inject' THEN 1 ELSE 0 END), 0),
51                COALESCE(SUM(CASE WHEN action = 'search' THEN 1 ELSE 0 END), 0),
52                COALESCE(SUM(CASE WHEN action = 'inject' THEN tokens ELSE 0 END), 0)
53             FROM event_log
54             WHERE date(created_at) = date('now')",
55            [],
56            |row| {
57                Ok((
58                    row.get::<_, i64>(0)?,
59                    row.get::<_, i64>(1)?,
60                    row.get::<_, i64>(2)?,
61                    row.get::<_, i64>(3)?,
62                ))
63            },
64        )?)
65    }
66
67    /// Purge events older than `days` days. Called by the maintenance scheduler.
68    pub fn purge_old_events(&self, days: u32) -> Result<u64> {
69        let deleted = self.conn().execute(
70            "DELETE FROM event_log
71             WHERE created_at < strftime('%Y-%m-%dT%H:%M:%fZ', 'now', '-' || ?1 || ' days')",
72            params![days],
73        )?;
74        Ok(deleted as u64)
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn test_write_and_read_events() {
84        let store = Store::open_in_memory().unwrap();
85        store
86            .write_event("save", "arch/decision", "/myproject", 0)
87            .unwrap();
88        store
89            .write_event("inject", "3 memories", "/myproject", 450)
90            .unwrap();
91
92        let events = store.recent_events(10).unwrap();
93        assert_eq!(events.len(), 2);
94        // newest first
95        assert_eq!(events[0].action, "inject");
96        assert_eq!(events[0].tokens, 450);
97        assert_eq!(events[1].action, "save");
98        assert_eq!(events[1].key, "arch/decision");
99    }
100
101    #[test]
102    fn test_events_today_summary() {
103        let store = Store::open_in_memory().unwrap();
104        store.write_event("save", "k1", "/", 0).unwrap();
105        store.write_event("save", "k2", "/", 0).unwrap();
106        store.write_event("search", "rust async", "/", 0).unwrap();
107        store.write_event("inject", "2 memories", "/", 300).unwrap();
108
109        let (saves, injections, searches, tokens) = store.events_today_summary().unwrap();
110        assert_eq!(saves, 2);
111        assert_eq!(injections, 1);
112        assert_eq!(searches, 1);
113        assert_eq!(tokens, 300);
114    }
115
116    #[test]
117    fn test_purge_old_events() {
118        let store = Store::open_in_memory().unwrap();
119        // Insert an old event directly
120        store
121            .conn()
122            .execute(
123                "INSERT INTO event_log (action, key, scope, tokens, created_at)
124             VALUES ('save', 'old', '/', 0, '2020-01-01T00:00:00.000Z')",
125                [],
126            )
127            .unwrap();
128        store.write_event("save", "new", "/", 0).unwrap();
129
130        let deleted = store.purge_old_events(30).unwrap();
131        assert_eq!(deleted, 1);
132        let events = store.recent_events(10).unwrap();
133        assert_eq!(events.len(), 1);
134        assert_eq!(events[0].key, "new");
135    }
136}