Skip to main content

nodedb_columnar/
predicate.rs

1//! Scan predicates for block-level predicate pushdown.
2//!
3//! A `ScanPredicate` describes a filter on a single column that can be
4//! evaluated against `BlockStats` to skip entire blocks without decompressing.
5
6use crate::format::BlockStats;
7
8/// A predicate on a single column for block-level pushdown.
9#[derive(Debug, Clone)]
10pub struct ScanPredicate {
11    /// Column index in the schema.
12    pub col_idx: usize,
13    /// The comparison operation.
14    pub op: PredicateOp,
15    /// The threshold value (as f64 for uniform comparison against BlockStats).
16    pub value: f64,
17}
18
19/// Comparison operator for scan predicates.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum PredicateOp {
22    /// `column > value`
23    Gt,
24    /// `column >= value`
25    Gte,
26    /// `column < value`
27    Lt,
28    /// `column <= value`
29    Lte,
30    /// `column = value`
31    Eq,
32    /// `column != value`
33    Ne,
34}
35
36impl ScanPredicate {
37    /// Create a predicate: column > value.
38    pub fn gt(col_idx: usize, value: f64) -> Self {
39        Self {
40            col_idx,
41            op: PredicateOp::Gt,
42            value,
43        }
44    }
45
46    /// Create a predicate: column >= value.
47    pub fn gte(col_idx: usize, value: f64) -> Self {
48        Self {
49            col_idx,
50            op: PredicateOp::Gte,
51            value,
52        }
53    }
54
55    /// Create a predicate: column < value.
56    pub fn lt(col_idx: usize, value: f64) -> Self {
57        Self {
58            col_idx,
59            op: PredicateOp::Lt,
60            value,
61        }
62    }
63
64    /// Create a predicate: column <= value.
65    pub fn lte(col_idx: usize, value: f64) -> Self {
66        Self {
67            col_idx,
68            op: PredicateOp::Lte,
69            value,
70        }
71    }
72
73    /// Create a predicate: column = value.
74    pub fn eq(col_idx: usize, value: f64) -> Self {
75        Self {
76            col_idx,
77            op: PredicateOp::Eq,
78            value,
79        }
80    }
81
82    /// Create a predicate: column != value.
83    /// Named `not_eq` to avoid conflict with `PartialEq::ne`.
84    pub fn not_eq(col_idx: usize, value: f64) -> Self {
85        Self {
86            col_idx,
87            op: PredicateOp::Ne,
88            value,
89        }
90    }
91
92    /// Whether a block can be entirely skipped based on its statistics.
93    ///
94    /// Returns `true` if the block provably contains no matching rows.
95    /// Returns `false` if the block might contain matching rows (must scan).
96    pub fn can_skip_block(&self, stats: &BlockStats) -> bool {
97        // Non-numeric columns (NaN stats) can never be skipped.
98        if stats.min.is_nan() || stats.max.is_nan() {
99            return false;
100        }
101
102        match self.op {
103            // column > value → skip if block.max <= value
104            PredicateOp::Gt => stats.max <= self.value,
105            // column >= value → skip if block.max < value
106            PredicateOp::Gte => stats.max < self.value,
107            // column < value → skip if block.min >= value
108            PredicateOp::Lt => stats.min >= self.value,
109            // column <= value → skip if block.min > value
110            PredicateOp::Lte => stats.min > self.value,
111            // column = value → skip if value outside [min, max]
112            PredicateOp::Eq => self.value < stats.min || self.value > stats.max,
113            // column != value → skip only if entire block is that single value
114            PredicateOp::Ne => stats.min == self.value && stats.max == self.value,
115        }
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    fn stats(min: f64, max: f64) -> BlockStats {
124        BlockStats {
125            min,
126            max,
127            null_count: 0,
128            row_count: 1024,
129        }
130    }
131
132    #[test]
133    fn gt_predicate() {
134        let pred = ScanPredicate::gt(0, 50.0);
135        // Block [10, 40] → max=40 ≤ 50 → skip.
136        assert!(pred.can_skip_block(&stats(10.0, 40.0)));
137        // Block [10, 60] → max=60 > 50 → scan.
138        assert!(!pred.can_skip_block(&stats(10.0, 60.0)));
139        // Block [10, 50] → max=50 ≤ 50 → skip (strict >).
140        assert!(pred.can_skip_block(&stats(10.0, 50.0)));
141    }
142
143    #[test]
144    fn gte_predicate() {
145        let pred = ScanPredicate::gte(0, 50.0);
146        // Block [10, 49] → max=49 < 50 → skip.
147        assert!(pred.can_skip_block(&stats(10.0, 49.0)));
148        // Block [10, 50] → max=50 ≥ 50 → scan.
149        assert!(!pred.can_skip_block(&stats(10.0, 50.0)));
150    }
151
152    #[test]
153    fn lt_predicate() {
154        let pred = ScanPredicate::lt(0, 50.0);
155        // Block [60, 100] → min=60 ≥ 50 → skip.
156        assert!(pred.can_skip_block(&stats(60.0, 100.0)));
157        // Block [40, 100] → min=40 < 50 → scan.
158        assert!(!pred.can_skip_block(&stats(40.0, 100.0)));
159    }
160
161    #[test]
162    fn eq_predicate() {
163        let pred = ScanPredicate::eq(0, 50.0);
164        // Block [10, 40] → 50 > max → skip.
165        assert!(pred.can_skip_block(&stats(10.0, 40.0)));
166        // Block [60, 100] → 50 < min → skip.
167        assert!(pred.can_skip_block(&stats(60.0, 100.0)));
168        // Block [40, 60] → 50 in range → scan.
169        assert!(!pred.can_skip_block(&stats(40.0, 60.0)));
170    }
171
172    #[test]
173    fn ne_predicate() {
174        let pred = ScanPredicate::not_eq(0, 50.0);
175        // Block [50, 50] → entire block is 50 → skip.
176        assert!(pred.can_skip_block(&stats(50.0, 50.0)));
177        // Block [40, 60] → not all 50 → scan.
178        assert!(!pred.can_skip_block(&stats(40.0, 60.0)));
179    }
180
181    #[test]
182    fn non_numeric_never_skipped() {
183        let pred = ScanPredicate::gt(0, 50.0);
184        let nan_stats = BlockStats::non_numeric(0, 1024);
185        assert!(!pred.can_skip_block(&nan_stats));
186    }
187}