1use crate::Database;
2use anyhow::Result;
3use serde::Serialize;
4
5#[derive(Debug, Serialize)]
6pub struct DatabaseStats {
7 pub file_size_bytes: i64,
9 pub file_size_human: String,
10
11 pub sqlite_version: String,
13 pub journal_mode: String,
14 pub auto_vacuum: String,
15 pub cache_size: i64,
16 pub page_size: i64,
17 pub page_count: i64,
18 pub freelist_count: i64,
19 pub encoding: String,
20
21 pub wal_checkpoint: Option<WalCheckpointStats>,
23
24 pub tables: Vec<TableStats>,
26
27 pub indexes: Vec<IndexStats>,
29
30 pub pool_size: u32,
32 pub pool_idle: u32,
33
34 pub integrity_check: String,
36
37 pub compile_options: Vec<String>,
39
40 pub memory_used: i64,
42 pub memory_high_water: i64,
43
44 pub cache_hit: i64,
46 pub cache_miss: i64,
47 pub cache_write: i64,
48 pub cache_spill: i64,
49}
50
51#[derive(Debug, Serialize)]
52pub struct WalCheckpointStats {
53 pub wal_pages: i64,
54 pub wal_frames_checkpointed: i64,
55}
56
57#[derive(Debug, Serialize)]
58pub struct TableStats {
59 pub name: String,
60 pub row_count: i64,
61 pub size_estimate: String,
62}
63
64#[derive(Debug, Serialize)]
65pub struct IndexStats {
66 pub name: String,
67 pub table_name: String,
68 pub is_unique: bool,
69 pub columns: String,
70}
71
72pub fn format_bytes(bytes: i64) -> String {
73 const KB: i64 = 1024;
74 const MB: i64 = KB * 1024;
75 const GB: i64 = MB * 1024;
76
77 if bytes >= GB {
78 format!("{:.2} GB", bytes as f64 / GB as f64)
79 } else if bytes >= MB {
80 format!("{:.2} MB", bytes as f64 / MB as f64)
81 } else if bytes >= KB {
82 format!("{:.2} KB", bytes as f64 / KB as f64)
83 } else {
84 format!("{} bytes", bytes)
85 }
86}
87
88pub fn get_database_stats(db: &Database, db_path: &str) -> Result<DatabaseStats> {
89 let conn = db.get()?;
90
91 let file_size_bytes = std::fs::metadata(db_path)
93 .map(|m| m.len() as i64)
94 .unwrap_or(0);
95 let file_size_human = format_bytes(file_size_bytes);
96
97 let sqlite_version: String = conn.query_row("SELECT sqlite_version()", [], |row| row.get(0))?;
99
100 let journal_mode: String = conn.query_row("PRAGMA journal_mode", [], |row| row.get(0))?;
102
103 let auto_vacuum: i64 = conn.query_row("PRAGMA auto_vacuum", [], |row| row.get(0))?;
104 let auto_vacuum = match auto_vacuum {
105 0 => "none".to_string(),
106 1 => "full".to_string(),
107 2 => "incremental".to_string(),
108 _ => "unknown".to_string(),
109 };
110
111 let cache_size: i64 = conn.query_row("PRAGMA cache_size", [], |row| row.get(0))?;
112
113 let page_size: i64 = conn.query_row("PRAGMA page_size", [], |row| row.get(0))?;
114
115 let page_count: i64 = conn.query_row("PRAGMA page_count", [], |row| row.get(0))?;
116
117 let freelist_count: i64 = conn.query_row("PRAGMA freelist_count", [], |row| row.get(0))?;
118
119 let encoding: String = conn.query_row("PRAGMA encoding", [], |row| row.get(0))?;
120
121 let wal_checkpoint = if journal_mode.to_lowercase() == "wal" {
123 let result: Result<(i64, i64), _> =
125 conn.query_row("PRAGMA wal_checkpoint(PASSIVE)", [], |row| {
126 Ok((row.get(1)?, row.get(2)?))
127 });
128 result.ok().map(|(pages, frames)| WalCheckpointStats {
129 wal_pages: pages,
130 wal_frames_checkpointed: frames,
131 })
132 } else {
133 None
134 };
135
136 let mut tables = Vec::new();
138 {
139 let mut stmt = conn.prepare(
140 "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
141 )?;
142 let table_names: Vec<String> = stmt
143 .query_map([], |row| row.get(0))?
144 .filter_map(|r| r.ok())
145 .collect();
146
147 for table_name in table_names {
148 let row_count: i64 = conn
150 .query_row(
151 &format!("SELECT COUNT(*) FROM \"{}\"", table_name),
152 [],
153 |row| row.get(0),
154 )
155 .unwrap_or(0);
156
157 let size_estimate = format!("~{} rows", row_count);
159
160 tables.push(TableStats {
161 name: table_name,
162 row_count,
163 size_estimate,
164 });
165 }
166 }
167
168 let mut indexes = Vec::new();
170 {
171 let mut stmt = conn.prepare(
172 "SELECT name, tbl_name, sql FROM sqlite_master WHERE type='index' AND name NOT LIKE 'sqlite_%' ORDER BY tbl_name, name"
173 )?;
174 let rows = stmt.query_map([], |row| {
175 Ok((
176 row.get::<_, String>(0)?,
177 row.get::<_, String>(1)?,
178 row.get::<_, Option<String>>(2)?,
179 ))
180 })?;
181
182 for row in rows.flatten() {
183 let (name, table_name, sql) = row;
184 let is_unique = sql
185 .as_ref()
186 .map(|s| s.to_uppercase().contains("UNIQUE"))
187 .unwrap_or(false);
188
189 let columns: String = conn
191 .query_row(&format!("PRAGMA index_info(\"{}\")", name), [], |row| {
192 row.get::<_, String>(2)
193 })
194 .unwrap_or_else(|_| {
195 let mut cols = Vec::new();
197 if let Ok(mut idx_stmt) =
198 conn.prepare(&format!("PRAGMA index_info(\"{}\")", name))
199 {
200 if let Ok(idx_rows) = idx_stmt.query_map([], |r| r.get::<_, String>(2)) {
201 cols = idx_rows.filter_map(|r| r.ok()).collect();
202 }
203 }
204 cols.join(", ")
205 });
206
207 indexes.push(IndexStats {
208 name,
209 table_name,
210 is_unique,
211 columns,
212 });
213 }
214 }
215
216 let integrity_check: String = conn
218 .query_row("PRAGMA quick_check(1)", [], |row| row.get(0))
219 .unwrap_or_else(|_| "unknown".to_string());
220
221 let mut compile_options = Vec::new();
223 {
224 let mut stmt = conn.prepare("PRAGMA compile_options")?;
225 let rows = stmt.query_map([], |row| row.get::<_, String>(0))?;
226 for row in rows.flatten() {
227 compile_options.push(row);
228 }
229 }
230
231 let memory_used: i64 = conn
233 .query_row("SELECT total_changes()", [], |row| row.get(0))
234 .unwrap_or(0);
235
236 let memory_high_water: i64 = 0;
238
239 let (cache_hit, cache_miss, cache_write, cache_spill) = get_cache_stats(&conn);
241
242 let pool_size = 10u32; let pool_idle = 0u32; Ok(DatabaseStats {
247 file_size_bytes,
248 file_size_human,
249 sqlite_version,
250 journal_mode,
251 auto_vacuum,
252 cache_size,
253 page_size,
254 page_count,
255 freelist_count,
256 encoding,
257 wal_checkpoint,
258 tables,
259 indexes,
260 pool_size,
261 pool_idle,
262 integrity_check,
263 compile_options,
264 memory_used,
265 memory_high_water,
266 cache_hit,
267 cache_miss,
268 cache_write,
269 cache_spill,
270 })
271}
272
273fn get_cache_stats(conn: &rusqlite::Connection) -> (i64, i64, i64, i64) {
274 let cache_hit: i64 = conn
276 .query_row(
277 "SELECT * FROM pragma_database_list WHERE name='main'",
278 [],
279 |_| Ok(0i64),
280 )
281 .unwrap_or(0);
282
283 (cache_hit, 0, 0, 0)
286}
287
288#[derive(Debug, Serialize)]
289pub struct DatabaseAnalysis {
290 pub fragmentation_percent: f64,
291 pub wasted_space_bytes: i64,
292 pub wasted_space_human: String,
293 pub recommendations: Vec<String>,
294}
295
296pub fn analyze_database(db: &Database, db_path: &str) -> Result<DatabaseAnalysis> {
297 let stats = get_database_stats(db, db_path)?;
298
299 let mut recommendations = Vec::new();
300
301 let fragmentation_percent = if stats.page_count > 0 {
303 (stats.freelist_count as f64 / stats.page_count as f64) * 100.0
304 } else {
305 0.0
306 };
307
308 let wasted_space_bytes = stats.freelist_count * stats.page_size;
309 let wasted_space_human = format_bytes(wasted_space_bytes);
310
311 if fragmentation_percent > 10.0 {
312 recommendations.push(format!(
313 "Database fragmentation is {:.1}%. Consider running VACUUM to reclaim {} of space.",
314 fragmentation_percent, wasted_space_human
315 ));
316 }
317
318 if stats.auto_vacuum == "none" && stats.file_size_bytes > 10 * 1024 * 1024 {
320 recommendations.push(
321 "Auto-vacuum is disabled. Consider enabling it for automatic space reclamation."
322 .to_string(),
323 );
324 }
325
326 if stats.cache_size.abs() < 2000 && stats.file_size_bytes > 50 * 1024 * 1024 {
328 recommendations.push(
329 "Cache size is relatively small for the database size. Consider increasing cache_size for better performance."
330 .to_string(),
331 );
332 }
333
334 if stats.integrity_check != "ok" {
336 recommendations.push(format!(
337 "Integrity check returned: {}. Database may have corruption issues.",
338 stats.integrity_check
339 ));
340 }
341
342 for table in &stats.tables {
344 if table.row_count > 1000 {
345 let has_index = stats.indexes.iter().any(|i| i.table_name == table.name);
346 if !has_index && !table.name.ends_with("_fts") && !table.name.contains("_content") {
347 recommendations.push(format!(
348 "Table '{}' has {} rows but no indexes. Consider adding indexes for frequently queried columns.",
349 table.name, table.row_count
350 ));
351 }
352 }
353 }
354
355 if recommendations.is_empty() {
356 recommendations.push("Database is healthy. No recommendations at this time.".to_string());
357 }
358
359 Ok(DatabaseAnalysis {
360 fragmentation_percent,
361 wasted_space_bytes,
362 wasted_space_human,
363 recommendations,
364 })
365}
366
367pub fn run_vacuum(db: &Database) -> Result<()> {
368 let conn = db.get()?;
369 conn.execute_batch("VACUUM")?;
370 Ok(())
371}
372
373pub fn run_analyze(db: &Database) -> Result<()> {
374 let conn = db.get()?;
375 conn.execute_batch("ANALYZE")?;
376 Ok(())
377}
378
379pub fn run_integrity_check(db: &Database) -> Result<Vec<String>> {
380 let conn = db.get()?;
381 let mut stmt = conn.prepare("PRAGMA integrity_check")?;
382 let rows = stmt.query_map([], |row| row.get::<_, String>(0))?;
383 let results: Vec<String> = rows.filter_map(|r| r.ok()).collect();
384 Ok(results)
385}