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