use std::collections::{HashMap, HashSet, VecDeque};
use std::hash::{Hash, Hasher};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use crate::pool::QueryRows;
const DEFAULT_CAPACITY: usize = 1024;
const DEFAULT_TTL_SECS: u64 = 300;
#[derive(Debug, Clone)]
pub struct QueryResultCacheConfig {
pub capacity: usize,
pub ttl: Duration,
}
impl Default for QueryResultCacheConfig {
fn default() -> Self {
Self {
capacity: DEFAULT_CAPACITY,
ttl: Duration::from_secs(DEFAULT_TTL_SECS),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CacheKey {
pub sql_hash: u64,
pub params_hash: u64,
pub tenant_id: Option<i64>,
}
impl CacheKey {
pub fn new(sql: &str, params: &[crate::Value], tenant_id: Option<i64>) -> Self {
let sql_hash = hash_str(sql);
let params_hash = hash_params(params);
Self {
sql_hash,
params_hash,
tenant_id,
}
}
pub fn from_hashes(sql_hash: u64, params_hash: u64) -> Self {
Self {
sql_hash,
params_hash,
tenant_id: None,
}
}
}
#[derive(Debug, Clone)]
pub struct CachedResult {
pub rows: QueryRows,
pub cached_at: Instant,
pub expires_at: Instant,
pub depends_on: HashSet<String>,
}
impl CachedResult {
pub fn new(rows: QueryRows, ttl: Duration, depends_on: HashSet<String>) -> Self {
let now = Instant::now();
Self {
rows,
cached_at: now,
expires_at: now + ttl,
depends_on,
}
}
pub fn is_expired(&self) -> bool {
Instant::now() >= self.expires_at
}
}
#[derive(Debug, Clone, Default)]
pub struct CacheStats {
pub hits: u64,
pub misses: u64,
pub evictions: u64,
pub invalidations: u64,
pub entry_count: usize,
}
impl CacheStats {
pub fn hit_rate(&self) -> f64 {
let total = self.hits + self.misses;
if total == 0 {
0.0
} else {
self.hits as f64 / total as f64
}
}
pub fn miss_rate(&self) -> f64 {
1.0 - self.hit_rate()
}
pub fn total_queries(&self) -> u64 {
self.hits + self.misses
}
}
pub struct QueryResultCache {
entries: Mutex<HashMap<CacheKey, CachedResult>>,
access_order: Mutex<VecDeque<CacheKey>>,
table_index: Mutex<HashMap<String, HashSet<CacheKey>>>,
stats: Mutex<CacheStats>,
config: QueryResultCacheConfig,
}
impl QueryResultCache {
pub fn new(config: QueryResultCacheConfig) -> Arc<Self> {
Arc::new(Self {
entries: Mutex::new(HashMap::with_capacity(config.capacity)),
access_order: Mutex::new(VecDeque::with_capacity(config.capacity)),
table_index: Mutex::new(HashMap::new()),
stats: Mutex::new(CacheStats::default()),
config,
})
}
pub fn with_default() -> Arc<Self> {
Self::new(QueryResultCacheConfig::default())
}
pub fn get(&self, key: &CacheKey) -> Option<QueryRows> {
let entries = self.entries.lock().unwrap();
let entry = match entries.get(key) {
Some(e) => e,
None => {
drop(entries);
self.record_miss();
return None;
}
};
if entry.is_expired() {
drop(entries);
self.remove_expired(key);
self.record_miss();
return None;
}
let rows = entry.rows.clone();
drop(entries);
self.touch_access(key);
self.record_hit();
Some(rows)
}
pub fn put(&self, key: CacheKey, rows: QueryRows, depends_on: HashSet<String>) {
let result = CachedResult::new(rows, self.config.ttl, depends_on.clone());
{
let mut entries = self.entries.lock().unwrap();
if entries.len() >= self.config.capacity && !entries.contains_key(&key) {
self.evict_lru(&mut entries);
}
entries.insert(key.clone(), result);
}
{
let mut order = self.access_order.lock().unwrap();
order.retain(|k| k != &key);
order.push_back(key.clone());
}
{
let mut idx = self.table_index.lock().unwrap();
for table in depends_on {
idx.entry(table).or_default().insert(key.clone());
}
}
self.update_entry_count();
}
pub fn invalidate_table(&self, table: &str) -> usize {
let keys_to_remove: Vec<CacheKey> = {
let idx = self.table_index.lock().unwrap();
idx.get(table)
.cloned()
.unwrap_or_default()
.into_iter()
.collect()
};
let count = keys_to_remove.len();
if count == 0 {
return 0;
}
{
let mut entries = self.entries.lock().unwrap();
for key in &keys_to_remove {
entries.remove(key);
}
}
{
let mut order = self.access_order.lock().unwrap();
order.retain(|k| !keys_to_remove.contains(k));
}
{
let mut idx = self.table_index.lock().unwrap();
if let Some(keys) = idx.get_mut(table) {
keys.clear();
}
}
{
let mut stats = self.stats.lock().unwrap();
stats.invalidations += count as u64;
}
self.update_entry_count();
count
}
pub fn clear(&self) {
{
let mut entries = self.entries.lock().unwrap();
entries.clear();
}
{
let mut order = self.access_order.lock().unwrap();
order.clear();
}
{
let mut idx = self.table_index.lock().unwrap();
idx.clear();
}
self.update_entry_count();
}
pub fn stats(&self) -> CacheStats {
self.stats.lock().unwrap().clone()
}
pub fn purge_expired(&self) -> usize {
let now = Instant::now();
let expired_keys: Vec<CacheKey> = {
let entries = self.entries.lock().unwrap();
entries
.iter()
.filter(|(_, v)| now >= v.expires_at)
.map(|(k, _)| k.clone())
.collect()
};
let count = expired_keys.len();
for key in &expired_keys {
self.remove_expired(key);
}
count
}
fn touch_access(&self, key: &CacheKey) {
let mut order = self.access_order.lock().unwrap();
order.retain(|k| k != key);
order.push_back(key.clone());
}
fn evict_lru(&self, entries: &mut HashMap<CacheKey, CachedResult>) {
let key_to_evict = {
let order = self.access_order.lock().unwrap();
order.front().cloned()
};
if let Some(key) = key_to_evict {
entries.remove(&key);
let mut order = self.access_order.lock().unwrap();
order.pop_front();
let mut idx = self.table_index.lock().unwrap();
for keys in idx.values_mut() {
keys.remove(&key);
}
let mut stats = self.stats.lock().unwrap();
stats.evictions += 1;
}
}
fn remove_expired(&self, key: &CacheKey) {
{
let mut entries = self.entries.lock().unwrap();
entries.remove(key);
}
{
let mut order = self.access_order.lock().unwrap();
order.retain(|k| k != key);
}
{
let mut idx = self.table_index.lock().unwrap();
for keys in idx.values_mut() {
keys.remove(key);
}
}
{
let mut stats = self.stats.lock().unwrap();
stats.evictions += 1;
}
self.update_entry_count();
}
fn record_hit(&self) {
let mut stats = self.stats.lock().unwrap();
stats.hits += 1;
}
fn record_miss(&self) {
let mut stats = self.stats.lock().unwrap();
stats.misses += 1;
}
fn update_entry_count(&self) {
let entries = self.entries.lock().unwrap();
let mut stats = self.stats.lock().unwrap();
stats.entry_count = entries.len();
}
}
fn hash_str(s: &str) -> u64 {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
s.hash(&mut hasher);
hasher.finish()
}
fn hash_params(params: &[crate::Value]) -> u64 {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
for param in params {
format!("{param:?}").hash(&mut hasher);
}
hasher.finish()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Value;
fn make_rows(n: usize) -> QueryRows {
(0..n)
.map(|i| {
let mut row = HashMap::new();
row.insert("id".to_string(), Value::I64(i as i64));
row
})
.collect()
}
#[test]
fn test_cache_put_get_hit() {
let cache = QueryResultCache::with_default();
let key = CacheKey::new("SELECT * FROM users", &[], None);
let rows = make_rows(3);
cache.put(key.clone(), rows.clone(), HashSet::new());
let got = cache.get(&key);
assert!(got.is_some());
assert_eq!(got.unwrap().len(), 3);
let stats = cache.stats();
assert_eq!(stats.hits, 1);
assert_eq!(stats.misses, 0);
}
#[test]
fn test_cache_miss() {
let cache = QueryResultCache::with_default();
let key = CacheKey::new("SELECT * FROM users", &[], None);
let got = cache.get(&key);
assert!(got.is_none());
let stats = cache.stats();
assert_eq!(stats.hits, 0);
assert_eq!(stats.misses, 1);
}
#[test]
fn test_cache_ttl_expiry() {
let config = QueryResultCacheConfig {
capacity: 10,
ttl: Duration::from_millis(10),
};
let cache = QueryResultCache::new(config);
let key = CacheKey::new("SELECT 1", &[], None);
cache.put(key.clone(), make_rows(1), HashSet::new());
std::thread::sleep(Duration::from_millis(20));
let got = cache.get(&key);
assert!(got.is_none());
let stats = cache.stats();
assert!(stats.evictions >= 1);
}
#[test]
fn test_cache_lru_eviction() {
let config = QueryResultCacheConfig {
capacity: 2,
ttl: Duration::from_secs(60),
};
let cache = QueryResultCache::new(config);
let k1 = CacheKey::from_hashes(1, 0);
let k2 = CacheKey::from_hashes(2, 0);
let k3 = CacheKey::from_hashes(3, 0);
cache.put(k1.clone(), make_rows(1), HashSet::new());
cache.put(k2.clone(), make_rows(1), HashSet::new());
let _ = cache.get(&k1);
cache.put(k3.clone(), make_rows(1), HashSet::new());
assert!(cache.get(&k2).is_none());
assert!(cache.get(&k1).is_some());
assert!(cache.get(&k3).is_some());
}
#[test]
fn test_cache_key_with_tenant() {
let k1 = CacheKey::new("SELECT 1", &[], Some(1));
let k2 = CacheKey::new("SELECT 1", &[], Some(2));
assert_ne!(k1, k2);
let k3 = CacheKey::new("SELECT 1", &[], Some(1));
assert_eq!(k1, k3);
}
#[test]
fn test_cache_key_params_hash() {
let k1 = CacheKey::new("SELECT * FROM t WHERE id = ?", &[Value::I64(1)], None);
let k2 = CacheKey::new("SELECT * FROM t WHERE id = ?", &[Value::I64(2)], None);
assert_ne!(k1, k2);
let k3 = CacheKey::new("SELECT * FROM t WHERE id = ?", &[Value::I64(1)], None);
assert_eq!(k1, k3);
}
#[test]
fn test_invalidate_table() {
let cache = QueryResultCache::with_default();
let key = CacheKey::new("SELECT * FROM users", &[], None);
let mut deps = HashSet::new();
deps.insert("users".to_string());
cache.put(key.clone(), make_rows(1), deps);
assert!(cache.get(&key).is_some());
let count = cache.invalidate_table("users");
assert_eq!(count, 1);
assert!(cache.get(&key).is_none());
let stats = cache.stats();
assert!(stats.invalidations >= 1);
}
#[test]
fn test_invalidate_unrelated_table() {
let cache = QueryResultCache::with_default();
let key = CacheKey::new("SELECT * FROM users", &[], None);
let mut deps = HashSet::new();
deps.insert("users".to_string());
cache.put(key.clone(), make_rows(1), deps);
let count = cache.invalidate_table("orders");
assert_eq!(count, 0);
assert!(cache.get(&key).is_some());
}
#[test]
fn test_cache_stats_hit_rate() {
let cache = QueryResultCache::with_default();
let key = CacheKey::new("SELECT 1", &[], None);
cache.put(key.clone(), make_rows(1), HashSet::new());
let _ = cache.get(&key);
let _ = cache.get(&CacheKey::from_hashes(999, 0));
let stats = cache.stats();
assert_eq!(stats.total_queries(), 2);
assert!((stats.hit_rate() - 0.5).abs() < 0.001);
}
#[test]
fn test_purge_expired() {
let config = QueryResultCacheConfig {
capacity: 10,
ttl: Duration::from_millis(10),
};
let cache = QueryResultCache::new(config);
let k1 = CacheKey::from_hashes(1, 0);
let k2 = CacheKey::from_hashes(2, 0);
cache.put(k1.clone(), make_rows(1), HashSet::new());
cache.put(k2.clone(), make_rows(1), HashSet::new());
std::thread::sleep(Duration::from_millis(20));
let purged = cache.purge_expired();
assert_eq!(purged, 2);
assert!(cache.get(&k1).is_none());
assert!(cache.get(&k2).is_none());
}
#[test]
fn test_clear() {
let cache = QueryResultCache::with_default();
let k1 = CacheKey::from_hashes(1, 0);
cache.put(k1.clone(), make_rows(1), HashSet::new());
assert!(cache.get(&k1).is_some());
cache.clear();
assert!(cache.get(&k1).is_none());
}
#[test]
fn test_capacity_limit() {
let config = QueryResultCacheConfig {
capacity: 3,
ttl: Duration::from_secs(60),
};
let cache = QueryResultCache::new(config);
for i in 0..5 {
let key = CacheKey::from_hashes(i, 0);
cache.put(key, make_rows(1), HashSet::new());
}
let stats = cache.stats();
assert!(stats.entry_count <= 3);
}
}