Skip to main content

mcpr_integrations/store/query/
store_ops.rs

1//! Query: `mcpr store stats` and `mcpr store vacuum` — operational commands.
2
3use rusqlite::params;
4use serde::Serialize;
5use std::path::Path;
6
7use super::QueryEngine;
8
9/// Database-level stats returned by `mcpr store stats`.
10#[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/// Result of a vacuum operation.
22#[derive(Debug, Serialize)]
23pub struct VacuumResult {
24    pub deleted_requests: u64,
25    pub deleted_sessions: u64,
26    pub dry_run: bool,
27}
28
29/// Parameters for the vacuum operation.
30pub struct VacuumParams {
31    pub before_ts: i64,
32    pub proxy: Option<String>,
33    pub dry_run: bool,
34}
35
36/// Count rows matching the vacuum filter (shared by vacuum and dry-run).
37fn 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
57/// Count orphaned sessions matching the vacuum filter.
58fn 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    /// Get database-level statistics.
70    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    /// Delete old requests and orphaned sessions, optionally scoped to one proxy.
104    /// In dry-run mode, returns counts without deleting.
105    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        // Delete old requests.
118        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        // Delete orphaned sessions.
131        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        // Reclaim disk space.
139        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}