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