1use crate::app_paths::AppPaths;
2use chrono::{DateTime, Local};
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use sha2::{Digest, Sha256};
6use std::error::Error;
7use std::fs;
8use std::path::PathBuf;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct CachedQuery {
12 pub id: u64,
13 pub query_hash: String,
14 pub query_text: String,
15 pub timestamp: DateTime<Local>,
16 pub row_count: usize,
17 pub file_path: String,
18 pub description: Option<String>,
19 pub expires_at: Option<DateTime<Local>>,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct CacheMetadata {
24 pub queries: Vec<CachedQuery>,
25 pub next_id: u64,
26}
27
28pub struct QueryCache {
29 cache_dir: PathBuf,
30 metadata_path: PathBuf,
31 metadata: CacheMetadata,
32}
33
34impl QueryCache {
35 pub fn new() -> Result<Self, Box<dyn Error>> {
36 let cache_dir = AppPaths::cache_dir()?;
37 let data_dir = AppPaths::cache_data_dir()?;
38
39 fs::create_dir_all(&data_dir)?;
41
42 let metadata_path = AppPaths::cache_metadata_file()?;
43
44 let metadata = if metadata_path.exists() {
46 let content = fs::read_to_string(&metadata_path)?;
47 serde_json::from_str(&content)?
48 } else {
49 CacheMetadata {
50 queries: Vec::new(),
51 next_id: 1,
52 }
53 };
54
55 Ok(Self {
56 cache_dir,
57 metadata_path,
58 metadata,
59 })
60 }
61
62 pub fn save_query(
63 &mut self,
64 query: &str,
65 data: &[Value],
66 description: Option<String>,
67 ) -> Result<u64, Box<dyn Error>> {
68 let mut hasher = Sha256::new();
70 hasher.update(query.as_bytes());
71 let query_hash = format!("{:x}", hasher.finalize());
72
73 if let Some(existing) = self
75 .metadata
76 .queries
77 .iter()
78 .find(|q| q.query_hash == query_hash)
79 {
80 return Ok(existing.id);
81 }
82
83 let id = self.metadata.next_id;
85 let filename = format!("query_{id:06}.json");
86 let file_path = self.cache_dir.join("data").join(&filename);
87
88 let json_data = serde_json::to_string_pretty(&data)?;
90 fs::write(&file_path, json_data)?;
91
92 let cached_query = CachedQuery {
94 id,
95 query_hash,
96 query_text: query.to_string(),
97 timestamp: Local::now(),
98 row_count: data.len(),
99 file_path: filename,
100 description,
101 expires_at: None, };
103
104 self.metadata.queries.push(cached_query);
105 self.metadata.next_id += 1;
106
107 self.save_metadata()?;
109
110 Ok(id)
111 }
112
113 pub fn load_query(&self, id: u64) -> Result<(String, Vec<Value>), Box<dyn Error>> {
114 let cached_query = self
115 .metadata
116 .queries
117 .iter()
118 .find(|q| q.id == id)
119 .ok_or(format!("Cache entry {id} not found"))?;
120
121 let file_path = self.cache_dir.join("data").join(&cached_query.file_path);
122 let json_data = fs::read_to_string(file_path)?;
123 let data: Vec<Value> = serde_json::from_str(&json_data)?;
124
125 Ok((cached_query.query_text.clone(), data))
126 }
127
128 #[must_use]
129 pub fn list_cached_queries(&self) -> &[CachedQuery] {
130 &self.metadata.queries
131 }
132
133 pub fn delete_query(&mut self, id: u64) -> Result<(), Box<dyn Error>> {
134 if let Some(pos) = self.metadata.queries.iter().position(|q| q.id == id) {
135 let cached_query = self.metadata.queries.remove(pos);
136 let file_path = self.cache_dir.join("data").join(&cached_query.file_path);
137 fs::remove_file(file_path)?;
138 self.save_metadata()?;
139 }
140 Ok(())
141 }
142
143 pub fn clear_all(&mut self) -> Result<(), Box<dyn Error>> {
144 let data_dir = self.cache_dir.join("data");
146 for entry in fs::read_dir(data_dir)? {
147 let entry = entry?;
148 if entry.path().extension().is_some_and(|ext| ext == "json") {
149 fs::remove_file(entry.path())?;
150 }
151 }
152
153 self.metadata.queries.clear();
155 self.metadata.next_id = 1;
156 self.save_metadata()?;
157
158 Ok(())
159 }
160
161 #[must_use]
162 pub fn get_cache_stats(&self) -> CacheStats {
163 let total_size: u64 = self
164 .metadata
165 .queries
166 .iter()
167 .filter_map(|q| {
168 let path = self.cache_dir.join("data").join(&q.file_path);
169 fs::metadata(path).ok().map(|m| m.len())
170 })
171 .sum();
172
173 let total_rows: usize = self.metadata.queries.iter().map(|q| q.row_count).sum();
174
175 CacheStats {
176 total_queries: self.metadata.queries.len(),
177 total_rows,
178 total_size_bytes: total_size,
179 oldest_entry: self
180 .metadata
181 .queries
182 .iter()
183 .min_by_key(|q| q.timestamp)
184 .map(|q| q.timestamp),
185 newest_entry: self
186 .metadata
187 .queries
188 .iter()
189 .max_by_key(|q| q.timestamp)
190 .map(|q| q.timestamp),
191 }
192 }
193
194 fn save_metadata(&self) -> Result<(), Box<dyn Error>> {
195 let json = serde_json::to_string_pretty(&self.metadata)?;
196 fs::write(&self.metadata_path, json)?;
197 Ok(())
198 }
199}
200
201#[derive(Debug)]
202pub struct CacheStats {
203 pub total_queries: usize,
204 pub total_rows: usize,
205 pub total_size_bytes: u64,
206 pub oldest_entry: Option<DateTime<Local>>,
207 pub newest_entry: Option<DateTime<Local>>,
208}
209
210impl CacheStats {
211 #[must_use]
212 pub fn format_size(&self) -> String {
213 let size = self.total_size_bytes as f64;
214 if size < 1024.0 {
215 format!("{size} B")
216 } else if size < 1024.0 * 1024.0 {
217 format!("{:.1} KB", size / 1024.0)
218 } else if size < 1024.0 * 1024.0 * 1024.0 {
219 format!("{:.1} MB", size / (1024.0 * 1024.0))
220 } else {
221 format!("{:.1} GB", size / (1024.0 * 1024.0 * 1024.0))
222 }
223 }
224}
225
226#[derive(Debug, Clone, PartialEq, Default)]
227pub enum QueryMode {
228 #[default]
229 Live, Cached, Hybrid, }