Skip to main content

mixtape_tools/sqlite/maintenance/
vacuum.rs

1//! Vacuum database tool
2
3use crate::prelude::*;
4use crate::sqlite::manager::with_connection;
5use std::path::Path;
6
7/// Input for vacuum operation
8#[derive(Debug, Deserialize, JsonSchema)]
9pub struct VacuumDatabaseInput {
10    /// Database file path. If not specified, uses the default database.
11    #[serde(default)]
12    pub db_path: Option<String>,
13}
14
15/// Tool for optimizing database storage (DESTRUCTIVE)
16///
17/// Rebuilds the database file, reclaiming unused space and optimizing storage.
18/// This can take time for large databases and temporarily locks the database.
19pub struct VacuumDatabaseTool;
20
21impl Tool for VacuumDatabaseTool {
22    type Input = VacuumDatabaseInput;
23
24    fn name(&self) -> &str {
25        "sqlite_vacuum"
26    }
27
28    fn description(&self) -> &str {
29        "Optimize database storage by rebuilding the database file. Reclaims unused space and defragments the database."
30    }
31
32    async fn execute(&self, input: Self::Input) -> Result<ToolResult, ToolError> {
33        let (size_before, size_after) = with_connection(input.db_path, |conn| {
34            // Get database path and size before vacuum
35            let db_path: String = conn
36                .query_row("PRAGMA database_list", [], |row| row.get(2))
37                .unwrap_or_else(|_| "unknown".to_string());
38
39            let size_before = Path::new(&db_path).metadata().map(|m| m.len()).unwrap_or(0);
40
41            // Perform vacuum
42            conn.execute("VACUUM", [])?;
43
44            // Get size after vacuum
45            let size_after = Path::new(&db_path).metadata().map(|m| m.len()).unwrap_or(0);
46
47            Ok((size_before, size_after))
48        })
49        .await?;
50
51        let saved = size_before.saturating_sub(size_after);
52        let response = serde_json::json!({
53            "status": "success",
54            "size_before_bytes": size_before,
55            "size_after_bytes": size_after,
56            "space_reclaimed_bytes": saved,
57            "message": format!(
58                "Database vacuumed. Size: {} -> {} ({} reclaimed)",
59                format_bytes(size_before),
60                format_bytes(size_after),
61                format_bytes(saved)
62            )
63        });
64        Ok(ToolResult::Json(response))
65    }
66}
67
68/// Format bytes into human-readable string
69fn format_bytes(bytes: u64) -> String {
70    const KB: u64 = 1024;
71    const MB: u64 = KB * 1024;
72    const GB: u64 = MB * 1024;
73
74    if bytes >= GB {
75        format!("{:.2} GB", bytes as f64 / GB as f64)
76    } else if bytes >= MB {
77        format!("{:.2} MB", bytes as f64 / MB as f64)
78    } else if bytes >= KB {
79        format!("{:.2} KB", bytes as f64 / KB as f64)
80    } else {
81        format!("{} bytes", bytes)
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88    use crate::sqlite::test_utils::{unwrap_json, TestDatabase};
89
90    #[tokio::test]
91    async fn test_vacuum_database() {
92        let db = TestDatabase::with_schema("CREATE TABLE test (id INTEGER, data TEXT);").await;
93
94        // Insert and delete data to create free space
95        for i in 0..100 {
96            db.execute(&format!(
97                "INSERT INTO test VALUES ({}, '{}')",
98                i,
99                "test data ".repeat(10)
100            ));
101        }
102        db.execute("DELETE FROM test");
103
104        // Vacuum
105        let tool = VacuumDatabaseTool;
106        let input = VacuumDatabaseInput {
107            db_path: Some(db.key()),
108        };
109
110        let result = tool.execute(input).await.unwrap();
111        let json = unwrap_json(result);
112
113        assert_eq!(json["status"].as_str().unwrap(), "success");
114        assert!(json["size_before_bytes"].as_u64().is_some());
115        assert!(json["size_after_bytes"].as_u64().is_some());
116    }
117
118    #[test]
119    fn test_format_bytes() {
120        assert_eq!(format_bytes(0), "0 bytes");
121        assert_eq!(format_bytes(512), "512 bytes");
122        assert_eq!(format_bytes(1024), "1.00 KB");
123        assert_eq!(format_bytes(1536), "1.50 KB");
124        assert_eq!(format_bytes(1048576), "1.00 MB");
125        assert_eq!(format_bytes(1073741824), "1.00 GB");
126    }
127
128    #[test]
129    fn test_tool_metadata() {
130        let tool = VacuumDatabaseTool;
131        assert_eq!(tool.name(), "sqlite_vacuum");
132        assert!(!tool.description().is_empty());
133    }
134}