1use std::cmp::Ordering;
10
11use akar_common::types::Value;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum ZoneMapCheckResult {
16 AlwaysScan = 0,
18 SkipScan = 1,
20}
21
22#[derive(Debug, Clone)]
24pub struct ColumnChunkStats {
25 pub min: Option<Value>,
27 pub max: Option<Value>,
29 pub guaranteed_no_nulls: bool,
31 pub guaranteed_all_nulls: bool,
33}
34
35impl ColumnChunkStats {
36 pub fn new(min: Option<Value>, max: Option<Value>) -> Self {
37 Self {
38 min,
39 max,
40 guaranteed_no_nulls: true,
41 guaranteed_all_nulls: false,
42 }
43 }
44
45 pub fn all_nulls() -> Self {
47 Self {
48 min: None,
49 max: None,
50 guaranteed_no_nulls: false,
51 guaranteed_all_nulls: true,
52 }
53 }
54
55 pub fn update(&mut self, val: &Value) {
67 if matches!(val, Value::Null) {
68 self.guaranteed_no_nulls = false;
69 return;
70 }
71 self.guaranteed_all_nulls = false;
72 match (&self.min, &self.max) {
73 (None, None) => {
74 self.min = Some(val.clone());
75 self.max = Some(val.clone());
76 }
77 (Some(min), Some(max)) => {
78 if value_cmp(val, min) == Some(Ordering::Less) {
79 self.min = Some(val.clone());
80 }
81 if value_cmp(val, max) == Some(Ordering::Greater) {
82 self.max = Some(val.clone());
83 }
84 }
85 _ => unreachable!(),
86 }
87 }
88}
89
90fn value_cmp(a: &Value, b: &Value) -> Option<Ordering> {
95 let ord = match (a, b) {
96 (Value::Bool(x), Value::Bool(y)) => x.cmp(y),
97 (Value::Int64(x), Value::Int64(y)) => x.cmp(y),
98 (Value::Int32(x), Value::Int32(y)) => x.cmp(y),
99 (Value::Int16(x), Value::Int16(y)) => x.cmp(y),
100 (Value::Int8(x), Value::Int8(y)) => x.cmp(y),
101 (Value::UInt64(x), Value::UInt64(y)) => x.cmp(y),
102 (Value::UInt32(x), Value::UInt32(y)) => x.cmp(y),
103 (Value::UInt16(x), Value::UInt16(y)) => x.cmp(y),
104 (Value::UInt8(x), Value::UInt8(y)) => x.cmp(y),
105 (Value::Int128(x), Value::Int128(y)) => x.cmp(y),
106 (Value::UInt128(x), Value::UInt128(y)) => x.cmp(y),
107 (Value::Double(x), Value::Double(y)) => x.partial_cmp(y)?,
108 (Value::Float(x), Value::Float(y)) => x.partial_cmp(y)?,
109 (Value::String(x), Value::String(y)) => x.cmp(y),
110 (Value::Date(x), Value::Date(y)) => x.cmp(y),
111 (Value::Timestamp(x), Value::Timestamp(y)) => x.cmp(y),
112 (Value::TimestampTz(x), Value::TimestampTz(y)) => x.0.cmp(&y.0),
113 (Value::TimestampNs(x), Value::TimestampNs(y)) => x.cmp(y),
114 (Value::TimestampMs(x), Value::TimestampMs(y)) => x.cmp(y),
115 (Value::TimestampSec(x), Value::TimestampSec(y)) => x.cmp(y),
116 (Value::InternalID(x), Value::InternalID(y)) => (x.table_id, x.offset).cmp(&(y.table_id, y.offset)),
118 _ => return None,
119 };
120 Some(ord)
121}
122
123fn in_range<T: PartialOrd>(min: &T, max: &T, val: &T) -> bool {
127 val >= min && val <= max
128}
129
130fn check_constant_predicate<T: PartialOrd>(min: &T, max: &T, constant: &T, op: &str) -> ZoneMapCheckResult {
133 match op {
134 "=" | "==" => {
135 if !in_range(min, max, constant) {
136 return ZoneMapCheckResult::SkipScan;
137 }
138 }
139 "!=" | "<>" => {
140 if constant == min && constant == max {
141 return ZoneMapCheckResult::SkipScan;
142 }
143 }
144 ">" => {
145 if constant >= max {
146 return ZoneMapCheckResult::SkipScan;
147 }
148 }
149 ">=" => {
150 if constant > max {
151 return ZoneMapCheckResult::SkipScan;
152 }
153 }
154 "<" => {
155 if constant <= min {
156 return ZoneMapCheckResult::SkipScan;
157 }
158 }
159 "<=" if constant < min => {
160 return ZoneMapCheckResult::SkipScan;
161 }
162 _ => {}
163 }
164 ZoneMapCheckResult::AlwaysScan
165}
166
167pub fn check_zone_map(stats: &ColumnChunkStats, op: &str, constant: &Value) -> ZoneMapCheckResult {
172 let (Some(min), Some(max)) = (&stats.min, &stats.max) else {
173 return ZoneMapCheckResult::AlwaysScan;
174 };
175
176 match (min, max, constant) {
178 (Value::Bool(a), Value::Bool(b), Value::Bool(c)) => check_constant_predicate(a, b, c, op),
179 (Value::Int64(a), Value::Int64(b), Value::Int64(c)) => check_constant_predicate(a, b, c, op),
181 (Value::Int32(a), Value::Int32(b), Value::Int32(c)) => check_constant_predicate(a, b, c, op),
182 (Value::Int16(a), Value::Int16(b), Value::Int16(c)) => check_constant_predicate(a, b, c, op),
183 (Value::Int8(a), Value::Int8(b), Value::Int8(c)) => check_constant_predicate(a, b, c, op),
184 (Value::UInt64(a), Value::UInt64(b), Value::UInt64(c)) => check_constant_predicate(a, b, c, op),
185 (Value::UInt32(a), Value::UInt32(b), Value::UInt32(c)) => check_constant_predicate(a, b, c, op),
186 (Value::UInt16(a), Value::UInt16(b), Value::UInt16(c)) => check_constant_predicate(a, b, c, op),
187 (Value::UInt8(a), Value::UInt8(b), Value::UInt8(c)) => check_constant_predicate(a, b, c, op),
188 (Value::Int128(a), Value::Int128(b), Value::Int128(c)) => check_constant_predicate(a, b, c, op),
189 (Value::UInt128(a), Value::UInt128(b), Value::UInt128(c)) => check_constant_predicate(a, b, c, op),
190 (Value::Double(a), Value::Double(b), Value::Double(c)) => check_constant_predicate(a, b, c, op),
192 (Value::Float(a), Value::Float(b), Value::Float(c)) => check_constant_predicate(a, b, c, op),
193 (Value::String(a), Value::String(b), Value::String(c)) => check_constant_predicate(a, b, c, op),
195 (Value::Date(a), Value::Date(b), Value::Date(c)) => check_constant_predicate(a, b, c, op),
197 (Value::Timestamp(a), Value::Timestamp(b), Value::Timestamp(c)) => check_constant_predicate(a, b, c, op),
198 (Value::TimestampTz(a), Value::TimestampTz(b), Value::TimestampTz(c)) => {
199 check_constant_predicate(&a.0, &b.0, &c.0, op)
201 }
202 (Value::TimestampNs(a), Value::TimestampNs(b), Value::TimestampNs(c)) => check_constant_predicate(a, b, c, op),
203 (Value::TimestampMs(a), Value::TimestampMs(b), Value::TimestampMs(c)) => check_constant_predicate(a, b, c, op),
204 (Value::TimestampSec(a), Value::TimestampSec(b), Value::TimestampSec(c)) => {
205 check_constant_predicate(a, b, c, op)
206 }
207 (Value::InternalID(a), Value::InternalID(b), Value::InternalID(c)) => check_constant_predicate(
210 &(a.table_id, a.offset),
211 &(b.table_id, b.offset),
212 &(c.table_id, c.offset),
213 op,
214 ),
215 _ => ZoneMapCheckResult::AlwaysScan,
217 }
218}
219
220pub fn check_null_zone_map(stats: &ColumnChunkStats, is_null: bool) -> ZoneMapCheckResult {
223 if is_null {
224 if stats.guaranteed_no_nulls {
225 return ZoneMapCheckResult::SkipScan;
226 }
227 } else if stats.guaranteed_all_nulls {
228 return ZoneMapCheckResult::SkipScan;
229 }
230 ZoneMapCheckResult::AlwaysScan
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236 use akar_common::types::{Date, InternalID};
237
238 fn int_stats(min: i64, max: i64) -> ColumnChunkStats {
239 ColumnChunkStats::new(Some(Value::Int64(min)), Some(Value::Int64(max)))
240 }
241
242 #[test]
243 fn test_eq_in_range() {
244 let stats = int_stats(10, 20);
245 assert_eq!(
246 check_zone_map(&stats, "=", &Value::Int64(15)),
247 ZoneMapCheckResult::AlwaysScan
248 );
249 }
250
251 #[test]
252 fn test_eq_out_of_range() {
253 let stats = int_stats(10, 20);
254 assert_eq!(
255 check_zone_map(&stats, "=", &Value::Int64(25)),
256 ZoneMapCheckResult::SkipScan
257 );
258 }
259
260 #[test]
261 fn test_eq_below_range() {
262 let stats = int_stats(10, 20);
263 assert_eq!(
264 check_zone_map(&stats, "=", &Value::Int64(5)),
265 ZoneMapCheckResult::SkipScan
266 );
267 }
268
269 #[test]
270 fn test_gt_all_below() {
271 let stats = int_stats(10, 20);
272 assert_eq!(
274 check_zone_map(&stats, ">", &Value::Int64(25)),
275 ZoneMapCheckResult::SkipScan
276 );
277 }
278
279 #[test]
280 fn test_gt_some_above() {
281 let stats = int_stats(10, 20);
282 assert_eq!(
283 check_zone_map(&stats, ">", &Value::Int64(5)),
284 ZoneMapCheckResult::AlwaysScan
285 );
286 }
287
288 #[test]
289 fn test_lt_all_above() {
290 let stats = int_stats(10, 20);
291 assert_eq!(
293 check_zone_map(&stats, "<", &Value::Int64(5)),
294 ZoneMapCheckResult::SkipScan
295 );
296 }
297
298 #[test]
299 fn test_lt_some_below() {
300 let stats = int_stats(10, 20);
301 assert_eq!(
302 check_zone_map(&stats, "<", &Value::Int64(25)),
303 ZoneMapCheckResult::AlwaysScan
304 );
305 }
306
307 #[test]
308 fn test_gte_eq_max_not_skip() {
309 let stats = int_stats(10, 20);
310 assert_eq!(
312 check_zone_map(&stats, ">=", &Value::Int64(20)),
313 ZoneMapCheckResult::AlwaysScan
314 );
315 }
316
317 #[test]
318 fn test_gte_gt_max() {
319 let stats = int_stats(10, 20);
320 assert_eq!(
322 check_zone_map(&stats, ">=", &Value::Int64(21)),
323 ZoneMapCheckResult::SkipScan
324 );
325 }
326
327 #[test]
328 fn test_lte_eq_min_not_skip() {
329 let stats = int_stats(10, 20);
330 assert_eq!(
331 check_zone_map(&stats, "<=", &Value::Int64(10)),
332 ZoneMapCheckResult::AlwaysScan
333 );
334 }
335
336 #[test]
337 fn test_lte_lt_min() {
338 let stats = int_stats(10, 20);
339 assert_eq!(
340 check_zone_map(&stats, "<=", &Value::Int64(9)),
341 ZoneMapCheckResult::SkipScan
342 );
343 }
344
345 #[test]
346 fn test_not_eq_single_value() {
347 let stats = int_stats(15, 15);
348 assert_eq!(
350 check_zone_map(&stats, "!=", &Value::Int64(15)),
351 ZoneMapCheckResult::SkipScan
352 );
353 }
354
355 #[test]
356 fn test_not_eq_range() {
357 let stats = int_stats(10, 20);
358 assert_eq!(
360 check_zone_map(&stats, "!=", &Value::Int64(15)),
361 ZoneMapCheckResult::AlwaysScan
362 );
363 }
364
365 #[test]
366 fn test_null_predicate_is_null_no_nulls() {
367 let stats = ColumnChunkStats {
368 min: Some(Value::Int64(1)),
369 max: Some(Value::Int64(10)),
370 guaranteed_no_nulls: true,
371 guaranteed_all_nulls: false,
372 };
373 assert_eq!(check_null_zone_map(&stats, true), ZoneMapCheckResult::SkipScan);
374 }
375
376 #[test]
377 fn test_null_predicate_is_null_has_nulls() {
378 let stats = ColumnChunkStats {
379 min: Some(Value::Int64(1)),
380 max: Some(Value::Int64(10)),
381 guaranteed_no_nulls: false,
382 guaranteed_all_nulls: false,
383 };
384 assert_eq!(check_null_zone_map(&stats, true), ZoneMapCheckResult::AlwaysScan);
385 }
386
387 #[test]
388 fn test_null_predicate_is_not_null_all_nulls() {
389 let stats = ColumnChunkStats {
390 min: None,
391 max: None,
392 guaranteed_no_nulls: false,
393 guaranteed_all_nulls: true,
394 };
395 assert_eq!(check_null_zone_map(&stats, false), ZoneMapCheckResult::SkipScan);
396 }
397
398 #[test]
399 fn test_no_stats_always_scan() {
400 let stats = ColumnChunkStats::new(None, None);
401 assert_eq!(
402 check_zone_map(&stats, "=", &Value::Int64(5)),
403 ZoneMapCheckResult::AlwaysScan
404 );
405 }
406
407 #[test]
408 fn test_string_zone_map() {
409 let stats = ColumnChunkStats::new(
410 Some(Value::String("apple".into())),
411 Some(Value::String("banana".into())),
412 );
413 assert_eq!(
415 check_zone_map(&stats, "=", &Value::String("cherry".into())),
416 ZoneMapCheckResult::SkipScan
417 );
418 assert_eq!(
420 check_zone_map(&stats, "=", &Value::String("avocado".into())),
421 ZoneMapCheckResult::AlwaysScan
422 );
423 }
424
425 #[test]
426 fn test_internal_id_zone_map() {
427 let stats = ColumnChunkStats::new(
428 Some(Value::InternalID(InternalID { offset: 0, table_id: 1 })),
429 Some(Value::InternalID(InternalID {
430 offset: 100,
431 table_id: 1,
432 })),
433 );
434 assert_eq!(
435 check_zone_map(
436 &stats,
437 "=",
438 &Value::InternalID(InternalID {
439 offset: 50,
440 table_id: 1
441 })
442 ),
443 ZoneMapCheckResult::AlwaysScan
444 );
445 assert_eq!(
446 check_zone_map(
447 &stats,
448 "=",
449 &Value::InternalID(InternalID {
450 offset: 200,
451 table_id: 1
452 })
453 ),
454 ZoneMapCheckResult::SkipScan
455 );
456 }
457
458 #[test]
459 fn test_double_zone_map() {
460 let stats = ColumnChunkStats::new(Some(Value::Double(1.5)), Some(Value::Double(9.5)));
461 assert_eq!(
462 check_zone_map(&stats, ">", &Value::Double(10.0)),
463 ZoneMapCheckResult::SkipScan
464 );
465 assert_eq!(
466 check_zone_map(&stats, "<", &Value::Double(1.0)),
467 ZoneMapCheckResult::SkipScan
468 );
469 }
470
471 #[test]
472 fn test_column_chunk_stats_update() {
473 let mut stats = ColumnChunkStats::new(None, None);
474 stats.update(&Value::Int64(5));
475 assert_eq!(stats.min, Some(Value::Int64(5)));
476 assert_eq!(stats.max, Some(Value::Int64(5)));
477
478 stats.update(&Value::Int64(3));
479 assert_eq!(stats.min, Some(Value::Int64(3)));
480 assert_eq!(stats.max, Some(Value::Int64(5)));
481
482 stats.update(&Value::Int64(10));
483 assert_eq!(stats.min, Some(Value::Int64(3)));
484 assert_eq!(stats.max, Some(Value::Int64(10)));
485 }
486
487 #[test]
488 fn test_column_chunk_stats_null_tracking() {
489 let mut stats = ColumnChunkStats::new(None, None);
490 assert!(stats.guaranteed_no_nulls, "empty chunk has no nulls");
491
492 stats.update(&Value::Null);
493 assert!(
494 !stats.guaranteed_no_nulls,
495 "appending Null must clear guaranteed_no_nulls"
496 );
497 assert_eq!(stats.min, None, "Null must not become min");
498 assert_eq!(stats.max, None, "Null must not become max");
499
500 stats.update(&Value::Int64(5));
501 assert_eq!(stats.min, Some(Value::Int64(5)));
502 assert_eq!(stats.max, Some(Value::Int64(5)));
503 assert!(!stats.guaranteed_no_nulls, "flag stays cleared after a null");
504 assert!(!stats.guaranteed_all_nulls, "non-null value clears all-nulls");
505
506 stats.update(&Value::Null);
507 assert_eq!(stats.min, Some(Value::Int64(5)), "later Null must not corrupt min/max");
508 assert_eq!(stats.max, Some(Value::Int64(5)));
509 assert_eq!(
510 check_null_zone_map(&stats, true),
511 ZoneMapCheckResult::AlwaysScan,
512 "IS NULL must scan when chunk has nulls"
513 );
514 }
515
516 #[test]
517 fn test_column_chunk_stats_all_nulls() {
518 let mut stats = ColumnChunkStats::new(None, None);
519 stats.update(&Value::Null);
520 stats.update(&Value::Null);
521 assert!(!stats.guaranteed_no_nulls);
522 assert_eq!(stats.min, None);
523 assert_eq!(stats.max, None);
524 }
525
526 #[test]
527 fn test_bool_zone_map_and_stats() {
528 let mut stats = ColumnChunkStats::new(None, None);
530 stats.update(&Value::Bool(true));
531 stats.update(&Value::Bool(false));
532 assert_eq!(stats.min, Some(Value::Bool(false)));
533 assert_eq!(stats.max, Some(Value::Bool(true)));
534
535 let single = ColumnChunkStats::new(Some(Value::Bool(true)), Some(Value::Bool(true)));
537 assert_eq!(
538 check_zone_map(&single, "!=", &Value::Bool(true)),
539 ZoneMapCheckResult::SkipScan
540 );
541 assert_eq!(
542 check_zone_map(&single, "=", &Value::Bool(true)),
543 ZoneMapCheckResult::AlwaysScan
544 );
545 assert_eq!(
546 check_zone_map(&single, "=", &Value::Bool(false)),
547 ZoneMapCheckResult::SkipScan
548 );
549 }
550
551 #[test]
552 fn test_date_zone_map_and_stats() {
553 let mut stats = ColumnChunkStats::new(None, None);
555 stats.update(&Value::Date(Date(10)));
556 stats.update(&Value::Date(Date(5)));
557 stats.update(&Value::Date(Date(20)));
558 assert_eq!(stats.min, Some(Value::Date(Date(5))));
559 assert_eq!(stats.max, Some(Value::Date(Date(20))));
560
561 assert_eq!(
562 check_zone_map(&stats, "=", &Value::Date(Date(25))),
563 ZoneMapCheckResult::SkipScan
564 );
565 assert_eq!(
566 check_zone_map(&stats, "=", &Value::Date(Date(10))),
567 ZoneMapCheckResult::AlwaysScan
568 );
569 }
570
571 #[test]
572 fn test_internal_id_zone_map_respects_table_id() {
573 let stats = ColumnChunkStats::new(
577 Some(Value::InternalID(InternalID { table_id: 1, offset: 5 })),
578 Some(Value::InternalID(InternalID { table_id: 1, offset: 5 })),
579 );
580 assert_eq!(
581 check_zone_map(&stats, "!=", &Value::InternalID(InternalID { table_id: 2, offset: 5 })),
582 ZoneMapCheckResult::AlwaysScan
583 );
584 assert_eq!(
585 check_zone_map(&stats, "=", &Value::InternalID(InternalID { table_id: 2, offset: 5 })),
586 ZoneMapCheckResult::SkipScan
587 );
588 }
589}