1use chronix_engine::index::{ZoneMapPredicate, ZoneMapResult};
10
11#[derive(Debug, Clone)]
14pub struct EncodedDomainEvaluator {
15 predicates: Vec<ColumnPredicate>,
16}
17
18#[derive(Debug, Clone)]
20pub struct ColumnPredicate {
21 pub column: String,
23 pub predicate: ZoneMapPredicate,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum RowGroupVerdict {
30 AllMatch,
33 Skip,
36 Decode,
39}
40
41impl EncodedDomainEvaluator {
42 #[must_use]
44 pub fn new(predicates: Vec<ColumnPredicate>) -> Self {
45 Self { predicates }
46 }
47
48 #[must_use]
67 pub fn evaluate<F>(&self, stats_fn: F) -> RowGroupVerdict
68 where
69 F: Fn(&str) -> Option<(f64, f64)>,
70 {
71 if self.predicates.is_empty() {
72 return RowGroupVerdict::Decode;
73 }
74
75 let mut all_match = true;
76
77 for pred in &self.predicates {
78 let Some((min, max)) = stats_fn(&pred.column) else {
79 all_match = false;
81 continue;
82 };
83
84 match pred.predicate.evaluate(min, max) {
85 ZoneMapResult::NoneMatch => return RowGroupVerdict::Skip,
86 ZoneMapResult::AllMatch => { }
87 ZoneMapResult::MaybeMatch => {
88 all_match = false;
89 }
90 }
91 }
92
93 if all_match {
94 RowGroupVerdict::AllMatch
95 } else {
96 RowGroupVerdict::Decode
97 }
98 }
99
100 #[must_use]
106 pub fn evaluate_batch<F>(&self, num_row_groups: usize, stats_fn: F) -> Vec<RowGroupVerdict>
107 where
108 F: Fn(usize, &str) -> Option<(f64, f64)>,
109 {
110 (0..num_row_groups)
111 .map(|rg| self.evaluate(|col| stats_fn(rg, col)))
112 .collect()
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use chronix_engine::index::ZoneMapOp;
120
121 fn make_evaluator(col: &str, op: ZoneMapOp, value: f64) -> EncodedDomainEvaluator {
122 EncodedDomainEvaluator::new(vec![ColumnPredicate {
123 column: col.to_string(),
124 predicate: ZoneMapPredicate { op, value },
125 }])
126 }
127
128 #[test]
129 fn skip_when_predicate_excludes() {
130 let eval = make_evaluator("value", ZoneMapOp::Gt, 100.0);
132 let verdict = eval.evaluate(|_col| Some((10.0, 50.0)));
133 assert_eq!(verdict, RowGroupVerdict::Skip);
134 }
135
136 #[test]
137 fn all_match_when_predicate_fully_satisfied() {
138 let eval = make_evaluator("value", ZoneMapOp::Gt, 5.0);
140 let verdict = eval.evaluate(|_col| Some((10.0, 100.0)));
141 assert_eq!(verdict, RowGroupVerdict::AllMatch);
142 }
143
144 #[test]
145 fn decode_when_predicate_partially_matches() {
146 let eval = make_evaluator("value", ZoneMapOp::Gt, 50.0);
148 let verdict = eval.evaluate(|_col| Some((10.0, 100.0)));
149 assert_eq!(verdict, RowGroupVerdict::Decode);
150 }
151
152 #[test]
153 fn decode_when_no_stats_available() {
154 let eval = make_evaluator("value", ZoneMapOp::Gt, 50.0);
155 let verdict = eval.evaluate(|_col| None);
156 assert_eq!(verdict, RowGroupVerdict::Decode);
157 }
158
159 #[test]
160 fn empty_predicates_returns_decode() {
161 let eval = EncodedDomainEvaluator::new(vec![]);
162 let verdict = eval.evaluate(|_col| Some((10.0, 100.0)));
163 assert_eq!(verdict, RowGroupVerdict::Decode);
164 }
165
166 #[test]
167 fn multiple_predicates_all_match() {
168 let eval = EncodedDomainEvaluator::new(vec![
170 ColumnPredicate {
171 column: "value".to_string(),
172 predicate: ZoneMapPredicate {
173 op: ZoneMapOp::Gt,
174 value: 5.0,
175 },
176 },
177 ColumnPredicate {
178 column: "value".to_string(),
179 predicate: ZoneMapPredicate {
180 op: ZoneMapOp::Lt,
181 value: 200.0,
182 },
183 },
184 ]);
185 let verdict = eval.evaluate(|_col| Some((10.0, 100.0)));
187 assert_eq!(verdict, RowGroupVerdict::AllMatch);
188 }
189
190 #[test]
191 fn multiple_predicates_one_excludes() {
192 let eval = EncodedDomainEvaluator::new(vec![
194 ColumnPredicate {
195 column: "value".to_string(),
196 predicate: ZoneMapPredicate {
197 op: ZoneMapOp::Gt,
198 value: 5.0,
199 },
200 },
201 ColumnPredicate {
202 column: "value".to_string(),
203 predicate: ZoneMapPredicate {
204 op: ZoneMapOp::Lt,
205 value: 8.0,
206 },
207 },
208 ]);
209 let verdict = eval.evaluate(|_col| Some((10.0, 100.0)));
211 assert_eq!(verdict, RowGroupVerdict::Skip);
212 }
213
214 #[test]
215 fn batch_evaluation() {
216 let eval = make_evaluator("value", ZoneMapOp::Gt, 50.0);
217 let stats = [
218 (10.0, 40.0), (60.0, 100.0), (30.0, 80.0), ];
222 let verdicts = eval.evaluate_batch(3, |rg, _col| Some(stats[rg]));
223 assert_eq!(verdicts[0], RowGroupVerdict::Skip);
224 assert_eq!(verdicts[1], RowGroupVerdict::AllMatch);
225 assert_eq!(verdicts[2], RowGroupVerdict::Decode);
226 }
227
228 #[test]
229 fn skip_entire_sorted_column() {
230 let eval = make_evaluator("value", ZoneMapOp::Eq, 75.0);
233 let rg_stats = [
234 (0.0, 20.0), (20.0, 40.0), (40.0, 60.0), (60.0, 80.0), (80.0, 100.0), ];
240 let verdicts = eval.evaluate_batch(5, |rg, _col| Some(rg_stats[rg]));
241 assert_eq!(
242 verdicts
243 .iter()
244 .filter(|v| **v == RowGroupVerdict::Skip)
245 .count(),
246 4
247 );
248 assert_eq!(verdicts[3], RowGroupVerdict::Decode);
249 }
250}