oxify_storage/
maintenance.rs

1//! Database Maintenance Utilities for SQLite
2//!
3//! Provides utilities for SQLite maintenance operations.
4//!
5//! ## Overview
6//!
7//! SQLite maintenance operations:
8//! - **VACUUM**: Rebuilds database file, reclaims space
9//! - **ANALYZE**: Updates statistics for query planner
10//! - **Statistics**: Provides insights into database health
11//!
12//! ## Usage Example
13//!
14//! ```ignore
15//! use oxify_storage::{MaintenanceService, MaintenanceConfig};
16//!
17//! let config = MaintenanceConfig::default();
18//! let maintenance = MaintenanceService::new(pool, config);
19//!
20//! // Run maintenance
21//! let results = maintenance.run_maintenance().await?;
22//! println!("Maintenance completed");
23//!
24//! // Get database size
25//! let size = maintenance.get_database_size_mb().await?;
26//! println!("Database size: {} MB", size);
27//! ```
28
29use crate::{DatabasePool, Result};
30use serde::{Deserialize, Serialize};
31use sqlx::Row;
32use tracing::info;
33
34/// Maintenance configuration
35#[derive(Debug, Clone)]
36pub struct MaintenanceConfig {
37    /// Enable automatic vacuum
38    pub auto_vacuum: bool,
39    /// Enable automatic analyze
40    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/// Table statistics (simplified for SQLite)
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct TableStats {
55    pub table_name: String,
56    pub row_count: i64,
57}
58
59/// Maintenance results
60#[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/// Index bloat information (simplified for SQLite)
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct IndexBloatInfo {
70    pub table_name: String,
71    pub index_name: String,
72}
73
74/// Maintenance service for database upkeep
75pub struct MaintenanceService {
76    pool: DatabasePool,
77    config: MaintenanceConfig,
78}
79
80impl MaintenanceService {
81    /// Create a new maintenance service
82    pub fn new(pool: DatabasePool, config: MaintenanceConfig) -> Self {
83        Self { pool, config }
84    }
85
86    /// Run all maintenance operations
87    pub async fn run_maintenance(&self) -> Result<MaintenanceResults> {
88        let mut results = MaintenanceResults::default();
89
90        // Run VACUUM if enabled
91        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        // Run ANALYZE if enabled
99        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    /// VACUUM the database
111    pub async fn vacuum(&self) -> Result<()> {
112        sqlx::query("VACUUM").execute(self.pool.pool()).await?;
113        Ok(())
114    }
115
116    /// ANALYZE the database
117    pub async fn analyze(&self) -> Result<()> {
118        sqlx::query("ANALYZE").execute(self.pool.pool()).await?;
119        Ok(())
120    }
121
122    /// VACUUM a specific table
123    pub async fn vacuum_table(&self, _table_name: &str) -> Result<()> {
124        // SQLite VACUUM cannot target individual tables
125        // Run full VACUUM instead
126        self.vacuum().await
127    }
128
129    /// ANALYZE a specific table
130    pub async fn analyze_table(&self, table_name: &str) -> Result<()> {
131        // For dynamic table names, we use Box::leak to get a static string
132        // This is safe because table names are short-lived
133        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    /// Get statistics for a specific table
139    pub async fn get_table_stats(&self, table_name: &str) -> Result<TableStats> {
140        // For dynamic table names, we use Box::leak to get a static string
141        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    /// List all user tables
154    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    /// Get database size in bytes
167    ///
168    /// Note: For SQLite, this returns the page count * page size
169    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    /// Get database size in megabytes
181    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    /// Get index information
187    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    /// Check database integrity
209    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    /// Optimize database (runs VACUUM and ANALYZE)
219    pub async fn optimize(&self) -> Result<()> {
220        self.vacuum().await?;
221        self.analyze().await?;
222        Ok(())
223    }
224
225    /// Get freelist count (unused pages)
226    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}