sql_cli/sql/
cache.rs

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        // Create directories if they don't exist
40        fs::create_dir_all(&data_dir)?;
41
42        let metadata_path = AppPaths::cache_metadata_file()?;
43
44        // Load or create metadata
45        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        // Generate hash for query
69        let mut hasher = Sha256::new();
70        hasher.update(query.as_bytes());
71        let query_hash = format!("{:x}", hasher.finalize());
72
73        // Check if query already cached
74        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        // Generate filename
84        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        // Save data to file
89        let json_data = serde_json::to_string_pretty(&data)?;
90        fs::write(&file_path, json_data)?;
91
92        // Add to metadata
93        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, // Could add TTL logic here
102        };
103
104        self.metadata.queries.push(cached_query);
105        self.metadata.next_id += 1;
106
107        // Save metadata
108        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        // Remove all data files
145        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        // Clear metadata
154        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, // Always query server
230    Cached, // Only use cached data
231    Hybrid, // Check cache first, then server
232}