use parking_lot::RwLock;
use std::collections::{HashMap, HashSet};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use crate::sdbql::ast::{BodyClause, Query};
use crate::sdbql::parser;
#[derive(Clone)]
pub struct PreparedStatement {
pub query: Arc<Query>,
pub hash: String,
pub created_at: Instant,
pub use_count: u64,
}
struct CacheEntry {
stmt: Arc<PreparedStatement>,
collections: HashSet<String>,
}
#[derive(Default)]
struct Inner {
entries: HashMap<String, CacheEntry>,
by_collection: HashMap<String, HashSet<String>>,
}
impl Inner {
fn remove_entry(&mut self, key: &str) {
if let Some(entry) = self.entries.remove(key) {
for coll in &entry.collections {
let emptied = match self.by_collection.get_mut(coll) {
Some(set) => {
set.remove(key);
set.is_empty()
}
None => false,
};
if emptied {
self.by_collection.remove(coll);
}
}
}
}
}
pub struct PreparedStatementCache {
inner: RwLock<Inner>,
max_entries: usize,
ttl: Duration,
hits: AtomicU64,
misses: AtomicU64,
}
impl PreparedStatementCache {
pub fn new(max_entries: usize, ttl_secs: u64) -> Self {
Self {
inner: RwLock::new(Inner::default()),
max_entries,
ttl: Duration::from_secs(ttl_secs),
hits: AtomicU64::new(0),
misses: AtomicU64::new(0),
}
}
pub fn get(&self, query_text: &str) -> Option<Arc<PreparedStatement>> {
let hash = Self::hash_query(query_text);
let inner = self.inner.read();
if let Some(entry) = inner.entries.get(&hash) {
if entry.stmt.created_at.elapsed() < self.ttl {
self.hits.fetch_add(1, Ordering::Relaxed);
return Some(entry.stmt.clone());
}
}
None
}
pub fn put(&self, query_text: &str, query: Query) -> Arc<PreparedStatement> {
let hash = Self::hash_query(query_text);
let collections = collect_collection_names(&query);
let stmt = Arc::new(PreparedStatement {
query: Arc::new(query),
hash: hash.clone(),
created_at: Instant::now(),
use_count: 0,
});
let mut inner = self.inner.write();
if inner.entries.len() >= self.max_entries {
let keys_to_remove: Vec<String> = inner
.entries
.keys()
.take(self.max_entries / 2)
.cloned()
.collect();
for key in &keys_to_remove {
inner.remove_entry(key);
}
}
for coll in &collections {
inner
.by_collection
.entry(coll.clone())
.or_default()
.insert(hash.clone());
}
inner.entries.insert(
hash,
CacheEntry {
stmt: stmt.clone(),
collections,
},
);
stmt
}
pub fn parse_if_needed(
&self,
query_text: &str,
) -> crate::error::DbResult<Arc<PreparedStatement>> {
if let Some(stmt) = self.get(query_text) {
Ok(stmt)
} else {
self.misses.fetch_add(1, Ordering::Relaxed);
let query = parser::parse(query_text)?;
Ok(self.put(query_text, query))
}
}
pub fn hash_query(query: &str) -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
query.hash(&mut hasher);
format!("{:016x}", hasher.finish())
}
pub fn stats(&self) -> (u64, u64, usize) {
let hits = self.hits.load(Ordering::Relaxed);
let misses = self.misses.load(Ordering::Relaxed);
let size = self.inner.read().entries.len();
(hits, misses, size)
}
pub fn invalidate_all(&self) {
let mut inner = self.inner.write();
inner.entries.clear();
inner.by_collection.clear();
self.hits.store(0, Ordering::Relaxed);
self.misses.store(0, Ordering::Relaxed);
}
pub fn invalidate_collection(&self, collection_name: &str) {
let mut inner = self.inner.write();
let Some(to_remove) = inner.by_collection.remove(collection_name) else {
return;
};
for key in &to_remove {
inner.remove_entry(key);
}
}
#[cfg(test)]
fn index_len(&self) -> usize {
self.inner.read().by_collection.len()
}
}
impl Default for PreparedStatementCache {
fn default() -> Self {
Self::new(1000, 300)
}
}
fn collect_collection_names(query: &Query) -> HashSet<String> {
let mut out = HashSet::new();
for clause in &query.body_clauses {
if let BodyClause::For(for_clause) = clause {
out.insert(for_clause.collection.clone());
}
}
out
}
use std::sync::OnceLock;
static PREPARED_STATEMENT_CACHE: OnceLock<PreparedStatementCache> = OnceLock::new();
pub fn get_prepared_statement_cache() -> &'static PreparedStatementCache {
PREPARED_STATEMENT_CACHE.get_or_init(PreparedStatementCache::default)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_basic() {
let cache = PreparedStatementCache::default();
let query = "FOR doc IN users RETURN doc";
let stmt1 = cache.parse_if_needed(query).unwrap();
let stmt2 = cache.parse_if_needed(query).unwrap();
assert_eq!(stmt1.hash, stmt2.hash);
let (hits, misses, _) = cache.stats();
assert_eq!(hits, 1);
assert_eq!(misses, 1);
}
#[test]
fn test_cache_miss() {
let cache = PreparedStatementCache::default();
let stmt1 = cache
.parse_if_needed("FOR doc IN users RETURN doc")
.unwrap();
let stmt2 = cache
.parse_if_needed("FOR doc IN orders RETURN doc")
.unwrap();
assert_ne!(stmt1.hash, stmt2.hash);
let (hits, misses, _) = cache.stats();
assert_eq!(hits, 0);
assert_eq!(misses, 2);
}
#[test]
fn test_invalidate_collection_uses_index() {
let cache = PreparedStatementCache::default();
let _ = cache
.parse_if_needed("FOR doc IN users RETURN doc")
.unwrap();
let _ = cache
.parse_if_needed("FOR doc IN orders RETURN doc")
.unwrap();
let (_, _, size_before) = cache.stats();
assert_eq!(size_before, 2);
cache.invalidate_collection("users");
let (_, _, size_after) = cache.stats();
assert_eq!(size_after, 1);
assert_eq!(cache.index_len(), 1);
let stmt = cache
.parse_if_needed("FOR doc IN orders RETURN doc")
.unwrap();
let (hits, _, _) = cache.stats();
assert!(hits >= 1);
assert_eq!(stmt.query.body_clauses.len(), 1);
}
#[test]
fn test_eviction_prunes_index() {
let cache = PreparedStatementCache::new(2, 300);
let _ = cache.parse_if_needed("FOR doc IN a RETURN doc").unwrap();
let _ = cache.parse_if_needed("FOR doc IN b RETURN doc").unwrap();
let _ = cache.parse_if_needed("FOR doc IN c RETURN doc").unwrap();
let (_, _, size) = cache.stats();
assert!(size <= 2);
assert!(cache.index_len() <= size);
}
}