1use radixdb_core::time_compat::Instant;
78use rustc_hash::FxHasher;
79use std::borrow::Cow;
80use std::hash::{Hash, Hasher};
81use std::sync::atomic::{AtomicU64, Ordering};
82use std::sync::RwLock;
83use std::time::Duration;
84
85use radixdb_core::{CompactArc, StringMap};
86use radixdb_core::{Result, Row, Value, ValueSet};
87
88#[inline]
91fn to_lowercase_cow(s: &str) -> Cow<'_, str> {
92 if s.bytes().all(|b| !b.is_ascii_uppercase()) {
93 Cow::Borrowed(s)
94 } else {
95 Cow::Owned(s.to_lowercase())
96 }
97}
98use radixdb_functions::FunctionRegistry;
99use radixdb_sql::ast::{Expression, InfixOperator};
100
101use super::expression::ExpressionEval;
102use super::utils::{expressions_equivalent, extract_and_conditions, extract_column_name};
103
104pub const DEFAULT_SEMANTIC_CACHE_SIZE: usize = 64;
112
113pub const DEFAULT_CACHE_TTL_SECS: u64 = 300;
121
122pub const DEFAULT_MAX_CACHED_ROWS: usize = 100_000;
130
131pub const DEFAULT_MAX_GLOBAL_CACHED_ROWS: usize = 1_000_000;
139
140pub const DEFAULT_MAX_GLOBAL_CACHED_BYTES: usize = 256 * 1024 * 1024;
142
143#[derive(Debug, Clone, PartialEq, Eq, Hash)]
145pub struct QueryFingerprint {
146 pub table_name: String,
148 pub columns: Vec<String>,
150 pub predicate_structure_hash: u64,
152}
153
154impl QueryFingerprint {
155 pub fn new(table_name: &str, columns: Vec<String>) -> Self {
157 Self {
158 table_name: table_name.to_lowercase(),
159 columns,
160 predicate_structure_hash: 0,
161 }
162 }
163
164 pub fn with_predicate(table_name: &str, columns: Vec<String>, predicate: &Expression) -> Self {
166 Self {
167 table_name: table_name.to_lowercase(),
168 columns,
169 predicate_structure_hash: hash_predicate_structure(predicate),
170 }
171 }
172}
173
174#[derive(Debug, Clone)]
176pub struct CachedResult {
177 pub fingerprint: QueryFingerprint,
179 pub column_names: Vec<String>,
181 pub rows: CompactArc<Vec<Row>>,
183 pub predicate: Option<Expression>,
185 pub cached_at: Instant,
187 pub last_accessed: Instant,
189 pub access_count: u64,
191 pub estimated_bytes: usize,
193}
194
195impl CachedResult {
196 pub fn new(
198 fingerprint: QueryFingerprint,
199 column_names: Vec<String>,
200 rows: Vec<Row>,
201 predicate: Option<Expression>,
202 ) -> Self {
203 let now = Instant::now();
204 let estimated_bytes = estimate_cached_rows_bytes(&rows);
205 Self {
206 fingerprint,
207 column_names,
208 rows: CompactArc::new(rows), predicate,
210 cached_at: now,
211 last_accessed: now,
212 access_count: 1,
213 estimated_bytes,
214 }
215 }
216
217 pub fn new_with_arc(
222 fingerprint: QueryFingerprint,
223 column_names: Vec<String>,
224 rows: CompactArc<Vec<Row>>,
225 predicate: Option<Expression>,
226 ) -> Self {
227 let now = Instant::now();
228 let estimated_bytes = estimate_cached_rows_bytes(&rows);
229 Self {
230 fingerprint,
231 column_names,
232 rows, predicate,
234 cached_at: now,
235 last_accessed: now,
236 access_count: 1,
237 estimated_bytes,
238 }
239 }
240
241 pub fn is_expired(&self, ttl: Duration) -> bool {
243 self.cached_at.elapsed() > ttl
244 }
245
246 #[inline]
249 pub fn is_expired_at(&self, ttl: Duration, now: Instant) -> bool {
250 now.duration_since(self.cached_at) > ttl
251 }
252
253 pub fn record_access(&mut self) {
255 self.last_accessed = Instant::now();
256 self.access_count += 1;
257 }
258}
259
260#[derive(Debug, Clone)]
262pub enum SubsumptionResult {
263 Subsumed {
265 filter: Box<Expression>,
267 },
268 Identical,
270 NoSubsumption,
272}
273
274pub struct SemanticCache {
279 cache: RwLock<StringMap<StringMap<Vec<CachedResult>>>>,
282 max_size: usize,
284 ttl: Duration,
286 max_rows: usize,
288 max_global_rows: usize,
290 max_global_bytes: usize,
292 global_row_count: AtomicU64,
294 global_byte_count: AtomicU64,
296 generation: AtomicU64,
298 stats: SemanticCacheStats,
300}
301
302#[derive(Debug, Default)]
304pub struct SemanticCacheStats {
305 pub hits: AtomicU64,
307 pub exact_hits: AtomicU64,
309 pub subsumption_hits: AtomicU64,
311 pub misses: AtomicU64,
313 pub ttl_evictions: AtomicU64,
315 pub size_evictions: AtomicU64,
317 pub lock_failures: AtomicU64,
320}
321
322#[derive(Debug, Clone, Default)]
324pub struct SemanticCacheStatsSnapshot {
325 pub hits: u64,
327 pub exact_hits: u64,
329 pub subsumption_hits: u64,
331 pub misses: u64,
333 pub ttl_evictions: u64,
335 pub size_evictions: u64,
337 pub lock_failures: u64,
339}
340
341#[derive(Debug)]
343pub enum CacheLookupResult {
344 ExactHit(CompactArc<Vec<Row>>),
346 SubsumptionHit {
348 rows: CompactArc<Vec<Row>>,
350 filter: Box<Expression>,
352 columns: Vec<String>,
354 },
355 Miss,
357}
358
359fn estimate_cached_rows_bytes(rows: &[Row]) -> usize {
360 rows.iter().fold(std::mem::size_of_val(rows), |total, row| {
361 row.iter().fold(
362 total.saturating_add(std::mem::size_of::<Row>()),
363 |row_total, value| {
364 let payload = match value {
365 Value::Text(text) => text.len(),
366 Value::Extension(bytes) => bytes.len(),
367 _ => 0,
368 };
369 row_total
370 .saturating_add(std::mem::size_of::<Value>())
371 .saturating_add(payload)
372 },
373 )
374 })
375}
376
377impl SemanticCache {
378 pub fn new() -> Self {
380 Self::with_config(
381 DEFAULT_SEMANTIC_CACHE_SIZE,
382 Duration::from_secs(DEFAULT_CACHE_TTL_SECS),
383 DEFAULT_MAX_CACHED_ROWS,
384 DEFAULT_MAX_GLOBAL_CACHED_ROWS,
385 )
386 }
387
388 pub fn with_config(
390 max_size: usize,
391 ttl: Duration,
392 max_rows: usize,
393 max_global_rows: usize,
394 ) -> Self {
395 Self::with_config_and_byte_limit(
396 max_size,
397 ttl,
398 max_rows,
399 max_global_rows,
400 DEFAULT_MAX_GLOBAL_CACHED_BYTES,
401 )
402 }
403
404 pub fn with_config_and_byte_limit(
406 max_size: usize,
407 ttl: Duration,
408 max_rows: usize,
409 max_global_rows: usize,
410 max_global_bytes: usize,
411 ) -> Self {
412 Self {
413 cache: RwLock::new(StringMap::new()),
414 max_size,
415 ttl,
416 max_rows,
417 max_global_rows,
418 max_global_bytes,
419 global_row_count: AtomicU64::new(0),
420 global_byte_count: AtomicU64::new(0),
421 generation: AtomicU64::new(0),
422 stats: SemanticCacheStats::default(),
423 }
424 }
425
426 pub fn lookup(
433 &self,
434 table_name: &str,
435 columns: &[String],
436 predicate: Option<&Expression>,
437 ) -> CacheLookupResult {
438 let (table_key, column_key) = Self::cache_keys(table_name, columns);
439
440 let hit_info = {
442 let cache = match self.cache.read() {
443 Ok(c) => c,
444 Err(_) => {
445 self.stats.lock_failures.fetch_add(1, Ordering::Relaxed);
446 return CacheLookupResult::Miss;
447 }
448 };
449
450 let table_cache = match cache.get(&table_key) {
452 Some(tc) => tc,
453 None => {
454 drop(cache);
455 self.record_miss();
456 return CacheLookupResult::Miss;
457 }
458 };
459
460 let entries = match table_cache.get(&column_key) {
461 Some(e) => e,
462 None => {
463 drop(cache);
464 self.record_miss();
465 return CacheLookupResult::Miss;
466 }
467 };
468
469 let mut found = None;
472 let now = Instant::now();
473 for (idx, entry) in entries.iter().enumerate() {
474 if entry.is_expired_at(self.ttl, now) {
476 continue;
477 }
478
479 if entry.column_names != columns {
481 continue;
482 }
483
484 match check_subsumption(entry.predicate.as_ref(), predicate) {
486 SubsumptionResult::Identical => {
487 let rows = entry.rows.clone();
488 let hash = entry.fingerprint.predicate_structure_hash;
489 found = Some((idx, hash, CacheLookupResult::ExactHit(rows)));
490 break;
491 }
492 SubsumptionResult::Subsumed { filter } => {
493 let rows = entry.rows.clone();
494 let columns = entry.column_names.clone();
495 let hash = entry.fingerprint.predicate_structure_hash;
496 found = Some((
497 idx,
498 hash,
499 CacheLookupResult::SubsumptionHit {
500 rows,
501 filter,
502 columns,
503 },
504 ));
505 break;
506 }
507 SubsumptionResult::NoSubsumption => {
508 continue;
510 }
511 }
512 }
513 found
514 }; match hit_info {
517 Some((idx, expected_hash, result)) => {
518 if let Ok(mut cache) = self.cache.write() {
522 if let Some(table_cache) = cache.get_mut(&table_key) {
523 if let Some(entries) = table_cache.get_mut(&column_key) {
524 if let Some(entry) = entries.get_mut(idx) {
525 if entry.fingerprint.predicate_structure_hash == expected_hash {
527 entry.record_access();
528 }
529 }
530 }
531 }
532 }
533 match &result {
535 CacheLookupResult::ExactHit(_) => self.record_exact_hit(),
536 CacheLookupResult::SubsumptionHit { .. } => self.record_subsumption_hit(),
537 CacheLookupResult::Miss => {}
538 }
539 result
540 }
541 None => {
542 self.record_miss();
543 CacheLookupResult::Miss
544 }
545 }
546 }
547
548 #[cfg(test)]
550 pub(crate) fn insert(
551 &self,
552 table_name: &str,
553 columns: Vec<String>,
554 rows: Vec<Row>,
555 predicate: Option<Expression>,
556 ) {
557 let new_row_count = rows.len();
558
559 if new_row_count > self.max_rows {
561 return;
562 }
563
564 let (table_key, column_key) = Self::cache_keys(table_name, &columns);
565 let fingerprint = match &predicate {
566 Some(p) => QueryFingerprint::with_predicate(table_name, columns.clone(), p),
567 None => QueryFingerprint::new(table_name, columns.clone()),
568 };
569
570 let entry = CachedResult::new(fingerprint, columns, rows, predicate);
571 self.insert_entry(
572 entry,
573 new_row_count,
574 table_key,
575 column_key,
576 self.generation(),
577 );
578 }
579
580 pub fn generation(&self) -> u64 {
582 self.generation.load(Ordering::Acquire)
583 }
584
585 pub fn insert_if_generation(
587 &self,
588 expected_generation: u64,
589 table_name: &str,
590 columns: Vec<String>,
591 rows: Vec<Row>,
592 predicate: Option<Expression>,
593 ) {
594 if self.generation() != expected_generation || rows.len() > self.max_rows {
595 return;
596 }
597 let new_row_count = rows.len();
598 let (table_key, column_key) = Self::cache_keys(table_name, &columns);
599 let fingerprint = match &predicate {
600 Some(predicate) => {
601 QueryFingerprint::with_predicate(table_name, columns.clone(), predicate)
602 }
603 None => QueryFingerprint::new(table_name, columns.clone()),
604 };
605 let entry = CachedResult::new(fingerprint, columns, rows, predicate);
606 self.insert_entry(
607 entry,
608 new_row_count,
609 table_key,
610 column_key,
611 expected_generation,
612 );
613 }
614
615 fn insert_entry(
617 &self,
618 entry: CachedResult,
619 new_row_count: usize,
620 table_key: String,
621 column_key: String,
622 expected_generation: u64,
623 ) {
624 if self.max_size == 0
625 || self.max_rows == 0
626 || self.max_global_rows == 0
627 || self.max_global_bytes == 0
628 {
629 return;
630 }
631 let new_byte_count = entry.estimated_bytes;
632 if new_row_count > self.max_global_rows || new_byte_count > self.max_global_bytes {
633 return;
634 }
635
636 let mut cache = match self.cache.write() {
637 Ok(c) => c,
638 Err(_) => {
639 self.stats.lock_failures.fetch_add(1, Ordering::Relaxed);
640 return;
641 }
642 };
643 if self.generation() != expected_generation {
644 return;
645 }
646
647 let current_global = self.global_row_count.load(Ordering::Relaxed) as usize;
651 let current_bytes = self.global_byte_count.load(Ordering::Relaxed) as usize;
652 let rows_to_free = (current_global + new_row_count).saturating_sub(self.max_global_rows);
653 let bytes_to_free = (current_bytes + new_byte_count).saturating_sub(self.max_global_bytes);
654 if rows_to_free > 0 || bytes_to_free > 0 {
655 self.evict_global_lru(&mut cache, rows_to_free, bytes_to_free);
656 }
657
658 let table_cache = cache.entry(table_key).or_default();
660 let entries = table_cache.entry(column_key).or_default();
661
662 let mut rows_freed: usize = 0;
664 let mut bytes_freed: usize = 0;
665 let before_len = entries.len();
666 let now = Instant::now();
667 entries.retain(|e| {
668 if e.is_expired_at(self.ttl, now) {
669 rows_freed += e.rows.len();
670 bytes_freed += e.estimated_bytes;
671 false
672 } else {
673 true
674 }
675 });
676 let evicted = before_len - entries.len();
677 if evicted > 0 {
678 self.stats
679 .ttl_evictions
680 .fetch_add(evicted as u64, Ordering::Relaxed);
681 }
682
683 while entries.len() >= self.max_size {
685 if let Some((idx, _)) = entries
686 .iter()
687 .enumerate()
688 .min_by_key(|(_, e)| (e.last_accessed, e.access_count))
689 {
690 rows_freed += entries[idx].rows.len();
691 bytes_freed += entries[idx].estimated_bytes;
692 entries.remove(idx);
693 self.stats.size_evictions.fetch_add(1, Ordering::Relaxed);
694 } else {
695 break;
696 }
697 }
698
699 if rows_freed > 0 {
701 self.global_row_count
702 .fetch_sub(rows_freed as u64, Ordering::Relaxed);
703 }
704 if bytes_freed > 0 {
705 self.global_byte_count
706 .fetch_sub(bytes_freed as u64, Ordering::Relaxed);
707 }
708
709 self.global_row_count
711 .fetch_add(new_row_count as u64, Ordering::Relaxed);
712 self.global_byte_count
713 .fetch_add(new_byte_count as u64, Ordering::Relaxed);
714 entries.push(entry);
715 }
716
717 fn evict_global_lru(
719 &self,
720 cache: &mut StringMap<StringMap<Vec<CachedResult>>>,
721 mut rows_to_free: usize,
722 mut bytes_to_free: usize,
723 ) {
724 while rows_to_free > 0 || bytes_to_free > 0 {
725 let mut oldest: Option<(String, String, usize, Instant, u64, usize, usize)> = None;
727
728 for (table_key, table_cache) in cache.iter() {
729 for (col_key, entries) in table_cache.iter() {
730 for (idx, entry) in entries.iter().enumerate() {
731 let dominated = match &oldest {
732 None => true,
733 Some((_, _, _, last_acc, acc_count, _, _)) => {
734 (entry.last_accessed, entry.access_count) < (*last_acc, *acc_count)
735 }
736 };
737 if dominated {
738 oldest = Some((
739 table_key.clone(),
740 col_key.clone(),
741 idx,
742 entry.last_accessed,
743 entry.access_count,
744 entry.rows.len(),
745 entry.estimated_bytes,
746 ));
747 }
748 }
749 }
750 }
751
752 match oldest {
753 Some((table_key, col_key, idx, _, _, row_count, byte_count)) => {
754 if let Some(table_cache) = cache.get_mut(&table_key) {
755 if let Some(entries) = table_cache.get_mut(&col_key) {
756 entries.remove(idx);
757 self.global_row_count
758 .fetch_sub(row_count as u64, Ordering::Relaxed);
759 self.global_byte_count
760 .fetch_sub(byte_count as u64, Ordering::Relaxed);
761 self.stats.size_evictions.fetch_add(1, Ordering::Relaxed);
762 rows_to_free = rows_to_free.saturating_sub(row_count);
763 bytes_to_free = bytes_to_free.saturating_sub(byte_count);
764
765 if entries.is_empty() {
767 table_cache.remove(&col_key);
768 }
769 }
770 if table_cache.is_empty() {
771 cache.remove(&table_key);
772 }
773 }
774 }
775 None => break, }
777 }
778 }
779
780 pub fn invalidate_table(&self, table_name: &str) {
782 let table_key = to_lowercase_cow(table_name);
783 match self.cache.write() {
784 Ok(mut cache) => {
785 self.generation.fetch_add(1, Ordering::AcqRel);
786 if let Some(table_cache) = cache.get(table_key.as_ref()) {
788 let rows_removed: usize = table_cache
789 .values()
790 .flat_map(|entries| entries.iter())
791 .map(|e| e.rows.len())
792 .sum();
793 let bytes_removed: usize = table_cache
794 .values()
795 .flat_map(|entries| entries.iter())
796 .map(|entry| entry.estimated_bytes)
797 .sum();
798 if rows_removed > 0 {
799 self.global_row_count
800 .fetch_sub(rows_removed as u64, Ordering::Relaxed);
801 }
802 if bytes_removed > 0 {
803 self.global_byte_count
804 .fetch_sub(bytes_removed as u64, Ordering::Relaxed);
805 }
806 }
807 cache.remove(table_key.as_ref());
809 }
810 Err(_) => {
811 self.stats.lock_failures.fetch_add(1, Ordering::Relaxed);
812 }
813 }
814 }
815
816 pub fn clear(&self) {
818 match self.cache.write() {
819 Ok(mut cache) => {
820 self.generation.fetch_add(1, Ordering::AcqRel);
821 cache.clear();
822 }
823 Err(_) => {
824 self.stats.lock_failures.fetch_add(1, Ordering::Relaxed);
825 }
826 }
827 self.global_row_count.store(0, Ordering::Relaxed);
829 self.global_byte_count.store(0, Ordering::Relaxed);
830 self.stats.hits.store(0, Ordering::Relaxed);
832 self.stats.exact_hits.store(0, Ordering::Relaxed);
833 self.stats.subsumption_hits.store(0, Ordering::Relaxed);
834 self.stats.misses.store(0, Ordering::Relaxed);
835 self.stats.ttl_evictions.store(0, Ordering::Relaxed);
836 self.stats.size_evictions.store(0, Ordering::Relaxed);
837 }
839
840 pub fn stats(&self) -> SemanticCacheStatsSnapshot {
842 SemanticCacheStatsSnapshot {
843 hits: self.stats.hits.load(Ordering::Relaxed),
844 exact_hits: self.stats.exact_hits.load(Ordering::Relaxed),
845 subsumption_hits: self.stats.subsumption_hits.load(Ordering::Relaxed),
846 misses: self.stats.misses.load(Ordering::Relaxed),
847 ttl_evictions: self.stats.ttl_evictions.load(Ordering::Relaxed),
848 size_evictions: self.stats.size_evictions.load(Ordering::Relaxed),
849 lock_failures: self.stats.lock_failures.load(Ordering::Relaxed),
850 }
851 }
852
853 pub fn size(&self) -> usize {
855 self.cache
856 .read()
857 .map(|c| {
858 c.values()
860 .map(|table_cache| table_cache.values().map(|v| v.len()).sum::<usize>())
861 .sum()
862 })
863 .unwrap_or(0)
864 }
865
866 pub fn filter_rows(
874 rows: Vec<Row>,
875 filter: &Expression,
876 columns: &[String],
877 _function_registry: &FunctionRegistry,
878 ) -> Result<Vec<Row>> {
879 let columns_vec: Vec<String> = columns.to_vec();
880 let mut eval = ExpressionEval::compile(filter, &columns_vec)?;
882
883 let mut result = Vec::with_capacity(rows.len());
884 for row in rows {
885 if eval.eval_bool_checked(&row)? {
886 result.push(row);
887 }
888 }
889 Ok(result)
890 }
891
892 fn cache_keys(table_name: &str, columns: &[String]) -> (String, String) {
896 let table_key = table_name.to_lowercase();
899 let mut sorted_cols = columns.to_vec();
900 sorted_cols.sort();
901 let column_key = sorted_cols.join("\0");
902 (table_key, column_key)
903 }
904
905 fn record_exact_hit(&self) {
906 self.stats.hits.fetch_add(1, Ordering::Relaxed);
907 self.stats.exact_hits.fetch_add(1, Ordering::Relaxed);
908 }
909
910 fn record_subsumption_hit(&self) {
911 self.stats.hits.fetch_add(1, Ordering::Relaxed);
912 self.stats.subsumption_hits.fetch_add(1, Ordering::Relaxed);
913 }
914
915 fn record_miss(&self) {
916 self.stats.misses.fetch_add(1, Ordering::Relaxed);
917 }
918}
919
920impl Default for SemanticCache {
921 fn default() -> Self {
922 Self::new()
923 }
924}
925
926fn hash_predicate_structure(expr: &Expression) -> u64 {
933 let mut hasher = FxHasher::default();
934 hash_expr_structure(expr, &mut hasher);
935 hasher.finish()
936}
937
938fn hash_expr_structure(expr: &Expression, hasher: &mut FxHasher) {
939 match expr {
940 Expression::Identifier(ident) => {
941 0u8.hash(hasher);
942 ident.value_lower.hash(hasher);
943 }
944 Expression::QualifiedIdentifier(qi) => {
945 1u8.hash(hasher);
946 qi.qualifier.value_lower.hash(hasher);
947 qi.name.value_lower.hash(hasher);
948 }
949 Expression::IntegerLiteral(_) => {
950 2u8.hash(hasher);
951 }
952 Expression::FloatLiteral(_) => {
953 3u8.hash(hasher);
954 }
955 Expression::StringLiteral(_) => {
956 4u8.hash(hasher);
957 }
958 Expression::BooleanLiteral(_) => {
959 5u8.hash(hasher);
960 }
961 Expression::NullLiteral(_) => {
962 6u8.hash(hasher);
963 }
964 Expression::Infix(infix) => {
965 7u8.hash(hasher);
966 std::mem::discriminant(&infix.op_type).hash(hasher);
968 hash_expr_structure(&infix.left, hasher);
969 hash_expr_structure(&infix.right, hasher);
970 }
971 Expression::Prefix(prefix) => {
972 8u8.hash(hasher);
973 prefix.operator.hash(hasher);
974 hash_expr_structure(&prefix.right, hasher);
975 }
976 Expression::Between(between) => {
977 9u8.hash(hasher);
978 hash_expr_structure(&between.expr, hasher);
979 }
980 Expression::In(in_expr) => {
981 10u8.hash(hasher);
982 hash_expr_structure(&in_expr.left, hasher);
983 }
984 Expression::FunctionCall(func) => {
985 11u8.hash(hasher);
986 func.function.to_lowercase().hash(hasher);
987 func.arguments.len().hash(hasher);
988 }
989 Expression::Case(_) => {
990 12u8.hash(hasher);
991 }
992 Expression::List(list) => {
993 13u8.hash(hasher);
994 list.elements.len().hash(hasher);
995 }
996 Expression::Window(win) => {
997 14u8.hash(hasher);
998 win.function.function.to_lowercase().hash(hasher);
999 win.function.arguments.len().hash(hasher);
1000 win.partition_by.len().hash(hasher);
1001 win.order_by.len().hash(hasher);
1002 }
1003 _ => {
1004 255u8.hash(hasher);
1006 }
1007 }
1008}
1009
1010pub fn check_subsumption(
1017 cached_predicate: Option<&Expression>,
1018 new_predicate: Option<&Expression>,
1019) -> SubsumptionResult {
1020 match (cached_predicate, new_predicate) {
1021 (None, None) => SubsumptionResult::Identical,
1023
1024 (Some(_), None) => SubsumptionResult::NoSubsumption,
1026
1027 (None, Some(new_pred)) => SubsumptionResult::Subsumed {
1030 filter: Box::new(new_pred.clone()),
1031 },
1032
1033 (Some(cached), Some(new)) => check_predicate_subsumption(cached, new),
1035 }
1036}
1037
1038fn check_predicate_subsumption(cached: &Expression, new: &Expression) -> SubsumptionResult {
1040 if expressions_equivalent(cached, new) {
1042 return SubsumptionResult::Identical;
1043 }
1044
1045 if let Some(result) = check_range_subsumption(cached, new) {
1047 return result;
1048 }
1049
1050 if let Some(result) = check_and_subsumption(cached, new) {
1052 return result;
1053 }
1054
1055 if let Some(result) = check_in_subsumption(cached, new) {
1057 return result;
1058 }
1059
1060 SubsumptionResult::NoSubsumption
1061}
1062
1063fn check_range_subsumption(cached: &Expression, new: &Expression) -> Option<SubsumptionResult> {
1069 let (cached_infix, new_infix) = match (cached, new) {
1071 (Expression::Infix(c), Expression::Infix(n)) => (c, n),
1072 _ => return None,
1073 };
1074
1075 let cached_col = extract_column_name(&cached_infix.left)?;
1077 let new_col = extract_column_name(&new_infix.left)?;
1078
1079 if !cached_col.eq_ignore_ascii_case(&new_col) {
1081 return None;
1082 }
1083
1084 let cached_val = extract_literal_value(&cached_infix.right)?;
1085 let new_val = extract_literal_value(&new_infix.right)?;
1086 let bound_order = new_val.cmp(&cached_val);
1087
1088 match (&cached_infix.op_type, &new_infix.op_type) {
1090 (
1092 InfixOperator::GreaterThan | InfixOperator::GreaterEqual,
1093 InfixOperator::GreaterThan | InfixOperator::GreaterEqual,
1094 ) => match bound_order {
1095 std::cmp::Ordering::Greater => Some(SubsumptionResult::Subsumed {
1096 filter: Box::new(new.clone()),
1097 }),
1098 std::cmp::Ordering::Less => None,
1099 std::cmp::Ordering::Equal => match (&cached_infix.op_type, &new_infix.op_type) {
1100 (InfixOperator::GreaterEqual, InfixOperator::GreaterThan) => {
1101 Some(SubsumptionResult::Subsumed {
1102 filter: Box::new(new.clone()),
1103 })
1104 }
1105 (cached_op, new_op) if cached_op == new_op => Some(SubsumptionResult::Identical),
1106 _ => None,
1107 },
1108 },
1109
1110 (
1112 InfixOperator::LessThan | InfixOperator::LessEqual,
1113 InfixOperator::LessThan | InfixOperator::LessEqual,
1114 ) => match bound_order {
1115 std::cmp::Ordering::Less => Some(SubsumptionResult::Subsumed {
1116 filter: Box::new(new.clone()),
1117 }),
1118 std::cmp::Ordering::Greater => None,
1119 std::cmp::Ordering::Equal => match (&cached_infix.op_type, &new_infix.op_type) {
1120 (InfixOperator::LessEqual, InfixOperator::LessThan) => {
1121 Some(SubsumptionResult::Subsumed {
1122 filter: Box::new(new.clone()),
1123 })
1124 }
1125 (cached_op, new_op) if cached_op == new_op => Some(SubsumptionResult::Identical),
1126 _ => None,
1127 },
1128 },
1129
1130 (InfixOperator::Equal, InfixOperator::Equal) => {
1132 if new_val == cached_val {
1133 Some(SubsumptionResult::Identical)
1134 } else {
1135 None
1136 }
1137 }
1138
1139 _ => None,
1140 }
1141}
1142
1143fn check_and_subsumption(cached: &Expression, new: &Expression) -> Option<SubsumptionResult> {
1147 let new_infix = match new {
1149 Expression::Infix(infix) if matches!(infix.op_type, InfixOperator::And) => infix,
1150 _ => return None,
1151 };
1152
1153 if expressions_equivalent(cached, &new_infix.left)
1155 || expressions_equivalent(cached, &new_infix.right)
1156 {
1157 return Some(SubsumptionResult::Subsumed {
1159 filter: Box::new(new.clone()),
1160 });
1161 }
1162
1163 if let Expression::Infix(cached_infix) = cached {
1165 if matches!(cached_infix.op_type, InfixOperator::And) {
1166 let cached_conditions = extract_and_conditions(cached);
1168 let new_conditions = extract_and_conditions(new);
1169
1170 let all_cached_present = cached_conditions.iter().all(|cc| {
1172 new_conditions
1173 .iter()
1174 .any(|nc| expressions_equivalent(cc, nc))
1175 });
1176
1177 if all_cached_present && new_conditions.len() > cached_conditions.len() {
1178 return Some(SubsumptionResult::Subsumed {
1179 filter: Box::new(new.clone()),
1180 });
1181 }
1182 }
1183 }
1184
1185 None
1186}
1187
1188fn check_in_subsumption(cached: &Expression, new: &Expression) -> Option<SubsumptionResult> {
1192 let (cached_in, new_in) = match (cached, new) {
1193 (Expression::In(c), Expression::In(n)) => (c, n),
1194 _ => return None,
1195 };
1196
1197 if cached_in.not || new_in.not {
1201 return None;
1202 }
1203
1204 if !expressions_equivalent(&cached_in.left, &new_in.left) {
1206 return None;
1207 }
1208
1209 let cached_values = extract_in_values(&cached_in.right)?;
1211 let new_values = extract_in_values(&new_in.right)?;
1212
1213 let is_subset = new_values.iter().all(|value| cached_values.contains(value));
1215
1216 if is_subset {
1217 if new_values.len() == cached_values.len() {
1218 Some(SubsumptionResult::Identical)
1219 } else {
1220 Some(SubsumptionResult::Subsumed {
1221 filter: Box::new(new.clone()),
1222 })
1223 }
1224 } else {
1225 None
1226 }
1227}
1228
1229fn extract_in_values(expr: &Expression) -> Option<ValueSet> {
1231 match expr {
1232 Expression::List(list) => list.elements.iter().map(extract_literal_value).collect(),
1233 Expression::ExpressionList(list) => {
1234 list.expressions.iter().map(extract_literal_value).collect()
1235 }
1236 _ => None,
1237 }
1238}
1239
1240fn extract_literal_value(expr: &Expression) -> Option<Value> {
1241 match expr {
1242 Expression::IntegerLiteral(lit) => Some(Value::Integer(lit.value)),
1243 Expression::FloatLiteral(lit) => Some(Value::Float(lit.value)),
1244 Expression::StringLiteral(lit) => Some(Value::Text(lit.value.clone())),
1245 Expression::BooleanLiteral(lit) => Some(Value::Boolean(lit.value)),
1246 _ => None,
1247 }
1248}
1249
1250#[cfg(test)]
1251mod tests {
1252 use super::*;
1253 use radixdb_sql::ast::{
1254 FloatLiteral, Identifier, InExpression, InfixExpression, ListExpression,
1255 };
1256 use radixdb_sql::token::{Position, Token, TokenType};
1257
1258 fn parsed_where(sql: &str) -> Expression {
1259 let statements = radixdb_sql::parse_sql(sql).unwrap();
1260 let radixdb_sql::Statement::Select(select) = &statements[0] else {
1261 panic!("expected SELECT")
1262 };
1263 select.where_clause.as_deref().unwrap().clone()
1264 }
1265
1266 #[test]
1267 fn r5_l03_semantic_and_classification_identity_preserve_complete_ast_semantic() {
1268 let cases = [
1269 ("x > 100", "x >= 100"),
1270 ("x < 100", "x <= 100"),
1271 ("x IN (1)", "x NOT IN (1)"),
1272 ("x IN ('a')", "x IN ('b')"),
1273 ];
1274 for (cached, new) in cases {
1275 let cached = parsed_where(&format!("SELECT * FROM t WHERE {cached}"));
1276 let new = parsed_where(&format!("SELECT * FROM t WHERE {new}"));
1277 assert!(matches!(
1278 check_subsumption(Some(&cached), Some(&new)),
1279 SubsumptionResult::NoSubsumption
1280 ));
1281 }
1282
1283 let cached = parsed_where("SELECT * FROM t WHERE x IN (1, 1, 2)");
1284 let same_set = parsed_where("SELECT * FROM t WHERE x IN (2, 1)");
1285 assert!(matches!(
1286 check_subsumption(Some(&cached), Some(&same_set)),
1287 SubsumptionResult::Identical
1288 ));
1289 }
1290
1291 #[test]
1292 fn r5_l03_cache_budgets_and_lru_follow_runtime_usage_semantic() {
1293 let byte_bounded =
1294 SemanticCache::with_config_and_byte_limit(8, Duration::from_secs(60), 8, 8, 128);
1295 byte_bounded.insert(
1296 "wide",
1297 vec!["payload".to_string()],
1298 vec![Row::from_values(vec![Value::Text("x".repeat(1024).into())])],
1299 None,
1300 );
1301 assert!(matches!(
1302 byte_bounded.lookup("wide", &["payload".to_string()], None),
1303 CacheLookupResult::Miss
1304 ));
1305
1306 let row_bounded = SemanticCache::with_config_and_byte_limit(
1307 8,
1308 Duration::from_secs(60),
1309 8,
1310 2,
1311 1024 * 1024,
1312 );
1313 row_bounded.insert(
1314 "same_table",
1315 vec!["a".to_string()],
1316 vec![
1317 Row::from_values(vec![Value::Integer(1)]),
1318 Row::from_values(vec![Value::Integer(2)]),
1319 ],
1320 None,
1321 );
1322 row_bounded.insert(
1323 "same_table",
1324 vec!["b".to_string()],
1325 vec![
1326 Row::from_values(vec![Value::Integer(3)]),
1327 Row::from_values(vec![Value::Integer(4)]),
1328 ],
1329 None,
1330 );
1331 assert!(matches!(
1332 row_bounded.lookup("same_table", &["a".to_string()], None),
1333 CacheLookupResult::Miss
1334 ));
1335 assert!(matches!(
1336 row_bounded.lookup("same_table", &["b".to_string()], None),
1337 CacheLookupResult::ExactHit(_)
1338 ));
1339 }
1340
1341 fn make_token() -> Token {
1342 Token {
1343 token_type: TokenType::Integer,
1344 literal: "".into(),
1345 position: Position::new(0, 1, 1),
1346 quoted: false,
1347 }
1348 }
1349
1350 fn make_identifier(name: &str) -> Expression {
1351 Expression::Identifier(Identifier::new(make_token(), name.to_string()))
1352 }
1353
1354 fn make_int_literal(val: i64) -> Expression {
1355 Expression::IntegerLiteral(radixdb_sql::ast::IntegerLiteral {
1356 token: make_token(),
1357 value: val,
1358 })
1359 }
1360
1361 fn make_gt(col: &str, val: i64) -> Expression {
1362 Expression::Infix(InfixExpression::new(
1363 make_token(),
1364 Box::new(make_identifier(col)),
1365 ">".to_string(),
1366 Box::new(make_int_literal(val)),
1367 ))
1368 }
1369
1370 fn make_lt(col: &str, val: i64) -> Expression {
1371 Expression::Infix(InfixExpression::new(
1372 make_token(),
1373 Box::new(make_identifier(col)),
1374 "<".to_string(),
1375 Box::new(make_int_literal(val)),
1376 ))
1377 }
1378
1379 fn make_and(left: Expression, right: Expression) -> Expression {
1380 Expression::Infix(InfixExpression::new(
1381 make_token(),
1382 Box::new(left),
1383 "AND".to_string(),
1384 Box::new(right),
1385 ))
1386 }
1387
1388 fn make_in(col: &str, values: Vec<i64>) -> Expression {
1389 Expression::In(InExpression {
1390 token: make_token(),
1391 left: Box::new(make_identifier(col)),
1392 right: Box::new(Expression::List(Box::new(ListExpression {
1393 token: make_token(),
1394 elements: values.into_iter().map(make_int_literal).collect(),
1395 }))),
1396 not: false,
1397 })
1398 }
1399
1400 #[test]
1401 fn test_identical_predicates() {
1402 let pred1 = make_gt("amount", 100);
1403 let pred2 = make_gt("amount", 100);
1404
1405 match check_subsumption(Some(&pred1), Some(&pred2)) {
1406 SubsumptionResult::Identical => {}
1407 other => panic!("Expected Identical, got {:?}", other),
1408 }
1409 }
1410
1411 #[test]
1412 fn test_range_subsumption_greater_than() {
1413 let cached = make_gt("amount", 100);
1415 let new = make_gt("amount", 150);
1416
1417 match check_subsumption(Some(&cached), Some(&new)) {
1418 SubsumptionResult::Subsumed { .. } => {}
1419 other => panic!("Expected Subsumed, got {:?}", other),
1420 }
1421
1422 match check_subsumption(Some(&new), Some(&cached)) {
1424 SubsumptionResult::NoSubsumption => {}
1425 other => panic!("Expected NoSubsumption, got {:?}", other),
1426 }
1427 }
1428
1429 #[test]
1430 fn test_range_subsumption_less_than() {
1431 let cached = make_lt("amount", 500);
1433 let new = make_lt("amount", 300);
1434
1435 match check_subsumption(Some(&cached), Some(&new)) {
1436 SubsumptionResult::Subsumed { .. } => {}
1437 other => panic!("Expected Subsumed, got {:?}", other),
1438 }
1439 }
1440
1441 #[test]
1442 fn test_and_subsumption() {
1443 let cached = make_gt("amount", 100);
1445 let status_check = make_gt("status", 0);
1446 let new = make_and(make_gt("amount", 100), status_check);
1447
1448 match check_subsumption(Some(&cached), Some(&new)) {
1449 SubsumptionResult::Subsumed { .. } => {}
1450 other => panic!("Expected Subsumed, got {:?}", other),
1451 }
1452 }
1453
1454 #[test]
1455 fn test_in_subsumption() {
1456 let cached = make_in("id", vec![1, 2, 3, 4, 5]);
1458 let new = make_in("id", vec![2, 3]);
1459
1460 match check_subsumption(Some(&cached), Some(&new)) {
1461 SubsumptionResult::Subsumed { .. } => {}
1462 other => panic!("Expected Subsumed, got {:?}", other),
1463 }
1464 }
1465
1466 #[test]
1467 fn test_no_predicate_to_predicate() {
1468 let new = make_gt("amount", 100);
1470
1471 match check_subsumption(None, Some(&new)) {
1472 SubsumptionResult::Subsumed { .. } => {}
1473 other => panic!("Expected Subsumed, got {:?}", other),
1474 }
1475 }
1476
1477 #[test]
1478 fn test_cache_basic() {
1479 let cache = SemanticCache::new();
1480
1481 let rows = vec![
1483 Row::from_values(vec![Value::Integer(1), Value::Integer(200)]),
1484 Row::from_values(vec![Value::Integer(2), Value::Integer(300)]),
1485 Row::from_values(vec![Value::Integer(3), Value::Integer(400)]),
1486 ];
1487
1488 cache.insert(
1489 "orders",
1490 vec!["id".to_string(), "amount".to_string()],
1491 rows.clone(),
1492 Some(make_gt("amount", 100)),
1493 );
1494
1495 assert_eq!(cache.size(), 1);
1496
1497 match cache.lookup(
1499 "orders",
1500 &["id".to_string(), "amount".to_string()],
1501 Some(&make_gt("amount", 100)),
1502 ) {
1503 CacheLookupResult::ExactHit(cached_rows) => {
1504 assert_eq!(cached_rows.len(), 3);
1505 }
1506 other => panic!("Expected ExactHit, got {:?}", other),
1507 }
1508
1509 let stats = cache.stats();
1510 assert_eq!(stats.exact_hits, 1);
1511 }
1512
1513 #[test]
1514 fn test_cache_subsumption_lookup() {
1515 let cache = SemanticCache::new();
1516
1517 let rows = vec![
1519 Row::from_values(vec![Value::Integer(1), Value::Integer(150)]),
1520 Row::from_values(vec![Value::Integer(2), Value::Integer(200)]),
1521 Row::from_values(vec![Value::Integer(3), Value::Integer(300)]),
1522 ];
1523
1524 cache.insert(
1525 "orders",
1526 vec!["id".to_string(), "amount".to_string()],
1527 rows,
1528 Some(make_gt("amount", 100)),
1529 );
1530
1531 match cache.lookup(
1533 "orders",
1534 &["id".to_string(), "amount".to_string()],
1535 Some(&make_gt("amount", 180)),
1536 ) {
1537 CacheLookupResult::SubsumptionHit { rows, .. } => {
1538 assert_eq!(rows.len(), 3); }
1540 other => panic!("Expected SubsumptionHit, got {:?}", other),
1541 }
1542
1543 let stats = cache.stats();
1544 assert_eq!(stats.subsumption_hits, 1);
1545 }
1546
1547 #[test]
1548 fn test_cache_invalidation() {
1549 let cache = SemanticCache::new();
1550
1551 cache.insert(
1552 "orders",
1553 vec!["id".to_string()],
1554 vec![Row::from_values(vec![Value::Integer(1)])],
1555 None,
1556 );
1557
1558 assert_eq!(cache.size(), 1);
1559
1560 cache.invalidate_table("orders");
1561 assert_eq!(cache.size(), 0);
1562 }
1563
1564 #[test]
1565 fn v2_r5_zero_and_one_capacity_are_hard_bounds() {
1566 let disabled = SemanticCache::with_config(0, Duration::from_secs(60), 10, 10);
1567 disabled.insert(
1568 "t",
1569 vec!["id".to_string()],
1570 vec![Row::from_values(vec![Value::Integer(1)])],
1571 None,
1572 );
1573 assert_eq!(disabled.size(), 0);
1574
1575 let one = SemanticCache::with_config(1, Duration::from_secs(60), 10, 10);
1576 one.insert(
1577 "t",
1578 vec!["id".to_string()],
1579 vec![Row::from_values(vec![Value::Integer(1)])],
1580 Some(make_gt("id", 0)),
1581 );
1582 one.insert(
1583 "t",
1584 vec!["id".to_string()],
1585 vec![Row::from_values(vec![Value::Integer(2)])],
1586 Some(make_gt("id", 1)),
1587 );
1588 assert_eq!(one.size(), 1);
1589 }
1590
1591 #[test]
1592 fn v2_r5_adjacent_floats_are_not_exact_cache_hits() {
1593 let left = Expression::FloatLiteral(FloatLiteral {
1594 token: Token::new(TokenType::Float, "0.0", Position::default()),
1595 value: 0.0,
1596 });
1597 let right = Expression::FloatLiteral(FloatLiteral {
1598 token: Token::new(TokenType::Float, "5e-324", Position::default()),
1599 value: f64::from_bits(1),
1600 });
1601 assert!(!expressions_equivalent(&left, &right));
1602 assert!(expressions_equivalent(
1603 &Expression::FloatLiteral(FloatLiteral {
1604 token: Token::new(TokenType::Float, "-0.0", Position::default()),
1605 value: -0.0,
1606 }),
1607 &left,
1608 ));
1609 }
1610}