use crate::common::time_compat::Instant;
use rustc_hash::FxHasher;
use std::borrow::Cow;
use std::hash::{Hash, Hasher};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::RwLock;
use std::time::Duration;
use crate::common::{CompactArc, StringMap};
use crate::core::{Result, Row};
#[inline]
fn to_lowercase_cow(s: &str) -> Cow<'_, str> {
if s.bytes().all(|b| !b.is_ascii_uppercase()) {
Cow::Borrowed(s)
} else {
Cow::Owned(s.to_lowercase())
}
}
use crate::functions::FunctionRegistry;
use crate::parser::ast::{Expression, InfixOperator};
use super::expression::ExpressionEval;
use super::utils::{expressions_equivalent, extract_and_conditions, extract_column_name};
pub const DEFAULT_SEMANTIC_CACHE_SIZE: usize = 64;
pub const DEFAULT_CACHE_TTL_SECS: u64 = 300;
pub const DEFAULT_MAX_CACHED_ROWS: usize = 100_000;
pub const DEFAULT_MAX_GLOBAL_CACHED_ROWS: usize = 1_000_000;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct QueryFingerprint {
pub table_name: String,
pub columns: Vec<String>,
pub predicate_structure_hash: u64,
}
impl QueryFingerprint {
pub fn new(table_name: &str, columns: Vec<String>) -> Self {
Self {
table_name: table_name.to_lowercase(),
columns,
predicate_structure_hash: 0,
}
}
pub fn with_predicate(table_name: &str, columns: Vec<String>, predicate: &Expression) -> Self {
Self {
table_name: table_name.to_lowercase(),
columns,
predicate_structure_hash: hash_predicate_structure(predicate),
}
}
}
#[derive(Debug, Clone)]
pub struct CachedResult {
pub fingerprint: QueryFingerprint,
pub column_names: Vec<String>,
pub rows: CompactArc<Vec<Row>>,
pub predicate: Option<Expression>,
pub cached_at: Instant,
pub last_accessed: Instant,
pub access_count: u64,
}
impl CachedResult {
pub fn new(
fingerprint: QueryFingerprint,
column_names: Vec<String>,
rows: Vec<Row>,
predicate: Option<Expression>,
) -> Self {
let now = Instant::now();
Self {
fingerprint,
column_names,
rows: CompactArc::new(rows), predicate,
cached_at: now,
last_accessed: now,
access_count: 1,
}
}
pub fn new_with_arc(
fingerprint: QueryFingerprint,
column_names: Vec<String>,
rows: CompactArc<Vec<Row>>,
predicate: Option<Expression>,
) -> Self {
let now = Instant::now();
Self {
fingerprint,
column_names,
rows, predicate,
cached_at: now,
last_accessed: now,
access_count: 1,
}
}
pub fn is_expired(&self, ttl: Duration) -> bool {
self.cached_at.elapsed() > ttl
}
#[inline]
pub fn is_expired_at(&self, ttl: Duration, now: Instant) -> bool {
now.duration_since(self.cached_at) > ttl
}
pub fn record_access(&mut self) {
self.last_accessed = Instant::now();
self.access_count += 1;
}
}
#[derive(Debug, Clone)]
pub enum SubsumptionResult {
Subsumed {
filter: Box<Expression>,
},
Identical,
NoSubsumption,
}
pub struct SemanticCache {
cache: RwLock<StringMap<StringMap<Vec<CachedResult>>>>,
max_size: usize,
ttl: Duration,
max_rows: usize,
max_global_rows: usize,
global_row_count: AtomicU64,
stats: SemanticCacheStats,
}
#[derive(Debug, Default)]
pub struct SemanticCacheStats {
pub hits: AtomicU64,
pub exact_hits: AtomicU64,
pub subsumption_hits: AtomicU64,
pub misses: AtomicU64,
pub ttl_evictions: AtomicU64,
pub size_evictions: AtomicU64,
pub lock_failures: AtomicU64,
}
#[derive(Debug, Clone, Default)]
pub struct SemanticCacheStatsSnapshot {
pub hits: u64,
pub exact_hits: u64,
pub subsumption_hits: u64,
pub misses: u64,
pub ttl_evictions: u64,
pub size_evictions: u64,
pub lock_failures: u64,
}
#[derive(Debug)]
pub enum CacheLookupResult {
ExactHit(CompactArc<Vec<Row>>),
SubsumptionHit {
rows: CompactArc<Vec<Row>>,
filter: Box<Expression>,
columns: Vec<String>,
},
Miss,
}
impl SemanticCache {
pub fn new() -> Self {
Self::with_config(
DEFAULT_SEMANTIC_CACHE_SIZE,
Duration::from_secs(DEFAULT_CACHE_TTL_SECS),
DEFAULT_MAX_CACHED_ROWS,
DEFAULT_MAX_GLOBAL_CACHED_ROWS,
)
}
pub fn with_config(
max_size: usize,
ttl: Duration,
max_rows: usize,
max_global_rows: usize,
) -> Self {
Self {
cache: RwLock::new(StringMap::new()),
max_size,
ttl,
max_rows,
max_global_rows,
global_row_count: AtomicU64::new(0),
stats: SemanticCacheStats::default(),
}
}
pub fn lookup(
&self,
table_name: &str,
columns: &[String],
predicate: Option<&Expression>,
) -> CacheLookupResult {
let (table_key, column_key) = Self::cache_keys(table_name, columns);
let hit_info = {
let cache = match self.cache.read() {
Ok(c) => c,
Err(_) => {
self.stats.lock_failures.fetch_add(1, Ordering::Relaxed);
return CacheLookupResult::Miss;
}
};
let table_cache = match cache.get(&table_key) {
Some(tc) => tc,
None => {
drop(cache);
self.record_miss();
return CacheLookupResult::Miss;
}
};
let entries = match table_cache.get(&column_key) {
Some(e) => e,
None => {
drop(cache);
self.record_miss();
return CacheLookupResult::Miss;
}
};
let mut found = None;
let now = Instant::now();
for (idx, entry) in entries.iter().enumerate() {
if entry.is_expired_at(self.ttl, now) {
continue;
}
if entry.column_names != columns {
continue;
}
match check_subsumption(entry.predicate.as_ref(), predicate) {
SubsumptionResult::Identical => {
let rows = entry.rows.clone();
let hash = entry.fingerprint.predicate_structure_hash;
found = Some((idx, hash, CacheLookupResult::ExactHit(rows)));
break;
}
SubsumptionResult::Subsumed { filter } => {
let rows = entry.rows.clone();
let columns = entry.column_names.clone();
let hash = entry.fingerprint.predicate_structure_hash;
found = Some((
idx,
hash,
CacheLookupResult::SubsumptionHit {
rows,
filter,
columns,
},
));
break;
}
SubsumptionResult::NoSubsumption => {
continue;
}
}
}
found
};
match hit_info {
Some((idx, expected_hash, result)) => {
if let Ok(mut cache) = self.cache.write() {
if let Some(table_cache) = cache.get_mut(&table_key) {
if let Some(entries) = table_cache.get_mut(&column_key) {
if let Some(entry) = entries.get_mut(idx) {
if entry.fingerprint.predicate_structure_hash == expected_hash {
entry.record_access();
}
}
}
}
}
match &result {
CacheLookupResult::ExactHit(_) => self.record_exact_hit(),
CacheLookupResult::SubsumptionHit { .. } => self.record_subsumption_hit(),
CacheLookupResult::Miss => {}
}
result
}
None => {
self.record_miss();
CacheLookupResult::Miss
}
}
}
pub fn insert(
&self,
table_name: &str,
columns: Vec<String>,
rows: Vec<Row>,
predicate: Option<Expression>,
) {
let new_row_count = rows.len();
if new_row_count > self.max_rows {
return;
}
let (table_key, column_key) = Self::cache_keys(table_name, &columns);
let fingerprint = match &predicate {
Some(p) => QueryFingerprint::with_predicate(table_name, columns.clone(), p),
None => QueryFingerprint::new(table_name, columns.clone()),
};
let entry = CachedResult::new(fingerprint, columns, rows, predicate);
self.insert_entry(entry, new_row_count, table_key, column_key);
}
pub fn insert_arc(
&self,
table_name: &str,
columns: Vec<String>,
rows: CompactArc<Vec<Row>>,
predicate: Option<Expression>,
) {
let new_row_count = rows.len();
if new_row_count > self.max_rows {
return;
}
let (table_key, column_key) = Self::cache_keys(table_name, &columns);
let fingerprint = match &predicate {
Some(p) => QueryFingerprint::with_predicate(table_name, columns.clone(), p),
None => QueryFingerprint::new(table_name, columns.clone()),
};
let entry = CachedResult::new_with_arc(fingerprint, columns, rows, predicate);
self.insert_entry(entry, new_row_count, table_key, column_key);
}
fn insert_entry(
&self,
entry: CachedResult,
new_row_count: usize,
table_key: String,
column_key: String,
) {
let mut cache = match self.cache.write() {
Ok(c) => c,
Err(_) => {
self.stats.lock_failures.fetch_add(1, Ordering::Relaxed);
return;
}
};
let current_global = self.global_row_count.load(Ordering::Relaxed) as usize;
if current_global + new_row_count > self.max_global_rows {
let rows_to_free =
(current_global + new_row_count).saturating_sub(self.max_global_rows);
self.evict_global_lru(&mut cache, rows_to_free, &table_key);
}
let table_cache = cache.entry(table_key).or_default();
let entries = table_cache.entry(column_key).or_default();
let mut rows_freed: usize = 0;
let before_len = entries.len();
let now = Instant::now();
entries.retain(|e| {
if e.is_expired_at(self.ttl, now) {
rows_freed += e.rows.len();
false
} else {
true
}
});
let evicted = before_len - entries.len();
if evicted > 0 {
self.stats
.ttl_evictions
.fetch_add(evicted as u64, Ordering::Relaxed);
}
while entries.len() >= self.max_size {
if let Some((idx, _)) = entries
.iter()
.enumerate()
.min_by_key(|(_, e)| (e.last_accessed, e.access_count))
{
rows_freed += entries[idx].rows.len();
entries.remove(idx);
self.stats.size_evictions.fetch_add(1, Ordering::Relaxed);
} else {
break;
}
}
if rows_freed > 0 {
self.global_row_count
.fetch_sub(rows_freed as u64, Ordering::Relaxed);
}
self.global_row_count
.fetch_add(new_row_count as u64, Ordering::Relaxed);
entries.push(entry);
}
fn evict_global_lru(
&self,
cache: &mut StringMap<StringMap<Vec<CachedResult>>>,
mut rows_to_free: usize,
skip_table: &str,
) {
while rows_to_free > 0 {
let mut oldest: Option<(String, String, usize, Instant, u64, usize)> = None;
for (table_key, table_cache) in cache.iter() {
if table_key == skip_table {
continue; }
for (col_key, entries) in table_cache.iter() {
for (idx, entry) in entries.iter().enumerate() {
let dominated = match &oldest {
None => true,
Some((_, _, _, last_acc, acc_count, _)) => {
(entry.last_accessed, entry.access_count) < (*last_acc, *acc_count)
}
};
if dominated {
oldest = Some((
table_key.clone(),
col_key.clone(),
idx,
entry.last_accessed,
entry.access_count,
entry.rows.len(),
));
}
}
}
}
match oldest {
Some((table_key, col_key, idx, _, _, row_count)) => {
if let Some(table_cache) = cache.get_mut(&table_key) {
if let Some(entries) = table_cache.get_mut(&col_key) {
entries.remove(idx);
self.global_row_count
.fetch_sub(row_count as u64, Ordering::Relaxed);
self.stats.size_evictions.fetch_add(1, Ordering::Relaxed);
rows_to_free = rows_to_free.saturating_sub(row_count);
if entries.is_empty() {
table_cache.remove(&col_key);
}
}
if table_cache.is_empty() {
cache.remove(&table_key);
}
}
}
None => break, }
}
}
pub fn invalidate_table(&self, table_name: &str) {
let table_key = to_lowercase_cow(table_name);
match self.cache.write() {
Ok(mut cache) => {
if let Some(table_cache) = cache.get(table_key.as_ref()) {
let rows_removed: usize = table_cache
.values()
.flat_map(|entries| entries.iter())
.map(|e| e.rows.len())
.sum();
if rows_removed > 0 {
self.global_row_count
.fetch_sub(rows_removed as u64, Ordering::Relaxed);
}
}
cache.remove(table_key.as_ref());
}
Err(_) => {
self.stats.lock_failures.fetch_add(1, Ordering::Relaxed);
}
}
}
pub fn clear(&self) {
match self.cache.write() {
Ok(mut cache) => cache.clear(),
Err(_) => {
self.stats.lock_failures.fetch_add(1, Ordering::Relaxed);
}
}
self.global_row_count.store(0, Ordering::Relaxed);
self.stats.hits.store(0, Ordering::Relaxed);
self.stats.exact_hits.store(0, Ordering::Relaxed);
self.stats.subsumption_hits.store(0, Ordering::Relaxed);
self.stats.misses.store(0, Ordering::Relaxed);
self.stats.ttl_evictions.store(0, Ordering::Relaxed);
self.stats.size_evictions.store(0, Ordering::Relaxed);
}
pub fn stats(&self) -> SemanticCacheStatsSnapshot {
SemanticCacheStatsSnapshot {
hits: self.stats.hits.load(Ordering::Relaxed),
exact_hits: self.stats.exact_hits.load(Ordering::Relaxed),
subsumption_hits: self.stats.subsumption_hits.load(Ordering::Relaxed),
misses: self.stats.misses.load(Ordering::Relaxed),
ttl_evictions: self.stats.ttl_evictions.load(Ordering::Relaxed),
size_evictions: self.stats.size_evictions.load(Ordering::Relaxed),
lock_failures: self.stats.lock_failures.load(Ordering::Relaxed),
}
}
pub fn size(&self) -> usize {
self.cache
.read()
.map(|c| {
c.values()
.map(|table_cache| table_cache.values().map(|v| v.len()).sum::<usize>())
.sum()
})
.unwrap_or(0)
}
pub fn filter_rows(
rows: Vec<Row>,
filter: &Expression,
columns: &[String],
_function_registry: &FunctionRegistry,
) -> Result<Vec<Row>> {
let columns_vec: Vec<String> = columns.to_vec();
let mut eval = ExpressionEval::compile(filter, &columns_vec)?;
let mut result = Vec::with_capacity(rows.len());
for row in rows {
if eval.eval_bool_checked(&row)? {
result.push(row);
}
}
Ok(result)
}
fn cache_keys(table_name: &str, columns: &[String]) -> (String, String) {
let table_key = table_name.to_lowercase();
let mut sorted_cols = columns.to_vec();
sorted_cols.sort();
let column_key = sorted_cols.join("\0");
(table_key, column_key)
}
fn record_exact_hit(&self) {
self.stats.hits.fetch_add(1, Ordering::Relaxed);
self.stats.exact_hits.fetch_add(1, Ordering::Relaxed);
}
fn record_subsumption_hit(&self) {
self.stats.hits.fetch_add(1, Ordering::Relaxed);
self.stats.subsumption_hits.fetch_add(1, Ordering::Relaxed);
}
fn record_miss(&self) {
self.stats.misses.fetch_add(1, Ordering::Relaxed);
}
}
impl Default for SemanticCache {
fn default() -> Self {
Self::new()
}
}
fn hash_predicate_structure(expr: &Expression) -> u64 {
let mut hasher = FxHasher::default();
hash_expr_structure(expr, &mut hasher);
hasher.finish()
}
fn hash_expr_structure(expr: &Expression, hasher: &mut FxHasher) {
match expr {
Expression::Identifier(ident) => {
0u8.hash(hasher);
ident.value_lower.hash(hasher);
}
Expression::QualifiedIdentifier(qi) => {
1u8.hash(hasher);
qi.qualifier.value_lower.hash(hasher);
qi.name.value_lower.hash(hasher);
}
Expression::IntegerLiteral(_) => {
2u8.hash(hasher);
}
Expression::FloatLiteral(_) => {
3u8.hash(hasher);
}
Expression::StringLiteral(_) => {
4u8.hash(hasher);
}
Expression::BooleanLiteral(_) => {
5u8.hash(hasher);
}
Expression::NullLiteral(_) => {
6u8.hash(hasher);
}
Expression::Infix(infix) => {
7u8.hash(hasher);
std::mem::discriminant(&infix.op_type).hash(hasher);
hash_expr_structure(&infix.left, hasher);
hash_expr_structure(&infix.right, hasher);
}
Expression::Prefix(prefix) => {
8u8.hash(hasher);
prefix.operator.hash(hasher);
hash_expr_structure(&prefix.right, hasher);
}
Expression::Between(between) => {
9u8.hash(hasher);
hash_expr_structure(&between.expr, hasher);
}
Expression::In(in_expr) => {
10u8.hash(hasher);
hash_expr_structure(&in_expr.left, hasher);
}
Expression::FunctionCall(func) => {
11u8.hash(hasher);
func.function.to_lowercase().hash(hasher);
func.arguments.len().hash(hasher);
}
Expression::Case(_) => {
12u8.hash(hasher);
}
Expression::List(list) => {
13u8.hash(hasher);
list.elements.len().hash(hasher);
}
Expression::Window(win) => {
14u8.hash(hasher);
win.function.function.to_lowercase().hash(hasher);
win.function.arguments.len().hash(hasher);
win.partition_by.len().hash(hasher);
win.order_by.len().hash(hasher);
}
_ => {
255u8.hash(hasher);
}
}
}
pub fn check_subsumption(
cached_predicate: Option<&Expression>,
new_predicate: Option<&Expression>,
) -> SubsumptionResult {
match (cached_predicate, new_predicate) {
(None, None) => SubsumptionResult::Identical,
(Some(_), None) => SubsumptionResult::NoSubsumption,
(None, Some(new_pred)) => SubsumptionResult::Subsumed {
filter: Box::new(new_pred.clone()),
},
(Some(cached), Some(new)) => check_predicate_subsumption(cached, new),
}
}
fn check_predicate_subsumption(cached: &Expression, new: &Expression) -> SubsumptionResult {
if expressions_equivalent(cached, new) {
return SubsumptionResult::Identical;
}
if let Some(result) = check_range_subsumption(cached, new) {
return result;
}
if let Some(result) = check_and_subsumption(cached, new) {
return result;
}
if let Some(result) = check_in_subsumption(cached, new) {
return result;
}
SubsumptionResult::NoSubsumption
}
fn check_range_subsumption(cached: &Expression, new: &Expression) -> Option<SubsumptionResult> {
let (cached_infix, new_infix) = match (cached, new) {
(Expression::Infix(c), Expression::Infix(n)) => (c, n),
_ => return None,
};
let cached_col = extract_column_name(&cached_infix.left)?;
let new_col = extract_column_name(&new_infix.left)?;
if !cached_col.eq_ignore_ascii_case(&new_col) {
return None;
}
let cached_val = extract_numeric_value(&cached_infix.right)?;
let new_val = extract_numeric_value(&new_infix.right)?;
match (&cached_infix.op_type, &new_infix.op_type) {
(InfixOperator::GreaterThan, InfixOperator::GreaterThan)
| (InfixOperator::GreaterThan, InfixOperator::GreaterEqual) => {
if new_val > cached_val {
Some(SubsumptionResult::Subsumed {
filter: Box::new(new.clone()),
})
} else if (new_val - cached_val).abs() < f64::EPSILON {
Some(SubsumptionResult::Identical)
} else {
None
}
}
(InfixOperator::GreaterEqual, InfixOperator::GreaterThan)
| (InfixOperator::GreaterEqual, InfixOperator::GreaterEqual) => {
if new_val >= cached_val {
if (new_val - cached_val).abs() < f64::EPSILON
&& matches!(cached_infix.op_type, InfixOperator::GreaterEqual)
&& matches!(new_infix.op_type, InfixOperator::GreaterEqual)
{
Some(SubsumptionResult::Identical)
} else {
Some(SubsumptionResult::Subsumed {
filter: Box::new(new.clone()),
})
}
} else {
None
}
}
(InfixOperator::LessThan, InfixOperator::LessThan)
| (InfixOperator::LessThan, InfixOperator::LessEqual) => {
if new_val < cached_val {
Some(SubsumptionResult::Subsumed {
filter: Box::new(new.clone()),
})
} else if (new_val - cached_val).abs() < f64::EPSILON {
Some(SubsumptionResult::Identical)
} else {
None
}
}
(InfixOperator::LessEqual, InfixOperator::LessThan)
| (InfixOperator::LessEqual, InfixOperator::LessEqual) => {
if new_val <= cached_val {
if (new_val - cached_val).abs() < f64::EPSILON
&& matches!(cached_infix.op_type, InfixOperator::LessEqual)
&& matches!(new_infix.op_type, InfixOperator::LessEqual)
{
Some(SubsumptionResult::Identical)
} else {
Some(SubsumptionResult::Subsumed {
filter: Box::new(new.clone()),
})
}
} else {
None
}
}
(InfixOperator::Equal, InfixOperator::Equal) => {
if (new_val - cached_val).abs() < f64::EPSILON {
Some(SubsumptionResult::Identical)
} else {
None
}
}
_ => None,
}
}
fn check_and_subsumption(cached: &Expression, new: &Expression) -> Option<SubsumptionResult> {
let new_infix = match new {
Expression::Infix(infix) if matches!(infix.op_type, InfixOperator::And) => infix,
_ => return None,
};
if expressions_equivalent(cached, &new_infix.left)
|| expressions_equivalent(cached, &new_infix.right)
{
return Some(SubsumptionResult::Subsumed {
filter: Box::new(new.clone()),
});
}
if let Expression::Infix(cached_infix) = cached {
if matches!(cached_infix.op_type, InfixOperator::And) {
let cached_conditions = extract_and_conditions(cached);
let new_conditions = extract_and_conditions(new);
let all_cached_present = cached_conditions.iter().all(|cc| {
new_conditions
.iter()
.any(|nc| expressions_equivalent(cc, nc))
});
if all_cached_present && new_conditions.len() > cached_conditions.len() {
return Some(SubsumptionResult::Subsumed {
filter: Box::new(new.clone()),
});
}
}
}
None
}
fn check_in_subsumption(cached: &Expression, new: &Expression) -> Option<SubsumptionResult> {
let (cached_in, new_in) = match (cached, new) {
(Expression::In(c), Expression::In(n)) => (c, n),
_ => return None,
};
if !expressions_equivalent(&cached_in.left, &new_in.left) {
return None;
}
let cached_values = extract_in_values(&cached_in.right)?;
let new_values = extract_in_values(&new_in.right)?;
let is_subset = new_values
.iter()
.all(|nv| cached_values.iter().any(|cv| values_equal(cv, nv)));
if is_subset {
if new_values.len() == cached_values.len() {
Some(SubsumptionResult::Identical)
} else {
Some(SubsumptionResult::Subsumed {
filter: Box::new(new.clone()),
})
}
} else {
None
}
}
#[derive(Debug, Clone, Copy)]
enum NumericValue {
Integer(i64),
Float(f64),
}
impl NumericValue {
fn as_f64(&self) -> f64 {
match self {
NumericValue::Integer(i) => *i as f64,
NumericValue::Float(f) => *f,
}
}
}
impl PartialEq for NumericValue {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(NumericValue::Integer(a), NumericValue::Integer(b)) => a == b,
_ => (self.as_f64() - other.as_f64()).abs() < 1e-10,
}
}
}
fn extract_in_values(expr: &Expression) -> Option<Vec<NumericValue>> {
match expr {
Expression::List(list) => Some(
list.elements
.iter()
.filter_map(|e| match e {
Expression::IntegerLiteral(lit) => Some(NumericValue::Integer(lit.value)),
Expression::FloatLiteral(lit) => Some(NumericValue::Float(lit.value)),
_ => None,
})
.collect(),
),
Expression::ExpressionList(list) => Some(
list.expressions
.iter()
.filter_map(|e| match e {
Expression::IntegerLiteral(lit) => Some(NumericValue::Integer(lit.value)),
Expression::FloatLiteral(lit) => Some(NumericValue::Float(lit.value)),
_ => None,
})
.collect(),
),
_ => None,
}
}
fn values_equal(a: &NumericValue, b: &NumericValue) -> bool {
a == b
}
fn extract_numeric_value(expr: &Expression) -> Option<f64> {
match expr {
Expression::IntegerLiteral(lit) => Some(lit.value as f64),
Expression::FloatLiteral(lit) => Some(lit.value),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::Value;
use crate::parser::ast::{Identifier, InExpression, InfixExpression, ListExpression};
use crate::parser::token::{Position, Token, TokenType};
fn make_token() -> Token {
Token {
token_type: TokenType::Integer,
literal: "".into(),
position: Position::new(0, 1, 1),
quoted: false,
}
}
fn make_identifier(name: &str) -> Expression {
Expression::Identifier(Identifier::new(make_token(), name.to_string()))
}
fn make_int_literal(val: i64) -> Expression {
Expression::IntegerLiteral(crate::parser::ast::IntegerLiteral {
token: make_token(),
value: val,
})
}
fn make_gt(col: &str, val: i64) -> Expression {
Expression::Infix(InfixExpression::new(
make_token(),
Box::new(make_identifier(col)),
">".to_string(),
Box::new(make_int_literal(val)),
))
}
fn make_lt(col: &str, val: i64) -> Expression {
Expression::Infix(InfixExpression::new(
make_token(),
Box::new(make_identifier(col)),
"<".to_string(),
Box::new(make_int_literal(val)),
))
}
fn make_and(left: Expression, right: Expression) -> Expression {
Expression::Infix(InfixExpression::new(
make_token(),
Box::new(left),
"AND".to_string(),
Box::new(right),
))
}
fn make_in(col: &str, values: Vec<i64>) -> Expression {
Expression::In(InExpression {
token: make_token(),
left: Box::new(make_identifier(col)),
right: Box::new(Expression::List(Box::new(ListExpression {
token: make_token(),
elements: values.into_iter().map(make_int_literal).collect(),
}))),
not: false,
})
}
#[test]
fn test_identical_predicates() {
let pred1 = make_gt("amount", 100);
let pred2 = make_gt("amount", 100);
match check_subsumption(Some(&pred1), Some(&pred2)) {
SubsumptionResult::Identical => {}
other => panic!("Expected Identical, got {:?}", other),
}
}
#[test]
fn test_range_subsumption_greater_than() {
let cached = make_gt("amount", 100);
let new = make_gt("amount", 150);
match check_subsumption(Some(&cached), Some(&new)) {
SubsumptionResult::Subsumed { .. } => {}
other => panic!("Expected Subsumed, got {:?}", other),
}
match check_subsumption(Some(&new), Some(&cached)) {
SubsumptionResult::NoSubsumption => {}
other => panic!("Expected NoSubsumption, got {:?}", other),
}
}
#[test]
fn test_range_subsumption_less_than() {
let cached = make_lt("amount", 500);
let new = make_lt("amount", 300);
match check_subsumption(Some(&cached), Some(&new)) {
SubsumptionResult::Subsumed { .. } => {}
other => panic!("Expected Subsumed, got {:?}", other),
}
}
#[test]
fn test_and_subsumption() {
let cached = make_gt("amount", 100);
let status_check = make_gt("status", 0);
let new = make_and(make_gt("amount", 100), status_check);
match check_subsumption(Some(&cached), Some(&new)) {
SubsumptionResult::Subsumed { .. } => {}
other => panic!("Expected Subsumed, got {:?}", other),
}
}
#[test]
fn test_in_subsumption() {
let cached = make_in("id", vec![1, 2, 3, 4, 5]);
let new = make_in("id", vec![2, 3]);
match check_subsumption(Some(&cached), Some(&new)) {
SubsumptionResult::Subsumed { .. } => {}
other => panic!("Expected Subsumed, got {:?}", other),
}
}
#[test]
fn test_no_predicate_to_predicate() {
let new = make_gt("amount", 100);
match check_subsumption(None, Some(&new)) {
SubsumptionResult::Subsumed { .. } => {}
other => panic!("Expected Subsumed, got {:?}", other),
}
}
#[test]
fn test_cache_basic() {
let cache = SemanticCache::new();
let rows = vec![
Row::from_values(vec![Value::Integer(1), Value::Integer(200)]),
Row::from_values(vec![Value::Integer(2), Value::Integer(300)]),
Row::from_values(vec![Value::Integer(3), Value::Integer(400)]),
];
cache.insert(
"orders",
vec!["id".to_string(), "amount".to_string()],
rows.clone(),
Some(make_gt("amount", 100)),
);
assert_eq!(cache.size(), 1);
match cache.lookup(
"orders",
&["id".to_string(), "amount".to_string()],
Some(&make_gt("amount", 100)),
) {
CacheLookupResult::ExactHit(cached_rows) => {
assert_eq!(cached_rows.len(), 3);
}
other => panic!("Expected ExactHit, got {:?}", other),
}
let stats = cache.stats();
assert_eq!(stats.exact_hits, 1);
}
#[test]
fn test_cache_subsumption_lookup() {
let cache = SemanticCache::new();
let rows = vec![
Row::from_values(vec![Value::Integer(1), Value::Integer(150)]),
Row::from_values(vec![Value::Integer(2), Value::Integer(200)]),
Row::from_values(vec![Value::Integer(3), Value::Integer(300)]),
];
cache.insert(
"orders",
vec!["id".to_string(), "amount".to_string()],
rows,
Some(make_gt("amount", 100)),
);
match cache.lookup(
"orders",
&["id".to_string(), "amount".to_string()],
Some(&make_gt("amount", 180)),
) {
CacheLookupResult::SubsumptionHit { rows, .. } => {
assert_eq!(rows.len(), 3); }
other => panic!("Expected SubsumptionHit, got {:?}", other),
}
let stats = cache.stats();
assert_eq!(stats.subsumption_hits, 1);
}
#[test]
fn test_cache_invalidation() {
let cache = SemanticCache::new();
cache.insert(
"orders",
vec!["id".to_string()],
vec![Row::from_values(vec![Value::Integer(1)])],
None,
);
assert_eq!(cache.size(), 1);
cache.invalidate_table("orders");
assert_eq!(cache.size(), 0);
}
}