1use radixdb_core::time_compat::{system_time_now, UNIX_EPOCH};
40use rustc_hash::{FxHashMap, FxHasher};
41use std::hash::{Hash, Hasher};
42use std::sync::RwLock;
43
44use radixdb_sql::ast::Expression;
45
46pub const DEFAULT_DECAY_FACTOR: f64 = 0.3;
48
49pub const MIN_SAMPLE_COUNT: u64 = 2;
51
52pub const MAX_CORRECTION_FACTOR: f64 = 100.0;
54
55pub const MIN_CORRECTION_FACTOR: f64 = 0.01;
57
58#[inline]
59fn valid_decay_factor(decay_factor: f64) -> f64 {
60 if decay_factor.is_finite() && (0.0..=1.0).contains(&decay_factor) {
61 decay_factor
62 } else {
63 DEFAULT_DECAY_FACTOR
64 }
65}
66
67#[inline]
68fn apply_finite_correction(base_estimate: u64, correction: f64) -> u64 {
69 if !correction.is_finite() || correction <= 0.0 {
70 return base_estimate;
71 }
72 let corrected = base_estimate as f64 * correction;
73 if !corrected.is_finite() || corrected >= u64::MAX as f64 {
74 u64::MAX
75 } else {
76 (corrected.round() as u64).max(1)
77 }
78}
79
80#[derive(Debug, Clone)]
82pub struct CardinalityFeedback {
83 pub predicate_hash: u64,
85 pub table_name: String,
87 pub column_name: Option<String>,
89 pub estimated_rows: u64,
91 pub actual_rows: u64,
93 pub correction_factor: f64,
95 pub sample_count: u64,
97 pub last_updated: i64,
99}
100
101impl CardinalityFeedback {
102 pub fn new(
104 predicate_hash: u64,
105 table_name: impl Into<String>,
106 column_name: Option<String>,
107 estimated_rows: u64,
108 actual_rows: u64,
109 ) -> Self {
110 let correction = if estimated_rows > 0 {
111 (actual_rows as f64 / estimated_rows as f64)
112 .clamp(MIN_CORRECTION_FACTOR, MAX_CORRECTION_FACTOR)
113 } else {
114 1.0
115 };
116
117 Self {
118 predicate_hash,
119 table_name: table_name.into(),
120 column_name,
121 estimated_rows,
122 actual_rows,
123 correction_factor: correction,
124 sample_count: 1,
125 last_updated: get_current_timestamp(),
126 }
127 }
128
129 pub fn update(&mut self, estimated_rows: u64, actual_rows: u64, decay_factor: f64) {
131 let decay_factor = valid_decay_factor(decay_factor);
132 let new_correction = if estimated_rows > 0 {
133 (actual_rows as f64 / estimated_rows as f64)
134 .clamp(MIN_CORRECTION_FACTOR, MAX_CORRECTION_FACTOR)
135 } else {
136 1.0
137 };
138
139 self.correction_factor =
141 decay_factor * new_correction + (1.0 - decay_factor) * self.correction_factor;
142
143 self.correction_factor = self
145 .correction_factor
146 .clamp(MIN_CORRECTION_FACTOR, MAX_CORRECTION_FACTOR);
147
148 self.estimated_rows = estimated_rows;
149 self.actual_rows = actual_rows;
150 self.sample_count += 1;
151 self.last_updated = get_current_timestamp();
152 }
153
154 pub fn is_reliable(&self) -> bool {
156 self.sample_count >= MIN_SAMPLE_COUNT
157 }
158
159 pub fn apply_correction(&self, base_estimate: u64) -> u64 {
161 if !self.is_reliable() {
162 return base_estimate;
163 }
164 apply_finite_correction(base_estimate, self.correction_factor)
165 }
166}
167
168#[derive(Debug)]
170pub struct FeedbackCache {
171 entries: RwLock<FxHashMap<(String, u64), CardinalityFeedback>>,
173 decay_factor: f64,
175 max_entries: usize,
177}
178
179impl Default for FeedbackCache {
180 fn default() -> Self {
181 Self::new()
182 }
183}
184
185impl FeedbackCache {
186 pub fn new() -> Self {
188 Self {
189 entries: RwLock::new(FxHashMap::default()),
190 decay_factor: DEFAULT_DECAY_FACTOR,
191 max_entries: 10000,
192 }
193 }
194
195 pub fn with_settings(decay_factor: f64, max_entries: usize) -> Self {
197 Self {
198 entries: RwLock::new(FxHashMap::default()),
199 decay_factor: valid_decay_factor(decay_factor),
200 max_entries,
201 }
202 }
203
204 pub fn record_feedback(
206 &self,
207 table_name: &str,
208 predicate_hash: u64,
209 column_name: Option<String>,
210 estimated_rows: u64,
211 actual_rows: u64,
212 ) {
213 if self.max_entries == 0 {
214 return;
215 }
216 let key = (table_name.to_string(), predicate_hash);
217
218 let mut entries = self.entries.write().unwrap();
219
220 if let Some(existing) = entries.get_mut(&key) {
221 existing.update(estimated_rows, actual_rows, self.decay_factor);
222 } else {
223 if entries.len() >= self.max_entries {
225 self.evict_oldest(&mut entries);
226 }
227
228 let feedback = CardinalityFeedback::new(
229 predicate_hash,
230 table_name,
231 column_name,
232 estimated_rows,
233 actual_rows,
234 );
235 entries.insert(key, feedback);
236 }
237 }
238
239 pub fn lookup(&self, table_name: &str, predicate_hash: u64) -> Option<CardinalityFeedback> {
241 let key = (table_name.to_string(), predicate_hash);
242 let entries = self.entries.read().unwrap();
243 entries.get(&key).cloned()
244 }
245
246 pub fn get_correction(&self, table_name: &str, predicate_hash: u64) -> f64 {
248 match self.lookup(table_name, predicate_hash) {
249 Some(feedback)
250 if feedback.is_reliable()
251 && feedback.correction_factor.is_finite()
252 && feedback.correction_factor > 0.0 =>
253 {
254 feedback.correction_factor
255 }
256 _ => 1.0,
257 }
258 }
259
260 pub fn apply_correction(&self, table_name: &str, predicate_hash: u64, estimate: u64) -> u64 {
262 let correction = self.get_correction(table_name, predicate_hash);
263 apply_finite_correction(estimate, correction)
264 }
265
266 pub fn clear(&self) {
268 self.entries.write().unwrap().clear();
269 }
270
271 pub fn invalidate_table(&self, table_name: &str) {
273 self.entries
274 .write()
275 .unwrap()
276 .retain(|(name, _), _| !name.eq_ignore_ascii_case(table_name));
277 }
278
279 pub fn len(&self) -> usize {
281 self.entries.read().unwrap().len()
282 }
283
284 pub fn is_empty(&self) -> bool {
286 self.entries.read().unwrap().is_empty()
287 }
288
289 fn evict_oldest(&self, entries: &mut FxHashMap<(String, u64), CardinalityFeedback>) {
291 let evict_count = (self.max_entries / 10).max(1);
293
294 let mut timestamps: Vec<_> = entries
295 .iter()
296 .map(|(k, v)| (k.clone(), v.last_updated))
297 .collect();
298
299 timestamps.sort_by_key(|(_, ts)| *ts);
300
301 for (key, _) in timestamps.into_iter().take(evict_count) {
302 entries.remove(&key);
303 }
304 }
305
306 pub fn get_table_feedback(&self, table_name: &str) -> Vec<CardinalityFeedback> {
308 let entries = self.entries.read().unwrap();
309 entries
310 .iter()
311 .filter(|((name, _), _)| name == table_name)
312 .map(|(_, fb)| fb.clone())
313 .collect()
314 }
315}
316
317pub fn fingerprint_predicate(table_name: &str, expr: &Expression) -> u64 {
328 let mut hasher = FxHasher::default();
329
330 table_name.hash(&mut hasher);
332
333 hash_expression_structure(expr, &mut hasher);
335
336 hasher.finish()
337}
338
339fn hash_expression_structure(expr: &Expression, hasher: &mut FxHasher) {
341 std::mem::discriminant(expr).hash(hasher);
343
344 match expr {
345 Expression::Identifier(id) => {
346 id.value.hash(hasher);
348 }
349 Expression::QualifiedIdentifier(qid) => {
350 qid.qualifier.value.hash(hasher);
352 qid.name.value.hash(hasher);
353 }
354 Expression::Infix(infix) => {
355 infix.op_type.hash(hasher);
357 hash_expression_structure(&infix.left, hasher);
358 hash_expression_structure(&infix.right, hasher);
359 }
360 Expression::Prefix(prefix) => {
361 prefix.op_type.hash(hasher);
362 hash_expression_structure(&prefix.right, hasher);
363 }
364 Expression::Between(between) => {
365 "BETWEEN".hash(hasher);
366 between.not.hash(hasher);
367 hash_expression_structure(&between.expr, hasher);
368 hash_expression_structure(&between.lower, hasher);
370 hash_expression_structure(&between.upper, hasher);
371 }
372 Expression::In(in_expr) => {
373 "IN".hash(hasher);
374 in_expr.not.hash(hasher);
375 hash_expression_structure(&in_expr.left, hasher);
376 hash_expression_structure(&in_expr.right, hasher);
378 }
379 Expression::Like(like) => {
380 "LIKE".hash(hasher);
381 like.operator.hash(hasher);
382 hash_expression_structure(&like.left, hasher);
383 }
386 Expression::FunctionCall(func) => {
387 "FUNCTION".hash(hasher);
388 func.function.hash(hasher);
389 func.arguments.len().hash(hasher);
390 for arg in &func.arguments {
391 hash_expression_structure(arg, hasher);
392 }
393 }
394 Expression::Case(case) => {
395 "CASE".hash(hasher);
396 case.when_clauses.len().hash(hasher);
397 case.else_value.is_some().hash(hasher);
398 }
399 Expression::Cast(cast) => {
400 "CAST".hash(hasher);
401 cast.type_name.hash(hasher);
402 hash_expression_structure(&cast.expr, hasher);
403 }
404 Expression::ScalarSubquery(_) => {
405 "SUBQUERY".hash(hasher);
406 }
408 Expression::Exists(_) => {
409 "EXISTS".hash(hasher);
410 }
411 Expression::IntegerLiteral(literal) => {
414 "INTEGER_LITERAL".hash(hasher);
415 literal.value.hash(hasher);
416 }
417 Expression::FloatLiteral(literal) => {
418 "FLOAT_LITERAL".hash(hasher);
419 literal.value.to_bits().hash(hasher);
420 }
421 Expression::StringLiteral(literal) => {
422 "STRING_LITERAL".hash(hasher);
423 literal.value.hash(hasher);
424 }
425 Expression::BooleanLiteral(literal) => {
426 "BOOLEAN_LITERAL".hash(hasher);
427 literal.value.hash(hasher);
428 }
429 Expression::NullLiteral(_) => {
430 "NULL_LITERAL".hash(hasher);
431 }
432 Expression::List(list) => {
434 "LIST".hash(hasher);
435 list.elements.len().hash(hasher);
436 for element in &list.elements {
437 hash_expression_structure(element, hasher);
438 }
439 }
440 Expression::Star(_) => {
441 "STAR".hash(hasher);
442 }
443 _ => {
444 "OTHER".hash(hasher);
446 }
447 }
448}
449
450pub fn extract_column_from_predicate(expr: &Expression) -> Option<String> {
452 match expr {
453 Expression::Infix(infix) => {
454 if let Expression::Identifier(id) = &*infix.left {
456 return Some(id.value.to_string());
457 }
458 if let Expression::QualifiedIdentifier(qid) = &*infix.left {
459 return Some(qid.name.value.to_string());
460 }
461 if let Expression::Identifier(id) = &*infix.right {
463 return Some(id.value.to_string());
464 }
465 if let Expression::QualifiedIdentifier(qid) = &*infix.right {
466 return Some(qid.name.value.to_string());
467 }
468 None
469 }
470 Expression::Between(between) => {
471 if let Expression::Identifier(id) = &*between.expr {
472 return Some(id.value.to_string());
473 }
474 if let Expression::QualifiedIdentifier(qid) = &*between.expr {
475 return Some(qid.name.value.to_string());
476 }
477 None
478 }
479 Expression::In(in_expr) => {
480 if let Expression::Identifier(id) = &*in_expr.left {
481 return Some(id.value.to_string());
482 }
483 if let Expression::QualifiedIdentifier(qid) = &*in_expr.left {
484 return Some(qid.name.value.to_string());
485 }
486 None
487 }
488 Expression::Like(like) => {
489 if let Expression::Identifier(id) = &*like.left {
490 return Some(id.value.to_string());
491 }
492 if let Expression::QualifiedIdentifier(qid) = &*like.left {
493 return Some(qid.name.value.to_string());
494 }
495 None
496 }
497 _ => None,
500 }
501}
502
503fn get_current_timestamp() -> i64 {
505 system_time_now()
506 .duration_since(UNIX_EPOCH)
507 .map(|d| d.as_nanos() as i64)
508 .unwrap_or(0)
509}
510
511static FEEDBACK_CACHE: std::sync::OnceLock<FeedbackCache> = std::sync::OnceLock::new();
513
514pub fn global_feedback_cache() -> &'static FeedbackCache {
516 FEEDBACK_CACHE.get_or_init(FeedbackCache::new)
517}
518
519#[cfg(test)]
520mod tests {
521 use super::*;
522 use radixdb_sql::ast::{InfixExpression, InfixOperator};
523 use radixdb_sql::{Identifier, IntegerLiteral, Position, Token, TokenType};
524
525 fn make_token(literal: &str) -> Token {
526 Token::new(TokenType::Identifier, literal, Position::new(0, 1, 1))
527 }
528
529 fn make_identifier(name: &str) -> Expression {
530 Expression::Identifier(Identifier::new(make_token(name), name.to_string()))
531 }
532
533 fn make_literal_int(val: i64) -> Expression {
534 Expression::IntegerLiteral(IntegerLiteral {
535 token: Token::new(TokenType::Integer, val.to_string(), Position::new(0, 1, 1)),
536 value: val,
537 })
538 }
539
540 fn make_equality(col: &str, val: i64) -> Expression {
541 Expression::Infix(InfixExpression {
542 token: Token::new(TokenType::Operator, "=", Position::new(0, 1, 1)),
543 left: Box::new(make_identifier(col)),
544 operator: "=".into(),
545 op_type: InfixOperator::Equal,
546 right: Box::new(make_literal_int(val)),
547 })
548 }
549
550 #[test]
551 fn test_feedback_entry_creation() {
552 let fb = CardinalityFeedback::new(12345, "users", None, 100, 1000);
553 assert_eq!(fb.correction_factor, 10.0);
554 assert_eq!(fb.sample_count, 1);
555 assert!(!fb.is_reliable()); }
557
558 #[test]
559 fn test_feedback_update_ema() {
560 let mut fb = CardinalityFeedback::new(12345, "users", None, 100, 1000);
561 fb.update(100, 100, DEFAULT_DECAY_FACTOR);
565 assert!((fb.correction_factor - 7.3).abs() < 0.001);
567 assert_eq!(fb.sample_count, 2);
568 assert!(fb.is_reliable());
569 }
570
571 #[test]
572 fn test_feedback_cache() {
573 let cache = FeedbackCache::new();
574
575 cache.record_feedback("users", 12345, Some("status".to_string()), 100, 1000);
577
578 assert_eq!(cache.get_correction("users", 12345), 1.0);
580
581 cache.record_feedback("users", 12345, Some("status".to_string()), 100, 1000);
583
584 let correction = cache.get_correction("users", 12345);
586 assert!(correction > 1.0);
587 }
588
589 #[test]
590 fn test_fingerprint_includes_literal_distribution() {
591 let pred1 = make_equality("status", 1);
593 let pred2 = make_equality("status", 2);
594
595 let hash1 = fingerprint_predicate("users", &pred1);
596 let hash2 = fingerprint_predicate("users", &pred2);
597
598 assert_ne!(hash1, hash2);
599 }
600
601 #[test]
602 fn test_fingerprint_different_columns() {
603 let pred1 = make_equality("status", 1);
605 let pred2 = make_equality("role", 1);
606
607 let hash1 = fingerprint_predicate("users", &pred1);
608 let hash2 = fingerprint_predicate("users", &pred2);
609
610 assert_ne!(hash1, hash2);
611 }
612
613 #[test]
614 fn test_fingerprint_different_tables() {
615 let pred = make_equality("status", 1);
617
618 let hash1 = fingerprint_predicate("users", &pred);
619 let hash2 = fingerprint_predicate("orders", &pred);
620
621 assert_ne!(hash1, hash2);
622 }
623
624 #[test]
625 fn test_extract_column() {
626 let pred = make_equality("status", 1);
627 let col = extract_column_from_predicate(&pred);
628 assert_eq!(col, Some("status".to_string()));
629 }
630
631 #[test]
632 fn test_apply_correction() {
633 let cache = FeedbackCache::new();
634
635 cache.record_feedback("users", 12345, None, 100, 500);
637 cache.record_feedback("users", 12345, None, 100, 500);
638
639 let corrected = cache.apply_correction("users", 12345, 200);
641
642 assert!(corrected > 200);
644 }
645
646 #[test]
647 fn v2_r5_small_capacity_and_invalid_decay_fail_safe() {
648 let disabled = FeedbackCache::with_settings(0.3, 0);
649 disabled.record_feedback("t", 1, None, 1, 10);
650 assert_eq!(disabled.len(), 0);
651
652 let one = FeedbackCache::with_settings(0.3, 1);
653 one.record_feedback("t", 1, None, 1, 10);
654 one.record_feedback("t", 2, None, 1, 10);
655 assert_eq!(one.len(), 1);
656
657 for invalid in [f64::NAN, f64::INFINITY, -1.0, 2.0] {
658 let cache = FeedbackCache::with_settings(invalid, 4);
659 cache.record_feedback("t", 1, None, 10, 100);
660 cache.record_feedback("t", 1, None, 10, 100);
661 let corrected = cache.apply_correction("t", 1, 10);
662 assert!((1..=1000).contains(&corrected));
663 }
664
665 let mut poisoned = CardinalityFeedback::new(1, "t", None, 10, 10);
666 poisoned.sample_count = MIN_SAMPLE_COUNT;
667 poisoned.correction_factor = f64::NAN;
668 assert_eq!(poisoned.apply_correction(77), 77);
669 }
670}