1use 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
26pub 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
40pub fn min_max(expr: Expression) -> Expression {
42 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
50pub fn sum(expr: Expression) -> Expression {
52 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
60pub 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
69pub 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
78pub 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
87pub 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
96pub 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
105pub 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
114pub mod bound {
119 use crate::aggregate_fn::AggregateFnRef;
120 use crate::expr::BoundExpression;
121
122 pub fn stat(expr: BoundExpression, aggregate_fn: AggregateFnRef) -> BoundExpression {
124 super::bound_stat(expr, aggregate_fn)
125 }
126
127 pub fn min_max(expr: BoundExpression) -> BoundExpression {
129 super::bound_min_max(expr)
130 }
131
132 pub fn sum(expr: BoundExpression) -> BoundExpression {
134 super::bound_sum(expr)
135 }
136
137 pub fn null_count(expr: BoundExpression) -> BoundExpression {
139 super::bound_null_count(expr)
140 }
141
142 pub fn all_null(expr: BoundExpression) -> BoundExpression {
144 super::bound_all_null(expr)
145 }
146
147 pub fn all_nan(expr: BoundExpression) -> BoundExpression {
149 super::bound_all_nan(expr)
150 }
151
152 pub fn all_non_null(expr: BoundExpression) -> BoundExpression {
154 super::bound_all_non_null(expr)
155 }
156
157 pub fn all_non_nan(expr: BoundExpression) -> BoundExpression {
159 super::bound_all_non_nan(expr)
160 }
161
162 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}