1use std::path::Path;
2use std::sync::Mutex;
3
4use rusqlite::{Connection, OptionalExtension};
5
6use crate::error::{RovenueError, RovenueResult};
7
8use super::schema::{LATEST, MIGRATIONS};
9
10#[derive(Debug, Clone)]
11pub struct SessionEventRow {
12 pub id: i64,
13 pub rovenue_id: String,
16 pub kind: String,
17 pub occurred_at: String,
18 pub duration_ms: Option<u32>,
19}
20
21#[derive(Debug, Clone)]
22pub struct QueuedEventRow {
23 pub id: i64,
24 pub envelope_json: String,
25}
26
27#[derive(Debug, Clone)]
28pub struct AttributeMutationRow {
29 pub id: i64,
30 pub rovenue_id: String,
32 pub key: String,
33 pub value: Option<String>,
35}
36
37pub struct CacheStore {
38 conn: Mutex<Connection>,
39}
40
41impl CacheStore {
42 pub fn open(path: &Path) -> RovenueResult<Self> {
43 let conn = Connection::open(path).map_err(|_| RovenueError::Storage())?;
44 conn.pragma_update(None, "journal_mode", "WAL").ok();
45 conn.pragma_update(None, "synchronous", "NORMAL").ok();
46 conn.pragma_update(None, "foreign_keys", "ON").ok();
47 Self::run_migrations(&conn)?;
48 Ok(Self {
49 conn: Mutex::new(conn),
50 })
51 }
52
53 pub fn open_in_memory() -> RovenueResult<Self> {
54 let conn = Connection::open_in_memory().map_err(|_| RovenueError::Storage())?;
55 Self::run_migrations(&conn)?;
56 Ok(Self {
57 conn: Mutex::new(conn),
58 })
59 }
60
61 fn run_migrations(conn: &Connection) -> RovenueResult<()> {
62 let current: u32 = conn
63 .query_row("SELECT version FROM schema_meta LIMIT 1", [], |r| r.get(0))
64 .optional()
65 .ok()
66 .flatten()
67 .unwrap_or(0);
68
69 for (idx, sql) in MIGRATIONS.iter().enumerate() {
70 let target = idx as u32 + 1;
71 if current < target {
72 conn.execute_batch(sql)
73 .map_err(|_| RovenueError::Storage())?;
74 }
75 }
76 Ok(())
77 }
78
79 pub fn schema_version(&self) -> RovenueResult<u32> {
80 let guard = self.conn.lock().map_err(|_| RovenueError::Storage())?;
81 guard
82 .query_row("SELECT version FROM schema_meta LIMIT 1", [], |r| {
83 r.get::<_, u32>(0)
84 })
85 .map_err(|_| RovenueError::Storage())
86 }
87
88 pub fn has_table(&self, name: &str) -> RovenueResult<bool> {
89 let guard = self.conn.lock().map_err(|_| RovenueError::Storage())?;
90 let count: i64 = guard
91 .query_row(
92 "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?1",
93 [name],
94 |r| r.get(0),
95 )
96 .map_err(|_| RovenueError::Storage())?;
97 Ok(count > 0)
98 }
99
100 pub fn with_conn<R>(
102 &self,
103 f: impl FnOnce(&Connection) -> rusqlite::Result<R>,
104 ) -> RovenueResult<R> {
105 let guard = self.conn.lock().map_err(|_| RovenueError::Storage())?;
106 f(&guard).map_err(|_| RovenueError::Storage())
107 }
108
109 pub const fn latest_schema_version() -> u32 {
111 LATEST
112 }
113
114 pub fn get_app_account_token(&self, scope: &str) -> RovenueResult<Option<String>> {
115 let guard = self.conn.lock().map_err(|_| RovenueError::Storage())?;
116 guard
117 .query_row(
118 "SELECT token FROM app_account_tokens WHERE scope = ?1",
119 [scope],
120 |r| r.get::<_, String>(0),
121 )
122 .optional()
123 .map_err(|_| RovenueError::Storage())
124 }
125
126 pub fn put_app_account_token(&self, scope: &str, token: &str) -> RovenueResult<()> {
127 let guard = self.conn.lock().map_err(|_| RovenueError::Storage())?;
128 guard
130 .execute(
131 "INSERT OR IGNORE INTO app_account_tokens (scope, token, created_at) \
132 VALUES (?1, ?2, datetime('now'))",
133 [scope, token],
134 )
135 .map_err(|_| RovenueError::Storage())?;
136 Ok(())
137 }
138
139 pub fn clear_app_account_tokens(&self) -> RovenueResult<()> {
142 let guard = self.conn.lock().map_err(|_| RovenueError::Storage())?;
143 guard
144 .execute("DELETE FROM app_account_tokens", [])
145 .map_err(|_| RovenueError::Storage())?;
146 Ok(())
147 }
148
149 pub fn count_app_account_tokens(&self) -> RovenueResult<i64> {
150 let guard = self.conn.lock().map_err(|_| RovenueError::Storage())?;
151 guard
152 .query_row("SELECT COUNT(*) FROM app_account_tokens", [], |r| r.get(0))
153 .map_err(|_| RovenueError::Storage())
154 }
155
156 pub fn append_session_event(
157 &self,
158 rovenue_id: &str,
159 kind: &str,
160 occurred_at: &str,
161 duration_ms: Option<u32>,
162 ) -> RovenueResult<()> {
163 let guard = self.conn.lock().map_err(|_| RovenueError::Storage())?;
164 guard
165 .execute(
166 "INSERT INTO session_events (rovenue_id, kind, occurred_at, duration_ms) \
167 VALUES (?1, ?2, ?3, ?4)",
168 rusqlite::params![rovenue_id, kind, occurred_at, duration_ms],
169 )
170 .map_err(|_| RovenueError::Storage())?;
171 guard
173 .execute(
174 "DELETE FROM session_events WHERE id NOT IN \
175 (SELECT id FROM session_events ORDER BY id DESC LIMIT 1000)",
176 [],
177 )
178 .map_err(|_| RovenueError::Storage())?;
179 Ok(())
180 }
181
182 pub fn list_session_events(&self, limit: usize) -> RovenueResult<Vec<SessionEventRow>> {
183 let guard = self.conn.lock().map_err(|_| RovenueError::Storage())?;
184 let mut stmt = guard
185 .prepare(
186 "SELECT id, rovenue_id, kind, occurred_at, duration_ms FROM session_events \
187 ORDER BY id ASC LIMIT ?1",
188 )
189 .map_err(|_| RovenueError::Storage())?;
190 let rows = stmt
191 .query_map([limit as i64], |r| {
192 Ok(SessionEventRow {
193 id: r.get(0)?,
194 rovenue_id: r.get(1)?,
195 kind: r.get(2)?,
196 occurred_at: r.get(3)?,
197 duration_ms: r.get(4)?,
198 })
199 })
200 .map_err(|_| RovenueError::Storage())?
201 .collect::<Result<Vec<_>, _>>()
202 .map_err(|_| RovenueError::Storage())?;
203 Ok(rows)
204 }
205
206 pub fn delete_session_events(&self, ids: &[i64]) -> RovenueResult<()> {
207 if ids.is_empty() {
208 return Ok(());
209 }
210 let guard = self.conn.lock().map_err(|_| RovenueError::Storage())?;
211 let placeholders = std::iter::repeat_n("?", ids.len())
212 .collect::<Vec<_>>()
213 .join(",");
214 let sql = format!("DELETE FROM session_events WHERE id IN ({placeholders})");
215 let params: Vec<&dyn rusqlite::ToSql> =
216 ids.iter().map(|i| i as &dyn rusqlite::ToSql).collect();
217 guard
218 .execute(&sql, params.as_slice())
219 .map_err(|_| RovenueError::Storage())?;
220 Ok(())
221 }
222
223 pub fn append_queued_event(&self, envelope_json: &str) -> RovenueResult<()> {
227 let guard = self.conn.lock().map_err(|_| RovenueError::Storage())?;
228 guard
229 .execute(
230 "INSERT INTO event_queue (envelope_json) VALUES (?1)",
231 [envelope_json],
232 )
233 .map_err(|_| RovenueError::Storage())?;
234 guard
236 .execute(
237 "DELETE FROM event_queue WHERE id NOT IN \
238 (SELECT id FROM event_queue ORDER BY id DESC LIMIT 100)",
239 [],
240 )
241 .map_err(|_| RovenueError::Storage())?;
242 Ok(())
243 }
244
245 pub fn list_queued_events(&self, limit: usize) -> RovenueResult<Vec<QueuedEventRow>> {
247 let guard = self.conn.lock().map_err(|_| RovenueError::Storage())?;
248 let mut stmt = guard
249 .prepare(
250 "SELECT id, envelope_json FROM event_queue \
251 ORDER BY id ASC LIMIT ?1",
252 )
253 .map_err(|_| RovenueError::Storage())?;
254 let rows = stmt
255 .query_map([limit as i64], |r| {
256 Ok(QueuedEventRow {
257 id: r.get(0)?,
258 envelope_json: r.get(1)?,
259 })
260 })
261 .map_err(|_| RovenueError::Storage())?
262 .collect::<Result<Vec<_>, _>>()
263 .map_err(|_| RovenueError::Storage())?;
264 Ok(rows)
265 }
266
267 pub fn delete_queued_events(&self, ids: &[i64]) -> RovenueResult<()> {
268 if ids.is_empty() {
269 return Ok(());
270 }
271 let guard = self.conn.lock().map_err(|_| RovenueError::Storage())?;
272 let placeholders = std::iter::repeat_n("?", ids.len())
273 .collect::<Vec<_>>()
274 .join(",");
275 let sql = format!("DELETE FROM event_queue WHERE id IN ({placeholders})");
276 let params: Vec<&dyn rusqlite::ToSql> =
277 ids.iter().map(|i| i as &dyn rusqlite::ToSql).collect();
278 guard
279 .execute(&sql, params.as_slice())
280 .map_err(|_| RovenueError::Storage())?;
281 Ok(())
282 }
283
284 pub fn append_attribute_mutation(
285 &self,
286 rovenue_id: &str,
287 key: &str,
288 value: Option<&str>,
289 ) -> RovenueResult<()> {
290 let guard = self.conn.lock().map_err(|_| RovenueError::Storage())?;
291 guard
292 .execute(
293 "INSERT INTO attribute_mutations (rovenue_id, key, value) VALUES (?1, ?2, ?3)",
294 rusqlite::params![rovenue_id, key, value],
295 )
296 .map_err(|_| RovenueError::Storage())?;
297 guard
299 .execute(
300 "DELETE FROM attribute_mutations WHERE id NOT IN \
301 (SELECT id FROM attribute_mutations ORDER BY id DESC LIMIT 1000)",
302 [],
303 )
304 .map_err(|_| RovenueError::Storage())?;
305 Ok(())
306 }
307
308 pub fn list_attribute_mutations(
309 &self,
310 limit: usize,
311 ) -> RovenueResult<Vec<AttributeMutationRow>> {
312 let guard = self.conn.lock().map_err(|_| RovenueError::Storage())?;
313 let mut stmt = guard
314 .prepare(
315 "SELECT id, rovenue_id, key, value FROM attribute_mutations \
316 ORDER BY id ASC LIMIT ?1",
317 )
318 .map_err(|_| RovenueError::Storage())?;
319 let rows = stmt
320 .query_map([limit as i64], |r| {
321 Ok(AttributeMutationRow {
322 id: r.get(0)?,
323 rovenue_id: r.get(1)?,
324 key: r.get(2)?,
325 value: r.get(3)?,
326 })
327 })
328 .map_err(|_| RovenueError::Storage())?
329 .collect::<Result<Vec<_>, _>>()
330 .map_err(|_| RovenueError::Storage())?;
331 Ok(rows)
332 }
333
334 pub fn delete_attribute_mutations(&self, ids: &[i64]) -> RovenueResult<()> {
335 if ids.is_empty() {
336 return Ok(());
337 }
338 let guard = self.conn.lock().map_err(|_| RovenueError::Storage())?;
339 let placeholders = std::iter::repeat_n("?", ids.len())
340 .collect::<Vec<_>>()
341 .join(",");
342 let sql = format!("DELETE FROM attribute_mutations WHERE id IN ({placeholders})");
343 let params: Vec<&dyn rusqlite::ToSql> =
344 ids.iter().map(|i| i as &dyn rusqlite::ToSql).collect();
345 guard
346 .execute(&sql, params.as_slice())
347 .map_err(|_| RovenueError::Storage())?;
348 Ok(())
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355
356 #[test]
357 fn app_account_tokens_table_round_trip() {
358 let store = CacheStore::open_in_memory().unwrap();
359 let scope = "subscriber-abc";
360 assert_eq!(store.get_app_account_token(scope).unwrap(), None);
362 store
364 .put_app_account_token(scope, "550e8400-e29b-41d4-a716-446655440000")
365 .unwrap();
366 assert_eq!(
367 store.get_app_account_token(scope).unwrap(),
368 Some("550e8400-e29b-41d4-a716-446655440000".to_string()),
369 );
370 store
372 .put_app_account_token(scope, "different-uuid")
373 .unwrap();
374 assert_eq!(
375 store.get_app_account_token(scope).unwrap(),
376 Some("550e8400-e29b-41d4-a716-446655440000".to_string()),
377 );
378 }
379
380 #[test]
381 fn session_events_fifo_drop_at_cap() {
382 let store = CacheStore::open_in_memory().unwrap();
383 for i in 0..1005 {
384 store
385 .append_session_event(
386 "rov_a",
387 "open",
388 &format!("2026-05-28T10:00:{:02}Z", i % 60),
389 None,
390 )
391 .unwrap();
392 }
393 let rows = store.list_session_events(2000).unwrap();
394 assert_eq!(rows.len(), 1000);
396 let ids: Vec<i64> = rows.iter().map(|r| r.id).collect();
398 store.delete_session_events(&ids).unwrap();
399 assert_eq!(store.list_session_events(2000).unwrap().len(), 0);
400 }
401
402 #[test]
403 fn event_queue_fifo_drop_at_cap() {
404 let store = CacheStore::open_in_memory().unwrap();
405 for i in 0..105 {
406 store
407 .append_queued_event(&format!(
408 r#"{{"eventType":"funnel_session_started","occurredAt":"2026-05-28T10:00:00Z","eventId":"evt_{i}"}}"#
409 ))
410 .unwrap();
411 }
412 let rows = store.list_queued_events(2000).unwrap();
413 assert_eq!(rows.len(), 100);
416 assert!(rows[0].envelope_json.contains("\"evt_5\""));
417 assert!(rows[99].envelope_json.contains("\"evt_104\""));
418 let ids: Vec<i64> = rows.iter().map(|r| r.id).collect();
419 store.delete_queued_events(&ids).unwrap();
420 assert_eq!(store.list_queued_events(2000).unwrap().len(), 0);
421 }
422
423 #[test]
424 fn attribute_mutations_crud() {
425 let store = CacheStore::open_in_memory().unwrap();
426 store
427 .append_attribute_mutation("rov_a", "$email", Some("a@b.com"))
428 .unwrap();
429 store
430 .append_attribute_mutation("rov_a", "favoriteTeam", Some("GS"))
431 .unwrap();
432 store
433 .append_attribute_mutation("rov_b", "country", None)
434 .unwrap(); let rows = store.list_attribute_mutations(100).unwrap();
437 assert_eq!(rows.len(), 3);
438 assert_eq!(rows[0].key, "$email");
439 assert_eq!(rows[0].value.as_deref(), Some("a@b.com"));
440 assert_eq!(rows[2].key, "country");
441 assert_eq!(rows[2].value, None);
442 assert_eq!(rows[2].rovenue_id, "rov_b");
443
444 let ids: Vec<i64> = rows.iter().take(2).map(|r| r.id).collect();
445 store.delete_attribute_mutations(&ids).unwrap();
446 assert_eq!(store.list_attribute_mutations(100).unwrap().len(), 1);
447 }
448}