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> = std::sync::LazyLock::new(DocInterner::new);
225
226/// Interner for documentation strings.
227#[derive(Debug, Default)]
228struct DocInterner {
229    strings: RwLock<HashMap<u64, Arc<str>>>,
230}
231
232impl DocInterner {
233    fn new() -> Self {
234        Self::default()
235    }
236
237    fn intern(&self, s: &str) -> DocString {
238        let hash = hash_source(s);
239
240        // Check if already interned
241        {
242            let strings = self.strings.read();
243            if let Some(arc) = strings.get(&hash) {
244                return DocString(Arc::clone(arc));
245            }
246        }
247
248        // Intern the string
249        let arc: Arc<str> = Arc::from(s);
250        {
251            let mut strings = self.strings.write();
252            strings.insert(hash, Arc::clone(&arc));
253        }
254
255        DocString(arc)
256    }
257}
258
259// ============================================================================
260// Lazy Field Attributes
261// ============================================================================
262
263/// Lazily computed field attributes.
264///
265/// Caches expensive attribute extraction to avoid repeated computation.
266#[derive(Debug, Clone, Default)]
267pub struct LazyFieldAttrs {
268    computed: std::sync::OnceLock<FieldAttrsCache>,
269}
270
271/// Cached field attribute values.
272#[derive(Debug, Clone, Default)]
273pub struct FieldAttrsCache {
274    /// Is this an ID field?
275    pub is_id: bool,
276    /// Is this an auto-generated field?
277    pub is_auto: bool,
278    /// Is this a unique field?
279    pub is_unique: bool,
280    /// Is this an indexed field?
281    pub is_indexed: bool,
282    /// Is this an updated_at field?
283    pub is_updated_at: bool,
284    /// Default value expression (if any).
285    pub default_value: Option<String>,
286    /// Mapped column name (if different from field name).
287    pub mapped_name: Option<String>,
288}
289
290impl LazyFieldAttrs {
291    /// Create new lazy field attributes.
292    pub const fn new() -> Self {
293        Self {
294            computed: std::sync::OnceLock::new(),
295        }
296    }
297
298    /// Get or compute the cached attributes.
299    pub fn get_or_init<F>(&self, f: F) -> &FieldAttrsCache
300    where
301        F: FnOnce() -> FieldAttrsCache,
302    {
303        self.computed.get_or_init(f)
304    }
305
306    /// Check if attributes have been computed.
307    pub fn is_computed(&self) -> bool {
308        self.computed.get().is_some()
309    }
310
311    /// Clear the cached attributes (creates a new instance).
312    pub fn reset(&mut self) {
313        *self = Self::new();
314    }
315}
316
317// ============================================================================
318// Optimized Validation Type Pool
319// ============================================================================
320
321/// Pool of commonly used validation types.
322///
323/// Pre-allocates common validation type combinations to avoid repeated
324/// allocation during schema parsing.
325#[derive(Debug, Default)]
326pub struct ValidationTypePool {
327    /// String validation (email, url, etc.)
328    pub string_validators: HashMap<&'static str, Arc<ValidatorDef>>,
329    /// Numeric validation (min, max, etc.)
330    pub numeric_validators: HashMap<&'static str, Arc<ValidatorDef>>,
331}
332
333/// A cached validator definition.
334#[derive(Debug, Clone)]
335pub struct ValidatorDef {
336    /// Validator name.
337    pub name: &'static str,
338    /// Validator type.
339    pub validator_type: ValidatorType,
340}
341
342/// Type of validator.
343#[derive(Debug, Clone, Copy, PartialEq, Eq)]
344pub enum ValidatorType {
345    /// String format validator (email, url, uuid, etc.)
346    StringFormat,
347    /// String length validator.
348    StringLength,
349    /// Numeric range validator.
350    NumericRange,
351    /// Regex pattern validator.
352    Pattern,
353    /// Custom validator.
354    Custom,
355}
356
357impl ValidationTypePool {
358    /// Create a new pool with common validators pre-allocated.
359    pub fn new() -> Self {
360        let mut pool = Self::default();
361        pool.init_common_validators();
362        pool
363    }
364
365    fn init_common_validators(&mut self) {
366        // Common string format validators
367        let string_formats = [
368            "email", "url", "uuid", "cuid", "cuid2", "nanoid", "ulid", "ipv4", "ipv6", "date",
369            "datetime", "time",
370        ];
371
372        for name in string_formats {
373            self.string_validators.insert(
374                name,
375                Arc::new(ValidatorDef {
376                    name,
377                    validator_type: ValidatorType::StringFormat,
378                }),
379            );
380        }
381
382        // Common numeric validators
383        let numeric_validators = [
384            "min",
385            "max",
386            "positive",
387            "negative",
388            "nonNegative",
389            "nonPositive",
390        ];
391        for name in numeric_validators {
392            self.numeric_validators.insert(
393                name,
394                Arc::new(ValidatorDef {
395                    name,
396                    validator_type: ValidatorType::NumericRange,
397                }),
398            );
399        }
400    }
401
402    /// Get a string format validator.
403    pub fn get_string_validator(&self, name: &str) -> Option<&Arc<ValidatorDef>> {
404        self.string_validators.get(name)
405    }
406
407    /// Get a numeric validator.
408    pub fn get_numeric_validator(&self, name: &str) -> Option<&Arc<ValidatorDef>> {
409        self.numeric_validators.get(name)
410    }
411}
412
413/// Global validation type pool.
414pub static VALIDATION_POOL: std::sync::LazyLock<ValidationTypePool> =
415    std::sync::LazyLock::new(ValidationTypePool::new);
416
417#[cfg(test)]
418mod tests {
419    use super::*;
420
421    #[test]
422    fn test_schema_cache_hit() {
423        let cache = SchemaCache::new();
424
425        let schema1 = cache.get_or_parse("model User { id Int @id }").unwrap();
426        let schema2 = cache.get_or_parse("model User { id Int @id }").unwrap();
427
428        // Should be the same Arc
429        assert!(Arc::ptr_eq(&schema1, &schema2));
430
431        let stats = cache.stats();
432        assert_eq!(stats.hits, 1);
433        assert_eq!(stats.misses, 1);
434    }
435
436    #[test]
437    fn test_schema_cache_miss() {
438        let cache = SchemaCache::new();
439
440        let _ = cache.get_or_parse("model User { id Int @id }").unwrap();
441        let _ = cache.get_or_parse("model Post { id Int @id }").unwrap();
442
443        let stats = cache.stats();
444        assert_eq!(stats.hits, 0);
445        assert_eq!(stats.misses, 2);
446    }
447
448    #[test]
449    fn test_schema_cache_clear() {
450        let cache = SchemaCache::new();
451
452        let _ = cache.get_or_parse("model User { id Int @id }").unwrap();
453        assert_eq!(cache.len(), 1);
454
455        cache.clear();
456        assert_eq!(cache.len(), 0);
457    }
458
459    #[test]
460    fn test_doc_string_interning() {
461        let doc1 = DocString::intern("User profile information");
462        let doc2 = DocString::intern("User profile information");
463
464        // Should share the same Arc
465        assert!(Arc::ptr_eq(doc1.as_arc(), doc2.as_arc()));
466    }
467
468    #[test]
469    fn test_doc_string_different() {
470        let doc1 = DocString::intern("User profile");
471        let doc2 = DocString::intern("Post content");
472
473        assert_ne!(doc1.as_str(), doc2.as_str());
474    }
475
476    #[test]
477    fn test_lazy_field_attrs() {
478        let lazy = LazyFieldAttrs::new();
479
480        assert!(!lazy.is_computed());
481
482        let attrs = lazy.get_or_init(|| FieldAttrsCache {
483            is_id: true,
484            is_auto: true,
485            ..Default::default()
486        });
487
488        assert!(lazy.is_computed());
489        assert!(attrs.is_id);
490        assert!(attrs.is_auto);
491    }
492
493    #[test]
494    fn test_validation_pool() {
495        let pool = ValidationTypePool::new();
496
497        assert!(pool.get_string_validator("email").is_some());
498        assert!(pool.get_string_validator("url").is_some());
499        assert!(pool.get_numeric_validator("min").is_some());
500        assert!(pool.get_numeric_validator("max").is_some());
501    }
502
503    #[test]
504    fn test_cache_stats_hit_rate() {
505        let stats = CacheStats {
506            hits: 8,
507            misses: 2,
508            cached_count: 5,
509        };
510
511        assert!((stats.hit_rate() - 0.8).abs() < 0.001);
512    }
513
514    #[test]
515    fn test_cache_stats_zero() {
516        let stats = CacheStats::default();
517        assert_eq!(stats.hit_rate(), 0.0);
518    }
519}