Skip to main content

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}