Skip to main content

kaizen/store/sqlite/
maintenance.rs

1use super::*;
2
3pub(super) fn old_session_ids(
4    tx: &rusqlite::Transaction<'_>,
5    cutoff_ms: i64,
6) -> Result<Vec<String>> {
7    let mut stmt = tx.prepare("SELECT id FROM sessions WHERE started_at_ms < ?1")?;
8    let rows = stmt.query_map(params![cutoff_ms], |r| r.get::<_, String>(0))?;
9    Ok(rows.filter_map(|r| r.ok()).collect())
10}
11
12impl Store {
13    /// Delete sessions with `started_at_ms` strictly before `cutoff_ms` and all dependent rows.
14    pub fn prune_sessions_started_before(&self, cutoff_ms: i64) -> Result<PruneStats> {
15        let tx = rusqlite::Transaction::new_unchecked(&self.conn, TransactionBehavior::Deferred)?;
16        let old_ids = old_session_ids(&tx, cutoff_ms)?;
17        let sessions_to_remove: i64 = tx.query_row(
18            "SELECT COUNT(*) FROM sessions WHERE started_at_ms < ?1",
19            params![cutoff_ms],
20            |r| r.get(0),
21        )?;
22        let events_to_remove: i64 = tx.query_row(
23            "SELECT COUNT(*) FROM events WHERE session_id IN \
24             (SELECT id FROM sessions WHERE started_at_ms < ?1)",
25            params![cutoff_ms],
26            |r| r.get(0),
27        )?;
28
29        let sub_old_sessions = "SELECT id FROM sessions WHERE started_at_ms < ?1";
30        tx.execute(
31            &format!(
32                "DELETE FROM tool_span_paths WHERE span_id IN \
33                 (SELECT span_id FROM tool_spans WHERE session_id IN ({sub_old_sessions}))"
34            ),
35            params![cutoff_ms],
36        )?;
37        tx.execute(
38            &format!("DELETE FROM tool_spans WHERE session_id IN ({sub_old_sessions})"),
39            params![cutoff_ms],
40        )?;
41        tx.execute(
42            &format!("DELETE FROM events WHERE session_id IN ({sub_old_sessions})"),
43            params![cutoff_ms],
44        )?;
45        tx.execute(
46            &format!("DELETE FROM files_touched WHERE session_id IN ({sub_old_sessions})"),
47            params![cutoff_ms],
48        )?;
49        tx.execute(
50            &format!("DELETE FROM skills_used WHERE session_id IN ({sub_old_sessions})"),
51            params![cutoff_ms],
52        )?;
53        tx.execute(
54            &format!("DELETE FROM rules_used WHERE session_id IN ({sub_old_sessions})"),
55            params![cutoff_ms],
56        )?;
57        tx.execute(
58            &format!("DELETE FROM sync_outbox WHERE session_id IN ({sub_old_sessions})"),
59            params![cutoff_ms],
60        )?;
61        tx.execute(
62            &format!("DELETE FROM session_repo_binding WHERE session_id IN ({sub_old_sessions})"),
63            params![cutoff_ms],
64        )?;
65        tx.execute(
66            &format!("DELETE FROM experiment_tags WHERE session_id IN ({sub_old_sessions})"),
67            params![cutoff_ms],
68        )?;
69        tx.execute(
70            &format!("DELETE FROM session_outcomes WHERE session_id IN ({sub_old_sessions})"),
71            params![cutoff_ms],
72        )?;
73        tx.execute(
74            &format!("DELETE FROM session_samples WHERE session_id IN ({sub_old_sessions})"),
75            params![cutoff_ms],
76        )?;
77        tx.execute(
78            "DELETE FROM sessions WHERE started_at_ms < ?1",
79            params![cutoff_ms],
80        )?;
81        tx.commit()?;
82        if let Some(mut writer) = self.search_writer.borrow_mut().take() {
83            let _ = writer.commit();
84        }
85        if let Err(err) = crate::search::delete_sessions(&self.root, &old_ids) {
86            tracing::warn!("search prune skipped: {err:#}");
87            let _ = self.sync_state_set_u64(SYNC_STATE_SEARCH_DIRTY_MS, now_ms());
88        }
89        self.invalidate_span_tree_cache();
90        Ok(PruneStats {
91            sessions_removed: sessions_to_remove as u64,
92            events_removed: events_to_remove as u64,
93        })
94    }
95
96    /// Reclaim file space after large deletes (exclusive lock; can be slow).
97    pub fn vacuum(&self) -> Result<()> {
98        self.conn.execute_batch("VACUUM;").context("VACUUM")?;
99        Ok(())
100    }
101}