use std::num::NonZeroUsize;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use lru::LruCache;
use oxisql_core::OxiSqlError;
use crate::decorrelate::PlannerOptions;
use crate::parameterize::parameterize;
use crate::{optimizer, parse_one, plan_statement_with_opts, LogicalPlan};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct PlanCacheKey {
template: String,
generation: u64,
}
pub struct PlanCache {
inner: Mutex<LruCache<PlanCacheKey, Arc<LogicalPlan>>>,
generation: AtomicU64,
}
impl PlanCache {
pub fn new(capacity: usize) -> Self {
let cap = NonZeroUsize::new(capacity.max(1)).unwrap_or(NonZeroUsize::MIN);
Self {
inner: Mutex::new(LruCache::new(cap)),
generation: AtomicU64::new(0),
}
}
pub fn invalidate_schema(&self) {
self.generation.fetch_add(1, Ordering::Release);
}
pub fn plan(&self, sql: &str) -> Result<Arc<LogicalPlan>, OxiSqlError> {
self.plan_with(sql, &PlannerOptions::default())
}
pub fn plan_with(
&self,
sql: &str,
opts: &PlannerOptions,
) -> Result<Arc<LogicalPlan>, OxiSqlError> {
let template = parameterize(sql).template;
let generation = self.generation.load(Ordering::Acquire);
let key = PlanCacheKey {
template,
generation,
};
{
let mut cache = self
.inner
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if let Some(plan) = cache.get(&key) {
return Ok(Arc::clone(plan));
}
}
let stmt = parse_one(sql)?;
let raw = plan_statement_with_opts(&stmt, opts)?;
let optimized = Arc::new(optimizer::optimize(raw));
{
let mut cache = self
.inner
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let entry = cache.get_or_insert(key, || Arc::clone(&optimized));
Ok(Arc::clone(entry))
}
}
pub fn len(&self) -> usize {
self.inner
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn clear(&self) {
self.inner
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.clear();
}
}
impl std::fmt::Debug for PlanCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let len = self.len();
let gen = self.generation.load(Ordering::Relaxed);
write!(f, "PlanCache(len={len}, generation={gen})")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_same_template_single_entry() {
let cache = PlanCache::new(16);
cache.plan("SELECT * FROM t WHERE id = 1").unwrap();
cache.plan("SELECT * FROM t WHERE id = 2").unwrap();
assert_eq!(
cache.len(),
1,
"both queries should map to the same template"
);
}
#[test]
fn test_different_structure_two_entries() {
let cache = PlanCache::new(16);
cache.plan("SELECT * FROM t WHERE id = 1").unwrap();
cache.plan("SELECT * FROM t WHERE name = 'x'").unwrap();
assert_eq!(cache.len(), 2);
}
#[test]
fn test_invalidate_schema_causes_miss() {
let cache = PlanCache::new(16);
cache.plan("SELECT * FROM t WHERE id = 1").unwrap();
assert_eq!(cache.len(), 1);
cache.invalidate_schema();
cache.plan("SELECT * FROM t WHERE id = 1").unwrap();
assert_eq!(cache.len(), 2);
}
#[test]
fn test_concurrent_access() {
use std::sync::Arc;
use std::thread;
let cache = Arc::new(PlanCache::new(16));
let sql = "SELECT * FROM t WHERE id = 42";
let handles: Vec<_> = (0..8)
.map(|_| {
let c = Arc::clone(&cache);
thread::spawn(move || c.plan(sql).expect("plan should succeed in thread"))
})
.collect();
for h in handles {
h.join().expect("thread should not panic");
}
assert_eq!(cache.len(), 1);
}
#[test]
fn test_clear() {
let cache = PlanCache::new(16);
cache.plan("SELECT * FROM t WHERE id = 1").unwrap();
assert_eq!(cache.len(), 1);
cache.clear();
assert!(cache.is_empty());
}
#[test]
fn test_plan_with_custom_opts() {
let cache = PlanCache::new(16);
let opts = PlannerOptions { decorrelate: false };
cache
.plan_with("SELECT * FROM t WHERE id = 99", &opts)
.unwrap();
assert_eq!(cache.len(), 1);
}
}