Skip to main content

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}