oxify_storage/
maintenance.rs1use crate::{DatabasePool, Result};
30use serde::{Deserialize, Serialize};
31use sqlx::Row;
32use tracing::info;
33
34#[derive(Debug, Clone)]
36pub struct MaintenanceConfig {
37 pub auto_vacuum: bool,
39 pub auto_analyze: bool,
41}
42
43impl Default for MaintenanceConfig {
44 fn default() -> Self {
45 Self {
46 auto_vacuum: true,
47 auto_analyze: true,
48 }
49 }
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct TableStats {
55 pub table_name: String,
56 pub row_count: i64,
57}
58
59#[derive(Debug, Clone, Default, Serialize, Deserialize)]
61pub struct MaintenanceResults {
62 pub vacuumed: bool,
63 pub analyzed: bool,
64 pub errors: Vec<String>,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct IndexBloatInfo {
70 pub table_name: String,
71 pub index_name: String,
72}
73
74pub struct MaintenanceService {
76 pool: DatabasePool,
77 config: MaintenanceConfig,
78}
79
80impl MaintenanceService {
81 pub fn new(pool: DatabasePool, config: MaintenanceConfig) -> Self {
83 Self { pool, config }
84 }
85
86 pub async fn run_maintenance(&self) -> Result<MaintenanceResults> {
88 let mut results = MaintenanceResults::default();
89
90 if self.config.auto_vacuum {
92 match self.vacuum().await {
93 Ok(()) => results.vacuumed = true,
94 Err(e) => results.errors.push(format!("VACUUM failed: {e}")),
95 }
96 }
97
98 if self.config.auto_analyze {
100 match self.analyze().await {
101 Ok(()) => results.analyzed = true,
102 Err(e) => results.errors.push(format!("ANALYZE failed: {e}")),
103 }
104 }
105
106 info!(?results, "Maintenance completed");
107 Ok(results)
108 }
109
110 pub async fn vacuum(&self) -> Result<()> {
112 sqlx::query("VACUUM").execute(self.pool.pool()).await?;
113 Ok(())
114 }
115
116 pub async fn analyze(&self) -> Result<()> {
118 sqlx::query("ANALYZE").execute(self.pool.pool()).await?;
119 Ok(())
120 }
121
122 pub async fn vacuum_table(&self, _table_name: &str) -> Result<()> {
124 self.vacuum().await
127 }
128
129 pub async fn analyze_table(&self, table_name: &str) -> Result<()> {
131 let query: &'static str = Box::leak(format!("ANALYZE {table_name}").into_boxed_str());
134 sqlx::query(query).execute(self.pool.pool()).await?;
135 Ok(())
136 }
137
138 pub async fn get_table_stats(&self, table_name: &str) -> Result<TableStats> {
140 let query: &'static str =
142 Box::leak(format!("SELECT COUNT(*) as count FROM {table_name}").into_boxed_str());
143 let row = sqlx::query(query).fetch_one(self.pool.pool()).await?;
144
145 let row_count: i64 = row.get("count");
146
147 Ok(TableStats {
148 table_name: table_name.to_string(),
149 row_count,
150 })
151 }
152
153 pub async fn list_tables(&self) -> Result<Vec<String>> {
155 let rows = sqlx::query(
156 "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name",
157 )
158 .fetch_all(self.pool.pool())
159 .await?;
160
161 let tables: Vec<String> = rows.into_iter().map(|r| r.get("name")).collect();
162
163 Ok(tables)
164 }
165
166 pub async fn get_database_size(&self) -> Result<i64> {
170 let row = sqlx::query(
171 "SELECT page_count * page_size as size FROM pragma_page_count(), pragma_page_size()",
172 )
173 .fetch_one(self.pool.pool())
174 .await?;
175
176 let size: i64 = row.get("size");
177 Ok(size)
178 }
179
180 pub async fn get_database_size_mb(&self) -> Result<f64> {
182 let size = self.get_database_size().await?;
183 Ok(size as f64 / 1024.0 / 1024.0)
184 }
185
186 pub async fn get_index_info(&self) -> Result<Vec<IndexBloatInfo>> {
188 let rows = sqlx::query(
189 "SELECT tbl_name as table_name, name as index_name
190 FROM sqlite_master
191 WHERE type='index' AND name NOT LIKE 'sqlite_%'
192 ORDER BY tbl_name, name",
193 )
194 .fetch_all(self.pool.pool())
195 .await?;
196
197 let indexes: Vec<IndexBloatInfo> = rows
198 .into_iter()
199 .map(|r| IndexBloatInfo {
200 table_name: r.get("table_name"),
201 index_name: r.get("index_name"),
202 })
203 .collect();
204
205 Ok(indexes)
206 }
207
208 pub async fn integrity_check(&self) -> Result<bool> {
210 let row = sqlx::query("SELECT integrity_check FROM pragma_integrity_check()")
211 .fetch_one(self.pool.pool())
212 .await?;
213
214 let result: String = row.get("integrity_check");
215 Ok(result == "ok")
216 }
217
218 pub async fn optimize(&self) -> Result<()> {
220 self.vacuum().await?;
221 self.analyze().await?;
222 Ok(())
223 }
224
225 pub async fn get_freelist_count(&self) -> Result<i64> {
227 let row = sqlx::query("SELECT freelist_count FROM pragma_freelist_count()")
228 .fetch_one(self.pool.pool())
229 .await?;
230
231 let count: i64 = row.get("freelist_count");
232 Ok(count)
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239
240 #[test]
241 fn test_maintenance_config_default() {
242 let config = MaintenanceConfig::default();
243 assert!(config.auto_vacuum);
244 assert!(config.auto_analyze);
245 }
246
247 #[test]
248 fn test_maintenance_results_default() {
249 let results = MaintenanceResults::default();
250 assert!(!results.vacuumed);
251 assert!(!results.analyzed);
252 assert!(results.errors.is_empty());
253 }
254}