use super::CypherQuery;
use std::collections::{HashMap, VecDeque};
use std::sync::{Arc, OnceLock, RwLock};
const CACHE_CAPACITY: usize = 512;
type PlanKey = (u64, u64, bool, u64);
struct PlanCache {
map: HashMap<PlanKey, Arc<CypherQuery>>,
order: VecDeque<PlanKey>,
}
impl PlanCache {
fn new() -> Self {
Self {
map: HashMap::with_capacity(CACHE_CAPACITY),
order: VecDeque::with_capacity(CACHE_CAPACITY),
}
}
}
static CACHE: OnceLock<RwLock<PlanCache>> = OnceLock::new();
fn cache() -> &'static RwLock<PlanCache> {
CACHE.get_or_init(|| RwLock::new(PlanCache::new()))
}
fn hash_query(query: &str) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
query.hash(&mut hasher);
hasher.finish()
}
pub fn get(graph_id: u64, version: u64, lazy: bool, query: &str) -> Option<Arc<CypherQuery>> {
let key = (graph_id, version, lazy, hash_query(query));
let guard = cache().read().expect("plan_cache RwLock poisoned");
guard.map.get(&key).map(Arc::clone)
}
pub fn insert(graph_id: u64, version: u64, lazy: bool, query: &str, plan: Arc<CypherQuery>) {
let key = (graph_id, version, lazy, hash_query(query));
let mut guard = cache().write().expect("plan_cache RwLock poisoned");
if guard.map.contains_key(&key) {
return; }
if guard.map.len() >= CACHE_CAPACITY {
if let Some(oldest) = guard.order.pop_front() {
guard.map.remove(&oldest);
}
}
guard.order.push_back(key);
guard.map.insert(key, plan);
}
#[cfg(test)]
pub fn clear_for_tests() {
let mut guard = cache().write().expect("plan_cache RwLock poisoned");
guard.map.clear();
guard.order.clear();
}
#[cfg(test)]
pub fn entry_count_for_tests() -> usize {
cache()
.read()
.expect("plan_cache RwLock poisoned")
.map
.len()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::languages::cypher::parser::parse_cypher;
use std::sync::Mutex;
static TEST_LOCK: Mutex<()> = Mutex::new(());
fn plan(q: &str) -> Arc<CypherQuery> {
Arc::new(parse_cypher(q).expect("parse"))
}
#[test]
fn miss_then_hit_same_key() {
let _g = TEST_LOCK.lock().unwrap_or_else(|p| p.into_inner());
clear_for_tests();
let q = "MATCH (n:T) RETURN n";
assert!(get(1, 0, false, q).is_none(), "cold miss");
insert(1, 0, false, q, plan(q));
assert!(get(1, 0, false, q).is_some(), "warm hit");
}
#[test]
fn version_graph_id_and_lazy_partition_the_key() {
let _g = TEST_LOCK.lock().unwrap_or_else(|p| p.into_inner());
clear_for_tests();
let q = "MATCH (n:T) RETURN n";
insert(7, 3, false, q, plan(q));
assert!(get(7, 4, false, q).is_none(), "version change invalidates");
assert!(
get(8, 3, false, q).is_none(),
"different graph never collides"
);
assert!(get(7, 3, true, q).is_none(), "lazy mode is part of the key");
assert!(get(7, 3, false, q).is_some(), "exact key hits");
}
#[test]
fn evicts_at_capacity() {
let _g = TEST_LOCK.lock().unwrap_or_else(|p| p.into_inner());
clear_for_tests();
for i in 0..(CACHE_CAPACITY as u64 + 5) {
insert(
1,
i,
false,
"MATCH (n:T) RETURN n",
plan("MATCH (n:T) RETURN n"),
);
}
assert_eq!(entry_count_for_tests(), CACHE_CAPACITY);
}
}