oxisql_parse/cache.rs
1//! LRU AST cache for parsed SQL statements.
2
3use lru::LruCache;
4use oxisql_core::OxiSqlError;
5use sqlparser::ast::Statement;
6use std::num::NonZeroUsize;
7use std::sync::Mutex;
8
9use crate::{parse_with_dialect, SqlDialect};
10
11// ── LRU AST cache ───────────────────────────────────────────────────────────
12
13/// A thread-safe LRU cache for parsed SQL ASTs.
14///
15/// Avoids re-parsing identical SQL strings. Cache capacity is configurable.
16/// Both the SQL text and the [`SqlDialect`] are used as the cache key so that
17/// the same query text parsed under different dialects is stored separately.
18///
19/// # Example
20///
21/// ```rust
22/// use oxisql_parse::{ParseCache, SqlDialect};
23///
24/// let cache = ParseCache::new(32);
25/// let stmts = cache.parse("SELECT 1", SqlDialect::Generic).unwrap();
26/// assert_eq!(stmts.len(), 1);
27/// assert_eq!(cache.len(), 1);
28/// ```
29pub struct ParseCache {
30 inner: Mutex<LruCache<(String, SqlDialect), Vec<Statement>>>,
31}
32
33impl ParseCache {
34 /// Create a new cache with the given capacity.
35 ///
36 /// If `capacity` is 0 it is silently promoted to 1 so the cache is always
37 /// functional.
38 pub fn new(capacity: usize) -> Self {
39 let cap = NonZeroUsize::new(capacity.max(1)).unwrap_or(NonZeroUsize::MIN);
40 Self {
41 inner: Mutex::new(LruCache::new(cap)),
42 }
43 }
44
45 /// Parse `sql` with `dialect`, returning the cached result if available.
46 ///
47 /// On a cache miss the SQL is parsed and the result stored before returning.
48 pub fn parse(&self, sql: &str, dialect: SqlDialect) -> Result<Vec<Statement>, OxiSqlError> {
49 let key = (sql.to_string(), dialect);
50 {
51 let mut cache = self
52 .inner
53 .lock()
54 .unwrap_or_else(|poisoned| poisoned.into_inner());
55 if let Some(stmts) = cache.get(&key) {
56 return Ok(stmts.clone());
57 }
58 }
59 let result = parse_with_dialect(sql, key.1)?;
60 {
61 let mut cache = self
62 .inner
63 .lock()
64 .unwrap_or_else(|poisoned| poisoned.into_inner());
65 cache.put(key, result.clone());
66 }
67 Ok(result)
68 }
69
70 /// Return the number of currently cached entries.
71 pub fn len(&self) -> usize {
72 self.inner
73 .lock()
74 .unwrap_or_else(|poisoned| poisoned.into_inner())
75 .len()
76 }
77
78 /// Return `true` if the cache holds no entries.
79 pub fn is_empty(&self) -> bool {
80 self.len() == 0
81 }
82
83 /// Evict all cached entries.
84 pub fn clear(&self) {
85 self.inner
86 .lock()
87 .unwrap_or_else(|poisoned| poisoned.into_inner())
88 .clear();
89 }
90}
91
92impl std::fmt::Debug for ParseCache {
93 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94 let len = self.len();
95 write!(f, "ParseCache(len={len})")
96 }
97}