1use std::sync::Arc;
7
8use vortex_array::ArrayRef;
9use vortex_array::IntoArray;
10use vortex_array::VortexSessionExecute;
11use vortex_array::aggregate_fn::AggregateFnRef;
12use vortex_array::aggregate_fn::AggregateFnSatisfaction;
13use vortex_array::aggregate_fn::fns::all_nan::AllNan;
14use vortex_array::aggregate_fn::fns::all_non_nan::AllNonNan;
15use vortex_array::aggregate_fn::fns::all_non_null::AllNonNull;
16use vortex_array::aggregate_fn::fns::all_null::AllNull;
17use vortex_array::aggregate_fn::fns::bounded_max::BOUNDED_MAX_BOUND;
18use vortex_array::aggregate_fn::fns::bounded_max::BoundedMax;
19use vortex_array::arrays::ConstantArray;
20use vortex_array::arrays::PrimitiveArray;
21use vortex_array::arrays::StructArray;
22use vortex_array::arrays::struct_::StructArrayExt;
23use vortex_array::dtype::DType;
24use vortex_array::expr::BoundExpression;
25use vortex_array::expr::Expression;
26use vortex_array::expr::eq;
27use vortex_array::expr::get_item;
28use vortex_array::expr::lit;
29use vortex_array::expr::root;
30use vortex_array::expr::stats::Stat;
31use vortex_array::scalar_fn::EmptyOptions;
32use vortex_array::scalar_fn::ScalarFnVTableExt;
33use vortex_array::scalar_fn::internal::row_count::RowCount;
34use vortex_array::scalar_fn::internal::row_count::contains_row_count;
35use vortex_array::scalar_fn::internal::row_count::substitute_row_count;
36use vortex_array::stats::bind::StatBinder;
37use vortex_array::stats::bind::bind_stats;
38use vortex_array::validity::Validity;
39use vortex_buffer::buffer;
40use vortex_error::VortexResult;
41use vortex_error::vortex_bail;
42use vortex_error::vortex_ensure;
43use vortex_mask::Mask;
44use vortex_runend::RunEnd;
45use vortex_session::VortexSession;
46
47use crate::layouts::zoned::schema::aggregate_stats_table_dtype;
48use crate::layouts::zoned::schema::legacy_stats_table_dtype;
49
50#[derive(Clone)]
55pub struct ZoneMap {
56 column_dtype: DType,
58 array: StructArray,
60 aggregate_fns: Arc<[AggregateFnRef]>,
62 zone_len: u64,
64 row_count: u64,
66}
67
68impl ZoneMap {
69 pub fn try_new(
72 column_dtype: DType,
73 array: StructArray,
74 aggregate_fns: Arc<[AggregateFnRef]>,
75 zone_len: u64,
76 row_count: u64,
77 ) -> VortexResult<Self> {
78 let expected_dtype = aggregate_stats_table_dtype(&column_dtype, &aggregate_fns);
79 if &expected_dtype != array.dtype() {
80 vortex_bail!("Array dtype does not match expected zone map dtype: {expected_dtype}");
81 }
82
83 Ok(unsafe { Self::new_unchecked(column_dtype, array, aggregate_fns, zone_len, row_count) })
85 }
86
87 pub(super) unsafe fn new_unchecked(
88 column_dtype: DType,
89 array: StructArray,
90 aggregate_fns: Arc<[AggregateFnRef]>,
91 zone_len: u64,
92 row_count: u64,
93 ) -> Self {
94 Self {
95 column_dtype,
96 array,
97 aggregate_fns,
98 zone_len,
99 row_count,
100 }
101 }
102
103 #[deprecated(note = "use aggregate-function zoned stats instead")]
107 pub fn dtype_for_stats_table(column_dtype: &DType, present_stats: &[Stat]) -> DType {
108 legacy_stats_table_dtype(column_dtype, present_stats)
109 }
110
111 #[cfg(test)]
112 fn try_new_legacy(
113 column_dtype: DType,
114 array: StructArray,
115 stats: Arc<[Stat]>,
116 zone_len: u64,
117 row_count: u64,
118 ) -> VortexResult<Self> {
119 let expected_dtype = legacy_stats_table_dtype(&column_dtype, &stats);
120 if &expected_dtype != array.dtype() {
121 vortex_bail!("Array dtype does not match expected zone map dtype: {expected_dtype}");
122 }
123
124 Ok(unsafe { Self::new_unchecked(column_dtype, array, Arc::new([]), zone_len, row_count) })
126 }
127
128 pub fn prune(
142 &self,
143 predicate: &BoundExpression,
144 session: &VortexSession,
145 ) -> VortexResult<Mask> {
146 let mut ctx = session.create_execution_ctx();
147 let num_zones = self.array.len();
148 let predicate = self.lower_stats(predicate.clone())?;
149
150 let array = self.array.clone().into_array();
151 let applied = array.apply_bound(&predicate)?;
152
153 if !contains_row_count(&applied) {
154 return applied.null_as_false().execute(&mut ctx);
155 }
156
157 let row_count_array = row_count_array(self.zone_len, self.row_count, num_zones)?;
158 let substituted = substitute_row_count(applied, &row_count_array)?;
159 substituted.null_as_false().execute(&mut ctx)
160 }
161
162 fn lower_stats(&self, predicate: BoundExpression) -> VortexResult<BoundExpression> {
163 let binder = ZoneMapStatsBinder { zone_map: self };
164 bind_stats(predicate, &binder)
165 }
166}
167
168struct ZoneMapStatsBinder<'a> {
169 zone_map: &'a ZoneMap,
170}
171
172impl StatBinder for ZoneMapStatsBinder<'_> {
173 fn bind_aggregate(
174 &self,
175 input: &BoundExpression,
176 aggregate_fn: &AggregateFnRef,
177 _stat_dtype: &DType,
178 ) -> VortexResult<Option<BoundExpression>> {
179 if !input.is_root() {
180 return Ok(None);
181 }
182 vortex_ensure!(
183 input.dtype() == &self.zone_map.column_dtype,
184 "Stats predicate root dtype {} does not match zone-map column dtype {}",
185 input.dtype(),
186 self.zone_map.column_dtype
187 );
188
189 if let Some(stat_expr) = self.zone_map.aggregate_field_expr(aggregate_fn) {
190 return Ok(Some(self.bind_target(stat_expr)?));
191 }
192
193 if aggregate_fn.is::<AllNull>() {
194 return self
195 .zone_map
196 .stat_field_expr(Stat::NullCount)
197 .map(|null_count| self.bind_target(eq(null_count, row_count_expr())))
198 .transpose();
199 }
200
201 if aggregate_fn.is::<AllNonNull>() {
202 return self
203 .zone_map
204 .stat_field_expr(Stat::NullCount)
205 .map(|null_count| self.bind_target(eq(null_count, lit(0u64))))
206 .transpose();
207 }
208
209 if aggregate_fn.is::<AllNan>() {
210 return self
211 .zone_map
212 .stat_field_expr(Stat::NaNCount)
213 .map(|nan_count| self.bind_target(eq(nan_count, row_count_expr())))
214 .transpose();
215 }
216
217 if aggregate_fn.is::<AllNonNan>() {
218 return self
219 .zone_map
220 .stat_field_expr(Stat::NaNCount)
221 .map(|nan_count| self.bind_target(eq(nan_count, lit(0u64))))
222 .transpose();
223 }
224
225 if let Some(stat) = Stat::from_aggregate_fn(aggregate_fn) {
226 return self
227 .zone_map
228 .stat_field_expr(stat)
229 .map(|expr| self.bind_target(expr))
230 .transpose();
231 }
232
233 Ok(None)
234 }
235}
236
237impl ZoneMapStatsBinder<'_> {
238 fn bind_target(&self, expr: Expression) -> VortexResult<BoundExpression> {
239 expr.bind(self.zone_map.array.dtype())
240 }
241}
242
243impl ZoneMap {
244 fn aggregate_field_expr(&self, requested: &AggregateFnRef) -> Option<Expression> {
245 let field_name = requested.to_string();
246 if self.array.unmasked_field_by_name_opt(&field_name).is_some() {
247 return Some(aggregate_result_expr(
248 requested,
249 get_item(field_name, root()),
250 ));
251 }
252
253 let mut approximate = None;
254 for stored in self.aggregate_fns.iter() {
255 let field_name = stored.to_string();
256 if self.array.unmasked_field_by_name_opt(&field_name).is_none() {
257 continue;
258 }
259
260 match stored.can_satisfy(requested) {
261 AggregateFnSatisfaction::Exact => {
262 return Some(aggregate_result_expr(stored, get_item(field_name, root())));
263 }
264 AggregateFnSatisfaction::Approximate => {
265 approximate = Some(aggregate_result_expr(stored, get_item(field_name, root())));
266 }
267 AggregateFnSatisfaction::No => {}
268 }
269 }
270
271 approximate
272 }
273
274 fn stat_field_expr(&self, stat: Stat) -> Option<Expression> {
275 if let Some(aggregate_fn) = stat.aggregate_fn()
276 && let Some(expr) = self.aggregate_field_expr(&aggregate_fn)
277 {
278 return Some(expr);
279 }
280
281 self.legacy_stat_field_expr(stat)
282 }
283
284 fn legacy_stat_field_expr(&self, stat: Stat) -> Option<Expression> {
285 if self.array.unmasked_field_by_name_opt(stat.name()).is_some() {
286 return Some(get_item(stat.name(), root()));
287 }
288
289 None
290 }
291}
292
293fn aggregate_result_expr(stored: &AggregateFnRef, state_expr: Expression) -> Expression {
294 if stored.is::<BoundedMax>() {
295 get_item(BOUNDED_MAX_BOUND, state_expr)
296 } else {
297 state_expr
298 }
299}
300
301fn row_count_expr() -> Expression {
302 RowCount.new_expr(EmptyOptions, [])
303}
304
305fn row_count_array(zone_len: u64, row_count: u64, num_zones: usize) -> VortexResult<ArrayRef> {
311 if num_zones == 0 {
312 return Ok(ConstantArray::new(0u64, 0).into_array());
313 }
314
315 let last_zone_len = row_count - zone_len.saturating_mul((num_zones as u64) - 1);
316 if num_zones == 1 || last_zone_len == zone_len {
317 return Ok(ConstantArray::new(last_zone_len, num_zones).into_array());
318 }
319
320 let ends = unsafe {
321 PrimitiveArray::new_unchecked(
322 buffer![num_zones as u64 - 1, num_zones as u64],
323 Validity::NonNullable,
324 )
325 }
326 .into_array();
327 let values = unsafe {
328 PrimitiveArray::new_unchecked(buffer![zone_len, last_zone_len], Validity::NonNullable)
329 }
330 .into_array();
331
332 Ok(unsafe { RunEnd::new_unchecked(ends, values, 0, num_zones) }.into_array())
335}
336
337#[cfg(test)]
338mod tests {
339 use std::num::NonZeroUsize;
340 use std::sync::Arc;
341
342 use vortex_array::IntoArray;
343 use vortex_array::VortexSessionExecute;
344 use vortex_array::aggregate_fn::AggregateFnVTableExt;
345 use vortex_array::aggregate_fn::EmptyOptions;
346 use vortex_array::aggregate_fn::NumericalAggregateOpts;
347 use vortex_array::aggregate_fn::fns::all_non_null::AllNonNull;
348 use vortex_array::aggregate_fn::fns::all_null::AllNull;
349 use vortex_array::aggregate_fn::fns::bounded_max::BOUNDED_MAX_BOUND;
350 use vortex_array::aggregate_fn::fns::bounded_max::BOUNDED_MAX_UNKNOWN;
351 use vortex_array::aggregate_fn::fns::bounded_max::BoundedMax;
352 use vortex_array::aggregate_fn::fns::bounded_max::BoundedMaxOptions;
353 use vortex_array::aggregate_fn::fns::bounded_min::BoundedMin;
354 use vortex_array::aggregate_fn::fns::bounded_min::BoundedMinOptions;
355 use vortex_array::aggregate_fn::fns::max::Max;
356 use vortex_array::aggregate_fn::fns::min::Min;
357 use vortex_array::aggregate_fn::fns::nan_count::NanCount;
358 use vortex_array::aggregate_fn::fns::null_count::NullCount;
359 use vortex_array::arrays::BoolArray;
360 use vortex_array::arrays::PrimitiveArray;
361 use vortex_array::arrays::StructArray;
362 use vortex_array::assert_arrays_eq;
363 use vortex_array::dtype::DType;
364 use vortex_array::dtype::DecimalDType;
365 use vortex_array::dtype::FieldNames;
366 use vortex_array::dtype::Nullability;
367 use vortex_array::dtype::PType;
368 use vortex_array::expr::BoundExpression;
369 use vortex_array::expr::Expression;
370 use vortex_array::expr::cast;
371 use vortex_array::expr::gt;
372 use vortex_array::expr::gt_eq;
373 use vortex_array::expr::is_not_null;
374 use vortex_array::expr::is_null;
375 use vortex_array::expr::lit;
376 use vortex_array::expr::lt;
377 use vortex_array::expr::not_eq;
378 use vortex_array::expr::root;
379 use vortex_array::expr::stats::Stat;
380 use vortex_array::stats::all_nan;
381 use vortex_array::stats::all_non_nan;
382 use vortex_array::stats::all_non_null;
383 use vortex_array::stats::all_null;
384 use vortex_array::validity::Validity;
385 use vortex_buffer::buffer;
386 use vortex_error::VortexResult;
387 use vortex_mask::Mask;
388
389 use crate::layouts::zoned::zone_map::ZoneMap;
390 use crate::test::SESSION;
391
392 fn falsify(expr: &Expression, dtype: DType) -> BoundExpression {
393 expr.bind(&dtype)
394 .unwrap()
395 .falsify(&SESSION)
396 .unwrap()
397 .unwrap()
398 }
399
400 fn prune(zone_map: &ZoneMap, predicate: &Expression) -> VortexResult<Mask> {
401 zone_map.prune(&predicate.bind(&zone_map.column_dtype)?, &SESSION)
402 }
403
404 fn default_bounded_stat_max_bytes() -> NonZeroUsize {
405 unsafe { NonZeroUsize::new_unchecked(64) }
407 }
408
409 #[test]
410 fn test_zone_map_prunes() {
411 let max = Max.bind(NumericalAggregateOpts::skip_nans());
423 let min = Min.bind(NumericalAggregateOpts::skip_nans());
424 let zone_map = ZoneMap::try_new(
425 PType::I32.into(),
426 StructArray::from_fields(&[
427 (
428 max.to_string(),
429 PrimitiveArray::new(buffer![5i32, 6i32, 7i32], Validity::AllValid).into_array(),
430 ),
431 (
432 min.to_string(),
433 PrimitiveArray::new(buffer![1i32, 2i32, 3i32], Validity::AllValid).into_array(),
434 ),
435 ])
436 .unwrap(),
437 Arc::new([max, min]),
438 3,
439 10,
440 )
441 .unwrap();
442 let ctx = &mut SESSION.create_execution_ctx();
443
444 let expr = gt_eq(root(), lit(6i32));
447 let pruning_expr = falsify(&expr, PType::I32.into());
448 let mask = zone_map.prune(&pruning_expr, &SESSION).unwrap();
449 assert_arrays_eq!(
450 mask.into_array(),
451 BoolArray::from_iter([true, false, false]),
452 ctx
453 );
454
455 let expr = gt(root(), lit(5i32));
458 let pruning_expr = falsify(&expr, PType::I32.into());
459 let mask = zone_map.prune(&pruning_expr, &SESSION).unwrap();
460 assert_arrays_eq!(
461 mask.into_array(),
462 BoolArray::from_iter([true, false, false]),
463 ctx
464 );
465
466 let expr = lt(root(), lit(2i32));
469 let pruning_expr = falsify(&expr, PType::I32.into());
470 let mask = zone_map.prune(&pruning_expr, &SESSION).unwrap();
471 assert_arrays_eq!(
472 mask.into_array(),
473 BoolArray::from_iter([false, true, true]),
474 ctx
475 );
476 }
477
478 #[test]
479 fn bounded_display_names_satisfy_min_max_rewrites() {
480 let bounded_max = BoundedMax.bind(BoundedMaxOptions {
481 max_bytes: default_bounded_stat_max_bytes(),
482 });
483 let bounded_min = BoundedMin.bind(BoundedMinOptions {
484 max_bytes: default_bounded_stat_max_bytes(),
485 });
486 let zone_map = ZoneMap::try_new(
487 PType::I32.into(),
488 StructArray::from_fields(&[
489 (
490 bounded_max.to_string(),
491 StructArray::try_new(
492 [BOUNDED_MAX_BOUND, BOUNDED_MAX_UNKNOWN].into(),
493 vec![
494 PrimitiveArray::new(buffer![5i32, 6i32, 7i32], Validity::AllValid)
495 .into_array(),
496 BoolArray::from_iter([false, false, false]).into_array(),
497 ],
498 3,
499 Validity::AllValid,
500 )
501 .unwrap()
502 .into_array(),
503 ),
504 (
505 bounded_min.to_string(),
506 PrimitiveArray::new(buffer![1i32, 2i32, 3i32], Validity::AllValid).into_array(),
507 ),
508 ])
509 .unwrap(),
510 Arc::new([bounded_max, bounded_min]),
511 3,
512 10,
513 )
514 .unwrap();
515 let ctx = &mut SESSION.create_execution_ctx();
516
517 let expr = gt(root(), lit(5i32));
518 let pruning_expr = falsify(&expr, PType::I32.into());
519 let mask = zone_map.prune(&pruning_expr, &SESSION).unwrap();
520 assert_arrays_eq!(
521 mask.into_array(),
522 BoolArray::from_iter([true, false, false]),
523 ctx
524 );
525
526 let expr = lt(root(), lit(2i32));
527 let pruning_expr = falsify(&expr, PType::I32.into());
528 let mask = zone_map.prune(&pruning_expr, &SESSION).unwrap();
529 assert_arrays_eq!(
530 mask.into_array(),
531 BoolArray::from_iter([false, true, true]),
532 ctx
533 );
534 }
535
536 #[test]
537 fn row_count_prunes_short_trailing_zone() {
538 let zone_map = ZoneMap::try_new_legacy(
539 PType::U64.into(),
540 StructArray::from_fields(&[(
541 "null_count",
542 PrimitiveArray::new(buffer![0u64, 0, 2], Validity::AllValid).into_array(),
543 )])
544 .unwrap(),
545 Arc::new([Stat::NullCount]),
546 4,
547 10,
548 )
549 .unwrap();
550
551 let expr = is_not_null(root());
552 let pruning_expr = falsify(&expr, PType::U64.into());
553
554 let mask = zone_map.prune(&pruning_expr, &SESSION).unwrap();
555 assert_arrays_eq!(
556 mask.into_array(),
557 BoolArray::from_iter([false, false, true]),
558 &mut SESSION.create_execution_ctx()
559 );
560 }
561
562 #[test]
563 fn row_count_substitution_handles_empty_zone_map() {
564 let zone_map = ZoneMap::try_new_legacy(
565 PType::U64.into(),
566 StructArray::from_fields(&[(
567 "null_count",
568 PrimitiveArray::new::<u64>(buffer![], Validity::AllValid).into_array(),
569 )])
570 .unwrap(),
571 Arc::new([Stat::NullCount]),
572 4,
573 0,
574 )
575 .unwrap();
576
577 let expr = is_not_null(root());
578 let pruning_expr = falsify(&expr, PType::U64.into());
579
580 let mask = zone_map.prune(&pruning_expr, &SESSION).unwrap();
581 assert_eq!(mask.len(), 0);
582 }
583
584 #[test]
585 fn is_null_falsification_uses_null_count() {
586 let zone_map = ZoneMap::try_new_legacy(
587 PType::U64.into(),
588 StructArray::from_fields(&[(
589 "null_count",
590 PrimitiveArray::new(buffer![0u64, 4, 2], Validity::AllValid).into_array(),
591 )])
592 .unwrap(),
593 Arc::new([Stat::NullCount]),
594 4,
595 10,
596 )
597 .unwrap();
598
599 let expr = is_null(root());
600 let pruning_expr = falsify(&expr, PType::U64.into());
601
602 let mask = zone_map.prune(&pruning_expr, &SESSION).unwrap();
603 assert_arrays_eq!(
604 mask.into_array(),
605 BoolArray::from_iter([true, false, false]),
606 &mut SESSION.create_execution_ctx()
607 );
608 }
609
610 #[test]
611 fn all_null_stat_fn_lowers_to_null_count_and_row_count() {
612 let zone_map = ZoneMap::try_new_legacy(
613 PType::U64.into(),
614 StructArray::from_fields(&[(
615 "null_count",
616 PrimitiveArray::new(buffer![0u64, 4, 2], Validity::AllValid).into_array(),
617 )])
618 .unwrap(),
619 Arc::new([Stat::NullCount]),
620 4,
621 10,
622 )
623 .unwrap();
624
625 let mask = prune(&zone_map, &all_null(root())).unwrap();
626 assert_arrays_eq!(
627 mask.into_array(),
628 BoolArray::from_iter([false, true, true]),
629 &mut SESSION.create_execution_ctx()
630 );
631 }
632
633 #[test]
634 fn all_non_null_stat_fn_lowers_to_null_count() {
635 let zone_map = ZoneMap::try_new_legacy(
636 PType::U64.into(),
637 StructArray::from_fields(&[(
638 "null_count",
639 PrimitiveArray::new(buffer![0u64, 4, 2], Validity::AllValid).into_array(),
640 )])
641 .unwrap(),
642 Arc::new([Stat::NullCount]),
643 4,
644 10,
645 )
646 .unwrap();
647
648 let mask = prune(&zone_map, &all_non_null(root())).unwrap();
649 assert_arrays_eq!(
650 mask.into_array(),
651 BoolArray::from_iter([true, false, false]),
652 &mut SESSION.create_execution_ctx()
653 );
654 }
655
656 #[test]
657 fn all_null_stat_fn_lowers_to_null_count_field() {
658 let null_count = NullCount.bind(EmptyOptions);
659 let zone_map = ZoneMap::try_new(
660 PType::U64.into(),
661 StructArray::from_fields(&[(
662 null_count.to_string(),
663 PrimitiveArray::new(buffer![4u64, 0, 2], Validity::AllValid).into_array(),
664 )])
665 .unwrap(),
666 Arc::new([null_count]),
667 4,
668 10,
669 )
670 .unwrap();
671 let ctx = &mut SESSION.create_execution_ctx();
672
673 let mask = prune(&zone_map, &all_null(root())).unwrap();
674 assert_arrays_eq!(
675 mask.into_array(),
676 BoolArray::from_iter([true, false, true]),
677 ctx
678 );
679
680 let mask = prune(&zone_map, &all_non_null(root())).unwrap();
681 assert_arrays_eq!(
682 mask.into_array(),
683 BoolArray::from_iter([false, true, false]),
684 ctx
685 );
686 }
687
688 #[test]
689 fn all_nan_stat_fn_lowers_to_nan_count_field() {
690 let nan_count = NanCount.bind(EmptyOptions);
691 let zone_map = ZoneMap::try_new(
692 PType::F32.into(),
693 StructArray::from_fields(&[(
694 nan_count.to_string(),
695 PrimitiveArray::new(buffer![4u64, 0, 2], Validity::AllValid).into_array(),
696 )])
697 .unwrap(),
698 Arc::new([nan_count]),
699 4,
700 10,
701 )
702 .unwrap();
703 let ctx = &mut SESSION.create_execution_ctx();
704
705 let mask = prune(&zone_map, &all_nan(root())).unwrap();
706 assert_arrays_eq!(
707 mask.into_array(),
708 BoolArray::from_iter([true, false, true]),
709 ctx
710 );
711
712 let mask = prune(&zone_map, &all_non_nan(root())).unwrap();
713 assert_arrays_eq!(
714 mask.into_array(),
715 BoolArray::from_iter([false, true, false]),
716 ctx
717 );
718 }
719
720 #[test]
721 fn non_float_nan_stat_fns_fail_to_bind() {
722 let dtype = DType::from(PType::I32);
723 for expr in [all_nan(root()), all_non_nan(root())] {
724 let error = expr.bind(&dtype).unwrap_err();
725 assert!(
726 error
727 .to_string()
728 .contains("does not support input dtype i32"),
729 "{error}"
730 );
731 }
732 }
733
734 #[test]
735 fn unavailable_stat_fn_lowers_to_unknown_mask() {
736 let zone_map = ZoneMap::try_new(
737 PType::U64.into(),
738 StructArray::try_new(FieldNames::empty(), vec![], 3, Validity::NonNullable).unwrap(),
739 Arc::new([]),
740 4,
741 10,
742 )
743 .unwrap();
744 let ctx = &mut SESSION.create_execution_ctx();
745
746 let mask = prune(&zone_map, &all_non_null(root())).unwrap();
747 assert_arrays_eq!(
748 mask.into_array(),
749 BoolArray::from_iter([false, false, false]),
750 ctx
751 );
752
753 let expr = gt(root(), lit(5u64));
754 let pruning_expr = falsify(&expr, PType::U64.into());
755 let mask = zone_map.prune(&pruning_expr, &SESSION).unwrap();
756 assert_arrays_eq!(
757 mask.into_array(),
758 BoolArray::from_iter([false, false, false]),
759 ctx
760 );
761 }
762
763 #[test]
764 fn float_min_max_stat_fn_requires_nan_count() {
765 let max = Max.bind(NumericalAggregateOpts::skip_nans());
766 let zone_map = ZoneMap::try_new(
767 PType::F32.into(),
768 StructArray::from_fields(&[(
769 max.to_string(),
770 PrimitiveArray::new(buffer![5.0f32, 6.0, 7.0], Validity::AllValid).into_array(),
771 )])
772 .unwrap(),
773 Arc::new([max.clone()]),
774 4,
775 12,
776 )
777 .unwrap();
778 let ctx = &mut SESSION.create_execution_ctx();
779
780 let expr = gt(root(), lit(5.0f32));
781 let pruning_expr = falsify(&expr, PType::F32.into());
782 let mask = zone_map.prune(&pruning_expr, &SESSION).unwrap();
783 assert_arrays_eq!(
784 mask.into_array(),
785 BoolArray::from_iter([false, false, false]),
786 ctx
787 );
788
789 let nan_count = NanCount.bind(EmptyOptions);
790 let zone_map = ZoneMap::try_new(
791 PType::F32.into(),
792 StructArray::from_fields(&[
793 (
794 max.to_string(),
795 PrimitiveArray::new(buffer![5.0f32, 6.0, 7.0], Validity::AllValid).into_array(),
796 ),
797 (
798 nan_count.to_string(),
799 PrimitiveArray::new(buffer![0u64, 0, 0], Validity::AllValid).into_array(),
800 ),
801 ])
802 .unwrap(),
803 Arc::new([max, nan_count]),
804 4,
805 12,
806 )
807 .unwrap();
808
809 let mask = zone_map.prune(&pruning_expr, &SESSION).unwrap();
810 assert_arrays_eq!(
811 mask.into_array(),
812 BoolArray::from_iter([true, false, false]),
813 ctx
814 );
815 }
816
817 #[test]
818 fn float_cast_min_max_stat_fn_uses_source_nan_count() {
819 let zone_map = ZoneMap::try_new_legacy(
820 PType::F32.into(),
821 StructArray::from_fields(&[
822 (
823 "max",
824 PrimitiveArray::new(buffer![5.0f32, 5.0], Validity::AllValid).into_array(),
825 ),
826 (
827 "max_is_truncated",
828 BoolArray::from_iter([false, false]).into_array(),
829 ),
830 (
831 "min",
832 PrimitiveArray::new(buffer![5.0f32, 5.0], Validity::AllValid).into_array(),
833 ),
834 (
835 "min_is_truncated",
836 BoolArray::from_iter([false, false]).into_array(),
837 ),
838 (
839 "nan_count",
840 PrimitiveArray::new(buffer![1u64, 0], Validity::AllValid).into_array(),
841 ),
842 ])
843 .unwrap(),
844 Arc::new([Stat::Max, Stat::Min, Stat::NaNCount]),
845 4,
846 8,
847 )
848 .unwrap();
849
850 let cast_dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
851 let expr = not_eq(cast(root(), cast_dtype), lit(5i32));
852 let pruning_expr = falsify(&expr, PType::F32.into());
853
854 let mask = zone_map.prune(&pruning_expr, &SESSION).unwrap();
855 assert_arrays_eq!(
856 mask.into_array(),
857 BoolArray::from_iter([false, true]),
858 &mut SESSION.create_execution_ctx()
859 );
860 }
861
862 #[test]
863 fn fixed_size_list_min_max_stat_fn_lowers_to_unknown_mask() {
864 let elem_dtype = Arc::new(DType::Decimal(
868 DecimalDType::new(10, 2),
869 Nullability::Nullable,
870 ));
871 let column_dtype = DType::FixedSizeList(elem_dtype, 1, Nullability::Nullable);
872
873 let zone_map = ZoneMap::try_new(
874 column_dtype,
875 StructArray::try_new(FieldNames::empty(), vec![], 3, Validity::NonNullable).unwrap(),
876 Arc::new([]),
877 4,
878 10,
879 )
880 .unwrap();
881
882 let max_fn = Stat::Max
883 .aggregate_fn()
884 .expect("max should have an aggregate function");
885 let predicate = is_null(vortex_array::stats::stat(root(), max_fn));
886
887 let mask = prune(&zone_map, &predicate).unwrap();
889 assert_arrays_eq!(
890 mask.into_array(),
891 BoolArray::from_iter([true, true, true]),
892 &mut SESSION.create_execution_ctx()
893 );
894 }
895
896 #[test]
897 fn unsupported_aggregate_input_dtype_errors() {
898 let zone_map = ZoneMap::try_new(
899 DType::Null,
900 StructArray::try_new(FieldNames::empty(), vec![], 3, Validity::NonNullable).unwrap(),
901 Arc::new([]),
902 4,
903 10,
904 )
905 .unwrap();
906
907 let max_fn = Stat::Max
908 .aggregate_fn()
909 .expect("max should have an aggregate function");
910 let predicate = is_null(vortex_array::stats::stat(root(), max_fn));
911 let error = prune(&zone_map, &predicate).unwrap_err();
912
913 assert!(
914 error
915 .to_string()
916 .contains("Aggregate function vortex.max() does not support input dtype null"),
917 "{error}"
918 );
919 }
920
921 #[test]
922 fn row_count_prunes_all_null_uniform_zones() {
923 let zone_map = ZoneMap::try_new_legacy(
924 PType::U64.into(),
925 StructArray::from_fields(&[(
926 "null_count",
927 PrimitiveArray::new(buffer![0u64, 4, 0], Validity::AllValid).into_array(),
928 )])
929 .unwrap(),
930 Arc::new([Stat::NullCount]),
931 4,
932 12,
933 )
934 .unwrap();
935
936 let expr = is_not_null(root());
937 let pruning_expr = falsify(&expr, PType::U64.into());
938
939 let mask = zone_map.prune(&pruning_expr, &SESSION).unwrap();
941 assert_arrays_eq!(
942 mask.into_array(),
943 BoolArray::from_iter([false, true, false]),
944 &mut SESSION.create_execution_ctx()
945 );
946 }
947
948 #[test]
949 fn all_null_stat_fn_lowers_to_aggregate_field() {
950 let all_null_agg = AllNull.bind(EmptyOptions);
951 let zone_map = ZoneMap::try_new(
952 PType::U64.into(),
953 StructArray::from_fields(&[(
954 all_null_agg.to_string(),
955 BoolArray::from_iter([Some(false), Some(true), Some(true)]).into_array(),
956 )])
957 .unwrap(),
958 Arc::new([all_null_agg]),
959 4,
960 10,
961 )
962 .unwrap();
963
964 let mask = prune(&zone_map, &all_null(root())).unwrap();
965 assert_arrays_eq!(
966 mask.into_array(),
967 BoolArray::from_iter([false, true, true]),
968 &mut SESSION.create_execution_ctx()
969 );
970 }
971
972 #[test]
973 fn all_non_null_stat_fn_lowers_to_aggregate_field() {
974 let all_non_null_agg = AllNonNull.bind(EmptyOptions);
975 let zone_map = ZoneMap::try_new(
976 PType::U64.into(),
977 StructArray::from_fields(&[(
978 all_non_null_agg.to_string(),
979 BoolArray::from_iter([Some(true), Some(false), Some(false)]).into_array(),
980 )])
981 .unwrap(),
982 Arc::new([all_non_null_agg]),
983 4,
984 10,
985 )
986 .unwrap();
987
988 let mask = prune(&zone_map, &all_non_null(root())).unwrap();
989 assert_arrays_eq!(
990 mask.into_array(),
991 BoolArray::from_iter([true, false, false]),
992 &mut SESSION.create_execution_ctx()
993 );
994 }
995}