sqry_core/cache/config.rs
1//! Cache configuration types.
2//!
3//! This module defines configuration structures for controlling cache behavior,
4//! including size limits, persistence, eviction policy selection, and TTL policies.
5
6use super::policy::CachePolicyKind;
7use std::path::PathBuf;
8use std::time::Duration;
9
10/// Cache configuration.
11///
12/// Controls cache behavior including size limits, persistence, and location.
13///
14/// # Default Values
15///
16/// - **`max_bytes`**: 50 MB (52,428,800 bytes)
17/// - **`enable_persistence`**: `true`
18/// - **`cache_root`**: `.sqry-cache` (relative to working directory)
19/// - **`background_writer`**: `true` (use background thread for writes)
20///
21/// # Environment Variables
22///
23/// Configuration can be overridden via environment variables:
24/// - `SQRY_CACHE_MAX_BYTES`: Maximum cache size in bytes
25/// - `SQRY_CACHE_DISABLE_PERSIST`: Set to `1` to disable persistence
26/// - `SQRY_CACHE_ROOT`: Custom cache directory location
27/// - `SQRY_CACHE_POLICY`: `lru`, `tiny_lfu`, or `hybrid` eviction policy
28/// - `SQRY_CACHE_POLICY_WINDOW`: Protected window ratio for hybrid `TinyLFU` (float, e.g. `0.2`)
29///
30/// # Examples
31///
32/// ```rust
33/// use sqry_core::cache::CacheConfig;
34///
35/// // Default configuration
36/// let config = CacheConfig::default();
37/// assert_eq!(config.max_bytes(), 50 * 1024 * 1024); // 50 MB
38///
39/// // Custom configuration
40/// let config = CacheConfig::new()
41/// .with_max_bytes(100 * 1024 * 1024) // 100 MB
42/// .with_persistence(false); // Memory-only
43/// ```
44#[derive(Debug, Clone)]
45pub struct CacheConfig {
46 /// Maximum cache size in bytes (default: 50 MB).
47 max_bytes: u64,
48
49 /// Enable persistent cache to disk (default: true).
50 enable_persistence: bool,
51
52 /// Cache root directory (default: `.sqry-cache`).
53 cache_root: PathBuf,
54
55 /// Use background writer thread for persistence (default: true).
56 background_writer: bool,
57
58 /// Eviction policy selection (default: LRU).
59 policy_kind: CachePolicyKind,
60
61 /// Fraction of cache reserved for `TinyLFU` protected window (0.0–1.0).
62 policy_window_ratio: f32,
63}
64
65impl CacheConfig {
66 /// Default maximum cache size (50 MB).
67 pub const DEFAULT_MAX_BYTES: u64 = 50 * 1024 * 1024; // 50 MB
68
69 /// Default cache root directory.
70 pub const DEFAULT_CACHE_ROOT: &'static str = ".sqry-cache";
71
72 /// Default protected window ratio for hybrid/tiny LFU policies.
73 pub const DEFAULT_POLICY_WINDOW_RATIO: f32 = 0.20;
74 /// Minimum allowed protected window ratio for `TinyLFU` policies.
75 pub const MIN_POLICY_WINDOW_RATIO: f32 = 0.05;
76 /// Maximum allowed protected window ratio for `TinyLFU` policies.
77 pub const MAX_POLICY_WINDOW_RATIO: f32 = 0.95;
78
79 /// Create a new cache configuration with default values.
80 ///
81 /// # Examples
82 ///
83 /// ```rust
84 /// use sqry_core::cache::CacheConfig;
85 ///
86 /// let config = CacheConfig::new();
87 /// ```
88 #[must_use]
89 pub fn new() -> Self {
90 Self {
91 max_bytes: Self::DEFAULT_MAX_BYTES,
92 enable_persistence: true,
93 cache_root: PathBuf::from(Self::DEFAULT_CACHE_ROOT),
94 background_writer: true,
95 policy_kind: CachePolicyKind::default(),
96 policy_window_ratio: Self::DEFAULT_POLICY_WINDOW_RATIO,
97 }
98 }
99
100 /// Create configuration from environment variables.
101 ///
102 /// Reads configuration from:
103 /// - `SQRY_CACHE_MAX_BYTES`: Override max cache size
104 /// - `SQRY_CACHE_DISABLE_PERSIST`: Set to `1` to disable persistence
105 /// - `SQRY_CACHE_ROOT`: Custom cache directory
106 ///
107 /// Falls back to default values if environment variables are not set.
108 ///
109 /// # Examples
110 ///
111 /// ```rust
112 /// use sqry_core::cache::CacheConfig;
113 ///
114 /// // Reads from environment, falls back to defaults
115 /// let config = CacheConfig::from_env();
116 /// ```
117 #[must_use]
118 pub fn from_env() -> Self {
119 let mut config = Self::new();
120
121 // Override max_bytes from environment
122 if let Ok(max_bytes_str) = std::env::var("SQRY_CACHE_MAX_BYTES")
123 && let Ok(max_bytes) = max_bytes_str.parse::<u64>()
124 {
125 config = config.with_max_bytes(max_bytes);
126 }
127
128 // Override persistence from environment
129 if let Ok(disable_persist) = std::env::var("SQRY_CACHE_DISABLE_PERSIST")
130 && (disable_persist == "1" || disable_persist.eq_ignore_ascii_case("true"))
131 {
132 config = config.with_persistence(false);
133 }
134
135 // Override cache root from environment
136 if let Ok(cache_root) = std::env::var("SQRY_CACHE_ROOT") {
137 config = config.with_cache_root(PathBuf::from(cache_root));
138 }
139
140 // Override eviction policy from environment
141 if let Ok(policy_str) = std::env::var("SQRY_CACHE_POLICY") {
142 if let Some(kind) = CachePolicyKind::parse(&policy_str) {
143 config = config.with_policy_kind(kind);
144 } else {
145 log::warn!(
146 "Invalid SQRY_CACHE_POLICY='{policy_str}' (expected lru|tiny_lfu|hybrid), falling back to LRU"
147 );
148 }
149 }
150
151 if let Ok(window_ratio_str) = std::env::var("SQRY_CACHE_POLICY_WINDOW")
152 && let Ok(ratio) = window_ratio_str.parse::<f32>()
153 {
154 config = config.with_policy_window_ratio(ratio);
155 }
156
157 config
158 }
159
160 /// Set maximum cache size in bytes.
161 ///
162 /// # Examples
163 ///
164 /// ```rust
165 /// use sqry_core::cache::CacheConfig;
166 ///
167 /// let config = CacheConfig::new().with_max_bytes(100 * 1024 * 1024); // 100 MB
168 /// ```
169 #[must_use]
170 pub fn with_max_bytes(mut self, max_bytes: u64) -> Self {
171 self.max_bytes = max_bytes;
172 self
173 }
174
175 /// Enable or disable persistent cache to disk.
176 ///
177 /// # Examples
178 ///
179 /// ```rust
180 /// use sqry_core::cache::CacheConfig;
181 ///
182 /// // Memory-only cache
183 /// let config = CacheConfig::new().with_persistence(false);
184 /// ```
185 #[must_use]
186 pub fn with_persistence(mut self, enable: bool) -> Self {
187 self.enable_persistence = enable;
188 self
189 }
190
191 /// Set cache root directory.
192 ///
193 /// # Examples
194 ///
195 /// ```rust
196 /// use sqry_core::cache::CacheConfig;
197 /// use std::path::PathBuf;
198 ///
199 /// let config = CacheConfig::new().with_cache_root(PathBuf::from("/tmp/sqry-cache"));
200 /// ```
201 #[must_use]
202 pub fn with_cache_root(mut self, cache_root: PathBuf) -> Self {
203 self.cache_root = cache_root;
204 self
205 }
206
207 /// Enable or disable background writer thread.
208 ///
209 /// When enabled, cache writes are performed asynchronously to avoid
210 /// blocking query execution. Disable for testing or debugging.
211 ///
212 /// # Examples
213 ///
214 /// ```rust
215 /// use sqry_core::cache::CacheConfig;
216 ///
217 /// // Synchronous writes (useful for testing)
218 /// let config = CacheConfig::new().with_background_writer(false);
219 /// ```
220 #[must_use]
221 pub fn with_background_writer(mut self, enable: bool) -> Self {
222 self.background_writer = enable;
223 self
224 }
225
226 /// Override eviction policy kind.
227 #[must_use]
228 pub fn with_policy_kind(mut self, kind: CachePolicyKind) -> Self {
229 self.policy_kind = kind;
230 self
231 }
232
233 /// Override protected window ratio for hybrid/TinyLFU policies.
234 #[must_use]
235 pub fn with_policy_window_ratio(mut self, ratio: f32) -> Self {
236 self.policy_window_ratio = Self::clamp_window_ratio(ratio);
237 self
238 }
239
240 fn clamp_window_ratio(ratio: f32) -> f32 {
241 if ratio.is_nan() || !ratio.is_finite() {
242 Self::DEFAULT_POLICY_WINDOW_RATIO
243 } else {
244 ratio.clamp(Self::MIN_POLICY_WINDOW_RATIO, Self::MAX_POLICY_WINDOW_RATIO)
245 }
246 }
247
248 /// Get maximum cache size in bytes.
249 #[must_use]
250 pub fn max_bytes(&self) -> u64 {
251 self.max_bytes
252 }
253
254 /// Check if persistence is enabled.
255 #[must_use]
256 pub fn is_persistence_enabled(&self) -> bool {
257 self.enable_persistence
258 }
259
260 /// Get cache root directory.
261 #[must_use]
262 pub fn cache_root(&self) -> &PathBuf {
263 &self.cache_root
264 }
265
266 /// Check if background writer is enabled.
267 #[must_use]
268 pub fn is_background_writer_enabled(&self) -> bool {
269 self.background_writer
270 }
271
272 /// Get eviction policy kind.
273 #[must_use]
274 pub fn policy_kind(&self) -> CachePolicyKind {
275 self.policy_kind
276 }
277
278 /// Get protected window ratio for TinyLFU/hybrid policies.
279 #[must_use]
280 pub fn policy_window_ratio(&self) -> f32 {
281 self.policy_window_ratio
282 }
283}
284
285impl Default for CacheConfig {
286 fn default() -> Self {
287 Self::new()
288 }
289}
290
291/// Cache policy for language plugins.
292///
293/// Plugins can opt out of caching or specify custom TTL (time-to-live) values.
294///
295/// # Default Policy
296///
297/// By default, all plugins use `CachePolicy::Enabled` with a 1-hour TTL.
298///
299/// # Examples
300///
301/// ```rust,ignore
302/// use sqry_core::cache::CachePolicy;
303/// use sqry_core::plugin::LanguagePlugin;
304/// use std::time::Duration;
305///
306/// impl LanguagePlugin for RustPlugin {
307/// fn cache_policy(&self) -> CachePolicy {
308/// // Stable grammar - long TTL
309/// CachePolicy::Enabled {
310/// ttl: Duration::from_secs(3600) // 1 hour
311/// }
312/// }
313/// }
314///
315/// impl LanguagePlugin for ExperimentalPlugin {
316/// fn cache_policy(&self) -> CachePolicy {
317/// // Experimental - disable caching
318/// CachePolicy::Disabled
319/// }
320/// }
321/// ```
322#[derive(Debug, Clone, PartialEq, Eq)]
323pub enum CachePolicy {
324 /// Caching enabled with the specified time-to-live.
325 ///
326 /// Cache entries expire after the TTL and will be reparsed on next access.
327 Enabled {
328 /// Time-to-live for cache entries.
329 ttl: Duration,
330 },
331
332 /// Caching disabled for this plugin.
333 ///
334 /// All queries will trigger fresh parsing, bypassing the cache entirely.
335 Disabled,
336}
337
338impl CachePolicy {
339 /// Default TTL for enabled caching (1 hour).
340 pub const DEFAULT_TTL: Duration = Duration::from_secs(3600);
341
342 /// Create an enabled policy with the default TTL.
343 ///
344 /// # Examples
345 ///
346 /// ```rust
347 /// use sqry_core::cache::CachePolicy;
348 ///
349 /// let policy = CachePolicy::default_enabled();
350 /// ```
351 #[must_use]
352 pub fn default_enabled() -> Self {
353 Self::Enabled {
354 ttl: Self::DEFAULT_TTL,
355 }
356 }
357
358 /// Create an enabled policy with a custom TTL.
359 ///
360 /// # Examples
361 ///
362 /// ```rust
363 /// use sqry_core::cache::CachePolicy;
364 /// use std::time::Duration;
365 ///
366 /// let policy = CachePolicy::enabled(Duration::from_secs(7200)); // 2 hours
367 /// ```
368 #[must_use]
369 pub fn enabled(ttl: Duration) -> Self {
370 Self::Enabled { ttl }
371 }
372
373 /// Create a disabled policy.
374 ///
375 /// # Examples
376 ///
377 /// ```rust
378 /// use sqry_core::cache::CachePolicy;
379 ///
380 /// let policy = CachePolicy::disabled();
381 /// ```
382 #[must_use]
383 pub fn disabled() -> Self {
384 Self::Disabled
385 }
386
387 /// Check if caching is enabled.
388 #[must_use]
389 pub fn is_enabled(&self) -> bool {
390 matches!(self, Self::Enabled { .. })
391 }
392
393 /// Get the TTL if caching is enabled.
394 #[must_use]
395 pub fn ttl(&self) -> Option<Duration> {
396 match self {
397 Self::Enabled { ttl } => Some(*ttl),
398 Self::Disabled => None,
399 }
400 }
401}
402
403impl Default for CachePolicy {
404 fn default() -> Self {
405 Self::default_enabled()
406 }
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412
413 #[test]
414 fn test_cache_config_default() {
415 let config = CacheConfig::default();
416
417 assert_eq!(config.max_bytes(), CacheConfig::DEFAULT_MAX_BYTES);
418 assert!(config.is_persistence_enabled());
419 assert_eq!(config.cache_root(), &PathBuf::from(".sqry-cache"));
420 assert!(config.is_background_writer_enabled());
421 assert_eq!(config.policy_kind(), CachePolicyKind::Lru);
422 assert!(
423 (config.policy_window_ratio() - CacheConfig::DEFAULT_POLICY_WINDOW_RATIO).abs()
424 < f32::EPSILON
425 );
426 }
427
428 #[test]
429 fn test_cache_config_builder() {
430 let config = CacheConfig::new()
431 .with_max_bytes(100 * 1024 * 1024)
432 .with_persistence(false)
433 .with_cache_root(PathBuf::from("/tmp/cache"))
434 .with_background_writer(false)
435 .with_policy_kind(CachePolicyKind::TinyLfu)
436 .with_policy_window_ratio(0.5);
437
438 assert_eq!(config.max_bytes(), 100 * 1024 * 1024);
439 assert!(!config.is_persistence_enabled());
440 assert_eq!(config.cache_root(), &PathBuf::from("/tmp/cache"));
441 assert!(!config.is_background_writer_enabled());
442 assert_eq!(config.policy_kind(), CachePolicyKind::TinyLfu);
443 assert!((config.policy_window_ratio() - 0.5).abs() < f32::EPSILON);
444 }
445
446 #[test]
447 fn test_cache_config_from_env() {
448 // Set environment variables for this test
449 // SAFETY: We're in a test environment and immediately clean up after
450 unsafe {
451 std::env::set_var("SQRY_CACHE_MAX_BYTES", "104857600"); // 100 MB
452 std::env::set_var("SQRY_CACHE_DISABLE_PERSIST", "1");
453 std::env::set_var("SQRY_CACHE_ROOT", "/tmp/test-cache");
454 std::env::set_var("SQRY_CACHE_POLICY", "tiny_lfu");
455 std::env::set_var("SQRY_CACHE_POLICY_WINDOW", "0.33");
456 }
457
458 let config = CacheConfig::from_env();
459
460 assert_eq!(config.max_bytes(), 104_857_600);
461 assert!(!config.is_persistence_enabled());
462 assert_eq!(config.cache_root(), &PathBuf::from("/tmp/test-cache"));
463 assert_eq!(config.policy_kind(), CachePolicyKind::TinyLfu);
464 assert!((config.policy_window_ratio() - 0.33).abs() < f32::EPSILON);
465
466 // Clean up
467 // SAFETY: We set these variables above, safe to remove them
468 unsafe {
469 std::env::remove_var("SQRY_CACHE_MAX_BYTES");
470 std::env::remove_var("SQRY_CACHE_DISABLE_PERSIST");
471 std::env::remove_var("SQRY_CACHE_ROOT");
472 std::env::remove_var("SQRY_CACHE_POLICY");
473 std::env::remove_var("SQRY_CACHE_POLICY_WINDOW");
474 }
475 }
476
477 #[test]
478 fn test_policy_window_ratio_clamp() {
479 let config = CacheConfig::new()
480 .with_policy_window_ratio(0.99)
481 .with_policy_window_ratio(0.01)
482 .with_policy_window_ratio(f32::NAN);
483 assert!(
484 (config.policy_window_ratio() - CacheConfig::DEFAULT_POLICY_WINDOW_RATIO).abs()
485 < f32::EPSILON
486 );
487 }
488
489 #[test]
490 fn test_cache_policy_default() {
491 let policy = CachePolicy::default();
492
493 assert!(policy.is_enabled());
494 assert_eq!(policy.ttl(), Some(Duration::from_secs(3600)));
495 }
496
497 #[test]
498 fn test_cache_policy_enabled() {
499 let policy = CachePolicy::enabled(Duration::from_secs(7200));
500
501 assert!(policy.is_enabled());
502 assert_eq!(policy.ttl(), Some(Duration::from_secs(7200)));
503 }
504
505 #[test]
506 fn test_cache_policy_disabled() {
507 let policy = CachePolicy::disabled();
508
509 assert!(!policy.is_enabled());
510 assert_eq!(policy.ttl(), None);
511 }
512
513 #[test]
514 fn test_cache_policy_equality() {
515 let policy1 = CachePolicy::enabled(Duration::from_secs(3600));
516 let policy2 = CachePolicy::enabled(Duration::from_secs(3600));
517 let policy3 = CachePolicy::enabled(Duration::from_secs(7200));
518 let policy4 = CachePolicy::disabled();
519
520 assert_eq!(policy1, policy2);
521 assert_ne!(policy1, policy3);
522 assert_ne!(policy1, policy4);
523 }
524}