Skip to main content

linkrs_query/optimizer/stats/feedback/
selectivity.rs

1//! Selective Correction Module
2//!
3//! Provide the functionality to dynamically adjust selective estimates based on historical performance feedback.
4//! Uses an exponentially weighted moving average (EWMA) algorithm to correct selectivity estimates.
5
6use parking_lot::RwLock;
7use std::collections::HashMap;
8
9/// Feedback-driven selective correction
10///
11/// Dynamically adjust the selective estimates based on historical feedback from executions.
12/// Uses an exponentially weighted moving average (EWMA) algorithm.
13///
14/// # Example
15/// ```
16/// use linkrs_query::optimizer::stats::feedback::selectivity::FeedbackDrivenSelectivity;
17///
18/// let mut feedback = FeedbackDrivenSelectivity::new(0.1);
19/// assert_eq!(feedback.estimated_selectivity(), 0.1);
20///
21/// // Update the feedback
22/// feedback.update_with_feedback(0.15);
23/// feedback.update_with_feedback(0.12);
24///
25/// // Obtain the corrected selective information
26/// let corrected = feedback.corrected_selectivity();
27/// assert!(corrected > 0.0 && corrected <= 0.99);
28/// ```
29#[derive(Debug, Clone)]
30pub struct FeedbackDrivenSelectivity {
31    /// Original estimate of selectivity
32    estimated_selectivity: f64,
33    /// Historical Selectivity (Sliding Window Average)
34    actual_selectivity_ewma: f64,
35    /// Correction factor
36    correction_factor: f64,
37    /// Number of feedbacks
38    feedback_count: u64,
39    /// EWMA smoothing factor
40    alpha: f64,
41    /// Minimum correction factor
42    min_correction: f64,
43    /// Maximum correction factor
44    max_correction: f64,
45    /// Selective upper limit (default 0.99)
46    selectivity_cap: f64,
47    /// Cumulative estimated error (used for calculating error statistics)
48    cumulative_estimation_error: f64,
49    /// Sum of the squared errors (used to calculate the standard deviation)
50    error_sum_squares: f64,
51    /// Total measured input rows across applied samples. Small samples
52    /// move the factor conservatively (see [`Self::update_with_measured_ratio`]).
53    total_input_rows: u64,
54    /// One staged outlier sample awaiting corroboration by an independent
55    /// execution before it may move the factor.
56    pending_outlier: Option<MeasuredSample>,
57}
58
59/// One measured execution sample: observed local-parameter ratio plus the
60/// input rows it was measured over.
61#[derive(Debug, Clone, Copy)]
62struct MeasuredSample {
63    ratio: f64,
64    input_rows: u64,
65}
66
67/// Samples deviating from the learned factor by more than this are staged
68/// instead of applied: a lone distorted measurement (truncated run,
69/// misattributed upstream error) must not yank the correction.
70const OUTLIER_DEVIATION: f64 = 4.0;
71/// A staged sample is adopted once an independent execution agrees with it
72/// within this factor.
73const CORROBORATION_AGREEMENT: f64 = 2.0;
74/// Input rows at which a sample earns the full EWMA step; smaller samples
75/// move the factor proportionally less. Keeps tiny measurements from
76/// flipping corrections while letting table-scale runs learn normally.
77const FULL_WEIGHT_INPUT_ROWS: f64 = 1000.0;
78
79impl FeedbackDrivenSelectivity {
80    /// Create a new feedback-driven, selective estimation method
81    pub fn new(estimated_selectivity: f64) -> Self {
82        Self {
83            estimated_selectivity,
84            actual_selectivity_ewma: estimated_selectivity,
85            correction_factor: 1.0,
86            feedback_count: 0,
87            alpha: 0.3,
88            min_correction: 0.1,
89            max_correction: 10.0,
90            selectivity_cap: 0.99,
91            cumulative_estimation_error: 0.0,
92            error_sum_squares: 0.0,
93            total_input_rows: 0,
94            pending_outlier: None,
95        }
96    }
97
98    /// Create using custom parameters
99    pub fn with_params(
100        estimated_selectivity: f64,
101        alpha: f64,
102        min_correction: f64,
103        max_correction: f64,
104    ) -> Self {
105        Self {
106            estimated_selectivity,
107            actual_selectivity_ewma: estimated_selectivity,
108            correction_factor: 1.0,
109            feedback_count: 0,
110            alpha,
111            min_correction,
112            max_correction,
113            selectivity_cap: 0.99,
114            cumulative_estimation_error: 0.0,
115            error_sum_squares: 0.0,
116            total_input_rows: 0,
117            pending_outlier: None,
118        }
119    }
120
121    /// Obtain the original estimate for selectivity.
122    pub fn estimated_selectivity(&self) -> f64 {
123        self.estimated_selectivity
124    }
125
126    /// Track a fresh uncorrected estimate while preserving the learned
127    /// correction factor and feedback history.
128    pub fn set_estimated_selectivity(&mut self, estimated: f64) {
129        if estimated > 0.0 && estimated.is_finite() {
130            self.estimated_selectivity = estimated;
131            self.actual_selectivity_ewma = estimated * self.correction_factor;
132        }
133    }
134
135    /// Obtain the corrected version of the selective content.
136    pub fn corrected_selectivity(&self) -> f64 {
137        self.corrected_for_estimate(self.estimated_selectivity)
138    }
139
140    /// Correct a fresh estimate with the learned factor without touching the
141    /// stored baseline, so snapshot reads stay free of planning-order drift.
142    pub fn corrected_for_estimate(&self, estimated: f64) -> f64 {
143        (estimated * self.correction_factor)
144            .clamp(self.min_correction * estimated, self.selectivity_cap)
145    }
146
147    /// Obtain the correction factor
148    pub fn correction_factor(&self) -> f64 {
149        self.correction_factor
150    }
151
152    /// Number of times feedback was obtained
153    pub fn feedback_count(&self) -> u64 {
154        self.feedback_count
155    }
156
157    /// Total measured input rows across applied samples.
158    pub fn total_input_rows(&self) -> u64 {
159        self.total_input_rows
160    }
161
162    /// Obtaining the confidence level for the selected estimate
163    ///
164    /// The confidence level is calculated based on the number of feedbacks received; the more feedback, the higher the confidence level.
165    /// Return value range: 0.0 - 1.0
166    pub fn estimation_confidence(&self) -> f64 {
167        // Calculate the confidence using the sigmoid function.
168        // The confidence level is close to 0.9 when the number of feedbacks reaches 100
169        let x = self.feedback_count as f64 * 0.1;
170        1.0 / (1.0 + (-x).exp())
171    }
172
173    /// Obtain the average estimated error.
174    pub fn avg_estimation_error(&self) -> f64 {
175        if self.feedback_count == 0 {
176            return 1.0;
177        }
178        self.cumulative_estimation_error / self.feedback_count as f64
179    }
180
181    /// Obtain the standard deviation of the estimated error
182    pub fn error_std_dev(&self) -> f64 {
183        if self.feedback_count < 2 {
184            return 0.0;
185        }
186        let n = self.feedback_count as f64;
187        let mean = self.cumulative_estimation_error / n;
188        let variance = (self.error_sum_squares / n) - (mean * mean);
189        variance.max(0.0).sqrt()
190    }
191
192    /// Update the correction factor (based on the new feedback).
193    ///
194    /// Uses an exponentially weighted moving average (EWMA) algorithm.
195    pub fn update_with_feedback(&mut self, actual_selectivity: f64) {
196        if self.estimated_selectivity <= 0.0 {
197            return;
198        }
199
200        let ratio = actual_selectivity / self.estimated_selectivity;
201
202        // Calculate the current estimated error.
203        let estimated = self.corrected_selectivity();
204        let error = (actual_selectivity - estimated).abs();
205        self.cumulative_estimation_error += error;
206        self.error_sum_squares += error * error;
207
208        // Use the EWMA to update the correction factor.
209        self.correction_factor = (1.0 - self.alpha) * self.correction_factor + self.alpha * ratio;
210
211        // Limit the range of the correction factors to avoid excessive correction.
212        self.correction_factor = self
213            .correction_factor
214            .clamp(self.min_correction, self.max_correction);
215
216        // Updated version of the practical selective EWMA (Exponential Moving Average)
217        self.actual_selectivity_ewma =
218            (1.0 - self.alpha) * self.actual_selectivity_ewma + self.alpha * actual_selectivity;
219
220        self.feedback_count += 1;
221    }
222
223    /// Update the correction factor from an observed estimated-vs-actual row
224    /// ratio (actual_rows / estimated_rows).
225    ///
226    /// Unlike [`update_with_feedback`], which receives an absolute
227    /// selectivity, this receives the correction ratio directly so the
228    /// feedback loop does not need to know the operator's input cardinality.
229    pub fn update_with_ratio(&mut self, ratio: f64) {
230        if self.estimated_selectivity <= 0.0 || !ratio.is_finite() {
231            return;
232        }
233
234        let error = (ratio - self.correction_factor).abs();
235        self.cumulative_estimation_error += error;
236        self.error_sum_squares += error * error;
237
238        // Use the EWMA to update the correction factor.
239        self.correction_factor = (1.0 - self.alpha) * self.correction_factor + self.alpha * ratio;
240
241        // Limit the range of the correction factors to avoid excessive correction.
242        self.correction_factor = self
243            .correction_factor
244            .clamp(self.min_correction, self.max_correction);
245
246        // Keep the smoothed absolute selectivity consistent with the factor.
247        self.actual_selectivity_ewma = self.estimated_selectivity * self.correction_factor;
248
249        self.feedback_count += 1;
250    }
251
252    /// Update the correction factor from a measured local-parameter ratio
253    /// over a known number of input rows.
254    ///
255    /// This is the feedback loop's write path. Unlike [`update_with_ratio`],
256    /// the ratio must already isolate the operator-local parameter
257    /// (e.g. observed-vs-estimated *selectivity*, not output rows), because
258    /// output ratios conflate upstream cardinality error. Two guards keep
259    /// distorted measurements out of the factor:
260    ///
261    /// - samples deviating from the learned factor by more than
262    ///   [`OUTLIER_DEVIATION`] are staged until an independent execution
263    ///   corroborates them (the first sample has no baseline and applies);
264    /// - the EWMA step scales with measured input rows, so tiny samples
265    ///   move the factor conservatively.
266    pub fn update_with_measured_ratio(&mut self, ratio: f64, input_rows: u64) {
267        if self.estimated_selectivity <= 0.0 || !ratio.is_finite() || ratio < 0.0 {
268            return;
269        }
270        let sample = MeasuredSample { ratio, input_rows };
271        if self.feedback_count > 0 && self.deviates(sample.ratio) {
272            if let Some(staged) = self.pending_outlier {
273                if agrees(staged.ratio, sample.ratio) {
274                    self.pending_outlier = None;
275                    self.apply_measured(staged);
276                    self.apply_measured(sample);
277                } else {
278                    self.pending_outlier = Some(sample);
279                }
280            } else {
281                self.pending_outlier = Some(sample);
282            }
283            return;
284        }
285        self.apply_measured(sample);
286    }
287
288    /// Relative deviation of a sample from the learned factor.
289    fn deviates(&self, ratio: f64) -> bool {
290        let factor = self.correction_factor;
291        if factor <= 0.0 || ratio <= 0.0 {
292            return ratio != factor;
293        }
294        ratio.max(factor) / ratio.min(factor) > OUTLIER_DEVIATION
295    }
296
297    /// Fold one corroborated sample into the factor with a step
298    /// proportional to its measured input rows.
299    fn apply_measured(&mut self, sample: MeasuredSample) {
300        let weight = (sample.input_rows as f64).max(1.0);
301        let step = self.alpha * weight / (weight + FULL_WEIGHT_INPUT_ROWS);
302        let error = (sample.ratio - self.correction_factor).abs();
303        self.cumulative_estimation_error += error;
304        self.error_sum_squares += error * error;
305        self.correction_factor = (1.0 - step) * self.correction_factor + step * sample.ratio;
306        self.correction_factor = self
307            .correction_factor
308            .clamp(self.min_correction, self.max_correction);
309        self.actual_selectivity_ewma = self.estimated_selectivity * self.correction_factor;
310        self.total_input_rows += sample.input_rows;
311        self.feedback_count += 1;
312    }
313
314    /// Batch update of feedback
315    pub fn update_with_batch(&mut self, actual_selectivities: &[f64]) {
316        for &selectivity in actual_selectivities {
317            self.update_with_feedback(selectivity);
318        }
319    }
320
321    /// Reset the correction factor
322    pub fn reset_correction(&mut self) {
323        self.correction_factor = 1.0;
324        self.actual_selectivity_ewma = self.estimated_selectivity;
325        self.feedback_count = 0;
326        self.cumulative_estimation_error = 0.0;
327        self.error_sum_squares = 0.0;
328        self.total_input_rows = 0;
329        self.pending_outlier = None;
330    }
331
332    /// Setting the EWMA smoothing factor
333    pub fn set_alpha(&mut self, alpha: f64) {
334        self.alpha = alpha.clamp(0.0, 1.0);
335    }
336
337    /// Set the correction range
338    pub fn set_correction_range(&mut self, min: f64, max: f64) {
339        self.min_correction = min.max(0.01);
340        self.max_correction = max.max(self.min_correction);
341    }
342
343    /// Setting a selective upper limit
344    pub fn set_selectivity_cap(&mut self, cap: f64) {
345        self.selectivity_cap = cap.clamp(0.5, 1.0);
346    }
347}
348
349impl Default for FeedbackDrivenSelectivity {
350    fn default() -> Self {
351        Self::new(0.1)
352    }
353}
354
355/// Selective Feedback Manager
356///
357/// Managing selective feedback based on multiple conditions.
358///
359/// # Examples
360/// ```
361/// use linkrs_query::optimizer::stats::feedback::selectivity::SelectivityFeedbackManager;
362///
363/// let manager = SelectivityFeedbackManager::new();
364/// manager.register_condition("age > 25".to_string(), 0.3);
365///
366/// let corrected = manager.get_corrected_selectivity("age > 25");
367/// assert!(corrected.is_some());
368/// ```
369#[derive(Debug)]
370pub struct SelectivityFeedbackManager {
371    /// Mapping from condition keys to selective correction
372    feedbacks: RwLock<HashMap<String, FeedbackDrivenSelectivity>>,
373    /// Default EWMA smoothing factor
374    default_alpha: f64,
375    /// Default minimum correction factor
376    default_min_correction: f64,
377    /// Default maximum correction factor
378    default_max_correction: f64,
379}
380
381impl SelectivityFeedbackManager {
382    /// Create a new feedback manager.
383    pub fn new() -> Self {
384        Self {
385            feedbacks: RwLock::new(HashMap::new()),
386            default_alpha: 0.3,
387            default_min_correction: 0.1,
388            default_max_correction: 10.0,
389        }
390    }
391
392    /// Created using custom parameters
393    pub fn with_params(alpha: f64, min_correction: f64, max_correction: f64) -> Self {
394        Self {
395            feedbacks: RwLock::new(HashMap::new()),
396            default_alpha: alpha,
397            default_min_correction: min_correction,
398            default_max_correction: max_correction,
399        }
400    }
401
402    /// Selective estimation of registration requirements
403    ///
404    /// Registers the condition only when the key is not present yet, so
405    /// repeated estimation of the same predicate does not reset previously
406    /// learned corrections.
407    pub fn register_condition(&self, key: String, estimated_selectivity: f64) {
408        let feedback = FeedbackDrivenSelectivity::with_params(
409            estimated_selectivity,
410            self.default_alpha,
411            self.default_min_correction,
412            self.default_max_correction,
413        );
414        self.feedbacks.write().entry(key).or_insert(feedback);
415    }
416
417    /// Update the baseline estimate of a registered condition while
418    /// preserving its learned correction factor, so baselines track fresh
419    /// statistics instead of freezing at first sight. Returns false when
420    /// the key is unknown.
421    pub fn refresh_estimated(&self, key: &str, estimated_selectivity: f64) -> bool {
422        let mut feedbacks = self.feedbacks.write();
423        if let Some(feedback) = feedbacks.get_mut(key) {
424            feedback.set_estimated_selectivity(estimated_selectivity);
425            true
426        } else {
427            false
428        }
429    }
430
431    /// Getting corrected selectivity
432    pub fn get_corrected_selectivity(&self, key: &str) -> Option<f64> {
433        self.feedbacks
434            .read()
435            .get(key)
436            .map(|f| f.corrected_selectivity())
437    }
438
439    /// Capture an immutable snapshot of the current learned corrections for
440    /// one planning decision to read through without mutating shared state.
441    pub fn snapshot(&self) -> SelectivitySnapshot {
442        SelectivitySnapshot {
443            corrections: self.feedbacks.read().clone(),
444        }
445    }
446
447    /// Update feedback
448    pub fn update_feedback(&self, key: &str, actual_selectivity: f64) -> bool {
449        let mut feedbacks = self.feedbacks.write();
450        if let Some(feedback) = feedbacks.get_mut(key) {
451            feedback.update_with_feedback(actual_selectivity);
452            true
453        } else {
454            false
455        }
456    }
457
458    /// Update feedback from an estimated-vs-actual row ratio.
459    ///
460    /// The ratio is the correction factor `actual_rows / estimated_rows`;
461    /// returns `false` when the key has not been registered.
462    pub fn update_feedback_ratio(&self, key: &str, ratio: f64) -> bool {
463        let mut feedbacks = self.feedbacks.write();
464        if let Some(feedback) = feedbacks.get_mut(key) {
465            feedback.update_with_ratio(ratio);
466            true
467        } else {
468            false
469        }
470    }
471
472    /// Update feedback from a measured local-parameter ratio over a known
473    /// number of input rows (the feedback loop's write path).
474    ///
475    /// Returns `false` when the key has not been registered; staged
476    /// outliers still return `true` (the sample was recorded, just not
477    /// applied yet).
478    pub fn update_measured_ratio(&self, key: &str, ratio: f64, input_rows: u64) -> bool {
479        let mut feedbacks = self.feedbacks.write();
480        if let Some(feedback) = feedbacks.get_mut(key) {
481            feedback.update_with_measured_ratio(ratio, input_rows);
482            true
483        } else {
484            false
485        }
486    }
487
488    /// Remove all feedback whose key starts with `space_prefix` (e.g.
489    /// `"myspace:"`), returning the number of removed entries.
490    pub fn remove_feedback_by_space(&self, space_prefix: &str) -> usize {
491        let mut feedbacks = self.feedbacks.write();
492        let mut removed = 0;
493        feedbacks.retain(|key, _| {
494            let keep = !key.starts_with(space_prefix);
495            if !keep {
496                removed += 1;
497            }
498            keep
499        });
500        removed
501    }
502
503    /// Batch Update Feedback
504    pub fn update_feedback_batch(&self, updates: &[(String, f64)]) {
505        let mut feedbacks = self.feedbacks.write();
506        for (key, actual_selectivity) in updates {
507            if let Some(feedback) = feedbacks.get_mut(key) {
508                feedback.update_with_feedback(*actual_selectivity);
509            }
510        }
511    }
512
513    /// Obtain feedback information
514    pub fn get_feedback(&self, key: &str) -> Option<FeedbackDrivenSelectivity> {
515        self.feedbacks.read().get(key).cloned()
516    }
517
518    /// Obtain all feedback keys.
519    pub fn get_all_keys(&self) -> Vec<String> {
520        self.feedbacks.read().keys().cloned().collect()
521    }
522
523    /// Clear all feedback.
524    pub fn clear_all(&self) {
525        self.feedbacks.write().clear();
526    }
527
528    /// Remove feedback that meets specific criteria.
529    pub fn remove_feedback(&self, key: &str) -> Option<FeedbackDrivenSelectivity> {
530        self.feedbacks.write().remove(key)
531    }
532
533    /// Number of feedback requests received
534    pub fn feedback_count(&self) -> usize {
535        self.feedbacks.read().len()
536    }
537
538    /// Set default parameters
539    pub fn set_default_params(&mut self, alpha: f64, min_correction: f64, max_correction: f64) {
540        self.default_alpha = alpha.clamp(0.0, 1.0);
541        self.default_min_correction = min_correction.max(0.01);
542        self.default_max_correction = max_correction.max(self.default_min_correction);
543    }
544}
545
546impl Default for SelectivityFeedbackManager {
547    fn default() -> Self {
548        Self::new()
549    }
550}
551
552/// Whether two independent samples agree closely enough for the staged
553/// one to be adopted.
554fn agrees(first: f64, second: f64) -> bool {
555    if first <= 0.0 || second <= 0.0 {
556        return first == second;
557    }
558    first.max(second) / first.min(second) <= CORROBORATION_AGREEMENT
559}
560
561/// Minimum execution samples before a learned correction is served to
562/// planning decisions. A single sample (possibly distorted by per-operator
563/// cardinality mismatch) must not flip structural go/no-go gates; repeated
564/// consistent executions promote the correction to trusted.
565pub const MIN_STABLE_FEEDBACK_COUNT: u64 = 3;
566
567/// Immutable point-in-time copy of the learned selectivity corrections.
568///
569/// Planning reads through a snapshot taken at the start of one decision, so
570/// repeated estimation of the same predicate can neither register new state
571/// nor move baselines: a decision is a pure function of the statistics plus
572/// the snapshot. Execution feedback keeps writing to the live manager, which
573/// later snapshots pick up as legitimate learning.
574#[derive(Debug, Clone, Default)]
575pub struct SelectivitySnapshot {
576    corrections: HashMap<String, FeedbackDrivenSelectivity>,
577}
578
579impl SelectivitySnapshot {
580    /// Correct a fresh estimate with the snapshotted factor, if the
581    /// condition had learned a stable correction (enough execution samples)
582    /// when the snapshot was taken. Otherwise `None`, and the caller keeps
583    /// the fresh statistics estimate.
584    pub fn corrected(&self, key: &str, fresh_estimate: f64) -> Option<f64> {
585        self.corrections
586            .get(key)
587            .filter(|feedback| feedback.feedback_count() >= MIN_STABLE_FEEDBACK_COUNT)
588            .map(|feedback| feedback.corrected_for_estimate(fresh_estimate))
589    }
590
591    /// Number of corrected conditions captured by the snapshot.
592    pub fn len(&self) -> usize {
593        self.corrections.len()
594    }
595
596    /// Whether the snapshot carries no learned corrections.
597    pub fn is_empty(&self) -> bool {
598        self.corrections.is_empty()
599    }
600}
601
602impl Clone for SelectivityFeedbackManager {
603    fn clone(&self) -> Self {
604        Self {
605            feedbacks: RwLock::new(self.feedbacks.read().clone()),
606            default_alpha: self.default_alpha,
607            default_min_correction: self.default_min_correction,
608            default_max_correction: self.default_max_correction,
609        }
610    }
611}
612
613#[cfg(test)]
614mod tests {
615    use super::*;
616
617    #[test]
618    fn test_feedback_driven_selectivity() {
619        let mut feedback = FeedbackDrivenSelectivity::new(0.1);
620        assert_eq!(feedback.estimated_selectivity(), 0.1);
621
622        // Updated feedback
623        feedback.update_with_feedback(0.15);
624        feedback.update_with_feedback(0.12);
625
626        // The corrected selectivity should be within a reasonable range.
627        let corrected = feedback.corrected_selectivity();
628        assert!(corrected > 0.0 && corrected <= 0.99);
629
630        // The number of feedback instances should be 2.
631        assert_eq!(feedback.feedback_count(), 2);
632    }
633
634    #[test]
635    fn test_feedback_correction_range() {
636        let mut feedback = FeedbackDrivenSelectivity::new(0.5);
637
638        // Numerous updates; testing of the correction factor limitations
639        for _ in 0..100 {
640            feedback.update_with_feedback(0.01); // Far lower than the estimated values
641        }
642
643        // The correction factor should be limited to its minimum value.
644        assert!(feedback.correction_factor() >= 0.1);
645    }
646
647    #[test]
648    fn test_estimation_confidence() {
649        let mut feedback = FeedbackDrivenSelectivity::new(0.1);
650        // Initial confidence should be 0.5 (sigmoid(0) = 0.5)
651        let initial_confidence = feedback.estimation_confidence();
652        assert!(initial_confidence < 0.55 && initial_confidence > 0.45);
653
654        // Add multiple feedback entries.
655        for i in 0..100 {
656            feedback.update_with_feedback(0.1 + (i as f64 * 0.001));
657        }
658
659        assert!(feedback.estimation_confidence() > 0.9); // High confidence level after receiving the feedback
660    }
661
662    #[test]
663    fn test_avg_estimation_error() {
664        let mut feedback = FeedbackDrivenSelectivity::new(0.5);
665
666        // 1.0% error without feedback
667        assert_eq!(feedback.avg_estimation_error(), 1.0);
668
669        // Add feedback
670        feedback.update_with_feedback(0.5); // No errors.
671        feedback.update_with_feedback(0.6); // There are errors.
672
673        assert!(feedback.avg_estimation_error() < 1.0);
674    }
675
676    #[test]
677    fn test_selectivity_feedback_manager() {
678        let manager = SelectivityFeedbackManager::new();
679        manager.register_condition("age > 25".to_string(), 0.3);
680        manager.register_condition("salary > 5000".to_string(), 0.2);
681
682        assert_eq!(manager.feedback_count(), 2);
683
684        // Updated feedback
685        manager.update_feedback("age > 25", 0.35);
686        manager.update_feedback("salary > 5000", 0.18);
687
688        // Obtain the corrected version of the selective content.
689        let corrected_age = manager.get_corrected_selectivity("age > 25");
690        assert!(corrected_age.is_some());
691
692        // Trying to obtain a condition that does not exist…
693        assert!(manager.get_corrected_selectivity("unknown").is_none());
694    }
695
696    #[test]
697    fn test_selectivity_cap() {
698        let mut feedback = FeedbackDrivenSelectivity::new(0.9);
699        feedback.set_selectivity_cap(0.95);
700
701        // Numerous updates have been made in an attempt to increase the level of selectivity beyond the maximum limit.
702        for _ in 0..50 {
703            feedback.update_with_feedback(1.0);
704        }
705
706        // The corrected selectivity should not exceed the upper limit.
707        assert!(feedback.corrected_selectivity() <= 0.95);
708    }
709
710    #[test]
711    fn test_measured_ratio_stages_outlier_until_corroborated() {
712        let mut feedback = FeedbackDrivenSelectivity::new(0.5);
713        // Establish a baseline near 1.0 with consistent samples.
714        for _ in 0..3 {
715            feedback.update_with_measured_ratio(1.0, 2000);
716        }
717        let baseline = feedback.correction_factor();
718        assert!((baseline - 1.0).abs() < 1e-9);
719        assert_eq!(feedback.feedback_count(), 3);
720
721        // A lone distorted sample (e.g. a truncated partition slice)
722        // must not move the factor by itself.
723        feedback.update_with_measured_ratio(0.0, 2000);
724        assert!((feedback.correction_factor() - baseline).abs() < 1e-9);
725        assert_eq!(feedback.feedback_count(), 3);
726
727        // An independent execution agreeing with the staged sample
728        // adopts both: the factor moves and both samples count.
729        feedback.update_with_measured_ratio(0.0, 2000);
730        assert!(feedback.correction_factor() < baseline);
731        assert_eq!(feedback.feedback_count(), 5);
732    }
733
734    #[test]
735    fn test_measured_ratio_weights_step_by_input_rows() {
736        let mut tiny = FeedbackDrivenSelectivity::new(0.5);
737        tiny.update_with_measured_ratio(0.1, 10);
738        let mut large = FeedbackDrivenSelectivity::new(0.5);
739        large.update_with_measured_ratio(0.1, 100_000);
740        // Same ratio, same single sample: the table-scale measurement
741        // moves the factor far more than the tiny one.
742        assert!(large.correction_factor() < tiny.correction_factor());
743        assert_eq!(tiny.total_input_rows(), 10);
744        assert_eq!(large.total_input_rows(), 100_000);
745    }
746
747    #[test]
748    fn test_measured_ratio_rejects_invalid_samples() {
749        let mut feedback = FeedbackDrivenSelectivity::new(0.5);
750        feedback.update_with_measured_ratio(f64::NAN, 100);
751        feedback.update_with_measured_ratio(-1.0, 100);
752        assert_eq!(feedback.feedback_count(), 0);
753        assert!((feedback.correction_factor() - 1.0).abs() < 1e-9);
754    }
755}