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> {
315 if num_zones == 0 {
316 return Ok(ConstantArray::new(0u64, 0).into_array());
317 }
318
319 let leading_rows = zone_len.saturating_mul((num_zones as u64) - 1);
320 let Some(last_zone_len) = row_count.checked_sub(leading_rows) else {
321 vortex_bail!(
322 "Zone map declares {num_zones} zones of {zone_len} rows, which is more than the \
323 {row_count} rows of the layout"
324 );
325 };
326 if num_zones == 1 || last_zone_len == zone_len {
327 return Ok(ConstantArray::new(last_zone_len, num_zones).into_array());
328 }
329
330 let ends = unsafe {
331 PrimitiveArray::new_unchecked(
332 buffer![num_zones as u64 - 1, num_zones as u64],
333 Validity::NonNullable,
334 )
335 }
336 .into_array();
337 let values = unsafe {
338 PrimitiveArray::new_unchecked(buffer![zone_len, last_zone_len], Validity::NonNullable)
339 }
340 .into_array();
341
342 Ok(unsafe { RunEnd::new_unchecked(ends, values, 0, num_zones) }.into_array())
345}
346
347#[cfg(test)]
348mod tests {
349 use std::num::NonZeroUsize;
350 use std::sync::Arc;
351
352 use rstest::rstest;
353 use vortex_array::IntoArray;
354 use vortex_array::VortexSessionExecute;
355 use vortex_array::aggregate_fn::AggregateFnVTableExt;
356 use vortex_array::aggregate_fn::EmptyOptions;
357 use vortex_array::aggregate_fn::NumericalAggregateOpts;
358 use vortex_array::aggregate_fn::fns::all_non_null::AllNonNull;
359 use vortex_array::aggregate_fn::fns::all_null::AllNull;
360 use vortex_array::aggregate_fn::fns::bounded_max::BOUNDED_MAX_BOUND;
361 use vortex_array::aggregate_fn::fns::bounded_max::BOUNDED_MAX_UNKNOWN;
362 use vortex_array::aggregate_fn::fns::bounded_max::BoundedMax;
363 use vortex_array::aggregate_fn::fns::bounded_max::BoundedMaxOptions;
364 use vortex_array::aggregate_fn::fns::bounded_min::BoundedMin;
365 use vortex_array::aggregate_fn::fns::bounded_min::BoundedMinOptions;
366 use vortex_array::aggregate_fn::fns::max::Max;
367 use vortex_array::aggregate_fn::fns::min::Min;
368 use vortex_array::aggregate_fn::fns::nan_count::NanCount;
369 use vortex_array::aggregate_fn::fns::null_count::NullCount;
370 use vortex_array::arrays::BoolArray;
371 use vortex_array::arrays::PrimitiveArray;
372 use vortex_array::arrays::StructArray;
373 use vortex_array::assert_arrays_eq;
374 use vortex_array::dtype::DType;
375 use vortex_array::dtype::DecimalDType;
376 use vortex_array::dtype::FieldNames;
377 use vortex_array::dtype::Nullability;
378 use vortex_array::dtype::PType;
379 use vortex_array::expr::BoundExpression;
380 use vortex_array::expr::Expression;
381 use vortex_array::expr::cast;
382 use vortex_array::expr::gt;
383 use vortex_array::expr::gt_eq;
384 use vortex_array::expr::is_not_null;
385 use vortex_array::expr::is_null;
386 use vortex_array::expr::lit;
387 use vortex_array::expr::lt;
388 use vortex_array::expr::not_eq;
389 use vortex_array::expr::root;
390 use vortex_array::expr::stats::Stat;
391 use vortex_array::stats::all_nan;
392 use vortex_array::stats::all_non_nan;
393 use vortex_array::stats::all_non_null;
394 use vortex_array::stats::all_null;
395 use vortex_array::validity::Validity;
396 use vortex_buffer::Buffer;
397 use vortex_buffer::buffer;
398 use vortex_error::VortexResult;
399 use vortex_mask::Mask;
400
401 use crate::layouts::zoned::zone_map::ZoneMap;
402 use crate::test::SESSION;
403
404 fn falsify(expr: &Expression, dtype: DType) -> BoundExpression {
405 expr.bind(&dtype)
406 .unwrap()
407 .falsify(&SESSION)
408 .unwrap()
409 .unwrap()
410 }
411
412 fn prune(zone_map: &ZoneMap, predicate: &Expression) -> VortexResult<Mask> {
413 zone_map.prune(&predicate.bind(&zone_map.column_dtype)?, &SESSION)
414 }
415
416 fn default_bounded_stat_max_bytes() -> NonZeroUsize {
417 unsafe { NonZeroUsize::new_unchecked(64) }
419 }
420
421 #[test]
422 fn test_zone_map_prunes() {
423 let max = Max.bind(NumericalAggregateOpts::skip_nans());
435 let min = Min.bind(NumericalAggregateOpts::skip_nans());
436 let zone_map = ZoneMap::try_new(
437 PType::I32.into(),
438 StructArray::from_fields(&[
439 (
440 max.to_string(),
441 PrimitiveArray::new(buffer![5i32, 6i32, 7i32], Validity::AllValid).into_array(),
442 ),
443 (
444 min.to_string(),
445 PrimitiveArray::new(buffer![1i32, 2i32, 3i32], Validity::AllValid).into_array(),
446 ),
447 ])
448 .unwrap(),
449 Arc::new([max, min]),
450 3,
451 10,
452 )
453 .unwrap();
454 let ctx = &mut SESSION.create_execution_ctx();
455
456 let expr = gt_eq(root(), lit(6i32));
459 let pruning_expr = falsify(&expr, PType::I32.into());
460 let mask = zone_map.prune(&pruning_expr, &SESSION).unwrap();
461 assert_arrays_eq!(
462 mask.into_array(),
463 BoolArray::from_iter([true, false, false]),
464 ctx
465 );
466
467 let expr = gt(root(), lit(5i32));
470 let pruning_expr = falsify(&expr, PType::I32.into());
471 let mask = zone_map.prune(&pruning_expr, &SESSION).unwrap();
472 assert_arrays_eq!(
473 mask.into_array(),
474 BoolArray::from_iter([true, false, false]),
475 ctx
476 );
477
478 let expr = lt(root(), lit(2i32));
481 let pruning_expr = falsify(&expr, PType::I32.into());
482 let mask = zone_map.prune(&pruning_expr, &SESSION).unwrap();
483 assert_arrays_eq!(
484 mask.into_array(),
485 BoolArray::from_iter([false, true, true]),
486 ctx
487 );
488 }
489
490 #[test]
491 fn bounded_display_names_satisfy_min_max_rewrites() {
492 let bounded_max = BoundedMax.bind(BoundedMaxOptions {
493 max_bytes: default_bounded_stat_max_bytes(),
494 });
495 let bounded_min = BoundedMin.bind(BoundedMinOptions {
496 max_bytes: default_bounded_stat_max_bytes(),
497 });
498 let zone_map = ZoneMap::try_new(
499 PType::I32.into(),
500 StructArray::from_fields(&[
501 (
502 bounded_max.to_string(),
503 StructArray::try_new(
504 [BOUNDED_MAX_BOUND, BOUNDED_MAX_UNKNOWN].into(),
505 vec![
506 PrimitiveArray::new(buffer![5i32, 6i32, 7i32], Validity::AllValid)
507 .into_array(),
508 BoolArray::from_iter([false, false, false]).into_array(),
509 ],
510 3,
511 Validity::AllValid,
512 )
513 .unwrap()
514 .into_array(),
515 ),
516 (
517 bounded_min.to_string(),
518 PrimitiveArray::new(buffer![1i32, 2i32, 3i32], Validity::AllValid).into_array(),
519 ),
520 ])
521 .unwrap(),
522 Arc::new([bounded_max, bounded_min]),
523 3,
524 10,
525 )
526 .unwrap();
527 let ctx = &mut SESSION.create_execution_ctx();
528
529 let expr = gt(root(), lit(5i32));
530 let pruning_expr = falsify(&expr, PType::I32.into());
531 let mask = zone_map.prune(&pruning_expr, &SESSION).unwrap();
532 assert_arrays_eq!(
533 mask.into_array(),
534 BoolArray::from_iter([true, false, false]),
535 ctx
536 );
537
538 let expr = lt(root(), lit(2i32));
539 let pruning_expr = falsify(&expr, PType::I32.into());
540 let mask = zone_map.prune(&pruning_expr, &SESSION).unwrap();
541 assert_arrays_eq!(
542 mask.into_array(),
543 BoolArray::from_iter([false, true, true]),
544 ctx
545 );
546 }
547
548 #[test]
549 fn row_count_prunes_short_trailing_zone() {
550 let zone_map = ZoneMap::try_new_legacy(
551 PType::U64.into(),
552 StructArray::from_fields(&[(
553 "null_count",
554 PrimitiveArray::new(buffer![0u64, 0, 2], Validity::AllValid).into_array(),
555 )])
556 .unwrap(),
557 Arc::new([Stat::NullCount]),
558 4,
559 10,
560 )
561 .unwrap();
562
563 let expr = is_not_null(root());
564 let pruning_expr = falsify(&expr, PType::U64.into());
565
566 let mask = zone_map.prune(&pruning_expr, &SESSION).unwrap();
567 assert_arrays_eq!(
568 mask.into_array(),
569 BoolArray::from_iter([false, false, true]),
570 &mut SESSION.create_execution_ctx()
571 );
572 }
573
574 #[rstest]
578 #[case::zones_overrun_row_count(4, 4, 10)]
581 #[case::leading_zones_overrun(8, 3, 10)]
583 #[case::no_rows_but_zones_present(4, 2, 0)]
585 fn row_count_rejects_zones_overrunning_the_layout(
586 #[case] zone_len: u64,
587 #[case] num_zones: usize,
588 #[case] row_count: u64,
589 ) {
590 let null_counts = PrimitiveArray::new(
591 Buffer::<u64>::from_iter(std::iter::repeat_n(0u64, num_zones)),
592 Validity::AllValid,
593 )
594 .into_array();
595 let zone_map = ZoneMap::try_new_legacy(
596 PType::U64.into(),
597 StructArray::from_fields(&[("null_count", null_counts)]).unwrap(),
598 Arc::new([Stat::NullCount]),
599 zone_len,
600 row_count,
601 )
602 .unwrap();
603
604 let pruning_expr = falsify(&is_not_null(root()), PType::U64.into());
605 let message = zone_map
606 .prune(&pruning_expr, &SESSION)
607 .err()
608 .map(|err| err.to_string())
609 .unwrap_or_default();
610 assert!(
611 message.contains("more than the"),
612 "zone_len {zone_len} x {num_zones} zones over {row_count} rows should have been \
613 rejected, got: {message}"
614 );
615 }
616
617 #[test]
618 fn row_count_substitution_handles_empty_zone_map() {
619 let zone_map = ZoneMap::try_new_legacy(
620 PType::U64.into(),
621 StructArray::from_fields(&[(
622 "null_count",
623 PrimitiveArray::new::<u64>(buffer![], Validity::AllValid).into_array(),
624 )])
625 .unwrap(),
626 Arc::new([Stat::NullCount]),
627 4,
628 0,
629 )
630 .unwrap();
631
632 let expr = is_not_null(root());
633 let pruning_expr = falsify(&expr, PType::U64.into());
634
635 let mask = zone_map.prune(&pruning_expr, &SESSION).unwrap();
636 assert_eq!(mask.len(), 0);
637 }
638
639 #[test]
640 fn is_null_falsification_uses_null_count() {
641 let zone_map = ZoneMap::try_new_legacy(
642 PType::U64.into(),
643 StructArray::from_fields(&[(
644 "null_count",
645 PrimitiveArray::new(buffer![0u64, 4, 2], Validity::AllValid).into_array(),
646 )])
647 .unwrap(),
648 Arc::new([Stat::NullCount]),
649 4,
650 10,
651 )
652 .unwrap();
653
654 let expr = is_null(root());
655 let pruning_expr = falsify(&expr, PType::U64.into());
656
657 let mask = zone_map.prune(&pruning_expr, &SESSION).unwrap();
658 assert_arrays_eq!(
659 mask.into_array(),
660 BoolArray::from_iter([true, false, false]),
661 &mut SESSION.create_execution_ctx()
662 );
663 }
664
665 #[test]
666 fn all_null_stat_fn_lowers_to_null_count_and_row_count() {
667 let zone_map = ZoneMap::try_new_legacy(
668 PType::U64.into(),
669 StructArray::from_fields(&[(
670 "null_count",
671 PrimitiveArray::new(buffer![0u64, 4, 2], Validity::AllValid).into_array(),
672 )])
673 .unwrap(),
674 Arc::new([Stat::NullCount]),
675 4,
676 10,
677 )
678 .unwrap();
679
680 let mask = prune(&zone_map, &all_null(root())).unwrap();
681 assert_arrays_eq!(
682 mask.into_array(),
683 BoolArray::from_iter([false, true, true]),
684 &mut SESSION.create_execution_ctx()
685 );
686 }
687
688 #[test]
689 fn all_non_null_stat_fn_lowers_to_null_count() {
690 let zone_map = ZoneMap::try_new_legacy(
691 PType::U64.into(),
692 StructArray::from_fields(&[(
693 "null_count",
694 PrimitiveArray::new(buffer![0u64, 4, 2], Validity::AllValid).into_array(),
695 )])
696 .unwrap(),
697 Arc::new([Stat::NullCount]),
698 4,
699 10,
700 )
701 .unwrap();
702
703 let mask = prune(&zone_map, &all_non_null(root())).unwrap();
704 assert_arrays_eq!(
705 mask.into_array(),
706 BoolArray::from_iter([true, false, false]),
707 &mut SESSION.create_execution_ctx()
708 );
709 }
710
711 #[test]
712 fn all_null_stat_fn_lowers_to_null_count_field() {
713 let null_count = NullCount.bind(EmptyOptions);
714 let zone_map = ZoneMap::try_new(
715 PType::U64.into(),
716 StructArray::from_fields(&[(
717 null_count.to_string(),
718 PrimitiveArray::new(buffer![4u64, 0, 2], Validity::AllValid).into_array(),
719 )])
720 .unwrap(),
721 Arc::new([null_count]),
722 4,
723 10,
724 )
725 .unwrap();
726 let ctx = &mut SESSION.create_execution_ctx();
727
728 let mask = prune(&zone_map, &all_null(root())).unwrap();
729 assert_arrays_eq!(
730 mask.into_array(),
731 BoolArray::from_iter([true, false, true]),
732 ctx
733 );
734
735 let mask = prune(&zone_map, &all_non_null(root())).unwrap();
736 assert_arrays_eq!(
737 mask.into_array(),
738 BoolArray::from_iter([false, true, false]),
739 ctx
740 );
741 }
742
743 #[test]
744 fn all_nan_stat_fn_lowers_to_nan_count_field() {
745 let nan_count = NanCount.bind(EmptyOptions);
746 let zone_map = ZoneMap::try_new(
747 PType::F32.into(),
748 StructArray::from_fields(&[(
749 nan_count.to_string(),
750 PrimitiveArray::new(buffer![4u64, 0, 2], Validity::AllValid).into_array(),
751 )])
752 .unwrap(),
753 Arc::new([nan_count]),
754 4,
755 10,
756 )
757 .unwrap();
758 let ctx = &mut SESSION.create_execution_ctx();
759
760 let mask = prune(&zone_map, &all_nan(root())).unwrap();
761 assert_arrays_eq!(
762 mask.into_array(),
763 BoolArray::from_iter([true, false, true]),
764 ctx
765 );
766
767 let mask = prune(&zone_map, &all_non_nan(root())).unwrap();
768 assert_arrays_eq!(
769 mask.into_array(),
770 BoolArray::from_iter([false, true, false]),
771 ctx
772 );
773 }
774
775 #[test]
776 fn non_float_nan_stat_fns_fail_to_bind() {
777 let dtype = DType::from(PType::I32);
778 for expr in [all_nan(root()), all_non_nan(root())] {
779 let error = expr.bind(&dtype).unwrap_err();
780 assert!(
781 error
782 .to_string()
783 .contains("does not support input dtype i32"),
784 "{error}"
785 );
786 }
787 }
788
789 #[test]
790 fn unavailable_stat_fn_lowers_to_unknown_mask() {
791 let zone_map = ZoneMap::try_new(
792 PType::U64.into(),
793 StructArray::try_new(FieldNames::empty(), vec![], 3, Validity::NonNullable).unwrap(),
794 Arc::new([]),
795 4,
796 10,
797 )
798 .unwrap();
799 let ctx = &mut SESSION.create_execution_ctx();
800
801 let mask = prune(&zone_map, &all_non_null(root())).unwrap();
802 assert_arrays_eq!(
803 mask.into_array(),
804 BoolArray::from_iter([false, false, false]),
805 ctx
806 );
807
808 let expr = gt(root(), lit(5u64));
809 let pruning_expr = falsify(&expr, PType::U64.into());
810 let mask = zone_map.prune(&pruning_expr, &SESSION).unwrap();
811 assert_arrays_eq!(
812 mask.into_array(),
813 BoolArray::from_iter([false, false, false]),
814 ctx
815 );
816 }
817
818 #[test]
819 fn float_min_max_stat_fn_requires_nan_count() {
820 let max = Max.bind(NumericalAggregateOpts::skip_nans());
821 let zone_map = ZoneMap::try_new(
822 PType::F32.into(),
823 StructArray::from_fields(&[(
824 max.to_string(),
825 PrimitiveArray::new(buffer![5.0f32, 6.0, 7.0], Validity::AllValid).into_array(),
826 )])
827 .unwrap(),
828 Arc::new([max.clone()]),
829 4,
830 12,
831 )
832 .unwrap();
833 let ctx = &mut SESSION.create_execution_ctx();
834
835 let expr = gt(root(), lit(5.0f32));
836 let pruning_expr = falsify(&expr, PType::F32.into());
837 let mask = zone_map.prune(&pruning_expr, &SESSION).unwrap();
838 assert_arrays_eq!(
839 mask.into_array(),
840 BoolArray::from_iter([false, false, false]),
841 ctx
842 );
843
844 let nan_count = NanCount.bind(EmptyOptions);
845 let zone_map = ZoneMap::try_new(
846 PType::F32.into(),
847 StructArray::from_fields(&[
848 (
849 max.to_string(),
850 PrimitiveArray::new(buffer![5.0f32, 6.0, 7.0], Validity::AllValid).into_array(),
851 ),
852 (
853 nan_count.to_string(),
854 PrimitiveArray::new(buffer![0u64, 0, 0], Validity::AllValid).into_array(),
855 ),
856 ])
857 .unwrap(),
858 Arc::new([max, nan_count]),
859 4,
860 12,
861 )
862 .unwrap();
863
864 let mask = zone_map.prune(&pruning_expr, &SESSION).unwrap();
865 assert_arrays_eq!(
866 mask.into_array(),
867 BoolArray::from_iter([true, false, false]),
868 ctx
869 );
870 }
871
872 #[test]
873 fn float_cast_min_max_stat_fn_uses_source_nan_count() {
874 let zone_map = ZoneMap::try_new_legacy(
875 PType::F32.into(),
876 StructArray::from_fields(&[
877 (
878 "max",
879 PrimitiveArray::new(buffer![5.0f32, 5.0], Validity::AllValid).into_array(),
880 ),
881 (
882 "max_is_truncated",
883 BoolArray::from_iter([false, false]).into_array(),
884 ),
885 (
886 "min",
887 PrimitiveArray::new(buffer![5.0f32, 5.0], Validity::AllValid).into_array(),
888 ),
889 (
890 "min_is_truncated",
891 BoolArray::from_iter([false, false]).into_array(),
892 ),
893 (
894 "nan_count",
895 PrimitiveArray::new(buffer![1u64, 0], Validity::AllValid).into_array(),
896 ),
897 ])
898 .unwrap(),
899 Arc::new([Stat::Max, Stat::Min, Stat::NaNCount]),
900 4,
901 8,
902 )
903 .unwrap();
904
905 let cast_dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
906 let expr = not_eq(cast(root(), cast_dtype), lit(5i32));
907 let pruning_expr = falsify(&expr, PType::F32.into());
908
909 let mask = zone_map.prune(&pruning_expr, &SESSION).unwrap();
910 assert_arrays_eq!(
911 mask.into_array(),
912 BoolArray::from_iter([false, true]),
913 &mut SESSION.create_execution_ctx()
914 );
915 }
916
917 #[test]
918 fn fixed_size_list_min_max_stat_fn_lowers_to_unknown_mask() {
919 let elem_dtype = Arc::new(DType::Decimal(
923 DecimalDType::new(10, 2),
924 Nullability::Nullable,
925 ));
926 let column_dtype = DType::FixedSizeList(elem_dtype, 1, Nullability::Nullable);
927
928 let zone_map = ZoneMap::try_new(
929 column_dtype,
930 StructArray::try_new(FieldNames::empty(), vec![], 3, Validity::NonNullable).unwrap(),
931 Arc::new([]),
932 4,
933 10,
934 )
935 .unwrap();
936
937 let max_fn = Stat::Max
938 .aggregate_fn()
939 .expect("max should have an aggregate function");
940 let predicate = is_null(vortex_array::stats::stat(root(), max_fn));
941
942 let mask = prune(&zone_map, &predicate).unwrap();
944 assert_arrays_eq!(
945 mask.into_array(),
946 BoolArray::from_iter([true, true, true]),
947 &mut SESSION.create_execution_ctx()
948 );
949 }
950
951 #[test]
952 fn unsupported_aggregate_input_dtype_errors() {
953 let zone_map = ZoneMap::try_new(
954 DType::Null,
955 StructArray::try_new(FieldNames::empty(), vec![], 3, Validity::NonNullable).unwrap(),
956 Arc::new([]),
957 4,
958 10,
959 )
960 .unwrap();
961
962 let max_fn = Stat::Max
963 .aggregate_fn()
964 .expect("max should have an aggregate function");
965 let predicate = is_null(vortex_array::stats::stat(root(), max_fn));
966 let error = prune(&zone_map, &predicate).unwrap_err();
967
968 assert!(
969 error
970 .to_string()
971 .contains("Aggregate function vortex.max() does not support input dtype null"),
972 "{error}"
973 );
974 }
975
976 #[test]
977 fn row_count_prunes_all_null_uniform_zones() {
978 let zone_map = ZoneMap::try_new_legacy(
979 PType::U64.into(),
980 StructArray::from_fields(&[(
981 "null_count",
982 PrimitiveArray::new(buffer![0u64, 4, 0], Validity::AllValid).into_array(),
983 )])
984 .unwrap(),
985 Arc::new([Stat::NullCount]),
986 4,
987 12,
988 )
989 .unwrap();
990
991 let expr = is_not_null(root());
992 let pruning_expr = falsify(&expr, PType::U64.into());
993
994 let mask = zone_map.prune(&pruning_expr, &SESSION).unwrap();
996 assert_arrays_eq!(
997 mask.into_array(),
998 BoolArray::from_iter([false, true, false]),
999 &mut SESSION.create_execution_ctx()
1000 );
1001 }
1002
1003 #[test]
1004 fn all_null_stat_fn_lowers_to_aggregate_field() {
1005 let all_null_agg = AllNull.bind(EmptyOptions);
1006 let zone_map = ZoneMap::try_new(
1007 PType::U64.into(),
1008 StructArray::from_fields(&[(
1009 all_null_agg.to_string(),
1010 BoolArray::from_iter([Some(false), Some(true), Some(true)]).into_array(),
1011 )])
1012 .unwrap(),
1013 Arc::new([all_null_agg]),
1014 4,
1015 10,
1016 )
1017 .unwrap();
1018
1019 let mask = prune(&zone_map, &all_null(root())).unwrap();
1020 assert_arrays_eq!(
1021 mask.into_array(),
1022 BoolArray::from_iter([false, true, true]),
1023 &mut SESSION.create_execution_ctx()
1024 );
1025 }
1026
1027 #[test]
1028 fn all_non_null_stat_fn_lowers_to_aggregate_field() {
1029 let all_non_null_agg = AllNonNull.bind(EmptyOptions);
1030 let zone_map = ZoneMap::try_new(
1031 PType::U64.into(),
1032 StructArray::from_fields(&[(
1033 all_non_null_agg.to_string(),
1034 BoolArray::from_iter([Some(true), Some(false), Some(false)]).into_array(),
1035 )])
1036 .unwrap(),
1037 Arc::new([all_non_null_agg]),
1038 4,
1039 10,
1040 )
1041 .unwrap();
1042
1043 let mask = prune(&zone_map, &all_non_null(root())).unwrap();
1044 assert_arrays_eq!(
1045 mask.into_array(),
1046 BoolArray::from_iter([true, false, false]),
1047 &mut SESSION.create_execution_ctx()
1048 );
1049 }
1050}