use std::sync::Arc;
#[cfg(target_arch = "wasm32")]
use std::sync::RwLock;
#[cfg(not(target_arch = "wasm32"))]
use parking_lot::RwLock;
use crate::ColumnarTable;
#[inline]
fn normalize_cache_key(table_name: &str) -> String {
table_name.to_uppercase()
}
#[derive(Debug, Clone, Default)]
pub struct CacheStats {
pub hits: u64,
pub misses: u64,
pub evictions: u64,
pub conversions: u64,
pub invalidations: u64,
}
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) * 100.0
}
}
}
struct CacheEntry {
data: Arc<ColumnarTable>,
size_bytes: usize,
}
pub struct ColumnarCache {
cache: RwLock<lru::LruCache<String, CacheEntry>>,
max_memory: usize,
current_memory: RwLock<usize>,
stats: RwLock<CacheStats>,
}
impl ColumnarCache {
pub fn new(max_memory: usize) -> Self {
let capacity = std::num::NonZeroUsize::new(1000).unwrap();
ColumnarCache {
cache: RwLock::new(lru::LruCache::new(capacity)),
max_memory,
current_memory: RwLock::new(0),
stats: RwLock::new(CacheStats::default()),
}
}
pub fn get(&self, table_name: &str) -> Option<Arc<ColumnarTable>> {
let key = normalize_cache_key(table_name);
#[cfg(not(target_arch = "wasm32"))]
{
let mut cache = self.cache.write();
let mut stats = self.stats.write();
if let Some(entry) = cache.get(&key) {
stats.hits += 1;
Some(Arc::clone(&entry.data))
} else {
stats.misses += 1;
None
}
}
#[cfg(target_arch = "wasm32")]
{
let mut cache = self.cache.write().unwrap();
let mut stats = self.stats.write().unwrap();
if let Some(entry) = cache.get(&key) {
stats.hits += 1;
Some(Arc::clone(&entry.data))
} else {
stats.misses += 1;
None
}
}
}
pub fn insert(&self, table_name: &str, columnar: ColumnarTable) -> Arc<ColumnarTable> {
let key = normalize_cache_key(table_name);
let size_bytes = columnar.size_in_bytes();
let data = Arc::new(columnar);
#[cfg(not(target_arch = "wasm32"))]
{
let mut cache = self.cache.write();
let mut current_memory = self.current_memory.write();
let mut stats = self.stats.write();
if let Some(old_entry) = cache.pop(&key) {
*current_memory = current_memory.saturating_sub(old_entry.size_bytes);
}
while *current_memory + size_bytes > self.max_memory {
if let Some((_, evicted)) = cache.pop_lru() {
*current_memory = current_memory.saturating_sub(evicted.size_bytes);
stats.evictions += 1;
} else {
break;
}
}
let entry = CacheEntry { data: Arc::clone(&data), size_bytes };
cache.put(key, entry);
*current_memory += size_bytes;
stats.conversions += 1;
}
#[cfg(target_arch = "wasm32")]
{
let mut cache = self.cache.write().unwrap();
let mut current_memory = self.current_memory.write().unwrap();
let mut stats = self.stats.write().unwrap();
if let Some(old_entry) = cache.pop(&key) {
*current_memory = current_memory.saturating_sub(old_entry.size_bytes);
}
while *current_memory + size_bytes > self.max_memory {
if let Some((_, evicted)) = cache.pop_lru() {
*current_memory = current_memory.saturating_sub(evicted.size_bytes);
stats.evictions += 1;
} else {
break;
}
}
let entry = CacheEntry { data: Arc::clone(&data), size_bytes };
cache.put(key, entry);
*current_memory += size_bytes;
stats.conversions += 1;
}
data
}
pub fn invalidate(&self, table_name: &str) {
let key = normalize_cache_key(table_name);
#[cfg(not(target_arch = "wasm32"))]
{
let mut cache = self.cache.write();
let mut current_memory = self.current_memory.write();
let mut stats = self.stats.write();
if let Some(entry) = cache.pop(&key) {
*current_memory = current_memory.saturating_sub(entry.size_bytes);
stats.invalidations += 1;
}
}
#[cfg(target_arch = "wasm32")]
{
let mut cache = self.cache.write().unwrap();
let mut current_memory = self.current_memory.write().unwrap();
let mut stats = self.stats.write().unwrap();
if let Some(entry) = cache.pop(&key) {
*current_memory = current_memory.saturating_sub(entry.size_bytes);
stats.invalidations += 1;
}
}
}
pub fn clear(&self) {
#[cfg(not(target_arch = "wasm32"))]
{
let mut cache = self.cache.write();
let mut current_memory = self.current_memory.write();
cache.clear();
*current_memory = 0;
}
#[cfg(target_arch = "wasm32")]
{
let mut cache = self.cache.write().unwrap();
let mut current_memory = self.current_memory.write().unwrap();
cache.clear();
*current_memory = 0;
}
}
pub fn stats(&self) -> CacheStats {
#[cfg(not(target_arch = "wasm32"))]
{
self.stats.read().clone()
}
#[cfg(target_arch = "wasm32")]
{
self.stats.read().unwrap().clone()
}
}
pub fn memory_usage(&self) -> usize {
#[cfg(not(target_arch = "wasm32"))]
{
*self.current_memory.read()
}
#[cfg(target_arch = "wasm32")]
{
*self.current_memory.read().unwrap()
}
}
pub fn max_memory(&self) -> usize {
self.max_memory
}
pub fn len(&self) -> usize {
#[cfg(not(target_arch = "wasm32"))]
{
self.cache.read().len()
}
#[cfg(target_arch = "wasm32")]
{
self.cache.read().unwrap().len()
}
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn contains(&self, table_name: &str) -> bool {
let key = normalize_cache_key(table_name);
#[cfg(not(target_arch = "wasm32"))]
{
self.cache.read().contains(&key)
}
#[cfg(target_arch = "wasm32")]
{
self.cache.read().unwrap().contains(&key)
}
}
}
impl std::fmt::Debug for ColumnarCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ColumnarCache")
.field("max_memory", &self.max_memory)
.field("current_memory", &self.memory_usage())
.field("entries", &self.len())
.field("stats", &self.stats())
.finish()
}
}
impl Clone for ColumnarCache {
fn clone(&self) -> Self {
ColumnarCache::new(self.max_memory)
}
}
#[cfg(test)]
mod tests {
use vibesql_types::SqlValue;
use super::*;
use crate::Row;
fn create_test_columnar(rows: usize) -> ColumnarTable {
let row_data: Vec<Row> = (0..rows)
.map(|i| {
Row::new(vec![
SqlValue::Integer(i as i64),
SqlValue::Varchar(arcstr::ArcStr::from(format!("name_{}", i))),
])
})
.collect();
let column_names = vec!["id".to_string(), "name".to_string()];
ColumnarTable::from_rows(&row_data, &column_names).unwrap()
}
#[test]
fn test_cache_basic_operations() {
let cache = ColumnarCache::new(1024 * 1024);
assert!(cache.is_empty());
assert_eq!(cache.len(), 0);
assert!(cache.get("test").is_none());
let columnar = create_test_columnar(100);
let _ = cache.insert("test", columnar);
assert!(!cache.is_empty());
assert_eq!(cache.len(), 1);
assert!(cache.contains("test"));
assert!(cache.get("test").is_some());
let stats = cache.stats();
assert_eq!(stats.hits, 1); assert_eq!(stats.misses, 1); assert_eq!(stats.conversions, 1);
}
#[test]
fn test_cache_invalidation() {
let cache = ColumnarCache::new(1024 * 1024);
let columnar = create_test_columnar(100);
let _ = cache.insert("test", columnar);
assert!(cache.contains("test"));
cache.invalidate("test");
assert!(!cache.contains("test"));
let stats = cache.stats();
assert_eq!(stats.invalidations, 1);
}
#[test]
fn test_cache_eviction() {
let cache = ColumnarCache::new(1024);
let columnar1 = create_test_columnar(10);
let _ = cache.insert("table1", columnar1);
assert!(cache.contains("table1"));
let columnar2 = create_test_columnar(10);
let _ = cache.insert("table2", columnar2);
let stats = cache.stats();
assert!(stats.evictions > 0 || cache.len() == 2);
}
#[test]
fn test_cache_arc_sharing() {
let cache = ColumnarCache::new(1024 * 1024);
let columnar = create_test_columnar(100);
let arc1 = cache.insert("test", columnar);
let arc2 = cache.get("test").unwrap();
assert!(Arc::ptr_eq(&arc1, &arc2));
assert_eq!(Arc::strong_count(&arc1), 3);
}
#[test]
fn test_cache_clear() {
let cache = ColumnarCache::new(1024 * 1024);
let _ = cache.insert("table1", create_test_columnar(10));
let _ = cache.insert("table2", create_test_columnar(10));
assert_eq!(cache.len(), 2);
assert!(cache.memory_usage() > 0);
cache.clear();
assert_eq!(cache.len(), 0);
assert_eq!(cache.memory_usage(), 0);
}
#[test]
fn test_cache_update_existing() {
let cache = ColumnarCache::new(1024 * 1024);
let columnar1 = create_test_columnar(10);
let _ = cache.insert("test", columnar1);
let memory1 = cache.memory_usage();
let columnar2 = create_test_columnar(100);
let _ = cache.insert("test", columnar2);
let memory2 = cache.memory_usage();
assert_ne!(memory1, memory2);
assert_eq!(cache.len(), 1);
}
#[test]
fn test_hit_rate() {
let stats =
CacheStats { hits: 80, misses: 20, evictions: 0, conversions: 0, invalidations: 0 };
assert!((stats.hit_rate() - 80.0).abs() < 0.001);
}
#[test]
fn test_hit_rate_empty() {
let stats = CacheStats::default();
assert_eq!(stats.hit_rate(), 0.0);
}
}