prax_schema/
cache.rs

1//! Schema caching for improved performance.
2//!
3//! This module provides:
4//! - Schema caching to avoid re-parsing
5//! - String interning for documentation strings
6//! - Lazy computed fields
7//!
8//! # Examples
9//!
10//! ```rust
11//! use prax_schema::cache::{SchemaCache, DocString};
12//!
13//! // Cache parsed schemas
14//! let mut cache = SchemaCache::new();
15//!
16//! let schema = cache.get_or_parse("model User { id Int @id }").unwrap();
17//! let schema2 = cache.get_or_parse("model User { id Int @id }").unwrap();
18//! // schema2 is the same Arc as schema (cached)
19//!
20//! // Intern documentation strings
21//! let doc1 = DocString::intern("User profile information");
22//! let doc2 = DocString::intern("User profile information");
23//! // doc1 and doc2 share the same allocation
24//! ```
25
26use parking_lot::RwLock;
27use std::collections::HashMap;
28use std::hash::{Hash, Hasher};
29use std::sync::Arc;
30
31use crate::ast::Schema;
32use crate::error::SchemaResult;
33use crate::parser::parse_schema;
34
35// ============================================================================
36// Schema Cache
37// ============================================================================
38
39/// A cache for parsed schemas.
40///
41/// Caches parsed schemas by their source text hash to avoid re-parsing
42/// identical schemas.
43#[derive(Debug, Default)]
44pub struct SchemaCache {
45    cache: RwLock<HashMap<u64, Arc<Schema>>>,
46    stats: RwLock<CacheStats>,
47}
48
49/// Statistics for the schema cache.
50#[derive(Debug, Clone, Default)]
51pub struct CacheStats {
52    /// Number of cache hits.
53    pub hits: u64,
54    /// Number of cache misses.
55    pub misses: u64,
56    /// Number of schemas currently cached.
57    pub cached_count: usize,
58}
59
60impl CacheStats {
61    /// Get the cache hit rate.
62    pub fn hit_rate(&self) -> f64 {
63        let total = self.hits + self.misses;
64        if total == 0 {
65            0.0
66        } else {
67            self.hits as f64 / total as f64
68        }
69    }
70}
71
72impl SchemaCache {
73    /// Create a new empty schema cache.
74    pub fn new() -> Self {
75        Self::default()
76    }
77
78    /// Create a cache with pre-allocated capacity.
79    pub fn with_capacity(capacity: usize) -> Self {
80        Self {
81            cache: RwLock::new(HashMap::with_capacity(capacity)),
82            stats: RwLock::default(),
83        }
84    }
85
86    /// Get a cached schema or parse and cache a new one.
87    ///
88    /// Returns an `Arc<Schema>` which can be cloned cheaply.
89    pub fn get_or_parse(&self, source: &str) -> SchemaResult<Arc<Schema>> {
90        let hash = hash_source(source);
91
92        // Try to get from cache first
93        {
94            let cache = self.cache.read();
95            if let Some(schema) = cache.get(&hash) {
96                self.stats.write().hits += 1;
97                return Ok(Arc::clone(schema));
98            }
99        }
100
101        // Parse and cache
102        let schema = parse_schema(source)?;
103        let schema = Arc::new(schema);
104
105        {
106            let mut cache = self.cache.write();
107            cache.insert(hash, Arc::clone(&schema));
108        }
109
110        {
111            let mut stats = self.stats.write();
112            stats.misses += 1;
113            stats.cached_count = self.cache.read().len();
114        }
115
116        Ok(schema)
117    }
118
119    /// Check if a schema is cached.
120    pub fn contains(&self, source: &str) -> bool {
121        let hash = hash_source(source);
122        self.cache.read().contains_key(&hash)
123    }
124
125    /// Clear the cache.
126    pub fn clear(&self) {
127        self.cache.write().clear();
128        self.stats.write().cached_count = 0;
129    }
130
131    /// Get cache statistics.
132    pub fn stats(&self) -> CacheStats {
133        let mut stats = self.stats.read().clone();
134        stats.cached_count = self.cache.read().len();
135        stats
136    }
137
138    /// Get the number of cached schemas.
139    pub fn len(&self) -> usize {
140        self.cache.read().len()
141    }
142
143    /// Check if the cache is empty.
144    pub fn is_empty(&self) -> bool {
145        self.cache.read().is_empty()
146    }
147
148    /// Remove a specific schema from the cache.
149    pub fn remove(&self, source: &str) -> bool {
150        let hash = hash_source(source);
151        self.cache.write().remove(&hash).is_some()
152    }
153}
154
155/// Hash a source string for caching.
156fn hash_source(source: &str) -> u64 {
157    use std::collections::hash_map::DefaultHasher;
158    let mut hasher = DefaultHasher::new();
159    source.hash(&mut hasher);
160    hasher.finish()
161}
162
163// ============================================================================
164// Documentation String Interning
165// ============================================================================
166
167/// An interned documentation string.
168///
169/// Uses `Arc<str>` for efficient sharing of identical documentation strings.
170/// Documentation comments are often duplicated across models (e.g., "id" field docs).
171#[derive(Debug, Clone, PartialEq, Eq, Hash)]
172pub struct DocString(Arc<str>);
173
174impl DocString {
175    /// Create a new documentation string (not interned).
176    pub fn new(s: impl AsRef<str>) -> Self {
177        Self(Arc::from(s.as_ref()))
178    }
179
180    /// Intern a documentation string.
181    ///
182    /// Returns a shared reference to the string if it's already interned,
183    /// or interns and returns a new reference.
184    pub fn intern(s: impl AsRef<str>) -> Self {
185        DOC_INTERNER.intern(s.as_ref())
186    }
187
188    /// Get the string as a slice.
189    pub fn as_str(&self) -> &str {
190        &self.0
191    }
192
193    /// Get the underlying Arc.
194    pub fn as_arc(&self) -> &Arc<str> {
195        &self.0
196    }
197}
198
199impl AsRef<str> for DocString {
200    fn as_ref(&self) -> &str {
201        &self.0
202    }
203}
204
205impl std::fmt::Display for DocString {
206    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
207        write!(f, "{}", self.0)
208    }
209}
210
211impl From<&str> for DocString {
212    fn from(s: &str) -> Self {
213        Self::new(s)
214    }
215}
216
217impl From<String> for DocString {
218    fn from(s: String) -> Self {
219        Self(Arc::from(s))
220    }
221}
222
223/// Global documentation string interner.
224static DOC_INTERNER: std::sync::LazyLock<DocInterner> =
225    std::sync::LazyLock::new(DocInterner::new);
226
227/// Interner for documentation strings.
228#[derive(Debug, Default)]
229struct DocInterner {
230    strings: RwLock<HashMap<u64, Arc<str>>>,
231}
232
233impl DocInterner {
234    fn new() -> Self {
235        Self::default()
236    }
237
238    fn intern(&self, s: &str) -> DocString {
239        let hash = hash_source(s);
240
241        // Check if already interned
242        {
243            let strings = self.strings.read();
244            if let Some(arc) = strings.get(&hash) {
245                return DocString(Arc::clone(arc));
246            }
247        }
248
249        // Intern the string
250        let arc: Arc<str> = Arc::from(s);
251        {
252            let mut strings = self.strings.write();
253            strings.insert(hash, Arc::clone(&arc));
254        }
255
256        DocString(arc)
257    }
258}
259
260// ============================================================================
261// Lazy Field Attributes
262// ============================================================================
263
264/// Lazily computed field attributes.
265///
266/// Caches expensive attribute extraction to avoid repeated computation.
267#[derive(Debug, Clone, Default)]
268pub struct LazyFieldAttrs {
269    computed: std::sync::OnceLock<FieldAttrsCache>,
270}
271
272/// Cached field attribute values.
273#[derive(Debug, Clone, Default)]
274pub struct FieldAttrsCache {
275    /// Is this an ID field?
276    pub is_id: bool,
277    /// Is this an auto-generated field?
278    pub is_auto: bool,
279    /// Is this a unique field?
280    pub is_unique: bool,
281    /// Is this an indexed field?
282    pub is_indexed: bool,
283    /// Is this an updated_at field?
284    pub is_updated_at: bool,
285    /// Default value expression (if any).
286    pub default_value: Option<String>,
287    /// Mapped column name (if different from field name).
288    pub mapped_name: Option<String>,
289}
290
291impl LazyFieldAttrs {
292    /// Create new lazy field attributes.
293    pub const fn new() -> Self {
294        Self {
295            computed: std::sync::OnceLock::new(),
296        }
297    }
298
299    /// Get or compute the cached attributes.
300    pub fn get_or_init<F>(&self, f: F) -> &FieldAttrsCache
301    where
302        F: FnOnce() -> FieldAttrsCache,
303    {
304        self.computed.get_or_init(f)
305    }
306
307    /// Check if attributes have been computed.
308    pub fn is_computed(&self) -> bool {
309        self.computed.get().is_some()
310    }
311
312    /// Clear the cached attributes (creates a new instance).
313    pub fn reset(&mut self) {
314        *self = Self::new();
315    }
316}
317
318// ============================================================================
319// Optimized Validation Type Pool
320// ============================================================================
321
322/// Pool of commonly used validation types.
323///
324/// Pre-allocates common validation type combinations to avoid repeated
325/// allocation during schema parsing.
326#[derive(Debug, Default)]
327pub struct ValidationTypePool {
328    /// String validation (email, url, etc.)
329    pub string_validators: HashMap<&'static str, Arc<ValidatorDef>>,
330    /// Numeric validation (min, max, etc.)
331    pub numeric_validators: HashMap<&'static str, Arc<ValidatorDef>>,
332}
333
334/// A cached validator definition.
335#[derive(Debug, Clone)]
336pub struct ValidatorDef {
337    /// Validator name.
338    pub name: &'static str,
339    /// Validator type.
340    pub validator_type: ValidatorType,
341}
342
343/// Type of validator.
344#[derive(Debug, Clone, Copy, PartialEq, Eq)]
345pub enum ValidatorType {
346    /// String format validator (email, url, uuid, etc.)
347    StringFormat,
348    /// String length validator.
349    StringLength,
350    /// Numeric range validator.
351    NumericRange,
352    /// Regex pattern validator.
353    Pattern,
354    /// Custom validator.
355    Custom,
356}
357
358impl ValidationTypePool {
359    /// Create a new pool with common validators pre-allocated.
360    pub fn new() -> Self {
361        let mut pool = Self::default();
362        pool.init_common_validators();
363        pool
364    }
365
366    fn init_common_validators(&mut self) {
367        // Common string format validators
368        let string_formats = [
369            "email", "url", "uuid", "cuid", "cuid2", "nanoid", "ulid",
370            "ipv4", "ipv6", "date", "datetime", "time",
371        ];
372
373        for name in string_formats {
374            self.string_validators.insert(name, Arc::new(ValidatorDef {
375                name,
376                validator_type: ValidatorType::StringFormat,
377            }));
378        }
379
380        // Common numeric validators
381        let numeric_validators = ["min", "max", "positive", "negative", "nonNegative", "nonPositive"];
382        for name in numeric_validators {
383            self.numeric_validators.insert(name, Arc::new(ValidatorDef {
384                name,
385                validator_type: ValidatorType::NumericRange,
386            }));
387        }
388    }
389
390    /// Get a string format validator.
391    pub fn get_string_validator(&self, name: &str) -> Option<&Arc<ValidatorDef>> {
392        self.string_validators.get(name)
393    }
394
395    /// Get a numeric validator.
396    pub fn get_numeric_validator(&self, name: &str) -> Option<&Arc<ValidatorDef>> {
397        self.numeric_validators.get(name)
398    }
399}
400
401/// Global validation type pool.
402pub static VALIDATION_POOL: std::sync::LazyLock<ValidationTypePool> =
403    std::sync::LazyLock::new(ValidationTypePool::new);
404
405#[cfg(test)]
406mod tests {
407    use super::*;
408
409    #[test]
410    fn test_schema_cache_hit() {
411        let cache = SchemaCache::new();
412
413        let schema1 = cache.get_or_parse("model User { id Int @id }").unwrap();
414        let schema2 = cache.get_or_parse("model User { id Int @id }").unwrap();
415
416        // Should be the same Arc
417        assert!(Arc::ptr_eq(&schema1, &schema2));
418
419        let stats = cache.stats();
420        assert_eq!(stats.hits, 1);
421        assert_eq!(stats.misses, 1);
422    }
423
424    #[test]
425    fn test_schema_cache_miss() {
426        let cache = SchemaCache::new();
427
428        let _ = cache.get_or_parse("model User { id Int @id }").unwrap();
429        let _ = cache.get_or_parse("model Post { id Int @id }").unwrap();
430
431        let stats = cache.stats();
432        assert_eq!(stats.hits, 0);
433        assert_eq!(stats.misses, 2);
434    }
435
436    #[test]
437    fn test_schema_cache_clear() {
438        let cache = SchemaCache::new();
439
440        let _ = cache.get_or_parse("model User { id Int @id }").unwrap();
441        assert_eq!(cache.len(), 1);
442
443        cache.clear();
444        assert_eq!(cache.len(), 0);
445    }
446
447    #[test]
448    fn test_doc_string_interning() {
449        let doc1 = DocString::intern("User profile information");
450        let doc2 = DocString::intern("User profile information");
451
452        // Should share the same Arc
453        assert!(Arc::ptr_eq(doc1.as_arc(), doc2.as_arc()));
454    }
455
456    #[test]
457    fn test_doc_string_different() {
458        let doc1 = DocString::intern("User profile");
459        let doc2 = DocString::intern("Post content");
460
461        assert_ne!(doc1.as_str(), doc2.as_str());
462    }
463
464    #[test]
465    fn test_lazy_field_attrs() {
466        let lazy = LazyFieldAttrs::new();
467
468        assert!(!lazy.is_computed());
469
470        let attrs = lazy.get_or_init(|| FieldAttrsCache {
471            is_id: true,
472            is_auto: true,
473            ..Default::default()
474        });
475
476        assert!(lazy.is_computed());
477        assert!(attrs.is_id);
478        assert!(attrs.is_auto);
479    }
480
481    #[test]
482    fn test_validation_pool() {
483        let pool = ValidationTypePool::new();
484
485        assert!(pool.get_string_validator("email").is_some());
486        assert!(pool.get_string_validator("url").is_some());
487        assert!(pool.get_numeric_validator("min").is_some());
488        assert!(pool.get_numeric_validator("max").is_some());
489    }
490
491    #[test]
492    fn test_cache_stats_hit_rate() {
493        let stats = CacheStats {
494            hits: 8,
495            misses: 2,
496            cached_count: 5,
497        };
498
499        assert!((stats.hit_rate() - 0.8).abs() < 0.001);
500    }
501
502    #[test]
503    fn test_cache_stats_zero() {
504        let stats = CacheStats::default();
505        assert_eq!(stats.hit_rate(), 0.0);
506    }
507}
508