use std::collections::HashMap;
use std::sync::Arc;
use std::sync::Mutex;
use lora_ast::Document;
use lora_compiler::CompiledQuery;
const DEFAULT_CAPACITY: usize = 256;
const EPOCHS_PER_QUERY: usize = 2;
pub(crate) struct PlanCache {
inner: Mutex<Inner>,
}
struct Inner {
entries: HashMap<String, Slot>,
counter: u64,
capacity: usize,
len: usize,
}
struct Slot {
document: Option<Arc<Document>>,
plans: Vec<Entry>,
last_used: u64,
}
struct Entry {
store_epoch: u64,
plan: Arc<CompiledQuery>,
}
impl Default for PlanCache {
fn default() -> Self {
Self::new()
}
}
impl PlanCache {
pub(crate) fn new() -> Self {
Self::with_capacity(DEFAULT_CAPACITY)
}
pub(crate) fn with_capacity(capacity: usize) -> Self {
Self {
inner: Mutex::new(Inner {
entries: HashMap::with_capacity(capacity),
counter: 0,
capacity,
len: 0,
}),
}
}
fn lock(&self) -> std::sync::MutexGuard<'_, Inner> {
self.inner
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
pub(crate) fn get(&self, query: &str, store_epoch: u64) -> Option<Arc<CompiledQuery>> {
let mut guard = self.lock();
let counter = guard.counter.wrapping_add(1);
guard.counter = counter;
let slot = guard.entries.get_mut(query)?;
let plan = slot
.plans
.iter()
.find(|entry| entry.store_epoch == store_epoch)?
.plan
.clone();
slot.last_used = counter;
Some(plan)
}
pub(crate) fn document(&self, query: &str) -> Option<Arc<Document>> {
self.lock()
.entries
.get(query)
.and_then(|slot| slot.document.clone())
}
#[cfg(test)]
pub(crate) fn insert(&self, query: &str, store_epoch: u64, plan: Arc<CompiledQuery>) {
self.insert_with_document(query, store_epoch, None, plan);
}
pub(crate) fn insert_with_document(
&self,
query: &str,
store_epoch: u64,
document: Option<Arc<Document>>,
plan: Arc<CompiledQuery>,
) {
let mut guard = self.lock();
if guard.capacity == 0 {
return;
}
let counter = guard.counter.wrapping_add(1);
guard.counter = counter;
if let Some(slot) = guard.entries.get_mut(query) {
slot.last_used = counter;
if slot.document.is_none() {
slot.document = document;
}
if let Some(entry) = slot
.plans
.iter_mut()
.find(|entry| entry.store_epoch == store_epoch)
{
entry.plan = plan;
return;
}
if slot.plans.len() >= EPOCHS_PER_QUERY {
let (oldest_idx, oldest_epoch) = slot
.plans
.iter()
.enumerate()
.map(|(idx, entry)| (idx, entry.store_epoch))
.min_by_key(|&(_, epoch)| epoch)
.expect("slot holds plans");
if store_epoch > oldest_epoch {
slot.plans[oldest_idx] = Entry { store_epoch, plan };
}
return;
}
slot.plans.push(Entry { store_epoch, plan });
guard.len += 1;
while guard.len > guard.capacity {
if !evict_oldest(&mut guard, Some(query)) {
break;
}
}
return;
}
while guard.len >= guard.capacity {
if !evict_oldest(&mut guard, None) {
break;
}
}
guard.entries.insert(
query.to_owned(),
Slot {
document,
plans: vec![Entry { store_epoch, plan }],
last_used: counter,
},
);
guard.len += 1;
}
#[cfg(test)]
pub(crate) fn len(&self) -> usize {
self.lock().len
}
}
fn evict_oldest(guard: &mut Inner, keep: Option<&str>) -> bool {
let oldest = guard
.entries
.iter()
.filter(|(query, _)| Some(query.as_str()) != keep)
.min_by_key(|(_, slot)| slot.last_used)
.map(|(query, _)| query.clone());
let Some(query) = oldest else {
return false;
};
if let Some(slot) = guard.entries.remove(&query) {
guard.len = guard.len.saturating_sub(slot.plans.len());
}
true
}
#[cfg(test)]
mod tests {
use super::*;
use lora_compiler::{PhysicalOp, PhysicalPlan};
fn dummy_plan() -> Arc<CompiledQuery> {
Arc::new(CompiledQuery {
physical: PhysicalPlan {
root: 0,
nodes: vec![PhysicalOp::Argument(lora_compiler::ArgumentExec)],
},
unions: Vec::new(),
})
}
#[test]
fn miss_then_hit() {
let cache = PlanCache::new();
let q = "MATCH (n) RETURN n";
assert!(cache.get(q, 1).is_none());
cache.insert(q, 1, dummy_plan());
let hit = cache.get(q, 1).expect("expected cache hit");
cache.insert(q, 1, hit.clone());
assert_eq!(cache.len(), 1);
}
#[test]
fn distinct_queries_are_independent() {
let cache = PlanCache::new();
cache.insert("MATCH (n) RETURN n", 1, dummy_plan());
cache.insert("MATCH (m) RETURN m", 1, dummy_plan());
assert_eq!(cache.len(), 2);
assert!(cache.get("MATCH (n) RETURN n", 1).is_some());
assert!(cache.get("MATCH (m) RETURN m", 1).is_some());
}
#[test]
fn distinct_store_epochs_are_independent() {
let cache = PlanCache::new();
let q = "MATCH (n) RETURN n";
cache.insert(q, 1, dummy_plan());
cache.insert(q, 2, dummy_plan());
assert_eq!(cache.len(), 2);
assert!(cache.get(q, 1).is_some());
assert!(cache.get(q, 2).is_some());
}
#[test]
fn lru_evicts_oldest() {
let cache = PlanCache::with_capacity(2);
cache.insert("a", 1, dummy_plan());
cache.insert("b", 1, dummy_plan());
let _ = cache.get("a", 1);
cache.insert("c", 1, dummy_plan());
assert_eq!(cache.len(), 2);
assert!(cache.get("a", 1).is_some());
assert!(cache.get("b", 1).is_none());
assert!(cache.get("c", 1).is_some());
}
#[test]
fn keeps_only_newest_epochs_per_query() {
let cache = PlanCache::new();
let q = "CREATE (:N)";
for epoch in 1..=10 {
cache.insert(q, epoch, dummy_plan());
}
assert_eq!(cache.len(), EPOCHS_PER_QUERY);
assert!(cache.get(q, 10).is_some());
assert!(cache.get(q, 9).is_some());
assert!(cache.get(q, 8).is_none());
cache.insert(q, 3, dummy_plan());
assert!(cache.get(q, 3).is_none());
}
#[test]
fn document_survives_epoch_changes() {
let cache = PlanCache::new();
let q = "MATCH (n) RETURN n";
let doc = Arc::new(lora_parser::parse_query(q).unwrap());
cache.insert_with_document(q, 1, Some(doc.clone()), dummy_plan());
cache.insert(q, 2, dummy_plan());
cache.insert(q, 3, dummy_plan());
assert!(Arc::ptr_eq(&cache.document(q).unwrap(), &doc));
}
#[test]
fn zero_capacity_disables_storage() {
let cache = PlanCache::with_capacity(0);
cache.insert("MATCH (n) RETURN n", 1, dummy_plan());
assert_eq!(cache.len(), 0);
assert!(cache.get("MATCH (n) RETURN n", 1).is_none());
}
}