mixtape_tools/sqlite/maintenance/
vacuum.rs1use crate::prelude::*;
4use crate::sqlite::manager::with_connection;
5use std::path::Path;
6
7#[derive(Debug, Deserialize, JsonSchema)]
9pub struct VacuumDatabaseInput {
10 #[serde(default)]
12 pub db_path: Option<String>,
13}
14
15pub 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 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 conn.execute("VACUUM", [])?;
43
44 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
68fn 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 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 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}