sqry_cli/commands/
cache.rs1use crate::args::{CacheAction, Cli};
4use anyhow::Result;
5use sqry_core::cache::{CacheConfig, CacheManager, PruneOptions, PruneOutputMode, PruneReport};
6use std::path::PathBuf;
7use std::time::Duration;
8
9pub fn run_cache(cli: &Cli, action: &CacheAction) -> Result<()> {
14 match action {
15 CacheAction::Stats { path } => {
16 let search_path = path.as_deref().unwrap_or(".");
17 show_cache_stats(cli, search_path)
18 }
19 CacheAction::Clear { path, confirm } => {
20 let search_path = path.as_deref().unwrap_or(".");
21 clear_cache(cli, search_path, *confirm);
22 Ok(())
23 }
24 CacheAction::Prune {
25 days,
26 size,
27 dry_run,
28 path,
29 } => prune_cache(cli, *days, size.as_deref(), *dry_run, path.as_deref()),
30 }
31}
32
33fn show_cache_stats(cli: &Cli, _path: &str) -> Result<()> {
35 let config = CacheConfig::from_env();
37 let cache = CacheManager::new(config);
38 let stats = cache.stats();
39
40 if cli.json {
41 let json_stats = serde_json::json!({
43 "ast_cache": {
44 "hits": stats.hits,
45 "misses": stats.misses,
46 "evictions": stats.evictions,
47 "entry_count": stats.entry_count,
48 "total_bytes": stats.total_bytes,
49 "total_mb": bytes_to_mb_lossy(stats.total_bytes),
50 "hit_rate": stats.hit_rate(),
51 },
52 });
53 println!("{}", serde_json::to_string_pretty(&json_stats)?);
54 } else {
55 println!("AST Cache Statistics");
57 println!("====================");
58 println!();
59 println!("Performance:");
60 println!(" Hit rate: {:.1}%", stats.hit_rate() * 100.0);
61 println!(" Hits: {}", stats.hits);
62 println!(" Misses: {}", stats.misses);
63 println!(" Evictions: {}", stats.evictions);
64 println!();
65 println!("Storage:");
66 println!(" Entries: {}", stats.entry_count);
67 println!(
68 " Memory: {:.2} MB",
69 bytes_to_mb_lossy(stats.total_bytes)
70 );
71 println!();
72
73 print_cache_effectiveness(stats.hits, stats.misses);
75
76 let cache_root =
78 std::env::var("SQRY_CACHE_ROOT").unwrap_or_else(|_| ".sqry-cache".to_string());
79 println!("Cache location: {cache_root}");
80
81 let disk_usage = get_disk_usage(&cache_root);
83 println!();
84 println!("Disk Usage:");
85 println!(" Files: {}", disk_usage.file_count);
86 println!(
87 " Total size: {:.2} MB",
88 bytes_to_mb_lossy(disk_usage.bytes)
89 );
90 }
91
92 Ok(())
93}
94
95fn print_cache_effectiveness(hits: usize, misses: usize) {
97 if hits + misses > 0 {
98 let total_accesses = hits + misses;
99 let avg_savings_ms = 50; let time_saved_ms = hits * avg_savings_ms;
101 let time_saved_sec = time_saved_ms / 1000;
102
103 println!("Estimated Impact:");
104 println!(" Total accesses: {total_accesses}");
105 println!(" Time saved: ~{time_saved_sec} seconds ({time_saved_ms} ms)");
106 println!();
107 }
108}
109
110struct DiskUsage {
111 file_count: usize,
112 bytes: u64,
113}
114
115fn get_disk_usage(cache_root: &str) -> DiskUsage {
116 use walkdir::WalkDir;
117
118 let mut file_count = 0;
119 let mut total_bytes = 0u64;
120
121 for entry in WalkDir::new(cache_root)
122 .into_iter()
123 .filter_map(std::result::Result::ok)
124 .filter(|e| e.file_type().is_file())
125 {
126 if let Ok(metadata) = entry.metadata() {
127 total_bytes += metadata.len();
128 file_count += 1;
129 }
130 }
131
132 DiskUsage {
133 file_count,
134 bytes: total_bytes,
135 }
136}
137
138fn u64_to_f64_lossy(value: u64) -> f64 {
139 let narrowed = u32::try_from(value).unwrap_or(u32::MAX);
140 f64::from(narrowed)
141}
142
143fn bytes_to_mb_lossy(bytes: u64) -> f64 {
144 u64_to_f64_lossy(bytes) / 1_048_576.0
145}
146
147fn clear_cache(_cli: &Cli, _path: &str, confirm: bool) {
149 if !confirm {
150 eprintln!("Error: Cache clear requires --confirm flag for safety");
151 eprintln!();
152 eprintln!("This will delete all cached AST data. Next queries will re-parse files.");
153 eprintln!();
154 eprintln!("To proceed, run:");
155 eprintln!(" sqry cache clear --confirm");
156 std::process::exit(1);
157 }
158
159 let config = CacheConfig::from_env();
161 let cache = CacheManager::new(config);
162
163 let stats_before = cache.stats();
165
166 cache.clear();
167
168 let stats_after = cache.stats();
170
171 println!("Cache cleared successfully");
172 println!();
173 println!("Removed:");
174 println!(" Entries: {}", stats_before.entry_count);
175 println!(
176 " Memory: {:.2} MB",
177 bytes_to_mb_lossy(stats_before.total_bytes)
178 );
179 println!();
180 println!("Current stats:");
181 println!(" Entries: {}", stats_after.entry_count);
182 println!(
183 " Memory: {:.2} MB",
184 bytes_to_mb_lossy(stats_after.total_bytes)
185 );
186}
187
188fn prune_cache(
190 cli: &Cli,
191 days: Option<u64>,
192 size_str: Option<&str>,
193 dry_run: bool,
194 path: Option<&str>,
195) -> Result<()> {
196 let options = build_prune_options(cli, days, size_str, dry_run, path)?;
197 let report = execute_cache_prune(&options)?;
198 write_prune_report(cli, dry_run, &report)?;
199
200 Ok(())
201}
202
203fn parse_byte_size(s: &str) -> Result<u64> {
205 let s = s.trim().to_uppercase();
206
207 let (num_str, unit) = if s.ends_with("GB") {
209 (&s[..s.len() - 2], 1024 * 1024 * 1024)
210 } else if s.ends_with("MB") {
211 (&s[..s.len() - 2], 1024 * 1024)
212 } else if s.ends_with("KB") {
213 (&s[..s.len() - 2], 1024)
214 } else if s.ends_with('B') {
215 (&s[..s.len() - 1], 1)
216 } else {
217 (&s[..], 1)
219 };
220
221 let num: u64 = num_str.trim().parse().map_err(|_| {
222 anyhow::anyhow!("Invalid size format {s}. Expected formats: 1GB, 500MB, 100KB")
223 })?;
224
225 Ok(num * unit)
226}
227
228fn build_prune_options(
229 cli: &Cli,
230 days: Option<u64>,
231 size_str: Option<&str>,
232 dry_run: bool,
233 path: Option<&str>,
234) -> Result<PruneOptions> {
235 let max_size = size_str.map(parse_byte_size).transpose()?;
237
238 let max_age = days.map(|d| Duration::from_secs(d * 24 * 3600));
240
241 let mut options = PruneOptions::new();
243
244 if let Some(age) = max_age {
245 options = options.with_max_age(age);
246 }
247
248 if let Some(size) = max_size {
249 options = options.with_max_size(size);
250 }
251
252 options = options.with_dry_run(dry_run);
253
254 let output_mode = if cli.json {
255 PruneOutputMode::Json
256 } else {
257 PruneOutputMode::Human
258 };
259 options = options.with_output_mode(output_mode);
260
261 if let Some(p) = path {
262 options = options.with_target_dir(PathBuf::from(p));
263 }
264
265 Ok(options)
266}
267
268fn execute_cache_prune(options: &PruneOptions) -> Result<PruneReport> {
269 let config = CacheConfig::from_env();
270 let cache = CacheManager::new(config);
271 cache.prune(options)
272}
273
274fn write_prune_report(cli: &Cli, dry_run: bool, report: &PruneReport) -> Result<()> {
275 if cli.json {
276 println!("{}", serde_json::to_string_pretty(report)?);
277 return Ok(());
278 }
279
280 let header = if dry_run {
281 "Cache Prune Preview (Dry Run)"
282 } else {
283 "Cache Prune Report"
284 };
285 println!("{header}");
286 println!("====================");
287 println!();
288
289 if report.entries_removed == 0 {
290 println!("No entries removed");
291 println!("Cache is within configured limits");
292 return Ok(());
293 }
294
295 println!("Entries:");
296 println!(" Considered: {}", report.entries_considered);
297 println!(" Removed: {}", report.entries_removed);
298 println!(" Remaining: {}", report.remaining_entries);
299 println!();
300 println!("Space:");
301 println!(
302 " Reclaimed: {:.2} MB",
303 bytes_to_mb_lossy(report.bytes_removed)
304 );
305 println!(
306 " Remaining: {:.2} MB",
307 bytes_to_mb_lossy(report.remaining_bytes)
308 );
309
310 if dry_run {
311 println!();
312 println!("Run without --dry-run to actually delete files");
313 }
314
315 Ok(())
316}