use super::{QueryCacheEntry, QueryEngine};
use crate::query::planner::{PlanType, QueryHints, QueryPlan};
use crate::query::{ParsedQuery, QueryType};
use crate::{Config, TableId};
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Instant;
use tempfile::TempDir;
async fn new_test_engine(config: &Config) -> (QueryEngine, TempDir) {
let temp_dir = TempDir::new().unwrap();
let platform = Arc::new(crate::platform::Platform::new(config).await.unwrap());
let storage = Arc::new(
crate::storage::StorageEngine::open(
temp_dir.path(),
config,
platform,
#[cfg(feature = "state_machine")]
None,
)
.await
.unwrap(),
);
let schema = Arc::new(
crate::schema::SchemaManager::new(temp_dir.path())
.await
.unwrap(),
);
let memory = Arc::new(crate::memory::MemoryManager::new(config).unwrap());
let engine = QueryEngine::new(storage, schema, memory, config).unwrap();
(engine, temp_dir)
}
fn minimal_plan(table: Option<TableId>) -> QueryPlan {
QueryPlan {
plan_type: PlanType::PointLookup,
table,
estimated_cost: 1.0,
estimated_rows: 0,
selected_indexes: Vec::new(),
steps: Vec::new(),
hints: QueryHints::default(),
}
}
fn seed_parsed(cql: &str, table: Option<TableId>) -> ParsedQuery {
ParsedQuery {
query_type: QueryType::Select,
table,
columns: vec!["*".to_string()],
where_clause: None,
values: Vec::new(),
set_clause: HashMap::new(),
order_by: Vec::new(),
limit: None,
cql: cql.to_string(),
}
}
fn seed_plan_cache(engine: &QueryEngine, cql: &str, table: Option<TableId>) {
engine.plan_cache.insert(
cql.to_string(),
QueryCacheEntry {
parsed_query: seed_parsed(cql, table.clone()),
plan: minimal_plan(table),
cached_at: Instant::now(),
hit_count: AtomicU64::new(0),
},
);
}
#[tokio::test]
async fn plan_cache_hit_bumps_counter_via_read_lock() {
let mut config = Config::default();
config.query.query_cache_size = Some(8);
let (engine, _temp) = new_test_engine(&config).await;
let cql = "SELECT * FROM t WHERE id = 1";
seed_plan_cache(&engine, cql, Some(TableId::new("t")));
{
let guard = engine.plan_cache.get(cql).expect("seeded entry present");
assert_eq!(guard.hit_count.load(Ordering::Relaxed), 0);
}
let _ = engine.execute(cql).await;
let hits = engine
.plan_cache
.get(cql)
.expect("entry still cached after a hit")
.hit_count
.load(Ordering::Relaxed);
assert_eq!(
hits, 1,
"a cache hit must bump the entry hit_count exactly once"
);
let stats = engine.stats();
assert_eq!(stats.total_queries, 1);
assert!(
stats.cache_hit_ratio > 0.0,
"cache_hit_ratio must be > 0 after a plan-cache hit"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn concurrent_plan_cache_hits_lose_no_increments() {
const TASKS: u64 = 64;
let mut config = Config::default();
config.query.query_cache_size = Some(8);
let (engine, _temp) = new_test_engine(&config).await;
let engine = Arc::new(engine);
let cql = "SELECT * FROM t WHERE id = 1";
seed_plan_cache(&engine, cql, Some(TableId::new("t")));
let mut handles = Vec::with_capacity(TASKS as usize);
for _ in 0..TASKS {
let engine = Arc::clone(&engine);
handles.push(tokio::spawn(async move {
let _ = engine.execute(cql).await;
}));
}
for h in handles {
h.await.expect("query task panicked");
}
let stats = engine.stats();
assert_eq!(
stats.total_queries, TASKS,
"every issued query must be counted exactly once"
);
let hits = engine
.plan_cache
.get(cql)
.expect("entry stays cached (all executes were hits, none re-cached)")
.hit_count
.load(Ordering::Relaxed);
assert_eq!(
hits, TASKS,
"concurrent hits must not lose increments (exact per-entry hit_count)"
);
assert!(
(stats.cache_hit_ratio - 1.0).abs() < f64::EPSILON,
"all queries hit the cache => cache_hit_ratio == 1.0 (cache_hits == total_queries)"
);
}
#[tokio::test]
#[cfg(feature = "state_machine")]
async fn placeholder_plan_is_evicted_and_query_proceeds_cold() {
let mut config = Config::default();
config.query.query_cache_size = Some(8);
let (engine, _temp) = new_test_engine(&config).await;
let cql = "SELECT * FROM t WHERE age > 5";
seed_plan_cache(&engine, cql, None); assert!(
engine.plan_cache.get(cql).is_some(),
"placeholder entry seeded into the legacy plan cache"
);
let _ = engine.execute(cql).await;
assert!(
engine.plan_cache.get(cql).is_none(),
"a non-reusable placeholder must be evicted from the legacy plan cache"
);
assert!(
engine.select_plan_cache.get(cql).is_some(),
"the query proceeded cold via the modern path (optimized plan now cached)"
);
}