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 predicate = self.lower_stats(predicate.clone())?;
150 self.prune_lowered(&predicate, session)
151 }
152
153 pub(super) fn prune_lowered(
159 &self,
160 predicate: &BoundExpression,
161 session: &VortexSession,
162 ) -> VortexResult<Mask> {
163 let mut ctx = session.create_execution_ctx();
164 let num_zones = self.array.len();
165
166 let array = self.array.clone().into_array();
167 let applied = array.apply_bound(predicate)?;
168
169 if !contains_row_count(&applied) {
170 return null_as_false_mask(applied, &mut ctx);
171 }
172
173 let row_count_array = row_count_array(self.zone_len, self.row_count, num_zones)?;
174 let substituted = substitute_row_count(applied, &row_count_array)?;
175 null_as_false_mask(substituted, &mut ctx)
176 }
177
178 pub(super) fn lower_stats(&self, predicate: BoundExpression) -> VortexResult<BoundExpression> {
180 let binder = ZoneMapStatsBinder { zone_map: self };
181 bind_stats(predicate, &binder)
182 }
183}
184
185fn null_as_false_mask(array: ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<Mask> {
190 if !array.dtype().is_nullable() {
191 return array.execute::<Mask>(ctx);
192 }
193 let len = array.len();
194 Ok(match array.execute::<Columnar>(ctx)? {
195 Columnar::Constant(c) => Mask::new(len, c.scalar().as_bool().value().unwrap_or(false)),
196 Columnar::Canonical(c) => {
197 let bool = c.into_array().execute::<BoolArray>(ctx)?;
198 match bool.as_ref().validity()? {
199 Validity::NonNullable | Validity::AllValid => {
200 Mask::from_buffer(bool.into_bit_buffer())
201 }
202 Validity::AllInvalid => Mask::new_false(len),
203 Validity::Array(v) => {
204 let validity_bits = v.execute::<BoolArray>(ctx)?.into_bit_buffer();
205 Mask::from_buffer(bool.into_bit_buffer() & &validity_bits)
206 }
207 }
208 }
209 })
210}
211
212struct ZoneMapStatsBinder<'a> {
213 zone_map: &'a ZoneMap,
214}
215
216impl StatBinder for ZoneMapStatsBinder<'_> {
217 fn bind_aggregate(
218 &self,
219 input: &BoundExpression,
220 aggregate_fn: &AggregateFnRef,
221 _stat_dtype: &DType,
222 ) -> VortexResult<Option<BoundExpression>> {
223 if !input.is_root() {
224 return Ok(None);
225 }
226 vortex_ensure_eq!(
227 input.dtype(),
228 &self.zone_map.column_dtype,
229 "Stats predicate root dtype does not match zone-map column dtype"
230 );
231
232 if let Some(stat_expr) = self.zone_map.aggregate_field_expr(aggregate_fn) {
233 return Ok(Some(self.bind_target(stat_expr)?));
234 }
235
236 if aggregate_fn.is::<AllNull>() {
237 return self
238 .zone_map
239 .stat_field_expr(Stat::NullCount)
240 .map(|null_count| self.bind_target(eq(null_count, row_count_expr())))
241 .transpose();
242 }
243
244 if aggregate_fn.is::<AllNonNull>() {
245 return self
246 .zone_map
247 .stat_field_expr(Stat::NullCount)
248 .map(|null_count| self.bind_target(eq(null_count, lit(0u64))))
249 .transpose();
250 }
251
252 if aggregate_fn.is::<AllNan>() {
253 return self
254 .zone_map
255 .stat_field_expr(Stat::NaNCount)
256 .map(|nan_count| self.bind_target(eq(nan_count, row_count_expr())))
257 .transpose();
258 }
259
260 if aggregate_fn.is::<AllNonNan>() {
261 return self
262 .zone_map
263 .stat_field_expr(Stat::NaNCount)
264 .map(|nan_count| self.bind_target(eq(nan_count, lit(0u64))))
265 .transpose();
266 }
267
268 if let Some(stat) = Stat::from_aggregate_fn(aggregate_fn) {
269 return self
270 .zone_map
271 .stat_field_expr(stat)
272 .map(|expr| self.bind_target(expr))
273 .transpose();
274 }
275
276 Ok(None)
277 }
278}
279
280impl ZoneMapStatsBinder<'_> {
281 fn bind_target(&self, expr: Expression) -> VortexResult<BoundExpression> {
282 expr.bind(self.zone_map.array.dtype())
283 }
284}
285
286impl ZoneMap {
287 fn aggregate_field_expr(&self, requested: &AggregateFnRef) -> Option<Expression> {
288 let field_name = requested.to_string();
289 if self.array.unmasked_field_by_name_opt(&field_name).is_some() {
290 return Some(aggregate_result_expr(
291 requested,
292 get_item(field_name, root()),
293 ));
294 }
295
296 let mut approximate = None;
297 for stored in self.aggregate_fns.iter() {
298 let field_name = stored.to_string();
299 if self.array.unmasked_field_by_name_opt(&field_name).is_none() {
300 continue;
301 }
302
303 match stored.can_satisfy(requested) {
304 AggregateFnSatisfaction::Exact => {
305 return Some(aggregate_result_expr(stored, get_item(field_name, root())));
306 }
307 AggregateFnSatisfaction::Approximate => {
308 approximate = Some(aggregate_result_expr(stored, get_item(field_name, root())));
309 }
310 AggregateFnSatisfaction::No => {}
311 }
312 }
313
314 approximate
315 }
316
317 fn stat_field_expr(&self, stat: Stat) -> Option<Expression> {
318 if let Some(aggregate_fn) = stat.aggregate_fn()
319 && let Some(expr) = self.aggregate_field_expr(&aggregate_fn)
320 {
321 return Some(expr);
322 }
323
324 self.legacy_stat_field_expr(stat)
325 }
326
327 fn legacy_stat_field_expr(&self, stat: Stat) -> Option<Expression> {
328 if self.array.unmasked_field_by_name_opt(stat.name()).is_some() {
329 return Some(get_item(stat.name(), root()));
330 }
331
332 None
333 }
334}
335
336fn aggregate_result_expr(stored: &AggregateFnRef, state_expr: Expression) -> Expression {
337 if stored.is::<BoundedMax>() {
338 get_item(BOUNDED_MAX_BOUND, state_expr)
339 } else {
340 state_expr
341 }
342}
343
344fn row_count_expr() -> Expression {
345 RowCount.new_expr(EmptyOptions, [])
346}
347
348fn row_count_array(zone_len: u64, row_count: u64, num_zones: usize) -> VortexResult<ArrayRef> {
358 if num_zones == 0 {
359 return Ok(ConstantArray::new(0u64, 0).into_array());
360 }
361
362 let leading_rows = zone_len.saturating_mul((num_zones as u64) - 1);
363 let Some(last_zone_len) = row_count.checked_sub(leading_rows) else {
364 vortex_bail!(
365 "Zone map declares {num_zones} zones of {zone_len} rows, which is more than the \
366 {row_count} rows of the layout"
367 );
368 };
369 if num_zones == 1 || last_zone_len == zone_len {
370 return Ok(ConstantArray::new(last_zone_len, num_zones).into_array());
371 }
372
373 let ends = unsafe {
374 PrimitiveArray::new_unchecked(
375 buffer![num_zones as u64 - 1, num_zones as u64],
376 Validity::NonNullable,
377 )
378 }
379 .into_array();
380 let values = unsafe {
381 PrimitiveArray::new_unchecked(buffer![zone_len, last_zone_len], Validity::NonNullable)
382 }
383 .into_array();
384
385 Ok(unsafe { RunEnd::new_unchecked(ends, values, 0, num_zones) }.into_array())
388}
389
390#[cfg(test)]
391mod tests {
392 use std::num::NonZeroUsize;
393 use std::sync::Arc;
394 use std::sync::atomic::AtomicI32;
395 use std::sync::atomic::Ordering;
396
397 use rstest::rstest;
398 use vortex_array::IntoArray;
399 use vortex_array::VortexSessionExecute;
400 use vortex_array::aggregate_fn::AggregateFnVTableExt;
401 use vortex_array::aggregate_fn::EmptyOptions;
402 use vortex_array::aggregate_fn::NumericalAggregateOpts;
403 use vortex_array::aggregate_fn::fns::all_non_null::AllNonNull;
404 use vortex_array::aggregate_fn::fns::all_null::AllNull;
405 use vortex_array::aggregate_fn::fns::bounded_max::BOUNDED_MAX_BOUND;
406 use vortex_array::aggregate_fn::fns::bounded_max::BOUNDED_MAX_UNKNOWN;
407 use vortex_array::aggregate_fn::fns::bounded_max::BoundedMax;
408 use vortex_array::aggregate_fn::fns::bounded_max::BoundedMaxOptions;
409 use vortex_array::aggregate_fn::fns::bounded_min::BoundedMin;
410 use vortex_array::aggregate_fn::fns::bounded_min::BoundedMinOptions;
411 use vortex_array::aggregate_fn::fns::max::Max;
412 use vortex_array::aggregate_fn::fns::min::Min;
413 use vortex_array::aggregate_fn::fns::nan_count::NanCount;
414 use vortex_array::aggregate_fn::fns::null_count::NullCount;
415 use vortex_array::arrays::BoolArray;
416 use vortex_array::arrays::PrimitiveArray;
417 use vortex_array::arrays::StructArray;
418 use vortex_array::assert_arrays_eq;
419 use vortex_array::dtype::DType;
420 use vortex_array::dtype::DecimalDType;
421 use vortex_array::dtype::FieldNames;
422 use vortex_array::dtype::Nullability;
423 use vortex_array::dtype::PType;
424 use vortex_array::expr::BoundExpression;
425 use vortex_array::expr::Expression;
426 use vortex_array::expr::cast;
427 use vortex_array::expr::dynamic;
428 use vortex_array::expr::gt;
429 use vortex_array::expr::gt_eq;
430 use vortex_array::expr::is_not_null;
431 use vortex_array::expr::is_null;
432 use vortex_array::expr::lit;
433 use vortex_array::expr::lt;
434 use vortex_array::expr::not_eq;
435 use vortex_array::expr::root;
436 use vortex_array::expr::stats::Stat;
437 use vortex_array::scalar::ScalarValue;
438 use vortex_array::scalar_fn::fns::operators::CompareOperator;
439 use vortex_array::stats::all_nan;
440 use vortex_array::stats::all_non_nan;
441 use vortex_array::stats::all_non_null;
442 use vortex_array::stats::all_null;
443 use vortex_array::validity::Validity;
444 use vortex_buffer::Buffer;
445 use vortex_buffer::buffer;
446 use vortex_error::VortexResult;
447 use vortex_mask::Mask;
448
449 use crate::layouts::zoned::zone_map::ZoneMap;
450 use crate::test::SESSION;
451
452 fn falsify(expr: &Expression, dtype: DType) -> BoundExpression {
453 expr.bind(&dtype)
454 .unwrap()
455 .falsify(&SESSION)
456 .unwrap()
457 .unwrap()
458 }
459
460 fn prune(zone_map: &ZoneMap, predicate: &Expression) -> VortexResult<Mask> {
461 zone_map.prune(&predicate.bind(&zone_map.column_dtype)?, &SESSION)
462 }
463
464 fn default_bounded_stat_max_bytes() -> NonZeroUsize {
465 unsafe { NonZeroUsize::new_unchecked(64) }
467 }
468
469 #[test]
470 fn test_zone_map_prunes() {
471 let max = Max.bind(NumericalAggregateOpts::skip_nans());
483 let min = Min.bind(NumericalAggregateOpts::skip_nans());
484 let zone_map = ZoneMap::try_new(
485 PType::I32.into(),
486 StructArray::from_fields(&[
487 (
488 max.to_string(),
489 PrimitiveArray::new(buffer![5i32, 6i32, 7i32], Validity::AllValid).into_array(),
490 ),
491 (
492 min.to_string(),
493 PrimitiveArray::new(buffer![1i32, 2i32, 3i32], Validity::AllValid).into_array(),
494 ),
495 ])
496 .unwrap(),
497 Arc::new([max, min]),
498 3,
499 10,
500 )
501 .unwrap();
502 let ctx = &mut SESSION.create_execution_ctx();
503
504 let expr = gt_eq(root(), lit(6i32));
507 let pruning_expr = falsify(&expr, PType::I32.into());
508 let mask = zone_map.prune(&pruning_expr, &SESSION).unwrap();
509 assert_arrays_eq!(
510 mask.into_array(),
511 BoolArray::from_iter([true, false, false]),
512 ctx
513 );
514
515 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));
529 let pruning_expr = falsify(&expr, PType::I32.into());
530 let mask = zone_map.prune(&pruning_expr, &SESSION).unwrap();
531 assert_arrays_eq!(
532 mask.into_array(),
533 BoolArray::from_iter([false, true, true]),
534 ctx
535 );
536 }
537
538 #[test]
539 fn bounded_display_names_satisfy_min_max_rewrites() {
540 let bounded_max = BoundedMax.bind(BoundedMaxOptions {
541 max_bytes: default_bounded_stat_max_bytes(),
542 });
543 let bounded_min = BoundedMin.bind(BoundedMinOptions {
544 max_bytes: default_bounded_stat_max_bytes(),
545 });
546 let zone_map = ZoneMap::try_new(
547 PType::I32.into(),
548 StructArray::from_fields(&[
549 (
550 bounded_max.to_string(),
551 StructArray::try_new(
552 [BOUNDED_MAX_BOUND, BOUNDED_MAX_UNKNOWN].into(),
553 vec![
554 PrimitiveArray::new(buffer![5i32, 6i32, 7i32], Validity::AllValid)
555 .into_array(),
556 BoolArray::from_iter([false, false, false]).into_array(),
557 ],
558 3,
559 Validity::AllValid,
560 )
561 .unwrap()
562 .into_array(),
563 ),
564 (
565 bounded_min.to_string(),
566 PrimitiveArray::new(buffer![1i32, 2i32, 3i32], Validity::AllValid).into_array(),
567 ),
568 ])
569 .unwrap(),
570 Arc::new([bounded_max, bounded_min]),
571 3,
572 10,
573 )
574 .unwrap();
575 let ctx = &mut SESSION.create_execution_ctx();
576
577 let expr = gt(root(), lit(5i32));
578 let pruning_expr = falsify(&expr, PType::I32.into());
579 let mask = zone_map.prune(&pruning_expr, &SESSION).unwrap();
580 assert_arrays_eq!(
581 mask.into_array(),
582 BoolArray::from_iter([true, false, false]),
583 ctx
584 );
585
586 let expr = lt(root(), lit(2i32));
587 let pruning_expr = falsify(&expr, PType::I32.into());
588 let mask = zone_map.prune(&pruning_expr, &SESSION).unwrap();
589 assert_arrays_eq!(
590 mask.into_array(),
591 BoolArray::from_iter([false, true, true]),
592 ctx
593 );
594 }
595
596 #[test]
597 fn row_count_prunes_short_trailing_zone() {
598 let zone_map = ZoneMap::try_new_legacy(
599 PType::U64.into(),
600 StructArray::from_fields(&[(
601 "null_count",
602 PrimitiveArray::new(buffer![0u64, 0, 2], Validity::AllValid).into_array(),
603 )])
604 .unwrap(),
605 Arc::new([Stat::NullCount]),
606 4,
607 10,
608 )
609 .unwrap();
610
611 let expr = is_not_null(root());
612 let pruning_expr = falsify(&expr, PType::U64.into());
613
614 let mask = zone_map.prune(&pruning_expr, &SESSION).unwrap();
615 assert_arrays_eq!(
616 mask.into_array(),
617 BoolArray::from_iter([false, false, true]),
618 &mut SESSION.create_execution_ctx()
619 );
620 }
621
622 #[rstest]
626 #[case::zones_overrun_row_count(4, 4, 10)]
629 #[case::leading_zones_overrun(8, 3, 10)]
631 #[case::no_rows_but_zones_present(4, 2, 0)]
633 fn row_count_rejects_zones_overrunning_the_layout(
634 #[case] zone_len: u64,
635 #[case] num_zones: usize,
636 #[case] row_count: u64,
637 ) {
638 let null_counts = PrimitiveArray::new(
639 Buffer::<u64>::from_iter(std::iter::repeat_n(0u64, num_zones)),
640 Validity::AllValid,
641 )
642 .into_array();
643 let zone_map = ZoneMap::try_new_legacy(
644 PType::U64.into(),
645 StructArray::from_fields(&[("null_count", null_counts)]).unwrap(),
646 Arc::new([Stat::NullCount]),
647 zone_len,
648 row_count,
649 )
650 .unwrap();
651
652 let pruning_expr = falsify(&is_not_null(root()), PType::U64.into());
653 let message = zone_map
654 .prune(&pruning_expr, &SESSION)
655 .err()
656 .map(|err| err.to_string())
657 .unwrap_or_default();
658 assert!(
659 message.contains("more than the"),
660 "zone_len {zone_len} x {num_zones} zones over {row_count} rows should have been \
661 rejected, got: {message}"
662 );
663 }
664
665 #[test]
666 fn row_count_substitution_handles_empty_zone_map() {
667 let zone_map = ZoneMap::try_new_legacy(
668 PType::U64.into(),
669 StructArray::from_fields(&[(
670 "null_count",
671 PrimitiveArray::new::<u64>(buffer![], Validity::AllValid).into_array(),
672 )])
673 .unwrap(),
674 Arc::new([Stat::NullCount]),
675 4,
676 0,
677 )
678 .unwrap();
679
680 let expr = is_not_null(root());
681 let pruning_expr = falsify(&expr, PType::U64.into());
682
683 let mask = zone_map.prune(&pruning_expr, &SESSION).unwrap();
684 assert_eq!(mask.len(), 0);
685 }
686
687 #[test]
688 fn is_null_falsification_uses_null_count() {
689 let zone_map = ZoneMap::try_new_legacy(
690 PType::U64.into(),
691 StructArray::from_fields(&[(
692 "null_count",
693 PrimitiveArray::new(buffer![0u64, 4, 2], Validity::AllValid).into_array(),
694 )])
695 .unwrap(),
696 Arc::new([Stat::NullCount]),
697 4,
698 10,
699 )
700 .unwrap();
701
702 let expr = is_null(root());
703 let pruning_expr = falsify(&expr, PType::U64.into());
704
705 let mask = zone_map.prune(&pruning_expr, &SESSION).unwrap();
706 assert_arrays_eq!(
707 mask.into_array(),
708 BoolArray::from_iter([true, false, false]),
709 &mut SESSION.create_execution_ctx()
710 );
711 }
712
713 #[test]
714 fn all_null_stat_fn_lowers_to_null_count_and_row_count() {
715 let zone_map = ZoneMap::try_new_legacy(
716 PType::U64.into(),
717 StructArray::from_fields(&[(
718 "null_count",
719 PrimitiveArray::new(buffer![0u64, 4, 2], Validity::AllValid).into_array(),
720 )])
721 .unwrap(),
722 Arc::new([Stat::NullCount]),
723 4,
724 10,
725 )
726 .unwrap();
727
728 let mask = prune(&zone_map, &all_null(root())).unwrap();
729 assert_arrays_eq!(
730 mask.into_array(),
731 BoolArray::from_iter([false, true, true]),
732 &mut SESSION.create_execution_ctx()
733 );
734 }
735
736 #[test]
737 fn all_non_null_stat_fn_lowers_to_null_count() {
738 let zone_map = ZoneMap::try_new_legacy(
739 PType::U64.into(),
740 StructArray::from_fields(&[(
741 "null_count",
742 PrimitiveArray::new(buffer![0u64, 4, 2], Validity::AllValid).into_array(),
743 )])
744 .unwrap(),
745 Arc::new([Stat::NullCount]),
746 4,
747 10,
748 )
749 .unwrap();
750
751 let mask = prune(&zone_map, &all_non_null(root())).unwrap();
752 assert_arrays_eq!(
753 mask.into_array(),
754 BoolArray::from_iter([true, false, false]),
755 &mut SESSION.create_execution_ctx()
756 );
757 }
758
759 #[test]
760 fn all_null_stat_fn_lowers_to_null_count_field() {
761 let null_count = NullCount.bind(EmptyOptions);
762 let zone_map = ZoneMap::try_new(
763 PType::U64.into(),
764 StructArray::from_fields(&[(
765 null_count.to_string(),
766 PrimitiveArray::new(buffer![4u64, 0, 2], Validity::AllValid).into_array(),
767 )])
768 .unwrap(),
769 Arc::new([null_count]),
770 4,
771 10,
772 )
773 .unwrap();
774 let ctx = &mut SESSION.create_execution_ctx();
775
776 let mask = prune(&zone_map, &all_null(root())).unwrap();
777 assert_arrays_eq!(
778 mask.into_array(),
779 BoolArray::from_iter([true, false, true]),
780 ctx
781 );
782
783 let mask = prune(&zone_map, &all_non_null(root())).unwrap();
784 assert_arrays_eq!(
785 mask.into_array(),
786 BoolArray::from_iter([false, true, false]),
787 ctx
788 );
789 }
790
791 #[test]
792 fn all_nan_stat_fn_lowers_to_nan_count_field() {
793 let nan_count = NanCount.bind(EmptyOptions);
794 let zone_map = ZoneMap::try_new(
795 PType::F32.into(),
796 StructArray::from_fields(&[(
797 nan_count.to_string(),
798 PrimitiveArray::new(buffer![4u64, 0, 2], Validity::AllValid).into_array(),
799 )])
800 .unwrap(),
801 Arc::new([nan_count]),
802 4,
803 10,
804 )
805 .unwrap();
806 let ctx = &mut SESSION.create_execution_ctx();
807
808 let mask = prune(&zone_map, &all_nan(root())).unwrap();
809 assert_arrays_eq!(
810 mask.into_array(),
811 BoolArray::from_iter([true, false, true]),
812 ctx
813 );
814
815 let mask = prune(&zone_map, &all_non_nan(root())).unwrap();
816 assert_arrays_eq!(
817 mask.into_array(),
818 BoolArray::from_iter([false, true, false]),
819 ctx
820 );
821 }
822
823 #[test]
824 fn non_float_nan_stat_fns_fail_to_bind() {
825 let dtype = DType::from(PType::I32);
826 for expr in [all_nan(root()), all_non_nan(root())] {
827 let error = expr.bind(&dtype).unwrap_err();
828 assert!(
829 error
830 .to_string()
831 .contains("does not support input dtype i32"),
832 "{error}"
833 );
834 }
835 }
836
837 #[test]
838 fn unavailable_stat_fn_lowers_to_unknown_mask() {
839 let zone_map = ZoneMap::try_new(
840 PType::U64.into(),
841 StructArray::try_new(FieldNames::empty(), vec![], 3, Validity::NonNullable).unwrap(),
842 Arc::new([]),
843 4,
844 10,
845 )
846 .unwrap();
847 let ctx = &mut SESSION.create_execution_ctx();
848
849 let mask = prune(&zone_map, &all_non_null(root())).unwrap();
850 assert_arrays_eq!(
851 mask.into_array(),
852 BoolArray::from_iter([false, false, false]),
853 ctx
854 );
855
856 let expr = gt(root(), lit(5u64));
857 let pruning_expr = falsify(&expr, PType::U64.into());
858 let mask = zone_map.prune(&pruning_expr, &SESSION).unwrap();
859 assert_arrays_eq!(
860 mask.into_array(),
861 BoolArray::from_iter([false, false, false]),
862 ctx
863 );
864 }
865
866 #[test]
867 fn float_min_max_stat_fn_requires_nan_count() {
868 let max = Max.bind(NumericalAggregateOpts::skip_nans());
869 let zone_map = ZoneMap::try_new(
870 PType::F32.into(),
871 StructArray::from_fields(&[(
872 max.to_string(),
873 PrimitiveArray::new(buffer![5.0f32, 6.0, 7.0], Validity::AllValid).into_array(),
874 )])
875 .unwrap(),
876 Arc::new([max.clone()]),
877 4,
878 12,
879 )
880 .unwrap();
881 let ctx = &mut SESSION.create_execution_ctx();
882
883 let expr = gt(root(), lit(5.0f32));
884 let pruning_expr = falsify(&expr, PType::F32.into());
885 let mask = zone_map.prune(&pruning_expr, &SESSION).unwrap();
886 assert_arrays_eq!(
887 mask.into_array(),
888 BoolArray::from_iter([false, false, false]),
889 ctx
890 );
891
892 let nan_count = NanCount.bind(EmptyOptions);
893 let zone_map = ZoneMap::try_new(
894 PType::F32.into(),
895 StructArray::from_fields(&[
896 (
897 max.to_string(),
898 PrimitiveArray::new(buffer![5.0f32, 6.0, 7.0], Validity::AllValid).into_array(),
899 ),
900 (
901 nan_count.to_string(),
902 PrimitiveArray::new(buffer![0u64, 0, 0], Validity::AllValid).into_array(),
903 ),
904 ])
905 .unwrap(),
906 Arc::new([max, nan_count]),
907 4,
908 12,
909 )
910 .unwrap();
911
912 let mask = zone_map.prune(&pruning_expr, &SESSION).unwrap();
913 assert_arrays_eq!(
914 mask.into_array(),
915 BoolArray::from_iter([true, false, false]),
916 ctx
917 );
918 }
919
920 #[test]
921 fn float_cast_min_max_stat_fn_uses_source_nan_count() {
922 let zone_map = ZoneMap::try_new_legacy(
923 PType::F32.into(),
924 StructArray::from_fields(&[
925 (
926 "max",
927 PrimitiveArray::new(buffer![5.0f32, 5.0], Validity::AllValid).into_array(),
928 ),
929 (
930 "max_is_truncated",
931 BoolArray::from_iter([false, false]).into_array(),
932 ),
933 (
934 "min",
935 PrimitiveArray::new(buffer![5.0f32, 5.0], Validity::AllValid).into_array(),
936 ),
937 (
938 "min_is_truncated",
939 BoolArray::from_iter([false, false]).into_array(),
940 ),
941 (
942 "nan_count",
943 PrimitiveArray::new(buffer![1u64, 0], Validity::AllValid).into_array(),
944 ),
945 ])
946 .unwrap(),
947 Arc::new([Stat::Max, Stat::Min, Stat::NaNCount]),
948 4,
949 8,
950 )
951 .unwrap();
952
953 let cast_dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
954 let expr = not_eq(cast(root(), cast_dtype), lit(5i32));
955 let pruning_expr = falsify(&expr, PType::F32.into());
956
957 let mask = zone_map.prune(&pruning_expr, &SESSION).unwrap();
958 assert_arrays_eq!(
959 mask.into_array(),
960 BoolArray::from_iter([false, true]),
961 &mut SESSION.create_execution_ctx()
962 );
963 }
964
965 #[test]
966 fn fixed_size_list_min_max_stat_fn_lowers_to_unknown_mask() {
967 let elem_dtype = Arc::new(DType::Decimal(
971 DecimalDType::new(10, 2),
972 Nullability::Nullable,
973 ));
974 let column_dtype = DType::FixedSizeList(elem_dtype, 1, Nullability::Nullable);
975
976 let zone_map = ZoneMap::try_new(
977 column_dtype,
978 StructArray::try_new(FieldNames::empty(), vec![], 3, Validity::NonNullable).unwrap(),
979 Arc::new([]),
980 4,
981 10,
982 )
983 .unwrap();
984
985 let max_fn = Stat::Max
986 .aggregate_fn()
987 .expect("max should have an aggregate function");
988 let predicate = is_null(vortex_array::stats::stat(root(), max_fn));
989
990 let mask = prune(&zone_map, &predicate).unwrap();
992 assert_arrays_eq!(
993 mask.into_array(),
994 BoolArray::from_iter([true, true, true]),
995 &mut SESSION.create_execution_ctx()
996 );
997 }
998
999 #[test]
1000 fn unsupported_aggregate_input_dtype_errors() {
1001 let zone_map = ZoneMap::try_new(
1002 DType::Null,
1003 StructArray::try_new(FieldNames::empty(), vec![], 3, Validity::NonNullable).unwrap(),
1004 Arc::new([]),
1005 4,
1006 10,
1007 )
1008 .unwrap();
1009
1010 let max_fn = Stat::Max
1011 .aggregate_fn()
1012 .expect("max should have an aggregate function");
1013 let predicate = is_null(vortex_array::stats::stat(root(), max_fn));
1014 let error = prune(&zone_map, &predicate).unwrap_err();
1015
1016 assert!(
1017 error
1018 .to_string()
1019 .contains("Aggregate function vortex.max() does not support input dtype null"),
1020 "{error}"
1021 );
1022 }
1023
1024 #[test]
1025 fn row_count_prunes_all_null_uniform_zones() {
1026 let zone_map = ZoneMap::try_new_legacy(
1027 PType::U64.into(),
1028 StructArray::from_fields(&[(
1029 "null_count",
1030 PrimitiveArray::new(buffer![0u64, 4, 0], Validity::AllValid).into_array(),
1031 )])
1032 .unwrap(),
1033 Arc::new([Stat::NullCount]),
1034 4,
1035 12,
1036 )
1037 .unwrap();
1038
1039 let expr = is_not_null(root());
1040 let pruning_expr = falsify(&expr, PType::U64.into());
1041
1042 let mask = zone_map.prune(&pruning_expr, &SESSION).unwrap();
1044 assert_arrays_eq!(
1045 mask.into_array(),
1046 BoolArray::from_iter([false, true, false]),
1047 &mut SESSION.create_execution_ctx()
1048 );
1049 }
1050
1051 #[test]
1052 fn all_null_stat_fn_lowers_to_aggregate_field() {
1053 let all_null_agg = AllNull.bind(EmptyOptions);
1054 let zone_map = ZoneMap::try_new(
1055 PType::U64.into(),
1056 StructArray::from_fields(&[(
1057 all_null_agg.to_string(),
1058 BoolArray::from_iter([Some(false), Some(true), Some(true)]).into_array(),
1059 )])
1060 .unwrap(),
1061 Arc::new([all_null_agg]),
1062 4,
1063 10,
1064 )
1065 .unwrap();
1066
1067 let mask = prune(&zone_map, &all_null(root())).unwrap();
1068 assert_arrays_eq!(
1069 mask.into_array(),
1070 BoolArray::from_iter([false, true, true]),
1071 &mut SESSION.create_execution_ctx()
1072 );
1073 }
1074
1075 #[test]
1076 fn all_non_null_stat_fn_lowers_to_aggregate_field() {
1077 let all_non_null_agg = AllNonNull.bind(EmptyOptions);
1078 let zone_map = ZoneMap::try_new(
1079 PType::U64.into(),
1080 StructArray::from_fields(&[(
1081 all_non_null_agg.to_string(),
1082 BoolArray::from_iter([Some(true), Some(false), Some(false)]).into_array(),
1083 )])
1084 .unwrap(),
1085 Arc::new([all_non_null_agg]),
1086 4,
1087 10,
1088 )
1089 .unwrap();
1090
1091 let mask = prune(&zone_map, &all_non_null(root())).unwrap();
1092 assert_arrays_eq!(
1093 mask.into_array(),
1094 BoolArray::from_iter([true, false, false]),
1095 &mut SESSION.create_execution_ctx()
1096 );
1097 }
1098
1099 #[test]
1100 fn lowered_dynamic_predicate_tracks_rhs_updates() -> VortexResult<()> {
1101 let max = Max.bind(NumericalAggregateOpts::skip_nans());
1102 let min = Min.bind(NumericalAggregateOpts::skip_nans());
1103 let zone_map = ZoneMap::try_new(
1104 PType::I32.into(),
1105 StructArray::from_fields(&[
1106 (
1107 max.to_string(),
1108 PrimitiveArray::new(buffer![5i32, 6i32, 7i32], Validity::AllValid).into_array(),
1109 ),
1110 (
1111 min.to_string(),
1112 PrimitiveArray::new(buffer![1i32, 2i32, 3i32], Validity::AllValid).into_array(),
1113 ),
1114 ])?,
1115 Arc::new([max, min]),
1116 3,
1117 10,
1118 )?;
1119 let ctx = &mut SESSION.create_execution_ctx();
1120
1121 let threshold = Arc::new(AtomicI32::new(5));
1122 let rhs = Arc::clone(&threshold);
1123 let expr = dynamic(
1124 CompareOperator::Gt,
1125 move || Some(ScalarValue::from(rhs.load(Ordering::Relaxed))),
1126 PType::I32.into(),
1127 true,
1128 root(),
1129 );
1130
1131 let lowered = zone_map.lower_stats(falsify(&expr, PType::I32.into()))?;
1134
1135 let mask = zone_map.prune_lowered(&lowered, &SESSION)?;
1137 assert_arrays_eq!(
1138 mask.into_array(),
1139 BoolArray::from_iter([true, false, false]),
1140 ctx
1141 );
1142
1143 threshold.store(6, Ordering::Relaxed);
1145 let mask = zone_map.prune_lowered(&lowered, &SESSION)?;
1146 assert_arrays_eq!(
1147 mask.into_array(),
1148 BoolArray::from_iter([true, true, false]),
1149 ctx
1150 );
1151
1152 Ok(())
1153 }
1154}