mcpr_integrations/store/query/
store_ops.rs1use rusqlite::params;
4use serde::Serialize;
5use std::path::Path;
6
7use super::QueryEngine;
8
9#[derive(Debug, Serialize)]
11pub struct StoreStats {
12 pub total_requests: i64,
13 pub total_sessions: i64,
14 pub oldest_ts: Option<i64>,
15 pub newest_ts: Option<i64>,
16 pub proxy_count: i64,
17 pub db_file_size: u64,
18 pub wal_file_size: u64,
19}
20
21#[derive(Debug, Serialize)]
23pub struct VacuumResult {
24 pub deleted_requests: u64,
25 pub deleted_sessions: u64,
26 pub dry_run: bool,
27}
28
29pub struct VacuumParams {
31 pub before_ts: i64,
32 pub proxy: Option<String>,
33 pub dry_run: bool,
34}
35
36fn count_matching_requests(
38 conn: &rusqlite::Connection,
39 before_ts: i64,
40 proxy: Option<&str>,
41) -> rusqlite::Result<i64> {
42 if let Some(proxy) = proxy {
43 conn.query_row(
44 "SELECT COUNT(*) FROM requests WHERE ts < ?1 AND proxy = ?2",
45 params![before_ts, proxy],
46 |row| row.get(0),
47 )
48 } else {
49 conn.query_row(
50 "SELECT COUNT(*) FROM requests WHERE ts < ?1",
51 params![before_ts],
52 |row| row.get(0),
53 )
54 }
55}
56
57fn count_orphaned_sessions(conn: &rusqlite::Connection, before_ts: i64) -> rusqlite::Result<i64> {
59 conn.query_row(
60 "SELECT COUNT(*) FROM sessions
61 WHERE session_id NOT IN (SELECT DISTINCT session_id FROM requests WHERE session_id IS NOT NULL)
62 AND (ended_at IS NOT NULL AND ended_at < ?1)",
63 params![before_ts],
64 |row| row.get(0),
65 )
66}
67
68impl QueryEngine {
69 pub fn store_stats(&self, db_path: &Path) -> Result<StoreStats, rusqlite::Error> {
71 let row = self.conn().query_row(
72 "SELECT COUNT(*), MIN(ts), MAX(ts), COUNT(DISTINCT proxy) FROM requests",
73 [],
74 |row| {
75 Ok((
76 row.get::<_, i64>(0)?,
77 row.get::<_, Option<i64>>(1)?,
78 row.get::<_, Option<i64>>(2)?,
79 row.get::<_, i64>(3)?,
80 ))
81 },
82 )?;
83
84 let total_sessions: i64 =
85 self.conn()
86 .query_row("SELECT COUNT(*) FROM sessions", [], |r| r.get(0))?;
87
88 let db_file_size = std::fs::metadata(db_path).map(|m| m.len()).unwrap_or(0);
89 let wal_path = db_path.with_extension("db-wal");
90 let wal_file_size = std::fs::metadata(wal_path).map(|m| m.len()).unwrap_or(0);
91
92 Ok(StoreStats {
93 total_requests: row.0,
94 total_sessions,
95 oldest_ts: row.1,
96 newest_ts: row.2,
97 proxy_count: row.3,
98 db_file_size,
99 wal_file_size,
100 })
101 }
102
103 pub fn vacuum(&self, params: &VacuumParams) -> Result<VacuumResult, rusqlite::Error> {
106 if params.dry_run {
107 let deleted_requests =
108 count_matching_requests(self.conn(), params.before_ts, params.proxy.as_deref())?;
109 let deleted_sessions = count_orphaned_sessions(self.conn(), params.before_ts)?;
110 return Ok(VacuumResult {
111 deleted_requests: deleted_requests as u64,
112 deleted_sessions: deleted_sessions as u64,
113 dry_run: true,
114 });
115 }
116
117 let deleted_requests = if let Some(ref proxy) = params.proxy {
119 self.conn().execute(
120 "DELETE FROM requests WHERE ts < ?1 AND proxy = ?2",
121 params![params.before_ts, proxy],
122 )?
123 } else {
124 self.conn().execute(
125 "DELETE FROM requests WHERE ts < ?1",
126 params![params.before_ts],
127 )?
128 };
129
130 let deleted_sessions = self.conn().execute(
132 "DELETE FROM sessions
133 WHERE session_id NOT IN (SELECT DISTINCT session_id FROM requests WHERE session_id IS NOT NULL)
134 AND (ended_at IS NOT NULL AND ended_at < ?1)",
135 params![params.before_ts],
136 )?;
137
138 self.conn().execute_batch("VACUUM;")?;
140 self.conn()
141 .execute_batch("PRAGMA wal_checkpoint(TRUNCATE);")?;
142
143 Ok(VacuumResult {
144 deleted_requests: deleted_requests as u64,
145 deleted_sessions: deleted_sessions as u64,
146 dry_run: false,
147 })
148 }
149}