Skip to main content

vortex_array/stats/
expr.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4//! Expression constructors for statistics backed by aggregate functions.
5
6use vortex_error::VortexExpect;
7
8use crate::aggregate_fn::AggregateFnRef;
9use crate::aggregate_fn::AggregateFnVTableExt;
10use crate::aggregate_fn::EmptyOptions;
11use crate::aggregate_fn::NumericalAggregateOpts;
12use crate::aggregate_fn::fns::all_nan::AllNan;
13use crate::aggregate_fn::fns::all_non_nan::AllNonNan;
14use crate::aggregate_fn::fns::all_non_null::AllNonNull;
15use crate::aggregate_fn::fns::all_null::AllNull;
16use crate::aggregate_fn::fns::min_max::MinMax;
17use crate::aggregate_fn::fns::nan_count::NanCount;
18use crate::aggregate_fn::fns::null_count::NullCount;
19use crate::aggregate_fn::fns::sum::Sum;
20use crate::expr::BoundExpression;
21use crate::expr::Expression;
22use crate::scalar_fn::ScalarFnVTableExt;
23pub use crate::scalar_fn::fns::stat::StatFn;
24pub use crate::scalar_fn::fns::stat::StatOptions;
25
26/// Creates an expression that reads a stored aggregate statistic for `expr`.
27///
28/// If the statistic is not available in the current stats scope, evaluating the expression returns
29/// a nullable all-null array with the aggregate return type.
30pub fn stat(expr: Expression, aggregate_fn: AggregateFnRef) -> Expression {
31    StatFn.new_expr(StatOptions::new(aggregate_fn), [expr])
32}
33
34fn bound_stat(expr: BoundExpression, aggregate_fn: AggregateFnRef) -> BoundExpression {
35    StatFn
36        .try_new_bound_expr(StatOptions::new(aggregate_fn), [expr])
37        .vortex_expect("stat expressions must use an aggregate supported by the child dtype")
38}
39
40/// Creates `stat(expr, min_max)`, returning a nullable `{ min, max }` struct statistic.
41pub fn min_max(expr: Expression) -> Expression {
42    // Statistics follow NaN-skipping semantics; request it explicitly rather than via the default.
43    stat(expr, MinMax.bind(NumericalAggregateOpts::skip_nans()))
44}
45
46fn bound_min_max(expr: BoundExpression) -> BoundExpression {
47    bound_stat(expr, MinMax.bind(NumericalAggregateOpts::skip_nans()))
48}
49
50/// Creates `stat(expr, sum)`, returning a nullable sum statistic.
51pub fn sum(expr: Expression) -> Expression {
52    // Statistics follow NaN-skipping semantics; request it explicitly rather than via the default.
53    stat(expr, Sum.bind(NumericalAggregateOpts::skip_nans()))
54}
55
56fn bound_sum(expr: BoundExpression) -> BoundExpression {
57    bound_stat(expr, Sum.bind(NumericalAggregateOpts::skip_nans()))
58}
59
60/// Creates `stat(expr, null_count)`, returning a nullable null-count statistic.
61pub fn null_count(expr: Expression) -> Expression {
62    stat(expr, NullCount.bind(EmptyOptions))
63}
64
65fn bound_null_count(expr: BoundExpression) -> BoundExpression {
66    bound_stat(expr, NullCount.bind(EmptyOptions))
67}
68
69/// Creates `stat(expr, all_null)`, returning a nullable all-null statistic.
70pub fn all_null(expr: Expression) -> Expression {
71    stat(expr, AllNull.bind(EmptyOptions))
72}
73
74fn bound_all_null(expr: BoundExpression) -> BoundExpression {
75    bound_stat(expr, AllNull.bind(EmptyOptions))
76}
77
78/// Creates `stat(expr, all_nan)`, returning a nullable all-NaN statistic.
79pub fn all_nan(expr: Expression) -> Expression {
80    stat(expr, AllNan.bind(EmptyOptions))
81}
82
83fn bound_all_nan(expr: BoundExpression) -> BoundExpression {
84    bound_stat(expr, AllNan.bind(EmptyOptions))
85}
86
87/// Creates `stat(expr, all_non_null)`, returning a nullable all-non-null statistic.
88pub fn all_non_null(expr: Expression) -> Expression {
89    stat(expr, AllNonNull.bind(EmptyOptions))
90}
91
92fn bound_all_non_null(expr: BoundExpression) -> BoundExpression {
93    bound_stat(expr, AllNonNull.bind(EmptyOptions))
94}
95
96/// Creates `stat(expr, all_non_nan)`, returning a nullable all-non-NaN statistic.
97pub fn all_non_nan(expr: Expression) -> Expression {
98    stat(expr, AllNonNan.bind(EmptyOptions))
99}
100
101fn bound_all_non_nan(expr: BoundExpression) -> BoundExpression {
102    bound_stat(expr, AllNonNan.bind(EmptyOptions))
103}
104
105/// Creates `stat(expr, nan_count)`, returning a nullable NaN-count statistic.
106pub fn nan_count(expr: Expression) -> Expression {
107    stat(expr, NanCount.bind(EmptyOptions))
108}
109
110fn bound_nan_count(expr: BoundExpression) -> BoundExpression {
111    bound_stat(expr, NanCount.bind(EmptyOptions))
112}
113
114/// Constructors for statistic expressions whose input has already been bound.
115///
116/// These mirror the constructors in [`crate::stats`] and panic when the aggregate does not support
117/// the input dtype.
118pub mod bound {
119    use crate::aggregate_fn::AggregateFnRef;
120    use crate::expr::BoundExpression;
121
122    /// Creates a bound expression that reads a stored aggregate statistic.
123    pub fn stat(expr: BoundExpression, aggregate_fn: AggregateFnRef) -> BoundExpression {
124        super::bound_stat(expr, aggregate_fn)
125    }
126
127    /// Creates a bound nullable `{ min, max }` statistic expression.
128    pub fn min_max(expr: BoundExpression) -> BoundExpression {
129        super::bound_min_max(expr)
130    }
131
132    /// Creates a bound nullable sum statistic expression.
133    pub fn sum(expr: BoundExpression) -> BoundExpression {
134        super::bound_sum(expr)
135    }
136
137    /// Creates a bound nullable null-count statistic expression.
138    pub fn null_count(expr: BoundExpression) -> BoundExpression {
139        super::bound_null_count(expr)
140    }
141
142    /// Creates a bound nullable all-null statistic expression.
143    pub fn all_null(expr: BoundExpression) -> BoundExpression {
144        super::bound_all_null(expr)
145    }
146
147    /// Creates a bound nullable all-NaN statistic expression.
148    pub fn all_nan(expr: BoundExpression) -> BoundExpression {
149        super::bound_all_nan(expr)
150    }
151
152    /// Creates a bound nullable all-non-null statistic expression.
153    pub fn all_non_null(expr: BoundExpression) -> BoundExpression {
154        super::bound_all_non_null(expr)
155    }
156
157    /// Creates a bound nullable all-non-NaN statistic expression.
158    pub fn all_non_nan(expr: BoundExpression) -> BoundExpression {
159        super::bound_all_non_nan(expr)
160    }
161
162    /// Creates a bound nullable NaN-count statistic expression.
163    pub fn nan_count(expr: BoundExpression) -> BoundExpression {
164        super::bound_nan_count(expr)
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use std::sync::LazyLock;
171
172    use vortex_buffer::buffer;
173    use vortex_error::VortexExpect;
174    use vortex_error::VortexResult;
175    use vortex_session::VortexSession;
176
177    use super::all_nan;
178    use super::all_non_nan;
179    use super::all_non_null;
180    use super::all_null;
181    use super::bound as bound_stats;
182    use super::null_count;
183    use super::stat;
184    use super::sum;
185    use crate::Canonical;
186    use crate::IntoArray;
187    use crate::VortexSessionExecute;
188    use crate::array_session;
189    use crate::arrays::Chunked;
190    use crate::arrays::ChunkedArray;
191    use crate::arrays::ConstantArray;
192    use crate::arrays::PrimitiveArray;
193    use crate::arrays::chunked::ChunkedArrayExt;
194    use crate::assert_arrays_eq;
195    use crate::dtype::DType;
196    use crate::dtype::Nullability;
197    use crate::dtype::PType;
198    use crate::expr::bound as bound_expr;
199    use crate::expr::root;
200    use crate::expr::stats::Precision;
201    use crate::expr::stats::Stat;
202    use crate::scalar::Scalar;
203    use crate::scalar::ScalarValue;
204    use crate::validity::Validity;
205
206    static SESSION: LazyLock<VortexSession> = LazyLock::new(array_session);
207
208    #[test]
209    fn bound_stats_constructor_preserves_child_and_dtype() -> VortexResult<()> {
210        let input_dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
211        let root = bound_expr::root(input_dtype.clone());
212        let bound = bound_stats::sum(root.clone());
213
214        assert_eq!(bound.children(), &[root]);
215        assert_eq!(
216            bound.dtype(),
217            &DType::Primitive(PType::I64, Nullability::Nullable)
218        );
219        assert_eq!(bound, sum(crate::expr::root()).bind(&input_dtype)?);
220        Ok(())
221    }
222
223    #[test]
224    fn stat_expr_reads_cached_sum() -> VortexResult<()> {
225        let array = buffer![1i32, 2, 3].into_array();
226        let sum_scalar = Scalar::primitive(6i64, Nullability::Nullable);
227        array.statistics().set(
228            Stat::Sum,
229            Precision::exact(sum_scalar.into_value().vortex_expect("non-null sum")),
230        );
231
232        let result = array.apply(&sum(root()))?;
233
234        let expected =
235            ConstantArray::new(Scalar::primitive(6i64, Nullability::Nullable), 3).into_array();
236        assert_arrays_eq!(result, expected, &mut SESSION.create_execution_ctx());
237
238        Ok(())
239    }
240
241    #[test]
242    fn stat_expr_returns_null_when_sum_is_missing() -> VortexResult<()> {
243        let array = buffer![1i32, 2, 3].into_array();
244
245        let result = array.apply(&sum(root()))?;
246
247        let expected = ConstantArray::new(
248            Scalar::null(DType::Primitive(PType::I64, Nullability::Nullable)),
249            3,
250        )
251        .into_array();
252        assert_arrays_eq!(result, expected, &mut SESSION.create_execution_ctx());
253
254        Ok(())
255    }
256
257    #[test]
258    fn stat_expr_reads_cached_sum_per_chunk() -> VortexResult<()> {
259        let chunk0 = buffer![1i32, 2].into_array();
260        let sum_scalar = Scalar::primitive(3i64, Nullability::Nullable);
261        chunk0.statistics().set(
262            Stat::Sum,
263            Precision::exact(sum_scalar.into_value().vortex_expect("non-null sum")),
264        );
265        let chunk1 = buffer![4i32, 5, 6].into_array();
266        let chunked = ChunkedArray::try_new(
267            vec![chunk0, chunk1],
268            DType::Primitive(PType::I32, Nullability::NonNullable),
269        )?
270        .into_array();
271
272        let result = chunked.apply(&sum(root()))?;
273
274        let chunked_result = result
275            .as_opt::<Chunked>()
276            .vortex_expect("stat expression should preserve chunked alignment");
277        assert_eq!(chunked_result.nchunks(), 2);
278
279        let expected = PrimitiveArray::new(
280            buffer![3i64, 3, 0, 0, 0],
281            Validity::from_iter([true, true, false, false, false]),
282        )
283        .into_array();
284        assert_arrays_eq!(result, expected, &mut SESSION.create_execution_ctx());
285
286        Ok(())
287    }
288
289    #[test]
290    fn stat_expr_reads_cached_null_count() -> VortexResult<()> {
291        let array =
292            PrimitiveArray::from_option_iter([Some(1i32), None, Some(3), None]).into_array();
293        let null_count_scalar = Scalar::primitive(2u64, Nullability::NonNullable);
294        array.statistics().set(
295            Stat::NullCount,
296            Precision::exact(
297                null_count_scalar
298                    .into_value()
299                    .vortex_expect("non-null null_count"),
300            ),
301        );
302
303        let result = array.apply(&null_count(root()))?;
304
305        let expected =
306            ConstantArray::new(Scalar::primitive(2u64, Nullability::Nullable), 4).into_array();
307        assert_arrays_eq!(result, expected, &mut SESSION.create_execution_ctx());
308
309        Ok(())
310    }
311
312    #[test]
313    fn stat_expr_reads_cached_all_null_from_null_count() -> VortexResult<()> {
314        let array = PrimitiveArray::from_option_iter::<i32, _>([None, None, None]).into_array();
315        array
316            .statistics()
317            .set(Stat::NullCount, Precision::exact(ScalarValue::from(3u64)));
318
319        let result = array.apply(&all_null(root()))?;
320
321        let expected =
322            ConstantArray::new(Scalar::bool(true, Nullability::Nullable), 3).into_array();
323        assert_arrays_eq!(result, expected, &mut SESSION.create_execution_ctx());
324
325        Ok(())
326    }
327
328    #[test]
329    fn stat_expr_reads_cached_all_null_false_from_inexact_low_null_count() -> VortexResult<()> {
330        let array = PrimitiveArray::from_option_iter::<i32, _>([None, Some(2), None]).into_array();
331        array
332            .statistics()
333            .set(Stat::NullCount, Precision::inexact(ScalarValue::from(2u64)));
334
335        let result = array.apply(&all_null(root()))?;
336
337        let expected =
338            ConstantArray::new(Scalar::bool(false, Nullability::Nullable), 3).into_array();
339        assert_arrays_eq!(result, expected, &mut SESSION.create_execution_ctx());
340
341        Ok(())
342    }
343
344    #[test]
345    fn stat_expr_returns_null_for_inexact_full_null_count_as_all_null() -> VortexResult<()> {
346        let array = PrimitiveArray::from_option_iter::<i32, _>([None, Some(2), None]).into_array();
347        array
348            .statistics()
349            .set(Stat::NullCount, Precision::inexact(ScalarValue::from(3u64)));
350
351        let result = array.apply(&all_null(root()))?;
352
353        let expected =
354            ConstantArray::new(Scalar::null(DType::Bool(Nullability::Nullable)), 3).into_array();
355        assert_arrays_eq!(result, expected, &mut SESSION.create_execution_ctx());
356
357        Ok(())
358    }
359
360    #[test]
361    fn stat_expr_reads_cached_all_non_null_from_null_count() -> VortexResult<()> {
362        let array = buffer![1i32, 2, 3].into_array();
363        array
364            .statistics()
365            .set(Stat::NullCount, Precision::exact(ScalarValue::from(0u64)));
366
367        let result = array.apply(&all_non_null(root()))?;
368
369        let expected =
370            ConstantArray::new(Scalar::bool(true, Nullability::Nullable), 3).into_array();
371        assert_arrays_eq!(result, expected, &mut SESSION.create_execution_ctx());
372
373        Ok(())
374    }
375
376    #[test]
377    fn stat_expr_reads_cached_all_non_null_true_from_inexact_zero_null_count() -> VortexResult<()> {
378        let array = buffer![1i32, 2, 3].into_array();
379        array
380            .statistics()
381            .set(Stat::NullCount, Precision::inexact(ScalarValue::from(0u64)));
382
383        let result = array.apply(&all_non_null(root()))?;
384
385        let expected =
386            ConstantArray::new(Scalar::bool(true, Nullability::Nullable), 3).into_array();
387        assert_arrays_eq!(result, expected, &mut SESSION.create_execution_ctx());
388
389        Ok(())
390    }
391
392    #[test]
393    fn stat_expr_returns_null_for_inexact_nonzero_null_count_as_all_non_null() -> VortexResult<()> {
394        let array =
395            PrimitiveArray::from_option_iter([Some(1i32), None, Some(3), None]).into_array();
396        array
397            .statistics()
398            .set(Stat::NullCount, Precision::inexact(ScalarValue::from(2u64)));
399
400        let result = array.apply(&all_non_null(root()))?;
401
402        let expected =
403            ConstantArray::new(Scalar::null(DType::Bool(Nullability::Nullable)), 4).into_array();
404        assert_arrays_eq!(result, expected, &mut SESSION.create_execution_ctx());
405
406        Ok(())
407    }
408
409    #[test]
410    fn stat_expr_rejects_all_nan_for_non_float() -> VortexResult<()> {
411        let array = PrimitiveArray::empty::<i32>(Nullability::NonNullable).into_array();
412        let mut ctx = SESSION.create_execution_ctx();
413
414        let result = array
415            .apply(&all_nan(root()))
416            .and_then(|array| array.execute::<Canonical>(&mut ctx));
417
418        assert!(result.is_err());
419        Ok(())
420    }
421
422    #[test]
423    fn stat_expr_reads_cached_all_nan_from_nan_count() -> VortexResult<()> {
424        let array =
425            PrimitiveArray::from_option_iter([Some(f32::NAN), Some(f32::NAN), Some(f32::NAN)])
426                .into_array();
427        array
428            .statistics()
429            .set(Stat::NaNCount, Precision::exact(ScalarValue::from(3u64)));
430
431        let result = array.apply(&all_nan(root()))?;
432
433        let expected =
434            ConstantArray::new(Scalar::bool(true, Nullability::Nullable), 3).into_array();
435        assert_arrays_eq!(result, expected, &mut SESSION.create_execution_ctx());
436
437        Ok(())
438    }
439
440    #[test]
441    fn stat_expr_reads_cached_all_nan_false_from_inexact_low_nan_count() -> VortexResult<()> {
442        let array =
443            PrimitiveArray::from_option_iter([Some(f32::NAN), Some(1.0f32), Some(f32::NAN)])
444                .into_array();
445        array
446            .statistics()
447            .set(Stat::NaNCount, Precision::inexact(ScalarValue::from(2u64)));
448
449        let result = array.apply(&all_nan(root()))?;
450
451        let expected =
452            ConstantArray::new(Scalar::bool(false, Nullability::Nullable), 3).into_array();
453        assert_arrays_eq!(result, expected, &mut SESSION.create_execution_ctx());
454
455        Ok(())
456    }
457
458    #[test]
459    fn stat_expr_returns_null_for_inexact_full_nan_count_as_all_nan() -> VortexResult<()> {
460        let array =
461            PrimitiveArray::from_option_iter([Some(f32::NAN), Some(1.0f32), Some(f32::NAN)])
462                .into_array();
463        array
464            .statistics()
465            .set(Stat::NaNCount, Precision::inexact(ScalarValue::from(3u64)));
466
467        let result = array.apply(&all_nan(root()))?;
468
469        let expected =
470            ConstantArray::new(Scalar::null(DType::Bool(Nullability::Nullable)), 3).into_array();
471        assert_arrays_eq!(result, expected, &mut SESSION.create_execution_ctx());
472
473        Ok(())
474    }
475
476    #[test]
477    fn stat_expr_reads_cached_all_non_nan_true_from_inexact_zero_nan_count() -> VortexResult<()> {
478        let array = buffer![1.0f32, 2.0, 3.0].into_array();
479        array
480            .statistics()
481            .set(Stat::NaNCount, Precision::inexact(ScalarValue::from(0u64)));
482
483        let result = array.apply(&all_non_nan(root()))?;
484
485        let expected =
486            ConstantArray::new(Scalar::bool(true, Nullability::Nullable), 3).into_array();
487        assert_arrays_eq!(result, expected, &mut SESSION.create_execution_ctx());
488
489        Ok(())
490    }
491
492    #[test]
493    fn stat_expr_returns_null_for_inexact_nonzero_nan_count_as_all_non_nan() -> VortexResult<()> {
494        let array = PrimitiveArray::from_option_iter([Some(1.0f32), Some(f32::NAN), Some(3.0)])
495            .into_array();
496        array
497            .statistics()
498            .set(Stat::NaNCount, Precision::inexact(ScalarValue::from(1u64)));
499
500        let result = array.apply(&all_non_nan(root()))?;
501
502        let expected =
503            ConstantArray::new(Scalar::null(DType::Bool(Nullability::Nullable)), 3).into_array();
504        assert_arrays_eq!(result, expected, &mut SESSION.create_execution_ctx());
505
506        Ok(())
507    }
508
509    #[test]
510    fn stat_expr_reads_cached_min_and_max() -> VortexResult<()> {
511        let array = buffer![3i32, 1, 2].into_array();
512        array
513            .statistics()
514            .set(Stat::Min, Precision::exact(ScalarValue::from(1i32)));
515        array
516            .statistics()
517            .set(Stat::Max, Precision::exact(ScalarValue::from(3i32)));
518
519        let min_result = array.clone().apply(&stat(
520            root(),
521            Stat::Min
522                .aggregate_fn()
523                .vortex_expect("min should have an aggregate function"),
524        ))?;
525        let expected_min =
526            ConstantArray::new(Scalar::primitive(1i32, Nullability::Nullable), 3).into_array();
527        assert_arrays_eq!(
528            min_result,
529            expected_min,
530            &mut SESSION.create_execution_ctx()
531        );
532
533        let max_result = array.apply(&stat(
534            root(),
535            Stat::Max
536                .aggregate_fn()
537                .vortex_expect("max should have an aggregate function"),
538        ))?;
539        let expected_max =
540            ConstantArray::new(Scalar::primitive(3i32, Nullability::Nullable), 3).into_array();
541        assert_arrays_eq!(
542            max_result,
543            expected_max,
544            &mut SESSION.create_execution_ctx()
545        );
546
547        Ok(())
548    }
549}